From 92ccd4ca526ec151556d93c2856b8f25988e71f7 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Fri, 19 May 2023 15:10:14 +0300 Subject: [PATCH] Fix typecasts list merging; add Defaults registry (#65) --- src/Compiler.php | 42 +++--- src/Defaults.php | 57 ++++++++ src/Registry.php | 9 +- tests/Schema/CompilerTest.php | 48 ++++++- tests/Schema/DefaultsTest.php | 123 ++++++++++++++++++ .../JoinedTableInheritanceTest.php | 2 +- .../SingleTableInheritanceTest.php | 4 +- tests/Schema/EntityTest.php | 4 +- tests/Schema/FieldsTest.php | 4 +- .../Generator/GenerateModifiersTest.php | 2 +- .../Schema/Generator/RenderModifiersTest.php | 2 +- tests/Schema/Generator/TableGeneratorTest.php | 4 +- .../Schema/Relation/Traits/FieldTraitTest.php | 4 +- 13 files changed, 269 insertions(+), 36 deletions(-) create mode 100644 src/Defaults.php create mode 100644 tests/Schema/DefaultsTest.php diff --git a/src/Compiler.php b/src/Compiler.php index db9413f..cc79bbb 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -4,10 +4,7 @@ namespace Cycle\Schema; -use Cycle\ORM\Mapper\Mapper; use Cycle\ORM\SchemaInterface as Schema; -use Cycle\ORM\Select\Repository; -use Cycle\ORM\Select\Source; use Cycle\Schema\Definition\Comparator\FieldComparator; use Cycle\Schema\Definition\Entity; use Cycle\Schema\Definition\Field; @@ -25,15 +22,6 @@ final class Compiler /** @var array> */ private array $result = []; - /** @var array */ - private array $defaults = [ - Schema::MAPPER => Mapper::class, - Schema::REPOSITORY => Repository::class, - Schema::SOURCE => Source::class, - Schema::SCOPE => null, - Schema::TYPECAST_HANDLER => null, - ]; - /** * Compile the registry schema. * @@ -41,7 +29,7 @@ final class Compiler */ public function compile(Registry $registry, array $generators = [], array $defaults = []): array { - $this->defaults = $defaults + $this->defaults; + $registry->getDefaults()->merge($defaults); foreach ($generators as $generator) { if (!$generator instanceof GeneratorInterface) { @@ -79,14 +67,16 @@ public function getSchema(): array */ private function compute(Registry $registry, Entity $entity): void { + $defaults = $registry->getDefaults(); + $schema = [ Schema::ENTITY => $entity->getClass(), - Schema::SOURCE => $entity->getSource() ?? $this->defaults[Schema::SOURCE], - Schema::MAPPER => $entity->getMapper() ?? $this->defaults[Schema::MAPPER], - Schema::REPOSITORY => $entity->getRepository() ?? $this->defaults[Schema::REPOSITORY], - Schema::SCOPE => $entity->getScope() ?? $this->defaults[Schema::SCOPE], + Schema::SOURCE => $entity->getSource() ?? $defaults[Schema::SOURCE], + Schema::MAPPER => $entity->getMapper() ?? $defaults[Schema::MAPPER], + Schema::REPOSITORY => $entity->getRepository() ?? $defaults[Schema::REPOSITORY], + Schema::SCOPE => $entity->getScope() ?? $defaults[Schema::SCOPE], Schema::SCHEMA => $entity->getSchema(), - Schema::TYPECAST_HANDLER => $entity->getTypecast() ?? $this->defaults[Schema::TYPECAST_HANDLER], + Schema::TYPECAST_HANDLER => $this->renderTypecastHandler($registry->getDefaults(), $entity), Schema::PRIMARY_KEY => $entity->getPrimaryFields()->getNames(), Schema::COLUMNS => $this->renderColumns($entity), Schema::FIND_BY_KEYS => $this->renderReferences($entity), @@ -226,4 +216,20 @@ private function renderRelations(Registry $registry, Entity $entity, array &$sch $relation->modifySchema($schema); } } + + private function renderTypecastHandler(Defaults $defaults, Entity $entity): array|null|string + { + $defaults = $defaults[Schema::TYPECAST_HANDLER] ?? []; + if (!\is_array($defaults)) { + $defaults = [$defaults]; + } + + if ($defaults === []) { + return $entity->getTypecast(); + } + + $typecast = $entity->getTypecast() ?? []; + + return \array_values(\array_unique(\array_merge(\is_array($typecast) ? $typecast : [$typecast], $defaults))); + } } diff --git a/src/Defaults.php b/src/Defaults.php new file mode 100644 index 0000000..c58e540 --- /dev/null +++ b/src/Defaults.php @@ -0,0 +1,57 @@ + $defaults + */ + public function __construct( + private array $defaults = [ + SchemaInterface::MAPPER => Mapper::class, + SchemaInterface::REPOSITORY => Repository::class, + SchemaInterface::SOURCE => Source::class, + SchemaInterface::SCOPE => null, + SchemaInterface::TYPECAST_HANDLER => null, + ] + ) { + } + + /** + * @param array $defaults + */ + public function merge(array $defaults): self + { + $this->defaults = $defaults + $this->defaults; + + return $this; + } + + public function offsetExists(mixed $offset): bool + { + return isset($this->defaults[$offset]); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->defaults[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + $this->defaults[$offset] = $value; + } + + public function offsetUnset(mixed $offset): void + { + unset($this->defaults[$offset]); + } +} diff --git a/src/Registry.php b/src/Registry.php index 51b1699..f61ac26 100644 --- a/src/Registry.php +++ b/src/Registry.php @@ -20,16 +20,18 @@ final class Registry implements \IteratorAggregate private \SplObjectStorage $tables; private \SplObjectStorage $children; private \SplObjectStorage $relations; + private Defaults $defaults; /** * @param DatabaseProviderInterface $dbal */ - public function __construct(DatabaseProviderInterface $dbal) + public function __construct(DatabaseProviderInterface $dbal, ?Defaults $defaults = null) { $this->dbal = $dbal; $this->tables = new \SplObjectStorage(); $this->children = new \SplObjectStorage(); $this->relations = new \SplObjectStorage(); + $this->defaults = $defaults ?? new Defaults(); } public function register(Entity $entity): self @@ -270,6 +272,11 @@ public function getRelations(Entity $entity): array return $this->relations[$entity]; } + public function getDefaults(): Defaults + { + return $this->defaults; + } + protected function hasInstance(Entity $entity): bool { return array_search($entity, $this->entities, true) !== false; diff --git a/tests/Schema/CompilerTest.php b/tests/Schema/CompilerTest.php index ec086be..776653f 100644 --- a/tests/Schema/CompilerTest.php +++ b/tests/Schema/CompilerTest.php @@ -5,6 +5,8 @@ namespace Cycle\Schema\Tests; use Cycle\Database\DatabaseProviderInterface; +use Cycle\ORM\Parser\Typecast; +use Cycle\ORM\SchemaInterface; use Cycle\Schema\Compiler; use Cycle\Schema\Definition\Entity; use Cycle\Schema\Definition\Field; @@ -13,14 +15,15 @@ use Cycle\Schema\Registry; use Cycle\Schema\Tests\Fixtures\Author; use Cycle\Schema\Tests\Fixtures\BrokenSchemaModifier; +use Cycle\Schema\Tests\Fixtures\Typecaster; use PHPUnit\Framework\TestCase; class CompilerTest extends TestCase { - public function testWrongGeneratorShouldThrowAnException() + public function testWrongGeneratorShouldThrowAnException(): void { $this->expectException(CompilerException::class); - $this->expectErrorMessage( + $this->expectExceptionMessage( 'Invalid generator `\'Cycle\\\\Schema\\\\Tests\\\\Fixtures\\\\Author\'`. ' . 'It should implement `Cycle\Schema\GeneratorInterface` interface.' ); @@ -40,10 +43,10 @@ public function testWrongGeneratorShouldThrowAnException() ]); } - public function testWrongEntitySchemaModifierShouldThrowAnException() + public function testWrongEntitySchemaModifierShouldThrowAnException(): void { $this->expectException(SchemaModifierException::class); - $this->expectErrorMessage( + $this->expectExceptionMessage( 'Unable to apply schema modifier `Cycle\Schema\Tests\Fixtures\BrokenSchemaModifier` ' . 'for the `author` role. Something went wrong' ); @@ -62,4 +65,41 @@ public function testWrongEntitySchemaModifierShouldThrowAnException() (new Compiler())->compile($r); } + + /** + * @dataProvider renderTypecastDataProvider + */ + public function testRenderTypecast(mixed $expected, array $defaults, mixed $entityTypecast = null): void + { + $entity = new Entity(); + $entity->setRole('author')->setClass(Author::class); + $entity->getFields()->set('id', (new Field())->setType('primary')->setColumn('id')); + if ($entityTypecast) { + $entity->setTypecast($entityTypecast); + } + + $r = new Registry($this->createMock(DatabaseProviderInterface::class)); + $r->register($entity); + + $schema = (new Compiler())->compile($r, [], $defaults); + + $this->assertSame($expected, $schema['author'][SchemaInterface::TYPECAST_HANDLER]); + } + + public static function renderTypecastDataProvider(): \Traversable + { + yield [null, []]; + yield [Typecaster::class, [], Typecaster::class]; + yield [[Typecaster::class], [SchemaInterface::TYPECAST_HANDLER => Typecaster::class]]; + yield [ + [Typecaster::class, Typecast::class], + [SchemaInterface::TYPECAST_HANDLER => Typecast::class], + Typecaster::class, + ]; + yield [ + [Typecaster::class, Typecast::class], + [SchemaInterface::TYPECAST_HANDLER => [Typecaster::class, Typecast::class]], + Typecaster::class, + ]; + } } diff --git a/tests/Schema/DefaultsTest.php b/tests/Schema/DefaultsTest.php new file mode 100644 index 0000000..0f82fde --- /dev/null +++ b/tests/Schema/DefaultsTest.php @@ -0,0 +1,123 @@ +assertSame(Mapper::class, $defaults[SchemaInterface::MAPPER]); + $this->assertSame(Repository::class, $defaults[SchemaInterface::REPOSITORY]); + $this->assertSame(Source::class, $defaults[SchemaInterface::SOURCE]); + $this->assertNull($defaults[SchemaInterface::SCOPE]); + $this->assertNull($defaults[SchemaInterface::TYPECAST_HANDLER]); + } + + /** + * @dataProvider mergeDataProvider + */ + public function testMerge(array $expected, array $values): void + { + $defaults = new Defaults(); + $defaults->merge($values); + + $ref = new \ReflectionProperty($defaults, 'defaults'); + $ref->setAccessible(true); + + $this->assertEquals($expected, $ref->getValue($defaults)); + } + + public function testOffsetExists(): void + { + $defaults = new Defaults(); + + $this->assertTrue($defaults->offsetExists(SchemaInterface::MAPPER)); + $this->assertFalse($defaults->offsetExists('foo')); + } + + public function testOffsetGet(): void + { + $defaults = new Defaults(); + + $this->assertSame(Mapper::class, $defaults->offsetGet(SchemaInterface::MAPPER)); + } + + public function testOffsetSet(): void + { + $defaults = new Defaults(); + $defaults->offsetSet('foo', 'bar'); + + $this->assertSame('bar', $defaults->offsetGet('foo')); + } + + public function testOffsetUnset(): void + { + $defaults = new Defaults(); + + $this->assertTrue($defaults->offsetExists(SchemaInterface::MAPPER)); + $defaults->offsetUnset(SchemaInterface::MAPPER); + $this->assertFalse($defaults->offsetExists(SchemaInterface::MAPPER)); + } + + public static function mergeDataProvider(): \Traversable + { + yield [ + [ + SchemaInterface::MAPPER => Mapper::class, + SchemaInterface::REPOSITORY => Repository::class, + SchemaInterface::SOURCE => Source::class, + SchemaInterface::SCOPE => null, + SchemaInterface::TYPECAST_HANDLER => null, + ], + [], + ]; + yield [ + [ + SchemaInterface::MAPPER => Mapper::class, + SchemaInterface::REPOSITORY => Repository::class, + SchemaInterface::SOURCE => Source::class, + SchemaInterface::SCOPE => null, + SchemaInterface::TYPECAST_HANDLER => null, + 'foo' => 'bar', + ], + [ + 'foo' => 'bar', + ], + ]; + yield [ + [ + SchemaInterface::MAPPER => Mapper::class, + SchemaInterface::REPOSITORY => Repository::class, + SchemaInterface::SOURCE => Source::class, + SchemaInterface::SCOPE => null, + SchemaInterface::TYPECAST_HANDLER => 'foo', + ], + [ + SchemaInterface::TYPECAST_HANDLER => 'foo', + ], + ]; + yield [ + [ + SchemaInterface::MAPPER => null, + SchemaInterface::REPOSITORY => Repository::class, + SchemaInterface::SOURCE => Source::class, + SchemaInterface::SCOPE => null, + SchemaInterface::TYPECAST_HANDLER => null, + ], + [ + SchemaInterface::MAPPER => null, + ], + ]; + } +} diff --git a/tests/Schema/Definition/Inheritance/JoinedTableInheritanceTest.php b/tests/Schema/Definition/Inheritance/JoinedTableInheritanceTest.php index b13a78e..5788f59 100644 --- a/tests/Schema/Definition/Inheritance/JoinedTableInheritanceTest.php +++ b/tests/Schema/Definition/Inheritance/JoinedTableInheritanceTest.php @@ -69,7 +69,7 @@ public function testJoinedTableWithoutOuterKeyShouldBeAddedToSchema() public function testJoinedTableWithNonExistsOuterKeyShouldThrowAnException() { $this->expectException(WrongParentKeyColumnException::class); - $this->expectErrorMessage('Outer key column `foo_bar` is not found among fields of the `user` role.'); + $this->expectExceptionMessage('Outer key column `foo_bar` is not found among fields of the `user` role.'); $r = new Registry( $this->createMock(DatabaseProviderInterface::class) diff --git a/tests/Schema/Definition/Inheritance/SingleTableInheritanceTest.php b/tests/Schema/Definition/Inheritance/SingleTableInheritanceTest.php index cae0456..07f2c37 100644 --- a/tests/Schema/Definition/Inheritance/SingleTableInheritanceTest.php +++ b/tests/Schema/Definition/Inheritance/SingleTableInheritanceTest.php @@ -87,7 +87,7 @@ public function testSingleTableWithExplicitPkShouldBeAddedToSchema() public function testSingleTableWithoutDiscriminatorColumnShouldThrowAnException() { $this->expectException(DiscriminatorColumnNotPresentException::class); - $this->expectErrorMessage('Discriminator column for the `user` role should be defined.'); + $this->expectExceptionMessage('Discriminator column for the `user` role should be defined.'); $r = new Registry( $this->createMock(DatabaseProviderInterface::class) @@ -107,7 +107,7 @@ public function testSingleTableWithoutDiscriminatorColumnShouldThrowAnException( public function testSingleTableWithNonExistsDiscriminatorColumnShouldThrowAnException() { $this->expectException(WrongDiscriminatorColumnException::class); - $this->expectErrorMessage('Discriminator column `type` is not found among fields of the `user` role.'); + $this->expectExceptionMessage('Discriminator column `type` is not found among fields of the `user` role.'); $r = new Registry( $this->createMock(DatabaseProviderInterface::class) diff --git a/tests/Schema/EntityTest.php b/tests/Schema/EntityTest.php index 418ed3a..2b57f31 100644 --- a/tests/Schema/EntityTest.php +++ b/tests/Schema/EntityTest.php @@ -89,7 +89,7 @@ public function testSetPrimaryKeys(): void public function testSetPrimaryKeysShouldThrowAnExceptionWhenUsedNonExistsColumn(): void { $this->expectException(FieldException::class); - $this->expectErrorMessage('Undefined field with column name `test`.'); + $this->expectExceptionMessage('Undefined field with column name `test`.'); $e = new Entity(); $e->setRole('role'); @@ -114,7 +114,7 @@ public function testPrimaryKeysShouldReturnEmptyArrayWithoutPK(): void public function testPrimaryKeysShouldThrowAnExceptionWhenNumberOfPKsNotMatches(): void { $this->expectException(EntityException::class); - $this->expectErrorMessage('Ambiguous primary key definition for `role`.'); + $this->expectExceptionMessage('Ambiguous primary key definition for `role`.'); $e = new Entity(); $e->setRole('role'); diff --git a/tests/Schema/FieldsTest.php b/tests/Schema/FieldsTest.php index a2252ea..a9030cf 100644 --- a/tests/Schema/FieldsTest.php +++ b/tests/Schema/FieldsTest.php @@ -119,7 +119,7 @@ public function testGetKeyByColumnName(): void public function testGetKeyByColumnNameShouldThrowAnExceptionWhenFieldNotFound(): void { $this->expectException(FieldException::class); - $this->expectErrorMessage('Undefined field with column name `slug`.'); + $this->expectExceptionMessage('Undefined field with column name `slug`.'); $m = new FieldMap(); $m->set('p_id', (new Field())->setColumn('id')); @@ -140,7 +140,7 @@ public function testGetByColumnName(): void public function testGetByColumnNameShouldThrowAnExceptionWhenFieldNotFound(): void { $this->expectException(FieldException::class); - $this->expectErrorMessage('Undefined field with column name `slug`.'); + $this->expectExceptionMessage('Undefined field with column name `slug`.'); $m = new FieldMap(); $m->set('p_id', $id = (new Field())->setColumn('id')); diff --git a/tests/Schema/Generator/GenerateModifiersTest.php b/tests/Schema/Generator/GenerateModifiersTest.php index d0e7f7e..21d2c56 100644 --- a/tests/Schema/Generator/GenerateModifiersTest.php +++ b/tests/Schema/Generator/GenerateModifiersTest.php @@ -77,7 +77,7 @@ public function modifySchema(array &$schema): void public function testErrorInsideModifierShouldThrowAnException(string $method) { $this->expectException(SchemaException::class); - $this->expectErrorMessage( + $this->expectExceptionMessage( 'Unable to compute modifier `Cycle\Schema\Tests\Fixtures\BrokenSchemaModifier` for the `user` role.' ); diff --git a/tests/Schema/Generator/RenderModifiersTest.php b/tests/Schema/Generator/RenderModifiersTest.php index f4757bd..5903e93 100644 --- a/tests/Schema/Generator/RenderModifiersTest.php +++ b/tests/Schema/Generator/RenderModifiersTest.php @@ -52,7 +52,7 @@ public function testEntityShouldBeRendered() public function testErrorInsideModifierShouldThrowAnException(string $method) { $this->expectException(SchemaException::class); - $this->expectErrorMessage( + $this->expectExceptionMessage( 'Unable to render modifier `Cycle\Schema\Tests\Fixtures\BrokenSchemaModifier` for the `user` role.' ); diff --git a/tests/Schema/Generator/TableGeneratorTest.php b/tests/Schema/Generator/TableGeneratorTest.php index dbf8145..c02a2ae 100644 --- a/tests/Schema/Generator/TableGeneratorTest.php +++ b/tests/Schema/Generator/TableGeneratorTest.php @@ -66,7 +66,7 @@ public function testCompiled(): void Schema::SCOPE => null, Schema::TYPECAST => [], Schema::SCHEMA => [], - Schema::TYPECAST_HANDLER => Typecaster::class, + Schema::TYPECAST_HANDLER => [Typecaster::class, 'default_typecaster'], ], ], $schema); } @@ -102,7 +102,7 @@ public function testCompiledWithPassedDefaultTypecastHandler(): void Schema::SCOPE => null, Schema::TYPECAST => [], Schema::SCHEMA => [], - Schema::TYPECAST_HANDLER => Typecaster::class, + Schema::TYPECAST_HANDLER => [Typecaster::class], ], ], $schema); } diff --git a/tests/Schema/Relation/Traits/FieldTraitTest.php b/tests/Schema/Relation/Traits/FieldTraitTest.php index d30e794..f518519 100644 --- a/tests/Schema/Relation/Traits/FieldTraitTest.php +++ b/tests/Schema/Relation/Traits/FieldTraitTest.php @@ -47,7 +47,7 @@ public function testGetsFieldShouldThrowAnExceptionIdFieldNotExists(): void $entity->setRole('post'); $this->expectException(RelationException::class); - $this->expectErrorMessage('Field `post`.`id` does not exists, referenced by `test`'); + $this->expectExceptionMessage('Field `post`.`id` does not exists, referenced by `test`'); $this->getField($entity, 123); } @@ -85,7 +85,7 @@ public function testGetsFieldsShouldThrowAnExcpetionIfFieldNotExists(): void ->set('id', $fieldId = (new Field())->setColumn('id')); $this->expectException(RelationException::class); - $this->expectErrorMessage('Field `post`.`slug` does not exists, referenced by `test`'); + $this->expectExceptionMessage('Field `post`.`slug` does not exists, referenced by `test`'); $this->getFields($entity, 234); }