Start MySQL implementation: config template, PDO layer and migrations

This commit is contained in:
Alex Assistant 2026-02-20 14:07:50 +03:00
parent e22476da57
commit 883ff30877
6 changed files with 182 additions and 0 deletions

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ Thumbs.db
# Local secrets # Local secrets
deploy-config.php deploy-config.php
config.php
# Logs/temp # Logs/temp
*.log *.log

View File

@ -55,6 +55,27 @@ photo-gallery/
php -S 127.0.0.1:8080 php -S 127.0.0.1:8080
``` ```
## MySQL конфиг и миграции (этап перехода на БД)
1. Создай локальный конфиг из шаблона:
```bash
cp config.php.example config.php
```
2. Заполни доступы к MySQL в `config.php`.
3. Прогони миграции:
```bash
php scripts/migrate.php
```
Файлы:
- `lib/db.php` — подключение PDO
- `migrations/*.sql` — схема БД
- `scripts/migrate.php` — runner миграций
Открыть в браузере: Открыть в браузере:
- `http://127.0.0.1:8080` - `http://127.0.0.1:8080`

11
config.php.example Normal file
View File

@ -0,0 +1,11 @@
<?php
return [
'db' => [
'host' => '127.0.0.1',
'port' => 3306,
'name' => 'photo_gallery',
'user' => 'gallery_user',
'pass' => 'change_me',
'charset' => 'utf8mb4',
],
];

47
lib/db.php Normal file
View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
function appConfig(): array
{
static $cfg = null;
if ($cfg !== null) {
return $cfg;
}
$path = __DIR__ . '/../config.php';
if (!is_file($path)) {
throw new RuntimeException('config.php not found. Copy config.php.example');
}
$cfg = require $path;
if (!is_array($cfg)) {
throw new RuntimeException('Invalid config.php format');
}
return $cfg;
}
function db(): PDO
{
static $pdo = null;
if ($pdo instanceof PDO) {
return $pdo;
}
$db = appConfig()['db'] ?? [];
$host = $db['host'] ?? '127.0.0.1';
$port = (int)($db['port'] ?? 3306);
$name = $db['name'] ?? '';
$user = $db['user'] ?? '';
$pass = $db['pass'] ?? '';
$charset = $db['charset'] ?? 'utf8mb4';
$dsn = "mysql:host={$host};port={$port};dbname={$name};charset={$charset}";
$pdo = new PDO($dsn, (string)$user, (string)$pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
return $pdo;
}

54
migrations/001_init.sql Normal file
View File

@ -0,0 +1,54 @@
CREATE TABLE IF NOT EXISTS sections (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
sort_order INT NOT NULL DEFAULT 1000,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_sections_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS photos (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
section_id BIGINT UNSIGNED NOT NULL,
code_name VARCHAR(191) NOT NULL,
description TEXT NULL,
sort_order INT NOT NULL DEFAULT 1000,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_photos_section FOREIGN KEY (section_id) REFERENCES sections(id) ON DELETE CASCADE,
UNIQUE KEY uq_photos_code_name (code_name),
KEY idx_photos_section_sort (section_id, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS photo_files (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
photo_id BIGINT UNSIGNED NOT NULL,
kind ENUM('before','after') NOT NULL,
file_path VARCHAR(500) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
size_bytes INT UNSIGNED NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_photo_files_photo FOREIGN KEY (photo_id) REFERENCES photos(id) ON DELETE CASCADE,
UNIQUE KEY uq_photo_files_kind (photo_id, kind)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS comment_users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
display_name VARCHAR(191) NOT NULL,
token_hash CHAR(64) NOT NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_comment_users_token_hash (token_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS photo_comments (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
photo_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NULL,
comment_text TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_photo_comments_photo FOREIGN KEY (photo_id) REFERENCES photos(id) ON DELETE CASCADE,
CONSTRAINT fk_photo_comments_user FOREIGN KEY (user_id) REFERENCES comment_users(id) ON DELETE SET NULL,
KEY idx_photo_comments_photo_created (photo_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

48
scripts/migrate.php Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
require __DIR__ . '/../lib/db.php';
try {
$pdo = db();
} catch (Throwable $e) {
fwrite(STDERR, "DB connection failed: " . $e->getMessage() . PHP_EOL);
exit(1);
}
$pdo->exec('CREATE TABLE IF NOT EXISTS migrations (name VARCHAR(191) PRIMARY KEY, applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci');
$files = glob(__DIR__ . '/../migrations/*.sql') ?: [];
sort($files, SORT_NATURAL);
$check = $pdo->prepare('SELECT 1 FROM migrations WHERE name = :name');
$mark = $pdo->prepare('INSERT INTO migrations(name) VALUES (:name)');
foreach ($files as $file) {
$name = basename($file);
$check->execute(['name' => $name]);
if ($check->fetchColumn()) {
echo "skip {$name}" . PHP_EOL;
continue;
}
echo "apply {$name}" . PHP_EOL;
$sql = file_get_contents($file);
if ($sql === false) {
throw new RuntimeException("Cannot read {$file}");
}
$pdo->beginTransaction();
try {
$pdo->exec($sql);
$mark->execute(['name' => $name]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
}
echo "done" . PHP_EOL;