From 7c52abd194488621bad6e4eb75ed86abf0c542cc Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 30 Mar 2026 16:47:20 +0100 Subject: [PATCH 1/5] fix: correction de l'echappement des identifiants sql --- spec/Connection/BaseConnection.spec.php | 9 +- src/Connection/BaseConnection.php | 118 +++++++++++++++++++++++- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/spec/Connection/BaseConnection.spec.php b/spec/Connection/BaseConnection.spec.php index c1ae00c..21b1a70 100644 --- a/spec/Connection/BaseConnection.spec.php +++ b/spec/Connection/BaseConnection.spec.php @@ -54,10 +54,17 @@ it(": Échappement des identifiants", function() { expect($this->connection->escapeIdentifiers('users.id'))->toBe('`users`.`id`'); - // expect($this->connection->escapeIdentifiers('count(*)'))->toBe('count(*)'); + expect($this->connection->escapeIdentifiers('count(id)'))->toBe('count(`id`)'); + expect($this->connection->escapeIdentifiers('count(users.id)'))->toBe('count(`users`.`id`)'); expect($this->connection->escapeIdentifiers(['users.id', 'name']))->toBe(['`users`.`id`', '`name`']); }); + it(": Échappement des identifiants reservés", function() { + expect($this->connection->escapeIdentifiers('users.*'))->toBe('`users`.*'); + expect($this->connection->escapeIdentifiers(['count(*)', 'count(users.*)']))->toBe(['count(*)', 'count(`users`.*)']); + expect($this->connection->escapeIdentifiers(['users.id', 'name', '*']))->toBe(['`users`.`id`', '`name`', '*']); + }); + it(": Échappement des chaînes", function() { $escaped = $this->connection->escapeString("O'Reilly"); expect($escaped)->toBe("'O''Reilly'"); diff --git a/src/Connection/BaseConnection.php b/src/Connection/BaseConnection.php index 49b1242..5a5ab16 100644 --- a/src/Connection/BaseConnection.php +++ b/src/Connection/BaseConnection.php @@ -768,6 +768,17 @@ public function escapeIdentifiers(mixed $item): mixed return array_map([$this, 'escapeIdentifiers'], $item); } + if ($item instanceof Stringable) { + $item = (string) $item; + } + + $item = trim($item); + + // Vérifier d'abord si c'est un appel de fonction SQL + if ($processed = $this->processSqlFunctionCall($item)) { + return $processed; + } + if (! isset($this->escapeCache[$item])) { $this->escapeCache[$item] = $this->doEscapeIdentifiers($item); } @@ -777,10 +788,6 @@ public function escapeIdentifiers(mixed $item): mixed protected function doEscapeIdentifiers(string $item): string { - if ($this->isReserved($item) || Utils::isSqlFunction($item)) { - return $item; - } - if (str_contains($item, '.')) { $parts = explode('.', $item); @@ -795,6 +802,10 @@ protected function doEscapeIdentifiers(string $item): string */ protected function escapeIdentifier(string $item): string { + if ($this->isReserved($item) || Utils::isSqlFunction($item)) { + return $item; + } + if ($this->isEscapedIdentifier($item)) { return $item; } @@ -802,6 +813,105 @@ protected function escapeIdentifier(string $item): string return $this->escapeChar . $item . $this->escapeChar; } + /** + * Traite un appel de fonction SQL et échappe ses paramètres si nécessaire + */ + protected function processSqlFunctionCall(string $value): ?string + { + // Pattern pour capturer: functionName(param1, param2, ...) + if (preg_match('/^(\w+)\s*\((.*)\)$/', $value, $matches)) { + $functionName = $matches[1]; + $parameters = $matches[2]; + + if (Utils::isSqlFunction($functionName)) { + // Si pas de paramètres, retourner tel quel + if (trim($parameters) === '') { + return $value; + } + + // Traiter chaque paramètre + $processedParams = []; + $params = $this->splitParameters($parameters); + + foreach ($params as $param) { + $param = trim($param); + $processedParams[] = $this->processSqlFunctionParameter($param); + } + + return $functionName . '(' . implode(', ', $processedParams) . ')'; + } + } + + return null; + } + + /** + * Traite un paramètre d'appel de fonction SQL + */ + protected function processSqlFunctionParameter(string $param): string + { + // Si c'est un wildcard + if ($param === '*') { + return $param; + } + + // Si le paramètre contient un point, c'est un identifiant qualifié + if (str_contains($param, '.')) { + // Séparer les parties et échapper chaque partie individuellement + $parts = explode('.', $param); + $parts = array_map($this->escapeIdentifier(...), $parts); + + return implode('.', $parts); + } + + // Si le paramètre est un identifiant simple + if (preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $param)) { + return $this->escapeIdentifier($param); + } + + // Si c'est un appel de fonction imbriqué + if (preg_match('/^(\w+)\s*\(.*\)$/', $param)) { + return $this->processSqlFunctionCall($param) ?? $param; + } + + // Sinon, garder tel quel (nombres, chaînes entre quotes, etc.) + return $param; + } + + /** + * Sépare les paramètres d'une fonction en gérant les virgules dans les parenthèses + */ + protected function splitParameters(string $parameters): array + { + $params = []; + $current = ''; + $depth = 0; + $length = strlen($parameters); + + for ($i = 0; $i < $length; $i++) { + $char = $parameters[$i]; + + if ($char === '(') { + $depth++; + $current .= $char; + } elseif ($char === ')') { + $depth--; + $current .= $char; + } elseif ($char === ',' && $depth === 0) { + $params[] = trim($current); + $current = ''; + } else { + $current .= $char; + } + } + + if (trim($current) !== '') { + $params[] = trim($current); + } + + return $params; + } + /** * Vérifie si un identifiant est réservé */ From e2c25ef547d112e037dcec834533169b68b176c1 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 30 Mar 2026 16:48:21 +0100 Subject: [PATCH 2/5] fix: bug sur la methode Utils::isAlias --- spec/Utils.spec.php | 5 +++-- src/Utils.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/Utils.spec.php b/spec/Utils.spec.php index eddaece..a89c19e 100644 --- a/spec/Utils.spec.php +++ b/spec/Utils.spec.php @@ -48,7 +48,7 @@ }); it(": isAlias", function() { - expect(Utils::isAlias('user_alias'))->toBe(true); + expect(Utils::isAlias('user_alias'))->toBe(false); expect(Utils::isAlias('AS user_alias'))->toBe(true); expect(Utils::isAlias('user-alias'))->toBe(false); // tiret pas autorisé expect(Utils::isAlias('user.alias'))->toBe(false); // point pas autorisé @@ -56,8 +56,9 @@ it(": extractAlias", function() { expect(Utils::extractAlias('AS alias'))->toBe('alias'); - expect(Utils::extractAlias('alias'))->toBe('alias'); + expect(Utils::extractAlias('alias'))->toBeNull(); expect(Utils::extractAlias(' AS alias '))->toBe('alias'); + expect(Utils::extractAlias('users AS alias '))->toBe('alias'); }); it(": extractOperatorFromColumn", function() { diff --git a/src/Utils.php b/src/Utils.php index 710133e..cefca9b 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -128,7 +128,7 @@ public static function isAlias(string $value): bool $clean = static::extractAlias($value); // Un alias valide ne contient que des lettres, chiffres, underscore - return preg_match('/^[a-zA-Z0-9_]+$/', $clean) === 1; + return preg_match('/^[a-zA-Z0-9_]+$/', $clean ?? '') === 1; } /** From e5a69f50377f5639d5a40905489d2e7b0e387a0b Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Mon, 30 Mar 2026 20:22:46 +0100 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20ajout=20de=20la=20m=C3=A9thode=20la?= =?UTF-8?q?stId=20pour=20r=C3=A9cup=C3=A9rer=20le=20dernier=20ID=20ins?= =?UTF-8?q?=C3=A9r=C3=A9=20et=20mise=20=C3=A0=20jour=20des=20r=C3=A9f?= =?UTF-8?q?=C3=A9rences=20dans=20les=20classes=20concern=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Builder/Concerns/DataMethods.php | 10 +++++++++- src/Connection/BaseConnection.php | 6 +++++- src/Model.php | 28 +++++++++++++++++++++++++--- src/Query/Result.php | 22 +++++++++++++++------- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/Builder/Concerns/DataMethods.php b/src/Builder/Concerns/DataMethods.php index 91451cf..ff11d31 100644 --- a/src/Builder/Concerns/DataMethods.php +++ b/src/Builder/Concerns/DataMethods.php @@ -167,7 +167,7 @@ public function insertUsing(array $columns, BuilderInterface|Closure $query) public function insertGetId(array $values, ?string $sequence = null) { if (is_bool($inserted = $this->insert($values))) { - return $inserted === true ? $this->db->lastId($this->getTable()) : null; + return $inserted === true ? $this->lastId($this->getTable()) : null; } return $inserted; @@ -195,6 +195,14 @@ protected function getKeyName(): string return 'id'; } + /** + * Récupère le dernier ID généré par l'auto-incrémentation + */ + public function lastId(?string $table = null): ?int + { + return $this->db->lastId($table); + } + /* |-------------------------------------------------------------------------- | RAW EXPRESSIONS diff --git a/src/Connection/BaseConnection.php b/src/Connection/BaseConnection.php index 5a5ab16..d500cb6 100644 --- a/src/Connection/BaseConnection.php +++ b/src/Connection/BaseConnection.php @@ -1243,7 +1243,11 @@ public function error(): array public function lastId(?string $table = null): ?int { try { - return (int) $this->pdo->lastInsertId($table); + if (-1 === $id = $this->result?->lastId() ?? -1) { + $id = $this->pdo->lastInsertId($table); + } + + return (int) $id; } catch (PDOException) { return null; } diff --git a/src/Model.php b/src/Model.php index 689d23f..14a65b7 100644 --- a/src/Model.php +++ b/src/Model.php @@ -241,10 +241,10 @@ public function table(string $table): static $key = $alias ?: $table; if (! isset($this->builders[$key])) { - $this->builders[$key] = $this->db->table($table); + $this->builders[$key] = $this->db->newQuery(); } - $this->currentBuilder = $this->builders[$key]->reset(); + $this->currentBuilder = $this->builders[$key]->table($table); $this->currentAlias = $key; return $this; @@ -269,6 +269,10 @@ public function builder(?string $table = null): BaseBuilder $this->table($this->table); } + if ($this->currentBuilder->getTable() === '') { + $this->currentBuilder->table($this->table); + } + return $this->currentBuilder; } @@ -649,6 +653,18 @@ public function countAllResults(bool $reset = true): int return (int) $count; } + /** + * Récupère le dernier ID inséré + */ + public function lastInsertId(): int|string + { + if ($this->lastInsertId === 0) { + $this->lastInsertId = $this->db->lastId(); + } + + return $this->lastInsertId; + } + /** * Active temporairement les soft deletes */ @@ -931,7 +947,13 @@ public function __isset(string $name): bool return true; } - return isset($this->builder()->{$name}); + try { + return isset($this->builder()->{$name}); + } catch (DatabaseException) { + $this->currentBuilder = null; // Réinitialiser le builder actuel en cas d'erreur + $this->currentAlias = null; + return false; + } } /** diff --git a/src/Query/Result.php b/src/Query/Result.php index a1d0ea2..628104a 100644 --- a/src/Query/Result.php +++ b/src/Query/Result.php @@ -50,11 +50,13 @@ class Result implements ResultInterface 'all' => 'get', 'one' => 'first', 'columnCount' => 'countColumn', - 'lastId' => 'insertID', + 'insertID' => 'lastId', ]; public function __construct(protected BaseConnection $db, protected PDOStatement $statement, protected bool $success = true) { + $this->details['insert_id'] = $db->getConnection()->lastInsertId(); + $db->triggerEvent($this, 'db:result'); } @@ -286,7 +288,9 @@ protected function fetchAssoc() return null; } - return $this->statement->fetch(PDO::FETCH_ASSOC); + $result = $this->statement->fetch(PDO::FETCH_ASSOC); + + return $result === false ? null : $result; } /** @@ -302,7 +306,9 @@ protected function fetchObject(string $className = 'stdClass') $this->statement->setFetchMode(PDO::FETCH_CLASS, $className); - return $this->statement->fetch(); + $result = $this->statement->fetch(); + + return $result === false ? null : $result; } /** @@ -340,12 +346,14 @@ public function numRows(): int /** * Return the last id generated by autoincrement - * - * @return int|string */ - public function insertID() + public function lastId(): int { - return $this->db->insertID(); + if ($this->details['insert_id'] !== -1) { + return $this->details['insert_id']; + } + + return $this->db->getConnection()->lastInsertId(); } public function __call(string $name, array $arguments): mixed From d323fc0476a1292462aeef427424a369c4dca5e0 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 8 Apr 2026 18:18:43 +0100 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20ajout=20de=20la=20m=C3=A9thode=20us?= =?UTF-8?q?eConnection=20pour=20g=C3=A9rer=20dynamiquement=20les=20connexi?= =?UTF-8?q?ons=20dans=20les=20migrations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Migration/Migration.php | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Migration/Migration.php b/src/Migration/Migration.php index 32eb162..d21e3a3 100644 --- a/src/Migration/Migration.php +++ b/src/Migration/Migration.php @@ -64,6 +64,14 @@ public function shouldRun(): bool return true; } + /** + * Défini le groupe de connextion à utiliser pour la migration + */ + protected function useConnection(): string + { + return 'default'; + } + /** * Initialise les éléments nécessaire pour le fonctionnement de la migration * @@ -71,9 +79,9 @@ public function shouldRun(): bool */ public function initialize(DatabaseManager $dbManager, BaseConnection $db): self { - $this->db = $db; - $this->dbManager = $dbManager; - $this->connections['default'] = $db; + $this->db = $db; + $this->dbManager = $dbManager; + $this->connections[$this->useConnection()] = $db; return $this; } @@ -194,7 +202,7 @@ protected function connection(string $name): ConnectionProxy */ protected function create(string $table, callable $callback, bool $ifNotExists = false): void { - $this->createOnConnection('default', $table, $callback, $ifNotExists); + $this->createOnConnection($this->useConnection(), $table, $callback, $ifNotExists); } /** @@ -210,7 +218,7 @@ protected function createIfNotExists(string $table, callable $callback): void */ protected function alter(string $table, callable $callback): void { - $this->alterOnConnection('default', $table, $callback); + $this->alterOnConnection($this->useConnection(), $table, $callback); } /** @@ -226,7 +234,7 @@ public function modify(string $table, callable $callback): void */ protected function drop(string $table, bool $ifExists = false): void { - $this->dropOnConnection('default', $table, $ifExists); + $this->dropOnConnection($this->useConnection(), $table, $ifExists); } /** @@ -242,7 +250,7 @@ protected function dropIfExists(string $table): void */ protected function rename(string $from, string $to): void { - $this->renameOnConnection('default', $from, $to); + $this->renameOnConnection($this->useConnection(), $from, $to); } /** @@ -250,7 +258,7 @@ protected function rename(string $from, string $to): void */ protected function hasTable(string $name): bool { - return $this->hasTableOnConnection('default', $name); + return $this->hasTableOnConnection($this->useConnection(), $name); } /** @@ -258,7 +266,7 @@ protected function hasTable(string $name): bool */ protected function hasColumn(string $table, string $column): bool { - return $this->hasColumnOnConnection('default', $table, $column); + return $this->hasColumnOnConnection($this->useConnection(), $table, $column); } /** From b1d2c886065be064fb3530e228e2bdaad51c1633 Mon Sep 17 00:00:00 2001 From: Dimitri Sitchet Tomkeu Date: Wed, 8 Apr 2026 19:05:58 +0100 Subject: [PATCH 5/5] patch: utilisation de la classe GeneratorCommand pour les commande de generation --- src/Commands/Generators/Migration.php | 38 +++++---------------------- src/Commands/Generators/Seeder.php | 32 ++++++---------------- 2 files changed, 15 insertions(+), 55 deletions(-) diff --git a/src/Commands/Generators/Migration.php b/src/Commands/Generators/Migration.php index d66855d..51a02ec 100644 --- a/src/Commands/Generators/Migration.php +++ b/src/Commands/Generators/Migration.php @@ -11,8 +11,7 @@ namespace BlitzPHP\Database\Commands\Generators; -use BlitzPHP\Cli\Console\Command; -use BlitzPHP\Cli\Traits\GeneratorTrait; +use BlitzPHP\Cli\Commands\Generators\GeneratorCommand; use InvalidArgumentException; /** @@ -21,15 +20,8 @@ * Analyse le nom de la migration pour déterminer automatiquement * l'action (create/modify) et la table concernée. */ -class Migration extends Command +class Migration extends GeneratorCommand { - use GeneratorTrait; - - /** - * {@inheritDoc} - */ - protected string $group = 'Générateurs'; - /** * {@inheritDoc} */ @@ -61,6 +53,11 @@ class Migration extends Command '--suffix' => 'Ajoute "Migration" au nom de la classe (par exemple, User => UserMigration)', ]; + protected string $component = 'Migration'; + protected string $directory = 'Database\Migrations'; + protected string $template = 'migration.tpl.php'; + protected string $templatePath = __DIR__ . '/Views'; + /** * Mots-clés pour les actions de création */ @@ -82,27 +79,6 @@ class Migration extends Command */ protected array $dropKeywords = ['drop', 'delete', 'remove']; - /** - * {@inheritDoc} - */ - public function handle() - { - $this->component = 'Migration'; - $this->directory = 'Database\Migrations'; - $this->template = 'migration.tpl.php'; - $this->templatePath = __DIR__ . '/Views'; - - try { - $this->generateClass($this->parameters()); - - return EXIT_SUCCESS; - } catch (InvalidArgumentException $e) { - $this->error($e->getMessage()); - - return EXIT_ERROR; - } - } - /** * Prépare les options et effectue les remplacements nécessaires. */ diff --git a/src/Commands/Generators/Seeder.php b/src/Commands/Generators/Seeder.php index 5222bc9..4000a32 100644 --- a/src/Commands/Generators/Seeder.php +++ b/src/Commands/Generators/Seeder.php @@ -11,22 +11,14 @@ namespace BlitzPHP\Database\Commands\Generators; -use BlitzPHP\Cli\Console\Command; -use BlitzPHP\Cli\Traits\GeneratorTrait; +use BlitzPHP\Cli\Commands\Generators\GeneratorCommand; /** * Génère un fichier squelette de seeder. */ -class Seeder extends Command +class Seeder extends GeneratorCommand { - use GeneratorTrait; - - /** - * {@inheritDoc} - */ - protected string $group = 'Generateurs'; - - /** + /** * {@inheritDoc} */ protected string $name = 'make:seeder'; @@ -57,17 +49,9 @@ class Seeder extends Command '--force' => 'Forcer l\'écrasement du fichier existant.', ]; - /** - * {@inheritDoc} - */ - public function handle() - { - $this->component = 'Seeder'; - $this->directory = 'Database\Seeds'; - $this->template = 'seeder.tpl.php'; - $this->templatePath = __DIR__ . '/Views'; - - $this->classNameLang = 'CLI.generator.className.seeder'; - $this->generateClass($this->parameters()); - } + protected string $component = 'Seeder'; + protected string $directory = 'Database\Seeds'; + protected string $template = 'seeder.tpl.php'; + protected string $templatePath = __DIR__ . '/Views'; + protected string $classNameLang = 'CLI.generator.className.seeder'; }