From da59dc66917f33b8867fecc14256253c4f4a455b Mon Sep 17 00:00:00 2001 From: Alexander Kornilov Date: Tue, 13 May 2025 20:39:51 +0300 Subject: [PATCH 1/3] Added the ability to specify typecasts in the Embeddable attribute --- src/Annotation/Embeddable.php | 10 +++ src/Annotation/Entity.php | 2 +- src/Configurator.php | 1 + .../Annotated/Fixtures/Fixtures26/Address.php | 41 ++++++++++++ tests/Annotated/Fixtures/Fixtures26/City.php | 25 +++++++ .../Fixtures/Fixtures26/CityTypecast.php | 67 +++++++++++++++++++ tests/Annotated/Fixtures/Fixtures26/User.php | 24 +++++++ .../Common/Relation/EmbeddedTestCase.php | 29 ++++++++ 8 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 tests/Annotated/Fixtures/Fixtures26/Address.php create mode 100644 tests/Annotated/Fixtures/Fixtures26/City.php create mode 100644 tests/Annotated/Fixtures/Fixtures26/CityTypecast.php create mode 100644 tests/Annotated/Fixtures/Fixtures26/User.php diff --git a/src/Annotation/Embeddable.php b/src/Annotation/Embeddable.php index 0b0dd374..6d83469f 100644 --- a/src/Annotation/Embeddable.php +++ b/src/Annotation/Embeddable.php @@ -20,12 +20,14 @@ class Embeddable * @param class-string|null $mapper Mapper class name. Defaults to {@see \Cycle\ORM\Mapper\Mapper}. * @param string $columnPrefix Custom prefix for embeddable entity columns. * @param Column[] $columns Embedded entity columns. + * @param non-empty-string|non-empty-string[]|null $typecast Typecasts for embeddable entity columns. */ public function __construct( protected ?string $role = null, protected ?string $mapper = null, protected string $columnPrefix = '', protected array $columns = [], + protected array|string|null $typecast = null, ) {} /** @@ -56,4 +58,12 @@ public function getColumns(): array { return $this->columns; } + + /** + * @return non-empty-string|non-empty-string[]|null + */ + public function getTypecast(): array|string|null + { + return $this->typecast; + } } diff --git a/src/Annotation/Entity.php b/src/Annotation/Entity.php index 9e7a4cbd..288cd2a4 100644 --- a/src/Annotation/Entity.php +++ b/src/Annotation/Entity.php @@ -29,7 +29,7 @@ class Entity * @param non-empty-string|null $database Database name. Defaults to null (default database). * @param class-string|null $source Entity source class (internal). * Defaults to {@see \Cycle\ORM\Select\Source} - * @param non-empty-string|non-empty-string[]|null $typecast + * @param non-empty-string|non-empty-string[]|null $typecast Typecasts for entity columns. * @param class-string|null $scope Class name of constraint to be applied to every entity query. * @param Column[] $columns Entity columns. * @param ForeignKey[] $foreignKeys Entity foreign keys. diff --git a/src/Configurator.php b/src/Configurator.php index 03a6b1d3..23a8c9b5 100644 --- a/src/Configurator.php +++ b/src/Configurator.php @@ -90,6 +90,7 @@ public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntityS // representing classes $e->setMapper($this->resolveName($emb->getMapper(), $class)); + $e->setTypecast($this->resolveTypecast($emb->getTypecast(), $class)); return $e; } diff --git a/tests/Annotated/Fixtures/Fixtures26/Address.php b/tests/Annotated/Fixtures/Fixtures26/Address.php new file mode 100644 index 00000000..2daa81b3 --- /dev/null +++ b/tests/Annotated/Fixtures/Fixtures26/Address.php @@ -0,0 +1,41 @@ +value = $value; + } + + public function __toString(): string + { + return $this->value; + } + + public static function fromString(string $value): self + { + return new self($value); + } +} diff --git a/tests/Annotated/Fixtures/Fixtures26/CityTypecast.php b/tests/Annotated/Fixtures/Fixtures26/CityTypecast.php new file mode 100644 index 00000000..bdc8d8c9 --- /dev/null +++ b/tests/Annotated/Fixtures/Fixtures26/CityTypecast.php @@ -0,0 +1,67 @@ +rules) as $column) { + if (!isset($data[$column])) { + continue; + } + + if (!\is_string($data[$column]) || $data[$column] === '') { + $data[$column] = null; + + continue; + } + + $data[$column] = City::fromString($data[$column]); + } + + return $data; + } + + public function uncast(array $data): array + { + foreach (array_keys($this->rules) as $column) { + if (!isset($data[$column])) { + continue; + } + + $value = $data[$column]; + + if (!$value instanceof City) { + continue; + } + + $data[$column] = (string)$value; + } + + return $data; + } + + public function setRules(array $rules): array + { + /** @var non-empty-string $rule */ + foreach ($rules as $key => $rule) { + if ($rule !== 'city') { + continue; + } + + unset($rules[$key]); + + $this->rules[$key] = $rule; + } + + return $rules; + } +} diff --git a/tests/Annotated/Fixtures/Fixtures26/User.php b/tests/Annotated/Fixtures/Fixtures26/User.php new file mode 100644 index 00000000..948b9e79 --- /dev/null +++ b/tests/Annotated/Fixtures/Fixtures26/User.php @@ -0,0 +1,24 @@ +assertSame($address, $schema['user:address:address'][Schema::COLUMNS]); $this->assertSame($workAddress, $schema['user:address:workAddress'][Schema::COLUMNS]); } + + #[DataProvider('allReadersProvider')] + public function testEmbeddedTypecast(ReaderInterface $reader): void + { + $tokenizer = new Tokenizer(new TokenizerConfig([ + 'directories' => [__DIR__ . '/../../../../Fixtures/Fixtures26'], + 'exclude' => [], + ])); + + $locator = $tokenizer->classLocator(); + + $r = new Registry($this->dbal); + + $schema = (new Compiler())->compile($r, [ + new Embeddings(new TokenizerEmbeddingLocator($locator, $reader), $reader), + new Entities(new TokenizerEntityLocator($locator, $reader), $reader), + new ResetTables(), + new MergeColumns($reader), + new GenerateRelations(), + new RenderTables(), + new RenderRelations(), + new MergeIndexes($reader), + new SyncTables(), + new GenerateTypecast(), + ]); + + $this->assertSame(CityTypecast::class, $schema['user:address:address'][Schema::TYPECAST_HANDLER][0]); + } } From fb4761c6c6e9f0b2d1be83e83211a2609910462f Mon Sep 17 00:00:00 2001 From: Alexander Kornilov Date: Wed, 14 May 2025 16:34:40 +0300 Subject: [PATCH 2/3] Fixed incorrect typecast resolve for embedded entity --- src/Configurator.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Configurator.php b/src/Configurator.php index 23a8c9b5..dc29b8a1 100644 --- a/src/Configurator.php +++ b/src/Configurator.php @@ -90,7 +90,18 @@ public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntityS // representing classes $e->setMapper($this->resolveName($emb->getMapper(), $class)); - $e->setTypecast($this->resolveTypecast($emb->getTypecast(), $class)); + + $typecast = $emb->getTypecast(); + + if (\is_array($typecast)) { + /** @var non-empty-string[] $typecast */ + $typecast = \array_map(fn(string $value): string => $this->resolveName($value, $class), $typecast); + } else { + /** @var non-empty-string|null $typecast */ + $typecast = $this->resolveName($typecast, $class); + } + + $e->setTypecast($typecast); return $e; } From 306033f775c5f844ad45d889ae46553bedc2150b Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Wed, 14 May 2025 17:49:20 +0400 Subject: [PATCH 3/3] Fix comments --- src/Annotation/Embeddable.php | 2 +- src/Annotation/Entity.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Annotation/Embeddable.php b/src/Annotation/Embeddable.php index 6d83469f..9a72d4a4 100644 --- a/src/Annotation/Embeddable.php +++ b/src/Annotation/Embeddable.php @@ -20,7 +20,7 @@ class Embeddable * @param class-string|null $mapper Mapper class name. Defaults to {@see \Cycle\ORM\Mapper\Mapper}. * @param string $columnPrefix Custom prefix for embeddable entity columns. * @param Column[] $columns Embedded entity columns. - * @param non-empty-string|non-empty-string[]|null $typecast Typecasts for embeddable entity columns. + * @param non-empty-string|non-empty-string[]|null $typecast Typecast handlers for entity columns. */ public function __construct( protected ?string $role = null, diff --git a/src/Annotation/Entity.php b/src/Annotation/Entity.php index 288cd2a4..6dfae68f 100644 --- a/src/Annotation/Entity.php +++ b/src/Annotation/Entity.php @@ -29,7 +29,7 @@ class Entity * @param non-empty-string|null $database Database name. Defaults to null (default database). * @param class-string|null $source Entity source class (internal). * Defaults to {@see \Cycle\ORM\Select\Source} - * @param non-empty-string|non-empty-string[]|null $typecast Typecasts for entity columns. + * @param non-empty-string|non-empty-string[]|null $typecast Typecast handlers for entity columns. * @param class-string|null $scope Class name of constraint to be applied to every entity query. * @param Column[] $columns Entity columns. * @param ForeignKey[] $foreignKeys Entity foreign keys.