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));
+ }
+}