tables)) { $results = $this->connection->query("SHOW TABLES"); $rows = $results->fetchAll(PDO::FETCH_ASSOC); $this->tables = array_map(function($row) { return $row["Tables_in_{$this->database}"]; }, $rows); } return $this->tables; } public function getTableDefinition(string $table): string { if (!isset($this->definitions[$table])) { $results = $this->connection->query("SHOW CREATE TABLE {$table}"); $row = $results->fetch(PDO::FETCH_ASSOC); $this->definitions[$table] = $row['Create Table']; } return $this->definitions[$table]; } public function parseTableDefinition(string $table, int $offset = 2): string { $this->extractLines($table); $tableLine = "\$this->table('{$table}'"; if (count($this->primary) === 1) { $tableLine .= ", ['id' => '{$this->primary[0]}']"; } elseif (count($this->primary) > 1) { $primaryString = implode(', ', array_map(function($key) {return "'{$key}'";}, $this->primary)); $tableLine .= ", ['id' = false, 'primary_key' => [{$primaryString}]]"; } $tableLine .= ')'; $output = [$this->prefixLine($tableLine, $offset)]; foreach ($this->columns as $column) { $output []= $this->prefixLine($column, $offset + 1); } foreach ($this->constraints as $constraint) { $output []= $this->prefixLine($constraint, $offset + 1); } $output []= $this->prefixLine('->create();', $offset + 1); return implode(PHP_EOL, $output); } public function getTableIndex(string $table): int { return array_search($table, $this->getTables()); } protected array $tables; protected array $definitions; protected array $primary; protected array $columns; protected array $constraints; protected function extractLines(string $table): void { $this->primary = []; $this->columns = []; $this->constraints = []; $lines = explode(PHP_EOL, $this->definitions[$table]); array_shift($lines); foreach ($lines as $line) { if (str_starts_with(trim($line), ') ENGINE')) { break; } if (str_starts_with(trim($line), '`id`') or str_starts_with(trim($line), 'KEY')) { continue; } if (str_starts_with(trim($line), 'PRIMARY KEY')) { if (!str_contains($line, '`id`')) { $ini = strpos($line, '(') + 1; $end = strpos($line, ')', $ini); $this->primary []= substr($line, $ini, $end - $ini); } continue; } if (str_starts_with(trim($line), 'CONSTRAINT')) { $this->constraints []= (new ConstraintParser($line))->parse(); continue; } $this->columns []= (new ColumnParser($line))->parse(); } } protected function prefixLine(string $line, int $length, string $character = "\t"): string { if ($length === 0) { return $line; } return implode('', array_fill(0, $length, $character)) . $line; } } class DataRepository { public function __construct(protected Incoviba\Common\Define\Connection $connection) {} public int $size; public function getData(string $table): Generator { $query = $this->connection->getQueryBuilder() ->select() ->from($table); $results = $this->connection->query($query); $this->size = $results->rowCount(); while ($row = $results->fetch(PDO::FETCH_ASSOC)) { yield $row; } } } class ColumnParser { public function __construct(protected string $line) {} public function parse(): string { return "->addColumn('{$this->getName()}', '{$this->getType()}'{$this->getOptions()})"; } protected int $ini; protected int $end; protected array $parts; protected array $options; protected function getName(): string { $ini = $this->getIni(); $end = $this->getEnd(); return substr($this->line, $ini, $end - $ini); } protected function getType(): string { $parts = $this->getParts(); $type = array_shift($parts); if (str_contains($type, '(')) { list($type, $length) = explode('(', $type); $this->options []= "'length' => " . rtrim($length, ')'); } return match($type) { 'int' => 'integer', 'tinyint' => 'boolean', 'varchar' => 'string', default => $type }; } protected function getOptions(): string { $parts = $this->getParts(); array_shift($parts); $validOptions = [ 'default' => function($parts) { $i = array_search('default', array_map(function($part) {return strtolower($part);}, $parts)); return ['default', $parts[$i + 1]]; }, 'null' => function($parts) { $value = true; $i = array_search('null', array_map(function($part) {return strtolower($part);}, $parts)); if (key_exists($i - 1, $parts) and strtolower($parts[$i - 1]) === 'not') { $value = false; } return ['null', $value]; }, 'unsigned' => function($parts) { return ['signed', false]; }, 'auto_increment' => function($parts) { return ['auto_increment', true]; } ]; foreach ($validOptions as $validOption => $callable) { if (str_contains(strtolower($this->line), $validOption)) { list($option, $value) = $callable($parts); if (strtolower($value) === 'null') { $value = 'null'; } if ($value !== 'null' and !is_bool($value) and !ctype_digit($value)) { $value = trim($value, "'"); $value = "'{$value}'"; } if (is_bool($value)) { $value = $value ? 'true' : 'false'; } $this->options []= "'{$option}' => {$value}"; } } if (count($this->options) === 0) { return ''; } return ', [' . implode(', ', $this->options) . ']'; } protected function getIni(): int { if (!isset($this->ini)) { $this->ini = strpos($this->line, '`') + 1; } return $this->ini; } protected function getEnd(): int { if (!isset($this->end)) { $this->end = strpos($this->line, '`', $this->getIni()); } return $this->end; } protected function getParts(): array { if (!isset($this->parts)) { $this->parts = explode(' ', trim(substr($this->line, $this->getEnd() + 1), ' ,')); } return $this->parts; } } class ConstraintParser { public function __construct(protected string $line) {} public function parse(): string { return "->addForeignKey({$this->getColumns()}, '{$this->getTable()}', {$this->getReferences()}, ['delete' => 'cascade', 'update' => 'cascade'])"; } protected function getColumns(): string { $ini = strpos(strtolower($this->line), 'foreign key') + strlen('FOREIGN KEY '); $ini = strpos($this->line, '(', $ini) + 1; $end = strpos(strtolower($this->line), ' references', $ini + 1) - strlen(')'); return $this->getNames($ini, $end); } protected function getTable(): string { $ini = strpos(strtolower($this->line), 'references') + strlen('REFERENCES ') + 1; $end = strpos($this->line, '`', $ini + 1); return substr($this->line, $ini, $end - $ini); } protected function getReferences(): string { $ini = strpos($this->line, '(', strpos(strtolower($this->line), 'references')) + 1; $end = strpos($this->line, ')', $ini); return $this->getNames($ini, $end); } protected function getNames($ini, $end): string { $names = substr($this->line, $ini, $end - $ini); if (!str_contains($names, ',')) { return str_replace('`', "'", $names); } $names = explode(', ', $names); $columns = array_map(function($name) {return str_replace('`', "'", $name);}, $names); return '[' . implode(', ', $columns) . ']'; } } class MigrationGenerator { public function __construct(protected Config $settings) { $this->databaseName = $settings->getEnv('DB_DATABASE'); $this->migrationsPath = $settings->getMigrationsPath(); $this->startDate = $settings->getStartDate(); $this->database = new Incoviba\Common\Implement\Database\MySQL($settings->getEnv('DB_HOST'), $settings->getEnv('DB_DATABASE'), $settings->getEnv('DB_USER'), $settings->getEnv('DB_PASSWORD')); $this->connection = new Incoviba\Common\Implement\Connection($this->database, new Incoviba\Common\Implement\Database\Query\Builder()); $this->tableRepository = new TableRepository($this->connection, $this->databaseName); $this->logger = $settings->getLogger(); } public function run(bool $execute = true): void { $this->logger->output('Running generate migrations' . (($execute) ? '' : ' [dry-run]'), 250); foreach ($this->tableRepository->getTables() as $tableName) { if (in_array($tableName, $this->settings->getSkip())) { continue; } $this->logger->output("Table: {$tableName}"); $filename = $this->buildFilename($tableName); if ($execute) { $this->logger->debug("Filename: {$filename}"); } else { $this->logger->output("Filename: {$filename}"); } $content = $this->buildFile($tableName); $this->logger->debug("Content: {$content}"); if ($execute) { $status = file_put_contents($filename, $content); $this->logger->debug("Saved: " . var_export($status, true)); try { $this->registerMigration($tableName); } catch (PDOException $exception) { $this->logger->warning($exception); } } } $this->logger->output("Total tables migrated: " . count($this->tableRepository->getTables()), 250); } protected Incoviba\Common\Define\Database $database; protected Incoviba\Common\Define\Connection $connection; protected string $databaseName; protected string $migrationsPath; protected DateTimeInterface $startDate; protected TableRepository $tableRepository; protected Logger $logger; protected function buildFilename(string $table): string { $i = $this->tableRepository->getTableIndex($table); $time = $this->startDate->add(new DateInterval("PT{$i}S")); return implode(DIRECTORY_SEPARATOR, [ $this->migrationsPath, "{$time->format('YmdHis')}_create_{$table}.php" ]); } protected function buildClassName(string $table): string { return 'Create' . str_replace(' ', '', ucwords(str_replace('_', ' ', $table))); } protected function buildHeader(): string { return "buildClassName($table)} extends Phinx\Migration\AbstractMigration"; } protected function buildFunction(string $table): string { $output = ["{", "\tpublic function change(): void", "\t{"]; $output []= $this->buildInitialSetup(); $this->tableRepository->getTableDefinition($table); $output []= $this->tableRepository->parseTableDefinition($table); $output []= $this->buildFinalSetup(); $output []= "\t}"; return implode(PHP_EOL, $output); } protected function buildInitialSetup(): string { return implode(PHP_EOL, [ "\t\t\$this->execute('SET unique_checks=0; SET foreign_key_checks=0;');", "\t\t\$this->execute(\"ALTER DATABASE CHARACTER SET 'utf8mb4';\");", "\t\t\$this->execute(\"ALTER DATABASE COLLATE='utf8mb4_general_ci';\");", '' ]); } protected function buildFinalSetup(): string { return "\t\t\$this->execute('SET unique_checks=1; SET foreign_key_checks=1;');"; } protected function buildFile(string $table): string { return implode(PHP_EOL, [ $this->buildHeader(), $this->buildClass($table), $this->buildFunction($table), '}', '' ]); } protected function registerMigration(string $table): void { $i = $this->tableRepository->getTableIndex($table); $time = $this->startDate->add(new DateInterval("PT{$i}S")); $this->logger->output("Registering migration: {$time->format('Y-m-d H:i:s')}"); $migrationName = $this->buildClassName($table); $query = $this->connection->getQueryBuilder() ->insert() ->into('phinxlog') ->columns(['version', 'migration_name', 'start_time', 'end_time', 'breakpoint']) ->values(['?', '?', '?', '?', 0]); $this->connection->execute($query, [ $time->format('YmdHis'), $migrationName, $time->format('Y-m-d H:i:s'), $time->format('Y-m-d H:i:s') ]); } } class SeedGenerator { public function __construct(protected Config $settings) { $this->databaseName = $settings->getEnv('DB_DATABASE'); $this->seedsPath = $settings->getSeedsPath(); $this->startDate = $settings->getStartDate(); $this->database = new Incoviba\Common\Implement\Database\MySQL($settings->getEnv('DB_HOST'), $settings->getEnv('DB_DATABASE'), $settings->getEnv('DB_USER'), $settings->getEnv('DB_PASSWORD')); $this->connection = new Incoviba\Common\Implement\Connection($this->database, new Incoviba\Common\Implement\Database\Query\Builder()); $this->tableRepository = new TableRepository($this->connection, $this->databaseName); $this->dataRepository = new DataRepository($this->connection); $this->logger = $settings->getLogger(); } public function run(bool $execute = true): void { $this->logger->output('Running generate seeds' . (($execute) ? '' : ' [dry-run]'), 250); $tables = $this->tableRepository->getTables(); foreach ($tables as $table) { if (in_array($table, $this->settings->getSkip())) { continue; } $this->logger->output("Table: {$table}"); $filename = $this->buildFilename($table); if ($execute) { $this->logger->debug("Filename: {$filename}"); } else { $this->logger->output("Filename: {$filename}"); } $content = $this->buildFile($table); $this->logger->debug("Content: {$content}"); if ($execute) { $status = file_put_contents($filename, $content); $this->logger->debug("Saved: " . var_export($status, true)); } } $this->logger->output("Total tables seeded: " . count($this->tableRepository->getTables()), 250); } protected Incoviba\Common\Define\Database $database; protected Incoviba\Common\Define\Connection $connection; protected string $databaseName; protected string $seedsPath; protected DateTimeInterface $startDate; protected TableRepository $tableRepository; protected DataRepository $dataRepository; protected Logger $logger; protected function buildFilename(string $table): string { $i = $this->tableRepository->getTableIndex($table); $time = $this->startDate->add(new DateInterval("PT{$i}S")); return implode(DIRECTORY_SEPARATOR, [ $this->seedsPath, "{$time->format('YmdHis')}_{$table}_seeder.php" ]); } protected function buildFile(string $table): string { return implode(PHP_EOL, [ $this->buildHeader(), $this->buildClass($table), "{", $this->buildFunction($table), '}', '' ]); } protected function buildHeader(): string { return "buildClassName($table)} extends AbstractSeed"; } protected function buildClassName(string $table): string { return str_replace(' ', '', ucwords(str_replace('_', ' ', $table))) . 'Seeder'; } protected function buildFunction(string $table): string { return implode(PHP_EOL, [ "\tpublic function run(): void", "\t{", $this->buildData($table), "", $this->buildInitialSetup(), "\t\t\$this->table('{$table}')", "\t\t\t->insert(\$data)", "\t\t\t->saveData();", $this->buildFinalSetup(), "\t}" ]); } protected function buildData(string $table): string { $output = ["\t\t\$data = ["]; $dataGenerator = $this->dataRepository->getData($table); foreach ($dataGenerator as $row) { $output []= "\t\t\t["; foreach ($row as $key => $value) { if (is_bool($value)) { $value = $value ? 1 : 0; } if (!ctype_digit("{$value}") and $value !== null) { if (str_contains($value, "'")) { $value = str_replace("'", "\'", $value); } $value = "'{$value}'"; } if ($value === null) { $value = 'null'; } if (strlen($value) > 2 and str_starts_with($value, '0')) { $value = "'{$value}'"; } $output []= "\t\t\t\t'{$key}' => {$value},"; } $output []= "\t\t\t],"; } $output []= "\t\t];"; $this->logger->debug("Total data: {$this->dataRepository->size}"); return implode(PHP_EOL, $output); } protected function buildInitialSetup(): string { return "\t\t\$this->execute('SET unique_checks=0; SET foreign_key_checks=0;');"; } protected function buildFinalSetup(): string { return "\t\t\$this->execute('SET unique_checks=1; SET foreign_key_checks=1;');"; } } class Logger { public function __construct(protected bool $verbose = false, protected $quiet = false) {} protected array $loggers; public function registerLog(Psr\Log\LoggerInterface $logger): Logger { $this->loggers['log'] = $logger; return $this; } public function registerOutput(Psr\Log\LoggerInterface $logger): Logger { $this->loggers['output'] = $logger; return $this; } public function log(string $message, int $level = 200): void { if ($this->canLogLevel($level)) { $this->loggers['log']->log($level, $message); } } public function output(string $message, int $level = 200): void { if ($this->canLogLevel($level)) { $this->loggers['output']->log($level, $message); $this->log($message, $level); } } public function __call(string $name, array $arguments) { if (method_exists($this->loggers['log'], $name)) { $levelMap = [ 'debug' => 100, 'info' => 200, 'notice' => 250, 'warning' => 300, 'error' => 400, 'critical' => 500, 'alert' => 550, 'emergency' => 600 ]; $level = $levelMap[strtolower($name)]; $this->log($arguments[0], $level); } } protected function canLogLevel(int $level): bool { $minQuiet = 300; $maxVerbose = 250; if ($this->quiet) { return $level >= $minQuiet; } if ($this->verbose) { return true; } return $level >= $maxVerbose; } } class Config { public function __construct() { $this->opts = getopt('d:m:s:r:vq', ['date:', 'migrations:', 'seeds:', 'run:', 'verbose', 'quiet']); } public function getStartDate(): DateTimeInterface { $dateString = '20141101080000'; if (isset($this->opts['d']) or isset($this->opts['date'])) { $dateString = $this->opts['d'] ?? $this->opts['date']; } return new DateTimeImmutable($dateString); } public function getMigrationsPath(): string { $migrationsPath = implode(DIRECTORY_SEPARATOR, [__DIR__, 'resources', 'database', 'migrations']); if (isset($this->opts['m']) or isset($this->opts['migrations'])) { $migrationsPath = $this->opts['m'] ?? $this->opts['migrations']; } return $migrationsPath; } public function getSeedsPath(): string { $seedsPath = implode(DIRECTORY_SEPARATOR, [__DIR__, 'resources', 'database', 'seeds']); if (isset($this->opts['s']) or isset($this->opts['seeds'])) { $seedsPath = $this->opts['s'] ?? $this->opts['seeds']; } return $seedsPath; } public function getRun(): int { $option = 0; if (isset($this->opts['r']) or isset($this->opts['run'])) { $option = $this->opts['r'] ?? $this->opts['run']; } if (ctype_digit("{$option}")) { return (int) $option; } return match($option) { 'm', 'migrations' => 1, 's', 'seeds' => 2, 'a', 'all' => 3, default => 0 }; } public function getEnv(string $name): mixed { return $_ENV[$name]; } public function getLogger(): Logger { return (new Logger($this->isVerbose(), $this->isQuiet())) ->registerLog($this->getPsrLogger()) ->registerOutput(new Monolog\Logger('output', [ new Monolog\Handler\FilterHandler( (new Monolog\Handler\StreamHandler(STDOUT)) ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, false, true)), Monolog\Level::Info, Monolog\Level::Notice ) ], [ new Monolog\Processor\ProcessIdProcessor(), new Monolog\Processor\MemoryUsageProcessor(), new Monolog\Processor\MemoryPeakUsageProcessor(), new Monolog\Processor\PsrLogMessageProcessor() ])); } public function getPsrLogger(): Psr\Log\LoggerInterface { return new Monolog\Logger('migrations', [ (new Monolog\Handler\RotatingFileHandler('/logs/migrations.log')) ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, false, false, true)), ], [ new Monolog\Processor\ProcessIdProcessor(), new Monolog\Processor\MemoryUsageProcessor(), new Monolog\Processor\MemoryPeakUsageProcessor(), new Monolog\Processor\PsrLogMessageProcessor() ] ); } public function setSkip(string|array $skip): Config { if (is_string($skip)) { $skip = [$skip]; } $this->skip = [...$this->skip ?? [], ...$skip]; return $this; } public function getSkip(): array { return $this->skip; } public function isVerbose(): bool { return isset($this->opts['v']) or isset($this->opts['verbose']); } public function isQuiet(): bool { return isset($this->opts['q']) or isset($this->opts['quiet']); } protected array $opts; protected array $skip; } $config = new Config(); $config->setSkip([ 'monolog', 'phinxlog', 'personas', 'datos_personas', 'proveedores', 'datos_proveedores' ]); Monolog\ErrorHandler::register($config->getPsrLogger()); try { $generator = new MigrationGenerator($config); $seeder = new SeedGenerator($config); switch ($config->getRun()) { case 0: $generator->run(false); $seeder->run(false); break; case 3: $generator->run(); $seeder->run(); break; case 1: $generator->run(); break; case 2: $seeder->run(); break; } } catch (Exception $exception) { $config->getLogger()->warning($exception); } catch (Error $error) { $config->getLogger()->error($error); }