Compare commits

18 Commits

Author SHA1 Message Date
9a479e6428 Merge branch 'develop' into release 2024-12-22 11:17:16 -03:00
dc3ef0a3bb General command 2024-12-22 11:03:03 -03:00
ab08f13d2f Configuration and Command 2024-12-22 11:02:51 -03:00
4f992a9294 Seed Generator 2024-12-22 11:02:31 -03:00
21c593c93b Use abstract class 2024-12-22 11:02:21 -03:00
01e6cef219 Interface and Abstract class for Generators 2024-12-22 11:02:02 -03:00
3dc7259ccb Ignore lock file 2024-12-21 19:01:04 -03:00
a51c15cb25 Docker 2024-12-21 19:00:48 -03:00
c142a8975f console 2024-12-21 19:00:40 -03:00
211b6b0b94 Config, setup and bootstrap 2024-12-21 19:00:31 -03:00
0fddc3a310 App extension 2024-12-21 19:00:15 -03:00
03f91bd721 Command 2024-12-21 19:00:07 -03:00
18742b3947 Generator 2024-12-21 19:00:02 -03:00
eb26f31ed6 Parsers 2024-12-21 18:59:55 -03:00
ea205df76d Repositories 2024-12-21 18:59:46 -03:00
916895e489 Concept 2024-12-21 18:59:34 -03:00
a274d03c91 Dependencies 2024-12-21 18:52:40 -03:00
a64421c564 Keep logs and cache 2024-12-21 15:42:05 -03:00
32 changed files with 1303 additions and 1 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@
**/log?/*
**/cache/*
**/.idea
**/vendor/
**/vendor/
/app/composer.lock

8
app/bin/app Normal file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env php
<?php
$app = require_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'bootstrap',
'app.php'
]);
$app->run();

119
app/bin/phinx Normal file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../vendor/robmorgan/phinx/bin/phinx)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/vendor/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->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';

119
app/bin/php-parse Normal file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../vendor/nikic/php-parser/bin/php-parse)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/vendor/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->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';

122
app/bin/phpunit Normal file
View File

@ -0,0 +1,122 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../vendor/phpunit/phpunit/phpunit)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/vendor/autoload.php';
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'] = $GLOBALS['__PHPUNIT_ISOLATION_BLACKLIST'] = array(realpath(__DIR__ . '/..'.'/vendor/phpunit/phpunit/phpunit'));
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->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';

32
app/bootstrap/app.php Normal file
View File

@ -0,0 +1,32 @@
<?php
function buildApp(): Symfony\Component\Console\Application
{
$builder = new DI\ContainerBuilder();
$baseFolder = dirname(__DIR__);
$folders = [
'configs',
'setups'
];
foreach ($folders as $folderName) {
$folder = implode(DIRECTORY_SEPARATOR, [$baseFolder, $folderName]);
if (!isset($folder)) {
continue;
}
$files = new FilesystemIterator($folder);
foreach ($files as $file) {
if ($file->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();

View File

@ -0,0 +1,9 @@
<?php
function buildCommands(\Symfony\Component\Console\Application &$app): \Symfony\Component\Console\Application {
$commands = $app->getContainer()->get('commands');
foreach ($commands as $commandClass) {
$app->add($app->getContainer()->get($commandClass));
}
return $app;
}
return buildCommands($app);

View File

@ -0,0 +1,6 @@
<?php
require_once implode(DIRECTORY_SEPARATOR, [
dirname(__DIR__),
'vendor',
'autoload.php'
]);

41
app/composer.json Normal file
View File

@ -0,0 +1,41 @@
{
"name": "provm/phinx-migration-generator",
"description": "Generate database migration files for Phinx for each table.",
"type": "project",
"require": {
"monolog/monolog": "^3.8",
"php-di/php-di": "^7.0",
"provm/database": "^2.3",
"provm/query_builder": "^1.1",
"symfony/console": "^7.2"
},
"require-dev": {
"phpunit/phpunit": "^11.5",
"robmorgan/phinx": "^0.16.6"
},
"authors": [
{
"name": "Aldarien",
"email": "aldarien85@gmail.com"
}
],
"autoload": {
"psr-4": {
"ProVM\\": "src/"
}
},
"config": {
"sort-packages": true,
"bin-dir": "./bin"
},
"repositories": [
{
"type": "git",
"url": "https://git.provm.cl/ProVM/database"
},
{
"type": "git",
"url": "https://git.provm.cl/ProVM/query_builder"
}
]
}

8
app/configs/commands.php Normal file
View File

@ -0,0 +1,8 @@
<?php
return [
'commands' => [
ProVM\Command\Generate::class,
ProVM\Command\GenerateMigrations::class,
ProVM\Command\GenerateSeeds::class,
]
];

13
app/configs/env.php Normal file
View File

@ -0,0 +1,13 @@
<?php
return [
...$_ENV,
'start_date' => '20141101080000',
'skips' => [
'monolog',
'phinxlog',
'personas',
'datos_personas',
'proveedores',
'datos_proveedores'
]
];

8
app/configs/paths.php Normal file
View File

@ -0,0 +1,8 @@
<?php
return [
'paths.base' => 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')
];

26
app/setups/concepts.php Normal file
View File

@ -0,0 +1,26 @@
<?php
use Psr\Container\ContainerInterface;
return [
ProVM\Concept\Database::class => 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,
]);
}
];

32
app/setups/generators.php Normal file
View File

@ -0,0 +1,32 @@
<?php
use Psr\Container\ContainerInterface;
return [
ProVM\Generator\Migration::class => 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')
);
}
];

16
app/setups/logs.php Normal file
View File

@ -0,0 +1,16 @@
<?php
use Psr\Container\ContainerInterface;
return [
Psr\Log\LoggerInterface::class => 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)
]);
}
];

View File

@ -0,0 +1,15 @@
<?php
use Psr\Container\ContainerInterface;
return [
ProVM\Repository\Table::class => 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));
}
];

View File

@ -0,0 +1,40 @@
<?php
namespace ProVM\Command;
use Symfony\Component\Console;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[Console\Attribute\AsCommand(name: 'generate', description: 'Generate database migrations and seeds.')]
class Generate extends Console\Command\Command
{
protected function configure()
{
parent::configure();
$this->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;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace ProVM\Command;
use Symfony\Component\Console;
use ProVM\Generator;
#[Console\Attribute\AsCommand(
name: 'generate:migrations'
)]
class GenerateMigrations extends Console\Command\Command
{
public function __construct(protected Generator\Migration $migrationGenerator, ?string $name = null)
{
parent::__construct($name);
}
protected function configure(): void
{
parent::configure();
$this->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;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace ProVM\Command;
use Symfony\Component\Console;
use ProVM\Generator;
#[Console\Attribute\AsCommand(
name: 'generate:seeds'
)]
class GenerateSeeds extends Console\Command\Command
{
public function __construct(protected Generator\Seed $seedGenerator, ?string $name = null)
{
parent::__construct($name);
}
protected function configure(): void
{
parent::configure();
$this->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;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace ProVM\Concept;
use Symfony\Component\Console\Style\StyleInterface;
interface Generator
{
public function generate(StyleInterface $io, bool $dryRun = false): void;
}

View File

@ -0,0 +1,7 @@
<?php
namespace ProVM\Concept;
interface Parser
{
public function parse(): string;
}

View File

@ -0,0 +1,19 @@
<?php
namespace ProVM\Enforce;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Style\StyleInterface;
use ProVM\Concept;
abstract class Generator implements Concept\Generator
{
public function __construct(protected LoggerInterface $logger) {}
protected function log(string $message, bool $output = false, ?StyleInterface $io = null): void
{
$this->logger->info($message);
if ($output) {
$io->note($message);
}
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace ProVM\Extend;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console;
class Application extends Console\Application
{
protected ContainerInterface $container;
public function getContainer(): ContainerInterface
{
return $this->container;
}
public function setContainer(ContainerInterface $container): Console\Application
{
$this->container = $container;
return $this;
}
}

View File

@ -0,0 +1,132 @@
<?php
namespace ProVM\Generator;
use DateTimeInterface;
use DateInterval;
use PDOException;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Style\StyleInterface;
use ProVM\Concept;
use ProVM\Enforce;
use ProVM\Repository;
class Migration extends Enforce\Generator
{
public function __construct(
protected Concept\Database $database,
protected Concept\Database\Connection $connection,
protected Concept\Database\Query\Builder $queryBuilder,
public Repository\Table $tableRepository,
LoggerInterface $logger,
protected DateTimeInterface $startDate,
protected string $databaseName,
protected string $migrationsPath,
protected array $skips)
{
parent::__construct($logger);
}
public function generate(StyleInterface $io, bool $dryRun = false): void
{
$this->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 "<?php
use Phinx\Db\Adapter\MysqlAdapter;
";
}
protected function buildClass(string $table): string
{
return "class {$this->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')
]);
}
}

139
app/src/Generator/Seed.php Normal file
View File

@ -0,0 +1,139 @@
<?php
namespace ProVM\Generator;
use DateTimeInterface;
use DateInterval;
use Psr\Log\LoggerInterface;
use ProVM\Concept;
use ProVM\Enforce;
use ProVM\Repository;
use Symfony\Component\Console\Style\StyleInterface;
class Seed extends Enforce\Generator
{
public function __construct(
protected Concept\Database $database,
protected Concept\Database\Connection $connection,
protected Concept\Database\Query\Builder $queryBuilder,
protected Repository\Table $tableRepository,
protected Repository\Data $dataRepository,
LoggerInterface $logger,
protected string $databaseName,
protected string $seedsPath,
protected DateTimeInterface $startDate,
protected array $skips)
{
parent::__construct($logger);
}
public function generate(StyleInterface $io, bool $dryRun = false): void
{
$this->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 "<?php
use Phinx\Seed\AbstractSeed;
";
}
protected function buildClass(string $table): string
{
return "class {$this->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;');";
}
}

108
app/src/Parser/Column.php Normal file
View File

@ -0,0 +1,108 @@
<?php
namespace ProVM\Parser;
use ProVM\Concept;
class Column implements Concept\Parser
{
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;
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace ProVM\Parser;
use ProVM\Concept;
class Constraint implements Concept\Parser
{
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) . ']';
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace ProVM\Repository;
use Generator;
use PDO;
use ProVM\Concept;
class Data
{
public function __construct(protected Concept\Database\Connection $connection, protected Concept\Database\Query\Builder $queryBuilder) {}
public int $size;
public function getAll(string $table): Generator
{
$query = $this->queryBuilder
->select()
->from($table);
$results = $this->connection->query($query);
$this->size = $results->rowCount();
while ($row = $results->fetch()) {
yield $row;
}
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace ProVM\Repository;
use PDO;
use ProVM\Concept;
use ProVM\Parser;
class Table
{
public function __construct(protected Concept\Database\Connection $connection, protected string $databaseName) {}
public function getAll(): array
{
if (!isset($this->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();
}
}
}

0
cache/.gitkeep vendored Normal file
View File

21
compose.yml Normal file
View File

@ -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: {}

0
logs/.gitkeep Normal file
View File