diff --git a/src/Annotation/Embeddable.php b/src/Annotation/Embeddable.php index 0b0dd374..9a72d4a4 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 Typecast handlers for 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..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 + * @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. diff --git a/src/Configurator.php b/src/Configurator.php index 03a6b1d3..dc29b8a1 100644 --- a/src/Configurator.php +++ b/src/Configurator.php @@ -91,6 +91,18 @@ public function initEmbedding(Embeddable $emb, \ReflectionClass $class): EntityS // representing classes $e->setMapper($this->resolveName($emb->getMapper(), $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; } 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]); + } }