diff --git a/src/Dto/AbstractDto.php b/src/Dto/AbstractDto.php index b641153..4892e27 100644 --- a/src/Dto/AbstractDto.php +++ b/src/Dto/AbstractDto.php @@ -4,6 +4,7 @@ namespace PhpCollective\Dto\Dto; +use PhpCollective\Dto\Transformer\TransformerRegistry; use PhpCollective\Dto\Utility\Json; use RuntimeException; @@ -18,7 +19,7 @@ abstract class AbstractDto extends Dto */ public function fromArray(array $data, bool $ignoreMissing = false, ?string $type = null): static { - if ($type === null && static::HAS_FAST_PATH) { + if ($type === null && static::HAS_FAST_PATH && !TransformerRegistry::hasAnyCaster()) { if (!$ignoreMissing) { $this->validateFieldNames($data); } diff --git a/src/Dto/Dto.php b/src/Dto/Dto.php index 3cbefbf..a74f7bf 100644 --- a/src/Dto/Dto.php +++ b/src/Dto/Dto.php @@ -274,7 +274,7 @@ public static function create(?array $data = null, bool $ignoreMissing = false, public function __construct(?array $data = null, bool $ignoreMissing = false, ?string $type = null) { if ($data) { - if ($type === null && static::HAS_FAST_PATH && !TransformerRegistry::hasAny()) { + if ($type === null && static::HAS_FAST_PATH && !TransformerRegistry::hasAnyCaster()) { if (!$ignoreMissing) { $this->validateFieldNames($data); } @@ -340,7 +340,7 @@ protected function _toArrayInternal(?string $type = null, ?array $fields = null, if ( !$touched && $fields === null && static::HAS_FAST_PATH && ($type === null || $type === static::TYPE_CAMEL || $type === static::TYPE_DEFAULT) - && !TransformerRegistry::hasAny() + && !TransformerRegistry::hasAnySerializer() ) { return $this->toArrayFast(); } diff --git a/src/Transformer/TransformerRegistry.php b/src/Transformer/TransformerRegistry.php index 31dc043..0db60dc 100644 --- a/src/Transformer/TransformerRegistry.php +++ b/src/Transformer/TransformerRegistry.php @@ -174,6 +174,28 @@ public static function hasAny(): bool return self::$casters !== [] || self::$serializers !== []; } + /** + * Check whether any caster is currently registered. + * Used to bypass generated fast paths on input (hydration). + * + * @return bool + */ + public static function hasAnyCaster(): bool + { + return self::$casters !== []; + } + + /** + * Check whether any serializer is currently registered. + * Used to bypass generated fast paths on output (serialization). + * + * @return bool + */ + public static function hasAnySerializer(): bool + { + return self::$serializers !== []; + } + /** * @param string $type * diff --git a/tests/Dto/DtoTest.php b/tests/Dto/DtoTest.php index fd6e5fa..7368a05 100644 --- a/tests/Dto/DtoTest.php +++ b/tests/Dto/DtoTest.php @@ -19,6 +19,7 @@ use PhpCollective\Dto\Test\TestDto\AssociativeCollectionDto; use PhpCollective\Dto\Test\TestDto\CollectionDto; use PhpCollective\Dto\Test\TestDto\CustomCollectionDto; +use PhpCollective\Dto\Test\TestDto\FastPathCasterDto; use PhpCollective\Dto\Test\TestDto\ImmutableCollectionDto; use PhpCollective\Dto\Test\TestDto\ImmutableDto; use PhpCollective\Dto\Test\TestDto\MapFromDto; @@ -1755,6 +1756,60 @@ public function testMultipleRequiredFieldsErrorMessageFormat(): void } } + public function testGlobalCasterAppliesToClassFieldOnFastPathFromArray(): void + { + TransformerRegistry::addCaster( + PlainClass::class, + fn (mixed $value): PlainClass => new PlainClass('cast:' . (string)$value), + ); + + $dto = (new FastPathCasterDto())->fromArray(['plainData' => 'hello']); + + $plain = $dto->getPlainData(); + $this->assertInstanceOf(PlainClass::class, $plain); + $this->assertSame('cast:hello', $plain->value); + } + + public function testGlobalCasterAppliesToClassFieldOnFastPathConstructor(): void + { + TransformerRegistry::addCaster( + PlainClass::class, + fn (mixed $value): PlainClass => new PlainClass('cast:' . (string)$value), + ); + + $dto = new FastPathCasterDto(['plainData' => 'hello']); + + $plain = $dto->getPlainData(); + $this->assertInstanceOf(PlainClass::class, $plain); + $this->assertSame('cast:hello', $plain->value); + } + + public function testGlobalSerializerAppliesToFastPathToArray(): void + { + TransformerRegistry::addSerializer( + PlainClass::class, + fn (PlainClass $value): string => 'serialized:' . $value->value, + ); + + $dto = (new FastPathCasterDto())->setPlainData(new PlainClass('abc')); + + $this->assertSame(['plainData' => 'serialized:abc'], $dto->toArray()); + } + + public function testGlobalSerializerDoesNotDisableFastPathHydration(): void + { + TransformerRegistry::addSerializer( + PlainClass::class, + fn (PlainClass $value): string => 'serialized:' . $value->value, + ); + + $dto = (new FastPathCasterDto())->fromArray(['plainData' => 'hello']); + + $plain = $dto->getPlainData(); + $this->assertInstanceOf(PlainClass::class, $plain); + $this->assertSame('hello', $plain->value); + } + public function testGlobalCasterAppliesToClassField(): void { TransformerRegistry::addCaster( diff --git a/tests/TestDto/FastPathCasterDto.php b/tests/TestDto/FastPathCasterDto.php new file mode 100644 index 0000000..ba6a5b5 --- /dev/null +++ b/tests/TestDto/FastPathCasterDto.php @@ -0,0 +1,122 @@ +> + */ + protected array $_metadata = [ + 'plainData' => [ + 'type' => PlainClass::class, + 'required' => false, + 'defaultValue' => null, + 'dto' => false, + 'collectionType' => null, + 'singularType' => null, + 'associative' => false, + 'key' => null, + 'serialize' => null, + 'factory' => null, + 'isClass' => true, + 'enum' => null, + ], + ]; + + /** + * @var array> + */ + protected array $_keyMap = [ + 'underscored' => [ + 'plain_data' => 'plainData', + ], + 'dashed' => [ + 'plain-data' => 'plainData', + ], + ]; + + public function getPlainData(): ?PlainClass + { + return $this->plainData; + } + + public function setPlainData(?PlainClass $plainData): self + { + $this->plainData = $plainData; + $this->_touchedFields['plainData'] = true; + + return $this; + } + + public function hasPlainData(): bool + { + return $this->plainData !== null; + } + + /** + * @param array $data + * + * @return void + */ + protected function setFromArrayFast(array $data): void + { + if (isset($data['plainData'])) { + $value = $data['plainData']; + if (!$value instanceof PlainClass) { + $value = new PlainClass($value); + } + $this->plainData = $value; + $this->_touchedFields['plainData'] = true; + } + } + + /** + * @return array + */ + protected function toArrayFast(): array + { + return [ + 'plainData' => $this->plainData !== null ? $this->plainData->value : null, + ]; + } + + /** + * @param string|null $type + * @param array|null $fields + * @param bool $touched + * + * @return array + */ + public function toArray(?string $type = null, ?array $fields = null, bool $touched = false): array + { + return $this->_toArrayInternal($type, $fields, $touched); + } + + /** + * @param array $data + * @param bool $ignoreMissing + * @param string|null $type + * + * @return static + */ + public static function createFromArray(array $data, bool $ignoreMissing = false, ?string $type = null): static + { + return static::_createFromArrayInternal($data, $ignoreMissing, $type); + } +}