From f4c27358303614580b79e3f50439c698c6907835 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Thu, 26 Dec 2024 20:56:15 -0500 Subject: [PATCH 1/9] Added empty data contract --- src/Contracts/EmptyData.php | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/Contracts/EmptyData.php diff --git a/src/Contracts/EmptyData.php b/src/Contracts/EmptyData.php new file mode 100644 index 0000000..743afd4 --- /dev/null +++ b/src/Contracts/EmptyData.php @@ -0,0 +1,8 @@ + Date: Fri, 27 Dec 2024 17:07:39 -0500 Subject: [PATCH 2/9] Added ability to create empty data of dto --- composer.json | 2 +- src/Concerns/BaseData.php | 11 +- src/Concerns/EmptyData.php | 26 +++ src/Contexts/ClassContext.php | 33 +++- src/Contexts/PropertyContext.php | 65 ++++++++ src/Contexts/TypeContext.php | 2 +- src/Contracts/EmptyData.php | 5 + src/Data.php | 7 +- src/Exceptions/DataCreationException.php | 12 ++ src/Exceptions/UnsupportedTypeException.php | 31 +++- tests/Unit/Concerns/EmptyDataTest.php | 172 ++++++++++++++++++++ 11 files changed, 349 insertions(+), 17 deletions(-) create mode 100644 src/Concerns/EmptyData.php create mode 100644 tests/Unit/Concerns/EmptyDataTest.php diff --git a/composer.json b/composer.json index ff236c9..a09f12f 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,6 @@ }, "scripts": { "ci-test": "XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite=ci --configuration phpunit.xml", - "phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon" + "phpstan": "vendor/bin/phpstan analyse --configuration phpstan.neon --memory-limit=256M" } } diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 634ebb8..285314f 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -16,6 +16,9 @@ trait BaseData { use HasNormalizers; + /** + * @throws DataCreationException + */ final public static function create(mixed ...$args): static { if (array_any( @@ -124,13 +127,7 @@ protected static function instanceWithConstructorCallFrom(ClassContext $context, $args[$paramName] = $propertyContext->deserializeFrom($value); } - $instance = $context->newInstanceWithConstructorCall(...$args); - - if (!$instance instanceof static) { - throw new DeserializeException('Could not create instance of ' . static::class); - } - - return $instance; + return $context->newInstanceWithConstructorCall(...$args); } /** diff --git a/src/Concerns/EmptyData.php b/src/Concerns/EmptyData.php new file mode 100644 index 0000000..a50036c --- /dev/null +++ b/src/Concerns/EmptyData.php @@ -0,0 +1,26 @@ + $classContext */ + $classContext = ClassContext::getInstance(new ReflectionClass(static::class)); + + return $classContext->emptyValue(); + } catch (Throwable $t) { + throw DataCreationException::unableToCreateEmptyInstance(static::class, $t); + } + } +} diff --git a/src/Contexts/ClassContext.php b/src/Contexts/ClassContext.php index 7f82f1f..7444a1f 100644 --- a/src/Contexts/ClassContext.php +++ b/src/Contexts/ClassContext.php @@ -2,13 +2,15 @@ namespace Nuxtifyts\PhpDto\Contexts; +use Nuxtifyts\PhpDto\Data; +use Nuxtifyts\PhpDto\Exceptions\DataCreationException; use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException; -use ReflectionClass; use ReflectionException; use ReflectionParameter; +use ReflectionClass; /** - * @template T of object + * @template T of Data */ class ClassContext { @@ -101,9 +103,36 @@ public function newInstanceWithoutConstructor(): mixed /** * @throws ReflectionException + * + * @return T */ public function newInstanceWithConstructorCall(mixed ...$args): mixed { return $this->reflection->newInstance(...$args); } + + /** + * @return T + * + * @throws ReflectionException + * @throws UnsupportedTypeException + * @throws DataCreationException + */ + public function emptyValue(): mixed + { + /** @var array $args */ + $args = []; + + foreach ($this->constructorParams as $paramName) { + $propertyContext = $this->properties[$paramName] ?? null; + + if (!$propertyContext) { + throw DataCreationException::invalidProperty(); + } + + $args[$paramName] = $propertyContext->emptyValue(); + } + + return $this->newInstanceWithConstructorCall(...$args); + } } diff --git a/src/Contexts/PropertyContext.php b/src/Contexts/PropertyContext.php index 54200c1..3619ae0 100644 --- a/src/Contexts/PropertyContext.php +++ b/src/Contexts/PropertyContext.php @@ -2,11 +2,15 @@ namespace Nuxtifyts\PhpDto\Contexts; +use BackedEnum; +use Nuxtifyts\PhpDto\Exceptions\DataCreationException; +use UnitEnum; 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\Data; use Nuxtifyts\PhpDto\DataCiphers\CipherConfig; use Nuxtifyts\PhpDto\DataRefiners\DataRefiner; use Nuxtifyts\PhpDto\Enums\Property\Type; @@ -18,8 +22,12 @@ use Nuxtifyts\PhpDto\Serializers\Serializer; use Nuxtifyts\PhpDto\Support\Traits\HasSerializers; use Nuxtifyts\PhpDto\Support\Traits\HasTypes; +use DateTimeInterface; +use ReflectionEnum; +use ReflectionException; use ReflectionProperty; use ReflectionAttribute; +use ReflectionClass; use Exception; class PropertyContext @@ -225,4 +233,61 @@ public function serializeFrom(object $object): array throw new SerializeException('Could not serialize value for property: ' . $this->propertyName); } } + + /** + * @throws UnsupportedTypeException + * @throws ReflectionException + * @throws DataCreationException + */ + public function emptyValue(): mixed + { + if ($this->isNullable) { + return null; + } + + if (! $typeContext = $this->typeContexts[0] ?? null) { + throw UnsupportedTypeException::emptyType(); + } + + switch (true) { + case $typeContext->type === Type::STRING: + return ''; + + case $typeContext->type === Type::INT: + return 0; + + case $typeContext->type === Type::FLOAT: + return 0.0; + + case $typeContext->type === Type::BOOLEAN: + return false; + + case $typeContext->type === Type::ARRAY: + return []; + + case $typeContext->type === Type::DATA: + /** @var null|ReflectionClass $reflection */ + $reflection = $typeContext->reflection; + + return !$reflection + ? throw UnsupportedTypeException::invalidReflection() + : ClassContext::getInstance($reflection)->emptyValue(); + + case $typeContext->type === Type::BACKED_ENUM: + /** @var null|ReflectionEnum $reflection */ + $reflection = $typeContext->reflection; + + return $reflection instanceof ReflectionEnum && $reflection->isBacked() + ? $reflection->getCases()[0]->getValue() + : throw UnsupportedTypeException::invalidReflection(); + + default: + /** @var null|DateTimeInterface $dateTime */ + $dateTime = $typeContext->reflection?->newInstance(); + + return $dateTime instanceof DateTimeInterface + ? $dateTime + : throw UnsupportedTypeException::invalidReflection(); + } + } } diff --git a/src/Contexts/TypeContext.php b/src/Contexts/TypeContext.php index 338ec09..4e502a1 100644 --- a/src/Contexts/TypeContext.php +++ b/src/Contexts/TypeContext.php @@ -109,7 +109,7 @@ public static function getInstances(PropertyContext $property): array ); break; default: - throw UnsupportedTypeException::from($type); + throw UnsupportedTypeException::unknownType($type); } } diff --git a/src/Contracts/EmptyData.php b/src/Contracts/EmptyData.php index 743afd4..0fc80f6 100644 --- a/src/Contracts/EmptyData.php +++ b/src/Contracts/EmptyData.php @@ -2,7 +2,12 @@ namespace Nuxtifyts\PhpDto\Contracts; +use Nuxtifyts\PhpDto\Exceptions\DataCreationException; + interface EmptyData { + /** + * @throws DataCreationException + */ public static function empty(): static; } diff --git a/src/Data.php b/src/Data.php index 3ca9164..9d6b125 100644 --- a/src/Data.php +++ b/src/Data.php @@ -3,12 +3,17 @@ namespace Nuxtifyts\PhpDto; use Nuxtifyts\PhpDto\Contracts\BaseData as BaseDataContract; +use Nuxtifyts\PhpDto\Contracts\EmptyData as EmptyDataContract; use Nuxtifyts\PhpDto\Concerns\BaseData; +use Nuxtifyts\PhpDto\Concerns\EmptyData; use Nuxtifyts\PhpDto\Exceptions\SerializeException; -abstract readonly class Data implements BaseDataContract +abstract readonly class Data implements + BaseDataContract, + EmptyDataContract { use BaseData; + use EmptyData; /** * @return array diff --git a/src/Exceptions/DataCreationException.php b/src/Exceptions/DataCreationException.php index e383ccd..9a8e3fc 100644 --- a/src/Exceptions/DataCreationException.php +++ b/src/Exceptions/DataCreationException.php @@ -9,6 +9,7 @@ class DataCreationException extends Exception { protected const int UNABLE_TO_CREATE_INSTANCE = 0; protected const int INVALID_PROPERTY = 1; + protected const int UNABLE_TO_CREATE_EMPTY_INSTANCE = 2; public static function unableToCreateInstance( string $class, @@ -28,4 +29,15 @@ public static function invalidProperty(): self code: self::INVALID_PROPERTY ); } + + public static function unableToCreateEmptyInstance( + string $class, + ?Throwable $previous = null + ): self { + return new self( + message: "Unable to create empty instance of class {$class}", + code: self::UNABLE_TO_CREATE_EMPTY_INSTANCE, + previous: $previous + ); + } } diff --git a/src/Exceptions/UnsupportedTypeException.php b/src/Exceptions/UnsupportedTypeException.php index f33a400..9604b18 100644 --- a/src/Exceptions/UnsupportedTypeException.php +++ b/src/Exceptions/UnsupportedTypeException.php @@ -6,15 +6,36 @@ class UnsupportedTypeException extends Exception { - final protected function __construct(string $message) + protected const int UNKNOWN_TYPE = 0; + protected const int EMPTY_TYPE = 1; + protected const int INVALID_REFLECTION = 2; + protected const int INVALID_TYPE = 3; + + public static function unknownType(?string $type = null): self + { + return new self( + 'Unknown type' . ($type ? " '{$type}'" : ''), + ); + } + + public static function emptyType(): self { - parent::__construct($message); + return new self( + 'Got empty type', + ); + } + + public static function invalidReflection(): self + { + return new self( + 'Invalid reflection for type', + ); } - public static function from(string $type): static + public static function invalidType(): self { - return new static( - "Unknown type '{$type}'" + return new self( + 'Invalid type', ); } } diff --git a/tests/Unit/Concerns/EmptyDataTest.php b/tests/Unit/Concerns/EmptyDataTest.php new file mode 100644 index 0000000..344186b --- /dev/null +++ b/tests/Unit/Concerns/EmptyDataTest.php @@ -0,0 +1,172 @@ +|EmptyDataContract $object + * @param array $expectedEmptyValues + * + * @throws Throwable + */ + #[Test] + #[DataProvider('will_be_able_to_create_empty_data_provider')] + public function will_be_able_to_create_empty_data( + EmptyDataContract|string $object, + array $expectedEmptyValues + ): void { + $emptyData = $object::empty(); + + foreach ($expectedEmptyValues as $property => $value) { + self::assertObjectHasProperty($property, $emptyData); + + if ($value instanceof DateTimeInterface) { + self::assertInstanceOf($value::class, $emptyData->{$property}); + self::assertEquals( + $value->format(DateTimeInterface::ATOM), + $emptyData->{$property}->format(DateTimeInterface::ATOM) + ); + } else { + self::assertEquals($value, $emptyData->{$property}); + } + } + } + + /** + * @return array + */ + public static function will_be_able_to_create_empty_data_provider(): array + { + return [ + 'Will default to null if property is nullable' => [ + 'object' => new readonly class ('') extends Data { + public function __construct( + public ?string $value + ) { + } + }, + 'expectedEmptyValues' => [ + 'value' => null + ] + ], + 'Will be able to create empty data with scalar types' => [ + 'object' => new readonly class ('', 0, 0.0, false, null) extends Data { + public function __construct( + public string $value, + public int $number, + public float $float, + public bool $bool, + public ?string $nullableString = null + ) { + } + }, + 'expectedEmptyValues' => [ + 'value' => '', + 'number' => 0, + 'float' => 0.0, + 'bool' => false, + 'nullableString' => null + ] + ], + 'Will be able to create empty data with array types' => [ + 'object' => new readonly class([], []) extends Data { + /** + * @param array $arrayOfIntegersOrFloats + * @param ?array $nullableArrayOfIntegers + */ + public function __construct( + #[ArrayOfScalarTypes([Type::INT, Type::FLOAT])] + public array $arrayOfIntegersOrFloats, + #[ArrayOfScalarTypes([Type::INT])] + public ?array $nullableArrayOfIntegers + ) { + } + }, + 'expectedEmptyValues' => [ + 'arrayOfIntegersOrFloats' => [], + 'nullableArrayOfIntegers' => null + ] + ], + 'Will be able to create empty data with data types array' => [ + 'object' => PointGroupData::class, + 'expectedEmptyValues' => [ + 'key' => '', + 'points' => [] + ] + ], + 'Will be able to create empty data with data types direct data type' => [ + 'object' => AddressData::class, + 'expectedEmptyValues' => [ + 'street' => '', + 'city' => '', + 'state' => '', + 'zip' => '', + 'country' => new CountryData('', ''), + 'coordinates' => null + ] + ], + 'will take first value of backed enum when calling empty' => [ + 'object' => YesOrNoData::class, + 'expectedEmptyValues' => [ + 'yesNo' => YesNoBackedEnum::YES, + ] + ], + 'Will default to a new instance for date time type' => [ + 'object' => new readonly class (new DateTime()) extends Data { + public function __construct( + public DateTime $dateTime + ) { + } + }, + 'expectedEmptyValues' => [ + 'dateTime' => new DateTime() + ] + ], + 'Will default to a new instance for date time immutable type' => [ + 'object' => new readonly class (new DateTimeImmutable()) extends Data { + public function __construct( + public DateTimeImmutable $dateTimeImmutable + ) { + } + }, + 'expectedEmptyValues' => [ + 'dateTimeImmutable' => new DateTimeImmutable() + ] + ], + ]; + } +} From 3e2cdf519c762e8b8e0101896fdddaf13cf4ec22 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 27 Dec 2024 17:11:46 -0500 Subject: [PATCH 3/9] Moving functions from Data to base data --- src/Concerns/BaseData.php | 13 +++++++++++++ src/Contracts/BaseData.php | 14 ++++++++++++++ src/Data.php | 16 ---------------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 285314f..0475adb 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -156,4 +156,17 @@ final public function jsonSerialize(): array throw new SerializeException($e->getMessage(), $e->getCode(), $e); } } + + /** + * @throws SerializeException + */ + final public function toArray(): array + { + return $this->jsonSerialize(); + } + + final public function toJson(): false|string + { + return json_encode($this); + } } diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 896a834..07d89ea 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -21,6 +21,20 @@ public static function create(mixed ...$args): static; */ public function jsonSerialize(): array; + /** + * @return array + * + * @throws SerializeException + */ + public function toArray(): array; + + /** + * @return false|string + * + * @throws SerializeException + */ + public function toJson(): false|string; + /** * @throws DeserializeException */ diff --git a/src/Data.php b/src/Data.php index 9d6b125..cecab75 100644 --- a/src/Data.php +++ b/src/Data.php @@ -6,7 +6,6 @@ use Nuxtifyts\PhpDto\Contracts\EmptyData as EmptyDataContract; use Nuxtifyts\PhpDto\Concerns\BaseData; use Nuxtifyts\PhpDto\Concerns\EmptyData; -use Nuxtifyts\PhpDto\Exceptions\SerializeException; abstract readonly class Data implements BaseDataContract, @@ -14,19 +13,4 @@ { use BaseData; use EmptyData; - - /** - * @return array - * - * @throws SerializeException - */ - final public function toArray(): array - { - return $this->jsonSerialize(); - } - - final public function toJson(): false|string - { - return json_encode($this); - } } From 31c12d7084e8755757001374a74fcd0bad44c588 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 27 Dec 2024 17:13:38 -0500 Subject: [PATCH 4/9] Adjusted empty data test --- tests/Unit/Concerns/EmptyDataTest.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Unit/Concerns/EmptyDataTest.php b/tests/Unit/Concerns/EmptyDataTest.php index 344186b..5ed0a32 100644 --- a/tests/Unit/Concerns/EmptyDataTest.php +++ b/tests/Unit/Concerns/EmptyDataTest.php @@ -5,7 +5,6 @@ use Nuxtifyts\PhpDto\Attributes\Property\Types\ArrayOfScalarTypes; use Nuxtifyts\PhpDto\Contexts\ClassContext; use Nuxtifyts\PhpDto\Contexts\PropertyContext; -use Nuxtifyts\PhpDto\Concerns\EmptyData; use Nuxtifyts\PhpDto\Contracts\EmptyData as EmptyDataContract; use Nuxtifyts\PhpDto\Data; use Nuxtifyts\PhpDto\Enums\Property\Type; @@ -26,7 +25,6 @@ use Throwable; #[CoversClass(Data::class)] -#[CoversClass(EmptyData::class)] #[CoversClass(PropertyContext::class)] #[CoversClass(ClassContext::class)] #[UsesClass(ArrayOfScalarTypes::class)] @@ -57,8 +55,8 @@ public function will_be_able_to_create_empty_data( if ($value instanceof DateTimeInterface) { self::assertInstanceOf($value::class, $emptyData->{$property}); self::assertEquals( - $value->format(DateTimeInterface::ATOM), - $emptyData->{$property}->format(DateTimeInterface::ATOM) + $value->format('Y-m-d H:i'), + $emptyData->{$property}->format('Y-m-d H:i') ); } else { self::assertEquals($value, $emptyData->{$property}); From fc3db88d982c9dcf7d5290e3733499c76a411d95 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 27 Dec 2024 17:19:10 -0500 Subject: [PATCH 5/9] Code cleanup --- src/Exceptions/FallbackResolverException.php | 9 -------- tests/Unit/Concerns/EmptyDataTest.php | 22 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Exceptions/FallbackResolverException.php b/src/Exceptions/FallbackResolverException.php index a91c322..8600299 100644 --- a/src/Exceptions/FallbackResolverException.php +++ b/src/Exceptions/FallbackResolverException.php @@ -7,7 +7,6 @@ class FallbackResolverException extends Exception { protected const int UNABLE_TO_FIND_RESOLVER_CLASS = 0; - protected const int UNABLE_TO_RESOLVE_DEFAULT_VALUE = 1; public static function unableToFindResolverClass(string $resolverClass): self { @@ -16,12 +15,4 @@ public static function unableToFindResolverClass(string $resolverClass): self self::UNABLE_TO_FIND_RESOLVER_CLASS ); } - - public static function unableToResolveDefaultValue(): self - { - return new self( - 'Unable to resolve default value', - self::UNABLE_TO_RESOLVE_DEFAULT_VALUE - ); - } } diff --git a/tests/Unit/Concerns/EmptyDataTest.php b/tests/Unit/Concerns/EmptyDataTest.php index 5ed0a32..dc13741 100644 --- a/tests/Unit/Concerns/EmptyDataTest.php +++ b/tests/Unit/Concerns/EmptyDataTest.php @@ -8,6 +8,7 @@ use Nuxtifyts\PhpDto\Contracts\EmptyData as EmptyDataContract; use Nuxtifyts\PhpDto\Data; use Nuxtifyts\PhpDto\Enums\Property\Type; +use Nuxtifyts\PhpDto\Exceptions\DataCreationException; use Nuxtifyts\PhpDto\Tests\Dummies\AddressData; use Nuxtifyts\PhpDto\Tests\Dummies\CountryData; use Nuxtifyts\PhpDto\Tests\Dummies\Enums\YesNoBackedEnum; @@ -27,6 +28,7 @@ #[CoversClass(Data::class)] #[CoversClass(PropertyContext::class)] #[CoversClass(ClassContext::class)] +#[CoversClass(DataCreationException::class)] #[UsesClass(ArrayOfScalarTypes::class)] #[UsesClass(PointGroupData::class)] #[UsesClass(PointData::class)] @@ -167,4 +169,24 @@ public function __construct( ], ]; } + + /** + * @throws Throwable + */ + #[Test] + public function will_throw_exception_when_parameter_is_not_linked_to_a_property(): void + { + $object = new readonly class('') extends Data { + public string $otherPropertyName; + + public function __construct( + string $parameterName + ) { + $this->otherPropertyName = $parameterName; + } + }; + + self::expectException(DataCreationException::class); + $object::empty(); + } } From 1c09a08abd12944d22ec5ad2d903a7e7b50f98ad Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 27 Dec 2024 17:29:10 -0500 Subject: [PATCH 6/9] Adjustment for property context --- src/Exceptions/UnknownTypeException.php | 25 ++++++++++++--------- src/Support/Traits/HasSerializers.php | 2 +- tests/Unit/Contexts/PropertyContextTest.php | 21 ++++++++++++++++- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/Exceptions/UnknownTypeException.php b/src/Exceptions/UnknownTypeException.php index 0213406..528fb88 100644 --- a/src/Exceptions/UnknownTypeException.php +++ b/src/Exceptions/UnknownTypeException.php @@ -7,19 +7,22 @@ class UnknownTypeException extends Exception { - final protected function __construct(string $message = "") - { - parent::__construct($message); - } + protected const int UNKNOWN_TYPE_EXCEPTION_CODE = 0; - public static function from( - Type $type, - Type ...$additionalTypes - ): static { - $types = implode(', ', array_column([$type, ...$additionalTypes], 'value')); + public static function unknownType(Type $type, Type ...$types): self + { + $types = array_map( + static fn (Type $type): string => $type->value, + $types + ); - return new static( - "Unknown type '{$types}'" + return new self( + sprintf( + 'Unknown type "%s". Known types are: %s.', + $type->value, + implode(', ', $types) + ), + self::UNKNOWN_TYPE_EXCEPTION_CODE ); } } diff --git a/src/Support/Traits/HasSerializers.php b/src/Support/Traits/HasSerializers.php index 844434f..d379e3d 100644 --- a/src/Support/Traits/HasSerializers.php +++ b/src/Support/Traits/HasSerializers.php @@ -36,7 +36,7 @@ protected function getSerializersFromPropertyContext( array_column($serializer::supportedTypes(), 'value') )) ? new $serializer() : null, self::serializersList() - ))) ?: throw UnknownTypeException::from(...$propertyContext->types); + ))) ?: throw UnknownTypeException::unknownType(...$propertyContext->types); } /** diff --git a/tests/Unit/Contexts/PropertyContextTest.php b/tests/Unit/Contexts/PropertyContextTest.php index 015ec45..6bee1df 100644 --- a/tests/Unit/Contexts/PropertyContextTest.php +++ b/tests/Unit/Contexts/PropertyContextTest.php @@ -4,12 +4,12 @@ use Nuxtifyts\PhpDto\Attributes\Property\Computed; use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner; -use Nuxtifyts\PhpDto\Contexts\ClassContext; use Nuxtifyts\PhpDto\Contexts\PropertyContext; use Nuxtifyts\PhpDto\Contexts\TypeContext; use Nuxtifyts\PhpDto\Data; use Nuxtifyts\PhpDto\DataRefiners\DateTimeRefiner; use Nuxtifyts\PhpDto\Enums\Property\Type; +use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException; use Nuxtifyts\PhpDto\Serializers\ScalarTypeSerializer; use Nuxtifyts\PhpDto\Tests\Dummies\ComputedPropertiesData; use Nuxtifyts\PhpDto\Tests\Dummies\Enums\YesNoBackedEnum; @@ -31,6 +31,7 @@ #[CoversClass(TypeContext::class)] #[CoversClass(Computed::class)] #[CoversClass(WithRefiner::class)] +#[CoversClass(UnsupportedTypeException::class)] #[UsesClass(ComputedPropertiesData::class)] #[UsesClass(ScalarTypeSerializer::class)] #[UsesClass(PersonData::class)] @@ -80,6 +81,24 @@ public function can_resolve_serializers_of_property(): void self::assertInstanceOf(ScalarTypeSerializer::class, $serializers[0]); } + /** + * @throws Throwable + */ + #[Test] + public function will_throw_an_exception_if_property_type_is_not_supported(): void + { + $object = new readonly class ('') extends Data { + public function __construct( + public mixed $value + ) { + } + }; + + $reflectionProperty = new ReflectionProperty($object::class, 'value'); + self::expectException(UnsupportedTypeException::class); + PropertyContext::getInstance($reflectionProperty); + } + /** * @throws Throwable */ From cd4c347eef8dd9823a84c7eb35276bf90fb5e57a Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 27 Dec 2024 17:41:43 -0500 Subject: [PATCH 7/9] Increased code coverage with tests --- .../Serializers/HasSerializersDummyClass.php | 39 +++++++++++++++++++ tests/Unit/Concerns/BaseDataTest.php | 26 +++++++++++++ tests/Unit/Contexts/PropertyContextTest.php | 24 ++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 tests/Dummies/Serializers/HasSerializersDummyClass.php diff --git a/tests/Dummies/Serializers/HasSerializersDummyClass.php b/tests/Dummies/Serializers/HasSerializersDummyClass.php new file mode 100644 index 0000000..7980a8d --- /dev/null +++ b/tests/Dummies/Serializers/HasSerializersDummyClass.php @@ -0,0 +1,39 @@ + + */ + public static function testGetSerializersFromPropertyContext(PropertyContext $propertyContext): array + { + return new self()->getSerializersFromPropertyContext($propertyContext); + } + + /** + * @return list + */ + protected static function serializersList(): array + { + return []; + } + + /** + * @return list + */ + protected function resolveSerializers(): array + { + return []; + } +} diff --git a/tests/Unit/Concerns/BaseDataTest.php b/tests/Unit/Concerns/BaseDataTest.php index 06f45e3..281e9bd 100644 --- a/tests/Unit/Concerns/BaseDataTest.php +++ b/tests/Unit/Concerns/BaseDataTest.php @@ -4,6 +4,7 @@ use DateTimeImmutable; use DateTimeInterface; +use Nuxtifyts\PhpDto\Attributes\Property\Computed; use Nuxtifyts\PhpDto\Data; use Nuxtifyts\PhpDto\Exceptions\DeserializeException; use Nuxtifyts\PhpDto\Exceptions\SerializeException; @@ -112,6 +113,31 @@ public function will_throw_an_exception_if_an_invalid_value_is_passed_to_from_fu PersonData::from(false); } + /** + * @throws Throwable + */ + #[Test] + public function will_throw_deserialization_exception_when_invalid_parameter_is_used_with_computed_properties(): void + { + $object = new readonly class ('a', 'b') extends Data { + #[Computed] + public string $c; + + public function __construct( + public string $a, + string $b + ) { + $this->c = $a . $b; + } + }; + + self::expectException(DeserializeException::class); + $object::from([ + 'a' => 'a', + 'b' => 'b' + ]); + } + /** * @throws Throwable */ diff --git a/tests/Unit/Contexts/PropertyContextTest.php b/tests/Unit/Contexts/PropertyContextTest.php index 6bee1df..4859d9d 100644 --- a/tests/Unit/Contexts/PropertyContextTest.php +++ b/tests/Unit/Contexts/PropertyContextTest.php @@ -2,6 +2,7 @@ namespace Nuxtifyts\PhpDto\Tests\Unit\Contexts; +use Nuxtifyts\PhpDto\Tests\Dummies\Serializers\HasSerializersDummyClass; use Nuxtifyts\PhpDto\Attributes\Property\Computed; use Nuxtifyts\PhpDto\Attributes\Property\WithRefiner; use Nuxtifyts\PhpDto\Contexts\PropertyContext; @@ -9,6 +10,7 @@ use Nuxtifyts\PhpDto\Data; use Nuxtifyts\PhpDto\DataRefiners\DateTimeRefiner; use Nuxtifyts\PhpDto\Enums\Property\Type; +use Nuxtifyts\PhpDto\Exceptions\UnknownTypeException; use Nuxtifyts\PhpDto\Exceptions\UnsupportedTypeException; use Nuxtifyts\PhpDto\Serializers\ScalarTypeSerializer; use Nuxtifyts\PhpDto\Tests\Dummies\ComputedPropertiesData; @@ -32,6 +34,7 @@ #[CoversClass(Computed::class)] #[CoversClass(WithRefiner::class)] #[CoversClass(UnsupportedTypeException::class)] +#[CoversClass(UnknownTypeException::class)] #[UsesClass(ComputedPropertiesData::class)] #[UsesClass(ScalarTypeSerializer::class)] #[UsesClass(PersonData::class)] @@ -39,6 +42,7 @@ #[UsesClass(CoordinatesData::class)] #[UsesClass(UnionMultipleTypeData::class)] #[UsesClass(PersonData::class)] +#[UsesClass(HasSerializersDummyClass::class)] final class PropertyContextTest extends UnitCase { /** @@ -99,6 +103,26 @@ public function __construct( PropertyContext::getInstance($reflectionProperty); } + /** + * @throws Throwable + */ + #[Test] + public function will_throw_and_exception_if_it_fails_to_resolve_a_serializer(): void + { + $object = new readonly class (1) extends Data { + public function __construct( + public int $value + ) { + } + }; + + $reflectionProperty = new ReflectionProperty($object::class, 'value'); + $propertyContext = PropertyContext::getInstance($reflectionProperty); + + self::expectException(UnknownTypeException::class); + HasSerializersDummyClass::testGetSerializersFromPropertyContext($propertyContext); + } + /** * @throws Throwable */ From c4162c27e5600949f52e8619a28d21f0ad941fd7 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 27 Dec 2024 17:53:33 -0500 Subject: [PATCH 8/9] Added documentation for empty data --- docs/EmptyData.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++ docs/Quickstart.md | 1 + 2 files changed, 61 insertions(+) create mode 100644 docs/EmptyData.md diff --git a/docs/EmptyData.md b/docs/EmptyData.md new file mode 100644 index 0000000..27e2c52 --- /dev/null +++ b/docs/EmptyData.md @@ -0,0 +1,60 @@ +Empty Data += + +Sometimes we may need to create a fresh instance of a DTO without any data, +and by default `Data` classes have the ability to create an `"empty"` instance: + +```php +use Nuxtifyts\PhpDto\Data; +use DateTimeImmutable; + +final reaconly class Todo extends Data +{ + public function __construct( + public string $title, + public string $content, + public Status $status, + public ?DateTimeImmutable $dueDate + ) {} +} +``` + +The `Status` enum is defined as follows: + +```php +enum Status: string +{ + case DEFAULT = 'default'; + case DONE = 'done'; + case CANCELED = 'canceled'; +} +``` + +By calling the `empty()` method, we can create a new instance of the `Todo` class with all properties set to `null`: + +```php +$emptyTodo = Todo::empty(); +``` + +The `$emptyTodo` variable will contain the following data: + +``` +[ + 'title' => '', + 'comtent' => '', + 'status' => Status::DEFAULT, + 'dueDate' => null +] +``` + +This is useful when we want to gradually fill in the data of a DTO instance, +here is a list of the empty values for each type: + +- `NULL`: `null` (Null takes priority over everything) +- `STRING`: `''` +- `INT`: `0` +- `FLOAT`: `0.0` +- `BOOLEAN`: `false` +- `ARRAY`: `[]` (Any type of array will default to an empty one) +- `DATETIME`: New instance of DateTime/DateTimeImmutable +- `BACKEDENUM`: First case of the enum diff --git a/docs/Quickstart.md b/docs/Quickstart.md index 1e3867f..e796df2 100644 --- a/docs/Quickstart.md +++ b/docs/Quickstart.md @@ -80,3 +80,4 @@ can be found here: - [Normalizers](https://github.com/nuxtifyts/php-dto/blob/main/docs/Normalizers.md) - [Property Attributes](https://github.com/nuxtifyts/php-dto/blob/main/docs/PropertyAttributes.md) - [Data Refiners](https://github.com/nuxtifyts/php-dto/blob/main/docs/DataRefiners.md) +- [Empty Data](https://github.com/nuxtifyts/php-dto/blob/main/docs/EmptyData.md) From 9745cdb8e11074859ea2b4b6e06213fced051ef6 Mon Sep 17 00:00:00 2001 From: Fa-BRAIK Date: Fri, 27 Dec 2024 17:59:24 -0500 Subject: [PATCH 9/9] Fixed phpstan issues --- src/Support/Traits/HasSerializers.php | 3 +++ tests/Dummies/Serializers/HasSerializersDummyClass.php | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Support/Traits/HasSerializers.php b/src/Support/Traits/HasSerializers.php index d379e3d..0d94c68 100644 --- a/src/Support/Traits/HasSerializers.php +++ b/src/Support/Traits/HasSerializers.php @@ -82,5 +82,8 @@ public function serializers(): array return $this->_serializers ??= $this->resolveSerializers(); } + /** + * @return list + */ abstract protected function resolveSerializers(): array; } diff --git a/tests/Dummies/Serializers/HasSerializersDummyClass.php b/tests/Dummies/Serializers/HasSerializersDummyClass.php index 7980a8d..2b7660f 100644 --- a/tests/Dummies/Serializers/HasSerializersDummyClass.php +++ b/tests/Dummies/Serializers/HasSerializersDummyClass.php @@ -4,6 +4,7 @@ use Nuxtifyts\PhpDto\Contexts\PropertyContext; use Nuxtifyts\PhpDto\Exceptions\UnknownTypeException; +use Nuxtifyts\PhpDto\Serializers\DateTimeSerializer; use Nuxtifyts\PhpDto\Serializers\Serializer; use Nuxtifyts\PhpDto\Support\Traits\HasSerializers; @@ -22,11 +23,13 @@ public static function testGetSerializersFromPropertyContext(PropertyContext $pr } /** - * @return list + * @return list> */ protected static function serializersList(): array { - return []; + return [ + DateTimeSerializer::class + ]; } /**