feat(core): Konfiguration, PDO-Datenbankschicht und Migrationen
SQLite als Standard (keine Einrichtung noetig, WAL aktiviert), optional MySQL/MariaDB. Beide Treiber nutzen dieselben vorbereiteten Statements. Die Migrationen laufen automatisch beim ersten Request; jede Migration liefert das SQL je Treiber. Damit entfaellt ein manueller Schritt beim Deployment ueber FTP. data/ enthaelt eine eigene .htaccess als zweite Absicherung, falls das Verzeichnis beim Hoster doch einmal im Webroot landet.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Initiales Schema.
|
||||
* Zeitstempel werden bewusst als UTC-Strings (Y-m-d H:i:s) gespeichert,
|
||||
* damit SQLite und MySQL identisch behandelt werden koennen.
|
||||
*/
|
||||
return [
|
||||
'sqlite' => [
|
||||
'CREATE TABLE IF NOT EXISTS links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
target_url TEXT NOT NULL,
|
||||
title TEXT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
click_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_clicked_at TEXT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_links_created_at ON links (created_at)',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS visits (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
link_id INTEGER NOT NULL,
|
||||
visited_at TEXT NOT NULL,
|
||||
referrer_host TEXT NULL,
|
||||
referrer_url TEXT NULL,
|
||||
user_agent TEXT NULL,
|
||||
device_type TEXT NOT NULL DEFAULT "unknown",
|
||||
browser TEXT NOT NULL DEFAULT "unknown",
|
||||
platform TEXT NOT NULL DEFAULT "unknown",
|
||||
visitor_hash TEXT NULL,
|
||||
FOREIGN KEY (link_id) REFERENCES links (id) ON DELETE CASCADE
|
||||
)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_visits_link_time ON visits (link_id, visited_at)',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS qr_designs (
|
||||
link_id INTEGER PRIMARY KEY,
|
||||
foreground_color TEXT NOT NULL DEFAULT "#000000",
|
||||
background_color TEXT NOT NULL DEFAULT "#ffffff",
|
||||
size INTEGER NOT NULL DEFAULT 512,
|
||||
margin INTEGER NOT NULL DEFAULT 4,
|
||||
error_correction TEXT NOT NULL DEFAULT "M",
|
||||
logo_path TEXT NULL,
|
||||
logo_scale INTEGER NOT NULL DEFAULT 22,
|
||||
logo_punchout INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY (link_id) REFERENCES links (id) ON DELETE CASCADE
|
||||
)',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS settings (
|
||||
key_name TEXT PRIMARY KEY,
|
||||
value TEXT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS login_attempts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
identifier TEXT NOT NULL,
|
||||
attempted_at TEXT NOT NULL,
|
||||
successful INTEGER NOT NULL DEFAULT 0
|
||||
)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_login_attempts ON login_attempts (identifier, attempted_at)',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'CREATE TABLE IF NOT EXISTS links (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
slug VARCHAR(190) NOT NULL,
|
||||
target_url TEXT NOT NULL,
|
||||
title VARCHAR(255) NULL,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
click_count INT NOT NULL DEFAULT 0,
|
||||
last_clicked_at DATETIME NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
UNIQUE KEY uniq_links_slug (slug),
|
||||
KEY idx_links_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS visits (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
link_id INT NOT NULL,
|
||||
visited_at DATETIME NOT NULL,
|
||||
referrer_host VARCHAR(255) NULL,
|
||||
referrer_url TEXT NULL,
|
||||
user_agent TEXT NULL,
|
||||
device_type VARCHAR(20) NOT NULL DEFAULT "unknown",
|
||||
browser VARCHAR(40) NOT NULL DEFAULT "unknown",
|
||||
platform VARCHAR(40) NOT NULL DEFAULT "unknown",
|
||||
visitor_hash VARCHAR(64) NULL,
|
||||
KEY idx_visits_link_time (link_id, visited_at),
|
||||
CONSTRAINT fk_visits_link FOREIGN KEY (link_id) REFERENCES links (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS qr_designs (
|
||||
link_id INT PRIMARY KEY,
|
||||
foreground_color VARCHAR(7) NOT NULL DEFAULT "#000000",
|
||||
background_color VARCHAR(7) NOT NULL DEFAULT "#ffffff",
|
||||
size INT NOT NULL DEFAULT 512,
|
||||
margin INT NOT NULL DEFAULT 4,
|
||||
error_correction VARCHAR(1) NOT NULL DEFAULT "M",
|
||||
logo_path VARCHAR(255) NULL,
|
||||
logo_scale INT NOT NULL DEFAULT 22,
|
||||
logo_punchout TINYINT(1) NOT NULL DEFAULT 1,
|
||||
updated_at DATETIME NOT NULL,
|
||||
CONSTRAINT fk_qr_link FOREIGN KEY (link_id) REFERENCES links (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS settings (
|
||||
key_name VARCHAR(190) PRIMARY KEY,
|
||||
value TEXT NULL,
|
||||
updated_at DATETIME NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
||||
|
||||
'CREATE TABLE IF NOT EXISTS login_attempts (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
identifier VARCHAR(64) NOT NULL,
|
||||
attempted_at DATETIME NOT NULL,
|
||||
successful TINYINT(1) NOT NULL DEFAULT 0,
|
||||
KEY idx_login_attempts (identifier, attempted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Mehrere Kurzlink-Domains auf einer Installation.
|
||||
*
|
||||
* Ein Kurzlink gehoert ab hier zu genau einer Domain; Slugs muessen nur
|
||||
* noch INNERHALB einer Domain eindeutig sein. "qr.example.de/messe" und
|
||||
* "short.example.de/messe" sind damit zwei verschiedene Links.
|
||||
*
|
||||
* Bestandslinks bekommen die Kennung 0 ("noch nicht zugeordnet"). Beim
|
||||
* ersten Zugriff legt die Anwendung aus APP_BASE_URL die Standarddomain
|
||||
* an und traegt sie bei diesen Links nach (siehe DomainRepository).
|
||||
* Ein Sentinel-Wert statt NULL, weil SQLite NULL-Werte in einem
|
||||
* UNIQUE-Index als jeweils verschieden behandelt - die Eindeutigkeit
|
||||
* waere sonst nicht durchgesetzt.
|
||||
*/
|
||||
return [
|
||||
'sqlite' => [
|
||||
'CREATE TABLE IF NOT EXISTS domains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
host TEXT NOT NULL UNIQUE,
|
||||
base_url TEXT NOT NULL,
|
||||
label TEXT NULL,
|
||||
is_default INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL
|
||||
)',
|
||||
|
||||
// SQLite kann eine UNIQUE-Spalte nicht nachtraeglich aendern,
|
||||
// deshalb wird die Tabelle neu aufgebaut.
|
||||
'PRAGMA foreign_keys = OFF',
|
||||
|
||||
'CREATE TABLE links_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
domain_id INTEGER NOT NULL DEFAULT 0,
|
||||
slug TEXT NOT NULL,
|
||||
target_url TEXT NOT NULL,
|
||||
title TEXT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
click_count INTEGER NOT NULL DEFAULT 0,
|
||||
last_clicked_at TEXT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE (domain_id, slug)
|
||||
)',
|
||||
|
||||
'INSERT INTO links_new
|
||||
(id, domain_id, slug, target_url, title, is_active, click_count, last_clicked_at, created_at, updated_at)
|
||||
SELECT id, 0, slug, target_url, title, is_active, click_count, last_clicked_at, created_at, updated_at
|
||||
FROM links',
|
||||
|
||||
'DROP TABLE links',
|
||||
'ALTER TABLE links_new RENAME TO links',
|
||||
|
||||
'CREATE INDEX IF NOT EXISTS idx_links_created_at ON links (created_at)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_links_domain ON links (domain_id)',
|
||||
|
||||
'PRAGMA foreign_keys = ON',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'CREATE TABLE IF NOT EXISTS domains (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
host VARCHAR(190) NOT NULL,
|
||||
base_url VARCHAR(255) NOT NULL,
|
||||
label VARCHAR(255) NULL,
|
||||
is_default TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at DATETIME NOT NULL,
|
||||
UNIQUE KEY uniq_domains_host (host)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4',
|
||||
|
||||
'ALTER TABLE links ADD COLUMN domain_id INT NOT NULL DEFAULT 0',
|
||||
'ALTER TABLE links DROP INDEX uniq_links_slug',
|
||||
'ALTER TABLE links ADD UNIQUE KEY uniq_links_domain_slug (domain_id, slug)',
|
||||
'ALTER TABLE links ADD INDEX idx_links_domain (domain_id)',
|
||||
],
|
||||
];
|
||||
Reference in New Issue
Block a user