diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9fee2cb..1b058d2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,7 +5,6 @@ parameters: - tests/ ignoreErrors: - message: '/Call to an undefined (static )?method Respect\\Relational\\(Sql|Db|Mapper)::\w+\(\)\./' - - message: '/Call to an undefined (static )?method Respect\\Data\\Collections\\(Collection|Composite|Typed)::\w+\(\)\./' - message: '/Unsafe usage of new static\(\)\./' - message: '/Cannot unset property .+ because it might have hooks in a subclass\./' - diff --git a/src/Mapper.php b/src/Mapper.php index 060308c..b67ea09 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -10,7 +10,6 @@ use Respect\Data\AbstractMapper; use Respect\Data\CollectionIterator; use Respect\Data\Collections\Collection; -use Respect\Data\Collections\Composite; use Respect\Data\Hydrator; use Respect\Data\Hydrators\PrestyledAssoc; use SplObjectStorage; @@ -151,94 +150,6 @@ private function flushSingle(object $entity): void } } - /** - * Extract composed columns from parent, UPDATE child tables using FK relationship. - * - * For existing entities (UPDATE path), the parent PK is known and we can - * update the child table directly: UPDATE comment SET text=? WHERE post_id=? - * - * For new entities (INSERT path), child inserts happen after the parent - * via insertCompositionChildren(). - * - * @param array $cols - * - * @return array - */ - private function extractAndOperateCompositions(Collection $collection, array $cols): array - { - if (!$collection instanceof Composite) { - return $cols; - } - - $parentPk = $this->style->identifier($collection->name); - $parentPkValue = $cols[$parentPk] ?? null; - $fkToParent = $this->style->remoteIdentifier($collection->name); - - foreach ($collection->compositions as $comp => $spec) { - $compCols = []; - foreach ($spec as $key) { - $dbKey = $this->style->realProperty($key); - if (!isset($cols[$dbKey])) { - continue; - } - - $compCols[$dbKey] = $cols[$dbKey]; - unset($cols[$dbKey]); - } - - if ($parentPkValue === null || empty($compCols)) { - continue; - } - - $this->db - ->update($comp) - ->set($compCols) - ->where([[$fkToParent, '=', $parentPkValue]]) - ->exec(); - } - - return $cols; - } - - private function insertCompositionChildren(Collection $collection, object|null $entity): void - { - if (!$collection instanceof Composite || $entity === null) { - return; - } - - $parentPk = $this->style->identifier($collection->name); - $parentPkValue = $this->entityFactory->get($entity, $parentPk); - - if ($parentPkValue === null) { - return; - } - - $fkToParent = $this->style->remoteIdentifier($collection->name); - $entityCols = $this->entityFactory->extractColumns($entity); - - foreach ($collection->compositions as $comp => $spec) { - $compCols = []; - foreach ($spec as $key) { - $dbKey = $this->style->realProperty($key); - if (!isset($entityCols[$key])) { - continue; - } - - $compCols[$dbKey] = $entityCols[$key]; - } - - if (empty($compCols)) { - continue; - } - - $compCols[$fkToParent] = $parentPkValue; - $this->db - ->insertInto($comp, array_keys($compCols)) - ->values(array_values($compCols)) - ->exec(); - } - } - /** * @param array $columns * @@ -271,7 +182,6 @@ private function rawDelete( /** @param array $columns */ private function rawUpdate(array $columns, Collection $collection): bool { - $columns = $this->extractAndOperateCompositions($collection, $columns); $condition = $this->guessCondition($columns, $collection); return $this->db @@ -287,8 +197,7 @@ private function rawInsert( Collection $collection, object|null $entity = null, ): bool { - $columns = $this->extractAndOperateCompositions($collection, $columns); - $result = $this->db + $result = $this->db ->insertInto($collection->name, array_keys($columns)) ->values(array_values($columns)) ->exec(); @@ -297,8 +206,6 @@ private function rawInsert( $this->checkNewIdentity($entity, $collection); } - $this->insertCompositionChildren($collection, $entity); - return $result; } @@ -352,18 +259,6 @@ private function buildSelectStatement(Sql $sql, array $collections): Sql foreach ($this->entityFactory->enumerateFields($c->name) as $dbCol => $styledProp) { $selectTable[] = self::aliasedColumn($tableSpecifier, $dbCol, $styledProp); } - - // Composition columns come after entity columns so they override on collision - if (!$c instanceof Composite) { - continue; - } - - foreach ($c->compositions as $composition => $columns) { - $compPrefix = $tableSpecifier . Composite::COMPOSITION_MARKER . $composition; - foreach ($columns as $col) { - $selectTable[] = self::aliasedColumn($compPrefix, $col, $col); - } - } } return $sql->select(...$selectTable); @@ -418,23 +313,6 @@ private function parseConditions(array &$conditions, Collection $collection, str return $parsedConditions; } - private function parseCompositions(Sql $sql, Collection $collection, string $entity): void - { - if (!$collection instanceof Composite) { - return; - } - - foreach (array_keys($collection->compositions) as $comp) { - $alias = $entity . Composite::COMPOSITION_MARKER . $comp; - $sql->innerJoin($comp); - $sql->as($alias); - $sql->on([ - $alias . '.' . $this->style->remoteIdentifier($entity) - => $entity . '.' . $this->style->identifier($entity), - ]); - } - } - /** * @param array $aliases * @param array $conditions @@ -468,7 +346,6 @@ private function parseCollection( //No parent collection means it's the first table in the query if ($parentAlias === null) { $sql->from($entity); - $this->parseCompositions($sql, $collection, $entity); return; } @@ -479,8 +356,6 @@ private function parseCollection( $sql->leftJoin($entity); } - $this->parseCompositions($sql, $collection, $entity); - if ($alias !== $entity) { $sql->as($alias); } diff --git a/tests/MapperTest.php b/tests/MapperTest.php index d391e3c..9cfaa2e 100644 --- a/tests/MapperTest.php +++ b/tests/MapperTest.php @@ -12,9 +12,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use ReflectionProperty; -use Respect\Data\Collections\Collection; -use Respect\Data\Collections\Composite; -use Respect\Data\Collections\Typed; use Respect\Data\EntityFactory; use Respect\Data\Hydrators\PrestyledAssoc; use Respect\Data\Styles; @@ -49,9 +46,6 @@ class MapperTest extends TestCase /** @var list> */ protected array $postsCategories; - /** @var list> */ - protected array $issues; - protected function setUp(): void { $conn = new PDO('sqlite::memory:'); @@ -82,11 +76,6 @@ protected function setUp(): void 'post_id INTEGER', 'category_id INTEGER', ])); - $conn->exec((string) Sql::createTable('issues', [ - 'id INTEGER PRIMARY KEY', - 'type VARCHAR(255)', - 'title VARCHAR(22)', - ])); $conn->exec((string) Sql::createTable('read_only_author', [ 'id INTEGER PRIMARY KEY', 'name VARCHAR(255)', @@ -109,11 +98,6 @@ protected function setUp(): void $this->postsCategories = [ ['id' => 66, 'post_id' => 5, 'category_id' => 2], ]; - $this->issues = [ - ['id' => 1, 'type' => 'bug', 'title' => 'Bug 1'], - ['id' => 2, 'type' => 'improvement', 'title' => 'Improvement 1'], - ]; - foreach ($this->authors as $row) { $db->insertInto('author', array_keys($row))->values(array_values($row))->exec(); } @@ -134,10 +118,6 @@ protected function setUp(): void $db->insertInto('post_category', array_keys($row))->values(array_values($row))->exec(); } - foreach ($this->issues as $row) { - $db->insertInto('issues', array_keys($row))->values(array_values($row))->exec(); - } - $db->insertInto('read_only_author', ['id', 'name', 'bio']) ->values([1, 'Alice', 'Alice bio']) ->exec(); @@ -618,149 +598,6 @@ public function testStyle(): void } } - public function testCompositesBringResultsFromTwoTables(): void - { - $mapper = $this->mapper; - $mapper->registerCollection('postComment', Composite::post( - ['comment' => ['text']], - with: [Collection::author()], - )); - $post = $mapper->fetch($mapper->postComment()); - $this->assertEquals(1, $post->author->id); - $this->assertEquals('Author 1', $post->author->name); - $this->assertEquals(5, $post->id); - $this->assertEquals('Post Title', $post->title); - $this->assertEquals('Comment Text', $post->text); - } - - public function testCompositesPersistsResultsOnTwoTables(): void - { - $mapper = $this->mapper; - $mapper->registerCollection('postComment', Composite::post( - ['comment' => ['text']], - with: [Collection::author()], - )); - $post = $mapper->fetch($mapper->postComment()); - $this->assertEquals(1, $post->author->id); - $this->assertEquals(5, $post->id); - $this->assertEquals('Post Title', $post->title); - $this->assertEquals('Comment Text', $post->text); - $post->title = 'Title Changed'; - $post->text = 'Comment Changed'; - - $mapper->persist($post, $mapper->postComment()); - $mapper->flush(); - $result = $this->query('select title from post where id=5') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Title Changed', $result->title); - $result = $this->query('select text from comment where id=7') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Comment Changed', $result->text); - } - - public function testCompositesPersistsNewlyCreatedEntitiesOnTwoTables(): void - { - $mapper = $this->mapper; - $mapper->registerCollection('postComment', Composite::post( - ['comment' => ['text']], - with: [Collection::author()], - )); - $post = new Post(); - $post->text = 'Comment X'; - $post->title = 'Post X'; - $authorX = new Author(); - $authorX->name = 'Author X'; - $post->author = $authorX; - $mapper->persist($post, $mapper->postComment()); - $mapper->flush(); - $result = $this->query( - 'select title, text from post order by id desc', - )->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Post X', $result->title); - $this->assertEquals('', $result->text); - $result = $this->query( - 'select text from comment order by id desc', - )->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Comment X', $result->text); - } - - public function testCompositesPersistDoesNotDropColumnsWithMatchingValues(): void - { - $mapper = $this->mapper; - $mapper->registerCollection('postComment', Composite::post( - ['comment' => ['text']], - with: [Collection::author()], - )); - $post = $mapper->fetch($mapper->postComment()); - $post->title = 'Same Value'; - $post->text = 'Same Value'; - - $mapper->persist($post, $mapper->postComment()); - $mapper->flush(); - $result = $this->query('select title from post where id=5') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Same Value', $result->title); - $result = $this->query('select text from comment where id=7') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Same Value', $result->text); - } - - public function testCompositeColumnOverridesParentOnNameCollision(): void - { - $mapper = $this->mapper; - $mapper->registerCollection('postComment', Composite::post( - ['comment' => ['text']], - with: [Collection::author()], - )); - $post = $mapper->fetch($mapper->postComment()); - - // Both post and comment have a 'text' column. - // The composite column (comment.text) should take precedence. - $this->assertEquals('Comment Text', $post->text); - $this->assertNotEquals('Post Text', $post->text); - } - - public function testTyped(): void - { - $mapper = new Mapper($this->conn, new PrestyledAssoc(new EntityFactory( - entityNamespace: '\Respect\Relational\\', - ))); - $mapper->registerCollection('typedIssues', Typed::issues('type')); - $issues = $mapper->fetchAll($mapper->typedIssues()); - $this->assertInstanceOf('\\Respect\Relational\\Bug', $issues[0]); - $this->assertInstanceOf('\\Respect\Relational\\Improvement', $issues[1]); - $this->assertEquals(1, $issues[0]->id); - $this->assertEquals('bug', $issues[0]->type); - $this->assertEquals('Bug 1', $issues[0]->title); - $this->assertEquals(2, $issues[1]->id); - $this->assertEquals('improvement', $issues[1]->type); - $issues[0]->title = 'Title Changed'; - $mapper->persist($issues[0], $mapper->typedIssues()); - $mapper->flush(); - $result = $this->query('select title from issues where id=1') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Title Changed', $result->title); - } - - public function testTypedSingle(): void - { - $mapper = new Mapper($this->conn, new PrestyledAssoc(new EntityFactory( - entityNamespace: '\Respect\Relational\\', - ))); - $mapper->registerCollection('typedIssues', Typed::issues('type')); - $issue = $mapper->fetch($mapper->typedIssues()); - $this->assertInstanceOf('\\Respect\Relational\\Bug', $issue); - $this->assertEquals(1, $issue->id); - $this->assertEquals('bug', $issue->type); - $this->assertEquals('Bug 1', $issue->title); - $issue->title = 'Title Changed'; - $mapper->persist($issue, $mapper->typedIssues()); - $mapper->flush(); - $result = $this->query('select title from issues where id=1') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Title Changed', $result->title); - } - public function testPersistNewWithArrayobject(): void { $mapper = $this->mapper; @@ -1074,58 +911,6 @@ public function testPersistWithUninitializedRelationSkipsCascade(): void $this->assertEquals('No Author', $result->title); } - public function testCompositeUpdateSkipsMissingSpecColumn(): void - { - // Composite spec asks for 'text' from comment, but we only change - // 'title' (a post column). The composite should not crash on the - // missing spec column — it should just skip it. - $mapper = $this->mapper; - $mapper->registerCollection('postComment', Composite::post( - ['comment' => ['text']], - with: [Collection::author()], - )); - $post = $mapper->fetch($mapper->postComment()); - - // Only change a parent column, leave composite column unchanged - $post->title = 'Only Title Changed'; - - $mapper->persist($post, $mapper->postComment()); - $mapper->flush(); - - $result = $this->query('select title from post where id=5') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Only Title Changed', $result->title); - // Comment text should remain untouched - $result = $this->query('select text from comment where id=7') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Comment Text', $result->text); - } - - public function testCompositeInsertWithNoMatchingColumnsSkipsChild(): void - { - // New entity where the composite spec columns are NOT set — the - // child INSERT should be skipped entirely (no empty INSERT). - $mapper = $this->mapper; - $mapper->registerCollection('postComment', Composite::post( - ['comment' => ['text']], - with: [Collection::author()], - )); - - $post = new Postcomment(); - $post->title = 'Post Without Comment'; - $author = new Author(); - $author->name = 'Author X'; - $post->author = $author; - // Note: $post->text is NOT set (uninitialized) - - $mapper->persist($post, $mapper->postComment()); - $mapper->flush(); - - $result = $this->query('select title from post order by id desc') - ->fetch(PDO::FETCH_OBJ); - $this->assertEquals('Post Without Comment', $result->title); - } - public function testFetchWithArrayConditions(): void { // Test multiple array conditions (hits the AND branch in parseConditions) diff --git a/tests/Stubs/Bug.php b/tests/Stubs/Bug.php deleted file mode 100644 index de0f984..0000000 --- a/tests/Stubs/Bug.php +++ /dev/null @@ -1,14 +0,0 @@ -