diff --git a/src/Commands/DatabaseCommand.php b/src/Commands/DatabaseCommand.php index b4e0ac6..2ec8d44 100644 --- a/src/Commands/DatabaseCommand.php +++ b/src/Commands/DatabaseCommand.php @@ -11,16 +11,15 @@ namespace BlitzPHP\Database\Commands; +use BlitzPHP\Autoloader\Autoloader; use BlitzPHP\Cli\Console\Command; -use BlitzPHP\Cli\Console\Console; +use BlitzPHP\Contracts\Autoloader\LocatorInterface; +use BlitzPHP\Contracts\Container\ContainerInterface; use BlitzPHP\Contracts\Database\ConnectionResolverInterface; use BlitzPHP\Database\Connection\BaseConnection; -use Psr\Log\LoggerInterface; -use RuntimeException; +use BlitzPHP\Database\DatabaseManager; +use BlitzPHP\Database\Migration\Runner; -/** - * @property BaseConnection $db - */ abstract class DatabaseCommand extends Command { /** @@ -28,41 +27,49 @@ abstract class DatabaseCommand extends Command */ protected string $group = 'Base de données'; - /** - * {@inheritDoc} - */ - protected string $service = 'Service de gestion de base de données'; - - private ?BaseConnection $_db = null; + protected ConnectionResolverInterface $resolver; - public function __construct(protected ConnectionResolverInterface $resolver) + public function __construct(protected ContainerInterface $container) { + $this->resolver = $container->get(ConnectionResolverInterface::class); } - public function __get($name) + protected function db(array|string|null $group = null, bool $shared = true): BaseConnection { - if (method_exists($this, $name)) { - return call_user_func([$this, $name]); - } - - return parent::__get($name); + return $this->resolver->connect($group, $shared); } - public function __set($name, $value) + /** + * Recupere les informations a utiliser pour la connexion a la base de données + * + * @return array [group, configuration] + */ + public function connectionInfo(array|string|null $group = null): array { - if (property_exists($this, $name = '_' . $name)) { - $this->{$name} = $value; - } else { - throw new RuntimeException(); - } + return $this->resolver->connectionInfo($group); } - protected function db(): BaseConnection + /** + * Recupere une instance de l'executeur de migration + */ + public function runner(string $namespace, ?string $group = null): Runner { - if (null === $this->_db) { - $this->_db = $this->resolver->connection(); - } + $namespaces = match($namespace) { + 'ALL' => array_keys($this->container->get(Autoloader::class)->getNamespace()), + default => [$namespace], + }; + + $locator = $this->container->get(LocatorInterface::class); + $files = []; - return $this->_db; + foreach ($namespaces as $namespace) { + $files[$namespace] = $locator->listNamespaceFiles($namespace, '/Database/Migrations/'); + } + + return new Runner( + $this->container->get(DatabaseManager::class), + $group, + $files, + ); } } diff --git a/src/Commands/Generators/Migration.php b/src/Commands/Generators/Migration.php index d626ccc..f44d269 100644 --- a/src/Commands/Generators/Migration.php +++ b/src/Commands/Generators/Migration.php @@ -16,7 +16,10 @@ use InvalidArgumentException; /** - * Genere un skelette de fichier de migration. + * Génère un squelette de fichier de migration. + * + * Analyse le nom de la migration pour déterminer automatiquement + * l'action (create/modify) et la table concernée. */ class Migration extends Command { @@ -25,7 +28,7 @@ class Migration extends Command /** * {@inheritDoc} */ - protected string $group = 'Generateurs'; + protected string $group = 'Générateurs'; /** * {@inheritDoc} @@ -40,28 +43,45 @@ class Migration extends Command /** * {@inheritDoc} */ - protected string $service = 'Service de génération de code'; + protected array $arguments = [ + 'name' => 'Le nom de la classe de migration (ex: create_users_table, CreateUsersTable, add_email_to_users)', + ]; /** * {@inheritDoc} */ - protected array $arguments = [ - 'name' => 'Le nom de la classe de migration.', + protected array $options = [ + '--table' => 'Force le nom de la table (optionnel - sera déduit du nom si non fourni)', + '--create' => 'Spécifie qu\'on veut créer une nouvelle table', + '--alter' => 'Spécifie qu\'on veut modifier une table existante', + '--session' => 'Génère une migration pour la table de sessions', + '--group' => 'Groupe de base de données', + '--namespace' => ['Définit le namespace de la migration', APP_NAMESPACE], + '--anonymous' => 'Générer une classe anonyme (au lieu d\'une classe nommée)', + '--suffix' => 'Ajoute "Migration" au nom de la classe (par exemple, User => UserMigration)', ]; /** - * {@inheritDoc} + * Mots-clés pour les actions de création */ - protected array $options = [ - '--table' => 'Nom de la table à utiliser.', - '--create' => 'Spécifie qu\'on veut créer une nouvelle table.', - '--modify' => 'Spécifie qu\'on veut modifier une table existante.', - '--session' => 'Génère un fichier de migration pour les sessions de la base de données.', - '--group' => 'Groupe de base de données utilisé pour les sessions de la base de données.', - '--namespace' => 'Définissez l\'espace de noms racine. Par défaut: "APP_NAMESPACE".', - '--suffix' => 'Ajouter le titre du composant au nom de la classe (par exemple, User => UserMigration).', + protected array $createKeywords = ['create', 'make', 'new', 'add']; + + /** + * Mots-clés pour les actions de modification + */ + protected array $modifyKeywords = [ + 'update', 'modify', 'alter', 'change', 'edit', + 'add', 'remove', 'drop', 'delete', 'rename', + 'add_column', 'remove_column', 'drop_column', 'rename_column', + 'add_index', 'remove_index', 'drop_index', + 'add_foreign', 'remove_foreign', 'drop_foreign', ]; + /** + * Mots-clés pour les actions de suppression + */ + protected array $dropKeywords = ['drop', 'delete', 'remove']; + /** * {@inheritDoc} */ @@ -72,69 +92,245 @@ public function handle() $this->template = 'migration.tpl.php'; $this->templatePath = __DIR__ . '/Views'; - $this->classNameLang = 'CLI.generator.className.migration'; - $this->generateClass($this->parameters()); - - return EXIT_SUCCESS; + try { + $this->generateClass($this->parameters()); + + return EXIT_SUCCESS; + } catch (InvalidArgumentException $e) { + $this->error($e->getMessage()); + + return EXIT_ERROR; + } } /** - * Préparez les options et effectuez les remplacements nécessaires. + * Prépare les options et effectue les remplacements nécessaires. */ protected function prepare(string $class): string { - $data = []; - $data['session'] = false; - $data['matchIP'] = true; // @todo a recuperer via les fichiers de configurations - - $create = $this->option('create', false); - $modify = $this->option('modify', false); + $name = $this->argument('name'); + $anonymous = $this->option('anonymous') === true; + + $parsed = $this->parseMigrationName($name); + + $create = $this->option('create'); + $alter = $this->option('alter'); + $table = $this->option('table'); + $session = $this->option('session') === true; - if ($create && $modify) { - throw new InvalidArgumentException('Impossible d\'utiliser "create" et "modify" au même moment pour la génération des migrations.'); + if ($create && $alter) { + throw new InvalidArgumentException( + 'Impossible d\'utiliser "create" et "alter" simultanément.' + ); } - if (! $create && ! $modify) { - $data['action'] = null; + if ($create) { + $action = 'create'; + $detectedTable = is_string($create) ? $create : $parsed['table']; + } elseif ($alter) { + $action = 'alter'; + $detectedTable = is_string($alter) ? $alter : $parsed['table']; } else { - $data['action'] = $create ? 'create' : 'modify'; + $action = $parsed['action']; + $detectedTable = $parsed['table']; + } + + // Si c'est une session, on force l'action à 'create' et la table par défaut + if ($session) { + $action = 'create'; + $table = $table ?: 'blitz_sessions'; + $detectedTable = $table; } - $table = $this->option('table'); - $group = $this->option('group'); + if (!$action) { + throw new InvalidArgumentException( + "Impossible de déterminer l'action à partir du nom '{$name}'.\n" . + "Utilisez --create=table ou --table=table pour spécifier explicitement." + ); + } - $data['group'] = is_string($group) ? $group : null; - $data['driver'] = config('database.' . ($data['group'] ?? 'default') . '.driver'); + $table = $this->cleanTableName($table ?: $detectedTable); - if (true === $this->option('session')) { - $data['session'] = true; - if ($data['action'] === null) { - $data['action'] = 'create'; - } + // Valider qu'on a une table (sauf pour certaines actions) + if (!$table && !in_array($action, ['drop', 'delete', 'remove'])) { + throw new InvalidArgumentException( + "Impossible de déterminer la table à partir du nom '{$name}'. \n" . + "Utilisez --table=tableName pour spécifier le nom de la table explicitement." + ); } - if (! is_string($table) || $table === '') { - if ($data['session']) { - $table = 'blitz_sessions'; - } elseif (is_string($create)) { - $table = $create; - } elseif (is_string($modify)) { - $table = $modify; - } else { - $table = null; + $group = $this->option('group'); + $matchIP = config('session.match_ip', false); + + $data = [ + 'group' => $group, + 'action' => $action, + 'table' => $table, + 'session' => $session, + 'matchIP' => $matchIP, + 'anonymous' => $anonymous, + ]; + + return $this->parseTemplate($class, [], [], $data); + } + + /** + * Analyse le nom de la migration pour en extraire l'action et la table. + * + * @return array{action: string|null, table: string|null} + */ + protected function parseMigrationName(string $name): array + { + $name = $this->normalizeName($name); + + $result = [ + 'action' => null, + 'table' => null, + ]; + + // Pattern 1: action_table (ex: create_users_table, add_email_to_users) + if (preg_match('/^(' . $this->getKeywordsPattern() . ')_(.+?)(?:_table)?$/', $name, $matches)) { + $result['action'] = $this->mapKeywordToAction($matches[1]); + $result['table'] = $this->extractTableName($matches[2]); + } + + // Pattern 2: ActionTable (ex: CreateUsersTable, AddEmailToUsers) + elseif (preg_match('/^(' . $this->getKeywordsPattern(true) . ')([A-Z][a-zA-Z0-9]+)$/', $name, $matches)) { + $result['action'] = $this->mapKeywordToAction(strtolower($matches[1])); + $result['table'] = $this->decamelize($matches[2]); + } + + // Pattern 3: table_action (ex: users_create, users_add_email) + elseif (preg_match('/^([a-z][a-z0-9_]+)_(' . $this->getKeywordsPattern() . ')(?:_(.+))?$/', $name, $matches)) { + $result['table'] = $matches[1]; + $result['action'] = $this->mapKeywordToAction($matches[2]); + + // Si c'est une modification de colonne, on garde le nom original + if (isset($matches[3])) { + $result['table'] .= ' (colonne: ' . $matches[3] . ')'; } } - $data['table'] = $table; + return $result; + } - return $this->parseTemplate($class, [], [], $data); + /** + * Nettoie le nom de la table en enlevant les préfixes/suffixes redondants et les caractères indésirables. + */ + protected function cleanTableName(?string $table): ?string + { + if ($table === null) { + return null; + } + + // Enlever les préfixes/suffixes "table" redondants + $table = preg_replace('/^(table_|tbl_)/', '', $table); + $table = preg_replace('/(_table|_tbl)$/', '', $table); + + // Enlever les caractères indésirables + $table = preg_replace('/[^a-z0-9_]/', '', $table); + + // Éviter les underscores multiples + $table = preg_replace('/_+/', '_', $table); + + $table = trim($table, '_'); + + return $table ?: null; + } + + /** + * Normalise le nom en snake_case pour l'analyse + */ + protected function normalizeName(string $name): string + { + // Convertir CamelCase en snake_case + $name = preg_replace('/(? 2 && in_array($parts[0], ['add', 'remove', 'drop'])) { + return end($parts); + } + + return $input; + } + + /** + * Obtient le pattern des mots-clés pour la regex + */ + protected function getKeywordsPattern(bool $forCamelCase = false): string + { + $allKeywords = array_merge( + $this->createKeywords, + $this->modifyKeywords, + $this->dropKeywords + ); + + $allKeywords = array_unique($allKeywords); + + if ($forCamelCase) { + // Pour CamelCase, on garde les mots tels quels + $allKeywords = array_map('ucfirst', $allKeywords); + } + + return implode('|', array_map('preg_quote', $allKeywords)); + } + + /** + * Mappe un mot-clé à une action + */ + protected function mapKeywordToAction(string $keyword): string + { + $keyword = strtolower($keyword); + + if (in_array($keyword, $this->createKeywords)) { + return 'create'; + } + + if (in_array($keyword, $this->dropKeywords)) { + return 'drop'; + } + + if (in_array($keyword, $this->modifyKeywords)) { + return 'alter'; + } + + // Par défaut, on considère comme une modification + return 'alter'; } /** - * Modifiez le nom de base du fichier avant de l'enregistrer. + * Modifie le nom de base du fichier avant de l'enregistrer. */ protected function basename(string $filename): string { - return gmdate(config('migrations.timestampFormat')) . basename($filename); + $timestamp = gmdate(config('migrations.timestampFormat', 'YmdHis_')); + + return $timestamp . $this->decamelize(basename($filename)); } } diff --git a/src/Commands/Generators/Views/migration.tpl.php b/src/Commands/Generators/Views/migration.tpl.php index 621bd2a..032ff30 100644 --- a/src/Commands/Generators/Views/migration.tpl.php +++ b/src/Commands/Generators/Views/migration.tpl.php @@ -1,57 +1,70 @@ <@php + namespace {namespace}; -use BlitzPHP\Database\Migration\Migration; - -use BlitzPHP\Database\Migration\Structure; +use BlitzPHP\Database\Migration\Migration; +use BlitzPHP\Database\Migration\Builder; + +return new class extends Migration + class {class} extends Migration -{ - - protected ?string $group = ''; - - public function up() - { - - // - - $this->('', function(Structure $table) { - - - $table->string('id', 128); - $table->ipAddress(); - $table->timestamp('timestamp'); - $table->binary('data'); - - $table->primary(['id', 'ip_address']); - - $table->primary('id'); - - $table->index('timestamp'); - - $table->id(); - $table->timestamps(); - +{ + /** + * Exécute la migration. + */ + public function up(): void + { + + $this->create('', function(Builder $table) { + $table->string('id', 128); + $table->ipAddress(); + $table->timestamp('timestamp'); + $table->binary('data'); + $table->index('timestamp'); + + $table->primary(['id', 'ip_address']); - // + $table->primary('id'); - }); + }); + + $this->create('', function(Builder $table) { + $table->id(); + $table->timestamps(); + }); + + $this->dropIfExists(''); + + $this->alter('', function(Builder $table) { + // + }); + + // } - public function down() + /** + * Annulle la migration. + */ + public function down(): void { - - // - - $this->dropIfExists(''); + + $this->dropIfExists(''); + + $this->create('', function(Builder $table) { + $table->id(); + $table->timestamps(); + }); + + $this->alter('', function(Builder $table) { + // + }); - $this->modify('', function(Structure $table) { - // - }); + // } -} +} diff --git a/src/Commands/Helper.php b/src/Commands/Helper.php deleted file mode 100644 index 2783081..0000000 --- a/src/Commands/Helper.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Database\Commands; - -use BlitzPHP\Contracts\Database\ConnectionResolverInterface; -use BlitzPHP\Database\Config\Services; -use BlitzPHP\Database\Migration\Runner; - -/** - * Aide a l'initialisation de la bd - */ -class Helper -{ - /** - * Recupere les informations a utiliser pour la connexion a la base de données - * - * @return array [group, configuration] - */ - public static function connectionInfo(array|string|null $group = null): array - { - return Services::container()->get(ConnectionResolverInterface::class)->connectionInfo($group); - } - - /** - * Recupere une instance de l'executeur de migration - */ - public static function runner(?string $group): Runner - { - [$group, $config] = self::connectionInfo($group); - - return Runner::instance(config('migrations'), $config); - } - - /** - * Recupere les fichiers de migrations dans les namespaces - */ - public static function getMigrationFiles(bool $all, ?string $namespace = null): array - { - if ($all) { - $namespaces = array_keys(Services::autoloader()->getNamespace()); - } elseif ($namespace) { - $namespaces = [$namespace]; - } else { - $namespaces = [APP_NAMESPACE]; - } - - $locator = Services::locator(); - - $files = []; - - foreach ($namespaces as $namespace) { - $files[$namespace] = $locator->listNamespaceFiles($namespace, '/Database/Migrations/'); - } - - return $files; - } -} diff --git a/src/Commands/Migration/Migrate.php b/src/Commands/Migration/Migrate.php index 66196b4..d45c28c 100644 --- a/src/Commands/Migration/Migrate.php +++ b/src/Commands/Migration/Migrate.php @@ -11,11 +11,12 @@ namespace BlitzPHP\Database\Commands\Migration; +use Ahc\Cli\Output\Color; use BlitzPHP\Database\Commands\DatabaseCommand; -use BlitzPHP\Database\Commands\Helper; +use BlitzPHP\Database\Migration\Runner; /** - * Execute toutes les nouvelles migrations. + * Exécute toutes les nouvelles migrations. */ class Migrate extends DatabaseCommand { @@ -27,15 +28,20 @@ class Migrate extends DatabaseCommand /** * {@inheritDoc} */ - protected string $description = 'Recherche et exécute toutes les nouvelles migrations dans la base de données.'; + protected string $description = 'Recherche et exécute toutes les nouvelles migrations.'; /** * {@inheritDoc} */ protected array $options = [ - '-n, --namespace' => 'Défini le namespace de la migration', - '-g, --group' => 'Défini le groupe de la base de données', - '--all' => 'Défini pour tous les namespaces, ignore l\'option (-n)', + '-g, --group' => 'Groupe de base de données à utiliser', + '-n, --namespace' => 'Namespace spécifique à migrer', + '--show-stats' => 'Afficher les statistiques de l\'opération', + '--continue-on-error' => 'Ne pas stopper le processus si une migration échoue', + '--all' => 'Migrer tous les namespaces. (Ignore l\option --namespace)', + '--pretend' => 'Simuler l\'exécution sans modifier la base (affiche les requêtes SQL)', + '--seed' => 'Exécuter les seeders après les migrations', + '-f, --force' => 'Forcer l\'exécution en production', ]; /** @@ -43,29 +49,138 @@ class Migrate extends DatabaseCommand */ public function handle() { - $this->colorize(lang('Migrations.latest'), 'yellow'); + if (on_prod() && !$this->option('force')) { + if (!$this->confirm('Êtes-vous sûr de vouloir exécuter des migrations en production ?')) { + return EXIT_SUCCESS; + } + } - $namespace = $this->option('namespace'); + $this->eol()->info('Recherche des migrations en attente...'); + $group = $this->option('group', 'default'); + $namespace = $this->option('namespace', APP_NAMESPACE); + $all = $this->option('all') === true; + $pretend = $this->option('pretend') === true; + $seed = $this->option('seed') === true; - $runner = Helper::runner($group); - - $runner->clearMessages(); - $runner->setFiles(Helper::getMigrationFiles($all = $this->option('all') === true, $namespace)); - $runner->setNamespace($all ? null : $namespace); + $runner = $this->runner($all ? 'ALL' : $namespace, $group); - if (! $runner->latest($group)) { - $this->fail(lang('Migrations.generalFault')); // @codeCoverageIgnore + if ($pretend) { + $this->pretendMode($runner); + return EXIT_SUCCESS; } - $messages = $runner->getMessages(); + $errorCount = 0; + $migrationsCount = 0; + + $runner->on('process.empty-migrations', function() { + $this->warning('Aucune migration en attente.'); + }) + ->on('process.migrations-disabled', function() { + $this->badge()->info('Les migrations sont désactivées dans la configuration.'); + }) + ->on('migration.error', function($payload) use(&$errorCount) { + ['migration' => $migration, 'exception' => $e] = $payload; + + $this->justify( + $this->getMigrationName($migration), + $this->color->error('Échec') + ); + + if (!$this->option('continue-on-error')) { + throw $e; + } + + $errorCount++; + }) + ->on('migration.ignored', function($payload) { + ['migration' => $migration] = $payload; + + $this->justify( + $this->getMigrationName($migration), + $this->color->warn('Ignoré') + ); + }) + ->on('migration.done', function($payload) use(&$migrationsCount) { + ['migration' => $migration, 'duration' => $duration] = $payload; + + $this->justify( + $this->getMigrationName($migration), + $this->color->comment($duration . ' ms') . ' ' . $this->color->ok('Exécuté') + ); + + $migrationsCount++; + }); - foreach ($messages as $message) { - $this->colorize($message['message'], $message['color']); + $executed = $runner->latest($group); + + if ($executed > 0) { + $this->newLine()->success("{$executed} migration(s) exécutée(s) avec succès."); + + if ($this->option('show-stats')) { + $this->displayStats($runner, $executed, $errorCount); + } } - $this->newLine()->success(lang('Migrations.migrated')); + if ($seed && $executed > 0) { + $this->newLine()->info('Exécution des seeders...'); + $this->call('db:seed', options: ['--group' => $group]); + } return EXIT_SUCCESS; } + + /** + * Mode simulation - affiche les requêtes SQL sans les exécuter + */ + protected function pretendMode(Runner $runner): void + { + $this->warning('Mode simulation activé - AUCUNE modification ne sera effectuée.'); + $this->newLine(); + + // TODO: Implémenter l'affichage des requêtes SQL + $this->info('Les requêtes SQL seraient affichées ici.'); + } + + /** + * Formate le nom de la migration pour l'affichage + */ + private function getMigrationName(object $migration): string + { + return sprintf( + '[%s] %s_%s', + $migration->namespace, + $migration->version, + $migration->migration + ); + } + + /** + * Affiche les statistiques d'exécution + */ + private function displayStats(Runner $runner, int $executed, int $errorCount): void + { + $batches = $runner->getLastBatch(); + $history = $runner->getHistory(); + + $options = ['sep' => '-', 'second' => ['fg' => Color::GREEN]]; + $data = [ + 'Total dans l\'historique' => $total = count($history), + 'Migrations exécutées' => $executed, + 'Migrations échouées' => $errorCount, + 'Migrations ignorées' => $total - $executed - $errorCount, + 'Dernier lot' => $batches, + 'Groupe de connexion' => $this->option('group', 'default'), + 'Namespace' => $this->option('all') ? 'Tous' : $this->option('namespace', APP_NAMESPACE), + // 'Durée totale d\'éxécution' => $duration . ' ms', + ]; + + $this->eol()->border(char: '*'); + + foreach ($data as $k => $v) { + $this->justify($k, (string) $v, $options); + } + + $this->border(char: '*'); + } } diff --git a/src/Commands/Migration/Refresh.php b/src/Commands/Migration/Refresh.php index 3780466..2fa3bd2 100644 --- a/src/Commands/Migration/Refresh.php +++ b/src/Commands/Migration/Refresh.php @@ -14,7 +14,7 @@ use BlitzPHP\Database\Commands\DatabaseCommand; /** - * Execute toutes les nouvelles migrations. + * Réinitialise et réexécute toutes les migrations. */ class Refresh extends DatabaseCommand { @@ -26,16 +26,17 @@ class Refresh extends DatabaseCommand /** * {@inheritDoc} */ - protected string $description = 'Effectue une restauration suivie d\'une migration pour actualiser l\'état actuel de la base de données.'; + protected string $description = 'Annule toutes les migrations puis les réexécute.'; /** * {@inheritDoc} */ protected array $options = [ '-n, --namespace' => 'Défini le namespace de la migration', - '-g, --group' => 'Défini le groupe de la base de données', '--all' => 'Défini pour tous les namespaces, ignore l\'option (-n)', - '-f, --force' => 'Forcer la commande - cette option vous permet de contourner la question de confirmation lors de l\'exécution de cette commande dans un environnement de production', + '-g, --group' => 'Groupe de base de données à utiliser', + '-f, --force' => 'Forcer l\'exécution en production', + '--seed' => 'Exécuter les seeders après le refresh', ]; /** @@ -43,23 +44,52 @@ class Refresh extends DatabaseCommand */ public function handle() { - $params = array_merge($this->parameters(), ['batch' => 0]); + if (on_prod() && !$this->option('force')) { + if (! $this->confirm('Êtes-vous sûr de vouloir réinitialiser toutes les migrations en production ?')) { + return EXIT_SUCCESS; + } + } - if (on_prod()) { - // @codeCoverageIgnoreStart - $force = $this->option('force'); + $this->eol()->info('Réinitialisation et réexécution des migrations...'); + + $group = $this->option('group', 'default'); + $seed = $this->option('seed') === true; - if (! $force && ! $this->confirm(lang('Migrations.refreshConfirm'))) { - return; - } + $this->newLine()->comment('Étape 1/2: Annulation de toutes les migrations'); + + $rollbackResult = $this->call('migrate:rollback', options: [ + '--group' => $group, + '--all' => true, + '--force' => true, + ]); + + if ($rollbackResult !== EXIT_SUCCESS) { + $this->error('Échec de l\'annulation des migrations.'); + + return $rollbackResult; + } + + $this->newLine()->comment('Étape 2/2: Réexécution des migrations'); + + $migrateResult = $this->call('migrate', options: [ + '--group' => $group, + ]); - $params['force'] = null; - // @codeCoverageIgnoreEnd + if ($migrateResult !== EXIT_SUCCESS) { + $this->error('Échec de l\'exécution des migrations.'); + + return $migrateResult; } - $this->call('migrate:rollback', [], $params); - $this->newLine(); - $this->call('migrate', [], $params); + if ($seed) { + $this->newLine()->info('Exécution des seeders...'); + $this->call('db:seed', options: [ + '--group' => $group, + ]); + } + + $this->newLine()->success('Refresh terminé avec succès !'); + return EXIT_SUCCESS; } diff --git a/src/Commands/Migration/Reset.php b/src/Commands/Migration/Reset.php new file mode 100644 index 0000000..3b20008 --- /dev/null +++ b/src/Commands/Migration/Reset.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Database\Commands\Migration; + +use BlitzPHP\Database\Commands\DatabaseCommand; + +/** + * Réinitialise toutes les migrations. + */ +class Reset extends DatabaseCommand +{ + /** + * {@inheritDoc} + */ + protected string $name = 'migrate:reset'; + + /** + * {@inheritDoc} + */ + protected string $description = 'Annule toutes les migrations.'; + + /** + * {@inheritDoc} + */ + protected array $options = [ + '-g, --group' => 'Groupe de base de données à utiliser', + '-f, --force' => 'Forcer l\'exécution en production', + ]; + + /** + * {@inheritDoc} + */ + public function handle() + { + if (on_prod() && !$this->option('force')) { + if (!$this->confirm('Êtes-vous sûr de vouloir réinitialiser TOUTES les migrations en production ?')) { + return EXIT_SUCCESS; + } + } + + $this->eol()->info('Réinitialisation de toutes les migrations...'); + + $group = $this->option('group', 'default'); + + $result = $this->call('migrate:rollback', [ + '--group' => $group, + '--all' => true, + '--force' => $this->option('force'), + ]); + + return $result; + } +} diff --git a/src/Commands/Migration/Rollback.php b/src/Commands/Migration/Rollback.php index 408d265..3689e6c 100644 --- a/src/Commands/Migration/Rollback.php +++ b/src/Commands/Migration/Rollback.php @@ -11,11 +11,12 @@ namespace BlitzPHP\Database\Commands\Migration; +use Ahc\Cli\Output\Color; use BlitzPHP\Database\Commands\DatabaseCommand; -use BlitzPHP\Database\Commands\Helper; +use BlitzPHP\Database\Migration\Runner; /** - * Exécute toutes les migrations dans l'ordre inverse, jusqu'à ce qu'elles aient toutes été désappliquées. + * Annule les dernières migrations. */ class Rollback extends DatabaseCommand { @@ -27,14 +28,18 @@ class Rollback extends DatabaseCommand /** * {@inheritDoc} */ - protected string $description = 'Recherche et annule toutes les migrations précédement exécutees.'; + protected string $description = 'Annule les dernières migrations.'; /** * {@inheritDoc} */ protected array $options = [ - '-b, --batch' => "Spécifiez un lot à restaurer\u{a0}; par exemple. \"3\" pour revenir au lot #3 ou \"-2\" pour revenir en arrière deux fois", - '-f, --force' => 'Forcer la commande - cette option vous permet de contourner la question de confirmation lors de l\'exécution de cette commande dans un environnement de production', + '-g, --group' => 'Groupe de base de données à utiliser', + '-b, --batch' => 'Numéro de lot cible (ex: 3 pour revenir au lot #3, -2 pour revenir de 2 lots)', + '--all' => 'Annuler toutes les migrations', + '--show-stats' => 'Afficher les statistiques de l\'opération', + '--continue-on-error' => 'Ne pas stopper le processus si une annulation échoue', + '-f, --force' => 'Forcer l\'exécution en production', ]; /** @@ -42,46 +47,110 @@ class Rollback extends DatabaseCommand */ public function handle() { - if (on_prod()) { - // @codeCoverageIgnoreStart - $force = $this->option('force'); - - if (! $force && ! $this->confirm(lang('Migrations.rollBackConfirm'))) { - return; + if (on_prod() && !$this->option('force')) { + if (!$this->confirm('Êtes-vous sûr de vouloir annuler des migrations en production ?')) { + return EXIT_SUCCESS; } - // @codeCoverageIgnoreEnd } - $runner = Helper::runner(null); - - $batch = $this->option('batch') ?? ($runner->getLastBatch() - 1); - - if (is_string($batch)) { - if (! ctype_digit($batch)) { - $this->fail('Numéro de lot invalide: ' . $batch, true); + $this->eol()->info('Recherche des migrations à annuler...'); - return EXIT_ERROR; - } + $group = $this->option('group', 'default'); + $batch = $this->option('all') ? 0 : $this->option('batch', 1); - $batch = (int) $batch; + if (is_string($batch) && !preg_match('/^-?\d+$/', $batch)) { + $this->error('Le numéro de lot doit être un entier.'); + return EXIT_ERROR; } - - $this->colorize(lang('Migrations.rollingBack') . ' ' . $batch, 'yellow'); - - $runner->setFiles(Helper::getMigrationFiles(true)); - - if (! $runner->regress($batch)) { - $this->error(lang('Migrations.generalFault')); // @codeCoverageIgnore + $batch = (int) $batch; + + $runner = $this->runner('ALL', $group); + + $rolledBack = 0; + $errorCount = 0; + + $runner->on('process.empty-migrations', function() { + $this->warning('Aucune migration à annuler'); + }) + ->on('migration.error', function($payload) use(&$errorCount) { + ['migration' => $migration, 'exception' => $e] = $payload; + + $this->justify( + $this->getMigrationName($migration), + $this->color->error('Échec') + ); + + if (!$this->option('continue-on-error')) { + throw $e; + } + + $errorCount++; + }) + ->on('migration.skipped', function($payload) { + ['migration' => $migration] = $payload; + + $this->justify( + $this->getMigrationName($migration), + $this->color->warn('Fichier introuvable') + ); + }) + ->on('migration.done', function($payload) use(&$rolledBack) { + ['migration' => $migration, 'duration' => $duration] = $payload; + + $this->justify( + $this->getMigrationName($migration), + $this->color->comment($duration . ' ms') . ' ' . $this->color->ok('Annulé') + ); + + $rolledBack++; + }); + + $runner->rollback($batch, $group); + + if ($rolledBack > 0) { + $this->newLine()->success("{$rolledBack} migration(s) annulée(s) avec succès."); + + if ($this->option('show-stats')) { + $this->displayStats($runner, $rolledBack, $errorCount, $batch); + } } - $messages = $runner->getMessages(); - - foreach ($messages as $message) { - $this->colorize($message['message'], $message['color']); - } + return EXIT_SUCCESS; + } - $this->newLine()->success('Fin de l\'annulation des migrations.'); + /** + * Formate le nom de la migration pour l'affichage + */ + private function getMigrationName(object $migration): string + { + return sprintf( + '[%s] %s_%s (batch #%d)', + $migration->namespace ?? $migration->history->namespace, + $migration->version ?? $migration->history->version, + $migration->migration ?? $migration->history->migration, + $migration->history->batch ?? '?' + ); + } - return EXIT_SUCCESS; + /** + * Affiche les statistiques + */ + private function displayStats(Runner $runner, int $rolledBack, int $errorCount, int $targetBatch): void + { + $options = ['sep' => '-', 'second' => ['fg' => Color::GREEN]]; + $data = [ + 'Migrations annulées' => $rolledBack, + 'Migrations échouées' => $errorCount, + 'Lot cible' => $targetBatch, + 'Groupe de connexion' => $this->option('group', 'default'), + ]; + + $this->eol()->border(char: '*'); + + foreach ($data as $k => $v) { + $this->justify($k, (string) $v, $options); + } + + $this->border(char: '*'); } } diff --git a/src/Commands/Migration/Status.php b/src/Commands/Migration/Status.php index 61cb507..4cb739d 100644 --- a/src/Commands/Migration/Status.php +++ b/src/Commands/Migration/Status.php @@ -11,11 +11,11 @@ namespace BlitzPHP\Database\Commands\Migration; +use Ahc\Cli\Output\Color; use BlitzPHP\Database\Commands\DatabaseCommand; -use BlitzPHP\Database\Commands\Helper; /** - * Execute toutes les nouvelles migrations. + * Affiche le statut des migrations. */ class Status extends DatabaseCommand { @@ -27,27 +27,13 @@ class Status extends DatabaseCommand /** * {@inheritDoc} */ - protected string $description = 'Affiche une liste de toutes les migrations et indique si elles ont été exécutées ou non.'; + protected string $description = 'Affiche le statut de toutes les migrations.'; /** * {@inheritDoc} */ protected array $options = [ - '-g, --group' => 'Défini le groupe de la base de données', - ]; - - /** - * Namespaces à ignorer quand on regarde les migrations. - * - * @var list - */ - protected array $ignoredNamespaces = [ - 'BlitzPHP', - 'Config', - 'Kint', - 'Laminas\ZendFrameworkBridge', - 'Laminas\Escaper', - 'Psr\Log', + '-g, --group' => 'Groupe de base de données à utiliser', ]; /** @@ -55,76 +41,71 @@ class Status extends DatabaseCommand */ public function handle() { - $group = $this->option('group', 'default'); - - $runner = Helper::runner($group); - - // Collection des statuts de migrations - $status = []; - - foreach (Helper::getMigrationFiles(true) as $namespace => $files) { - if (! on_test()) { - // Rendre Tests\\Support détectable pour les tests - $this->ignoredNamespaces[] = 'Tests\Support'; // @codeCoverageIgnore - } - - if (in_array($namespace, $this->ignoredNamespaces, true)) { - continue; - } - - if (APP_NAMESPACE !== 'App' && $namespace === 'App') { - continue; // @codeCoverageIgnore - } - - $migrations = $runner->findNamespaceMigrations($namespace, $files); - - if (empty($migrations)) { - continue; - } - - $history = $runner->getHistory($group); - ksort($migrations); + $this->eol()->info('Récupération du statut des migrations...'); - foreach ($migrations as $uid => $migration) { - $migrations[$uid]->name = mb_substr($migration->name, mb_strpos($migration->name, $uid . '_')); - - $date = '---'; - $group = '---'; - $batch = '---'; - - foreach ($history as $row) { - // @codeCoverageIgnoreStart - if ($runner->getObjectUid($row) !== $migration->uid) { - continue; - } - - $date = date('Y-m-d H:i:s', $row->time); - $group = $row->group; - $batch = $row->batch; - // @codeCoverageIgnoreEnd - } + $group = $this->option('group', 'default'); + $runner = $this->runner('ALL', $group); + + $history = $runner->getHistory($group); + $files = $runner->findMigrationFiles(); + + if (empty($files)) { + $this->warning('Aucun fichier de migration trouvé.'); + return EXIT_SUCCESS; + } + + $executedMap = []; + foreach ($history as $item) { + $key = $item->migration . '_' . $item->version; + $executedMap[$key] = $item; + } - $status[] = [ - 'namespace' => $namespace, - 'version' => $migration->version, - 'nom' => $migration->name, - 'groupe' => $group, - 'migré le' => $date, - 'batch' => $batch, - ]; - } + $tbody = []; + + foreach ($files as $file) { + $key = $file->migration . '_' . $file->version; + $executed = isset($executedMap[$key]); + + $date = $executed ? date('Y-m-d H:i', $executedMap[$key]->time) : '---'; + $batch = $executed ? $executedMap[$key]->batch : '---'; + $status = $executed + ? $this->color->ok('EXÉCUTÉE') + : $this->color->warn('EN ATTENTE'); + + $tbody[] = [ + $this->getMigrationName($file), + $date, + $batch, + $status, + ]; } - if (! $status) { - // @codeCoverageIgnoreStart - $this->error(lang('Migrations.noneFound'))->newLine(); + $this->table(['MIGRATION', 'EXÉCUTÉE', 'LOT', 'STATUT'], $tbody); - return; - // @codeCoverageIgnoreEnd - } + // Statistiques + $total = count($files); + $executedCount = count($history); + $pendingCount = $total - $executedCount; - $this->table($status, ['head' => 'boldYellow']); + $this->newLine()->info('RÉSUMÉ'); + $this->justify('Total migrations', (string) $total); + $this->justify('Exécutées', (string) $executedCount, ['second' => ['fg' => Color::GREEN]]); + $this->justify('En attente', (string) $pendingCount, ['second' => $pendingCount > 0 ? ['fg' => Color::YELLOW] : []]); + $this->justify('Dernier lot', (string) $runner->getLastBatch()); return EXIT_SUCCESS; } + + /** + * Formate le nom de la migration pour l'affichage + */ + private function getMigrationName(object $migration): string + { + return sprintf( + '[%s] %s_%s', + $migration->namespace, + $migration->version, + $migration->migration + ); + } } diff --git a/src/Connection/BaseConnection.php b/src/Connection/BaseConnection.php index 0a653de..ec43515 100644 --- a/src/Connection/BaseConnection.php +++ b/src/Connection/BaseConnection.php @@ -29,7 +29,14 @@ use Throwable; /** - * Connexion de base à la base de données (PDO uniquement) + * Connexion de base à la base de données + * + * @method bool tableExists(string $name) Vérifie si une table existe + * @method array getColumnNames(string $table) Retourne les noms des champs d'une table + * @method bool columnExists(string $column, string $table) Vérifie si un champ existe dans une table + * @method array getColumnData(string $table) Retourne les métadonnées des champs d'une table + * @method array getIndexData(string $table) Retourne les métadonnées des index d'une table + * @method array getForeignKeyData(string $table) Retourne les métadonnées des clés étrangères d'une table */ abstract class BaseConnection implements ConnectionInterface { @@ -67,14 +74,14 @@ abstract class BaseConnection implements ConnectionInterface /** * Gestionnaire de métadonnées */ - protected MetadataCollector $metadata; + protected ?MetadataCollector $metadata = null; protected array $proxyMethods = [ 'listTables', 'tableExists', - 'getFieldNames', - 'fieldExists', - 'getFieldData', + 'getColumnNames', + 'columnExists', + 'getColumnData', 'getIndexData', 'getForeignKeyData', 'resetDataCache' => 'clearCache', diff --git a/src/Connection/MetadataCollector.php b/src/Connection/MetadataCollector.php index 81e7b4c..95d5f5e 100644 --- a/src/Connection/MetadataCollector.php +++ b/src/Connection/MetadataCollector.php @@ -57,7 +57,7 @@ public function clearCache(): self */ public function listTables(bool $constrainByPrefix = false): array { - if ($this->cache['table'] !== []) { + if ($this->cache['tables'] !== []) { return $this->filterTables($this->cache['tables'], $constrainByPrefix); } diff --git a/src/Creator/BaseCreator.php b/src/Creator/BaseCreator.php index abdb6aa..d5c0f7c 100644 --- a/src/Creator/BaseCreator.php +++ b/src/Creator/BaseCreator.php @@ -170,6 +170,11 @@ class BaseCreator */ protected array $mapTypes = []; + /** + * Ancienes donnees pour les raisons de performance. + */ + protected array $dataCache = []; + /** * Constructor. */ @@ -253,8 +258,8 @@ public function createDatabase(string $dbName, bool $ifNotExists = false): bool // @codeCoverageIgnoreEnd } - if (! empty($this->db->dataCache['db_names'])) { - $this->db->dataCache['db_names'][] = $dbName; + if (! empty($this->dataCache['db_names'])) { + $this->dataCache['db_names'][] = $dbName; } return true; @@ -308,10 +313,10 @@ public function dropDatabase(string $dbName): bool return false; } - if (! empty($this->db->dataCache['db_names'])) { - $key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true); + if (! empty($this->dataCache['db_names'])) { + $key = array_search(strtolower($dbName), array_map('strtolower', $this->dataCache['db_names']), true); if ($key !== false) { - unset($this->db->dataCache['db_names'][$key]); + unset($this->dataCache['db_names'][$key]); } } @@ -431,7 +436,7 @@ public function addForeignKey(array|string $fieldName = '', string $tableName = public function dropKey(string $table, string $keyName, bool $prefixKeyName = true): bool { $keyName = $this->db->escapeIdentifiers(($prefixKeyName === true ? $this->db->prefix : '') . $keyName); - $table = $this->db->escapeIdentifiers($this->db->prefix . $table); + $table = $this->db->prefixTable($table); $dropKeyAsConstraint = $this->dropKeyAsConstraint($table, $keyName); if ($dropKeyAsConstraint === true) { @@ -488,7 +493,7 @@ public function dropPrimaryKey(string $table, string $keyName = ''): bool { $sql = sprintf( 'ALTER TABLE %s DROP CONSTRAINT %s', - $this->db->escapeIdentifiers($this->db->prefix . $table), + $this->db->prefixTable($table), ($keyName === '') ? $this->db->escapeIdentifiers('pk_' . $this->db->prefix . $table) : $this->db->escapeIdentifiers($keyName), ); @@ -504,7 +509,7 @@ public function dropForeignKey(string $table, string $foreignName) { $sql = sprintf( (string) $this->dropConstraintStr, - $this->db->escapeIdentifiers($this->db->prefix . $table), + $this->db->prefixTable($table), $this->db->escapeIdentifiers($foreignName) ); @@ -530,7 +535,7 @@ public function createTable(string $table, bool $ifNotExists = false, array $att throw new InvalidArgumentException('Un nom de table est nécessaire pour cette opération.'); } - $table = $this->db->prefix . $table; + $table = $this->db->prefixTable($table); if ($this->fields === []) { throw new RuntimeException('Des informations sur le champ sont requises.'); @@ -546,8 +551,8 @@ public function createTable(string $table, bool $ifNotExists = false, array $att $sql = $this->_createTable($table, $attributes); if (($result = $this->db->query($sql)) !== false) { - if (isset($this->db->dataCache['table_names']) && ! in_array($table, $this->db->dataCache['table_names'], true)) { - $this->db->dataCache['table_names'][] = $table; + if (isset($this->dataCache['table_names']) && ! in_array($table, $this->dataCache['table_names'], true)) { + $this->dataCache['table_names'][] = $table; } // La plupart des bases de données ne permettent pas de créer des index à partir de l'instruction CREATE TABLE @@ -622,11 +627,12 @@ public function dropTable(string $tableName, bool $ifExists = false, bool $casca return false; } - if ($this->db->prefix !== '' && str_starts_with($tableName, $this->db->prefix)) { - $tableName = substr($tableName, strlen($this->db->prefix)); + $prefix = $this->db->getPrefix(); + if ($prefix !== '' && str_starts_with($tableName, $prefix)) { + $tableName = substr($tableName, strlen($prefix)); } - if (($query = $this->_dropTable($this->db->prefix . $tableName, $ifExists, $cascade)) === true) { + if (($query = $this->_dropTable($prefix . $tableName, $ifExists, $cascade)) === true) { return true; } @@ -636,15 +642,15 @@ public function dropTable(string $tableName, bool $ifExists = false, bool $casca $this->db->enableForeignKeyChecks(); - if ($query && ! empty($this->db->dataCache['table_names'])) { + if ($query && ! empty($this->dataCache['table_names'])) { $key = array_search( - strtolower($this->db->prefix . $tableName), - array_map('strtolower', $this->db->dataCache['table_names']), + strtolower($prefix . $tableName), + array_map('strtolower', $this->dataCache['table_names']), true ); if ($key !== false) { - unset($this->db->dataCache['table_names'][$key]); + unset($this->dataCache['table_names'][$key]); } } @@ -694,19 +700,19 @@ public function renameTable(string $tableName, string $newTableName) $result = $this->db->query(sprintf( $this->renameTableStr, - $this->db->escapeIdentifiers($this->db->prefix . $tableName), - $this->db->escapeIdentifiers($this->db->prefix . $newTableName) + $this->db->prefixTable($tableName), + $this->db->prefixTable($newTableName) )); - if ($result && ! empty($this->db->dataCache['table_names'])) { + if ($result && ! empty($this->dataCache['table_names'])) { $key = array_search( strtolower($this->db->prefix . $tableName), - array_map('strtolower', $this->db->dataCache['table_names']), + array_map('strtolower', $this->dataCache['table_names']), true ); if ($key !== false) { - $this->db->dataCache['table_names'][$key] = $this->db->prefix . $newTableName; + $this->dataCache['table_names'][$key] = $this->db->prefix . $newTableName; } } diff --git a/src/DatabaseManager.php b/src/DatabaseManager.php index 75606ce..d27e588 100644 --- a/src/DatabaseManager.php +++ b/src/DatabaseManager.php @@ -42,6 +42,13 @@ class DatabaseManager implements ConnectionResolverInterface */ protected array $connections = []; + /** + * Stack de connexions actives + * + * @var list + */ + protected array $stack = []; + /** * Constructeur * @@ -84,6 +91,8 @@ public function connect($group = null, bool $shared = true): ConnectionInterface $this->connections[$groupName] = $connection; } + $this->stack[] = $groupName; + return $connection; } @@ -134,16 +143,18 @@ public function connectionInfo(array|string|null $group = null): array /** * Crée un builder pour une connexion */ - public function builder(ConnectionInterface $db): BaseBuilder + public function builder(?ConnectionInterface $db = null): BaseBuilder { - return new BaseBuilder($db); + return new BaseBuilder($db ?? $this->activeConnection()); } /** * Crée un creator pour une connexion */ - public function creator(ConnectionInterface $db): BaseCreator + public function creator(?ConnectionInterface $db = null): BaseCreator { + $db = $db ?? $this->activeConnection(); + $driver = $this->normalizeDriver($db->getDriver()); $className = "BlitzPHP\\Database\\Creator\\{$driver}"; @@ -174,6 +185,36 @@ public function getConnections(): array return $this->connections; } + /** + * Retourne les noms de tous les groupes de connexion configurés + * + * @return list + */ + public function getConnectionNames(): array + { + return array_keys($this->connections); + } + + /** + * Récupère une connexion par son nom + */ + public function getConnection(string $name): ?ConnectionInterface + { + return $this->connections[$name] ?? null; + } + + /** + * Récupère la connexion active (dernière utilisée) + */ + public function activeConnection(): ?ConnectionInterface + { + if ($this->stack === []) { + $this->connection(); + } + + return $this->connections[$this->stack[count($this->stack) - 1]] ?? null; + } + /** * Ferme toutes les connexions */ diff --git a/src/Exceptions/CreatorException.php b/src/Exceptions/CreatorException.php new file mode 100644 index 0000000..1465897 --- /dev/null +++ b/src/Exceptions/CreatorException.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Database\Exceptions; + +/** + * Exception pour la couche Creator + */ +class CreatorException extends DatabaseException +{ + public static function unsupportedFeature(string $feature): self + { + return new static(static::t( + 'La fonctionnalité "%s" n\'est pas supportée par ce pilote de base de données.', + [$feature] + )); + } + + public static function missingFieldDefinition(string $table): self + { + return new static(static::t( + 'Aucun champ défini pour la table "%s".', + [$table] + )); + } + + public static function invalidFieldType(string $type): self + { + return new static(static::t( + 'Type de champ invalide : "%s".', + [$type] + )); + } +} diff --git a/src/Listeners/DatabaseListener.php b/src/Listeners/DatabaseListener.php index 28ca286..459f6a8 100644 --- a/src/Listeners/DatabaseListener.php +++ b/src/Listeners/DatabaseListener.php @@ -32,11 +32,11 @@ public function listen(EventManagerInterface $event): void private function addInfoToAboutCommand() { - if (! class_exists(\BlitzPHP\Cli\Commands\Utilities\About::class)) { + if (! class_exists(\BlitzPHP\Cli\Commands\Config\About::class)) { return; } - \BlitzPHP\Cli\Commands\Utilities\About::add('Gestionnaires', static fn (ConnectionResolverInterface $connectionResolver) => array_filter([ + \BlitzPHP\Cli\Commands\Config\About::add('Gestionnaires', static fn (ConnectionResolverInterface $connectionResolver) => array_filter([ 'Base de données' => static function () use ($connectionResolver) { [$group, $config] = $connectionResolver->connectionInfo(); diff --git a/src/Migration/Builder.php b/src/Migration/Builder.php new file mode 100644 index 0000000..a8b9f4d --- /dev/null +++ b/src/Migration/Builder.php @@ -0,0 +1,1385 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Database\Migration; + +use BlitzPHP\Database\Connection\BaseConnection; +use BlitzPHP\Database\Migration\Definitions\Column; +use BlitzPHP\Database\Migration\Definitions\ForeignId; +use BlitzPHP\Database\Migration\Definitions\ForeignKey; +use BlitzPHP\Database\Migration\Definitions\Index; +use BlitzPHP\Database\Query\Expression; +use BlitzPHP\Traits\Macroable; +use Closure; + +/** + * Constructeur de définition de table + * + * Cette classe permet de définir la structure d'une table de manière fluide. + * Elle est utilisée par les migrations pour décrire les modifications à apporter. + * + * @credit Laravel Framework - Illuminate\Database\Schema\Blueprint + */ +class Builder +{ + use Macroable; + + /** + * Type d'action (create, alter, drop, rename) + */ + protected string $action = 'create'; + + /** + * Liste des colonnes à ajouter/modifier + * + * @var array + */ + protected array $columns = []; + + /** + * Liste des index à ajouter + * + * @var array + */ + protected array $indexes = []; + + /** + * Liste des clés étrangères à ajouter + * + * @var array + */ + protected array $foreignKeys = []; + + /** + * Connexion utilisée pour ce builder + */ + protected BaseConnection $db; + + /** + * Éléments à supprimer + * + * @var array{ + * columns?: array, + * primary?: string, + * unique?: array, + * index?: array, + * foreign?: array + * } + */ + protected array $drops = []; + + /** + * Informations de renommage + * + * @var array{to?: string} + */ + protected array $renames = []; + + /** + * Moteur de stockage a utiliser sur la table. + */ + public string $engine = ''; + + /** + * Charset par défaut + */ + public string $charset = ''; + + /** + * Collation par defaut. + */ + public string $collation = ''; + + /** + * Commentaire de la table + */ + public string $comment = ''; + + /** + * Doit-on creer une table temporaire. + */ + public bool $temporary = false; + + /** + * La colonne après laquelle de nouvelles colonnes seront ajoutées. + */ + public ?string $after = null; + + /** + * Constructeur + * + * @param string $table Nom de la table + * @param string $prefix Préfixe de la table + */ + public function __construct(protected string $table, protected string $prefix = '') + { + } + + /** + * Spécifie la connexion utilisée pour ce builder + * + * @internal + */ + public function setConnection(BaseConnection $db): self + { + $this->db = $db; + + return $this; + } + + /** + * Récupère l'instance de la connexion utilisée par ce builder + * + * @internal + */ + public function getConnection(): BaseConnection + { + return $this->db; + } + + /* + |-------------------------------------------------------------------------- + | Actions + |-------------------------------------------------------------------------- + */ + + /** + * Indique que la table doit être créée + * + * @internal + */ + public function createTable(bool $ifNotExists = false): void + { + $this->action = $ifNotExists ? 'createIfNotExists' : 'create'; + } + + /** + * Indique que la table doit être modifiée + * + * @internal + */ + public function alterTable(): void + { + $this->action = 'alter'; + } + + /** + * Indique que la table doit être supprimée + * + * @internal + */ + public function dropTable(bool $ifExists = false): void + { + $this->action = $ifExists ? 'dropIfExists' : 'drop'; + } + + /** + * Indique que la table doit être renommée + * + * @internal + */ + public function renameTable(string $to): void + { + $this->action = 'rename'; + + $this->renames['to'] = $to; + } + + /** + * Définit le moteur de stockage pour la table + */ + public function engine(string $engine): static + { + $this->engine = $engine; + + return $this; + } + + /** + * Spécifie que le moteur InnoDB doit être utilisé (MySQL uniquement) + */ + public function innoDb(): static + { + return $this->engine('InnoDB'); + } + + /** + * Définit le jeu de caractères pour la table + */ + public function charset(string $charset): static + { + $this->charset = $charset; + + return $this; + } + + /** + * Définit la collation pour la table + */ + public function collation(string $collation): static + { + $this->collation = $collation; + + return $this; + } + + /** + * Indique que la table doit être temporaire + */ + public function temporary(): static + { + $this->temporary = true; + + return $this; + } + + /** + * Ajoute un commentaire à la table + */ + public function comment(string $comment): static + { + $this->comment = $comment; + + return $this; + } + + /** + * Ajoute les colonnes après la colonne spécifiée + */ + public function after(string $column, Closure $callback): static + { + $this->after = $column; + $callback($this); + $this->after = null; + + return $this; + } + + /* + |-------------------------------------------------------------------------- + | Types de colonnes + |-------------------------------------------------------------------------- + */ + + /** + * Ajoute une colonne auto-incrémentée (alias de bigIncrements) + */ + public function id(string $column = 'id'): Column + { + return $this->bigIncrements($column); + } + + /** + * Ajoute une colonne INTEGER auto-incrémentée + */ + public function increments(string $column): Column + { + return $this->unsignedInteger($column, true); + } + + /** + * Ajoute une colonne INTEGER auto-incrémentée + */ + public function integerIncrements(string $column): Column + { + return $this->unsignedInteger($column, true); + } + + /** + * Ajoute une colonne TINYINT auto-incrémentée (1 octet) + */ + public function tinyIncrements(string $column): Column + { + return $this->unsignedTinyInteger($column, true); + } + + /** + * Ajoute une colonne SMALLINT auto-incrémentée (2 octets) + */ + public function smallIncrements(string $column): Column + { + return $this->unsignedSmallInteger($column, true); + } + + /** + * Ajoute une colonne MEDIUMINT auto-incrémentée (3 octets) + */ + public function mediumIncrements(string $column): Column + { + return $this->unsignedMediumInteger($column, true); + } + + /** + * Ajoute une colonne BIGINT auto-incrémentée + */ + public function bigIncrements(string $column): Column + { + return $this->unsignedBigInteger($column, true); + } + + /** + * Ajoute une colonne CHAR + */ + public function char(string $column, ?int $length = null): Column + { + $length = max($length ?: Runner::$defaultStringLength, 1); + + return $this->addColumn('char', $column, ['length' => $length]); + } + + /** + * Ajoute une colonne de type chaîne de caractères + */ + public function string(string $column, ?int $length = null): Column + { + $length = max($length ?: Runner::$defaultStringLength, 1); + + return $this->addColumn('string', $column, ['length' => $length]); + } + + /** + * Ajoute une colonne TINYTEXT + */ + public function tinyText(string $column): Column + { + return $this->addColumn('tinyText', $column); + } + + /** + * Ajoute une colonne de type texte + */ + public function text(string $column): Column + { + return $this->addColumn('text', $column); + } + + /** + * Ajoute une colonne MEDIUMTEXT + */ + public function mediumText(string $column): Column + { + return $this->addColumn('mediumText', $column); + } + + /** + * Ajoute une colonne LONGTEXT + */ + public function longText(string $column): Column + { + return $this->addColumn('longText', $column); + } + + /** + * Ajoute une colonne de type entier + */ + public function integer(string $column, bool $autoIncrement = false, bool $unsigned = false): Column + { + return $this->addColumn('integer', $column, compact('autoIncrement', 'unsigned')); + } + + /** + * Ajoute une colonne TINYINT + */ + public function tinyInteger(string $column, bool $autoIncrement = false, bool $unsigned = false): Column + { + return $this->addColumn('tinyInteger', $column, compact('autoIncrement', 'unsigned')); + } + + /** + * Ajoute une colonne SMALLINT + */ + public function smallInteger(string $column, bool $autoIncrement = false, bool $unsigned = false): Column + { + return $this->addColumn('smallInteger', $column, compact('autoIncrement', 'unsigned')); + } + + /** + * Ajoute une colonne MEDIUMINT + */ + public function mediumInteger(string $column, bool $autoIncrement = false, bool $unsigned = false): Column + { + return $this->addColumn('mediumInteger', $column, compact('autoIncrement', 'unsigned')); + } + + /** + * Ajoute une colonne de type entier long + */ + public function bigInteger(string $column, bool $autoIncrement = false, bool $unsigned = false): Column + { + return $this->addColumn('bigInteger', $column, compact('autoIncrement', 'unsigned')); + } + + /** + * Ajoute une colonne INTEGER non signée + */ + public function unsignedInteger(string $column, bool $autoIncrement = false): Column + { + return $this->integer($column, $autoIncrement, true); + } + + /** + * Ajoute une colonne TINYINT non signée + */ + public function unsignedTinyInteger(string $column, bool $autoIncrement = false): Column + { + return $this->tinyInteger($column, $autoIncrement, true); + } + + /** + * Ajoute une colonne SMALLINT non signée + */ + public function unsignedSmallInteger(string $column, bool $autoIncrement = false): Column + { + return $this->smallInteger($column, $autoIncrement, true); + } + + /** + * Ajoute une colonne MEDIUMINT non signée + */ + public function unsignedMediumInteger(string $column, bool $autoIncrement = false): Column + { + return $this->mediumInteger($column, $autoIncrement, true); + } + + /** + * Ajoute une colonne BIGINT non signée + */ + public function unsignedBigInteger(string $column, bool $autoIncrement = false): Column + { + return $this->bigInteger($column, $autoIncrement, true); + } + + /** + * Ajoute une colonne de clé étrangère (BIGINT non signé) + */ + public function foreignId(string $column): ForeignId + { + return $this->addColumnDefinition(new ForeignId($this, [ + 'type' => 'bigInteger', + 'name' => $column, + 'autoIncrement' => false, + 'unsigned' => true, + ])); + } + + /** + * Ajoute une colonne de clé étrangère pour le modèle donné + * + * @param string $model Classe du modèle + * @param string|null $column Nom de la colonne + */ + public function foreignIdFor(string $model, ?string $column = null): ForeignId + { + // Par défaut, on utilise une BIGINT non signée + // Dans une implémentation réelle, on détecterait le type de clé du modèle + return $this->foreignId($column ?? strtolower(basename(str_replace('\\', '/', $model))) . '_id'); + } + + /** + * Ajoute une colonne de type nombre à virgule flottante + */ + public function float(string $column, int $total = 8, int $places = 2, bool $unsigned = false): Column + { + return $this->addColumn('float', $column, compact('total', 'places', 'unsigned')); + } + + /** + * Ajoute une colonne de type DOUBLE + */ + public function double(string $column, ?int $total = null, ?int $places = null, bool $unsigned = false): Column + { + return $this->addColumn('double', $column, compact('total', 'places', 'unsigned')); + } + + /** + * Ajoute une colonne de type nombre décimal + */ + public function decimal(string $column, int $total = 8, int $places = 2, bool $unsigned = false): Column + { + return $this->addColumn('decimal', $column, compact('total', 'places', 'unsigned')); + } + + /** + * Ajoute une colonne FLOAT non signée + */ + public function unsignedFloat(string $column, int $total = 8, int $places = 2): Column + { + return $this->float($column, $total, $places, true); + } + + /** + * Ajoute une colonne DOUBLE non signée + */ + public function unsignedDouble(string $column, ?int $total = null, ?int $places = null): Column + { + return $this->double($column, $total, $places, true); + } + + /** + * Ajoute une colonne DECIMAL non signée + */ + public function unsignedDecimal(string $column, int $total = 8, int $places = 2): Column + { + return $this->decimal($column, $total, $places, true); + } + + /** + * Ajoute une colonne de type booléen + */ + public function boolean(string $column): Column + { + return $this->addColumn('boolean', $column); + } + + /** + * Ajoute une colonne de type ENUM + */ + public function enum(string $column, array $allowed): Column + { + return $this->addColumn('enum', $column, ['allowed' => $allowed]); + } + + /** + * Ajoute une colonne de type SET + */ + public function set(string $column, array $allowed): Column + { + return $this->addColumn('set', $column, ['allowed' => $allowed]); + } + + /** + * Ajoute une colonne de type JSON + */ + public function json(string $column): Column + { + return $this->addColumn('json', $column); + } + + /** + * Ajoute une colonne de type JSON binaire + */ + public function jsonb(string $column): Column + { + return $this->addColumn('jsonb', $column); + } + + /** + * Ajoute une colonne de type date + */ + public function date(string $column): Column + { + return $this->addColumn('date', $column); + } + + /** + * Ajoute une colonne de type date et heure + */ + public function dateTime(string $column, int $precision = 0): Column + { + return $this->addColumn('dateTime', $column, ['precision' => $precision]); + } + + /** + * Ajoute une colonne DATETIME (avec fuseau horaire) + */ + public function dateTimeTz(string $column, int $precision = 0): Column + { + return $this->addColumn('dateTimeTz', $column, ['precision' => $precision]); + } + + /** + * Ajoute une colonne de type time + */ + public function time(string $column, int $precision = 0): Column + { + return $this->addColumn('time', $column, ['precision' => $precision]); + } + + /** + * Ajoute une colonne TIME (avec fuseau horaire) + */ + public function timeTz(string $column, int $precision = 0): Column + { + return $this->addColumn('timeTz', $column, ['precision' => $precision]); + } + + /** + * Ajoute une colonne de type timestamp + */ + public function timestamp(string $column, int $precision = 0): Column + { + return $this->addColumn('timestamp', $column, ['precision' => $precision]); + } + + /** + * Ajoute une colonne TIMESTAMP (avec fuseau horaire) + */ + public function timestampTz(string $column, int $precision = 0): Column + { + return $this->addColumn('timestampTz', $column, ['precision' => $precision]); + } + + /** + * Ajoute les colonnes created_at et updated_at + */ + public function timestamps(int $precision = 0): void + { + $this->timestamp('created_at', $precision)->nullable(); + $this->timestamp('updated_at', $precision)->nullable(); + } + + /** + * Ajoute les colonnes timestamps nullables + */ + public function nullableTimestamps(int $precision = 0): void + { + $this->timestamps($precision); + } + + /** + * Ajoute les colonnes TIMESTAMP avec fuseau horaire + */ + public function timestampsTz(int $precision = 0): void + { + $this->timestampTz('created_at', $precision)->nullable(); + $this->timestampTz('updated_at', $precision)->nullable(); + } + + /** + * Ajoute les colonnes DATETIME + */ + public function datetimes(int $precision = 0): void + { + $this->datetime('created_at', $precision)->nullable(); + $this->datetime('updated_at', $precision)->nullable(); + } + + /** + * Ajoute une colonne de suppression logique (soft delete) + */ + public function softDeletes(string $column = 'deleted_at', int $precision = 0): Column + { + return $this->timestamp($column, $precision)->nullable(); + } + + /** + * Ajoute une colonne de suppression logique avec fuseau horaire + */ + public function softDeletesTz(string $column = 'deleted_at', int $precision = 0): Column + { + return $this->timestampTz($column, $precision)->nullable(); + } + + /** + * Ajoute une colonne de suppression logique DATETIME + */ + public function softDeletesDatetime(string $column = 'deleted_at', int $precision = 0): Column + { + return $this->datetime($column, $precision)->nullable(); + } + + /** + * Ajoute une colonne YEAR + */ + public function year(string $column): Column + { + return $this->addColumn('year', $column); + } + + /** + * Ajoute une colonne binaire + */ + public function binary(string $column): Column + { + return $this->addColumn('binary', $column); + } + + /** + * Ajoute une colonne de type UUID + */ + public function uuid(string $column): Column + { + return $this->addColumn('uuid', $column); + } + + /** + * Ajoute une colonne UUID avec contrainte de clé étrangère + */ + public function foreignUuid(string $column): ForeignId + { + return $this->addColumnDefinition(new ForeignId($this, [ + 'type' => 'uuid', + 'name' => $column, + ])); + } + + /** + * Ajoute une colonne ULID (Universally Unique Lexicographically Sortable Identifier) + */ + public function ulid(string $column = 'ulid', int $length = 26): Column + { + return $this->char($column, $length); + } + + /** + * Ajoute une colonne ULID avec contrainte de clé étrangère + */ + public function foreignUlid(string $column, int $length = 26): ForeignId + { + return $this->addColumnDefinition(new ForeignId($this, [ + 'type' => 'char', + 'name' => $column, + 'length' => $length, + ])); + } + + /** + * Ajoute une colonne d'adresse IP + */ + public function ipAddress(string $column = 'ip_address'): Column + { + return $this->addColumn('ipAddress', $column); + } + + /** + * Ajoute une colonne d'adresse MAC + */ + public function macAddress(string $column = 'mac_address'): Column + { + return $this->addColumn('macAddress', $column); + } + + /** + * Ajoute une colonne de type geometry + */ + public function geometry(string $column): Column + { + return $this->addColumn('geometry', $column); + } + + /** + * Ajoute une colonne de type point + */ + public function point(string $column, ?int $srid = null): Column + { + return $this->addColumn('point', $column, compact('srid')); + } + + /** + * Ajoute une colonne de type linestring + */ + public function lineString(string $column): Column + { + return $this->addColumn('linestring', $column); + } + + /** + * Ajoute une colonne de type polygon + */ + public function polygon(string $column): Column + { + return $this->addColumn('polygon', $column); + } + + /** + * Ajoute une colonne de type geometrycollection + */ + public function geometryCollection(string $column): Column + { + return $this->addColumn('geometrycollection', $column); + } + + /** + * Ajoute une colonne de type multipoint + */ + public function multiPoint(string $column): Column + { + return $this->addColumn('multipoint', $column); + } + + /** + * Ajoute une colonne de type multilinestring + */ + public function multiLineString(string $column): Column + { + return $this->addColumn('multilinestring', $column); + } + + /** + * Ajoute une colonne de type multipolygon + */ + public function multiPolygon(string $column): Column + { + return $this->addColumn('multipolygon', $column); + } + + /** + * Ajoute une colonne de type multipolygon Z + */ + public function multiPolygonZ(string $column): Column + { + return $this->addColumn('multipolygonz', $column); + } + + /** + * Ajoute une colonne calculée (generated/computed) + */ + public function computed(string $column, string $expression): Column + { + return $this->addColumn('computed', $column, compact('expression')); + } + + /** + * Ajoute la colonne remember_token + */ + public function rememberToken(): Column + { + return $this->string('remember_token', 100)->nullable(); + } + + /* + |-------------------------------------------------------------------------- + | Méthodes pour les tables polymorphiques + |-------------------------------------------------------------------------- + */ + + /** + * Ajoute les colonnes pour une table polymorphique + */ + public function morphs(string $name, ?string $indexName = null): void + { + if (Runner::$defaultMorphKeyType === 'uuid') { + $this->uuidMorphs($name, $indexName); + } elseif (Runner::$defaultMorphKeyType === 'ulid') { + $this->ulidMorphs($name, $indexName); + } else { + $this->numericMorphs($name, $indexName); + } + } + + /** + * Ajoute les colonnes nullables pour une table polymorphique + */ + public function nullableMorphs(string $name, ?string $indexName = null): void + { + if (Runner::$defaultMorphKeyType === 'uuid') { + $this->nullableUuidMorphs($name, $indexName); + } elseif (Runner::$defaultMorphKeyType === 'ulid') { + $this->nullableUlidMorphs($name, $indexName); + } else { + $this->nullableNumericMorphs($name, $indexName); + } + } + + /** + * Ajoute les colonnes pour une table polymorphique avec IDs numériques + */ + public function numericMorphs(string $name, ?string $indexName = null): void + { + $this->string("{$name}_type"); + $this->unsignedBigInteger("{$name}_id"); + $this->index(["{$name}_type", "{$name}_id"], $indexName); + } + + /** + * Ajoute les colonnes nullables pour une table polymorphique avec IDs numériques + */ + public function nullableNumericMorphs(string $name, ?string $indexName = null): void + { + $this->string("{$name}_type")->nullable(); + $this->unsignedBigInteger("{$name}_id")->nullable(); + $this->index(["{$name}_type", "{$name}_id"], $indexName); + } + + /** + * Ajoute les colonnes pour une table polymorphique avec UUIDs + */ + public function uuidMorphs(string $name, ?string $indexName = null): void + { + $this->string("{$name}_type"); + $this->uuid("{$name}_id"); + $this->index(["{$name}_type", "{$name}_id"], $indexName); + } + + /** + * Ajoute les colonnes nullables pour une table polymorphique avec UUIDs + */ + public function nullableUuidMorphs(string $name, ?string $indexName = null): void + { + $this->string("{$name}_type")->nullable(); + $this->uuid("{$name}_id")->nullable(); + $this->index(["{$name}_type", "{$name}_id"], $indexName); + } + + /** + * Ajoute les colonnes pour une table polymorphique avec ULIDs + */ + public function ulidMorphs(string $name, ?string $indexName = null): void + { + $this->string("{$name}_type"); + $this->ulid("{$name}_id"); + $this->index(["{$name}_type", "{$name}_id"], $indexName); + } + + /** + * Ajoute les colonnes nullables pour une table polymorphique avec ULIDs + */ + public function nullableUlidMorphs(string $name, ?string $indexName = null): void + { + $this->string("{$name}_type")->nullable(); + $this->ulid("{$name}_id")->nullable(); + $this->index(["{$name}_type", "{$name}_id"], $indexName); + } + + /* + |-------------------------------------------------------------------------- + | Index + |-------------------------------------------------------------------------- + */ + + /** + * Ajoute une clé primaire + */ + public function primary(array|string $columns, ?string $name = null, ?string $algorithm = null): Index + { + return $this->addIndex('primary', $columns, $name, $algorithm); + } + + /** + * Ajoute un index unique + */ + public function unique(array|string $columns, ?string $name = null, ?string $algorithm = null): Index + { + return $this->addIndex('unique', $columns, $name, $algorithm); + } + + /** + * Ajoute un index simple + */ + public function index(array|string $columns, ?string $name = null, ?string $algorithm = null): Index + { + return $this->addIndex('index', $columns, $name, $algorithm); + } + + /** + * Ajoute un index FULLTEXT + */ + public function fullText(array|string $columns, ?string $name = null, ?string $algorithm = null): Index + { + return $this->addIndex('fulltext', $columns, $name, $algorithm); + } + + /** + * Ajoute un index spatial + */ + public function spatialIndex(array|string $columns, ?string $name = null): Index + { + return $this->addIndex('spatialIndex', $columns, $name); + } + + /** + * Ajoute un index brut (expression) + */ + public function rawIndex(string $expression, string $name): Index + { + return $this->addIndex('index', [new Expression($expression)], $name); + } + + /** + * Ajoute une clé étrangère + */ + public function foreign(array|string $columns, ?string $name = null): ForeignKey + { + return $this->addForeignKey($columns, $name); + } + + /** + * Supprime une clé étrangère avec sa colonne + */ + public function dropConstrainedForeignId(string $column): void + { + $this->dropForeign([$column]); + $this->dropColumn($column); + } + + /** + * Supprime une clé étrangère pour le modèle donné + */ + public function dropForeignIdFor(string $model, ?string $column = null): void + { + $column = $column ?? strtolower(basename(str_replace('\\', '/', $model))) . '_id'; + + $this->dropForeign([$column]); + } + + /** + * Supprime une clé étrangère avec sa colonne pour le modèle donné + */ + public function dropConstrainedForeignIdFor(string $model, ?string $column = null): void + { + $column = $column ?? strtolower(basename(str_replace('\\', '/', $model))) . '_id'; + + $this->dropConstrainedForeignId($column); + } + + /** + * Renomme un index + */ + public function renameIndex(string $from, string $to): void + { + $this->renames['index'][$from] = $to; + } + + /* + |-------------------------------------------------------------------------- + | Suppressions + |-------------------------------------------------------------------------- + */ + + /** + * Supprime une ou plusieurs colonnes + */ + public function dropColumn(array|string $columns): void + { + foreach ((array) $columns as $column) { + $this->drops['columns'][] = $column; + } + } + + /** + * Supprime la clé primaire + */ + public function dropPrimary(?string $name = null): void + { + $this->drops['primary'] = $name ?? 'primary'; + } + + /** + * Supprime un index unique + */ + public function dropUnique(array|string $columns): void + { + $this->dropNamedIndex('unique', $columns); + } + + /** + * Supprime un index simple + */ + public function dropIndex(array|string $columns): void + { + $this->dropNamedIndex('index', $columns); + } + + /** + * Supprime un index FULLTEXT + */ + public function dropFullText(array|string $columns): void + { + $this->dropNamedIndex('fulltext', $columns); + } + + /** + * Supprime un index spatial + */ + public function dropSpatialIndex(array|string $columns): void + { + $this->dropNamedIndex('spatialIndex', $columns); + } + + /** + * Supprime une clé étrangère + */ + public function dropForeign(array|string $columns): void + { + $this->dropNamedIndex('foreign', $columns); + } + + /** + * Supprime les colonnes de timestamps + */ + public function dropTimestamps(): void + { + $this->dropColumn(['created_at', 'updated_at']); + } + + /** + * Supprime les colonnes de timestamps avec fuseau horaire + */ + public function dropTimestampsTz(): void + { + $this->dropTimestamps(); + } + + /** + * Supprime la colonne de suppression logique + */ + public function dropSoftDeletes(string $column = 'deleted_at'): void + { + $this->dropColumn($column); + } + + /** + * Supprime la colonne de suppression logique avec fuseau horaire + */ + public function dropSoftDeletesTz(string $column = 'deleted_at'): void + { + $this->dropSoftDeletes($column); + } + + /** + * Supprime la colonne remember_token + */ + public function dropRememberToken(): void + { + $this->dropColumn('remember_token'); + } + + /** + * Supprime les colonnes polymorphiques + */ + public function dropMorphs(string $name, ?string $indexName = null): void + { + $this->dropIndex($indexName ?? $this->createIndexName('index', ["{$name}_type", "{$name}_id"])); + + $this->dropColumn(["{$name}_type", "{$name}_id"]); + } + + /* + |-------------------------------------------------------------------------- + | Méthodes internes + |-------------------------------------------------------------------------- + */ + + /** + * Ajoute une colonne + */ + protected function addColumn(string $type, string $name, array $attributes = []): Column + { + return $this->addColumnDefinition(new Column( + array_merge(['type' => $type, 'name' => $name], $attributes) + )); + } + + /** + * Ajoute une définition de colonne + */ + protected function addColumnDefinition(Column $definition): Column + { + $this->columns[] = $definition; + + if ($this->after) { + $definition->after($this->after); + + $this->after = $definition->name; + } + + return $definition; + } + + /** + * Ajoute un index + */ + protected function addIndex(string $type, array|string $columns, ?string $name, ?string $algorithm = null): Index + { + $columns = (array) $columns; + $name = $name ?? $this->createIndexName($type, $columns); + $index = new Index(array_filter(compact('type', 'name', 'columns', 'algorithm'))); + $this->indexes[] = $index; + + return $index; + } + + /** + * Ajoute une clé étrangère + */ + protected function addForeignKey(array|string $columns, ?string $name): ForeignKey + { + $columns = (array) $columns; + $name = $name ?? $this->createIndexName('foreign', $columns); + $fk = new ForeignKey(compact('name', 'columns')); + $this->foreignKeys[] = $fk; + + return $fk; + } + + /** + * Supprime un index nommé + */ + protected function dropNamedIndex(string $type, array|string $columns): void + { + if (is_string($columns)) { + $this->drops[$type][] = $columns; + } else { + $name = $this->createIndexName($type, $columns); + $this->drops[$type][] = $name; + } + } + + /** + * Crée un nom d'index par défaut + * + * Exemple : users_email_unique pour un index unique sur la colonne email de la table users + * + * @internal + */ + public function createIndexName(string $type, array $columns): string + { + $index = strtolower($this->table . '_' . implode('_', $columns) . '_' . $type); + + return str_replace(['-', '.'], '_', $index); + } + + /** + * Supprime une colonne du blueprint + */ + public function removeColumn(string $column): self + { + $this->columns = array_values(array_filter($this->columns, fn($c) => $c->name != $column)); + + return $this; + } + + /* + |-------------------------------------------------------------------------- + | Getters pour Executor + |-------------------------------------------------------------------------- + */ + + /** + * Récupère le nom de la table + */ + public function getTable(): string + { + return $this->table; + } + + /** + * Récupère le préfixe de la table + */ + public function getPrefix(): string + { + return $this->prefix ?? ''; + } + + /** + * Récupère l'action à effectuer + */ + public function getAction(): string + { + return $this->action; + } + + /** + * Récupère les colonnes + * + * @return array + */ + public function getColumns(): array + { + return $this->columns; + } + + /** + * Récupère les colonnes ajoutées (non modifiées) + * + * @return array + */ + public function getAddedColumns(): array + { + return array_filter($this->columns, fn($column) => !$column->change); + } + + /** + * Récupère les colonnes modifiées + * + * @return array + */ + public function getChangedColumns(): array + { + return array_filter($this->columns, fn($column) => (bool) $column->change); + } + + /** + * Récupère les index + * + * @return array + */ + public function getIndexes(): array + { + return $this->indexes; + } + + /** + * Récupère les clés étrangères + * + * @return array + */ + public function getForeignKeys(): array + { + return $this->foreignKeys; + } + + /** + * Récupère les éléments à supprimer + */ + public function getDrops(): array + { + return $this->drops; + } + + /** + * Récupère les informations de renommage + */ + public function getRenames(): array + { + return $this->renames; + } + + /** + * Récupère le moteur de stockage + */ + public function getEngine(): ?string + { + return $this->engine ?? null; + } + + /** + * Récupère le jeu de caractères + */ + public function getCharset(): ?string + { + return $this->charset ?? null; + } + + /** + * Récupère la collation + */ + public function getCollation(): ?string + { + return $this->collation ?? null; + } + + /** + * Vérifie si la table est temporaire + */ + public function isTemporary(): bool + { + return $this->temporary ?? false; + } + + /** + * Récupère le commentaire + */ + public function getComment(): ?string + { + return $this->comment ?? null; + } +} diff --git a/src/Migration/ConnectionProxy.php b/src/Migration/ConnectionProxy.php new file mode 100644 index 0000000..9635fe0 --- /dev/null +++ b/src/Migration/ConnectionProxy.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Database\Migration; + +use BlitzPHP\Database\Connection\BaseConnection; + +/** + * Proxy pour permettre les opérations sur différentes connexions + * + * Permet d'écrire : $this->connection('sqlite')->create('users', ...) + */ +class ConnectionProxy +{ + public function __construct(protected BaseConnection $connection, protected Migration $migration) + { + } + + /** + * Crée une nouvelle table sur cette connexion + */ + public function create(string $table, callable $callback, bool $ifNotExists = false): void + { + $this->migration->createOnConnection( + $this->getConnectionName(), + $table, + $callback, + $ifNotExists + ); + } + + /** + * Crée une nouvelle table si elle n'existe pas + */ + public function createIfNotExists(string $table, callable $callback): void + { + $this->create($table, $callback, true); + } + + /** + * Modifie une table existante sur cette connexion + */ + public function alter(string $table, callable $callback): void + { + $this->migration->alterOnConnection( + $this->getConnectionName(), + $table, + $callback + ); + } + + /** + * Alias de alter() + */ + public function modify(string $table, callable $callback): void + { + $this->alter($table, $callback); + } + + /** + * Supprime une table sur cette connexion + */ + public function drop(string $table, bool $ifExists = false): void + { + $this->migration->dropOnConnection( + $this->getConnectionName(), + $table, + $ifExists + ); + } + + /** + * Supprime une table si elle existe + */ + public function dropIfExists(string $table): void + { + $this->drop($table, true); + } + + /** + * Renomme une table sur cette connexion + */ + public function rename(string $from, string $to): void + { + $this->migration->renameOnConnection( + $this->getConnectionName(), + $from, + $to + ); + } + + /** + * Vérifie si une table existe + */ + public function hasTable(string $name): bool + { + return $this->migration->hasTableOnConnection( + $this->getConnectionName(), + $name + ); + } + + /** + * Vérifie si un champ existe dans une table + */ + public function hasColumn(string $table, string $column): bool + { + return $this->migration->hasColumnOnConnection( + $this->getConnectionName(), + $table, + $column + ); + } + + /** + * Retourne le nom de la connexion (pour usage interne) + */ + protected function getConnectionName(): string + { + // Trouver le nom de la connexion à partir des connections enregistrées + foreach ($this->migration->getConnections() as $name => $conn) { + if ($conn === $this->connection) { + return $name; + } + } + + return 'unknown'; + } +} diff --git a/src/Migration/Definitions/Column.php b/src/Migration/Definitions/Column.php index 59c5e62..7329c06 100644 --- a/src/Migration/Definitions/Column.php +++ b/src/Migration/Definitions/Column.php @@ -16,28 +16,64 @@ /** * Definition des colonnes de la struture de migrations * - * @method $this invisible() Specify that the column should be invisible to "SELECT *" (MySQL) - * @method $this autoIncrement() Set INTEGER columns as auto-increment (primary key) - * @method $this change() Change the column - * @method $this virtualAs(string $expression) Create a virtual generated column (MySQL/PostgreSQL/SQLite) - * @method $this default(mixed $value) Specify a "default" value for the column - * @method $this startingValue(int $startingValue) Set the starting value of an auto-incrementing field (MySQL/PostgreSQL) - * @method $this fulltext(string $indexName = null) Add a fulltext index - * @method $this always(bool $value = true) Used as a modifier for generatedAs() (PostgreSQL) - * @method $this index(string $indexName = null) Add an index - * @method $this nullable(bool $value = true) Allow NULL values to be inserted into the column - * @method $this persisted() Mark the computed generated column as persistent (SQL Server) - * @method $this primary() Add a primary index - * @method $this spatialIndex(string $indexName = null) Add a spatial index - * @method $this first() Place the column "first" in the table (MySQL) - * @method $this type(string $type) Specify a type for the column - * @method $this unique(string $indexName = null) Add a unique index - * @method $this unsigned() Set the INTEGER column as UNSIGNED (MySQL) - * @method $this useCurrentOnUpdate() Set the TIMESTAMP column to use CURRENT_TIMESTAMP when updating (MySQL) - * @method $this useCurrent() Set the TIMESTAMP column to use CURRENT_TIMESTAMP as default value + * /** + * @method $this after(string $column) Place la colonne "après" une autre colonne (MySQL) + * @method $this always(bool $value = true) Utilisé comme modificateur pour generatedAs() (PostgreSQL) + * @method $this autoIncrement() Définit les colonnes INTEGER comme auto-incrémentées (clé primaire) + * @method $this change() Modifie la colonne + * @method $this charset(string $charset) Spécifie un jeu de caractères pour la colonne (MySQL) + * @method $this collation(string $collation) Spécifie une collation pour la colonne (MySQL/PostgreSQL/SQL Server) + * @method $this comment(string $comment) Ajoute un commentaire à la colonne (MySQL/PostgreSQL) + * @method $this default(mixed $value) Spécifie une valeur "par défaut" pour la colonne + * @method $this first() Place la colonne "en premier" dans la table (MySQL) + * @method $this from(int $startingValue) Définit la valeur de départ d'un champ auto-incrémenté (MySQL/PostgreSQL) + * @method $this generatedAs(string|\Illuminate\Database\Query\Expression $expression = null) Crée une colonne d'identité conforme SQL (PostgreSQL) + * @method $this index(string $indexName = null) Ajoute un index + * @method $this invisible() Spécifie que la colonne doit être invisible pour "SELECT *" (MySQL) + * @method $this nullable(bool $value = true) Autorise l'insertion de valeurs NULL dans la colonne + * @method $this persisted() Marque la colonne générée calculée comme persistante (SQL Server) + * @method $this primary() Ajoute un index primaire + * @method $this fulltext(string $indexName = null) Ajoute un index FULLTEXT + * @method $this spatialIndex(string $indexName = null) Ajoute un index spatial + * @method $this startingValue(int $startingValue) Définit la valeur de départ d'un champ auto-incrémenté (MySQL/PostgreSQL) + * @method $this storedAs(string $expression) Crée une colonne générée stockée (MySQL/PostgreSQL/SQLite) + * @method $this type(string $type) Spécifie un type pour la colonne + * @method $this unique(string $indexName = null) Ajoute un index unique + * @method $this unsigned() Définit la colonne INTEGER comme NON SIGNÉE (MySQL) + * @method $this useCurrent() Définit la colonne TIMESTAMP pour utiliser CURRENT_TIMESTAMP comme valeur par défaut + * @method $this useCurrentOnUpdate() Définit la colonne TIMESTAMP pour utiliser CURRENT_TIMESTAMP lors de la mise à jour (MySQL) + * @method $this virtualAs(string $expression) Crée une colonne générée virtuelle (MySQL/PostgreSQL/SQLite) * * @credit Laravel Framework - Illuminate\Database\Schema\ColumnDefinition */ class Column extends Fluent { + protected array $_indexes = ['primary', 'unique', 'index', 'fulltext', 'spatialIndex']; + + /** + * Vérifie si la colonne a des index fluides + */ + public function hasFluentIndexes(): bool + { + foreach ($this->_indexes as $index) { + if (isset($this->attributes[$index])) { + return true; + } + } + return false; + } + + /** + * Récupère tous les index fluides + */ + public function getFluentIndexes(): array + { + $indexes = []; + foreach ($this->_indexes as $index) { + if (isset($this->attributes[$index])) { + $indexes[$index] = $this->attributes[$index]; + } + } + return $indexes; + } } diff --git a/src/Migration/Definitions/ForeignId.php b/src/Migration/Definitions/ForeignId.php index 886ceed..9345ef9 100644 --- a/src/Migration/Definitions/ForeignId.php +++ b/src/Migration/Definitions/ForeignId.php @@ -11,49 +11,37 @@ namespace BlitzPHP\Database\Migration\Definitions; -use BlitzPHP\Database\Migration\Structure; +use BlitzPHP\Database\Migration\Builder; use BlitzPHP\Utilities\String\Text; -use BlitzPHP\Utilities\Support\Fluent; /** * @credit Laravel Framework - Illuminate\Database\Schema\ForeignIdDefinition */ class ForeignId extends Column { - /** - * L'instance du constructeure de structure. - */ - protected Structure $structure; - /** * Creation d'une nouvelle definition d'une colone ID etrangere. * - * @return void + * @param Builder $builder L'instance du constructeure de structure. */ - public function __construct(Structure $structure, array $attributes = []) + public function __construct(protected Builder $builder, array $attributes = []) { parent::__construct($attributes); - - $this->structure = $structure; } /** * Cree une contrainte de cle etrangere sur cette colonne "id" conventionellement a la table referencee. - * - * @return ForeignKey */ - public function constrained(?string $table = null, string $column = 'id'): Fluent + public function constrained(?string $table = null, string $column = 'id', ?string $indexName = null): ForeignKey { - return $this->references($column)->on($table ?? Text::of($this->name)->beforeLast('_' . $column)->plural()); + return $this->references($column, $indexName)->on($table ?? Text::of($this->name)->beforeLast('_' . $column)->plural()); } /** * Specifie quelle colone cet ID etrangere reference danson another table. - * - * @return ForeignKey */ - public function references(string $column): Fluent + public function references(string $column, ?string $indexName = null): ForeignKey { - return $this->structure->foreign($this->name)->references($column); + return $this->builder->foreign($this->name, $indexName)->references($column); } } diff --git a/src/Migration/Definitions/ForeignKey.php b/src/Migration/Definitions/ForeignKey.php index 89a628a..55d8eef 100644 --- a/src/Migration/Definitions/ForeignKey.php +++ b/src/Migration/Definitions/ForeignKey.php @@ -14,61 +14,69 @@ use BlitzPHP\Utilities\Support\Fluent; /** - * @method $this references(array|string $columns) Specifie la/les colone(s) de reference - * @method $this deferrable(bool $value = true) Specifie que l'index unique est deferrable (PostgreSQL) - * @method $this on(string $table) Specifie la table de reference - * @method $this onDelete(string $action) Ajoute une action ON DELETE - * @method $this onUpdate(string $action) Ajoute une action ON UPDATE - * @method $this initiallyImmediate(bool $value = true) Specifie si verifie la contrainte d'indexe unique immediatement ou pas (PostgreSQL) - * + * @method $this deferrable(bool $value = true) Définit la clé étrangère comme différable (PostgreSQL) + * @method $this initiallyImmediate(bool $value = true) Définit le moment par défaut pour vérifier la contrainte (PostgreSQL) + * @method $this on(string $table) Spécifie la table référencée + * @method $this onDelete(string $action) Ajoute une action ON DELETE + * @method $this onUpdate(string $action) Ajoute une action ON UPDATE + * @method $this references(string|array $columns) Spécifie la ou les colonnes référencées + * * @credit Laravel Framework - Illuminate\Database\Schema\ForeignKeyDefinition */ class ForeignKey extends Fluent { /** - * Indique que les updates doivent etre en cascade. + * Indique que les mises à jour doivent être en cascade. */ - public function cascadeOnUpdate(): self + public function cascadeOnUpdate(): static { return $this->onUpdate('cascade'); } /** - * Indique que les updates doivent etre restreint. + * Indique que les mises à jour doivent être restreintes. */ - public function restrictOnUpdate(): self + public function restrictOnUpdate(): static { return $this->onUpdate('restrict'); } /** - * Indique que les deletes doivent etre en cascade. + * Indique que les mises à jour doivent être sans action. + */ + public function noActionOnUpdate(): static + { + return $this->onUpdate('no action'); + } + + /** + * Indique que les suppressions doivent être en cascade. */ - public function cascadeOnDelete(): self + public function cascadeOnDelete(): static { return $this->onDelete('cascade'); } /** - * Indique que les deletes doivent etre restreint. + * Indique que les suppressions doivent être restreintes. */ - public function restrictOnDelete(): self + public function restrictOnDelete(): static { return $this->onDelete('restrict'); } /** - * Indique que les deletes doivent mettre la valeur de la cle etrangere a null. + * Indique que les suppressions doivent définir la valeur de la clé étrangère à null. */ - public function nullOnDelete(): self + public function nullOnDelete(): static { return $this->onDelete('set null'); } /** - * Indique que les deletes doivent avoir "no action". + * Indique que les suppressions doivent être sans action. */ - public function noActionOnDelete() + public function noActionOnDelete(): static { return $this->onDelete('no action'); } diff --git a/src/Migration/History.php b/src/Migration/History.php new file mode 100644 index 0000000..628dbd7 --- /dev/null +++ b/src/Migration/History.php @@ -0,0 +1,170 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Database\Migration; + +use BlitzPHP\Database\Connection\BaseConnection; +use BlitzPHP\Database\DatabaseManager; + +/** + * Gestionnaire de l'historique des migrations + * + * Cette classe s'occupe de la table de migrations qui enregistre + * toutes les migrations exécutées. + */ +class History +{ + /** + * Connection à la base de données + */ + protected BaseConnection $db; + + /** + * Indique si la table d'historique a été vérifiée/créée + */ + private bool $tableChecked = false; + + /** + * Constructeur + * + * @param string $table Nom de la table d'historique des migrations + */ + public function __construct(protected DatabaseManager $dbManager, protected string $table = 'migrations') + { + $this->db = $this->dbManager->activeConnection(); + + $this->ensureTable(); + } + + /** + * Crée la table d'historique si elle n'existe pas + */ + protected function ensureTable(): void + { + if ($this->tableChecked || $this->db->tableExists($this->table)) { + return; + } + + $builder = new Builder($this->table); + $builder->id(); + $builder->string('migration'); + $builder->string('version'); + $builder->string('namespace'); + $builder->string('group'); + $builder->unsignedInteger('batch'); + $builder->integer('time'); + $builder->createTable(true); + + (new Transformer($this->dbManager->creator($this->db)))->process($builder); + + $this->tableChecked = true; + } + + /** + * Récupère tout l'historique + * + * @return array + */ + public function getAll(?string $group = null): array + { + return $this->db->table($this->table) + ->orderBy('batch', 'ASC') + ->orderBy('id', 'ASC') + ->when($group, function ($query) use ($group) { + $query->where('group', $group); + }) + ->all(); + } + + /** + * Récupère l'historique d'un lot + * + * @return array + */ + public function getBatch(int $batch, string $order = 'asc'): array + { + return $this->db->table($this->table) + ->where('batch', $batch) + ->orderBy('id', $order) + ->all(); + } + + /** + * Récupère le dernier numéro de lot + */ + public function getLastBatch(): int + { + return (int) $this->db->table($this->table)->max('batch'); + } + + /** + * Récupère tous les numéros de lots + * + * @return array + */ + public function getBatches(): array + { + $rows = $this->db->table($this->table) + ->select('batch') + ->distinct() + ->orderBy('batch', 'ASC') + ->all(); + + return array_column($rows, 'batch'); + } + + /** + * Ajoute une entrée dans l'historique + * + * @param string $version Version de la migration + * @param string $migration Nom de la migration + * @param string $namespace Namespace + * @param string $group Groupe de connexion + * @param int $batch Numéro de lot + */ + public function add(string $version, string $migration, string $namespace, string $group, int $batch): void + { + $this->db->table($this->table)->insert([ + 'version' => $version, + 'migration' => $migration, + 'namespace' => $namespace, + 'group' => $group, + 'batch' => $batch, + 'time' => time(), + ]); + } + + /** + * Supprime une entrée de l'historique + */ + public function remove(int $id): void + { + $this->db->table($this->table)->where('id', $id)->delete(); + } + + /** + * Vide l'historique + */ + public function clear(): void + { + $this->db->table($this->table)->truncate(); + } + + /** + * Vérifie si une migration a déjà été exécutée + */ + public function has(string $class): bool + { + return $this->db->table($this->table) + ->where('class', $class) + ->count() > 0; + } +} diff --git a/src/Migration/Migration.php b/src/Migration/Migration.php index 5abafdc..dbf376a 100644 --- a/src/Migration/Migration.php +++ b/src/Migration/Migration.php @@ -12,136 +12,262 @@ namespace BlitzPHP\Database\Migration; use BlitzPHP\Database\Connection\BaseConnection; -use InvalidArgumentException; +use BlitzPHP\Database\DatabaseManager; /** - * Migration - * - * Classe abstraite de gestion de migrations de base de donnees + * Classe de base pour les migrations de base de données */ abstract class Migration { /** - * @var list Liste des taches + * Liste des builders de tables + * + * @var list + */ + protected array $builders = []; + + /** + * Connexion par défaut + */ + protected BaseConnection $db; + + /** + * Gestionnaire de base de données + */ + protected DatabaseManager $dbManager; + + /** + * Connexions alternatives pour cette migration + * + * @var array */ - private array $structures = []; + protected array $connections = []; /** - * Nom du group a utiliser pour lexecuter les migrations + * Méthode appelée lors de l'application de la migration */ - protected ?string $group = null; + abstract public function up(): void; /** - * Definition des etapes d'execution d'une migration. + * Méthode appelée lors de l'annulation de la migration */ - abstract public function up(); + abstract public function down(): void; /** - * Definition des etapes d'annulation d'une migration. + * Détermine si cette migration doit être exécutée + * + * Peut être surchargée pour des conditions complexes */ - abstract public function down(); + public function shouldRun(): bool + { + return true; + } - public function __construct(protected BaseConnection $db) + /** + * Initialise les éléments nécessaire pour le fonctionnement de la migration + * + * @internal Utilisé par le Runner pour injecter la connexion et le gestionnaire de bd + */ + public function initialize(DatabaseManager $dbManager, BaseConnection $db): self { + $this->db = $db; + $this->dbManager = $dbManager; + $this->connections['default'] = $db; + + return $this; } /** - * Renvoi la liste des executions - * - * @return list + * Récupère les builders de tables * - * @internal Utilisee par le `runner` + * @return list + * + * @internal Utilisé par le Runner */ - final public function getStructure(): array + public function getBuilders(): array { - return $this->structures; + return $this->builders; } /** - * Renvoi le nom du groupe a utiliser pour la connexion a la base de donnees + * Récupère les connexions utilisées par cette migration * - * @internal Utilisee par le `runner` + * @return array + * + * @internal Utilisé par le ConnectionProxy */ - final public function getGroup(): ?string + public function getConnections(): array { - return $this->group; + return $this->connections; } /** - * Cree une nouvelle table dans la structure. + * Crée une nouvelle table sur une connexion spécifique + * + * @internal Utilisé par le ConnectionProxy */ - final protected function create(string $table, bool|callable $ifNotExists, ?callable $callback = null): void + public function createOnConnection(string $connection, string $table, callable $callback, bool $ifNotExists = false): void { - if (is_callable($ifNotExists)) { - $callback = $ifNotExists; - $ifNotExists = false; - } elseif ($callback === null) { - throw new InvalidArgumentException('Si vous passez un booléen en second argument de la méthode create, le troisième doit être un callback'); - } + $builder = $this->makeBuilderFor($connection, $table); + $callback($builder); + $builder->createTable($ifNotExists); - $structure = $this->build($table, $callback); - $structure->create($ifNotExists); + $this->builders[] = $builder; + } - $this->structures[] = $structure; + /** + * Modifie une table existante sur une connexion spécifique + * + * @internal Utilisé par le ConnectionProxy + */ + public function alterOnConnection(string $connection, string $table, callable $callback): void + { + $builder = $this->makeBuilderFor($connection, $table); + $callback($builder); + $builder->alterTable(); + + $this->builders[] = $builder; } /** - * Modifie une table de la structure. + * Supprime une table existante sur une connexion spécifique + * + * @internal Utilisé par le ConnectionProxy */ - final protected function modify(string $table, callable $callback): void + public function dropOnConnection(string $connection, string $table, bool $ifExists): void { - $structure = $this->build($table, $callback); - $structure->modify(); + $builder = $this->makeBuilderFor($connection, $table); + $builder->dropTable($ifExists); + + $this->builders[] = $builder; + } - $this->structures[] = $structure; + /** + * renomme une table existante sur une connexion spécifique + * + * @internal Utilisé par le ConnectionProxy + */ + public function renameOnConnection(string $connection, string $from, string $to): void + { + $builder = $this->makeBuilderFor($connection, $from); + $builder->renameTable($to); + + $this->builders[] = $builder; } /** - * Supprime une table de la structure. + * Vérifie si une table existe sur une connexion spécifique + * + * @internal Utilisé par le ConnectionProxy */ - final protected function drop(string $table, bool $ifExists = false): void + public function hasTableOnConnection(string $connection, string $table): bool { - $structure = $this->createStructure($table); - $structure->drop($ifExists); + return ($this->connections[$connection] ?? $this->db)->tableExists($table); + } - $this->structures[] = $structure; + /** + * Vérifie si un champ existe dans une table sur une connexion spécifique + * + * @internal Utilisé par le ConnectionProxy + */ + public function hasColumnOnConnection(string $connection, string $table, string $column): bool + { + return ($this->connections[$connection] ?? $this->db)->columnExists($column, $table); } /** - * Supprime une table de la structure si elle existe. + * Sélectionne une connexion alternative pour la suite des opérations */ - final protected function dropIfExists(string $table): void + protected function connection(string $name): ConnectionProxy { - $structure = $this->createStructure($table); - $structure->dropIfExists(); + if (!isset($this->connections[$name])) { + // Résoudre la connexion via le DatabaseManager + $this->connections[$name] = $this->db->dbManager()->connect($name); + } - $this->structures[] = $structure; + return new ConnectionProxy($this->connections[$name], $this); } /** - * Renomme une table + * Crée une nouvelle table sur la connexion courante */ - final protected function rename(string $from, string $to): void + protected function create(string $table, callable $callback, bool $ifNotExists = false): void { - $structure = $this->createStructure($from); - $structure->rename($to); + $this->createOnConnection('default', $table, $callback, $ifNotExists); + } - $this->structures[] = $structure; + /** + * Crée une nouvelle table si elle n'existe pas + */ + protected function createIfNotExists(string $table, callable $callback): void + { + $this->create($table, $callback, true); + } + + /** + * Modifie une table existante sur la connexion courante + */ + protected function alter(string $table, callable $callback): void + { + $this->alterOnConnection('default', $table, $callback); + } + + /** + * Alias de alter() pour compatibilité + */ + public function modify(string $table, callable $callback): void + { + $this->alter($table, $callback); + } + + /** + * Supprime une table + */ + protected function drop(string $table, bool $ifExists = false): void + { + $this->dropOnConnection('default', $table, $ifExists); } /** - * Execute le callback avec la structure + * Supprime une table si elle existe */ - private function build(string $table, callable $callback): Structure + protected function dropIfExists(string $table): void { - return $callback($this->createStructure($table)); + $this->drop($table, true); } /** - * Cree et renvoi une structure + * Renomme une table */ - private function createStructure(string $table): Structure + protected function rename(string $from, string $to): void { - return new Structure($table); + $this->renameOnConnection('default', $from, $to); + } + + /** + * Vérifie si une table existe + */ + protected function hasTable(string $name): bool + { + return $this->hasTableOnConnection('default', $name); + } + + /** + * Vérifie si un champ existe dans une table + */ + protected function hasColumn(string $table, string $column): bool + { + return $this->hasColumnOnConnection('default', $table, $column); + } + + /** + * Crée un builder pour une connexion et une table spécifiques + */ + private function makeBuilderFor(string $connection, string $table): Builder + { + $builder = new Builder($table); + $builder->setConnection($this->connections[$connection] ?? $this->db); + + return $builder; } } diff --git a/src/Migration/Runner.php b/src/Migration/Runner.php index e5e299e..564934d 100644 --- a/src/Migration/Runner.php +++ b/src/Migration/Runner.php @@ -11,836 +11,511 @@ namespace BlitzPHP\Database\Migration; -use BlitzPHP\Contracts\Database\ConnectionInterface; use BlitzPHP\Database\Connection\BaseConnection; -use BlitzPHP\Database\Database; -use BlitzPHP\Database\Exceptions\MigrationException; -use PDO; +use BlitzPHP\Database\DatabaseManager; use RuntimeException; -use stdClass; /** - * Classe pour executer les migrations - * - * @credit CodeIgniter4 - CodeIgniter\Database\MigrationRunner + * Exécuteur de migrations + * + * Cette classe orchestre la découverte, l'ordonnancement et l'exécution + * des fichiers de migration, avec support des classes anonymes et nommées. */ class Runner { /** - * Specifie si les migrations sont activees ou pas. - */ - protected bool $enabled = false; - - /** - * Nom de la table dans laquelle seront stockees les meta informations de migrations. + * Longueur de chaîne par défaut pour les migrations. */ - protected string $table; - - /** - * Le namespace où se trouvent les migrations. - * `null` correspond à tous les espaces de noms. - */ - protected ?string $namespace = null; - - /** - * Liste des fichiers des migrations. - * Le framework est responsable de la recherche de tous les fichiers necessaires regroupes par namespace. - * - * @var array> [namespace => [fichiers]] - */ - protected array $files = []; - - /** - * Le groupe de la base de donnee a migrer. - */ - protected string $group = ''; - - /** - * Le nom de la migration. - */ - protected string $name; + public static int $defaultStringLength = 255; /** - * Le format (pattern) utiliser pour trouver la version du fichier de migration. + * Type par défaut de clé pour les relations de polymorphiques. */ - protected string $regex = '/\A(\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6})_(\w+)\z/'; + public static string $defaultMorphKeyType = 'int'; /** - * Connexion a la base de donnees. + * Connexion à la base de données */ protected BaseConnection $db; /** - * si true, on continue l'activite sans levee d'exception en cas d'erreur. + * Gestionnaire d'historique */ - protected bool $silent = false; + protected History $history; /** - * @var list> utiliser pour renvoyer les messages pour la console. + * Pattern de reconnaissance des fichiers de migration */ - protected array $messages = []; + protected string $pattern = '/\A(\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6})_(\w+)\z/'; /** - * Verifie si on s'est deja rassurer que la table existe ou pas. + * Spécifie si les migrations sont activées ou pas. */ - protected bool $tableChecked = false; - - /** - * Chemin d'accès complet permettant de localiser les fichiers de migration. - */ - protected string $path; - - /** - * Le filtre du groupe de la base de donnees. - */ - protected ?string $groupFilter = null; - - /** - * Utiliser pour sauter la migration courrante. - */ - protected bool $groupSkip = false; + protected bool $enabled = false; /** - * singleton + * Cache des instances de migration chargées + * + * @var array */ - private static $_instance; + protected array $loaded = []; /** - * Longueur par défaut des champs de type chaînes (varchar/char) pour les migrations. + * Callbacks pour les événements + * + * @var array> */ - public static int $defaultStringLength = 255; + protected array $listeners = []; /** - * La migration peut gérer plusieurs bases de données. - * Elle doit donc toujours utiliser le groupe de bases de données par défaut afin de créer la table `migrations` dans le groupe de bases de données par défaut. - * Par conséquent, le passage de $db est uniquement à des fins de test. + * Constructeur * - * @param array|ConnectionInterface|string|null $db Groupe de DB. À des fins de test uniquement. + * @param array> $paths Chemins de recherche des migrations */ - public function __construct(array $config, $db = null) + public function __construct(protected DatabaseManager $dbManager, protected ?string $group, protected array $paths = []) { - $this->enabled = $config['enabled'] ?? false; - $this->table = $config['table'] ?? 'migrations'; - - $this->namespace = defined('APP_NAMESPACE') ? constant('APP_NAMESPACE') : 'App'; + $config = config('migrations'); - // Même si une connexion DB est transmise comme il s'agit d'un test, - // on suppose que le nom de groupe par défaut est utilisé. - // $this->group = is_string($db) ? $db : config('database.connection'); - - if ($db instanceof ConnectionInterface) { - $this->db = $db; - } else { - $this->db = Database::connection($db, static::class); - } + $this->enabled = $config['enabled'] ?? false; + $this->db = $this->dbManager->connect($group); + $this->history = new History($dbManager, $config['table'] ?? 'migrations'); } /** - * singleton constructor - * - * @param mixed|null $db + * Enregistre un écouteur pour un événement */ - public static function instance(array $config, $db = null): self + public function on(string $event, callable $callback): self { - if (null === self::$_instance) { - self::$_instance = new self($config, $db); + if (!isset($this->listeners[$event])) { + $this->listeners[$event] = []; } - - return self::$_instance; + + $this->listeners[$event][] = $callback; + + return $this; } /** - * Définir la longueur de la chaîne par défaut pour les migrations. + * Déclenche un événement */ - public static function defaultStringLength(int $length): void + protected function fire(string $event, array $payload = []): void { - static::$defaultStringLength = $length; + if (!isset($this->listeners[$event])) { + return; + } + + foreach ($this->listeners[$event] as $callback) { + $callback($payload, $event, $this); + } } /** - * Localisez et exécutez toutes les nouvelles migrations. + * Exécute toutes les migrations en attente * - * @throws MigrationException - * @throws RuntimeException + * @param string|null $group Groupe de connexion + * @return int Nombre de migrations exécutées */ - public function latest(?string $group = null): bool + public function latest(?string $group = null): int { - if (! $this->enabled) { - throw MigrationException::disabledMigrations(); + if (!$this->enabled) { + $this->fire('process.migrations-disabled'); + return 0; } - $this->ensureTable(); - - if ($group !== null) { - $this->groupFilter = $group; - $this->setGroup($group); - } - - $migrations = $this->getMigrations(); + $migrations = $this->getPendingMigrations($group); if ($migrations === []) { - return true; + $this->fire('process.empty-migrations'); + return 0; } - foreach ($this->getHistory((string) $group) as $history) { - unset($migrations[$this->getObjectUid($history)]); - } - - $batch = $this->getLastBatch() + 1; + $this->fire('process.start', [ + 'count' => count($migrations), + 'group' => $group, + 'start' => $start = microtime(true), + ]); + + $batch = $this->history->getLastBatch() + 1; + $executed = 0; foreach ($migrations as $migration) { - if ($this->migrate('up', $migration)) { - if ($this->groupSkip === true) { - $this->groupSkip = false; - - continue; - } - - $this->addHistory($migration, $batch); - } else { - $this->regress(-1); - - $message = 'Migration failed!'; - - if ($this->silent) { - $this->pushMessage($message, 'red'); - - return false; - } - - throw new RuntimeException($message); + $success = $this->runMigration($migration, 'up', $group, $batch); + if ($success) { + $executed++; } } - $data = get_object_vars($this); - $data['method'] = 'latest'; - $this->db->triggerEvent($data, 'migrate'); + $this->fire('process.completed', [ + 'executed' => $executed, + 'total' => count($migrations), + 'batch' => $batch, + 'duration' => number_format(microtime(true) - $start, 4), + ]); - return true; + return $executed; } /** - * Migrer vers un lot précédent - * - * Appelle chaque étape de migration requise pour accéder au lot fourni - * - * @param int $targetBatch Numéro de lot cible, ou négatif pour un lot relatif, 0 pour tous - * - * @return bool Vrai en cas de succès, FAUX en cas d'échec ou aucune migration n'est trouvée + * Annule les dernières migrations * - * @throws MigrationException - * @throws RuntimeException + * @param int $steps Nombre de lots à annuler + * @param string|null $group Groupe de connexion + * @return int Nombre de migrations annulées */ - public function regress(int $targetBatch = 0): bool + public function rollback(int $steps = 1, ?string $group = null): int { - if (! $this->enabled) { - throw MigrationException::disabledMigrations(); - } - - $this->ensureTable(); - - $batches = $this->getBatches(); - - if ($targetBatch < 0) { - $targetBatch = $batches[count($batches) - 1 + $targetBatch] ?? 0; + if (!$this->enabled) { + $this->fire('process.migrations-disabled'); + return 0; } - if ($batches === [] && $targetBatch === 0) { - return true; - } - - if ($targetBatch !== 0 && ! in_array($targetBatch, $batches, true)) { - $message = 'Target batch not found: ' . $targetBatch; - - if ($this->silent) { - $this->pushMessage($message, 'red'); + $batches = $this->history->getBatches(); - return false; - } - - throw new RuntimeException($message); + if ($batches === []) { + $this->fire('process.empty-migrations'); + return 0; } - $tmpNamespace = $this->namespace; - - $this->namespace = null; - $allMigrations = $this->getMigrations(); - - $migrations = []; - - while ($batch = array_pop($batches)) { - if ($batch <= $targetBatch) { - break; - } - - foreach ($this->getBatchHistory($batch, 'desc') as $history) { - $uid = $this->getObjectUid($history); - - if (! isset($allMigrations[$uid])) { - $message = 'Il y a une lacune dans la séquence de migration près du numéro de version: ' . $history->version; + $this->fire('process.start', [ + 'steps' => $steps, + 'available_batches' => count($batches), + 'group' => $group, + 'start' => $start = microtime(true), + ]); - if ($this->silent) { - $this->pushMessage($message, 'red'); + $targetBatch = $steps === 0 ? 0 : (count($batches) - $steps); + $rolledBack = 0; - return false; - } + for ($i = count($batches) - 1; $i >= $targetBatch; $i--) { + $batchMigrations = $this->history->getBatch($batches[$i], 'desc'); - throw new RuntimeException($message); + foreach ($batchMigrations as $history) { + $migration = $this->createMigrationFromHistory($history); + + if (!$migration->path) { + $this->fire('migration.skipped', ['migration' => $migration]); + continue; } - $migration = $allMigrations[$uid]; - $migration->history = $history; - $migrations[] = $migration; - } - } - - foreach ($migrations as $migration) { - if ($this->migrate('down', $migration)) { - $this->removeHistory($migration->history); - } else { - $message = 'Migration failed!'; - - if ($this->silent) { - $this->pushMessage($message, 'red'); - - return false; + $success = $this->runMigration($migration, 'down', $group); + if ($success) { + $rolledBack++; } - - throw new RuntimeException($message); } } - $data = get_object_vars($this); - $data['method'] = 'regress'; - $this->db->triggerEvent($data, 'migrate'); - - $this->namespace = $tmpNamespace; + $this->fire('process.completed', [ + 'rolled_back' => $rolledBack, + 'target_batch' => $targetBatch, + 'duration' => number_format(microtime(true) - $start, 4), + ]); - return true; + return $rolledBack; } /** - * Migrer un seul fichier, quel que soit l'ordre ou les lots. - * La méthode "up" ou "down" est déterminée par la présence dans l'historique. - * REMARQUE : cette méthode n'est pas recommandée et est principalement fournie à des fins de test. + * Annule toutes les migrations * - * @param string $path Chemin d'accès complet vers un fichier de migration valide. - * @param string $path Espace de noms de la migration cible. - */ - public function force(string $path, string $namespace, ?string $group = null) - { - if (! $this->enabled) { - throw MigrationException::disabledMigrations(); - } - - $this->ensureTable(); - - if ($group !== null) { - $this->groupFilter = $group; - $this->setGroup($group); - } - - $migration = $this->migrationFromFile($path, $namespace); - if (empty($migration)) { - $message = 'Migration file not found: ' . $path; - - if ($this->silent) { - $this->pushMessage($message, 'red'); - - return false; - } - - throw new RuntimeException($message); - } - - $method = 'up'; - $this->setNamespace($migration->namespace); - - foreach ($this->getHistory($this->group) as $history) { - if ($this->getObjectUid($history) === $migration->uid) { - $method = 'down'; - $migration->history = $history; - break; - } - } - - if ($method === 'up') { - $batch = $this->getLastBatch() + 1; - - if ($this->migrate('up', $migration) && $this->groupSkip === false) { - $this->addHistory($migration, $batch); - - return true; - } - - $this->groupSkip = false; - } elseif ($this->migrate('down', $migration)) { - $this->removeHistory($migration->history); - - return true; - } - - $message = 'Migration failed!'; - - if ($this->silent) { - $this->pushMessage($message, 'red'); - - return false; - } - - throw new RuntimeException($message); - } - - /** - * Permet à d'autres scripts d'effectuer des modifications à la volée selon les besoins. - */ - public function setNamespace(?string $namespace): self - { - $this->namespace = $namespace; - - return $this; - } - - /** - * Permet à d'autres scripts d'effectuer des modifications à la volée selon les besoins. - */ - public function setGroup(string $group): self - { - $this->group = $group; - - return $this; - } - - /** - * Permet à d'autres scripts d'effectuer des modifications à la volée selon les besoins. - */ - public function setFiles(array $files): self - { - $this->files = $files; - - return $this; - } - - public function setName(string $name): self - { - $this->name = $name; - - return $this; - } - - /** - * Si $silent == true, alors aucune exception ne sera levée - * et le programme tentera de continuer normalement. + * @param string|null $group Groupe de connexion + * @return int Nombre de migrations annulées */ - public function setSilent(bool $silent): self + public function reset(?string $group = null): int { - $this->silent = $silent; - - return $this; + return $this->rollback(PHP_INT_MAX, $group); } /** - * Récupère les messages formatés pour la sortie CLI. - */ - public function getMessages(): array - { - return $this->messages; - } - - /** - * Recupere toutes les migrations + * Réinitialise et réexécute toutes les migrations + * + * @param string|null $group Groupe de connexion + * @return array{reset: int, latest: int} Nombre de migrations annulées et exécutées */ - private function getMigrations(): array + public function refresh(?string $group = null): array { - $migrations = []; + $reset = $this->reset($group); + $latest = $this->latest($group); - foreach ($this->files as $namespace => $files) { - foreach ($this->findNamespaceMigrations($namespace, $files) as $migration) { - $migrations[$migration->uid] = $migration; - } - } - - // Sort migrations ascending by their UID (version) - ksort($migrations); - - return $migrations; + return ['reset' => $reset, 'latest' => $latest]; } /** - * Récupère la liste des scripts de migration disponibles pour un namespace. + * Crée un objet migration à partir de l'historique */ - public function findNamespaceMigrations(string $namespace, array $files): array + protected function createMigrationFromHistory(object $history): object { - $migrations = []; + $migration = (object) [ + 'path' => null, + 'version' => $history->version, + 'migration' => $history->migration, + 'namespace' => $history->namespace, + 'history' => $history, + ]; + // Trouver le chemin du fichier + $files = $this->findMigrationFiles(); foreach ($files as $file) { - $file = empty($this->path) ? $file : $this->path . str_replace($this->path, '', $file); - - if ($migration = $this->migrationFromFile($file, $namespace)) { - $migrations[] = $migration; + if ($file->version === $history->version && + $file->migration === $history->migration && + $file->namespace === $history->namespace) { + $migration->path = $file->path; + break; } } - return $migrations; + return $migration; } /** - * Créer un objet de migration à partir d'un chemin d'accès à un fichier. - * - * @param string $path Chemin d'accès complet à un fichier de migration valide. - * - * @return false|object Renvoie l'objet de migration ou false en cas d'échec. + * Récupère les migrations en attente + * + * @return array */ - protected function migrationFromFile(string $path, string $namespace) + protected function getPendingMigrations(?string $group): array { - if (substr($path, -4) !== '.php') { - return false; + $files = $this->findMigrationFiles(); + $executed = $this->history->getAll($group); + + $executedMap = []; + foreach ($executed as $item) { + $key = implode('.', [$item->namespace, $item->migration, $item->version]); + $executedMap[$key] = true; } - // Retrait de l'extension - $filename = basename($path, '.php'); - - // Si le fichier ne match pas avec le format des migrations, pas la peine de continuer - if (! preg_match($this->regex, $filename)) { - return false; - } - - $migration = new stdClass(); - - $migration->version = $this->getMigrationNumber($filename); - $migration->name = $this->getMigrationName($filename); - $migration->path = $path; - $migration->class = $this->getMigrationClass($path); - $migration->namespace = $namespace; - $migration->uid = $this->getObjectUid($migration); - - return $migration; + return array_filter($files, function($file) use ($executedMap) { + $key = implode('.', [$file->namespace, $file->migration, $file->version]); + return !isset($executedMap[$key]); + }); } /** - * Extrait le numero de la migration a partir du nom du fichier. + * Trouve tous les fichiers de migration * - * @return string Portion numerique du nom de fichier de la migration - */ - protected function getMigrationNumber(string $filename): string - { - preg_match($this->regex, $filename, $matches); - - return count($matches) ? $matches[1] : '0'; - } - - /** - * Extrait le nom de la classe de migration - */ - protected function getMigrationClass(string $path): string - { - $php = file_get_contents($path); - $tokens = token_get_all($php); - $dlm = false; - $namespace = ''; - $className = ''; - - foreach ($tokens as $i => $token) { - if ($i < 2) { - continue; - } - - if ((isset($tokens[$i - 2][1]) && ($tokens[$i - 2][1] === 'phpnamespace' || $tokens[$i - 2][1] === 'namespace')) || ($dlm && $tokens[$i - 1][0] === T_NS_SEPARATOR && $token[0] === T_STRING)) { - if (! $dlm) { - $namespace = 0; + * @return array + */ + public function findMigrationFiles(): array + { + $files = []; + + foreach ($this->paths as $namespace => $paths) { + foreach ($paths as $file) { + $name = basename($file, '.php'); + if (preg_match($this->pattern, $name, $matches)) { + $files[] = (object) [ + 'path' => $file, + 'version' => $matches[1], + 'migration' => $matches[2], + 'namespace' => $namespace, + ]; } - if (isset($token[1])) { - $namespace = $namespace ? $namespace . '\\' . $token[1] : $token[1]; - $dlm = true; - } - } elseif ($dlm && ($token[0] !== T_NS_SEPARATOR) && ($token[0] !== T_STRING)) { - $dlm = false; - } - - if (($tokens[$i - 2][0] === T_CLASS || (isset($tokens[$i - 2][1]) && $tokens[$i - 2][1] === 'phpclass')) - && $tokens[$i - 1][0] === T_WHITESPACE - && $token[0] === T_STRING) { - $className = $token[1]; - break; } } - if (empty($className)) { - return ''; - } + usort($files, fn($a, $b) => strcmp($a->version, $b->version)); - return $namespace . '\\' . $className; + return $files; } /** - * Utilise les parties non reproductibles d'une migration ou d'un historique pour créer une clé unique triable. + * Charge une instance de migration + * + * @param object $migration Données de la migration + * @param bool $fresh Forcer un rechargement frais + * + * @throws RuntimeException */ - public function getObjectUid(object $migration): string + protected function loadMigration(object $migration, bool $fresh = false): Migration { - return preg_replace('/[^0-9]/', '', $migration->version) . $migration->class; - } + $cacheKey = $migration->path . ':' . ($fresh ? 'fresh' : 'cached'); - /** - * Extrait le nom de la migration d'un nom de fichier - * - * Remarque : Le nom de la migration doit être le nom de la classe, mais peut-être le sont-ils - * différent. - * - * @param string $migration Un nom de fichier de migration sans chemin. - */ - protected function getMigrationName(string $migration): string - { - preg_match($this->regex, $migration, $matches); + // Retourner l'instance en cache si disponible et pas de rechargement frais + if (!$fresh && isset($this->loaded[$cacheKey])) { + return clone $this->loaded[$cacheKey]; + } - return count($matches) ? $matches[2] : ''; - } + // Détecter le type de migration + $content = file_get_contents($migration->path); + if ($content === false) { + throw new RuntimeException("Impossible de lire le fichier : {$migration->path}"); + } - /** - * Definit les messages CLI - */ - private function pushMessage(string $message, string $color = 'green'): self - { - $this->messages[] = compact('message', 'color'); + $isAnonymous = str_contains($content, 'return new class extends Migration'); - return $this; - } + if ($isAnonymous) { + // Mode anonyme : le fichier retourne directement l'instance + $instance = require $migration->path; + + if (!$instance instanceof Migration) { + throw new RuntimeException( + "Le fichier {$migration->path} doit retourner une instance de Migration" + ); + } + } else { + // Mode classique : chercher la classe déclarée + require_once $migration->path; + + $className = $this->extractClassName($content, $migration->namespace); + + if (!$className || !class_exists($className)) { + throw new RuntimeException( + "Impossible de trouver la classe de migration dans {$migration->path}" + ); + } + + $instance = new $className(); + } - /** - * Efface les messages CLI. - */ - public function clearMessages(): self - { - $this->messages = []; + $instance = $instance->initialize($this->dbManager, $this->db); + + // Mettre en cache pour les appels suivants + if (!$fresh) { + $this->loaded[$migration->path . ':cached'] = $instance; + } - return $this; + return $instance; } /** - * Tronque la table d'historique. + * Extrait le nom de classe du contenu PHP */ - public function clearHistory(): void + protected function extractClassName(string $content, string $namespace): ?string { - if ($this->db->tableExists($this->table)) { - $this->db->table($this->table)->truncate(); + // Chercher "class NomDeClasse extends Migration" + if (preg_match('/class\s+([a-zA-Z0-9_]+)\s+extends\s+Migration/', $content, $matches)) { + $className = $matches[1]; + + // Si le namespace est vide ou déjà présent + if (empty($namespace) || str_starts_with($className, $namespace)) { + return $className; + } + + return $namespace . '\\' . $className; } + + return null; } /** - * Ajouter un historique à la table. - */ - protected function addHistory(object $migration, int $batch): void - { - $this->db->table($this->table)->insert([ - 'version' => $migration->version, - 'class' => $migration->class, - 'group' => $this->group, - 'namespace' => $migration->namespace, - 'time' => time(), + * Exécute une migration + * + * @param object $migration Données de la migration + * @param string $direction Direction (up|down) + * @param string|null $group Groupe + * @param int|null $batch Lot (pour up) + * @return bool Succès ou échec + */ + protected function runMigration(object $migration, string $direction, ?string $group, ?int $batch = null): bool + { + $this->fire('migration.before', [ + 'migration' => $migration, + 'direction' => $direction, + 'group' => $group, 'batch' => $batch, ]); - $this->pushMessage( - sprintf( - 'Running: %s %s_%s', - $migration->namespace, - $migration->version, - $migration->class - ), - 'yellow' - ); - } - - /** - * Supprime un seul historique. - */ - protected function removeHistory(object $history): void - { - $this->db->table($this->table)->where('id', $history->id)->delete(); - - $this->pushMessage( - sprintf( - 'Rolling back: %s %s_%s', - $history->namespace, - $history->version, - $history->class - ), - 'yellow' - ); - } - - /** - * Récupère l'historique complet des migrations de la base de données pour un groupe. - */ - public function getHistory(string $group = 'default'): array - { - $this->ensureTable(); - - $builder = $this->db->table($this->table); - - // Si un groupe a été spécifié, utilisez-le. - if ($group !== '') { - $builder->where('group', $group); - } - - // Si un namespace a été spécifié, utilisez-le. - if ($this->namespace !== null) { - $builder->where('namespace', $this->namespace); - } - - return $builder->sortAsc('id')->all(); - } - - /** - * Renvoie l'historique des migrations pour un seul lot. - */ - public function getBatchHistory(int $batch, string $order = 'asc'): array - { - $this->ensureTable(); + try { + // Pour le rollback, on force un rechargement frais + $fresh = ($direction === 'down'); + $instance = $this->loadMigration($migration, $fresh); - return $this->db->table($this->table)->where('batch', $batch)->orderBy('id', $order)->all(); - } + // Vérifier si la migration doit être exécutée + if ($direction === 'up' && !$instance->shouldRun()) { + $this->fire('migration.ignored', ['migration' => $migration]); + + // On marque comme réussie pour ne pas bloquer les suivantes + return true; + } - /** - * Renvoie tous les lots de l'historique de la base de données dans l'ordre. - */ - public function getBatches(): array - { - $this->ensureTable(); + // Exécuter la migration + $start = microtime(true); + $instance->{$direction}(); + + // On doit créer un transformer pour chaque connexion utilisée + $transformers = []; + + foreach ($instance->getBuilders() as $builder) { + $conn = $builder->getConnection(); + $connKey = spl_object_hash($conn); + + if (!isset($transformers[$connKey])) { + $transformers[$connKey] = new Transformer($this->dbManager->creator($conn)); + } + + $transformers[$connKey]->process($builder); + } - $batches = $this->db->table($this->table) - ->select('batch') - ->distinct() - ->sortAsc('batch') - ->all(PDO::FETCH_ASSOC); + // Mettre à jour l'historique + if ($direction === 'up' && $batch) { + $this->history->add( + $migration->version, + $migration->migration, + $migration->namespace, + $group ?? 'default', + $batch + ); + } elseif ($direction === 'down') { + $this->removeFromHistory($migration, $group); + } - return array_map('intval', array_column($batches, 'batch')); - } + $this->fire('migration.done', [ + 'migration' => $migration, + 'duration' => number_format(microtime(true) - $start, 3), + ]); - /** - * Renvoie la valeur du dernier lot dans la base de données. - */ - public function getLastBatch(): int - { - $this->ensureTable(); + return true; + } catch (\Throwable $e) { + $this->fire('migration.error', [ + 'migration' => $migration, + 'direction' => $direction, + 'error' => $e->getMessage(), + 'exception' => $e, + ]); - return (int) $this->db->table($this->table)->max('batch'); + return false; + } } /** - * Renvoie le numéro de version de la première migration d'un lot. - * Principalement utilisé à des fins de test. + * Supprime une entrée de l'historique */ - public function getBatchStart(int $batch, int $targetBatch = 0): string + protected function removeFromHistory(object $migration, ?string $group): void { - // Convertir un lot relatif en lot absolu - if ($batch < 0) { - $batches = $this->getBatches(); - $batch = $batches[count($batches) - 1 + $targetBatch] ?? 0; + $history = $this->history->getAll($group); + + foreach ($history as $entry) { + if ($entry->migration === $migration->migration && + $entry->version === $migration->version && + $entry->namespace === $migration->namespace) { + + $this->history->remove($entry->id); + + break; + } } - - $migration = $this->db->table($this->table)->where('batch', $batch)->sortAsc('id')->first(); - - return $migration->version ?? '0'; } /** - * Renvoie le numéro de version de la dernière migration d'un lot. - * Principalement utilisé à des fins de test. + * Récupère l'historique des migrations */ - public function getBatchEnd(int $batch, int $targetBatch = 0): string + public function getHistory(?string $group = null): array { - // Convertir un lot relatif en lot absolu - if ($batch < 0) { - $batches = $this->getBatches(); - $batch = $batches[count($batches) - 1 + $targetBatch] ?? 0; - } - - $migration = $this->db->table($this->table)->where('batch', $batch)->sortDesc('id')->first(); - - return $migration->version ?? '0'; + return $this->history->getAll($group); } /** - * S'assure que nous avons créé notre table de migrations dans la base de données. + * Récupère le dernier numéro de lot */ - public function ensureTable(): void + public function getLastBatch(): int { - if ($this->tableChecked || $this->db->tableExists($this->table)) { - return; - } - - $structure = new Structure($this->table); - $structure->bigIncrements('id'); - $structure->string('version'); - $structure->string('class'); - $structure->string('group'); - $structure->string('namespace'); - $structure->integer('time'); - $structure->integer('batch')->unsigned(); - $structure->create(true); - - $transformer = new Transformer($this->db); - $transformer->process($structure); - - $this->tableChecked = true; + return $this->history->getLastBatch(); } /** - * Gère l'exécution effective d'une migration. - * - * @param string $direction "up" ou "down" - * @param object $migration La migration à exécuter + * Vide le cache des instances chargées */ - protected function migrate(string $direction, object $migration): bool + public function clearCache(): self { - include_once $migration->path; - - $class = $migration->class; - $this->setName($migration->name); - - // Valider la structure du fichier de migration - if (! class_exists($class, false)) { - $message = sprintf('The migration class "%s" could not be found.', $class); - - if ($this->silent) { - $this->pushMessage($message, 'red'); - - return false; - } + $this->loaded = []; - throw new RuntimeException($message); - } - - /** @var Migration $instance */ - $instance = new $class($this->db); - $group = $instance->getGroup() ?? $this->group; - - if ($direction === 'up' && $this->groupFilter !== null && $this->groupFilter !== $group) { - $this->groupSkip = true; - - return true; - } - - if (! is_callable([$instance, $direction])) { - $message = sprintf('The migration class is missing an "%s" method.', $direction); - - if ($this->silent) { - $this->pushMessage($message, 'red'); - - return false; - } - - throw new RuntimeException($message); - } - - $instance->{$direction}(); - - $transformer = new Transformer($this->db); - - foreach ($instance->getStructure() as $structure) { - $transformer->process($structure); - } - - return true; + return $this; } } diff --git a/src/Migration/Structure.php b/src/Migration/Structure.php deleted file mode 100644 index 442e841..0000000 --- a/src/Migration/Structure.php +++ /dev/null @@ -1,1170 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Database\Migration; - -use BlitzPHP\Database\Migration\Definitions\Column; -use BlitzPHP\Database\Migration\Definitions\ForeignId; -use BlitzPHP\Database\Migration\Definitions\ForeignKey; -use BlitzPHP\Utilities\Support\Fluent; -use Closure; - -/** - * Classe pour define la structure de la table a migrer. - * - * @credit Laravel Framework - Illuminate\Database\Schema\Blueprint - */ -class Structure -{ - /** - * Nom de la table - */ - protected string $table; - - /** - * Prefixe de la table - */ - protected string $prefix; - - /** - * @var list Colonnes que l'on veut ajouter a la table - */ - protected array $columns = []; - - /** - * @var list Commandes qu'on souhaite executer sur la table. - */ - protected array $commands = []; - - /** - * Moteur de stockage a utiliser sur la table. - */ - public string $engine = ''; - - /** - * Charset par défaut - */ - public string $charset = ''; - - /** - * Collation par defaut. - */ - public string $collation = ''; - - /** - * Doit-on creer une table temporaire. - */ - public bool $temporary = false; - - /** - * The column to add new columns after. - */ - public string $after = ''; - - public function __construct(string $table, ?Closure $callback = null, string $prefix = '') - { - $this->table = $table; - $this->prefix = $prefix; - - if (null !== $callback) { - $callback($this); - } - } - - /** - * Indique qu'on veut ajouter une colonne a la table - */ - public function add(): Fluent - { - return $this->addCommand('add'); - } - - /** - * Indique qu'on veut creer la table. - */ - public function create(bool $ifNotExists = false): Fluent - { - return $this->addCommand('create', compact('ifNotExists')); - } - - /** - * Indique qu'on veut modifier la table. - */ - public function modify(): Fluent - { - return $this->addCommand('modify'); - } - - /** - * Indique qu'on veut une table temporaire. - * - * @return void - */ - public function temporary() - { - $this->temporary = true; - } - - /** - * Indique qu'on veut supprimer la table. - */ - public function drop(bool $ifExists = false): Fluent - { - if ($ifExists) { - return $this->dropIfExists(); - } - - return $this->addCommand('drop'); - } - - /** - * Indique qu'on veut supprimer la table si elle existe. - */ - public function dropIfExists(): Fluent - { - return $this->addCommand('dropIfExists'); - } - - /** - * Indique qu'on veut supprimer un champs. - * - * @param array|mixed $columns - */ - public function dropColumn($columns): Fluent - { - $columns = is_array($columns) ? $columns : func_get_args(); - - return $this->addCommand('dropColumn', compact('columns')); - } - - /** - * Indique qu'on veut renommer un champs. - */ - public function renameColumn(string $from, string $to): Fluent - { - return $this->addCommand('renameColumn', compact('from', 'to')); - } - - /** - * Indique qu'on veut supprimer une cle primaire. - */ - public function dropPrimary(array|string|null $index = null): Fluent - { - return $this->dropIndexCommand('dropPrimary', 'primary', $index); - } - - /** - * Indique qu'on veut supprimer une cle unique. - */ - public function dropUnique(array|string $index): Fluent - { - return $this->dropIndexCommand('dropUnique', 'unique', $index); - } - - /** - * Indique qu'on veut supprimer un index. - */ - public function dropIndex(array|string $index): Fluent - { - return $this->dropIndexCommand('dropIndex', 'index', $index); - } - - /** - * Indicate that the given fulltext index should be dropped. - */ - public function dropFullText(array|string $index): Fluent - { - return $this->dropIndexCommand('dropFullText', 'fulltext', $index); - } - - /** - * Indique qu'on veut supprimer un index spacial. - */ - public function dropSpatialIndex(array|string $index): Fluent - { - return $this->dropIndexCommand('dropSpatialIndex', 'spatialIndex', $index); - } - - /** - * Indique qu'on veut supprimer une cle etrangere. - */ - public function dropForeign(array|string $index): Fluent - { - return $this->dropIndexCommand('dropForeign', 'foreign', $index); - } - - /** - * Indicate that the given column and foreign key should be dropped. - */ - public function dropConstrainedForeignId(string $column): Fluent - { - $this->dropForeign([$column]); - - return $this->dropColumn($column); - } - - /** - * Indique qu'on veut renommer un indexe. - */ - public function renameIndex(string $from, string $to): Fluent - { - return $this->addCommand('renameIndex', compact('from', 'to')); - } - - /** - * Indique qu'on veut supprimer les colones de type timestamp. - */ - public function dropTimestamps(): Column - { - return $this->dropColumn('created_at', 'updated_at'); - } - - /** - * Indique qu'on veut supprimer les colones de type timestamp. - */ - public function dropTimestampsTz(): Column - { - return $this->dropTimestamps(); - } - - /** - * Indicate that the soft delete column should be dropped. - */ - public function dropSoftDeletes(string $column = 'deleted_at'): void - { - $this->dropColumn($column); - } - - /** - * Indicate that the soft delete column should be dropped. - */ - public function dropSoftDeletesTz(string $column = 'deleted_at'): void - { - $this->dropSoftDeletes($column); - } - - /** - * Indicate that the remember token column should be dropped. - */ - public function dropRememberToken(): void - { - $this->dropColumn('remember_token'); - } - - /** - * Indique qu'on veut supprimer les colones polymorphe. - * - * @return void - */ - public function dropMorphs(string $name, ?string $indexName = null) - { - $this->dropIndex($indexName ?: $this->createIndexName('index', ["{$name}_type", "{$name}_id"])); - - $this->dropColumn("{$name}_type", "{$name}_id"); - } - - /** - * Rennome la table avec le nom donné. - */ - public function rename(string $to): Fluent - { - return $this->addCommand('rename', compact('to')); - } - - /** - * Specifie les clés primaire de la table. - */ - public function primary(array|string $columns, ?string $name = null, ?string $algorithm = null): Fluent - { - return $this->indexCommand('primary', $columns, $name, $algorithm); - } - - /** - * Specifie un indexe unique pour la table. - */ - public function unique(array|string $columns, ?string $name = null, ?string $algorithm = null): Fluent - { - return $this->indexCommand('unique', $columns, $name, $algorithm); - } - - /** - * Specifie un index pour la table. - */ - public function index(array|string $columns, ?string $name = null, ?string $algorithm = null): Fluent - { - return $this->indexCommand('index', $columns, $name, $algorithm); - } - - /** - * Specify an fulltext for the table. - */ - public function fullText(array|string $columns, ?string $name = null, ?string $algorithm = null): Fluent - { - return $this->indexCommand('fulltext', $columns, $name, $algorithm); - } - - /** - * Specifie un index spacial pour la table. - */ - public function spatialIndex(array|string $columns, ?string $name = null): Fluent - { - return $this->indexCommand('spatialIndex', $columns, $name); - } - - /** - * Specifie une clé étrangère pour la table. - * - * @return ForeignKey - */ - public function foreign(array|string $columns, ?string $name = null): Fluent - { - return $this->indexCommand('foreign', $columns, $name); - } - - /** - * Create a new auto-incrementing big integer (8-byte) column on the table. - */ - public function id(string $column = 'id'): Column - { - return $this->bigIncrements($column); - } - - /** - * Créé une nouvelle colonne de type entier auto-incrementé (4-byte) sur la table. - */ - public function increments(string $column): Column - { - return $this->unsignedInteger($column, true)->primary(); - } - - /** - * Créé une nouvelle colonne de type entier auto-incrementé (4-byte) sur la table. - */ - public function integerIncrements(string $column): Column - { - return $this->increments($column); - } - - /** - * Créé une nouvelle colonne de type tiny-integer auto-incrementé (1-byte) sur la table. - */ - public function tinyIncrements(string $column): Column - { - return $this->unsignedTinyInteger($column, true)->primary(); - } - - /** - * Créé une nouvelle colonne de type small integer auto-incrementé (2-byte) sur la table. - */ - public function smallIncrements(string $column): Column - { - return $this->unsignedSmallInteger($column, true)->primary(); - } - - /** - * Create a new auto-incrementing medium integer (3-byte) column on the table. - */ - public function mediumIncrements(string $column): Column - { - return $this->unsignedMediumInteger($column, true)->primary(); - } - - /** - * Create a new auto-incrementing big integer (8-byte) column on the table. - */ - public function bigIncrements(string $column): Column - { - return $this->unsignedBigInteger($column, true)->primary(); - } - - /** - * Create a new char column on the table. - */ - public function char(string $column, ?int $length = null): Column - { - $length = max($length ?: Runner::$defaultStringLength, 1); - - return $this->addColumn('char', $column, compact('length')); - } - - /** - * Create a new string column on the table. - */ - public function string(string $column, ?int $length = null): Column - { - $length = max($length ?: Runner::$defaultStringLength, 1); - - return $this->addColumn('string', $column, compact('length')); - } - - /** - * Create a new tiny text column on the table. - */ - public function tinyText(string $column): Column - { - return $this->addColumn('tinyText', $column); - } - - /** - * Create a new text column on the table. - */ - public function text(string $column): Column - { - return $this->addColumn('text', $column); - } - - /** - * Create a new medium text column on the table. - */ - public function mediumText(string $column): Column - { - return $this->addColumn('mediumText', $column); - } - - /** - * Create a new long text column on the table. - */ - public function longText(string $column): Column - { - return $this->addColumn('longText', $column); - } - - /** - * Create a new integer (4-byte) column on the table. - */ - public function integer(string $column, bool $autoIncrement = false, bool $unsigned = false): Column - { - return $this->addColumn('integer', $column, compact('autoIncrement', 'unsigned')); - } - - /** - * Create a new tiny integer (1-byte) column on the table. - */ - public function tinyInteger(string $column, bool $autoIncrement = false, bool $unsigned = false): Column - { - return $this->addColumn('tinyInteger', $column, compact('autoIncrement', 'unsigned')); - } - - /** - * Create a new small integer (2-byte) column on the table. - */ - public function smallInteger(string $column, bool $autoIncrement = false, bool $unsigned = false): Column - { - return $this->addColumn('smallInteger', $column, compact('autoIncrement', 'unsigned')); - } - - /** - * Create a new medium integer (3-byte) column on the table. - */ - public function mediumInteger(string $column, bool $autoIncrement = false, bool $unsigned = false): Column - { - return $this->addColumn('mediumInteger', $column, compact('autoIncrement', 'unsigned')); - } - - /** - * Create a new big integer (8-byte) column on the table. - */ - public function bigInteger(string $column, bool $autoIncrement = false, bool $unsigned = false): Column - { - return $this->addColumn('bigInteger', $column, compact('autoIncrement', 'unsigned')); - } - - /** - * Create a new unsigned integer (4-byte) column on the table. - */ - public function unsignedInteger(string $column, bool $autoIncrement = false): Column - { - return $this->integer($column, $autoIncrement, true); - } - - /** - * Create a new unsigned tiny integer (1-byte) column on the table. - */ - public function unsignedTinyInteger(string $column, bool $autoIncrement = false): Column - { - return $this->tinyInteger($column, $autoIncrement, true); - } - - /** - * Create a new unsigned small integer (2-byte) column on the table. - */ - public function unsignedSmallInteger(string $column, bool $autoIncrement = false): Column - { - return $this->smallInteger($column, $autoIncrement, true); - } - - /** - * Create a new unsigned medium integer (3-byte) column on the table. - */ - public function unsignedMediumInteger(string $column, bool $autoIncrement = false): Column - { - return $this->mediumInteger($column, $autoIncrement, true); - } - - /** - * Create a new unsigned big integer (8-byte) column on the table. - */ - public function unsignedBigInteger(string $column, bool $autoIncrement = false): Column - { - return $this->bigInteger($column, $autoIncrement, true); - } - - /** - * Create a new unsigned big integer (8-byte) column on the table. - */ - public function foreignId(string $column): ForeignId - { - return $this->addColumnDefinition(new ForeignId($this, [ - 'type' => 'bigInteger', - 'name' => $column, - 'autoIncrement' => false, - 'unsigned' => true, - ])); - } - - /** - * Create a new float column on the table. - * - * @param mixed $unsigned - */ - public function float(string $column, int $total = 8, int $places = 2, $unsigned = false): Column - { - return $this->addColumn('float', $column, compact('total', 'places', 'unsigned')); - } - - /** - * Create a new double column on the table. - * - * @param mixed $unsigned - */ - public function double(string $column, ?int $total = null, ?int $places = null, $unsigned = false): Column - { - return $this->addColumn('double', $column, compact('total', 'places', 'unsigned')); - } - - /** - * Create a new decimal column on the table. - * - * @param mixed $unsigned - */ - public function decimal(string $column, int $total = 8, int $places = 2, $unsigned = false): Column - { - return $this->addColumn('decimal', $column, compact('total', 'places', 'unsigned')); - } - - /** - * Create a new unsigned float column on the table. - */ - public function unsignedFloat(string $column, int $total = 8, int $places = 2): Column - { - return $this->float($column, $total, $places, true); - } - - /** - * Create a new unsigned double column on the table. - */ - public function unsignedDouble(string $column, ?int $total = null, ?int $places = null): Column - { - return $this->double($column, $total, $places, true); - } - - /** - * Create a new unsigned decimal column on the table. - */ - public function unsignedDecimal(string $column, int $total = 8, int $places = 2): Column - { - return $this->decimal($column, $total, $places, true); - } - - /** - * Create a new boolean column on the table. - */ - public function boolean(string $column): Column - { - return $this->addColumn('boolean', $column); - } - - /** - * Create a new enum column on the table. - */ - public function enum(string $column, array $allowed): Column - { - return $this->addColumn('enum', $column, compact('allowed')); - } - - /** - * Create a new set column on the table. - */ - public function set(string $column, array $allowed): Column - { - return $this->addColumn('set', $column, compact('allowed')); - } - - /** - * Create a new json column on the table. - */ - public function json(string $column): Column - { - return $this->addColumn('json', $column); - } - - /** - * Create a new jsonb column on the table. - */ - public function jsonb(string $column): Column - { - return $this->addColumn('jsonb', $column); - } - - /** - * Create a new date column on the table. - */ - public function date(string $column): Column - { - return $this->addColumn('date', $column); - } - - /** - * Create a new date-time column on the table. - */ - public function dateTime(string $column, int $precision = 0): Column - { - return $this->addColumn('dateTime', $column, compact('precision')); - } - - /** - * Create a new date-time column (with time zone) on the table. - */ - public function dateTimeTz(string $column, int $precision = 0): Column - { - return $this->addColumn('dateTimeTz', $column, compact('precision')); - } - - /** - * Create a new time column on the table. - */ - public function time(string $column, int $precision = 0): Column - { - return $this->addColumn('time', $column, compact('precision')); - } - - /** - * Create a new time column (with time zone) on the table. - */ - public function timeTz(string $column, int $precision = 0): Column - { - return $this->addColumn('timeTz', $column, compact('precision')); - } - - /** - * Create a new timestamp column on the table. - */ - public function timestamp(string $column, int $precision = 0): Column - { - return $this->addColumn('timestamp', $column, compact('precision')); - } - - /** - * Create a new timestamp (with time zone) column on the table. - */ - public function timestampTz(string $column, int $precision = 0): Column - { - return $this->addColumn('timestampTz', $column, compact('precision')); - } - - /** - * Add nullable creation and update timestamps to the table. - */ - public function timestamps(int $precision = 0): void - { - $this->timestamp('created_at', $precision)->nullable(); - - $this->timestamp('updated_at', $precision)->nullable(); - } - - /** - * Add nullable creation and update timestamps to the table. - * - * Alias for self::timestamps(). - */ - public function nullableTimestamps(int $precision = 0): void - { - $this->timestamps($precision); - } - - /** - * Add creation and update timestampTz columns to the table. - * - * @return void - */ - public function timestampsTz(int $precision = 0) - { - $this->timestampTz('created_at', $precision)->nullable(); - - $this->timestampTz('updated_at', $precision)->nullable(); - } - - /** - * Add a "deleted at" timestamp for the table. - */ - public function softDeletes(string $column = 'deleted_at', int $precision = 0): Column - { - return $this->timestamp($column, $precision)->nullable(); - } - - /** - * Add a "deleted at" timestampTz for the table. - */ - public function softDeletesTz(string $column = 'deleted_at', int $precision = 0): Column - { - return $this->timestampTz($column, $precision)->nullable(); - } - - /** - * Create a new year column on the table. - */ - public function year(string $column): Column - { - return $this->addColumn('year', $column); - } - - /** - * Create a new binary column on the table. - */ - public function binary(string $column): Column - { - return $this->addColumn('binary', $column); - } - - /** - * Create a new uuid column on the table. - */ - public function uuid(string $column): Column - { - return $this->addColumn('uuid', $column); - } - - /** - * Create a new UUID column on the table with a foreign key constraint. - */ - public function foreignUuid(string $column): ForeignId - { - return $this->addColumnDefinition(new ForeignId($this, [ - 'type' => 'uuid', - 'name' => $column, - ])); - } - - /** - * Create a new ULID column on the table. - */ - public function ulid(string $column = 'uuid', int $length = 26): Column - { - return $this->char($column, $length); - } - - /** - * Create a new ULID column on the table with a foreign key constraint. - */ - public function foreignUlid(string $column, int $length = 26): ForeignId - { - return $this->addColumnDefinition(new ForeignId($this, [ - 'type' => 'char', - 'name' => $column, - 'length' => $length, - ])); - } - - /** - * Create a new IP address column on the table. - */ - public function ipAddress(string $column = 'ip_address'): Column - { - return $this->addColumn('ipAddress', $column); - } - - /** - * Create a new MAC address column on the table. - */ - public function macAddress(string $column = 'mac_address'): Column - { - return $this->addColumn('macAddress', $column); - } - - /** - * Create a new geometry column on the table. - */ - public function geometry(string $column): Column - { - return $this->addColumn('geometry', $column); - } - - /** - * Create a new point column on the table. - */ - public function point(string $column, ?int $srid = null): Column - { - return $this->addColumn('point', $column, compact('srid')); - } - - /** - * Create a new linestring column on the table. - */ - public function lineString(string $column): Column - { - return $this->addColumn('linestring', $column); - } - - /** - * Create a new polygon column on the table. - */ - public function polygon(string $column): Column - { - return $this->addColumn('polygon', $column); - } - - /** - * Create a new geometrycollection column on the table. - */ - public function geometryCollection(string $column): Column - { - return $this->addColumn('geometrycollection', $column); - } - - /** - * Create a new multipoint column on the table. - */ - public function multiPoint(string $column): Column - { - return $this->addColumn('multipoint', $column); - } - - /** - * Create a new multilinestring column on the table. - */ - public function multiLineString(string $column): Column - { - return $this->addColumn('multilinestring', $column); - } - - /** - * Create a new multipolygon column on the table. - */ - public function multiPolygon(string $column): Column - { - return $this->addColumn('multipolygon', $column); - } - - /** - * Create a new multipolygon column on the table. - */ - public function multiPolygonZ(string $column): Column - { - return $this->addColumn('multipolygonz', $column); - } - - /** - * Create a new generated, computed column on the table. - */ - public function computed(string $column, string $expression): Column - { - return $this->addColumn('computed', $column, compact('expression')); - } - - /** - * Add the proper columns for a polymorphic table. - */ - public function morphs(string $name, ?string $indexName = null): void - { - $this->string("{$name}_type"); - - $this->unsignedBigInteger("{$name}_id"); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add nullable columns for a polymorphic table. - */ - public function nullableMorphs(string $name, ?string $indexName = null): void - { - $this->string("{$name}_type")->nullable(); - - $this->unsignedBigInteger("{$name}_id")->nullable(); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add the proper columns for a polymorphic table using numeric IDs (incremental). - */ - public function numericMorphs(string $name, ?string $indexName = null): void - { - $this->string("{$name}_type"); - - $this->unsignedBigInteger("{$name}_id"); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add nullable columns for a polymorphic table using numeric IDs (incremental). - */ - public function nullableNumericMorphs(string $name, ?string $indexName = null): void - { - $this->string("{$name}_type")->nullable(); - - $this->unsignedBigInteger("{$name}_id")->nullable(); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add the proper columns for a polymorphic table using UUIDs. - */ - public function uuidMorphs(string $name, ?string $indexName = null): void - { - $this->string("{$name}_type"); - - $this->uuid("{$name}_id"); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add nullable columns for a polymorphic table using UUIDs. - */ - public function nullableUuidMorphs(string $name, ?string $indexName = null): void - { - $this->string("{$name}_type")->nullable(); - - $this->uuid("{$name}_id")->nullable(); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add the proper columns for a polymorphic table using ULIDs. - */ - public function ulidMorphs(string $name, ?string $indexName = null): void - { - $this->string("{$name}_type"); - - $this->ulid("{$name}_id"); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Add nullable columns for a polymorphic table using ULIDs. - */ - public function nullableUlidMorphs(string $name, ?string $indexName = null): void - { - $this->string("{$name}_type")->nullable(); - - $this->ulid("{$name}_id")->nullable(); - - $this->index(["{$name}_type", "{$name}_id"], $indexName); - } - - /** - * Adds the `remember_token` column to the table. - */ - public function rememberToken(): Column - { - return $this->string('remember_token', 100)->nullable(); - } - - /** - * Add a comment to the table. - */ - public function comment(string $comment): Fluent - { - return $this->addCommand('tableComment', compact('comment')); - } - - /** - * Add a new index command to the structure. - */ - protected function indexCommand(string $type, array|string $columns, ?string $index = null, ?string $algorithm = null): Fluent - { - $columns = (array) $columns; - - // If no name was specified for this index, we will create one using a basic - // convention of the table name, followed by the columns, followed by an - // index type, such as primary or index, which makes the index unique. - $index = $index ?: $this->createIndexName($type, $columns); - - return $this->addCommand( - $type, - compact('index', 'columns', 'algorithm') - ); - } - - /** - * Cree une nouvelle commande de suppression d'indexe dans la structure. - */ - protected function dropIndexCommand(string $command, string $type, array|string $index): Fluent - { - $columns = []; - - // If the given "index" is actually an array of columns, the developer means - // to drop an index merely by specifying the columns involved without the - // conventional name, so we will build the index name from the columns. - if (is_array($index)) { - $index = $this->createIndexName($type, $columns = $index); - } - - return $this->indexCommand($command, $columns, $index); - } - - /** - * Create a default index name for the table. - */ - protected function createIndexName(string $type, array $columns): string - { - $index = strtolower($this->prefix . $this->table . '_' . implode('_', $columns) . '_' . $type); - - return str_replace(['-', '.'], '_', $index); - } - - /** - * Ajoute un champ a la structure. - */ - public function addColumn(string $type, string $name, array $parameters = []): Column - { - return $this->addColumnDefinition(new Column( - array_merge(compact('type', 'name'), $parameters) - )); - } - - /** - * Retire un champ a la structure - */ - public function removeColumn(string $name): self - { - $this->columns = array_values(array_filter($this->columns, static fn ($c) => $c['name'] !== $name)); - - return $this; - } - - /** - * Add the columns from the callback after the given column. - */ - public function after(string $column, Closure $callback): void - { - $this->after = $column; - - $callback($this); - - $this->after = null; - } - - /** - * Add a new column definition to the structure. - */ - protected function addColumnDefinition(Column $definition): Column - { - $this->columns[] = $definition; - - if ($this->after) { - $definition->after($this->after); - - $this->after = $definition->name; - } - - return $definition; - } - - /** - * Ajoute une nouvelle commande a la structure - */ - protected function addCommand(string $name, array $parameters = []): Fluent - { - $this->commands[] = $command = $this->createCommand($name, $parameters); - - return $command; - } - - /** - * Cree une nouvelle commande - */ - protected function createCommand(string $name, array $parameters = []): Fluent - { - return new Fluent(array_merge(compact('name'), $parameters)); - } - - /** - * Recupere la table que la structure decrit. - * - * @internal utilisee par le `transformer` - */ - public function getTable(): string - { - return $this->table; - } - - /** - * Get the columns on the schema. - * - * @return list - * - * @internal utilisee par le `transformer` - */ - public function getColumns(?bool $added = null): array - { - if ($added === null) { - return $this->columns; - } - - if ($added === true) { - return $this->getAddedColumns(); - } - - return $this->getChangedColumns(); - } - - /** - * Get the commands on the schema. - * - * @return list - * - * @internal utilisee par le `transformer` - */ - public function getCommands(): array - { - return $this->commands; - } - - /** - * Recupere les colones de la structure qui doivent etre ajoutees. - * - * @return list - * - * @internal utilisee par le `transformer` - */ - public function getAddedColumns(): array - { - return array_filter($this->columns, static fn ($column) => ! $column->change); - } - - /** - * Recupere les colones de la structure qui doivent etre modifiees. - * - * @return list - * - * @internal utilisee par le `transformer` - */ - public function getChangedColumns(): array - { - return array_filter($this->columns, static fn ($column) => (bool) $column->change); - } -} diff --git a/src/Migration/Transformer.php b/src/Migration/Transformer.php index 63c6516..4ccc6f1 100644 --- a/src/Migration/Transformer.php +++ b/src/Migration/Transformer.php @@ -11,318 +11,341 @@ namespace BlitzPHP\Database\Migration; -use BlitzPHP\Contracts\Database\ConnectionInterface; use BlitzPHP\Database\Creator\BaseCreator; -use BlitzPHP\Database\Database; use BlitzPHP\Database\Exceptions\MigrationException; use BlitzPHP\Database\Migration\Definitions\Column; -use BlitzPHP\Database\RawSql; -use BlitzPHP\Utilities\Helpers; -use BlitzPHP\Utilities\Support\Fluent; +use BlitzPHP\Database\Migration\Definitions\ForeignKey; +use BlitzPHP\Database\Migration\Definitions\Index; +use BlitzPHP\Database\Query\Expression; /** * Transforme les objets de structure en elements compatible avec le Creator */ class Transformer { - private BaseCreator $creator; - - public function __construct(ConnectionInterface $db) + public function __construct(private BaseCreator $creator) { - $this->creator = Database::creator($db); } /** * Demarrage de la manipulation de la base de donnees */ - public function process(Structure $structure) + public function process(Builder $builder) { - $commands = $this->getCommands($structure); - - $commandsName = array_map(static fn ($command) => $command->name, $commands); - - if (in_array('create', $commandsName, true)) { - $this->createTable($structure, $commands); - } elseif (in_array('modify', $commandsName, true)) { - $this->modifyTable($structure, $commands); - } elseif (in_array('rename', $commandsName, true)) { - $command = array_filter($commands, static fn ($command) => $command->name === 'rename'); - - $this->renameTable($structure->getTable(), $command[0]->to); - } elseif (in_array('drop', $commandsName, true)) { - $this->dropTable($structure->getTable()); - } elseif (in_array('dropIfExists', $commandsName, true)) { - $this->dropTable($structure->getTable(), true); - } + $table = $builder->getTable(); + $action = $builder->getAction(); + + $this->addFluentIndexes($builder); + + match ($action) { + 'create' => $this->createTable($builder), + 'createIfNotExists' => $this->createTable($builder, true), + 'alter' => $this->alterTable($builder), + 'drop' => $this->creator->dropTable($table, false), + 'dropIfExists' => $this->creator->dropTable($table, true), + 'rename' => $this->creator->renameTable($table, $builder->getRenames()['to']), + default => null, + }; } /** - * Creation d'une nouvelle table + * Crée une nouvelle table */ - public function createTable(Structure $structure, array $commands = []): void + protected function createTable(Builder $builder, bool $ifNotExists = false): void { - $ifNotExists = array_filter($commands, fn ($command) => $command->name === 'create' && $this->is($command, 'ifNotExists')); + $primaryKeyColumns = []; + $autoIncrementColumns = []; + $explicitPrimaryColumns = []; + + foreach ($builder->getColumns() as $column) { + $this->creator->addField([ + $column->name => $this->makeColumn($column) + ]); + + if ($column->autoIncrement ?? false) { + $autoIncrementColumns[] = $column->name; + } + + if ($column->primary ?? false) { + $explicitPrimaryColumns[] = $column->name; + } + } + + // Détermination de la clé primaire + if ($explicitPrimaryColumns !== []) { + // L'utilisateur a explicitement demandé une clé primaire + $primaryKeyColumns = $explicitPrimaryColumns; + } elseif (count($autoIncrementColumns) === 1) { + // Une seule colonne auto_increment => c'est la clé primaire + $primaryKeyColumns = $autoIncrementColumns; + } elseif (count($autoIncrementColumns) > 1) { + // Plusieurs auto_increment (cas rare) - on prévient + trigger_error( + "Plusieurs colonnes auto_increment détectées. Utilisez primary() pour spécifier la clé primaire.", + E_USER_WARNING + ); + } - foreach ($this->getColumns($structure, true) as $column) { - $this->creator->addField([$column->name => $this->makeColumn($column)]); - $this->processKeys($column); + if ($primaryKeyColumns !== []) { + $this->creator->addPrimaryKey( + $primaryKeyColumns, + $builder->createIndexName('primary', $primaryKeyColumns) + ); + } + + foreach ($builder->getIndexes() as $index) { + $this->addIndex($index); } - foreach ($commands as $command) { - $this->processCommand($command); + foreach ($builder->getForeignKeys() as $foreignKey) { + $this->addForeignKey($foreignKey); } $attributes = []; - if ($structure->engine !== '') { - $attributes['ENGINE'] = $structure->engine; + if ('' !== $engine = $builder->getEngine()) { + $attributes['ENGINE'] = $engine; } - if ($structure->charset !== '') { - $attributes['DEFAULT CHARACTER SET'] = $structure->charset; + if ('' !== $charset = $builder->getCharset()) { + $attributes['DEFAULT CHARACTER SET'] = $charset; } - if ($structure->collation !== '') { - $attributes['COLLATE'] = $structure->collation; + if ('' !== $collation = $builder->getCollation()) { + $attributes['COLLATE'] = $collation; } - $this->creator->createTable($structure->getTable(), $ifNotExists !== [], $attributes); + $this->creator->createTable($builder->getTable(), $ifNotExists, $attributes); } /** - * Modification d'une table + * Modifie une table existante */ - public function modifyTable(Structure $structure, array $commands = []): void + protected function alterTable(Builder $builder): void { - $table = $structure->getTable(); + $table = $builder->getTable(); - foreach ($this->getColumns($structure, true) as $column) { + // Colonnes ajoutées + foreach ($builder->getAddedColumns() as $column) { $this->creator->addColumn($table, [$column->name => $this->makeColumn($column)]); - $this->processKeys($column); $this->creator->processIndexes($table); } - foreach ($this->getColumns($structure, false) as $column) { + // Colonnes modifiées + foreach ($builder->getChangedColumns() as $column) { $this->creator->modifyColumn($table, [$column->name => $this->makeColumn($column)]); - $this->processKeys($column); $this->creator->processIndexes($table); } - foreach ($commands as $command) { - if ($command->name === 'dropColumn') { - $this->creator->dropColumn($table, $command->columns); - } elseif ($command->name === 'renameColumn') { - $this->creator->renameColumn($table, $command->from, $command->to); - } elseif ($command->name === 'dropIndex') { - $this->creator->dropKey($table, $command->columns); - } elseif ($command->name === 'dropUnique') { - $this->creator->dropKey($table, $command->index); - } elseif ($command->name === 'dropForeign') { - $this->creator->dropForeignKey($table, $command->index); - } elseif ($command->name === 'dropPrimary') { - $this->creator->dropPrimaryKey($table, $command->index); - } + // Colonnes supprimées + foreach ($builder->getDrops() as $type => $items) { + $this->processDrops($table, $type, $items); + } - if ($this->processCommand($command)) { - $this->creator->processIndexes($table); - } + // Nouveaux index + foreach ($builder->getIndexes() as $index) { + $this->addIndex($index); + $this->creator->processIndexes($table); } - } - /** - * Suppression d'une table - */ - public function dropTable(string $table, bool $ifExists = true): void - { - $this->creator->dropTable($table, $ifExists); + // Nouvelles clés étrangères + foreach ($builder->getForeignKeys() as $foreignKey) { + $this->addForeignKey($foreignKey); + $this->creator->processIndexes($table); + } } - /** - * Renommage d'une table - */ - public function renameTable(string $table, string $to): void - { - $this->creator->renameTable($table, $to); - } + /** - * Traite les clés d'une colonne donnée. - * - * Cette fonction vérifie si la colonne est une clé primaire, une clé unique ou un index, - * et ajoute la clé appropriée au créateur. + * Convertit les index fluides des colonnes en commandes explicites */ - private function processKeys(object $column): void + protected function addFluentIndexes(Builder $builder): void { - if ($this->is($column, 'primary')) { - $this->creator->addPrimaryKey($column->name); - } elseif ($this->is($column, 'unique')) { - $this->creator->addUniqueKey($column->name); - } elseif ($this->is($column, 'index')) { - $this->creator->addKey($column->name); + $existingIndexes = []; + foreach ($builder->getIndexes() as $index) { + $key = $index->type . ':' . implode(',', $index->columns); + $existingIndexes[$key] = true; + } + + foreach ($builder->getColumns() as $column) { + // Ignorer les colonnes qui n'ont pas d'index + if (!$column->hasFluentIndexes()) { + continue; + } + + foreach ($column->getFluentIndexes() as $indexType => $value) { + // Gestion spéciale pour les index vectoriels (si supportés) + $indexMethod = $indexType === 'index' && $column->type === 'vector' + ? 'vectorIndex' + : $indexType; + + // Créer une clé unique pour cet index potentiel + $indexKey = $indexType . ':' . $column->name; + + // Vérifier si un index similaire existe déjà + if (isset($existingIndexes[$indexKey])) { + // Ignorer car déjà ajouté explicitement + continue; + } + + // Cas 1: $value === true (index avec nom auto-généré) + if ($value === true) { + // Éviter la duplication de clé primaire pour auto-increment (MySQL) + if ($indexType === 'primary' && $column->autoIncrement && $this->creator->getConnection()->getDriver() === 'mysql') { + continue; + } + + $builder->{$indexMethod}($column->name); + $existingIndexes[$indexKey] = true; + } + + // Cas 2: $value === false (suppression d'index) + elseif ($value === false && $column->change) { + $dropMethod = 'drop' . ucfirst($indexMethod); + $builder->{$dropMethod}([$column->name]); + } + + // Cas 3: $value est une chaîne (nom d'index explicite) + elseif (is_string($value)) { + $builder->{$indexMethod}($column->name, $value); + $existingIndexes[$indexKey] = true; + } + } + + // Nettoyer les attributs d'index pour éviter les doublons + foreach (array_keys($column->getFluentIndexes()) as $indexType) { + unset($column[$indexType]); + } } } /** - * Traiter une commande de modification de table. - * - * Cette fonction traite différents types de commandes de modification d'une table de base de données, notamment l'ajout de clés primaires, de clés uniques, d'index et de clés étrangères, - * y compris l'ajout de clés primaires, de clés uniques, d'index et de clés étrangères. - * - * @param object $command Objet de commande contenant les détails de la modification à effectuer. - * Propriétés attendues : - * - name: string (Le type de commande : 'primary', 'unique', 'index', ou 'foreign') - * - columns: string|array (colonne(s) affectée(s) par la commande) - * - index: string|null (le nom de l'index, le cas échéant) - * Pour les commandes de clés étrangères : - * - on: string (La table référencée) - * - references: string (La colonne référencée) - * - cascadeOnDelete, restrictOnDelete, nullOnDelete, noActionOnDelete: bool - * - cascadeOnUpdate, restrictOnUpdate, nullOnUpdate, noActionOnUpdate: bool - * - onDelete, onUpdate: string (actions `ON DELETE` et `ON UPDATE` personnalisées) - * - * @return bool Retourne true si une commande a été traitée, false sinon. + * Ajoute un index */ - private function processCommand($command): bool + protected function addIndex(Index $index): void { - $process = false; - - if ($command->name === 'primary') { - $this->creator->addPrimaryKey($command->columns, $command->index); - $process = true; - } elseif ($command->name === 'unique') { - $this->creator->addUniqueKey($command->columns, $command->index); - $process = true; - } elseif ($command->name === 'index') { - $this->creator->addKey($command->columns, false, false, $command->index); - $process = true; - } elseif ($command->name === 'foreign') { - $onDelete = match (true) { - ($command->cascadeOnDelete ?? null) === true => 'cascade', - ($command->restrictOnDelete ?? null) === true => 'restrict', - ($command->nullOnDelete ?? null) === true => 'set null', - ($command->noActionOnDelete ?? null) === true => 'no action', - default => $command->onDelete ?? '' - }; - $onUpdate = match (true) { - ($command->cascadeOnUpdate ?? null) === true => 'cascade', - ($command->restrictOnUpdate ?? null) === true => 'restrict', - ($command->nullOnUpdate ?? null) === true => 'set null', - ($command->noActionOnUpdate ?? null) === true => 'no action', - default => $command->onUpdate ?? '' - }; - $this->creator->addForeignKey( - $command->columns ?? '', - $command->on ?? '', - $command->references ?? '', - $onUpdate, - $onDelete, - $command->index ?? '' - ); - $process = true; - } - - return $process; + $type = $index->type; + $columns = $index->columns; + $name = $index->name ?? ''; + + match ($type) { + 'primary' => $this->creator->addPrimaryKey($columns, $name), + 'unique' => $this->creator->addUniqueKey($columns, $name), + 'index' => $this->creator->addKey($columns, false, false, $name), + default => null, + }; } /** - * Recupere les colonnes a prendre en compte. + * Ajoute une clé étrangère */ - private function getColumns(Structure $structure, ?bool $added = null): array + protected function addForeignKey(ForeignKey $fk): void { - $columns = Helpers::collect($structure->getColumns($added))->map(static fn (Column $column) => $column->getAttributes())->all(); - - return array_map(static fn ($column) => (object) $column, $columns); + $onDelete = match (true) { + ($fk->cascadeOnDelete ?? null) === true => 'cascade', + ($fk->restrictOnDelete ?? null) === true => 'restrict', + ($fk->nullOnDelete ?? null) === true => 'set null', + ($fk->noActionOnDelete ?? null) === true => 'no action', + default => $fk->onDelete ?? '' + }; + $onUpdate = match (true) { + ($fk->cascadeOnUpdate ?? null) === true => 'cascade', + ($fk->restrictOnUpdate ?? null) === true => 'restrict', + ($fk->nullOnUpdate ?? null) === true => 'set null', + ($fk->noActionOnUpdate ?? null) === true => 'no action', + default => $fk->onUpdate ?? '' + }; + + $this->creator->addForeignKey( + $fk->columns, + $fk->on, + $fk->references ?? $fk->columns, + $onUpdate, + $onDelete, + $fk->name ?? '' + ); } /** - * Recupere les commandes a executer. + * Traite les suppressions */ - private function getCommands(Structure $structure): array + protected function processDrops(string $table, string $type, mixed $items): void { - $commands = Helpers::collect($structure->getCommands())->map(static fn (Fluent $command) => $command->getAttributes())->all(); - - return array_map(static fn ($command) => (object) $command, $commands); + $items = (array) $items; + + foreach ($items as $item) { + match ($type) { + 'columns' => $this->creator->dropColumn($table, $item), + 'primary' => $this->creator->dropPrimaryKey($table, $item), + 'unique', 'index' => $this->creator->dropKey($table, $item), + 'foreign' => $this->creator->dropForeignKey($table, $item), + default => null, + }; + } } /** * Fabrique un tableau contenant les definition d'un champs */ - private function makeColumn(object $column): array + private function makeColumn(Column $column): array { if (empty($column->name) || empty($column->type)) { throw new MigrationException('Nom ou type du champ non defini'); } - $definition = []; - - $definition['type'] = $this->creator->typeOf($column->type); - - if (is_array($definition['type'])) { - if (isset($definition['type'][1])) { - $definition['constraint'] = $definition['type'][1]; - } - $definition['type'] = $definition['type'][0]; - } - if (str_contains($definition['type'], '|')) { - $parts = explode('|', $definition['type']); - $definition['type'] = $parts[(int) $this->is($column, 'primary')]; + $attributes = []; + $type = $this->creator->typeOf($column->type); + + if (is_array($type)) { + $attributes['type'] = $type[0]; + $attributes['constraint'] = $type[1] ?? null; + } else if (str_contains($type, '|')) { + $parts = explode('|', $type); + $attributes['type'] = $parts[$column->primary === true ? 1 : 0]; + } elseif (str_contains($type, '{precision}')) { + $attributes['type'] = str_replace('{precision}', $column->precision, $type); + } else { + $attributes['type'] = $type; } - if (str_contains($definition['type'], '{precision}')) { - $definition['type'] = str_replace('{precision}', $column->precision, $definition['type']); + + if (isset($column->length)) { + $attributes['constraint'] = $column->length; + } elseif (isset($column->allowed)) { + $attributes['constraint'] = (array) $column->allowed; + } elseif (isset($column->total) || isset($column->places)) { + $attributes['constraint'] = ($column->total ?? 8) . ', ' . ($column->places ?? 2); + } elseif (isset($column->precision)) { + $attributes['constraint'] = $column->precision; } - if (property_exists($column, 'nullable')) { - $definition['null'] = $column->nullable; + if (isset($column->nullable)) { + $attributes['null'] = $column->nullable; } - if ($this->is($column, 'unsigned')) { - $definition['unsigned'] = true; + if ($column->unsigned === true) { + $attributes['unsigned'] = true; } - if ($this->is($column, 'useCurrent')) { - $definition['default'] = new RawSql('CURRENT_TIMESTAMP'); - } elseif (property_exists($column, 'default')) { - $definition['default'] = $column->type === 'boolean' ? (int) $column->default : $column->default; + + if ($column->useCurrent === true) { + $attributes['default'] = new Expression('CURRENT_TIMESTAMP'); + } elseif (isset($column->default)) { + $attributes['default'] = $column->type === 'boolean' ? (int) $column->default : $column->default; } - if ($this->isInteger($column) && $this->is($column, 'autoIncrement')) { - $definition['auto_increment'] = true; + + if ($column->autoIncrement === true) { + $attributes['auto_increment'] = true; } if (! empty($column->comment)) { - $definition['comment'] = addslashes($column->comment); + $attributes['comment'] = addslashes($column->comment); } if (! empty($column->collation)) { - $definition['collate'] = '"' . htmlspecialchars($column->collation) . '"'; - } - if (! empty($column->after)) { - $definition['after'] = $column->after; - } elseif ($this->is($column, 'first')) { - $definition['first'] = true; + $attributes['collate'] = '"' . htmlspecialchars($column->collation) . '"'; } - if (isset($column->length)) { - $definition['constraint'] = $column->length; - } elseif (isset($column->allowed)) { - $definition['constraint'] = (array) $column->allowed; - } elseif (isset($column->total) || isset($column->places)) { - $definition['constraint'] = ($column->total ?? 8) . ', ' . ($column->places ?? 2); - } elseif (isset($column->precision)) { - $definition['constraint'] = $column->precision; + if (! empty($column->after)) { + $attributes['after'] = $column->after; + } elseif ($column->first === true) { + $attributes['first'] = true; } - return $definition; - } - - /** - * Verifie si le champ a une certaine propriete particuliere - * - * Par exemple, on peut tester si un champ doit etre null en mettant is('nullable') - */ - private function is(object $column, string $property, mixed $match = true): bool - { - return property_exists($column, $property) && $column->{$property} === $match; - } - - /** - * Verifie si le champ est de type integer. - */ - private function isInteger(object $column): bool - { - return in_array($column->type, ['integer', 'int', 'bigInteger', 'mediumInteger', 'smallInteger', 'tinyInteger'], true); + return $attributes; } } diff --git a/src/Providers/DatabaseProvider.php b/src/Providers/DatabaseProvider.php index 58ed00e..3e5df18 100644 --- a/src/Providers/DatabaseProvider.php +++ b/src/Providers/DatabaseProvider.php @@ -12,6 +12,7 @@ namespace BlitzPHP\Database\Providers; use BlitzPHP\Container\AbstractProvider; +use BlitzPHP\Contracts\Container\ContainerInterface; use BlitzPHP\Contracts\Database\ConnectionInterface; use BlitzPHP\Contracts\Database\ConnectionResolverInterface; use BlitzPHP\Database\Config\Services; @@ -25,8 +26,10 @@ class DatabaseProvider extends AbstractProvider public static function definitions(): array { return [ - ConnectionResolverInterface::class => static fn () => new DatabaseManager(Services::logger(), Services::event()), - ConnectionInterface::class => static fn (ConnectionResolverInterface $resolver) => $resolver->connect(), + 'database' => static fn () => Services::database(), + DatabaseManager::class => static fn () => Services::dbManager(), + ConnectionResolverInterface::class => static fn (ContainerInterface $container) => $container->get(DatabaseManager::class), + ConnectionInterface::class => static fn (ConnectionResolverInterface $resolver) => $resolver->connect(), ]; } }