From 897770534b53a04553672a0a60baf0a11ffcdf05 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Sat, 27 Jun 2026 09:48:25 -0300 Subject: [PATCH] Resolve scope names to tables via Style::realName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mapper used the scope name verbatim as the SQL table and alias. Route every table emission (from/join/insertInto/update/deleteFrom) and the composition check through `Style::realName`, while the alias stays the PHP scope name and an `AS` bridges them whenever they differ — including the root table, which returned before the join-path `AS` logic and so could emit `FROM post_tag` against an undefined `postTag` alias for a multi-word root. DSL calls in the tests become PHP-conventional camelCase (post_category -> postCategory); the test schema, columns and seed data stay snake_case. Requires respect/data with Stylable::realName. --- src/Mapper.php | 24 ++++++++++++++---------- tests/MapperTest.php | 17 +++++++++++++++-- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Mapper.php b/src/Mapper.php index 7ba52fb..3382ff6 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -24,7 +24,7 @@ use function iterator_to_array; /** Maps objects to database operations */ -final class Mapper extends AbstractMapper +class Mapper extends AbstractMapper { public readonly Db $db; @@ -168,7 +168,7 @@ private function rawDelete( $condition = $this->guessCondition($columns, $scope); return $this->db - ->deleteFrom($scope->name) + ->deleteFrom($this->style->realName($scope->name)) ->where($condition) ->exec(); } @@ -179,7 +179,7 @@ private function rawUpdate(array $columns, Scope $scope): bool $condition = $this->guessCondition($columns, $scope); return $this->db - ->update($scope->name) + ->update($this->style->realName($scope->name)) ->set($columns) ->where($condition) ->exec(); @@ -192,7 +192,7 @@ private function rawInsert( object|null $entity = null, ): bool { $result = $this->db - ->insertInto($scope->name, array_keys($columns)) + ->insertInto($this->style->realName($scope->name), array_keys($columns)) ->values(array_values($columns)) ->exec(); @@ -362,18 +362,21 @@ private function parseScope( //No parent scope means it's the first table in the query if ($parentAlias === null) { - $sql->from($entity); + $sql->from($s->realName($entity)); + if ($s->realName($entity) !== $alias) { + $sql->as($alias); + } return; } if ($scope->required) { - $sql->innerJoin($entity); + $sql->innerJoin($s->realName($entity)); } else { - $sql->leftJoin($entity); + $sql->leftJoin($s->realName($entity)); } - if ($alias !== $entity) { + if ($s->realName($entity) !== $alias) { $sql->as($alias); } @@ -396,9 +399,10 @@ private function isCompositionJoin(Scope $scope, string $entity, string $parent) foreach ($scope->with as $child) { $connected = $child->name; + $table = $this->style->realName($entity); if ( - $entity === $this->style->composed($parent, $connected) - || $entity === $this->style->composed($connected, $parent) + $table === $this->style->composed($parent, $connected) + || $table === $this->style->composed($connected, $parent) ) { return true; } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index e65883d..a015f5c 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -365,7 +365,7 @@ public function testNtoN(): void { $mapper = $this->mapper; $comments = $mapper->fetchAll($mapper->comment([ - $mapper->post([$mapper->post_category([$mapper->category(filter: 2)])]), + $mapper->post([$mapper->postCategory([$mapper->category(filter: 2)])]), ])); $comment = current($comments); $this->assertEquals(1, count($comments)); @@ -379,11 +379,24 @@ public function testNtoN(): void public function testManyToManyReverse(): void { $mapper = $this->mapper; - $cat = $mapper->fetch($mapper->category([$mapper->post_category([$mapper->post(filter: 5)])])); + $cat = $mapper->fetch($mapper->category([$mapper->postCategory([$mapper->post(filter: 5)])])); $this->assertEquals(2, $cat->id); $this->assertEquals('Sample Category', $cat->name); } + public function testFetchMultiWordEntityAsRoot(): void + { + // A multi-word root scope (postCategory → table post_category) must emit + // `FROM post_category AS postCategory`: realName differs from the PHP + // alias, so the root needs its own AS just like joined tables do. + $mapper = $this->mapper; + $junction = $mapper->fetch($mapper->postCategory(filter: 66)); + // The fetch succeeding at all proves the alias was emitted: without the + // root AS the query would be `FROM post_category` while SELECT references + // an undefined `postCategory.id`. + $this->assertEquals(66, $junction->id); + } + public function testSimplePersist(): void { $mapper = $this->mapper;