diff --git a/.gitignore b/.gitignore index 7f99ea4..bbf8d33 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **/log?/* **/cache/* **/.idea -**/vendor/ \ No newline at end of file +**/vendor/ +/app/composer.lock diff --git a/app/bin/app b/app/bin/app new file mode 100644 index 0000000..bb3a3eb --- /dev/null +++ b/app/bin/app @@ -0,0 +1,8 @@ +#!/usr/bin/env php +run(); \ No newline at end of file diff --git a/app/bin/phinx b/app/bin/phinx new file mode 100644 index 0000000..dea8f96 --- /dev/null +++ b/app/bin/phinx @@ -0,0 +1,119 @@ +#!/usr/bin/env php +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/vendor/robmorgan/phinx/bin/phinx'); + } +} + +return include __DIR__ . '/..'.'/vendor/robmorgan/phinx/bin/phinx'; diff --git a/app/bin/php-parse b/app/bin/php-parse new file mode 100644 index 0000000..a208b76 --- /dev/null +++ b/app/bin/php-parse @@ -0,0 +1,119 @@ +#!/usr/bin/env php +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = $this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/vendor/nikic/php-parser/bin/php-parse'); + } +} + +return include __DIR__ . '/..'.'/vendor/nikic/php-parser/bin/php-parse'; diff --git a/app/bin/phpunit b/app/bin/phpunit new file mode 100644 index 0000000..a6b823e --- /dev/null +++ b/app/bin/phpunit @@ -0,0 +1,122 @@ +#!/usr/bin/env php +realpath = realpath($opened_path) ?: $opened_path; + $opened_path = 'phpvfscomposer://'.$this->realpath; + $this->handle = fopen($this->realpath, $mode); + $this->position = 0; + + return (bool) $this->handle; + } + + public function stream_read($count) + { + $data = fread($this->handle, $count); + + if ($this->position === 0) { + $data = preg_replace('{^#!.*\r?\n}', '', $data); + } + $data = str_replace('__DIR__', var_export(dirname($this->realpath), true), $data); + $data = str_replace('__FILE__', var_export($this->realpath, true), $data); + + $this->position += strlen($data); + + return $data; + } + + public function stream_cast($castAs) + { + return $this->handle; + } + + public function stream_close() + { + fclose($this->handle); + } + + public function stream_lock($operation) + { + return $operation ? flock($this->handle, $operation) : true; + } + + public function stream_seek($offset, $whence) + { + if (0 === fseek($this->handle, $offset, $whence)) { + $this->position = ftell($this->handle); + return true; + } + + return false; + } + + public function stream_tell() + { + return $this->position; + } + + public function stream_eof() + { + return feof($this->handle); + } + + public function stream_stat() + { + return array(); + } + + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + public function url_stat($path, $flags) + { + $path = substr($path, 17); + if (file_exists($path)) { + return stat($path); + } + + return false; + } + } + } + + if ( + (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true)) + || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper')) + ) { + return include("phpvfscomposer://" . __DIR__ . '/..'.'/vendor/phpunit/phpunit/phpunit'); + } +} + +return include __DIR__ . '/..'.'/vendor/phpunit/phpunit/phpunit'; diff --git a/app/bootstrap/app.php b/app/bootstrap/app.php new file mode 100644 index 0000000..a6423e0 --- /dev/null +++ b/app/bootstrap/app.php @@ -0,0 +1,32 @@ +isDir()) { + continue; + } + $builder->addDefinitions($file->getRealPath()); + } + } + + $app = (new ProVM\Extend\Application()) + ->setContainer($builder->build()); + return require_once 'commands.php'; +} + +require_once 'composer.php'; + +return buildApp(); diff --git a/app/bootstrap/commands.php b/app/bootstrap/commands.php new file mode 100644 index 0000000..9f47863 --- /dev/null +++ b/app/bootstrap/commands.php @@ -0,0 +1,9 @@ +getContainer()->get('commands'); + foreach ($commands as $commandClass) { + $app->add($app->getContainer()->get($commandClass)); + } + return $app; +} +return buildCommands($app); \ No newline at end of file diff --git a/app/bootstrap/composer.php b/app/bootstrap/composer.php new file mode 100644 index 0000000..f29699c --- /dev/null +++ b/app/bootstrap/composer.php @@ -0,0 +1,6 @@ + [ + ProVM\Command\Generate::class, + ProVM\Command\GenerateMigrations::class, + ProVM\Command\GenerateSeeds::class, + ] +]; \ No newline at end of file diff --git a/app/configs/env.php b/app/configs/env.php new file mode 100644 index 0000000..721340d --- /dev/null +++ b/app/configs/env.php @@ -0,0 +1,13 @@ + '20141101080000', + 'skips' => [ + 'monolog', + 'phinxlog', + 'personas', + 'datos_personas', + 'proveedores', + 'datos_proveedores' + ] +]; \ No newline at end of file diff --git a/app/configs/paths.php b/app/configs/paths.php new file mode 100644 index 0000000..52a6e0e --- /dev/null +++ b/app/configs/paths.php @@ -0,0 +1,8 @@ + dirname(__DIR__), + 'paths.resources' => DI\String('{paths.base}/resources'), + 'paths.database' => DI\String('{paths.resources}/database'), + 'paths.migrations' => DI\String('{paths.database}/migrations'), + 'paths.seeds' => DI\String('{paths.database}/seeds') +]; \ No newline at end of file diff --git a/app/setups/concepts.php b/app/setups/concepts.php new file mode 100644 index 0000000..7ec4b2e --- /dev/null +++ b/app/setups/concepts.php @@ -0,0 +1,26 @@ + function(ContainerInterface $container) { + return (new ProVM\Database\MySQL()) + ->setHost($container->get('DB_HOST')) + ->setName($container->get('DB_DATABASE')) + ->setUser($container->get('DB_USER')) + ->setPassword($container->get('DB_PASSWORD')); + }, + ProVM\Concept\Database\Connection::class => function(ContainerInterface $container) { + return new ProVM\Database\Connection($container->get(ProVM\Concept\Database::class)); + }, + ProVM\Concept\Database\Query\Builder::class => function(ContainerInterface $container) { + return new ProVM\Database\Query\Builder([ + ProVM\Concept\Database\Query\Select::class => ProVM\Database\Query\MySQL\Select::class, + ProVM\Concept\Database\Query\Insert::class => ProVM\Database\Query\MySQL\Insert::class, + ProVM\Concept\Database\Query\Update::class => ProVM\Database\Query\MySQL\Update::class, + ProVM\Concept\Database\Query\Delete::class => ProVM\Database\Query\MySQL\Delete::class, + ProVM\Concept\Database\Query\Create::class => ProVM\Database\Query\MySQL\Create::class, + ProVM\Concept\Database\Query\Drop::class => ProVM\Database\Query\MySQL\Drop::class, + ProVM\Concept\Database\Query\Truncate::class => ProVM\Database\Query\MySQL\Truncate::class, + ]); + } +]; \ No newline at end of file diff --git a/app/setups/generators.php b/app/setups/generators.php new file mode 100644 index 0000000..7db3889 --- /dev/null +++ b/app/setups/generators.php @@ -0,0 +1,32 @@ + function(ContainerInterface $container) { + return new ProVM\Generator\Migration( + $container->get(ProVM\Concept\Database::class), + $container->get(ProVM\Concept\Database\Connection::class), + $container->get(ProVM\Concept\Database\Query\Builder::class), + $container->get(ProVM\Repository\Table::class), + $container->get(Psr\Log\LoggerInterface::class), + new DateTimeImmutable($container->get('start_date')), + $container->get('DB_DATABASE'), + $container->get('paths.migrations'), + $container->get('skips') + ); + }, + ProVM\Generator\Seed::class => function(ContainerInterface $container) { + return new ProVM\Generator\Seed( + $container->get(ProVM\Concept\Database::class), + $container->get(ProVM\Concept\Database\Connection::class), + $container->get(ProVM\Concept\Database\Query\Builder::class), + $container->get(ProVM\Repository\Table::class), + $container->get(ProVM\Repository\Data::class), + $container->get(Psr\Log\LoggerInterface::class), + $container->get('DB_DATABASE'), + $container->get('paths.seeds'), + new DateTimeImmutable($container->get('start_date')), + $container->get('skips') + ); + } +]; \ No newline at end of file diff --git a/app/setups/logs.php b/app/setups/logs.php new file mode 100644 index 0000000..a7eb285 --- /dev/null +++ b/app/setups/logs.php @@ -0,0 +1,16 @@ + function(ContainerInterface $container) { + return new Monolog\Logger('migrations', [ + (new Monolog\Handler\RotatingFileHandler('/logs/migrations.log')) + ->setFormatter(new Monolog\Formatter\LineFormatter(null, null, false, false, true)) + ], [ + $container->get(Monolog\Processor\IntrospectionProcessor::class), + $container->get(Monolog\Processor\MemoryUsageProcessor::class), + $container->get(Monolog\Processor\MemoryPeakUsageProcessor::class), + $container->get(Monolog\Processor\PsrLogMessageProcessor::class) + ]); + } +]; \ No newline at end of file diff --git a/app/setups/repositories.php b/app/setups/repositories.php new file mode 100644 index 0000000..2d30a9b --- /dev/null +++ b/app/setups/repositories.php @@ -0,0 +1,15 @@ + function(ContainerInterface $container) { + return new ProVM\Repository\Table( + $container->get(ProVM\Concept\Database\Connection::class), + $container->get('DB_DATABASE')); + }, + ProVM\Repository\Data::class => function(ContainerInterface $container) { + return new ProVM\Repository\Data( + $container->get(ProVM\Concept\Database\Connection::class), + $container->get(ProVM\Concept\Database\Query\Builder::class)); + } +]; \ No newline at end of file diff --git a/app/src/Command/Generate.php b/app/src/Command/Generate.php new file mode 100644 index 0000000..11a1860 --- /dev/null +++ b/app/src/Command/Generate.php @@ -0,0 +1,40 @@ +addOption('dry-run', 'd'); + } + public function execute(InputInterface $input, OutputInterface $output): int + { + $io = new Console\Style\SymfonyStyle($input, $output); + $io->title('Generate'); + + $dryRun = $input->hasOption('dry-run'); + + $commands = [ + 'generate:migrations', + 'generate:seeds' + ]; + foreach ($commands as $commandName) { + $command = $this->getApplication()->find($commandName); + $arguments = [ + 'command' => $commandName, + ]; + if ($dryRun) { + $arguments['--dry-run'] = true; + } + $command->run(new Console\Input\ArrayInput($arguments), $output); + } + + return Console\Command\Command::SUCCESS; + } +} \ No newline at end of file diff --git a/app/src/Command/GenerateMigrations.php b/app/src/Command/GenerateMigrations.php new file mode 100644 index 0000000..13e2b04 --- /dev/null +++ b/app/src/Command/GenerateMigrations.php @@ -0,0 +1,34 @@ +addOption('dry-run', 'd', Console\Input\InputOption::VALUE_OPTIONAL, default: false); + } + + public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int + { + $io = new Console\Style\SymfonyStyle($input, $output); + $io->title('Generate Migrations'); + + $dryRun = $input->getOption('dry-run'); + + $this->migrationGenerator->generate($io, $dryRun); + + return Console\Command\Command::SUCCESS; + } +} \ No newline at end of file diff --git a/app/src/Command/GenerateSeeds.php b/app/src/Command/GenerateSeeds.php new file mode 100644 index 0000000..5a24cd0 --- /dev/null +++ b/app/src/Command/GenerateSeeds.php @@ -0,0 +1,34 @@ +addOption('dry-run', 'd', Console\Input\InputOption::VALUE_OPTIONAL, default: false); + } + + public function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int + { + $io = new Console\Style\SymfonyStyle($input, $output); + $io->title('Generate Seeds'); + + $dryRun = $input->getOption('dry-run'); + + $this->seedGenerator->generate($io, $dryRun); + + return Console\Command\Command::SUCCESS; + } +} \ No newline at end of file diff --git a/app/src/Concept/Generator.php b/app/src/Concept/Generator.php new file mode 100644 index 0000000..ab7ffc0 --- /dev/null +++ b/app/src/Concept/Generator.php @@ -0,0 +1,9 @@ +logger->info($message); + if ($output) { + $io->note($message); + } + } +} \ No newline at end of file diff --git a/app/src/Extend/Application.php b/app/src/Extend/Application.php new file mode 100644 index 0000000..7a50870 --- /dev/null +++ b/app/src/Extend/Application.php @@ -0,0 +1,20 @@ +container; + } + public function setContainer(ContainerInterface $container): Console\Application + { + $this->container = $container; + return $this; + } +} \ No newline at end of file diff --git a/app/src/Generator/Migration.php b/app/src/Generator/Migration.php new file mode 100644 index 0000000..bc20f6c --- /dev/null +++ b/app/src/Generator/Migration.php @@ -0,0 +1,132 @@ +log('Running generate migrations' . (($dryRun) ? ' [dry-run]' : ''), true, $io); + foreach ($this->tableRepository->getAll() as $tableName) { + if (in_array($tableName, $this->skips)) { + continue; + } + $this->log("Table: {$tableName}", true, $io); + $filename = $this->buildFilename($tableName); + $this->log("Filename: {$filename}", $dryRun, $io); + $content = $this->buildFile($tableName); + $this->log("Content: {$content}"); + + if ($dryRun) { + $status = file_put_contents($filename, $content); + $this->log("Saved: " . var_export($status, true)); + try { + $this->registerMigration($tableName); + } catch (PDOException $exception) { + $this->logger->warning($exception); + } + } + } + $this->log("Total tables migrated: " . count($this->tableRepository->getAll()), true, $io); + } + + protected function buildFilename(string $table): string + { + $i = $this->tableRepository->getIndex($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->getDefinition($table); + $output []= $this->tableRepository->parseDefinition($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->getIndex($table); + $time = $this->startDate->add(new DateInterval("PT{$i}S")); + $this->logger->info("Registering migration: {$time->format('Y-m-d H:i:s')}"); + $migrationName = $this->buildClassName($table); + + $query = $this->queryBuilder + ->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') + ]); + } +} \ No newline at end of file diff --git a/app/src/Generator/Seed.php b/app/src/Generator/Seed.php new file mode 100644 index 0000000..4ce6bd5 --- /dev/null +++ b/app/src/Generator/Seed.php @@ -0,0 +1,139 @@ +log('Running generate seeds' . (($dryRun) ? ' [dry-run]' : ''), true, $io); + $tables = $this->tableRepository->getAll(); + foreach ($tables as $table) { + if (in_array($table, $this->skips)) { + continue; + } + $this->log("Table: {$table}"); + $filename = $this->buildFilename($table); + $this->log("Filename: {$filename}", $dryRun, $io); + $content = $this->buildFile($table); + $this->log("Content: {$content}"); + + if ($dryRun) { + $status = file_put_contents($filename, $content); + $this->log("Saved: " . var_export($status, true)); + } + } + $this->log("Total tables seeded: " . count($this->tableRepository->getAll()), true, $io); + } + + protected function buildFilename(string $table): string + { + $i = $this->tableRepository->getIndex($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->getAll($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;');"; + } +} \ No newline at end of file diff --git a/app/src/Parser/Column.php b/app/src/Parser/Column.php new file mode 100644 index 0000000..fb1f91e --- /dev/null +++ b/app/src/Parser/Column.php @@ -0,0 +1,108 @@ +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; + } +} \ No newline at end of file diff --git a/app/src/Parser/Constraint.php b/app/src/Parser/Constraint.php new file mode 100644 index 0000000..f123518 --- /dev/null +++ b/app/src/Parser/Constraint.php @@ -0,0 +1,44 @@ +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) . ']'; + } +} \ No newline at end of file diff --git a/app/src/Repository/Data.php b/app/src/Repository/Data.php new file mode 100644 index 0000000..fc1a8c7 --- /dev/null +++ b/app/src/Repository/Data.php @@ -0,0 +1,25 @@ +queryBuilder + ->select() + ->from($table); + $results = $this->connection->query($query); + $this->size = $results->rowCount(); + while ($row = $results->fetch()) { + yield $row; + } + } +} \ No newline at end of file diff --git a/app/src/Repository/Table.php b/app/src/Repository/Table.php new file mode 100644 index 0000000..afbf37b --- /dev/null +++ b/app/src/Repository/Table.php @@ -0,0 +1,95 @@ +tables)) { + $results = $this->connection->query('SHOW TABLES'); + $rows = $results->fetchAll(); + $this->tables = array_map(function(array $row) { + return $row["Tables_in_{$this->databaseName}"]; + }, $rows); + } + return $this->tables; + } + public function getDefinition(string $table): string + { + if (!isset($this->definitions[$table])) { + $results = $this->connection->query("SHOW CREATE TABLE {$table}"); + $rows = $results->fetchAll(); + $this->definitions[$table] = $rows[0]["Create Table"]; + } + return $this->definitions[$table]; + } + public function parseDefinition(string $table, int $tabOffset = 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(string $key) {return "'{$key}'";}, $this->primary)); + $tableLine .= ", ['id' => false, 'primary key' => [{$primaryString}]]"; + } + $tableLine .= ')'; + $output = [str_repeat("\t", $tabOffset) . $tableLine]; + foreach ($this->columns as $column) { + $output[] = str_repeat("\t", $tabOffset + 1) . $column; + } + foreach ($this->constraints as $constraint) { + $output[] = str_repeat("\t", $tabOffset + 1) . $constraint; + } + return implode(PHP_EOL, $output); + } + public function getIndex(string $table): int + { + return array_search($table, $this->tables); + } + + 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) { + $trimmedLine = trim($line); + if (str_starts_with($trimmedLine, ') ENGINE')) { + break; + } + if (str_starts_with($trimmedLine, '`id`') or str_starts_with($trimmedLine, 'KEY')) { + continue; + } + if (str_starts_with($trimmedLine, 'PRIMARY KEY')) { + if (str_contains($line, '`id`')) { + continue; + } + $ini = strpos($line, '(') +1; + $end = strpos($line, ')', $ini); + $this->primary []= substr($line, $ini, $end - $ini); + continue; + } + if (str_starts_with($trimmedLine, 'CONSTRAINT')) { + $this->constraints []= (new Parser\Constraint($line))->parse(); + continue; + } + $this->columns []= (new Parser\Column($line))->parse(); + } + } +} \ No newline at end of file diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f471712 --- /dev/null +++ b/compose.yml @@ -0,0 +1,21 @@ +name: pmg +services: + app: + image: php:cli + env_file: ./app/.env + restart: no + working_dir: /app + volumes: + - ./app:/app + - ./logs:/logs + depends_on: + - db + + db: + image: mariadb + env_file: .db.env + volumes: + - pmg-data:/var/lib/mysql + +volumes: + pmg-data: {} diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29