diff --git a/README.md b/README.md index fb8624f..3a64b60 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # PHP Pure Data objects -![Packagist Version](https://img.shields.io/packagist/v/nuxtifyts/php-dto) +![Packagist Version](https://img.shields.io/packagist/v/nuxtifyts/php-dto?style=for-the-badge&cacheSeconds=3600) +![PhpStan Level](https://img.shields.io/badge/PHPStan-level%2010-brightgreen.svg?style=for-the-badge) This package enabled the creation of data objects which can be used in various ways. Using modern PHP syntax, it allows you to hydrate data from arrays, objects, and other data sources. diff --git a/docs/DefaultValues.md b/docs/DefaultValues.md new file mode 100644 index 0000000..760f0d8 --- /dev/null +++ b/docs/DefaultValues.md @@ -0,0 +1,87 @@ +Default Values += + +Sometimes, we may need to specify that a property has a default value, +we can achieve that using plain PHP for some property types but not all of them. + +```php +use Nuxtifyts\PhpDto\Data; + +final readonly class User extends Data +{ + public function __construct( + public string $firstName, + public string $lastName, + public string $email, + public UserType $type = UserType::DEFAULT, + public UserConfigData $config, + ) {} +} +``` + +On the other hand, if we want to specify, for example, a default value for UserType depending +on the provided email address, or if you want to provide a default value for complex data such as +`UserConfigData` which is another DTO, there is no way to do it using plain PHP, +that's where `DefaultsTo` attribute comes in. + +```php +use Nuxtifyts\PhpDto\Data; +use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo; + +final readonly class User extends Data +{ + public function __construct( + public string $firstName, + public string $lastName, + public string $email, + #[DefaultsTo(UserType::DEFAULT)] + public UserType $type, + #[DefaultsTo(UserConfigDataFallbackResolver::class)] + public UserConfigData $config, + ) {} +} +``` + +The `DefaultsTo` attribute provides the ability to specify default values for complex types, +such as DateTimes and DTOs. + +For more details checkout the [DefaultValues](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md) +guide. + +In this example, the `UserConfigDataFallbackResolver` would look like this: + +```php +use Nuxtifyts\PhpDto\Contexts\PropertyContext; +use Nuxtifyts\PhpDto\FallbackResolver\FallbackResolver; + +class UserConfigDataFallbackResolver implements FallbackResolver +{ + /** + * @param array $rawData + */ + public static function resolve(array $rawData, PropertyContext $property) : mixed{ + $email = $rawData['email'] ?? null; + + return match(true) { + str_contains($email, 'example.com') => new UserConfigData(/** Admin configs */), + default => new UserConfigData(/** User configs */) + } + } +} +``` + +>! When using `DefaultsTo` attribute, priority is given to the attribute instead of the parameter's default value. + +If ever needed to create a new instance of a DTO with complex default value, +using the constructor is no longer possible, instead, you can make use of the +`create` function provided by the DTO class. + +Using the same example above, we can create a new instance of `User` with the default value for `config`: + +```php +$user = User::create( + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@example.com' +); +``` diff --git a/docs/PropertyAttributes.md b/docs/PropertyAttributes.md index 305e276..c7cd562 100644 --- a/docs/PropertyAttributes.md +++ b/docs/PropertyAttributes.md @@ -4,6 +4,7 @@ Property Attributes In order to provide more functionality to your DTOs, you can use the following attributes: - [Computed](#Computed) - To define a property that is computed from other properties. - [Aliases](#Aliases) - To define aliases for a property. +- [DefaultsTo](#DefaultsTo) - To define a default value for a property using a fallback resolver. - [CipherTarget](#CipherTarget) - To define a property that should be encrypted/decrypted. Computed @@ -109,3 +110,52 @@ public function __construct( ) {} ``` +DefaultsTo +- + +Sometimes, we may need to specify that a property has a default value, +we can achieve that using plain PHP for some property types but not all of them. + +```php +use Nuxtifyts\PhpDto\Data; + +final readonly class User extends Data +{ + public function __construct( + public string $firstName, + public string $lastName, + public string $email, + public UserType $type = UserType::DEFAULT, + public UserConfigData $config, + ) {} +} +``` + +On the other hand, if we want to specify, for example, a default value for UserType depending +on the provided email address, or if you want to provide a default value for complex data such as +`UserConfigData` which is another DTO, there is no way to do it using plain PHP, +that's where `DefaultsTo` attribute comes in. + +```php +use Nuxtifyts\PhpDto\Data; +use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo; + +final readonly class User extends Data +{ + public function __construct( + public string $firstName, + public string $lastName, + public string $email, + #[DefaultsTo(UserType::DEFAULT)] + public UserType $type, + #[DefaultsTo(UserConfigDataFallbackResolver::class)] + public UserConfigData $config, + ) {} +} +``` + +The `DefaultsTo` attribute provides the ability to specify default values for complex types, +such as DateTimes and DTOs. + +For more details checkout the [DefaultValues](https://github.com/nuxtifyts/php-dto/blob/main/docs/DefaultValues.md) +guide. diff --git a/src/Attributes/Property/DefaultsTo.php b/src/Attributes/Property/DefaultsTo.php new file mode 100644 index 0000000..5ed2fb4 --- /dev/null +++ b/src/Attributes/Property/DefaultsTo.php @@ -0,0 +1,40 @@ +> */ + protected static array $_resolverReflections = []; + + /** @var ?class-string */ + protected(set) ?string $fallbackResolverClass = null; + + /** + * @param BackedEnum|array|int|string|float|bool|null $value + * + * @throws FallbackResolverException + */ + public function __construct( + protected(set) BackedEnum|array|int|string|float|bool|null $value + ) { + if (is_string($value) && class_exists($value)) { + /** @var ReflectionClass $reflection */ + $reflection = self::$_resolverReflections[$value] ??= new ReflectionClass($value); + + if (!$reflection->implementsInterface(FallbackResolver::class)) { + throw FallbackResolverException::unableToFindResolverClass($value); + } else { + /** @var class-string $value */ + $this->fallbackResolverClass = $value; + } + } + } +} diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 5199df0..634ebb8 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -3,13 +3,11 @@ namespace Nuxtifyts\PhpDto\Concerns; use Nuxtifyts\PhpDto\Contexts\ClassContext; +use Nuxtifyts\PhpDto\Exceptions\DataCreationException; use Nuxtifyts\PhpDto\Exceptions\DeserializeException; use Nuxtifyts\PhpDto\Exceptions\SerializeException; -use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DecipherDataPipe; +use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipeline; use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable; -use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\RefineDataPipe; -use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\ResolveValuesFromAliasesPipe; -use Nuxtifyts\PhpDto\Support\Pipeline; use Nuxtifyts\PhpDto\Support\Traits\HasNormalizers; use ReflectionClass; use Throwable; @@ -18,6 +16,40 @@ trait BaseData { use HasNormalizers; + final public static function create(mixed ...$args): static + { + if (array_any( + array_keys($args), + static fn (string|int $arg) => is_numeric($arg) + )) { + throw DataCreationException::invalidProperty(); + } + + try { + $value = static::normalizeValue($args, static::class); + + if ($value === false) { + throw new DeserializeException( + code: DeserializeException::INVALID_VALUE_ERROR_CODE + ); + } + + /** @var ClassContext $context */ + $context = ClassContext::getInstance(new ReflectionClass(static::class)); + + $data = DeserializePipeline::createFromArray() + ->sendThenReturn(new DeserializePipelinePassable( + classContext: $context, + data: $value + )) + ->data; + + return static::instanceWithConstructorCallFrom($context, $data); + } catch (Throwable $e) { + throw DataCreationException::unableToCreateInstance(static::class, $e); + } + } + /** * @throws DeserializeException */ @@ -26,7 +58,7 @@ final public static function from(mixed $value): static try { $value = static::normalizeValue($value, static::class); - if (empty($value)) { + if ($value === false) { throw new DeserializeException( code: DeserializeException::INVALID_VALUE_ERROR_CODE ); @@ -35,10 +67,7 @@ final public static function from(mixed $value): static /** @var ClassContext $context */ $context = ClassContext::getInstance(new ReflectionClass(static::class)); - $data = new Pipeline(DeserializePipelinePassable::class) - ->through(ResolveValuesFromAliasesPipe::class) - ->through(RefineDataPipe::class) - ->through(DecipherDataPipe::class) + $data = DeserializePipeline::hydrateFromArray() ->sendThenReturn(new DeserializePipelinePassable( classContext: $context, data: $value diff --git a/src/Contexts/PropertyContext.php b/src/Contexts/PropertyContext.php index 7d1976e..54200c1 100644 --- a/src/Contexts/PropertyContext.php +++ b/src/Contexts/PropertyContext.php @@ -5,6 +5,7 @@ use Nuxtifyts\PhpDto\Attributes\Property\Aliases; use Nuxtifyts\PhpDto\Attributes\Property\CipherTarget; use Nuxtifyts\PhpDto\Attributes\Property\Computed; +use Nuxtifyts\PhpDto\Attributes\Property\DefaultsTo; use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner; use Nuxtifyts\PhpDto\DataCiphers\CipherConfig; use Nuxtifyts\PhpDto\DataRefiners\DataRefiner; @@ -13,6 +14,7 @@ use Nuxtifyts\PhpDto\Exceptions\SerializeException; use Nuxtifyts\PhpDto\Exceptions\UnknownTypeException; use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException; +use Nuxtifyts\PhpDto\FallbackResolver\FallbackConfig; use Nuxtifyts\PhpDto\Serializers\Serializer; use Nuxtifyts\PhpDto\Support\Traits\HasSerializers; use Nuxtifyts\PhpDto\Support\Traits\HasTypes; @@ -42,6 +44,8 @@ class PropertyContext private(set) ?CipherConfig $cipherConfig = null; + private(set) ?FallbackConfig $fallbackConfig = null; + /** @var list */ private(set) array $dataRefiners = []; @@ -112,6 +116,16 @@ private function syncPropertyAttributes(): void encoded: $instance->encoded ); } + + if ($defaultsToAttribute = $this->reflection->getAttributes(DefaultsTo::class)[0] ?? null) { + /** @var ReflectionAttribute $defaultsToAttribute */ + $instance = $defaultsToAttribute->newInstance(); + + $this->fallbackConfig = new FallbackConfig( + value: $instance->value, + resolverClass: $instance->fallbackResolverClass + ); + } } public function getValue(object $object): mixed diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 9f05178..896a834 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -2,12 +2,18 @@ namespace Nuxtifyts\PhpDto\Contracts; +use Nuxtifyts\PhpDto\Exceptions\DataCreationException; use Nuxtifyts\PhpDto\Exceptions\DeserializeException; use Nuxtifyts\PhpDto\Exceptions\SerializeException; use JsonSerializable; interface BaseData extends JsonSerializable { + /** + * @throws DataCreationException + */ + public static function create(mixed ...$args): static; + /** * @return array * diff --git a/src/Exceptions/DataCreationException.php b/src/Exceptions/DataCreationException.php new file mode 100644 index 0000000..e383ccd --- /dev/null +++ b/src/Exceptions/DataCreationException.php @@ -0,0 +1,31 @@ +|int|string|float|bool|null $value + * @param ?class-string $resolverClass + */ + public function __construct( + public BackedEnum|array|int|string|float|bool|null $value, + public ?string $resolverClass = null + ) { + } +} diff --git a/src/FallbackResolver/FallbackResolver.php b/src/FallbackResolver/FallbackResolver.php new file mode 100644 index 0000000..04b57c4 --- /dev/null +++ b/src/FallbackResolver/FallbackResolver.php @@ -0,0 +1,16 @@ + $rawData + * + * @throws FallbackResolverException + */ + public static function resolve(array $rawData, PropertyContext $property): mixed; +} diff --git a/src/Normalizers/ArrayNormalizer.php b/src/Normalizers/ArrayNormalizer.php index 1dc0c84..952c109 100644 --- a/src/Normalizers/ArrayNormalizer.php +++ b/src/Normalizers/ArrayNormalizer.php @@ -8,7 +8,10 @@ public function normalize(): array|false { if ( !is_array($this->value) - || array_is_list($this->value) + || ( + array_is_list($this->value) + && !empty($this->value) + ) ) { return false; } diff --git a/src/Pipelines/DeserializePipeline/DeserializePipeline.php b/src/Pipelines/DeserializePipeline/DeserializePipeline.php new file mode 100644 index 0000000..45b5b7e --- /dev/null +++ b/src/Pipelines/DeserializePipeline/DeserializePipeline.php @@ -0,0 +1,36 @@ + + */ +class DeserializePipeline extends Pipeline +{ + public static function hydrateFromArray(): self + { + return new DeserializePipeline(DeserializePipelinePassable::class) + ->through(ResolveValuesFromAliasesPipe::class) + ->through(RefineDataPipe::class) + ->through(DecipherDataPipe::class) + ->through(ResolveDefaultDataPipe::class); + } + + /** + * @desc Basically the same as hydrateFromArray, but without deciphering data. + * This is used when create a new instance using the `create` static method from `BaseData`. + */ + public static function createFromArray(): self + { + return new DeserializePipeline(DeserializePipelinePassable::class) + ->through(ResolveValuesFromAliasesPipe::class) + ->through(RefineDataPipe::class) + ->through(ResolveDefaultDataPipe::class); + } +} diff --git a/src/Pipelines/DeserializePipeline/DecipherDataPipe.php b/src/Pipelines/DeserializePipeline/Pipes/DecipherDataPipe.php similarity index 87% rename from src/Pipelines/DeserializePipeline/DecipherDataPipe.php rename to src/Pipelines/DeserializePipeline/Pipes/DecipherDataPipe.php index 7661231..7e26bbc 100644 --- a/src/Pipelines/DeserializePipeline/DecipherDataPipe.php +++ b/src/Pipelines/DeserializePipeline/Pipes/DecipherDataPipe.php @@ -1,7 +1,8 @@ diff --git a/src/Pipelines/DeserializePipeline/Pipes/ResolveDefaultDataPipe.php b/src/Pipelines/DeserializePipeline/Pipes/ResolveDefaultDataPipe.php new file mode 100644 index 0000000..27f0eed --- /dev/null +++ b/src/Pipelines/DeserializePipeline/Pipes/ResolveDefaultDataPipe.php @@ -0,0 +1,50 @@ + + */ +readonly class ResolveDefaultDataPipe extends Pipe +{ + public function handle(Passable $passable): DeserializePipelinePassable + { + $data = $passable->data; + + foreach ($passable->classContext->properties as $propertyContext) { + if (array_key_exists($propertyContext->propertyName, $data)) { + continue; + } + + if ($propertyContext->fallbackConfig) { + $data[$propertyContext->propertyName] = $propertyContext->fallbackConfig->resolverClass + ? $propertyContext->fallbackConfig->resolverClass::resolve($data, $propertyContext) + : $propertyContext->fallbackConfig->value; + } + + $constructorParameters = $propertyContext->reflection + ->getDeclaringClass() + ->getConstructor() + ?->getParameters(); + + if ( + $propertyParameter = array_find( + $constructorParameters ?? [], + fn (ReflectionParameter $parameter) => $parameter->getName() === $propertyContext->propertyName + ) + ) { + /** @var ReflectionParameter $propertyParameter */ + if ($propertyParameter->isDefaultValueAvailable()) { + $data[$propertyContext->propertyName] = $propertyParameter->getDefaultValue(); + } + } + } + + return $passable->with(data: $data); + } +} diff --git a/src/Pipelines/DeserializePipeline/ResolveValuesFromAliasesPipe.php b/src/Pipelines/DeserializePipeline/Pipes/ResolveValuesFromAliasesPipe.php similarity index 85% rename from src/Pipelines/DeserializePipeline/ResolveValuesFromAliasesPipe.php rename to src/Pipelines/DeserializePipeline/Pipes/ResolveValuesFromAliasesPipe.php index 55a0dcb..6380bb4 100644 --- a/src/Pipelines/DeserializePipeline/ResolveValuesFromAliasesPipe.php +++ b/src/Pipelines/DeserializePipeline/Pipes/ResolveValuesFromAliasesPipe.php @@ -1,7 +1,8 @@ normalize(); - if (!empty($normalized)) { + if ($normalized !== false) { return $normalized; } } diff --git a/tests/Dummies/FallbackResolvers/DummyPointsFallbackResolver.php b/tests/Dummies/FallbackResolvers/DummyPointsFallbackResolver.php new file mode 100644 index 0000000..388c442 --- /dev/null +++ b/tests/Dummies/FallbackResolvers/DummyPointsFallbackResolver.php @@ -0,0 +1,22 @@ + + */ + public static function resolve(array $rawData, PropertyContext $property): array + { + return [ + new PointData(1, 2), + new PointData(3, 4), + new PointData(5, 6), + ]; + } +} diff --git a/tests/Dummies/FallbackResolvers/DummyUserFallbackResolver.php b/tests/Dummies/FallbackResolvers/DummyUserFallbackResolver.php new file mode 100644 index 0000000..1552307 --- /dev/null +++ b/tests/Dummies/FallbackResolvers/DummyUserFallbackResolver.php @@ -0,0 +1,22 @@ +propertyName, $rawData)) { + return $rawData[$property->propertyName]; + } + + return new UserData( + 'John', + 'Doe' + ); + } +} diff --git a/tests/Dummies/PointData.php b/tests/Dummies/PointData.php new file mode 100644 index 0000000..6468ebb --- /dev/null +++ b/tests/Dummies/PointData.php @@ -0,0 +1,14 @@ + $points + */ + public function __construct( + #[CipherTarget] + public string $key, + #[ArrayOfData(PointData::class)] + #[DefaultsTo(DummyPointsFallbackResolver::class)] + public array $points + ) { + } +} diff --git a/tests/Unit/Attributes/AliasesAttributeTest.php b/tests/Unit/Attributes/AliasesTest.php similarity index 89% rename from tests/Unit/Attributes/AliasesAttributeTest.php rename to tests/Unit/Attributes/AliasesTest.php index bc15c68..fff56e3 100644 --- a/tests/Unit/Attributes/AliasesAttributeTest.php +++ b/tests/Unit/Attributes/AliasesTest.php @@ -4,7 +4,7 @@ use Nuxtifyts\PhpDto\Attributes\Property\Aliases; use Nuxtifyts\PhpDto\Contexts\PropertyContext; -use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\ResolveValuesFromAliasesPipe; +use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\ResolveValuesFromAliasesPipe; use Nuxtifyts\PhpDto\Tests\Dummies\PersonData; use Nuxtifyts\PhpDto\Tests\Unit\UnitCase; use PHPUnit\Framework\Attributes\CoversClass; @@ -16,7 +16,7 @@ #[CoversClass(ResolveValuesFromAliasesPipe::class)] #[CoversClass(Aliases::class)] #[UsesClass(PersonData::class)] -final class AliasesAttributeTest extends UnitCase +final class AliasesTest extends UnitCase { /** * @throws Throwable diff --git a/tests/Unit/Attributes/CipherTargetTest.php b/tests/Unit/Attributes/CipherTargetTest.php index cb62ed8..cb320c0 100644 --- a/tests/Unit/Attributes/CipherTargetTest.php +++ b/tests/Unit/Attributes/CipherTargetTest.php @@ -11,7 +11,7 @@ use Nuxtifyts\PhpDto\DataCiphers\DefaultDataCipher; use Nuxtifyts\PhpDto\Exceptions\DataCipherException; use Nuxtifyts\PhpDto\Exceptions\DeserializeException; -use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DecipherDataPipe; +use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\DecipherDataPipe; use Nuxtifyts\PhpDto\Tests\Dummies\PersonData; use Nuxtifyts\PhpDto\Tests\Unit\UnitCase; use PHPUnit\Framework\Attributes\CoversClass; diff --git a/tests/Unit/Attributes/DefaultsToTest.php b/tests/Unit/Attributes/DefaultsToTest.php new file mode 100644 index 0000000..c6c7ead --- /dev/null +++ b/tests/Unit/Attributes/DefaultsToTest.php @@ -0,0 +1,145 @@ + $arrayData + * @param array $expectedSerializedData + * + * @throws Throwable + */ + #[Test] + #[DataProvider('should_be_able_to_resolve_default_values_data_provider')] + public function should_be_able_to_resolve_default_values( + Data $object, + array $arrayData, + array $expectedSerializedData + ): void { + self::assertEquals( + $expectedSerializedData, + $object::from($arrayData)->toArray() + ); + } + + /** + * @return array + */ + public static function should_be_able_to_resolve_default_values_data_provider(): array + { + return [ + 'Resolves default scalar type value' => [ + 'object' => new readonly class ('') extends Data { + public function __construct( + #[DefaultsTo('John')] + public string $name + ) { + } + }, + 'arrayData' => [], + 'expectedSerializedData' => [ + 'name' => 'John' + ] + ], + 'Resolves default complex type value' => [ + 'object' => new readonly class ( + new UserData('John', 'Doe') + ) extends Data { + public function __construct( + #[DefaultsTo(DummyUserFallbackResolver::class)] + public UserData $userData + ) { + } + }, + 'arrayData' => [], + 'expectedSerializedData' => [ + 'userData' => [ + 'firstName' => 'John', + 'lastName' => 'Doe' + ] + ] + ], + 'Resolves array of scalar type values' => [ + 'object' => new readonly class([]) extends Data { + /** + * @param list $names + */ + public function __construct( + #[ArrayOfScalarTypes(Type::STRING)] + #[DefaultsTo(['John', 'Jane'])] + public array $names + ) { + } + }, + 'arrayData' => [], + 'expectedSerializedData' => [ + 'names' => ['John', 'Jane'] + ] + ], + 'Allows pure php way of defaulting 1' => [ + 'object' => new readonly class ('') extends Data { + public function __construct( + public string $name = 'John' + ) { + } + }, + 'arrayData' => [], + 'expectedSerializedData' => [ + 'name' => 'John' + ] + ], + 'Allows pure php way of defaulting 2' => [ + 'object' => new readonly class([]) extends Data { + /** + * @param list $names + */ + public function __construct( + #[ArrayOfScalarTypes(Type::STRING)] + public array $names = ['John', 'Jane'] + ) { + } + }, + 'arrayData' => [], + 'expectedSerializedData' => [ + 'names' => ['John', 'Jane'] + ] + ], + ]; + } +} diff --git a/tests/Unit/Concerns/BaseDataTest.php b/tests/Unit/Concerns/BaseDataTest.php index 26fb957..06f45e3 100644 --- a/tests/Unit/Concerns/BaseDataTest.php +++ b/tests/Unit/Concerns/BaseDataTest.php @@ -3,11 +3,13 @@ namespace Nuxtifyts\PhpDto\Tests\Unit\Concerns; use DateTimeImmutable; +use DateTimeInterface; use Nuxtifyts\PhpDto\Data; use Nuxtifyts\PhpDto\Exceptions\DeserializeException; use Nuxtifyts\PhpDto\Exceptions\SerializeException; +use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipeline; use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\DeserializePipelinePassable; -use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\RefineDataPipe; +use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\RefineDataPipe; use Nuxtifyts\PhpDto\Serializers\BackedEnumSerializer; use Nuxtifyts\PhpDto\Serializers\DataSerializer; use Nuxtifyts\PhpDto\Serializers\DateTimeSerializer; @@ -19,23 +21,25 @@ use Nuxtifyts\PhpDto\Tests\Dummies\CoordinatesData; use Nuxtifyts\PhpDto\Tests\Dummies\CountryData; use Nuxtifyts\PhpDto\Tests\Dummies\Enums\YesNoBackedEnum; +use Nuxtifyts\PhpDto\Tests\Dummies\FallbackResolvers\DummyPointsFallbackResolver; use Nuxtifyts\PhpDto\Tests\Dummies\InvitationData; +use Nuxtifyts\PhpDto\Tests\Dummies\PersonData; +use Nuxtifyts\PhpDto\Tests\Dummies\PointData; +use Nuxtifyts\PhpDto\Tests\Dummies\PointGroupData; use Nuxtifyts\PhpDto\Tests\Dummies\RefundableItemData; use Nuxtifyts\PhpDto\Tests\Dummies\UnionMultipleComplexData; use Nuxtifyts\PhpDto\Tests\Dummies\UnionMultipleTypeData; +use Nuxtifyts\PhpDto\Tests\Dummies\UnionTypedData; use Nuxtifyts\PhpDto\Tests\Dummies\UserData; use Nuxtifyts\PhpDto\Tests\Dummies\UserGroupData; use Nuxtifyts\PhpDto\Tests\Dummies\UserLocationData; use Nuxtifyts\PhpDto\Tests\Dummies\YesOrNoData; use Nuxtifyts\PhpDto\Tests\Unit\UnitCase; -use Nuxtifyts\PhpDto\Tests\Dummies\PersonData; -use Nuxtifyts\PhpDto\Tests\Dummies\UnionTypedData; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use Throwable; -use DateTimeInterface; #[CoversClass(Data::class)] #[CoversClass(DeserializeException::class)] @@ -43,6 +47,7 @@ #[CoversClass(Pipeline::class)] #[CoversClass(Pipe::class)] #[CoversClass(DeserializePipelinePassable::class)] +#[CoversClass(DeserializePipeline::class)] #[CoversClass(RefineDataPipe::class)] #[CoversClass(Passable::class)] #[CoversClass(DateTimeSerializer::class)] @@ -61,6 +66,9 @@ #[UsesClass(UserLocationData::class)] #[UsesClass(UserGroupData::class)] #[UsesClass(ComputedPropertiesData::class)] +#[UsesClass(PointGroupData::class)] +#[UsesClass(PointData::class)] +#[UsesClass(DummyPointsFallbackResolver::class)] final class BaseDataTest extends UnitCase { /** @@ -556,4 +564,33 @@ public static function will_perform_serialization_and_deserialization_data_provi ] ]; } + + /** + * @throws Throwable + */ + #[Test] + public function will_be_able_to_create_an_instance_using_create(): void + { + $point = PointData::create( + x: 1, + y: 2 + ); + + self::assertInstanceOf(PointData::class, $point); + self::assertEquals(1, $point->x); + self::assertEquals(2, $point->y); + + // Make sure we skip deciphering the key + $pointGroup = PointGroupData::create( + key: 'random-key' + ); + + self::assertInstanceOf(PointGroupData::class, $pointGroup); + self::assertEquals('random-key', $pointGroup->key); + self::assertEquals([ + new PointData(1, 2), + new PointData(3, 4), + new PointData(5, 6) + ], $pointGroup->points); + } } diff --git a/tests/Unit/DataRefiners/DateTimeRefinerTest.php b/tests/Unit/DataRefiners/DateTimeRefinerTest.php index 6e3ea6c..f655e01 100644 --- a/tests/Unit/DataRefiners/DateTimeRefinerTest.php +++ b/tests/Unit/DataRefiners/DateTimeRefinerTest.php @@ -2,12 +2,15 @@ namespace Nuxtifyts\PhpDto\Tests\Unit\DataRefiners; +use DateTimeImmutable; +use DateTimeInterface; use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner; use Nuxtifyts\PhpDto\Contexts\PropertyContext; use Nuxtifyts\PhpDto\Data; use Nuxtifyts\PhpDto\DataRefiners\DateTimeRefiner; +use Nuxtifyts\PhpDto\Exceptions\DeserializeException; use Nuxtifyts\PhpDto\Exceptions\InvalidRefiner; -use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\RefineDataPipe; +use Nuxtifyts\PhpDto\Pipelines\DeserializePipeline\Pipes\RefineDataPipe; use Nuxtifyts\PhpDto\Serializers\DateTimeSerializer; use Nuxtifyts\PhpDto\Tests\Unit\UnitCase; use PHPUnit\Framework\Attributes\CoversClass; @@ -15,8 +18,6 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use ReflectionProperty; -use DateTimeInterface; -use DateTimeImmutable; use Throwable; #[CoversClass(DateTimeRefiner::class)] @@ -160,4 +161,27 @@ public function __construct( self::assertInstanceOf(DateTimeImmutable::class, $object2->date); self::assertEquals($now->format('Y-m-d'), $object2->date->format('Y-m-d')); } + + /** + * @throws Throwable + */ + #[Test] + public function will_fail_to_refine_value_if_wrong_value_is_used(): void + { + $object = new readonly class (null) extends Data { + public function __construct( + #[WithRefiner(DateTimeRefiner::class)] + public ?DateTimeImmutable $date + ) { + } + }; + + $now = new DateTimeImmutable(); + + self::expectException(DeserializeException::class); + + $object::from([ + 'date' => $now->format('Y/m-d') + ]); + } }