From 5296b7a21bf81538652f8da2368f8308fa6a18d9 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Sun, 15 Mar 2026 02:39:11 -0300 Subject: [PATCH] Complete AbstractMapper abstraction, add style tests Remove SQL-specific internals from AbstractMapper: make fetch() and fetchAll() abstract, remove createStatement(), fetchHydrated(), and parseHydrated() which encoded PDO/hydration patterns that don't belong in a generic data layer. The only abstract methods are now flush(), fetch(), and fetchAll(). Add comprehensive style tests: 6 pure naming-convention test files (AbstractStyle, Standard, CakePHP, NorthWind, Plural, Sakila) covering table/entity, column/property, foreign key, and composition naming for all styles. Add InMemoryMapper test double that exercises the full AbstractMapper contract and all 7 Stylable methods using arrays instead of SQL. Add 4 integration test files with entity stubs that verify typed fetch, nested FK resolution, and persist/flush round-trips through the in-memory backend. --- phpcs.xml.dist | 5 + phpstan.neon.dist | 2 +- src/AbstractMapper.php | 48 +--- tests/AbstractMapperTest.php | 10 +- tests/InMemoryMapper.php | 248 ++++++++++++++++++ tests/Styles/AbstractStyleTest.php | 117 +++++++++ tests/Styles/CakePHP/Author.php | 12 + .../Styles/CakePHP/CakePHPIntegrationTest.php | 99 +++++++ tests/Styles/CakePHP/Category.php | 14 + tests/Styles/CakePHP/Comment.php | 14 + tests/Styles/CakePHP/Post.php | 16 ++ tests/Styles/CakePHP/PostCategory.php | 14 + tests/Styles/CakePHPTest.php | 96 +++++++ tests/Styles/NorthWind/Authors.php | 12 + tests/Styles/NorthWind/Categories.php | 14 + tests/Styles/NorthWind/Comments.php | 14 + .../NorthWind/NorthWindIntegrationTest.php | 99 +++++++ tests/Styles/NorthWind/PostCategories.php | 14 + tests/Styles/NorthWind/Posts.php | 16 ++ tests/Styles/NorthWindTest.php | 96 +++++++ tests/Styles/Plural/Author.php | 12 + tests/Styles/Plural/Category.php | 12 + tests/Styles/Plural/Comment.php | 14 + tests/Styles/Plural/PluralIntegrationTest.php | 99 +++++++ tests/Styles/Plural/Post.php | 16 ++ tests/Styles/Plural/PostCategory.php | 14 + tests/Styles/PluralTest.php | 96 +++++++ tests/Styles/Sakila/Author.php | 12 + tests/Styles/Sakila/Category.php | 16 ++ tests/Styles/Sakila/Comment.php | 14 + tests/Styles/Sakila/Post.php | 16 ++ tests/Styles/Sakila/PostCategory.php | 14 + tests/Styles/Sakila/SakilaIntegrationTest.php | 99 +++++++ tests/Styles/SakilaTest.php | 96 +++++++ tests/Styles/StandardTest.php | 96 +++++++ 35 files changed, 1540 insertions(+), 46 deletions(-) create mode 100644 tests/InMemoryMapper.php create mode 100644 tests/Styles/AbstractStyleTest.php create mode 100644 tests/Styles/CakePHP/Author.php create mode 100644 tests/Styles/CakePHP/CakePHPIntegrationTest.php create mode 100644 tests/Styles/CakePHP/Category.php create mode 100644 tests/Styles/CakePHP/Comment.php create mode 100644 tests/Styles/CakePHP/Post.php create mode 100644 tests/Styles/CakePHP/PostCategory.php create mode 100644 tests/Styles/CakePHPTest.php create mode 100644 tests/Styles/NorthWind/Authors.php create mode 100644 tests/Styles/NorthWind/Categories.php create mode 100644 tests/Styles/NorthWind/Comments.php create mode 100644 tests/Styles/NorthWind/NorthWindIntegrationTest.php create mode 100644 tests/Styles/NorthWind/PostCategories.php create mode 100644 tests/Styles/NorthWind/Posts.php create mode 100644 tests/Styles/NorthWindTest.php create mode 100644 tests/Styles/Plural/Author.php create mode 100644 tests/Styles/Plural/Category.php create mode 100644 tests/Styles/Plural/Comment.php create mode 100644 tests/Styles/Plural/PluralIntegrationTest.php create mode 100644 tests/Styles/Plural/Post.php create mode 100644 tests/Styles/Plural/PostCategory.php create mode 100644 tests/Styles/PluralTest.php create mode 100644 tests/Styles/Sakila/Author.php create mode 100644 tests/Styles/Sakila/Category.php create mode 100644 tests/Styles/Sakila/Comment.php create mode 100644 tests/Styles/Sakila/Post.php create mode 100644 tests/Styles/Sakila/PostCategory.php create mode 100644 tests/Styles/Sakila/SakilaIntegrationTest.php create mode 100644 tests/Styles/SakilaTest.php create mode 100644 tests/Styles/StandardTest.php diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 73c9491..8ba42d7 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -17,4 +17,9 @@ + + + + tests/ + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index ab92ff1..25a1e80 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -5,7 +5,7 @@ parameters: - tests/ ignoreErrors: - message: '/Call to an undefined (static )?method Respect\\Data\\(AbstractMapper|Collections\\(Collection|Filtered|Mix|Typed))::\w+\(\)\./' - - message: '/Access to an undefined property Respect\\Data\\(AbstractMapper|Collections\\Collection)::\$\w+\./' + - message: '/Access to an undefined property Respect\\Data\\(AbstractMapper|InMemoryMapper|Collections\\Collection)::\$\w+\./' - message: '/Unsafe usage of new static\(\)\./' - message: '/Expression .+ on a separate line does not do anything\./' diff --git a/src/AbstractMapper.php b/src/AbstractMapper.php index 22d6ee2..923fc49 100644 --- a/src/AbstractMapper.php +++ b/src/AbstractMapper.php @@ -48,6 +48,11 @@ public function setStyle(Styles\Stylable $style): static abstract public function flush(): void; + abstract public function fetch(Collection $collection, mixed $extra = null): mixed; + + /** @return array */ + abstract public function fetchAll(Collection $collection, mixed $extra = null): array; + public function reset(): void { $this->changed = new SplObjectStorage(); @@ -62,30 +67,6 @@ public function markTracked(object $entity, Collection $collection): bool return true; } - public function fetch(Collection $fromCollection, mixed $withExtra = null): mixed - { - $statement = $this->createStatement($fromCollection, $withExtra); - $hydrated = $this->fetchHydrated($fromCollection, $statement); - if (!$hydrated) { - return false; - } - - return $this->parseHydrated($hydrated); - } - - /** @return array */ - public function fetchAll(Collection $fromCollection, mixed $withExtra = null): array - { - $statement = $this->createStatement($fromCollection, $withExtra); - $entities = []; - - while ($hydrated = $this->fetchHydrated($fromCollection, $statement)) { - $entities[] = $this->parseHydrated($hydrated); - } - - return $entities; - } - public function persist(object $object, Collection $onCollection): bool { $this->changed[$object] = true; @@ -125,25 +106,6 @@ public function registerCollection(string $alias, Collection $collection): void $this->collections[$alias] = $collection; } - abstract protected function createStatement(Collection $fromCollection, mixed $withExtra = null): mixed; - - protected function parseHydrated(SplObjectStorage $hydrated): mixed - { - $this->tracked->addAll($hydrated); - $hydrated->rewind(); - - return $hydrated->current(); - } - - protected function fetchHydrated(Collection $collection, mixed $statement): SplObjectStorage|false - { - if (!$collection->hasMore()) { - return $this->fetchSingle($collection, $statement); - } - - return $this->fetchMulti($collection, $statement); - } - public function __get(string $name): Collection { if (isset($this->collections[$name])) { diff --git a/tests/AbstractMapperTest.php b/tests/AbstractMapperTest.php index d05b37d..eded59d 100644 --- a/tests/AbstractMapperTest.php +++ b/tests/AbstractMapperTest.php @@ -22,9 +22,15 @@ public function flush(): void { } - protected function createStatement(Collection $fromCollection, mixed $withExtra = null): mixed + public function fetch(Collection $collection, mixed $extra = null): mixed { - return null; + return false; + } + + /** @return array */ + public function fetchAll(Collection $collection, mixed $extra = null): array + { + return []; } }; } diff --git a/tests/InMemoryMapper.php b/tests/InMemoryMapper.php new file mode 100644 index 0000000..e777032 --- /dev/null +++ b/tests/InMemoryMapper.php @@ -0,0 +1,248 @@ +>> */ + private array $tables = []; + + private int $lastInsertId = 1000; + + public string $entityNamespace = '\\'; + + /** @param list> $rows */ + public function seed(string $table, array $rows): void + { + $this->tables[$table] = $rows; + } + + public function fetch(Collection $collection, mixed $extra = null): mixed + { + $name = (string) $collection->getName(); + $rows = $this->tables[$name] ?? []; + $condition = $collection->getCondition(); + $style = $this->getStyle(); + + if ($condition !== null && $condition !== []) { + $pk = $style->identifier($name); + $pkValue = is_array($condition) ? reset($condition) : $condition; + $rows = array_values(array_filter( + $rows, + static fn(array $row): bool => isset($row[$pk]) && $row[$pk] == $pkValue, + )); + } + + if ($rows === []) { + return false; + } + + $row = $rows[0]; + $entity = $this->createEntity($name); + + foreach ($row as $key => $value) { + $entity->{$key} = $value; + } + + if ($collection->hasMore()) { + $this->resolveRelations($entity, $collection); + } + + $this->markTracked($entity, $collection); + + return $entity; + } + + /** @return array */ + public function fetchAll(Collection $collection, mixed $extra = null): array + { + $name = (string) $collection->getName(); + $rows = $this->tables[$name] ?? []; + $condition = $collection->getCondition(); + $style = $this->getStyle(); + + if ($condition !== null && $condition !== []) { + $pk = $style->identifier($name); + $pkValue = is_array($condition) ? reset($condition) : $condition; + $rows = array_values(array_filter( + $rows, + static fn(array $row): bool => isset($row[$pk]) && $row[$pk] == $pkValue, + )); + } + + $entities = []; + + foreach ($rows as $row) { + $entity = $this->createEntity($name); + + foreach ($row as $key => $value) { + $entity->{$key} = $value; + } + + if ($collection->hasMore()) { + $this->resolveRelations($entity, $collection); + } + + $this->markTracked($entity, $collection); + $entities[] = $entity; + } + + return $entities; + } + + public function flush(): void + { + foreach ($this->new as $entity) { + $collection = $this->tracked[$entity]; + assert($collection instanceof Collection); + $tableName = (string) $collection->getName(); + $pk = $this->getStyle()->identifier($tableName); + $row = get_object_vars($entity); + + if (!isset($row[$pk])) { + ++$this->lastInsertId; + $entity->{$pk} = $this->lastInsertId; + $row[$pk] = $this->lastInsertId; + } + + $this->tables[$tableName][] = $row; + } + + foreach ($this->changed as $entity) { + if ($this->new->offsetExists($entity)) { + continue; + } + + if ($this->removed->offsetExists($entity)) { + continue; + } + + $collection = $this->tracked[$entity]; + assert($collection instanceof Collection); + $tableName = (string) $collection->getName(); + $pk = $this->getStyle()->identifier($tableName); + $pkValue = $entity->{$pk}; + $row = get_object_vars($entity); + + foreach ($this->tables[$tableName] as $index => $existing) { + if (isset($existing[$pk]) && $existing[$pk] == $pkValue) { + $this->tables[$tableName][$index] = $row; + + break; + } + } + } + + foreach ($this->removed as $entity) { + $collection = $this->tracked[$entity]; + assert($collection instanceof Collection); + $tableName = (string) $collection->getName(); + $pk = $this->getStyle()->identifier($tableName); + $pkValue = $entity->{$pk}; + + foreach ($this->tables[$tableName] as $index => $existing) { + if (isset($existing[$pk]) && $existing[$pk] == $pkValue) { + unset($this->tables[$tableName][$index]); + $this->tables[$tableName] = array_values($this->tables[$tableName]); + + break; + } + } + } + + $this->reset(); + } + + private function resolveRelations(object $entity, Collection $collection): void + { + $style = $this->getStyle(); + $next = $collection->getNext(); + + if ($next !== null) { + $nextName = (string) $next->getName(); + $fkCol = $style->remoteIdentifier($nextName); + + if (array_key_exists($fkCol, get_object_vars($entity))) { + $fkValue = $entity->{$fkCol}; + $childEntity = $this->findRelatedEntity($nextName, $fkValue, $next); + + if ($childEntity !== null) { + $entity->{$fkCol} = $childEntity; + } + } + } + + foreach ($collection->getChildren() as $child) { + $childName = (string) $child->getName(); + $fkCol = $style->remoteIdentifier($childName); + + if (!array_key_exists($fkCol, get_object_vars($entity))) { + continue; + } + + $fkValue = $entity->{$fkCol}; + $childEntity = $this->findRelatedEntity($childName, $fkValue, $child); + + if ($childEntity === null) { + continue; + } + + $entity->{$fkCol} = $childEntity; + } + } + + private function findRelatedEntity(string $tableName, mixed $fkValue, Collection $collection): object|null + { + $style = $this->getStyle(); + $pk = $style->identifier($tableName); + $rows = $this->tables[$tableName] ?? []; + + foreach ($rows as $row) { + if (!isset($row[$pk]) || $row[$pk] != $fkValue) { + continue; + } + + $childEntity = $this->createEntity($tableName); + + foreach ($row as $key => $value) { + $childEntity->{$key} = $value; + } + + if ($collection->hasMore()) { + $this->resolveRelations($childEntity, $collection); + } + + $this->markTracked($childEntity, $collection); + + return $childEntity; + } + + return null; + } + + private function createEntity(string $entityName): object + { + $className = $this->getStyle()->styledName($entityName); + $fullClass = $this->entityNamespace . $className; + + if (class_exists($fullClass)) { + return new $fullClass(); + } + + return new stdClass(); + } +} diff --git a/tests/Styles/AbstractStyleTest.php b/tests/Styles/AbstractStyleTest.php new file mode 100644 index 0000000..d45c541 --- /dev/null +++ b/tests/Styles/AbstractStyleTest.php @@ -0,0 +1,117 @@ +style = new class extends AbstractStyle { + public function styledProperty(string $name): string + { + return $name; + } + + public function realName(string $name): string + { + return $name; + } + + public function realProperty(string $name): string + { + return $name; + } + + public function styledName(string $name): string + { + return $name; + } + + public function identifier(string $name): string + { + return 'id'; + } + + public function remoteIdentifier(string $name): string + { + return $name . '_id'; + } + + public function composed(string $left, string $right): string + { + return $left . '_' . $right; + } + + public function isRemoteIdentifier(string $name): bool + { + return false; + } + + public function remoteFromIdentifier(string $name): string|null + { + return null; + } + }; + } + + /** @return array */ + public static function singularPluralProvider(): array + { + return [ + ['post', 'posts'], + ['comment', 'comments'], + ['category', 'categories'], + ['tag', 'tags'], + ['entity', 'entities'], + ]; + } + + /** @return array */ + public static function camelCaseToSeparatorProvider(): array + { + return [ + ['-', 'HenriqueMoody', 'Henrique-Moody'], + [' ', 'AlexandreGaigalas', 'Alexandre Gaigalas'], + ['_', 'AugustoPascutti', 'Augusto_Pascutti'], + ]; + } + + #[DataProvider('singularPluralProvider')] + public function testPluralToSingularAndViceVersa(string $singular, string $plural): void + { + $pluralToSingular = new ReflectionMethod($this->style, 'pluralToSingular'); + $this->assertEquals($singular, $pluralToSingular->invoke($this->style, $plural)); + + $singularToPlural = new ReflectionMethod($this->style, 'singularToPlural'); + $this->assertEquals($plural, $singularToPlural->invoke($this->style, $singular)); + } + + #[DataProvider('camelCaseToSeparatorProvider')] + public function testCamelCaseToSeparatorAndViceVersa( + string $separator, + string $camelCase, + string $separated, + ): void { + $camelCaseToSeparatorMethod = new ReflectionMethod($this->style, 'camelCaseToSeparator'); + $this->assertEquals( + $separated, + $camelCaseToSeparatorMethod->invoke($this->style, $camelCase, $separator), + ); + + $separatorToCamelCaseMethod = new ReflectionMethod($this->style, 'separatorToCamelCase'); + $this->assertEquals( + $camelCase, + $separatorToCamelCaseMethod->invoke($this->style, $separated, $separator), + ); + } +} diff --git a/tests/Styles/CakePHP/Author.php b/tests/Styles/CakePHP/Author.php new file mode 100644 index 0000000..9373e4d --- /dev/null +++ b/tests/Styles/CakePHP/Author.php @@ -0,0 +1,12 @@ +style = new CakePHP(); + $this->mapper = new InMemoryMapper(); + $this->mapper->setStyle($this->style); + $this->mapper->entityNamespace = __NAMESPACE__ . '\\'; + + $this->mapper->seed('posts', [ + ['id' => 5, 'title' => 'Post Title', 'text' => 'Post Text', 'author_id' => 1], + ]); + $this->mapper->seed('authors', [ + ['id' => 1, 'name' => 'Author 1'], + ]); + $this->mapper->seed('comments', [ + ['id' => 7, 'post_id' => 5, 'text' => 'Comment Text'], + ['id' => 8, 'post_id' => 4, 'text' => 'Comment Text 2'], + ]); + $this->mapper->seed('categories', [ + ['id' => 2, 'name' => 'Sample Category', 'category_id' => null], + ['id' => 3, 'name' => 'NONON', 'category_id' => null], + ]); + $this->mapper->seed('post_categories', [ + ['id' => 66, 'post_id' => 5, 'category_id' => 2], + ]); + } + + public function testFetchingEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comments[8]->fetch(); + $this->assertInstanceOf(Comment::class, $comment); + } + + public function testFetchingAllEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comments->fetchAll(); + $this->assertInstanceOf(Comment::class, $comment[1]); + + $categories = $mapper->post_categories->categories->fetch(); + $this->assertInstanceOf(PostCategory::class, $categories); + $this->assertInstanceOf(Category::class, $categories->category_id); + } + + public function testFetchingAllEntityTypedNested(): void + { + $mapper = $this->mapper; + $comment = $mapper->comments->posts->authors->fetchAll(); + $this->assertInstanceOf(Comment::class, $comment[0]); + $this->assertInstanceOf(Post::class, $comment[0]->post_id); + $this->assertInstanceOf(Author::class, $comment[0]->post_id->author_id); + } + + public function testPersistingEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comments[8]->fetch(); + $this->assertInstanceOf(Comment::class, $comment); + $comment->text = 'HeyHey'; + $mapper->comments->persist($comment); + $mapper->flush(); + + $updated = $mapper->comments[8]->fetch(); + $this->assertInstanceOf(Comment::class, $updated); + $this->assertEquals('HeyHey', $updated->text); + } + + public function testPersistingNewEntityTyped(): void + { + $mapper = $this->mapper; + $comment = new Comment(); + $comment->text = 'HeyHey'; + $mapper->comments->persist($comment); + $mapper->flush(); + + $this->assertNotNull($comment->id); + $allComments = $mapper->comments->fetchAll(); + $this->assertCount(3, $allComments); + } +} diff --git a/tests/Styles/CakePHP/Category.php b/tests/Styles/CakePHP/Category.php new file mode 100644 index 0000000..1b84156 --- /dev/null +++ b/tests/Styles/CakePHP/Category.php @@ -0,0 +1,14 @@ +style = new CakePHP(); + } + + /** @return array> */ + public static function tableEntityProvider(): array + { + return [ + ['posts', 'Post'], + ['comments', 'Comment'], + ['categories', 'Category'], + ['post_categories', 'PostCategory'], + ['post_tags', 'PostTag'], + ]; + } + + /** @return array> */ + public static function manyToManyTableProvider(): array + { + return [ + ['post', 'category', 'post_categories'], + ['user', 'group', 'user_groups'], + ['group', 'profile', 'group_profiles'], + ]; + } + + /** @return array> */ + public static function columnsPropertyProvider(): array + { + return [ + ['id'], + ['text'], + ['name'], + ['content'], + ['created'], + ]; + } + + /** @return array> */ + public static function foreignProvider(): array + { + return [ + ['posts', 'post_id'], + ['authors', 'author_id'], + ['tags', 'tag_id'], + ['users', 'user_id'], + ]; + } + + #[DataProvider('tableEntityProvider')] + public function testTableAndEntitiesMethods(string $table, string $entity): void + { + $this->assertEquals($entity, $this->style->styledName($table)); + $this->assertEquals($table, $this->style->realName($entity)); + $this->assertEquals('id', $this->style->identifier($table)); + } + + #[DataProvider('columnsPropertyProvider')] + public function testColumnsAndPropertiesMethods(string $column): void + { + $this->assertEquals($column, $this->style->styledProperty($column)); + $this->assertEquals($column, $this->style->realProperty($column)); + $this->assertFalse($this->style->isRemoteIdentifier($column)); + $this->assertNull($this->style->remoteFromIdentifier($column)); + } + + #[DataProvider('manyToManyTableProvider')] + public function testTableFromLeftRightTable(string $left, string $right, string $table): void + { + $this->assertEquals($table, $this->style->composed($left, $right)); + } + + #[DataProvider('foreignProvider')] + public function testForeign(string $table, string $foreign): void + { + $this->assertTrue($this->style->isRemoteIdentifier($foreign)); + $this->assertEquals($table, $this->style->remoteFromIdentifier($foreign)); + $this->assertEquals($foreign, $this->style->remoteIdentifier($table)); + } +} diff --git a/tests/Styles/NorthWind/Authors.php b/tests/Styles/NorthWind/Authors.php new file mode 100644 index 0000000..24da07d --- /dev/null +++ b/tests/Styles/NorthWind/Authors.php @@ -0,0 +1,12 @@ +style = new NorthWind(); + $this->mapper = new InMemoryMapper(); + $this->mapper->setStyle($this->style); + $this->mapper->entityNamespace = __NAMESPACE__ . '\\'; + + $this->mapper->seed('Posts', [ + ['PostID' => 5, 'Title' => 'Post Title', 'Text' => 'Post Text', 'AuthorID' => 1], + ]); + $this->mapper->seed('Authors', [ + ['AuthorID' => 1, 'Name' => 'Author 1'], + ]); + $this->mapper->seed('Comments', [ + ['CommentID' => 7, 'PostID' => 5, 'Text' => 'Comment Text'], + ['CommentID' => 8, 'PostID' => 4, 'Text' => 'Comment Text 2'], + ]); + $this->mapper->seed('Categories', [ + ['CategoryID' => 2, 'Name' => 'Sample Category', 'Description' => 'Category description'], + ['CategoryID' => 3, 'Name' => 'NONON', 'Description' => null], + ]); + $this->mapper->seed('PostCategories', [ + ['PostCategoryID' => 66, 'PostID' => 5, 'CategoryID' => 2], + ]); + } + + public function testFetchingEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->Comments[8]->fetch(); + $this->assertInstanceOf(Comments::class, $comment); + } + + public function testFetchingAllEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->Comments->fetchAll(); + $this->assertInstanceOf(Comments::class, $comment[1]); + + $categories = $mapper->PostCategories->Categories->fetch(); + $this->assertInstanceOf(PostCategories::class, $categories); + $this->assertInstanceOf(Categories::class, $categories->CategoryID); + } + + public function testFetchingAllEntityTypedNested(): void + { + $mapper = $this->mapper; + $comment = $mapper->Comments->Posts->Authors->fetchAll(); + $this->assertInstanceOf(Comments::class, $comment[0]); + $this->assertInstanceOf(Posts::class, $comment[0]->PostID); + $this->assertInstanceOf(Authors::class, $comment[0]->PostID->AuthorID); + } + + public function testPersistingEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->Comments[8]->fetch(); + $this->assertInstanceOf(Comments::class, $comment); + $comment->Text = 'HeyHey'; + $mapper->Comments->persist($comment); + $mapper->flush(); + + $updated = $mapper->Comments[8]->fetch(); + $this->assertInstanceOf(Comments::class, $updated); + $this->assertEquals('HeyHey', $updated->Text); + } + + public function testPersistingNewEntityTyped(): void + { + $mapper = $this->mapper; + $comment = new Comments(); + $comment->Text = 'HeyHey'; + $mapper->Comments->persist($comment); + $mapper->flush(); + + $this->assertNotNull($comment->CommentID); + $allComments = $mapper->Comments->fetchAll(); + $this->assertCount(3, $allComments); + } +} diff --git a/tests/Styles/NorthWind/PostCategories.php b/tests/Styles/NorthWind/PostCategories.php new file mode 100644 index 0000000..87421df --- /dev/null +++ b/tests/Styles/NorthWind/PostCategories.php @@ -0,0 +1,14 @@ +style = new NorthWind(); + } + + /** @return array> */ + public static function tableEntityProvider(): array + { + return [ + ['Posts', 'Posts'], + ['Comments', 'Comments'], + ['Categories', 'Categories'], + ['PostCategories', 'PostCategories'], + ['PostTags', 'PostTags'], + ]; + } + + /** @return array> */ + public static function manyToManyTableProvider(): array + { + return [ + ['Posts', 'Categories', 'PostCategories'], + ['Users', 'Groups', 'UserGroups'], + ['Groups', 'Profiles', 'GroupProfiles'], + ]; + } + + /** @return array> */ + public static function columnsPropertyProvider(): array + { + return [ + ['Text'], + ['Name'], + ['Content'], + ['Created'], + ['Updated'], + ]; + } + + /** @return array> */ + public static function keyProvider(): array + { + return [ + ['Posts', 'PostID'], + ['Authors', 'AuthorID'], + ['Tags', 'TagID'], + ['Users', 'UserID'], + ]; + } + + #[DataProvider('tableEntityProvider')] + public function testTableAndEntitiesMethods(string $table, string $entity): void + { + $this->assertEquals($entity, $this->style->styledName($table)); + $this->assertEquals($table, $this->style->realName($entity)); + } + + #[DataProvider('columnsPropertyProvider')] + public function testColumnsAndPropertiesMethods(string $column): void + { + $this->assertEquals($column, $this->style->styledProperty($column)); + $this->assertEquals($column, $this->style->realProperty($column)); + $this->assertFalse($this->style->isRemoteIdentifier($column)); + $this->assertNull($this->style->remoteFromIdentifier($column)); + } + + #[DataProvider('manyToManyTableProvider')] + public function testTableFromLeftRightTable(string $left, string $right, string $table): void + { + $this->assertEquals($table, $this->style->composed($left, $right)); + } + + #[DataProvider('keyProvider')] + public function testKeys(string $table, string $foreign): void + { + $this->assertTrue($this->style->isRemoteIdentifier($foreign)); + $this->assertEquals($table, $this->style->remoteFromIdentifier($foreign)); + $this->assertEquals($foreign, $this->style->identifier($table)); + $this->assertEquals($foreign, $this->style->remoteIdentifier($table)); + } +} diff --git a/tests/Styles/Plural/Author.php b/tests/Styles/Plural/Author.php new file mode 100644 index 0000000..cd62e74 --- /dev/null +++ b/tests/Styles/Plural/Author.php @@ -0,0 +1,12 @@ +style = new Plural(); + $this->mapper = new InMemoryMapper(); + $this->mapper->setStyle($this->style); + $this->mapper->entityNamespace = __NAMESPACE__ . '\\'; + + $this->mapper->seed('posts', [ + ['id' => 5, 'title' => 'Post Title', 'text' => 'Post Text', 'author_id' => 1], + ]); + $this->mapper->seed('authors', [ + ['id' => 1, 'name' => 'Author 1'], + ]); + $this->mapper->seed('comments', [ + ['id' => 7, 'post_id' => 5, 'text' => 'Comment Text'], + ['id' => 8, 'post_id' => 4, 'text' => 'Comment Text 2'], + ]); + $this->mapper->seed('categories', [ + ['id' => 2, 'name' => 'Sample Category'], + ['id' => 3, 'name' => 'NONON'], + ]); + $this->mapper->seed('posts_categories', [ + ['id' => 66, 'post_id' => 5, 'category_id' => 2], + ]); + } + + public function testFetchingEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comments[8]->fetch(); + $this->assertInstanceOf(Comment::class, $comment); + } + + public function testFetchingAllEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comments->fetchAll(); + $this->assertInstanceOf(Comment::class, $comment[1]); + + $categories = $mapper->posts_categories->categories->fetch(); + $this->assertInstanceOf(PostCategory::class, $categories); + $this->assertInstanceOf(Category::class, $categories->category_id); + } + + public function testFetchingAllEntityTypedNested(): void + { + $mapper = $this->mapper; + $comment = $mapper->comments->posts->authors->fetchAll(); + $this->assertInstanceOf(Comment::class, $comment[0]); + $this->assertInstanceOf(Post::class, $comment[0]->post_id); + $this->assertInstanceOf(Author::class, $comment[0]->post_id->author_id); + } + + public function testPersistingEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comments[8]->fetch(); + $this->assertInstanceOf(Comment::class, $comment); + $comment->text = 'HeyHey'; + $mapper->comments->persist($comment); + $mapper->flush(); + + $updated = $mapper->comments[8]->fetch(); + $this->assertInstanceOf(Comment::class, $updated); + $this->assertEquals('HeyHey', $updated->text); + } + + public function testPersistingNewEntityTyped(): void + { + $mapper = $this->mapper; + $comment = new Comment(); + $comment->text = 'HeyHey'; + $mapper->comments->persist($comment); + $mapper->flush(); + + $this->assertNotNull($comment->id); + $allComments = $mapper->comments->fetchAll(); + $this->assertCount(3, $allComments); + } +} diff --git a/tests/Styles/Plural/Post.php b/tests/Styles/Plural/Post.php new file mode 100644 index 0000000..d8789ab --- /dev/null +++ b/tests/Styles/Plural/Post.php @@ -0,0 +1,16 @@ +style = new Plural(); + } + + /** @return array> */ + public static function tableEntityProvider(): array + { + return [ + ['posts', 'Post'], + ['comments', 'Comment'], + ['categories', 'Category'], + ['posts_categories', 'PostCategory'], + ['posts_tags', 'PostTag'], + ]; + } + + /** @return array> */ + public static function manyToManyTableProvider(): array + { + return [ + ['post', 'category', 'posts_categories'], + ['user', 'group', 'users_groups'], + ['group', 'profile', 'groups_profiles'], + ]; + } + + /** @return array> */ + public static function columnsPropertyProvider(): array + { + return [ + ['id'], + ['text'], + ['name'], + ['content'], + ['created'], + ]; + } + + /** @return array> */ + public static function foreignProvider(): array + { + return [ + ['posts', 'post_id'], + ['authors', 'author_id'], + ['tags', 'tag_id'], + ['users', 'user_id'], + ]; + } + + #[DataProvider('tableEntityProvider')] + public function testTableAndEntitiesMethods(string $table, string $entity): void + { + $this->assertEquals($entity, $this->style->styledName($table)); + $this->assertEquals($table, $this->style->realName($entity)); + $this->assertEquals('id', $this->style->identifier($table)); + } + + #[DataProvider('columnsPropertyProvider')] + public function testColumnsAndPropertiesMethods(string $column): void + { + $this->assertEquals($column, $this->style->styledProperty($column)); + $this->assertEquals($column, $this->style->realProperty($column)); + $this->assertFalse($this->style->isRemoteIdentifier($column)); + $this->assertNull($this->style->remoteFromIdentifier($column)); + } + + #[DataProvider('manyToManyTableProvider')] + public function testTableFromLeftRightTable(string $left, string $right, string $table): void + { + $this->assertEquals($table, $this->style->composed($left, $right)); + } + + #[DataProvider('foreignProvider')] + public function testForeign(string $table, string $foreign): void + { + $this->assertTrue($this->style->isRemoteIdentifier($foreign)); + $this->assertEquals($table, $this->style->remoteFromIdentifier($foreign)); + $this->assertEquals($foreign, $this->style->remoteIdentifier($table)); + } +} diff --git a/tests/Styles/Sakila/Author.php b/tests/Styles/Sakila/Author.php new file mode 100644 index 0000000..c433467 --- /dev/null +++ b/tests/Styles/Sakila/Author.php @@ -0,0 +1,12 @@ +style = new Sakila(); + $this->mapper = new InMemoryMapper(); + $this->mapper->setStyle($this->style); + $this->mapper->entityNamespace = __NAMESPACE__ . '\\'; + + $this->mapper->seed('post', [ + ['post_id' => 5, 'title' => 'Post Title', 'text' => 'Post Text', 'author_id' => 1], + ]); + $this->mapper->seed('author', [ + ['author_id' => 1, 'name' => 'Author 1'], + ]); + $this->mapper->seed('comment', [ + ['comment_id' => 7, 'post_id' => 5, 'text' => 'Comment Text'], + ['comment_id' => 8, 'post_id' => 4, 'text' => 'Comment Text 2'], + ]); + $this->mapper->seed('category', [ + ['category_id' => 2, 'name' => 'Sample Category', 'content' => null], + ['category_id' => 3, 'name' => 'NONON', 'content' => null], + ]); + $this->mapper->seed('post_category', [ + ['post_category_id' => 66, 'post_id' => 5, 'category_id' => 2], + ]); + } + + public function testFetchingEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comment[8]->fetch(); + $this->assertInstanceOf(Comment::class, $comment); + } + + public function testFetchingAllEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comment->fetchAll(); + $this->assertInstanceOf(Comment::class, $comment[1]); + + $categories = $mapper->post_category->category->fetch(); + $this->assertInstanceOf(PostCategory::class, $categories); + $this->assertInstanceOf(Category::class, $categories->category_id); + } + + public function testFetchingAllEntityTypedNested(): void + { + $mapper = $this->mapper; + $comment = $mapper->comment->post->author->fetchAll(); + $this->assertInstanceOf(Comment::class, $comment[0]); + $this->assertInstanceOf(Post::class, $comment[0]->post_id); + $this->assertInstanceOf(Author::class, $comment[0]->post_id->author_id); + } + + public function testPersistingEntityTyped(): void + { + $mapper = $this->mapper; + $comment = $mapper->comment[8]->fetch(); + $this->assertInstanceOf(Comment::class, $comment); + $comment->text = 'HeyHey'; + $mapper->comment->persist($comment); + $mapper->flush(); + + $updated = $mapper->comment[8]->fetch(); + $this->assertInstanceOf(Comment::class, $updated); + $this->assertEquals('HeyHey', $updated->text); + } + + public function testPersistingNewEntityTyped(): void + { + $mapper = $this->mapper; + $comment = new Comment(); + $comment->text = 'HeyHey'; + $mapper->comment->persist($comment); + $mapper->flush(); + + $this->assertNotNull($comment->comment_id); + $allComments = $mapper->comment->fetchAll(); + $this->assertCount(3, $allComments); + } +} diff --git a/tests/Styles/SakilaTest.php b/tests/Styles/SakilaTest.php new file mode 100644 index 0000000..7df68a9 --- /dev/null +++ b/tests/Styles/SakilaTest.php @@ -0,0 +1,96 @@ +style = new Sakila(); + } + + /** @return array> */ + public static function tableEntityProvider(): array + { + return [ + ['post', 'Post'], + ['comment', 'Comment'], + ['category', 'Category'], + ['post_category', 'PostCategory'], + ['post_tag', 'PostTag'], + ]; + } + + /** @return array> */ + public static function manyToManyTableProvider(): array + { + return [ + ['post', 'category', 'post_category'], + ['user', 'group', 'user_group'], + ['group', 'profile', 'group_profile'], + ]; + } + + /** @return array> */ + public static function columnsPropertyProvider(): array + { + return [ + ['id'], + ['text'], + ['name'], + ['content'], + ['created'], + ]; + } + + /** @return array> */ + public static function keyProvider(): array + { + return [ + ['post', 'post_id'], + ['author', 'author_id'], + ['tag', 'tag_id'], + ['user', 'user_id'], + ]; + } + + #[DataProvider('tableEntityProvider')] + public function testTableAndEntitiesMethods(string $table, string $entity): void + { + $this->assertEquals($entity, $this->style->styledName($table)); + $this->assertEquals($table, $this->style->realName($entity)); + } + + #[DataProvider('columnsPropertyProvider')] + public function testColumnsAndPropertiesMethods(string $column): void + { + $this->assertEquals($column, $this->style->styledProperty($column)); + $this->assertEquals($column, $this->style->realProperty($column)); + $this->assertFalse($this->style->isRemoteIdentifier($column)); + $this->assertNull($this->style->remoteFromIdentifier($column)); + } + + #[DataProvider('manyToManyTableProvider')] + public function testTableFromLeftRightTable(string $left, string $right, string $table): void + { + $this->assertEquals($table, $this->style->composed($left, $right)); + } + + #[DataProvider('keyProvider')] + public function testForeign(string $table, string $key): void + { + $this->assertTrue($this->style->isRemoteIdentifier($key)); + $this->assertEquals($table, $this->style->remoteFromIdentifier($key)); + $this->assertEquals($key, $this->style->identifier($table)); + $this->assertEquals($key, $this->style->remoteIdentifier($table)); + } +} diff --git a/tests/Styles/StandardTest.php b/tests/Styles/StandardTest.php new file mode 100644 index 0000000..2ac49b2 --- /dev/null +++ b/tests/Styles/StandardTest.php @@ -0,0 +1,96 @@ +style = new Standard(); + } + + /** @return array> */ + public static function tableEntityProvider(): array + { + return [ + ['post', 'Post'], + ['comment', 'Comment'], + ['category', 'Category'], + ['post_category', 'PostCategory'], + ['post_tag', 'PostTag'], + ]; + } + + /** @return array> */ + public static function manyToManyTableProvider(): array + { + return [ + ['post', 'category', 'post_category'], + ['user', 'group', 'user_group'], + ['group', 'profile', 'group_profile'], + ]; + } + + /** @return array> */ + public static function columnsPropertyProvider(): array + { + return [ + ['id'], + ['text'], + ['name'], + ['content'], + ['created'], + ]; + } + + /** @return array> */ + public static function foreignProvider(): array + { + return [ + ['post', 'post_id'], + ['author', 'author_id'], + ['tag', 'tag_id'], + ['user', 'user_id'], + ]; + } + + #[DataProvider('tableEntityProvider')] + public function testTableAndEntitiesMethods(string $table, string $entity): void + { + $this->assertEquals($entity, $this->style->styledName($table)); + $this->assertEquals($table, $this->style->realName($entity)); + $this->assertEquals('id', $this->style->identifier($table)); + } + + #[DataProvider('columnsPropertyProvider')] + public function testColumnsAndPropertiesMethods(string $name): void + { + $this->assertEquals($name, $this->style->styledProperty($name)); + $this->assertEquals($name, $this->style->realProperty($name)); + $this->assertFalse($this->style->isRemoteIdentifier($name)); + $this->assertNull($this->style->remoteFromIdentifier($name)); + } + + #[DataProvider('manyToManyTableProvider')] + public function testTableFromLeftRightTable(string $left, string $right, string $table): void + { + $this->assertEquals($table, $this->style->composed($left, $right)); + } + + #[DataProvider('foreignProvider')] + public function testForeign(string $table, string $foreign): void + { + $this->assertTrue($this->style->isRemoteIdentifier($foreign)); + $this->assertEquals($table, $this->style->remoteFromIdentifier($foreign)); + $this->assertEquals($foreign, $this->style->remoteIdentifier($table)); + } +}