From c004a91b5fec5b7e5af1410eeefb48bdf520d5dd Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Sat, 27 Jun 2026 10:03:03 -0300 Subject: [PATCH] Match column-prefix specifiers case-insensitively in PrestyledAssoc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With PHP-conventional camelCase scope names, the column specifier (e.g. `postCategory`) is emitted as an unquoted SQL alias. PostgreSQL folds unquoted identifiers to lower case, so the row comes back prefixed `postcategory__`, while the scope map was keyed by the camelCase name — raising "Unknown column prefix". Snake_case names never tripped this because they were already lower case. Key the scope map and group the row by the lowercased prefix, so a specifier round-trips regardless of how the driver folds identifier case (PostgreSQL lowercases, MySQL/SQLite preserve). Property names are unaffected — they were already lower case in practice and resolve through the entity factory. --- src/Hydrators/PrestyledAssoc.php | 9 +++++++-- tests/Hydrators/PrestyledAssocTest.php | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Hydrators/PrestyledAssoc.php b/src/Hydrators/PrestyledAssoc.php index f83dc2d..28cc0b5 100644 --- a/src/Hydrators/PrestyledAssoc.php +++ b/src/Hydrators/PrestyledAssoc.php @@ -11,6 +11,7 @@ use function explode; use function is_array; +use function strtolower; /** * Hydrates associative rows whose keys are pre-styled as `specifier__styledProp`. @@ -41,7 +42,7 @@ public function hydrateAll( $grouped = []; foreach ($raw as $alias => $value) { [$prefix, $prop] = explode('__', $alias, 2); - $grouped[$prefix][$prop] = $value; + $grouped[strtolower($prefix)][$prop] = $value; } /** @var SplObjectStorage $entities */ @@ -83,9 +84,13 @@ private function buildScopeMap(Scope $scope): array return $this->scopeMap; } + // Keys are lowercased: a column specifier survives the round-trip through + // the database as the alias case the driver chose (PostgreSQL folds + // unquoted identifiers to lower case, others preserve them), so the + // prefix is matched case-insensitively. $this->scopeMap = []; foreach (ScopeIterator::recursive($scope) as $spec => $c) { - $this->scopeMap[$spec] = $c; + $this->scopeMap[strtolower($spec)] = $c; } $this->cachedScope = $scope; diff --git a/tests/Hydrators/PrestyledAssocTest.php b/tests/Hydrators/PrestyledAssocTest.php index e729735..8cbd991 100644 --- a/tests/Hydrators/PrestyledAssocTest.php +++ b/tests/Hydrators/PrestyledAssocTest.php @@ -53,6 +53,26 @@ public function hydrateSingleEntity(): void $this->assertEquals('Alice', $this->factory->get($entity, 'name')); } + #[Test] + public function hydrateMatchesSpecifierPrefixCaseInsensitively(): void + { + // A camelCase scope name (`edgeCaseEntity`) becomes the column specifier; + // PostgreSQL folds the unquoted alias to lower case, so the row comes back + // prefixed `edgecaseentity__`. The prefix must still resolve to its scope. + $hydrator = new PrestyledAssoc($this->factory); + $scope = new Scope('edgeCaseEntity'); + + $result = $hydrator->hydrateAll( + ['edgecaseentity__initialized' => 'folded'], + $scope, + ); + + $this->assertNotFalse($result); + $this->assertCount(1, $result); + $result->rewind(); + $this->assertEquals('folded', $this->factory->get($result->current(), 'initialized')); + } + #[Test] public function hydrateMultipleEntitiesFromJoinedRow(): void {