From dcc9919b4fb18e4639c9b6f22a6e4abeaae670e1 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 25 Feb 2026 14:34:34 +0100 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20Refactorisation=20du=20syst=C3=A8m?= =?UTF-8?q?e=20Seeder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Suppression des anciennes classes Faker et Generator, introduction d'une nouvelle classe Seed - Suppression des anciennes classes Faker et Generator afin de rationaliser le processus de seed. - Introduction d'une nouvelle classe Seed pour gérer les définitions de tables et la génération de données. - Mise à jour de la classe Seeder afin d'utiliser la nouvelle classe Seed pour gérer les opérations de seed. - Amélioration de la structure globale pour une meilleure maintenabilité et une plus grande clarté dans le seed de données. --- composer.json | 1 + src/Commands/Seed.php | 150 +++++++++-- src/Config/Services.php | 22 +- src/Exceptions/DatabaseException.php | 5 + src/Exceptions/SeederException.php | 31 +++ src/Seeder/Factory.php | 84 +++++++ src/Seeder/Faker.php | 238 ------------------ src/Seeder/Generator.php | 155 ------------ src/Seeder/Seed.php | 358 +++++++++++++++++++++++++++ src/Seeder/Seeder.php | 207 ++++++++-------- src/Seeder/Table.php | 300 ---------------------- src/Seeder/TableDef.php | 101 -------- 12 files changed, 723 insertions(+), 929 deletions(-) create mode 100644 src/Exceptions/SeederException.php create mode 100644 src/Seeder/Factory.php delete mode 100644 src/Seeder/Faker.php delete mode 100644 src/Seeder/Generator.php create mode 100644 src/Seeder/Seed.php delete mode 100644 src/Seeder/Table.php delete mode 100644 src/Seeder/TableDef.php diff --git a/composer.json b/composer.json index e35ce6d..c22546d 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "require-dev": { "blitz-php/coding-standard": "^1.5", "blitz-php/framework": "^1.0.0-rc", + "fakerphp/faker": "^1.24", "kahlan/kahlan": "^6.1.0", "phpstan/phpstan": "^2.1.36" }, diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 691fb2b..4b46ff4 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -11,7 +11,7 @@ namespace BlitzPHP\Database\Commands; -use BlitzPHP\Container\Services; +use BlitzPHP\Database\Connection\BaseConnection; use BlitzPHP\Database\Seeder\Seeder; use InvalidArgumentException; @@ -34,55 +34,151 @@ class Seed extends DatabaseCommand * {@inheritDoc} */ protected array $arguments = [ - 'name' => 'Nom du seedr a executer', + 'name' => 'Nom du seeder à exécuter (ex: DatabaseSeeder ou Users\\UserSeeder)', ]; + /** + * {@inheritDoc} + */ + protected array $options = [ + '--group' => 'Groupe de connexion à utiliser', + '--silent' => 'Mode silencieux (pas de sortie)', + '--locale' => 'Langue à utiliser pour Faker (ex: fr_FR, en_US)', + ]; + + protected ?BaseConnection $db = null; + /** * {@inheritDoc} */ public function handle() { - if (empty($name = $this->argument('name'))) { - $name = $this->prompt(lang('Migrations.migSeeder'), null, static function ($val) { + $group = $this->option('group'); + $silent = $this->option('silent') !== null; + $locale = $this->option('locale'); + + $this->db = $this->resolver->connect($group); + + $name = $this->getSeederName(); + + $seeder = $this->resolveSeeder($name); + + $this->configureSeeder($seeder, $locale, $silent); + + $this->runSeeder($seeder); + + return EXIT_SUCCESS; + } + + /** + * Récupère le nom du seeder + */ + protected function getSeederName(): string + { + if (null !== $name = $this->argument('name')) { + return $name; + } + + return $this->prompt( + 'Quel seeder souhaitez-vous exécuter ?', + 'DatabaseSeeder', + function ($val) { if (empty($val)) { throw new InvalidArgumentException('Veuillez entrer le nom du seeder.'); } - return $val; - }); - } + } + ); + } + + /** + * Résout la classe du seeder + */ + protected function resolveSeeder(string $name): Seeder + { + // Chemins possibles + $paths = [ + APP_NAMESPACE . '\\Database\\Seeds\\', + APP_NAMESPACE . '\\Database\\Seeders\\', + 'Database\\Seeds\\', + 'Database\\Seeders\\', + ]; - $seedClass = APP_NAMESPACE . '\Database\Seeds\\'; - $seedClass .= str_replace($seedClass, '', $name); + $className = $name; - /** - * @var Seeder - */ - $seeder = new $seedClass($this->db); + // Si le nom ne contient pas de namespace, on essaie les chemins standards + if (!str_contains($name, '\\')) { + foreach ($paths as $path) { + $fullClass = $path . $name; + if (class_exists($fullClass)) { + $className = $fullClass; + break; + } + } + } - if ($seeder->getLocale() === '') { - $seeder->setLocale(config('app.language')); + if (!class_exists($className)) { + throw new InvalidArgumentException( + "Le seeder '{$name}' n'a pas été trouvé.\n" . + "Chemins recherchés :\n" . + implode("\n", array_map(fn($p) => "- {$p}{$name}", $paths)) + ); } - $this->task('Demarrage du seed')->eol(); - sleep(2); - $this->info('Remplissage en cours de traitement'); + return new $className($this->db); + } - if (method_exists($seeder, 'run')) { - Services::container()->call([$seeder, 'run']); + /** + * Configure le seeder + */ + protected function configureSeeder(Seeder $seeder, ?string $locale, bool $silent): void + { + if ($locale !== null) { + $seeder->setLocale($locale); + } elseif ($seeder->getLocale() === '') { + $seeder->setLocale(config('app.language', 'fr_FR')); + } + + if ($silent) { + $seeder->setSilent(true); } + } - $usedSeed = [ - Services::container()->call([$seeder, 'execute']), - ...$seeder->getSeeded(), + /** + * Exécute le seeder + */ + protected function runSeeder(Seeder $seeder): void + { + $this->task('Démarrage du seed')->eol(); + + $this->info('Remplissage en cours...'); + + $seeder->setCommand($this)->execute(); + + $executed = [ + $seeder::class, + ...$seeder->getCalled(), ]; - $this->eol()->success('Opération terminée.'); + $this->eol()->success('Opération terminée avec succès !'); + + $this->eol()->write('Seeders exécutés :'); - foreach ($usedSeed as $seeded) { - $this->eol()->write('- ')->writer->yellow($seeded); + foreach (array_unique($executed) as $seeded) { + $this->eol()->write(' ✔ ')->writer->green($seeded); } - return EXIT_SUCCESS; + $this->displayStats($seeder); + } + + /** + * Affiche les statistiques d'exécution + */ + protected function displayStats(Seeder $seeder): void + { + // Note: Cette méthode suppose que vous avez un moyen de récupérer les stats + // À adapter selon votre implémentation réelle + + $this->eol()->write('Langue utilisée : ')->writer->yellow($seeder->getLocale()); } } diff --git a/src/Config/Services.php b/src/Config/Services.php index 155a10f..64c41e0 100644 --- a/src/Config/Services.php +++ b/src/Config/Services.php @@ -12,6 +12,8 @@ namespace BlitzPHP\Database\Config; use BlitzPHP\Container\Services as BaseServices; +use BlitzPHP\Contracts\Database\BuilderInterface; +use BlitzPHP\Contracts\Database\ConnectionInterface; use BlitzPHP\Database\Builder\BaseBuilder; use BlitzPHP\Database\Connection\BaseConnection; use BlitzPHP\Database\DatabaseManager; @@ -32,7 +34,7 @@ class Services extends BaseServices /** * Récupère le gestionnaire de base de données */ - protected static function manager(): DatabaseManager + public static function dbManager(): DatabaseManager { if (static::$manager === null) { static::$manager = new DatabaseManager(static::logger(), static::event()); @@ -43,10 +45,12 @@ protected static function manager(): DatabaseManager /** * Récupère une connexion à la base de données + * + * @return BaseConnection */ - public static function database(?string $group = null, bool $shared = true): BaseConnection + public static function database(?string $group = null, bool $shared = true): ConnectionInterface { - $connection = static::manager()->connect($group, $shared); + $connection = static::dbManager()->connect($group, $shared); if (!$connection instanceof BaseConnection) { throw new InvalidArgumentException('La connexion retournée n\'est pas une instance de BaseConnection'); @@ -57,8 +61,12 @@ public static function database(?string $group = null, bool $shared = true): Bas /** * Récupère un query builder + * + * @return BaseBuilder + * + * @deprecated 1.0 use static::database()->table($tablename) instead */ - public static function builder(?string $group = null, bool $shared = true): BaseBuilder + public static function builder(?string $group = null, bool $shared = true): BuilderInterface { $key = 'builder_' . ($group ?? 'default'); @@ -66,7 +74,7 @@ public static function builder(?string $group = null, bool $shared = true): Base return static::$instances[$key]; } - $builder = static::manager()->builder(static::database($group, $shared)); + $builder = static::dbManager()->builder(static::database($group, $shared)); if ($shared) { static::$instances[$key] = $builder; @@ -78,7 +86,7 @@ public static function builder(?string $group = null, bool $shared = true): Base /** * Récupère un exportateur de base de données */ - public static function dbExporter(?BaseConnection $db = null, array $config = [], bool $shared = true): Exporter + public static function dbExporter(?ConnectionInterface $db = null, array $config = [], bool $shared = true): Exporter { if ($shared) { return static::sharedInstance('dbExporter', $db, $config); @@ -102,7 +110,7 @@ public static function dbExporter(?BaseConnection $db = null, array $config = [] /** * Récupère un importateur de base de données */ - public static function dbImporter(?BaseConnection $db = null, array $config = [], bool $shared = true): Importer + public static function dbImporter(?ConnectionInterface $db = null, array $config = [], bool $shared = true): Importer { if ($shared) { return static::sharedInstance('dbImporter', $db, $config); diff --git a/src/Exceptions/DatabaseException.php b/src/Exceptions/DatabaseException.php index 91718d2..ceab403 100644 --- a/src/Exceptions/DatabaseException.php +++ b/src/Exceptions/DatabaseException.php @@ -21,4 +21,9 @@ class DatabaseException extends Error implements ExceptionInterface * @var int */ protected $code = 8; + + protected static function t(string $message, array $args = []): string + { + return sprintf($message, $args); + } } diff --git a/src/Exceptions/SeederException.php b/src/Exceptions/SeederException.php new file mode 100644 index 0000000..07fd034 --- /dev/null +++ b/src/Exceptions/SeederException.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Database\Seeder; + +use Faker\Factory as FakerFactory; +use Faker\Generator as FakerGenerator; + +/** + * Générateur de configurations pour les seeders + * + * @mixin FakerGenerator + */ +class Factory +{ + /** + * Instance Faker + */ + protected FakerGenerator $faker; + + /** + * Constructeur + */ + public function __construct(string $locale = 'fr_FR') + { + $this->faker = FakerFactory::create($locale); + } + + /** + * Appel Faker (retourne une configuration) + */ + public function __call(string $name, array $arguments): array + { + if ($name === 'unique') { + return ['faker:unique', $arguments[0] ?? null, array_slice($arguments, 1) ?? []]; + } + + return ['faker', $name, $arguments]; + } + + /** + * Propriété Faker (retourne une configuration) + */ + public function __get(string $name): array + { + return ['faker', $name, []]; + } + + /** + * Relation avec une autre table + */ + public function relation(string $table, string $column = 'id'): array + { + return ['relation', $table, $column]; + } + + /** + * Valeur optionnelle + */ + public function optional(float $weight = 0.5, mixed $default = null, mixed $value = null): array + { + if ($value === null) { + // Si pas de valeur fournie, on utilisera une closure + return ['optional', $weight, $default]; + } + return ['optional', $weight, $default, $value]; + } + + /** + * Valeur fixe (pour compatibilité) + */ + public function raw(mixed $value): mixed + { + return $value; + } +} diff --git a/src/Seeder/Faker.php b/src/Seeder/Faker.php deleted file mode 100644 index da639c9..0000000 --- a/src/Seeder/Faker.php +++ /dev/null @@ -1,238 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Database\Seeder; - -use DateTime; - -/** - * Generator - * - * @credit tebazil/db-seeder - * - * @property string $address - * @property string $amPm - * @property string $bankAccountNumber - * @property string $buildingNumber - * @property int $century - * @property string $chrome - * @property string $city - * @property string $citySuffix - * @property string $colorName - * @property string $company - * @property string $companyEmail - * @property string $companySuffix - * @property string $country - * @property string $countryCode - * @property string $countryISOAlpha3 - * @property string $creditCardDetails - * @property DateTime $creditCardExpirationDate - * @property string $creditCardExpirationDateString - * @property string $creditCardNumber - * @property string $creditCardType - * @property string $currencyCode - * @property DateTime $dateTime - * @property DateTime $dateTimeAD - * @property DateTime $dateTimeThisCentury - * @property DateTime $dateTimeThisDecade - * @property DateTime $dateTimeThisMonth - * @property DateTime $dateTimeThisYear - * @property int $dayOfMonth - * @property int $dayOfWeek - * @property string $domainName - * @property string $domainWord - * @property string $ean13 - * @property string $ean8 - * @property string $email - * @property string $fileExtension - * @property string $firefox - * @property string $firstName - * @property string $firstNameFemale - * @method array shuffleArray(array $array = array()) - * @property string $firstNameMale - * @method string asciify($string = '****') - * @property string $freeEmail - * @method int biasedNumberBetween($min = 0, $max = 100, $function = 'sqrt') - * @property string $freeEmailDomain - * @method bool boolean($chanceOfGettingTrue = 50) - * @method string bothify($string = '## ??') - * - * @property string $hexColor - * @property string $internetExplorer - * @property string $ipv4 - * @property string $ipv6 - * @property string $isbn10 - * @property string $isbn13 - * @property string $iso8601 - * @property string $languageCode - * @method string creditCardNumber($type = null, $formatted = false, $separator = '-') - * @property string $lastName - * @property float $latitude - * @property string $linuxPlatformToken - * @property string $linuxProcessor - * @property string $locale - * @method string date($format = 'Y-m-d', $max = 'now') - * @property string $localIpv4 - * @property float $longitude - * @property string $macAddress - * @property string $macPlatformToken - * @property string $macProcessor - * @property string $md5 - * @property string $mimeType - * @property int $month - * @property string $monthName - * @property string $name - * @property string $opera - * @property string $paragraph - * @property array|string $paragraphs - * @property string $password - * @property string $phoneNumber - * @property string $postcode - * @property string $randomAscii - * @property int $randomDigit - * @property int $randomDigitNotNull - * @property string $randomLetter - * @method DateTime dateTimeBetween($startDate = '-30 years', $endDate = 'now') - * @method string file($sourceDirectory = '/tmp', $targetDirectory = '/tmp', $fullPath = true) - * @method string image($dir = null, $width = 640, $height = 480, $category = null, $fullPath = true) - * - * @property string $rgbColor - * @property array $rgbColorAsArray - * @property string $rgbCssColor - * @property string $safari - * @property string $safeColorName - * @property string $safeEmail - * @property string $safeEmailDomain - * @property string $safeHexColor - * @method string imageUrl($width = 640, $height = 480, $category = null, $randomize = true) - * - * @property string $sentence - * @property array|string $sentences - * @property string $sha1 - * @property string $sha256 - * @method string lexify($string = '????') - * @method int numberBetween($min = 0, $max = 2147483647) - * @method string numerify($string = '###') - * @method string paragraph($nbSentences = 3, $variableNbSentences = true) - * @method array|string paragraphs($nb = 3, $asText = false) - * @method string password($minLength = 6, $maxLength = 20) - * @method float randomFloat($nbMaxDecimals = null, $min = 0, $max = null) - * @method int randomNumber($nbDigits = null, $strict = false) - * @method string realText($maxNbChars = 200, $indexSize = 2) - * @method string regexify($regex = '') - * @method string sentence($nbWords = 6, $variableNbWords = true) - * @method array|string sentences($nb = 3, $asText = false) - * @method array|string shuffle($arg = '') - * @method string shuffleString($string = '', $encoding = 'UTF-8') - * @method string slug($nbWords = 6, $variableNbWords = true) - * @method string text($maxNbChars = 200) - * @method string time($format = 'H:i:s', $max = 'now') - * - * @property string $slug - * @property string $streetAddress - * @property string $streetName - * @property string $streetSuffix - * @property string $swiftBicNumber - * @property string $text - * @property string $timezone - * @property string $title - * @property string $titleFemale - * @property string $titleMale - * @property string $tld - * @property int $unixTime - * @property string $url - * @property string $userAgent - * @method string toLower($string = '') - * @method string toUpper($string = '') - * @method array|string words($nb = 3, $asText = false) - * - * @property string $userName - * @property string $uuid - * @property string $vat - * @property string $windowsPlatformToken - * @property string $word - * @property array|string $words - * @property int $year - */ -class Faker -{ - public const OPTIONAL = 'optional'; - public const UNIQUE = 'unique'; - public const VALID = 'valid'; - private const DEFAULT_OPTIONS = [ - self::OPTIONAL => false, - self::UNIQUE => false, - self::VALID => false, - ]; - - private array $options = self::DEFAULT_OPTIONS; - - public function __get(string $property) - { - return $this->retrive($property); - } - - public function __call(string $method, array $arguments) - { - return $this->retrive($method, $arguments); - } - - /** - * Specifie qu'on souhaite avoir des données optionelles - */ - public function optional(float $weight = 0.5, mixed $default = null): self - { - $this->options[self::OPTIONAL] = func_get_args(); - - return $this; - } - - /** - * Specifie qu'on souhaite avoir des donnees uniques - */ - public function unique(bool $reset = false, int $maxRetries = 10000): self - { - $this->options[self::UNIQUE] = func_get_args(); - - return $this; - } - - /** - * Specifie qu'on veut les donnees valide - * - * @param mixed $validator - */ - public function valid($validator, int $maxRetries = 10000): self - { - $this->options[self::VALID] = func_get_args(); - - return $this; - } - - /** - * Facade de recuperation les valeurs de configuration de fakerphp - */ - private function retrive(string $name, ?array $arguments = null): mixed - { - $return = [Generator::FAKER, $name, $arguments, $this->options]; - $this->optionsReset(); - - return $return; - } - - /** - * Reinitialise les options du generateur - */ - private function optionsReset() - { - $this->options = self::DEFAULT_OPTIONS; - } -} diff --git a/src/Seeder/Generator.php b/src/Seeder/Generator.php deleted file mode 100644 index e792988..0000000 --- a/src/Seeder/Generator.php +++ /dev/null @@ -1,155 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Database\Seeder; - -use Faker\Factory; -use Faker\Generator as TrueFaker; -use InvalidArgumentException; - -/** - * Generateur de données - * - * @credit tebazil/db-seeder - */ -class Generator -{ - public const PK = 'pk'; - public const FAKER = 'faker'; - public const RELATION = 'relation'; - - private bool $reset = false; - - /** - * Instance du generateur de fake data - */ - private ?TrueFaker $faker = null; - - /** - * Valeur courante de la cle primaire - */ - private int $pkValue = 1; - - /** - * Liste des tables pour lesquelles les donnees doivent etre generees - */ - private array $tables = []; - - public function __construct(private string $locale) - { - } - - /** - * Recupere une valeur generee - */ - public function getValue(array|string $config): mixed - { - if (! is_array($config)) { - $config = [$config]; - } - - return match ($config[0]) { - self::PK => $this->pkValue(), - self::FAKER => $this->fakerValue($config[1], $config[2] ?? [], $config[3] ?? []), - self::RELATION => $this->relationValue($config[1], $config[2] ?? ''), - default => is_callable($config[0]) ? $config[0]() : $config[0] - }; - } - - /** - * Reinitialise les valeurs du generateur - * - * @return void - */ - public function reset() - { - $this->reset = true; - } - - /** - * Definition des champs d'une table - */ - public function setColumns(string $table, array $columns) - { - $this->tables[$table] = $columns; - } - - /** - * Recupere la cle primaire auto incrementee - */ - private function pkValue(): int - { - if ($this->reset) { - $this->pkValue = 1; - $this->reset = false; - } - - return $this->pkValue++; - } - - /** - * Recupere une valeur generee par Faker - */ - private function fakerValue(mixed $format, array $arguments, array $options): mixed - { - if (empty($format)) { - return null; - } - - $faker = $this->faker(); - - if (isset($options[Faker::UNIQUE]) && is_array($options[Faker::UNIQUE])) { - $faker = call_user_func_array([$faker, 'unique'], $options[Faker::UNIQUE]); - } - - if (isset($options[Faker::OPTIONAL]) && is_array($options[Faker::OPTIONAL])) { - $faker = call_user_func_array([$faker, 'optinal'], $options[Faker::OPTIONAL]); - } - - if (isset($options[Faker::VALID]) && is_array($options[Faker::VALID])) { - $faker = call_user_func_array([$faker, 'valid'], $options[Faker::VALID]); - } - - return $faker->format($format, $arguments); - } - - /** - * Recupere une valeur issue d'une relation - */ - private function relationValue(string $table, string $column): mixed - { - if (! $this->isColumnSet($table, $column)) { - throw new InvalidArgumentException("Table {$table} , column {$column} is not filled"); - } - - return $this->tables[$table][$column][array_rand($this->tables[$table][$column])]; - } - - /** - * Instance unique du generateur faker pour le generateur courant - */ - private function faker(): TrueFaker - { - if (null === $this->faker) { - $this->faker = Factory::create($this->locale); - } - - return $this->faker; - } - - /** - * Verifie si un champ de table a une valeur renseignee - */ - private function isColumnSet(string $table, string $column): bool - { - return isset($this->tables[$table]) && isset($this->tables[$table][$column]); - } -} diff --git a/src/Seeder/Seed.php b/src/Seeder/Seed.php new file mode 100644 index 0000000..2db72af --- /dev/null +++ b/src/Seeder/Seed.php @@ -0,0 +1,358 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace BlitzPHP\Database\Seeder; + +use BlitzPHP\Database\Builder\BaseBuilder; +use BlitzPHP\Database\Connection\BaseConnection; +use BlitzPHP\Database\Exceptions\SeederException; +use Faker\Generator as FakerGenerator; +use PDO; + +/** + * Définition d'une table à remplir + */ +class Seed +{ + /** + * Builder pour la table + */ + protected BaseBuilder $builder; + + /** + * Définition des colonnes + * + * @var array + */ + protected array $columns = []; + + /** + * Closures pour les cas complexes + * + * @var array + */ + protected array $closures = []; + + /** + * Données brutes + */ + protected array $rawData = []; + + /** + * Nombre de lignes à générer + */ + protected int $rowCount = 30; + + /** + * Faut-il vider la table avant ? + */ + protected bool $truncate = false; + + /** + * Données générées + */ + protected array $generated = []; + + /** + * Cache des valeurs résolues + */ + protected array $cache = []; + + /** + * Constructeur + * + * @param BaseConnection $db Connexion à la base de données + * @param string $table Nom de la table + * @param FakerGenerator $faker Générateur Faker + */ + public function __construct(protected BaseConnection $db, protected string $table, protected FakerGenerator $faker) + { + $this->builder = $db->table($table); + } + + /** + * Définit les colonnes + */ + public function columns(array $columns): self + { + foreach ($columns as $name => $definition) { + $this->column($name, $definition); + } + + return $this; + } + + /** + * Ajoute une colonne + */ + public function column(string $name, mixed $definition): self + { + // Si c'est une closure, on la garde à part pour la flexibilité + if (is_callable($definition) && !is_string($definition)) { + $this->closures[$name] = $definition; + } else { + $this->columns[$name] = $definition; + } + + return $this; + } + + /** + * Définit le nombre de lignes à insérer en base de données + */ + public function rows(int $count): self + { + $this->rowCount = $count; + + return $this; + } + + /** + * Définit des données brutes + */ + public function data(array $data): self + { + if ($data === []) { + throw SeederException::dataCannotBeEmpty(); + } + + $firstRow = reset($data); + + if (! is_array($firstRow)) { + $this->rawData = [$data]; + } else { + $this->rawData = $data; + } + + return $this; + } + + /** + * Vide la table avant insertion + */ + public function truncate(bool $truncate = true): self + { + $this->truncate = $truncate; + + return $this; + } + + /** + * Exécute le seed + */ + public function execute(): void + { + if ($this->truncate) { + $this->builder->truncate(); + } + + if (!empty($this->rawData)) { + $this->insertRawData(); + } else { + $this->generateData(); + } + } + + /** + * Insère les données brutes + */ + protected function insertRawData(): void + { + $columns = array_keys(reset($this->rawData)); + + foreach ($this->rawData as $row) { + $data = []; + foreach ($columns as $column) { + $data[$column] = $row[$column] ?? null; + } + + $this->builder->insert($data); + } + } + + /** + * Génère les données + */ + protected function generateData(): void + { + // Résoudre les dépendances une seule fois + $dependencies = $this->resolveDependencies(); + + // Générer les données ligne par ligne + for ($i = 0; $i < $this->rowCount; $i++) { + $row = []; + + // Traiter les configurations d'abord (plus rapides) + foreach ($this->columns as $column => $definition) { + $row[$column] = $this->resolveConfig($definition, $dependencies); + } + + // Traiter les closures ensuite (plus flexibles) + foreach ($this->closures as $column => $closure) { + $row[$column] = $closure($this->faker, $dependencies, $i); + } + + $this->generated[] = $row; + } + + // Insérer les données + foreach ($this->generated as $row) { + $this->builder->insert($row); + } + } + + /** + * Résout une configuration + */ + protected function resolveConfig(mixed $config, array $dependencies): mixed + { + // Cache pour les appels répétés + $cacheKey = is_array($config) ? md5(serialize($config)) : null; + + if ($cacheKey && isset($this->cache[$cacheKey])) { + return $this->cache[$cacheKey]; + } + + $result = match (true) { + // Valeur simple (pas de configuration) + !is_array($config) => $config, + + // Configuration standard + default => $this->resolveArrayConfig($config, $dependencies) + }; + + if ($cacheKey) { + $this->cache[$cacheKey] = $result; + } + + return $result; + } + + /** + * Résout une configuration sous forme de tableau + */ + protected function resolveArrayConfig(array $config, array $dependencies): mixed + { + $type = $config[0] ?? null; + + return match ($type) { + 'faker' => $this->resolveFaker($config), + 'faker:unique' => $this->resolveUniqueFaker($config), + 'relation' => $this->resolveRelation($config, $dependencies), + 'optional' => $this->resolveOptional($config, $dependencies), + default => $config // Retourne tel quel si non reconnu + }; + } + + /** + * Résout un appel Faker + */ + protected function resolveFaker(array $config): mixed + { + $method = $config[1] ?? null; + $arguments = $config[2] ?? []; + + if (!$method) { + throw SeederException::unspecifiedFakerMethod(); + } + + return $this->faker->{$method}(...$arguments); + } + + /** + * Résout un appel Faker unique + */ + protected function resolveUniqueFaker(array $config): mixed + { + $method = $config[1] ?? null; + $arguments = $config[2] ?? []; + $maxRetries = $config[3] ?? 10000; + + if (!$method) { + throw SeederException::unspecifiedFakerMethod(); + } + + return $this->faker->unique(maxRetries: (int) $maxRetries)->{$method}(...$arguments); + } + + /** + * Résout une relation + */ + protected function resolveRelation(array $config, array $dependencies): mixed + { + $table = $config[1] ?? null; + $column = $config[2] ?? 'id'; + + if (!$table || !isset($dependencies[$table])) { + throw SeederException::relationTableNotFound($table); + } + + $values = $dependencies[$table]; + return $values[array_rand($values)]; + } + + /** + * Résout une valeur optionnelle + */ + protected function resolveOptional(array $config, array $dependencies): mixed + { + $weight = $config[1] ?? 0.5; + $default = $config[2] ?? null; + $value = $config[3] ?? null; + + if (mt_rand() / mt_getrandmax() <= $weight) { + if ($value === null) { + // Pas de valeur fournie, on utilise une closure par défaut + // ou on retourne null + return null; + } + return $this->resolveConfig($value, $dependencies); + } + + return $default; + } + + /** + * Résout les dépendances entre tables + */ + protected function resolveDependencies(): array + { + $dependencies = []; + + // Chercher les relations dans les configurations + foreach ($this->columns as $definition) { + if (is_array($definition) && ($definition[0] ?? null) === 'relation') { + $table = $definition[1] ?? null; + if ($table && $table !== $this->table) { + $dependencies[$table] = $this->getTableValues($table, $definition[2] ?? 'id'); + } + } + } + + // TODO Chercher aussi dans les closures (moins probable mais possible) + foreach ($this->closures as $closure) { + // On ne peut pas analyser les closures statiquement + // On laisse l'utilisateur gérer + } + + return $dependencies; + } + + /** + * Récupère les valeurs d'une table pour les relations + */ + protected function getTableValues(string $table, string $column): array + { + return $this->db->table($table) + ->select($column) + ->all(PDO::FETCH_COLUMN); + } +} diff --git a/src/Seeder/Seeder.php b/src/Seeder/Seeder.php index 6d6ec85..1672901 100644 --- a/src/Seeder/Seeder.php +++ b/src/Seeder/Seeder.php @@ -11,196 +11,201 @@ namespace BlitzPHP\Database\Seeder; +use BlitzPHP\Database\Commands\Seed as SeedCommand; use BlitzPHP\Database\Connection\BaseConnection; -use BlitzPHP\Database\Exceptions\DatabaseException; -use InvalidArgumentException; +use BlitzPHP\Database\Exceptions\SeederException; /** - * Genere du faux contenu pour remplir une base de donnees. + * Classe de base pour les seeders * - * @credit tebazil/db-seeder + * @inspired https://github.com/tebazil/db-seeder */ abstract class Seeder { /** - * @var list Liste des tables a remplir + * Connexion à la base de données */ - private array $tables = []; + protected BaseConnection $db; /** - * Générateur de contenu + * Factory pour les configurations */ - private ?Generator $generator = null; + protected Factory $factory; /** - * Liste des tables qui ont deja été remplies + * Instance de la console */ - private array $filledTablesNames = []; + protected ?SeedCommand $command = null; /** - * @var list Liste des seeders executes + * Tables à remplir + * + * @var array */ - private array $seeded = []; + protected array $seeds = []; /** - * Langue à utiliser pour la génération des fake data via Faker + * Seeders appelés + * + * @var list */ - protected string $locale = ''; + protected array $called = []; /** - * If true, will not display CLI messages. + * Mode silencieux (pas de sortie) */ protected bool $silent = false; - public function __construct(protected BaseConnection $db) + /** + * Langue pour Faker + */ + protected string $locale = 'fr_FR'; + + /** + * Constructeur + * + * @param BaseConnection $db Connexion à la base de données + */ + public function __construct(BaseConnection $db) { + $this->db = $db; + + $this->factory = new Factory($this->locale); } /** - * Sets the silent treatment. + * Définit l'instance de commande */ - public function setSilent(bool $silent): self + public function setCommand(SeedCommand $command): self { - $this->silent = $silent; + $this->command = $command; return $this; } /** - * Recupere la langue de generation de contenu + * Définit le mode silencieux */ - public function getLocale(): string + public function setSilent(bool $silent): self { - return $this->locale; + $this->silent = $silent; + + return $this; } /** - * Modifie la langue de generation de contenu + * Définit la langue */ public function setLocale(string $locale): self { $this->locale = $locale; - + $this->factory = new Factory($locale); + return $this; } /** - * Recupere la liste des sous seeder executes via la methode call() - * - * @return list + * Récupère la langue */ - public function getSeeded(): array + public function getLocale(): string { - return $this->seeded; + return $this->locale; } /** - * Lance la generation des donnees + * Récupère les seeders appelés + * + * @return list */ - public function execute(): string + public function getCalled(): array { - $this->checkCrossDependentTables(); - - $tableNames = array_keys($this->tables); - sort($tableNames); - - $foolProofCounter = 0; - $tableNamesIntersection = []; - - while ($tableNamesIntersection !== $tableNames) { - if ($foolProofCounter++ > 500) { - throw new DatabaseException("Quelque chose d'inattendu s'est produit\u{a0}: certaines tables ne peuvent peut-être pas être remplies"); - } + return $this->called; + } - foreach ($this->tables as $tableName => $table) { - if (! $table->isFilled() && $table->canBeFilled($this->filledTablesNames)) { - $table->fill(); - $this->generator->setColumns($tableName, $table->getColumns()); + /** + * Accès à la factory (pour la syntaxe $this->faker->...) + */ + public function __get(string $name): mixed + { + if ($name === 'faker') { + return $this->factory; + } + + throw SeederException::propertyNotFound($name); + } - if (! in_array($tableName, $this->filledTablesNames, true)) { - $this->filledTablesNames[] = $tableName; - } - } - } + /** + * Méthode principale à implémenter + */ + abstract public function run(): void; - $tableNamesIntersection = array_intersect($this->filledTablesNames, $tableNames); - sort($tableNamesIntersection); + /** + * Définit une table à remplir + */ + protected function table(string $table): Seed + { + if (!isset($this->seeds[$table])) { + $this->seeds[$table] = new Seed($this->db, $table, $this->factory->faker); } - return static::class; + return $this->seeds[$table]; } /** - * Specifie la table a remplir. + * Appelle d'autres seeders */ - protected function table(string $name, bool $truncate = false): TableDef + protected function call(array|string $seeders): self { - if (! isset($this->tables[$name])) { - $this->tables[$name] = new Table($this->generator(), $this->db->table($name), $truncate); + foreach ((array) $seeders as $seeder) { + $seeder = $this->resolve($seeder); + $seeder->setSilent($this->silent)->run(); + $this->called[] = $seeder::class; } - return new TableDef($this->tables[$name]); + return $this; } /** - * Charge le seeder spécifié et l'exécute. - * - * @throws InvalidArgumentException + * Résout un nom de seeder en instance */ - protected function call(array|string $classes) + protected function resolve(string $class): self { - $classes = (array) $classes; - - foreach ($classes as $class) { - $class = trim($class); - - if ($class === '') { - throw new InvalidArgumentException('Aucun seeder n\'a été spécifié.'); - } - - /** @var Seeder $seeder */ - $seeder = new $class($this->db); - $seeder->setSilent($this->silent); - - if (method_exists($seeder, 'run')) { - call_user_func([$seeder, 'run'], new Faker()); - } - - $this->seeded[] = $seeder->execute(); - - unset($seeder); + if (!class_exists($class)) { + throw SeederException::seederClassDoesNotExist($class); } + + return new $class($this->db); } /** - * Singleton pour avoir le générateur + * Exécute le seeder */ - private function generator(): Generator + public function execute(): void { - if (null === $this->generator) { - $this->generator = new Generator($this->locale); + $this->run(); + + foreach ($this->seeds as $seed) { + $seed->execute(); } - return $this->generator; + $this->seeds = []; } /** - * Verifie les dependences entres les tables + * Affiche un message si pas en mode silencieux */ - private function checkCrossDependentTables() + protected function output(string $message, string $type = 'info'): void { - $dependencyMap = []; - - foreach ($this->tables as $tableName => $table) { - $dependencyMap[$tableName] = $table->getDependsOn(); + if ($this->silent) { + return; } - foreach ($dependencyMap as $tableName => $tableDependencies) { - foreach ($tableDependencies as $dependencyTableName) { - if (in_array($tableName, $dependencyMap[$dependencyTableName], true)) { - throw new InvalidArgumentException('Vous ne pouvez pas passer des tables qui dépendent les unes des autres'); - } - } + if ($this->command) { + $this->command->{$type}($message); + } else if(defined('STDOUT')) { + fwrite(STDOUT, $message . PHP_EOL); + } else { + echo $message . PHP_EOL; } } } diff --git a/src/Seeder/Table.php b/src/Seeder/Table.php deleted file mode 100644 index bce1c3f..0000000 --- a/src/Seeder/Table.php +++ /dev/null @@ -1,300 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Database\Seeder; - -use BlitzPHP\Database\Builder\BaseBuilder; -use InvalidArgumentException; - -/** - * @credit tebazil/db-seeder - */ -class Table -{ - public const DEFAULT_ROW_QUANTITY = 30; - - /** - * Nom de la table courrante - */ - private string $name; - - /** - * Nom des champs dans lesquels les donnees seront inserees - */ - private array $columns = []; - - /** - * Quantite de donnees a remplir lors de l'execution - */ - private ?int $rowQuantity = null; - - /** - * Donnees a remplir lors de l'execution - */ - private array $rows = []; - - /** - * Donnees brutes a remplir (specifiees par l'utilisateur) - */ - private array $rawData = []; - - /** - * Drapeau specifiant que la table a ete remplie - */ - private bool $isFilled = false; - - /** - * Drapeau specifiant que la table a ete partiellement remplie - */ - private bool $isPartiallyFilled = false; - - /** - * Liste des tables dont depend cette table - */ - private array $dependsOn = []; - - private array $selfDependentColumns = []; - private array $columnConfig = []; - - /** - * constructor - */ - public function __construct(private Generator $generator, private BaseBuilder $builder, private bool $truncateTable) - { - $this->name = $builder->getTable(); - } - - /** - * Definit les colonnes où les insertions se passeront - */ - public function setColumns(array $columns): self - { - $columnNames = array_keys($columns); - - foreach ($columnNames as $columnName) { - $this->columns[$columnName] = []; - } - - $this->columnConfig = $columns; - - $this->calcDependsOn(); - $this->calcSelfDependentColumns(); - - return $this; - } - - /** - * Definit le nombre d'element a generer - */ - public function setRowQuantity(int $rows = self::DEFAULT_ROW_QUANTITY): self - { - $this->rowQuantity = $rows; - - return $this; - } - - /** - * Defini les donnees brutes a inserer - */ - public function setRawData(array $rawData, array $columnNames = []): self - { - if ($rawData === []) { - throw new InvalidArgumentException('$rawData cannot be empty array'); - } - if (! is_array($firstRow = reset($rawData))) { - throw new InvalidArgumentException('$rawData should be an array of arrays (2d array)'); - } - if (is_numeric(key($firstRow)) && ! $columnNames) { - throw new InvalidArgumentException('Either provide $rawData line arrays with corresponding column name keys, or provide column names in $columnNames'); - } - - $this->rawData = $rawData; - $columnNames = $columnNames ?: array_keys(reset($this->rawData)); - $this->columnConfig = []; // just in case - - foreach ($columnNames as $columnName) { - if ($columnName) { - $this->columns[$columnName] = []; // we skip false columns and empty columns - } - - $this->columnConfig[] = $columnName; - } - - return $this; - } - - /** - * Remplissage des donnees - */ - public function fill(bool $writeDatabase = true) - { - [] === $this->rawData - ? $this->fillFromGenerators($this->columnConfig) - : $this->fillFromRawData($this->columnConfig, $this->rawData); - - if ($this->selfDependentColumns) { - if ($this->isPartiallyFilled) { - $this->isFilled = true; // second run - } else { - $this->isPartiallyFilled = true; // first run - } - } else { - $this->isFilled = true; // no self-dependent columns - } - - if ($this->isFilled && $writeDatabase) { - $this->insertData(); - } - } - - /** - * Véerifie si la table a déjà étée chargée - */ - public function isFilled(): bool - { - return $this->isFilled; - } - - /** - * Verifie si la table peut etre chargee - */ - public function canBeFilled(array $filledTableNames): bool - { - $intersection = array_intersect($filledTableNames, $this->dependsOn); - sort($intersection); - - return $intersection === $this->dependsOn; - } - - /** - * Recuperes les valeurs des champs a inserer dans la table - */ - public function getRows(): array - { - return $this->rows; - } - - /** - * Liste des colonnes de la table dans lesquelles on fera les insertions - */ - public function getColumns(): array - { - return $this->columns; - } - - /** - * Liste des tables dont depend cette table - */ - public function getDependsOn(): array - { - return $this->dependsOn; - } - - /** - * Ajoute les donnees de generation a partir des donnees brutes renseignees par l'utilisateur. - */ - private function fillFromRawData(array $columnConfig, array $data) - { - $sizeofColumns = count($columnConfig); - $data = array_values($data); - $sizeofData = count($data); - - for ($rowNo = 0; $rowNo < ($this->rowQuantity ?? $sizeofData); $rowNo++) { - $dataKey = ($rowNo < $sizeofData) ? $rowNo : ($rowNo % $sizeofData); - $rowData = array_values($data[$dataKey]); - - for ($i = 0; $i < $sizeofColumns; $i++) { - if (! $columnConfig[$i]) { - continue; - } - - $this->rows[$rowNo][$columnConfig[$i]] = $rowData[$i]; - $this->columns[$columnConfig[$i]][$rowNo] = $rowData[$i]; - } - } - } - - /** - * Ajoute les donnees de generation a partir du generateur (via Faker si necessaire). - */ - private function fillFromGenerators(array $columnConfig) - { - $this->generator->reset(); - - for ($rowNo = 0; $rowNo < $this->rowQuantity ?? self::DEFAULT_ROW_QUANTITY; $rowNo++) { - foreach ($columnConfig as $column => $config) { - // first and second run separation - if ($this->selfDependentColumns) { - $columnIsSelfDependent = in_array($column, $this->selfDependentColumns, true); - if (! $this->isPartiallyFilled) { - if ($columnIsSelfDependent) { - continue; - } - } elseif (! $columnIsSelfDependent) { - continue; - } - } - - $value = $this->generator->getValue($config); - - $this->rows[$rowNo][$column] = $value; - $this->columns[$column][$rowNo] = $value; - } - } - } - - private function calcDependsOn() - { - if ($this->rawData) { - return false; - } - - foreach ($this->columnConfig as $name => $config) { - if (! is_callable($config)) { - if (is_array($config) && ($config[0] === Generator::RELATION) && ($this->name !== $config[1])) { - $this->dependsOn[] = $config[1]; - } - } - } - - sort($this->dependsOn); - } - - private function calcSelfDependentColumns() - { - if ($this->rawData) { - return false; - } - - foreach ($this->columnConfig as $name => $config) { - if (! is_callable($config)) { - if (is_array($config) && ($config[0] === Generator::RELATION) && ($config[1] === $this->name)) { - $this->selfDependentColumns[] = $name; - } - } - } - } - - /** - * Insertion des donnees generees en db - */ - private function insertData() - { - if (true === $this->truncateTable) { - $this->builder->db()->disableFk(); - $this->builder->truncate($this->name); - } - - foreach ($this->rows as $row) { - $this->builder->into($this->name)->insert($row); - } - } -} diff --git a/src/Seeder/TableDef.php b/src/Seeder/TableDef.php deleted file mode 100644 index 643b695..0000000 --- a/src/Seeder/TableDef.php +++ /dev/null @@ -1,101 +0,0 @@ - - * - * For the full copyright and license information, please view - * the LICENSE file that was distributed with this source code. - */ - -namespace BlitzPHP\Database\Seeder; - -use BlitzPHP\Utilities\Iterable\Arr; -use Exception; -use InvalidArgumentException; - -/** - * @credit tebazil/db-seeder - */ -class TableDef -{ - public function __construct(private Table $table) - { - } - - /** - * Defini les type de donnees a generer pour chaque colone qu'on souhaite remplir dans la base de donnees - */ - public function columns(array $columns): self - { - $columns = $this->preprocess($columns); - $this->table->setColumns($columns); - - return $this; - } - - /** - * Specifie le nombre de ligne a inserer dans la table - */ - public function rows(int $rows = Table::DEFAULT_ROW_QUANTITY): self - { - $this->table->setRowQuantity($rows); - - return $this; - } - - /** - * Definit les donnees brutes a inserer - */ - public function data(array $data): self - { - $dim = Arr::dimensions($data); - - if ($dim > 2) { - throw new InvalidArgumentException('Vous ne pouvez pas inserer un tableau de dimension supperieure a 2'); - } - - if ($dim === 1) { - $columnNames = array_keys($data); - $data = [array_values($data)]; - } else { - $columnNames = array_keys(reset($data)); - $data = array_map('array_values', $data); - } - - $this->table->setRawData($data, $columnNames); - - return $this; - } - - /** - * Undocumented function - */ - private function preprocess(array $columns): array - { - $newColumns = []; - - foreach ($columns as $key => $value) { - if (is_numeric($key)) { - if (! is_scalar($value)) { - throw new Exception("Si la colonne est configurée à la volée, sa valeur doit être scalaire - soit id, soit clé étrangère, c'est-à-dire status_id"); - } - - $config = explode('_', $value); - - if ($config[0] === 'id') { - $newColumns[$value] = [Generator::PK]; - } elseif (count($config) === 2 || $config[1] === 'id') { - $newColumns[$value] = [Generator::RELATION, $config[0], 'id']; - } else { - throw new Exception('Le champ ' . $value . ' est mal configuré'); - } - } else { - $newColumns[$key] = $value; - } - } - - return $newColumns; - } -} From 364afb9e94bf379c8eaebcb10ee17fdcde7f96cb Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 25 Feb 2026 15:36:00 +0100 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20ajout=20de=20callbacks=20avant=20et?= =?UTF-8?q?=20apr=C3=A8s=20insertion=20dans=20le=20Seeder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Commands/Seed.php | 15 +----------- src/Seeder/Seed.php | 55 +++++++++++++++++++++++++++++++++++++++++-- src/Seeder/Seeder.php | 13 ++++++---- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/src/Commands/Seed.php b/src/Commands/Seed.php index 4b46ff4..875d852 100644 --- a/src/Commands/Seed.php +++ b/src/Commands/Seed.php @@ -55,7 +55,7 @@ public function handle() { $group = $this->option('group'); $silent = $this->option('silent') !== null; - $locale = $this->option('locale'); + $locale = $this->option('locale', config('app.language', 'fr_FR')); $this->db = $this->resolver->connect($group); @@ -167,18 +167,5 @@ protected function runSeeder(Seeder $seeder): void foreach (array_unique($executed) as $seeded) { $this->eol()->write(' ✔ ')->writer->green($seeded); } - - $this->displayStats($seeder); - } - - /** - * Affiche les statistiques d'exécution - */ - protected function displayStats(Seeder $seeder): void - { - // Note: Cette méthode suppose que vous avez un moyen de récupérer les stats - // À adapter selon votre implémentation réelle - - $this->eol()->write('Langue utilisée : ')->writer->yellow($seeder->getLocale()); } } diff --git a/src/Seeder/Seed.php b/src/Seeder/Seed.php index 2db72af..3e45317 100644 --- a/src/Seeder/Seed.php +++ b/src/Seeder/Seed.php @@ -56,6 +56,20 @@ class Seed */ protected bool $truncate = false; + /** + * Callbacks avant insertion + * + * @var list + */ + protected array $beforeInsertCallbacks = []; + + /** + * Callbacks après insertion + * + * @var list + */ + protected array $afterInsertCallbacks = []; + /** * Données générées */ @@ -145,6 +159,26 @@ public function truncate(bool $truncate = true): self return $this; } + /** + * Ajoute un callback avant chaque insertion + */ + public function beforeInsert(callable $callback): self + { + $this->beforeInsertCallbacks[] = $callback; + + return $this; + } + + /** + * Ajoute un callback après chaque insertion + */ + public function afterInsert(callable $callback): self + { + $this->afterInsertCallbacks[] = $callback; + + return $this; + } + /** * Exécute le seed */ @@ -168,13 +202,15 @@ protected function insertRawData(): void { $columns = array_keys(reset($this->rawData)); - foreach ($this->rawData as $row) { + foreach ($this->rawData as $index => $row) { $data = []; foreach ($columns as $column) { $data[$column] = $row[$column] ?? null; } + $this->executeCallbacks($this->beforeInsertCallbacks, $data, $index); $this->builder->insert($data); + $this->executeCallbacks($this->afterInsertCallbacks, $data, $index, $this->db->lastId()); } } @@ -204,8 +240,10 @@ protected function generateData(): void } // Insérer les données - foreach ($this->generated as $row) { + foreach ($this->generated as $index => $row) { + $this->executeCallbacks($this->beforeInsertCallbacks, $row, $index); $this->builder->insert($row); + $this->executeCallbacks($this->afterInsertCallbacks, $row, $index, $this->db->lastId()); } } @@ -355,4 +393,17 @@ protected function getTableValues(string $table, string $column): array ->select($column) ->all(PDO::FETCH_COLUMN); } + + /** + * Exécute les callbacks + */ + protected function executeCallbacks(array $callbacks, array &$data, int $index, $insertId = null): void + { + foreach ($callbacks as $callback) { + $result = $callback($data, $index, $insertId); + if ($result !== null) { + $data = $result; // Permet de modifier les données + } + } + } } diff --git a/src/Seeder/Seeder.php b/src/Seeder/Seeder.php index 1672901..1cbe188 100644 --- a/src/Seeder/Seeder.php +++ b/src/Seeder/Seeder.php @@ -11,7 +11,7 @@ namespace BlitzPHP\Database\Seeder; -use BlitzPHP\Database\Commands\Seed as SeedCommand; +use BlitzPHP\Cli\Console\Command; use BlitzPHP\Database\Connection\BaseConnection; use BlitzPHP\Database\Exceptions\SeederException; @@ -35,7 +35,7 @@ abstract class Seeder /** * Instance de la console */ - protected ?SeedCommand $command = null; + protected ?Command $command = null; /** * Tables à remplir @@ -76,7 +76,7 @@ public function __construct(BaseConnection $db) /** * Définit l'instance de commande */ - public function setCommand(SeedCommand $command): self + public function setCommand(?Command $command): self { $this->command = $command; @@ -158,7 +158,12 @@ protected function call(array|string $seeders): self { foreach ((array) $seeders as $seeder) { $seeder = $this->resolve($seeder); - $seeder->setSilent($this->silent)->run(); + + $seeder->setSilent($this->silent) + ->setCommand($this->command) + ->setLocale($this->locale) + ->run(); + $this->called[] = $seeder::class; } From 8f07ca8bf2af90cff6dab8954c820a51d8eaf495 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 25 Feb 2026 16:18:40 +0100 Subject: [PATCH 3/3] =?UTF-8?q?patch:=20optimisation=20de=20l'insertion=20?= =?UTF-8?q?de=20gros=20volume=20de=20donn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit utilisation de array_chunk pour limiter le lot de données à insérer utilisation de bulkInsert à la place de insert avec de garantir une insertation rapide --- src/Seeder/Seed.php | 123 ++++++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 38 deletions(-) diff --git a/src/Seeder/Seed.php b/src/Seeder/Seed.php index 3e45317..6d6e5ba 100644 --- a/src/Seeder/Seed.php +++ b/src/Seeder/Seed.php @@ -22,6 +22,11 @@ */ class Seed { + /** + * Taille des lots pour bulk insert + */ + protected const DEFAULT_BULK_SIZE = 100; + /** * Builder pour la table */ @@ -51,6 +56,11 @@ class Seed */ protected int $rowCount = 30; + /** + * Taille des lots pour bulk insert + */ + protected int $bulkSize = self::DEFAULT_BULK_SIZE; + /** * Faut-il vider la table avant ? */ @@ -129,6 +139,16 @@ public function rows(int $count): self return $this; } + /** + * Définit la taille des lots pour bulk insert + */ + public function bulkSize(int $size): self + { + $this->bulkSize = max(1, $size); + + return $this; + } + /** * Définit des données brutes */ @@ -161,6 +181,8 @@ public function truncate(bool $truncate = true): self /** * Ajoute un callback avant chaque insertion + * + * @param Closure(array $data, int $index, ?int $insertId): array|null|void $callback */ public function beforeInsert(callable $callback): self { @@ -171,6 +193,8 @@ public function beforeInsert(callable $callback): self /** * Ajoute un callback après chaque insertion + * + * @param Closure(array $data, int $index, ?int $insertId): array|null|void $callback */ public function afterInsert(callable $callback): self { @@ -188,10 +212,10 @@ public function execute(): void $this->builder->truncate(); } - if (!empty($this->rawData)) { + if ($this->rawData !== []) { $this->insertRawData(); } else { - $this->generateData(); + $this->generateAndInsertData(); } } @@ -202,50 +226,73 @@ protected function insertRawData(): void { $columns = array_keys(reset($this->rawData)); - foreach ($this->rawData as $index => $row) { + $chunks = array_chunk($this->rawData, $this->bulkSize); + + foreach ($chunks as $chunkIndex => $chunk) { $data = []; - foreach ($columns as $column) { - $data[$column] = $row[$column] ?? null; + foreach ($chunk as $rowIndex => $row) { + $preparedRow = []; + foreach ($columns as $column) { + $preparedRow[$column] = $row[$column] ?? null; + } + + $this->executeCallbacks($this->beforeInsertCallbacks, $preparedRow, $rowIndex); + $data[] = $preparedRow; + } + + $this->builder->bulkInsert($data); + + // Callbacks after (avec l'ID du premier élément comme approximation) + $firstId = $this->db->lastId(); + foreach ($data as $index => $row) { + $this->executeCallbacks($this->afterInsertCallbacks, $row, $index, $firstId + $index); } - - $this->executeCallbacks($this->beforeInsertCallbacks, $data, $index); - $this->builder->insert($data); - $this->executeCallbacks($this->afterInsertCallbacks, $data, $index, $this->db->lastId()); } } /** - * Génère les données + * Génère et insère les données */ - protected function generateData(): void + protected function generateAndInsertData(): void { - // Résoudre les dépendances une seule fois - $dependencies = $this->resolveDependencies(); - - // Générer les données ligne par ligne - for ($i = 0; $i < $this->rowCount; $i++) { - $row = []; - - // Traiter les configurations d'abord (plus rapides) - foreach ($this->columns as $column => $definition) { - $row[$column] = $this->resolveConfig($definition, $dependencies); - } - - // Traiter les closures ensuite (plus flexibles) - foreach ($this->closures as $column => $closure) { - $row[$column] = $closure($this->faker, $dependencies, $i); - } - - $this->generated[] = $row; - } - - // Insérer les données - foreach ($this->generated as $index => $row) { - $this->executeCallbacks($this->beforeInsertCallbacks, $row, $index); - $this->builder->insert($row); - $this->executeCallbacks($this->afterInsertCallbacks, $row, $index, $this->db->lastId()); - } - } + // Résoudre les dépendances une seule fois + $dependencies = $this->resolveDependencies(); + + // Générer les données par lots + $batches = (int) ceil($this->rowCount / $this->bulkSize); + + for ($batch = 0; $batch < $batches; $batch++) { + $batchData = []; + $start = $batch * $this->bulkSize; + $end = min($start + $this->bulkSize, $this->rowCount); + + for ($i = $start; $i < $end; $i++) { + $row = []; + + // Traiter les configurations d'abord (plus rapides) + foreach ($this->columns as $column => $definition) { + $row[$column] = $this->resolveConfig($definition, $dependencies); + } + + // Traiter les closures ensuite (plus flexibles) + foreach ($this->closures as $column => $closure) { + $row[$column] = $closure($this->faker, $dependencies, $i); + } + + $this->executeCallbacks($this->beforeInsertCallbacks, $row, $i); + + $batchData[] = $row; + } + + $this->builder->bulkInsert($batchData); + + // Callbacks after (avec approximation des IDs) + $firstId = $this->db->lastId(); + foreach ($batchData as $offset => $row) { + $this->executeCallbacks($this->afterInsertCallbacks, $row, $start + $offset, $firstId + $offset); + } + } + } /** * Résout une configuration