Commit a45e66f7 authored by Administrator's avatar Administrator

Update 5 files via Son of Anton

parent 6d82d38f
<?php
declare(strict_types=1);
// Make sure your Database constructor stores dbName.
// Find and verify/replace the constructor:
namespace App\Core;
private string $dbName;
private ?\PDO $pdo = null;
private array $config;
use PDO;
use PDOStatement;
use PDOException;
final class Database
{
private ?PDO $pdo = null;
private string $host;
private int $port;
private string $name;
private string $user;
private string $pass;
private string $charset;
private array $queryLog = [];
private bool $queryLogEnabled = false;
private array $beforeInsertCallbacks = [];
private array $afterInsertCallbacks = [];
private array $beforeUpdateCallbacks = [];
private array $afterUpdateCallbacks = [];
private array $afterDeleteCallbacks = [];
public function __construct(string $host, int $port, string $name, string $user, string $pass, string $charset = 'utf8mb4')
public function __construct(array $config)
{
$this->host = $host;
$this->port = $port;
$this->name = $name;
$this->user = $user;
$this->pass = $pass;
$this->charset = $charset;
$this->config = $config;
$this->dbName = $config['name'] ?? '';
}
private function connect(): PDO
private function connect(): \PDO
{
if ($this->pdo === null) {
$dsn = "mysql:host={$this->host};port={$this->port};dbname={$this->name};charset={$this->charset}";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES '{$this->charset}' COLLATE '{$this->charset}_unicode_ci', sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'",
];
$this->pdo = new PDO($dsn, $this->user, $this->pass, $options);
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
$this->config['host'] ?? '127.0.0.1',
$this->config['port'] ?? '3306',
$this->config['name'] ?? '',
$this->config['charset'] ?? 'utf8mb4'
);
$this->pdo = new \PDO($dsn, $this->config['user'] ?? 'root', $this->config['pass'] ?? '', [
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
\PDO::ATTR_EMULATE_PREPARES => false,
\PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'utf8mb4' COLLATE 'utf8mb4_unicode_ci'",
]);
}
return $this->pdo;
}
public function pdo(): PDO
{
return $this->connect();
}
public function query(string $sql, array $params = []): PDOStatement
{
$start = microtime(true);
$stmt = $this->connect()->prepare($sql);
$stmt->execute($params);
$elapsed = (microtime(true) - $start) * 1000;
if ($this->queryLogEnabled) {
$this->queryLog[] = [
'sql' => $sql,
'params' => $params,
'time' => round($elapsed, 2),
];
}
return $stmt;
}
public function select(string $sql, array $params = []): array
{
return $this->query($sql, $params)->fetchAll();
}
public function selectOne(string $sql, array $params = []): ?array
{
$result = $this->query($sql, $params)->fetch();
return $result !== false ? $result : null;
}
public function insert(string $table, array $data): int
{
foreach ($this->beforeInsertCallbacks as $cb) {
$cb($table, $data, null);
}
$columns = implode(', ', array_map(fn($c) => "`{$c}`", array_keys($data)));
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$sql = "INSERT INTO `{$table}` ({$columns}) VALUES ({$placeholders})";
$this->query($sql, array_values($data));
$id = (int) $this->connect()->lastInsertId();
foreach ($this->afterInsertCallbacks as $cb) {
$cb($table, $data, $id);
}
return $id;
}
public function update(string $table, array $data, string $where, array $whereParams = []): int
{
foreach ($this->beforeUpdateCallbacks as $cb) {
$cb($table, $data, null);
}
$set = implode(', ', array_map(fn($c) => "`{$c}` = ?", array_keys($data)));
$sql = "UPDATE `{$table}` SET {$set} WHERE {$where}";
$params = array_merge(array_values($data), $whereParams);
$stmt = $this->query($sql, $params);
$count = $stmt->rowCount();
foreach ($this->afterUpdateCallbacks as $cb) {
$cb($table, $data, null);
}
return $count;
}
public function delete(string $table, string $where, array $whereParams = []): int
{
$sql = "DELETE FROM `{$table}` WHERE {$where}";
$stmt = $this->query($sql, $whereParams);
$count = $stmt->rowCount();
foreach ($this->afterDeleteCallbacks as $cb) {
$cb($table, [], null);
}
return $count;
}
public function beginTransaction(): bool
{
return $this->connect()->beginTransaction();
}
public function commit(): bool
{
return $this->connect()->commit();
}
public function rollBack(): bool
{
return $this->connect()->rollBack();
}
public function inTransaction(): bool
{
return $this->connect()->inTransaction();
}
public function lastInsertId(): int
{
return (int) $this->connect()->lastInsertId();
}
public function raw(string $sql): PDOStatement
{
return $this->connect()->query($sql);
}
public function enableQueryLog(): void
{
$this->queryLogEnabled = true;
}
public function disableQueryLog(): void
{
$this->queryLogEnabled = false;
}
public function getQueryLog(): array
{
return $this->queryLog;
}
public function onBeforeInsert(callable $fn): void
{
$this->beforeInsertCallbacks[] = $fn;
}
public function onAfterInsert(callable $fn): void
{
$this->afterInsertCallbacks[] = $fn;
}
public function onBeforeUpdate(callable $fn): void
{
$this->beforeUpdateCallbacks[] = $fn;
}
public function onAfterUpdate(callable $fn): void
{
$this->afterUpdateCallbacks[] = $fn;
}
public function onAfterDelete(callable $fn): void
{
$this->afterDeleteCallbacks[] = $fn;
}
public function tableExists(string $table): bool
{
$stmt = $this->query("SHOW TABLES LIKE ?", [$table]);
return $stmt->rowCount() > 0;
}
}
\ No newline at end of file
return $this->pdo;
}
\ No newline at end of file
......@@ -15,29 +15,27 @@ final class SeederRunner
$this->db = $db;
}
public function seedAll(): array
public function run(): array
{
$this->ensureSeedsTable();
$ran = $this->getRanSeeds();
$executed = $this->getExecutedSeeds();
$files = $this->getSeedFiles();
$results = [];
foreach ($files as $file) {
$name = basename($file, '.php');
if (in_array($name, $ran)) {
if (in_array($name, $executed)) {
continue;
}
require_once $file;
echo " Running: {$name}...\n";
$fnName = str_replace(['-', '.'], '_', $name);
if (function_exists($fnName)) {
($fnName)($this->db);
} else {
$seed = require $file;
if (is_callable($seed)) {
$seed($this->db);
}
$seed = require $file;
if (is_callable($seed)) {
$seed($this->db);
} elseif (is_object($seed) && method_exists($seed, 'run')) {
$seed->run($this->db);
}
$this->db->insert('seeds', [
......@@ -51,36 +49,50 @@ final class SeederRunner
return $results;
}
public function runOne(string $name): bool
public function runSingle(string $seedName): void
{
$this->ensureSeedsTable();
$file = $this->getSeedDir() . '/' . $name . '.php';
$files = $this->getSeedFiles();
$targetFile = null;
foreach ($files as $file) {
if (str_contains(basename($file, '.php'), $seedName)) {
$targetFile = $file;
break;
}
}
if (!file_exists($file)) {
return false;
if (!$targetFile) {
throw new \RuntimeException("Seed not found: {$seedName}");
}
$seed = require $file;
$seed = require $targetFile;
if (is_callable($seed)) {
$seed($this->db);
} elseif (is_object($seed) && method_exists($seed, 'run')) {
$seed->run($this->db);
}
$ran = $this->getRanSeeds();
if (!in_array($name, $ran)) {
// Record it (ignore duplicate)
$name = basename($targetFile, '.php');
$existing = $this->db->selectOne("SELECT id FROM seeds WHERE seed = ?", [$name]);
if (!$existing) {
$this->db->insert('seeds', [
'seed' => $name,
'executed_at' => date('Y-m-d H:i:s'),
]);
}
return true;
}
private function ensureSeedsTable(): void
{
if (!$this->db->tableExists('seeds')) {
// Seeds table might not exist if migrations haven't created it
// The schema.sql has it, but individual migrations might not
// Create it directly
$this->db->raw("
CREATE TABLE `seeds` (
CREATE TABLE IF NOT EXISTS `seeds` (
`id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`seed` VARCHAR(255) NOT NULL,
`executed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
......@@ -90,7 +102,7 @@ final class SeederRunner
}
}
private function getRanSeeds(): array
private function getExecutedSeeds(): array
{
$rows = $this->db->select("SELECT seed FROM seeds ORDER BY id");
return array_column($rows, 'seed');
......
<?php
declare(strict_types=1);
// Load environment
/**
* CLI Entry Point — Migrations, Seeds, and System Commands.
*
* Usage:
* php cli.php migrate Run all pending migrations
* php cli.php migrate:rollback Rollback last batch
* php cli.php migrate:status Show migration status
* php cli.php seed Run all pending seeds
* php cli.php seed:run <Name> Run a specific seed
* php cli.php cron Run background jobs
*/
// Prevent web access
if (PHP_SAPI !== 'cli') {
die('CLI only.');
}
require_once __DIR__ . '/app/Core/Autoloader.php';
\App\Core\Autoloader::register();
// Load .env
$envFile = __DIR__ . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || str_starts_with($line, '#')) continue;
if ($line === '' || $line[0] === '#') continue;
if (str_contains($line, '=')) {
[$key, $val] = explode('=', $line, 2);
$_ENV[trim($key)] = trim($val);
$_SERVER[trim($key)] = trim($val);
[$key, $value] = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
if (!isset($_ENV[$key])) {
$_ENV[$key] = $value;
putenv("{$key}={$value}");
}
}
}
}
require_once __DIR__ . '/app/Core/Autoloader.php';
\App\Core\Autoloader::register();
// Minimal bootstrap — just database, no session/routing
$config = [];
$configDir = __DIR__ . '/config';
if (is_dir($configDir)) {
foreach (glob($configDir . '/*.php') as $configFile) {
$key = basename($configFile, '.php');
$config[$key] = require $configFile;
}
}
$config = new \App\Core\Config();
$config->loadAll();
// Build database config
$dbConfig = [
'host' => $_ENV['DB_HOST'] ?? $config['database']['host'] ?? '127.0.0.1',
'port' => $_ENV['DB_PORT'] ?? $config['database']['port'] ?? '3306',
'name' => $_ENV['DB_NAME'] ?? $config['database']['name'] ?? 'the_club_erp',
'user' => $_ENV['DB_USER'] ?? $config['database']['user'] ?? 'root',
'pass' => $_ENV['DB_PASS'] ?? $config['database']['pass'] ?? '',
'charset' => $config['database']['charset'] ?? 'utf8mb4',
'collation' => $config['database']['collation'] ?? 'utf8mb4_unicode_ci',
];
$db = new \App\Core\Database(
$config->get('database.host', '127.0.0.1'),
(int) $config->get('database.port', '3306'),
$config->get('database.name', 'the_club_erp'),
$config->get('database.user', 'root'),
$config->get('database.pass', ''),
$config->get('database.charset', 'utf8mb4')
);
$db = new \App\Core\Database($dbConfig);
$command = $argv[1] ?? 'help';
$arg2 = $argv[2] ?? null;
echo "╔══════════════════════════════════════╗\n";
echo "║ THE CLUB ERP — CLI Tool ║\n";
echo "╚══════════════════════════════════════╝\n\n";
echo "\n";
switch ($command) {
case 'migrate':
$runner = new \App\Core\Migration\MigrationRunner($db);
$results = $runner->migrate();
if (empty($results)) {
echo " ✓ Nothing to migrate. All up to date.\n";
echo "✅ Nothing to migrate. All migrations are up to date.\n";
} else {
foreach ($results as $m) {
echo " ✓ Migrated: {$m}\n";
echo "✅ Ran " . count($results) . " migration(s):\n";
foreach ($results as $name) {
echo " ✔ {$name}\n";
}
}
break;
......@@ -54,10 +86,11 @@ switch ($command) {
$runner = new \App\Core\Migration\MigrationRunner($db);
$results = $runner->rollback();
if (empty($results)) {
echo " Nothing to rollback.\n";
echo "⚠️ Nothing to rollback.\n";
} else {
foreach ($results as $m) {
echo " ↩ Rolled back: {$m}\n";
echo "✅ Rolled back " . count($results) . " migration(s):\n";
foreach ($results as $name) {
echo " ↩ {$name}\n";
}
}
break;
......@@ -68,56 +101,57 @@ switch ($command) {
echo str_pad('Migration', 60) . "Status\n";
echo str_repeat('─', 70) . "\n";
foreach ($status as $s) {
$icon = $s['status'] === 'Ran' ? '' : '○';
echo " {$icon} " . str_pad($s['migration'], 58) . $s['status'] . "\n";
$icon = $s['status'] === 'Ran' ? '' : '○';
echo "{$icon} " . str_pad($s['migration'], 58) . $s['status'] . "\n";
}
break;
case 'seed':
echo "🌱 Running seeds...\n";
$runner = new \App\Core\Seeder\SeederRunner($db);
$results = $runner->seedAll();
$results = $runner->run();
if (empty($results)) {
echo " ✓ Nothing to seed. All seeds already ran.\n";
echo "✅ Nothing to seed. All seeds already executed.\n";
} else {
foreach ($results as $s) {
echo " ✓ Seeded: {$s}\n";
echo "✅ Ran " . count($results) . " seed(s):\n";
foreach ($results as $name) {
echo " 🌱 {$name}\n";
}
}
break;
case 'seed:run':
$name = $argv[2] ?? null;
if (!$name) {
echo " ✗ Please provide seed name.\n";
if (!$arg2) {
echo "❌ Please specify seed name: php cli.php seed:run SeedName\n";
exit(1);
}
$runner = new \App\Core\Seeder\SeederRunner($db);
if ($runner->runOne($name)) {
echo " ✓ Seeded: {$name}\n";
} else {
echo " ✗ Seed not found: {$name}\n";
}
$runner->runSingle($arg2);
echo "✅ Seed executed: {$arg2}\n";
break;
case 'cron':
echo " Running background jobs...\n";
echo "⏰ Running cron jobs...\n";
$cronRunner = __DIR__ . '/cron/runner.php';
if (file_exists($cronRunner)) {
require $cronRunner;
} else {
echo " ✗ Cron runner not found (Phase 15).\n";
echo "⚠️ No cron runner found.\n";
}
break;
case 'help':
default:
echo "Available commands:\n";
echo " migrate Run all pending migrations\n";
echo " migrate:rollback Rollback last migration batch\n";
echo " migrate:status Show migration status\n";
echo " seed Run all pending seeds\n";
echo " seed:run <name> Run a specific seed\n";
echo " cron Run background jobs\n";
echo "THE CLUB ERP — CLI Commands\n";
echo "───────────────────────────────────\n";
echo " php cli.php migrate Run pending migrations\n";
echo " php cli.php migrate:rollback Rollback last batch\n";
echo " php cli.php migrate:status Show migration status\n";
echo " php cli.php seed Run all pending seeds\n";
echo " php cli.php seed:run <Name> Run specific seed\n";
echo " php cli.php cron Run background jobs\n";
echo " php cli.php help Show this help\n";
break;
}
echo "\nDone.\n";
\ No newline at end of file
echo "\n";
\ No newline at end of file
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment