From 4d09b8cf79e8703a7427ceb6a6a6cc1782c52c8f Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 12:01:20 +0500 Subject: [PATCH 01/18] improve tests --- tests/Hydrator/Fixture/BarDto.php | 16 -- tests/Hydrator/Fixture/BarDtoCollection.php | 16 -- tests/Hydrator/Fixture/BazDto.php | 23 -- tests/Hydrator/Fixture/TestEnum.php | 22 -- tests/Hydrator/Fixture/TestEnumDto.php | 16 -- tests/Hydrator/Fixture/TestJsonDto.php | 16 -- tests/Hydrator/Fixture/TestJsonTypeDto.php | 13 -- tests/Hydrator/Fixture/TestJsonableObject.php | 17 -- .../WithUnsupportedPropertyTypeDto.php | 16 -- .../Fixture/WithUntypedPropertyDto.php | 16 -- .../Hydrator/HydrableObjectCollectionTest.php | 62 ------ tests/{Hydrator => }/HydratorTest.php | 210 +++--------------- tests/ObjectCollectionTest.php | 62 ++++++ tests/fixtures/BarDto.php | 10 + tests/fixtures/BarDtoCollection.php | 12 + tests/fixtures/BazDto.php | 17 ++ .../{Hydrator/Fixture => fixtures}/FooDto.php | 19 +- .../WithUnsupportedPropertyTypeDto.php | 10 + tests/fixtures/WithUntypedPropertyDto.php | 10 + 19 files changed, 153 insertions(+), 430 deletions(-) delete mode 100644 tests/Hydrator/Fixture/BarDto.php delete mode 100644 tests/Hydrator/Fixture/BarDtoCollection.php delete mode 100644 tests/Hydrator/Fixture/BazDto.php delete mode 100644 tests/Hydrator/Fixture/TestEnum.php delete mode 100644 tests/Hydrator/Fixture/TestEnumDto.php delete mode 100644 tests/Hydrator/Fixture/TestJsonDto.php delete mode 100644 tests/Hydrator/Fixture/TestJsonTypeDto.php delete mode 100644 tests/Hydrator/Fixture/TestJsonableObject.php delete mode 100644 tests/Hydrator/Fixture/WithUnsupportedPropertyTypeDto.php delete mode 100644 tests/Hydrator/Fixture/WithUntypedPropertyDto.php delete mode 100644 tests/Hydrator/HydrableObjectCollectionTest.php rename tests/{Hydrator => }/HydratorTest.php (50%) create mode 100644 tests/ObjectCollectionTest.php create mode 100644 tests/fixtures/BarDto.php create mode 100644 tests/fixtures/BarDtoCollection.php create mode 100644 tests/fixtures/BazDto.php rename tests/{Hydrator/Fixture => fixtures}/FooDto.php (63%) create mode 100644 tests/fixtures/WithUnsupportedPropertyTypeDto.php create mode 100644 tests/fixtures/WithUntypedPropertyDto.php diff --git a/tests/Hydrator/Fixture/BarDto.php b/tests/Hydrator/Fixture/BarDto.php deleted file mode 100644 index c052df0..0000000 --- a/tests/Hydrator/Fixture/BarDto.php +++ /dev/null @@ -1,16 +0,0 @@ -assertInstanceOf(HydrableObjectCollectionInterface::class, $collection); - } - - /** - * @return void - */ - public function testAdd() : void - { - $store = [ - new Fixture\BarDto(), - new Fixture\BarDto(), - new Fixture\BarDto(), - ]; - - $collection = new HydrableObjectCollection(); - - $collection->add($store[0]); - $collection->add($store[1]); - $collection->add($store[2]); - - $this->assertSame($store, $collection->getIterator()->getArrayCopy()); - } - - /** - * @return void - */ - public function testUnexpectedObject() : void - { - $collection = new Fixture\BarDtoCollection(); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The <' . Fixture\BarDtoCollection::class . '> collection ' . - 'must contain only the <' . Fixture\BarDto::class . '> objects.'); - - $collection->add(new Fixture\BazDto()); - } -} diff --git a/tests/Hydrator/HydratorTest.php b/tests/HydratorTest.php similarity index 50% rename from tests/Hydrator/HydratorTest.php rename to tests/HydratorTest.php index b110df0..7128b8b 100644 --- a/tests/Hydrator/HydratorTest.php +++ b/tests/HydratorTest.php @@ -39,11 +39,6 @@ public function testHydrate() : void 'int' => 0, 'float' => 0.0, 'string' => 'foo', - 'array' => [ - 'foo', - 'bar', - 100, - ], 'dateTime' => '2005-08-15T15:52:01.000+00:00', 'barDto' => [ 'value' => 'foo', @@ -63,7 +58,7 @@ public function testHydrate() : void 'alias' => 'value', ]; - $object = new Fixture\FooDto(); + $object = new Fixtures\FooDto(); $hydrator = new Hydrator(); $hydrator->hydrate($object, $data); @@ -74,129 +69,20 @@ public function testHydrate() : void $this->assertSame($data['int'], $object->int); $this->assertSame($data['float'], $object->float); $this->assertSame($data['string'], $object->string); - $this->assertSame($data['array'], $object->array->getArrayCopy()); $this->assertSame($data['dateTime'], $object->dateTime->format(DateTimeInterface::RFC3339_EXTENDED)); $this->assertSame($data['barDto']['value'], $object->barDto->value); - $this->assertSame($data['barDtoCollection'][0]['value'], $object->barDtoCollection->getIterator()[0]->value); - $this->assertSame($data['barDtoCollection'][1]['value'], $object->barDtoCollection->getIterator()[1]->value); + $this->assertSame($data['barDtoCollection'][0]['value'], $object->barDtoCollection->get(0)->value); + $this->assertSame($data['barDtoCollection'][1]['value'], $object->barDtoCollection->get(1)->value); $this->assertSame($data['simpleArray'], $object->simpleArray); $this->assertSame($data['alias'], $object->hidden); } - /** - * @return void - */ - public function testJsonTypeArrayAccess() : void - { - $object = (new Hydrator)->hydrate(new Fixture\TestJsonTypeDto(), ['json' => '[]']); - - $this->assertFalse(isset($object->json['foo'])); - $this->assertNull($object->json['foo']); - - $object->json['foo'] = 1; - $this->assertTrue(isset($object->json['foo'])); - $this->assertSame(1, $object->json['foo']); - - unset($object->json['foo']); - $this->assertFalse(isset($object->json['foo'])); - $this->assertNull($object->json['foo']); - } - - /** - * @return void - */ - public function testJsonTypeJsonSerialize() : void - { - $object = (new Hydrator)->hydrate(new Fixture\TestJsonTypeDto(), ['json' => '[]']); - - $object->json['foo'] = 1; - $object->json['bar'] = 2; - - $this->assertSame([ - 'foo' => 1, - 'bar' => 2, - ], $object->json->jsonSerialize()); - } - - /** - * @return void - */ - public function testJsonTypeDeserializeJson() : void - { - $json = '{"foo":1,"bar":2,"baz":{"qux":3}}'; - - $object = (new Hydrator)->hydrate(new Fixture\TestJsonTypeDto(), ['json' => $json]); - - $this->assertSame(1, $object->json['foo']); - $this->assertSame(2, $object->json['bar']); - $this->assertSame(3, $object->json['baz']['qux']); - } - - /** - * @return void - */ - public function testJsonTypeInvalidJsonSyntax() : void - { - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage( - 'The property only accepts valid JSON data (Syntax error).' - ); - - (new Hydrator)->hydrate(new Fixture\TestJsonTypeDto(), ['json' => '{']); - } - - /** - * @return void - */ - public function testJsonTypeInvalidJsonType() : void - { - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts a string.'); - - (new Hydrator)->hydrate(new Fixture\TestJsonTypeDto(), ['json' => []]); - } - - /** - * @return void - */ - public function testJsonableObjectDeserializeJson() : void - { - $json = '{"foo":"foo:value","bar":"bar:value"}'; - - $object = (new Hydrator)->hydrate(new Fixture\TestJsonDto(), ['json' => $json]); - - $this->assertSame('foo:value', $object->json->foo); - $this->assertSame('bar:value', $object->json->bar); - } - - /** - * @return void - */ - public function testJsonableObjectInvalidJsonSyntax() : void - { - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts valid JSON data (Syntax error).'); - - (new Hydrator)->hydrate(new Fixture\TestJsonDto(), ['json' => '{']); - } - - /** - * @return void - */ - public function testJsonableObjectInvalidJsonType() : void - { - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts a string.'); - - (new Hydrator)->hydrate(new Fixture\TestJsonDto(), ['json' => []]); - } - /** * @return void */ public function testMissingRequiredValueException() : void { - $object = new Fixture\BarDto(); + $object = new Fixtures\BarDto(); $hydrator = new Hydrator(); $this->expectException(Exception\MissingRequiredValueException::class); @@ -209,12 +95,12 @@ public function testMissingRequiredValueException() : void /** * @return void */ - public function testUntypedObjectPropertyException() : void + public function testUntypedPropertyException() : void { - $object = new Fixture\WithUntypedPropertyDto(); + $object = new Fixtures\WithUntypedPropertyDto(); $hydrator = new Hydrator(); - $this->expectException(Exception\UntypedObjectPropertyException::class); + $this->expectException(Exception\UntypedPropertyException::class); $this->expectExceptionMessage('The property is not typed.'); $hydrator->hydrate($object, [ @@ -225,14 +111,14 @@ public function testUntypedObjectPropertyException() : void /** * @return void */ - public function testUnsupportedObjectPropertyTypeException() : void + public function testUnsupportedPropertyTypeException() : void { - $object = new Fixture\WithUnsupportedPropertyTypeDto(); + $object = new Fixtures\WithUnsupportedPropertyTypeDto(); $hydrator = new Hydrator(); - $this->expectException(Exception\UnsupportedObjectPropertyTypeException::class); + $this->expectException(Exception\UnsupportedPropertyTypeException::class); $this->expectExceptionMessage('The property ' . - 'contains the unhydrable type.'); + 'contains an unsupported type .'); $hydrator->hydrate($object, [ 'value' => 'foo', @@ -244,11 +130,11 @@ public function testUnsupportedObjectPropertyTypeException() : void */ public function testInvalidValueExceptionForNonNullableProperty() : void { - $object = new Fixture\BazDto(); + $object = new Fixtures\BazDto(); $hydrator = new Hydrator(); $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property does not support null.'); + $this->expectExceptionMessage('The property cannot accept null.'); $hydrator->hydrate($object, [ 'nonNullable' => null, @@ -264,11 +150,11 @@ public function testInvalidValueExceptionForNonNullableProperty() : void */ public function testInvalidValueExceptionForScalarProperty($nonScalarValue) : void { - $object = new Fixture\BazDto(); + $object = new Fixtures\BazDto(); $hydrator = new Hydrator(); $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts a scalar value.'); + $this->expectExceptionMessage('The property accepts a string only.'); $hydrator->hydrate($object, [ 'scalar' => $nonScalarValue, @@ -284,11 +170,11 @@ public function testInvalidValueExceptionForScalarProperty($nonScalarValue) : vo */ public function testInvalidValueExceptionForArrayProperty($nonArrayValue) : void { - $object = new Fixture\BazDto(); + $object = new Fixtures\BazDto(); $hydrator = new Hydrator(); $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts an array.'); + $this->expectExceptionMessage('The property accepts an array only.'); $hydrator->hydrate($object, [ 'array' => $nonArrayValue, @@ -304,11 +190,13 @@ public function testInvalidValueExceptionForArrayProperty($nonArrayValue) : void */ public function testInvalidValueExceptionForDateTimeProperty($nonDateTimeValue) : void { - $object = new Fixture\BazDto(); + $object = new Fixtures\BazDto(); $hydrator = new Hydrator(); $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts a valid date-time string.'); + $this->expectExceptionMessage( + 'The property accepts a valid date-time string or a timestamp only.' + ); $hydrator->hydrate($object, [ 'dateTime' => $nonDateTimeValue, @@ -324,11 +212,11 @@ public function testInvalidValueExceptionForDateTimeProperty($nonDateTimeValue) */ public function testInvalidValueExceptionForOneToOneProperty($nonArrayValue) : void { - $object = new Fixture\BazDto(); + $object = new Fixtures\BazDto(); $hydrator = new Hydrator(); $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts an array.'); + $this->expectExceptionMessage('The property accepts an array only.'); $hydrator->hydrate($object, [ 'oneToOne' => $nonArrayValue, @@ -344,63 +232,17 @@ public function testInvalidValueExceptionForOneToOneProperty($nonArrayValue) : v */ public function testInvalidValueExceptionForOneToManyProperty($nonArrayValue) : void { - $object = new Fixture\BazDto(); + $object = new Fixtures\BazDto(); $hydrator = new Hydrator(); $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts an array.'); + $this->expectExceptionMessage('The property accepts an array only.'); $hydrator->hydrate($object, [ 'oneToMany' => $nonArrayValue, ]); } - public function testEnumerableValue() : void - { - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => 'A']); - $this->assertSame('A:value', $object->foo->getValue()); - - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => 'B']); - $this->assertSame('B:value', $object->foo->getValue()); - - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => 'C']); - $this->assertSame('C:value', $object->foo->getValue()); - - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => '0']); - $this->assertSame('0:value', $object->foo->getValue()); - - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => '1']); - $this->assertSame('1:value', $object->foo->getValue()); - - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => '2']); - $this->assertSame('2:value', $object->foo->getValue()); - - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => 0]); - $this->assertSame('0:value', $object->foo->getValue()); - - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => 1]); - $this->assertSame('1:value', $object->foo->getValue()); - - $object = (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => 2]); - $this->assertSame('2:value', $object->foo->getValue()); - } - - public function testUnknownEnumerableValue() : void - { - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts one of the enum values.'); - - (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => 'D']); - } - - public function testInvalidEnumerableValue() : void - { - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property only accepts an integer or a string.'); - - (new Hydrator)->hydrate(new Fixture\TestEnumDto(), ['foo' => []]); - } - /** * @return array */ @@ -439,10 +281,8 @@ public function nonDateTimeDataProvider() : array { return [ [true], - [1], [1.1], [''], - ['0'], ['non-date-time-string'], [new \stdClass], [function () { diff --git a/tests/ObjectCollectionTest.php b/tests/ObjectCollectionTest.php new file mode 100644 index 0000000..030fd16 --- /dev/null +++ b/tests/ObjectCollectionTest.php @@ -0,0 +1,62 @@ +assertInstanceOf(ObjectCollectionInterface::class, $collection); + } + + /** + * @return void + */ + public function testAdd() : void + { + $store = [ + new Fixtures\BarDto(), + new Fixtures\BarDto(), + new Fixtures\BarDto(), + ]; + + $collection = new Fixtures\BarDtoCollection(); + + $collection->add(0, $store[0]); + $collection->add(1, $store[1]); + $collection->add(2, $store[2]); + + $this->assertSame($store, $collection->all()); + } + + /** + * @return void + */ + public function testUnexpectedObject() : void + { + $collection = new Fixtures\BarDtoCollection(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The <' . Fixtures\BarDtoCollection::class . '> collection ' . + 'must contain the <' . Fixtures\BarDto::class . '> objects only.'); + + $collection->add(0, new Fixtures\BazDto()); + } +} diff --git a/tests/fixtures/BarDto.php b/tests/fixtures/BarDto.php new file mode 100644 index 0000000..bb0922f --- /dev/null +++ b/tests/fixtures/BarDto.php @@ -0,0 +1,10 @@ + Date: Fri, 22 Oct 2021 12:01:41 +0500 Subject: [PATCH 02/18] release v2 --- src/{Hydrator => }/Annotation/Alias.php | 26 +- .../Exception/ExceptionInterface.php | 0 .../Exception/HydrationException.php | 0 .../Exception/InvalidObjectException.php | 0 .../Exception/InvalidValueException.php | 0 .../MissingRequiredValueException.php | 0 .../UnsupportedPropertyTypeException.php} | 4 +- .../UntypedPropertyException.php} | 4 +- src/Hydrator.php | 697 ++++++++++++++++++ src/Hydrator/EnumerableObject.php | 44 -- src/Hydrator/EnumerableObjectInterface.php | 26 - src/Hydrator/HydrableObjectCollection.php | 65 -- .../HydrableObjectCollectionInterface.php | 40 - src/Hydrator/HydrableObjectInterface.php | 19 - src/Hydrator/Hydrator.php | 574 --------------- src/Hydrator/HydratorInterface.php | 33 - src/Hydrator/Json.php | 79 -- src/Hydrator/JsonableObjectInterface.php | 19 - src/HydratorInterface.php | 45 ++ src/ObjectCollection.php | 98 +++ src/ObjectCollectionInterface.php | 62 ++ 21 files changed, 930 insertions(+), 905 deletions(-) rename src/{Hydrator => }/Annotation/Alias.php (54%) rename src/{Hydrator => }/Exception/ExceptionInterface.php (100%) rename src/{Hydrator => }/Exception/HydrationException.php (100%) rename src/{Hydrator => }/Exception/InvalidObjectException.php (100%) rename src/{Hydrator => }/Exception/InvalidValueException.php (100%) rename src/{Hydrator => }/Exception/MissingRequiredValueException.php (100%) rename src/{Hydrator/Exception/UnsupportedObjectPropertyTypeException.php => Exception/UnsupportedPropertyTypeException.php} (76%) rename src/{Hydrator/Exception/UntypedObjectPropertyException.php => Exception/UntypedPropertyException.php} (78%) create mode 100644 src/Hydrator.php delete mode 100644 src/Hydrator/EnumerableObject.php delete mode 100644 src/Hydrator/EnumerableObjectInterface.php delete mode 100644 src/Hydrator/HydrableObjectCollection.php delete mode 100644 src/Hydrator/HydrableObjectCollectionInterface.php delete mode 100644 src/Hydrator/HydrableObjectInterface.php delete mode 100644 src/Hydrator/Hydrator.php delete mode 100644 src/Hydrator/HydratorInterface.php delete mode 100644 src/Hydrator/Json.php delete mode 100644 src/Hydrator/JsonableObjectInterface.php create mode 100644 src/HydratorInterface.php create mode 100644 src/ObjectCollection.php create mode 100644 src/ObjectCollectionInterface.php diff --git a/src/Hydrator/Annotation/Alias.php b/src/Annotation/Alias.php similarity index 54% rename from src/Hydrator/Annotation/Alias.php rename to src/Annotation/Alias.php index be08087..6d7f41f 100644 --- a/src/Hydrator/Annotation/Alias.php +++ b/src/Annotation/Alias.php @@ -12,19 +12,41 @@ namespace Sunrise\Hydrator\Annotation; /** - * Alias + * Import classes + */ +use Attribute; + +/** + * A property alias * * @Annotation * * @Target({"PROPERTY"}) + * + * @NamedArgumentConstructor + * + * @Attributes({ + * @Attribute("value", type="string", required=true), + * }) */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class Alias { /** - * @Required + * The attribute value * * @var string */ public $value; + + /** + * Constructor of the class + * + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } } diff --git a/src/Hydrator/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php similarity index 100% rename from src/Hydrator/Exception/ExceptionInterface.php rename to src/Exception/ExceptionInterface.php diff --git a/src/Hydrator/Exception/HydrationException.php b/src/Exception/HydrationException.php similarity index 100% rename from src/Hydrator/Exception/HydrationException.php rename to src/Exception/HydrationException.php diff --git a/src/Hydrator/Exception/InvalidObjectException.php b/src/Exception/InvalidObjectException.php similarity index 100% rename from src/Hydrator/Exception/InvalidObjectException.php rename to src/Exception/InvalidObjectException.php diff --git a/src/Hydrator/Exception/InvalidValueException.php b/src/Exception/InvalidValueException.php similarity index 100% rename from src/Hydrator/Exception/InvalidValueException.php rename to src/Exception/InvalidValueException.php diff --git a/src/Hydrator/Exception/MissingRequiredValueException.php b/src/Exception/MissingRequiredValueException.php similarity index 100% rename from src/Hydrator/Exception/MissingRequiredValueException.php rename to src/Exception/MissingRequiredValueException.php diff --git a/src/Hydrator/Exception/UnsupportedObjectPropertyTypeException.php b/src/Exception/UnsupportedPropertyTypeException.php similarity index 76% rename from src/Hydrator/Exception/UnsupportedObjectPropertyTypeException.php rename to src/Exception/UnsupportedPropertyTypeException.php index be88d63..19f15ef 100644 --- a/src/Hydrator/Exception/UnsupportedObjectPropertyTypeException.php +++ b/src/Exception/UnsupportedPropertyTypeException.php @@ -12,8 +12,8 @@ namespace Sunrise\Hydrator\Exception; /** - * UnsupportedObjectPropertyTypeException + * UnsupportedPropertyTypeException */ -class UnsupportedObjectPropertyTypeException extends InvalidObjectException +class UnsupportedPropertyTypeException extends InvalidObjectException { } diff --git a/src/Hydrator/Exception/UntypedObjectPropertyException.php b/src/Exception/UntypedPropertyException.php similarity index 78% rename from src/Hydrator/Exception/UntypedObjectPropertyException.php rename to src/Exception/UntypedPropertyException.php index c3148c5..e5779d5 100644 --- a/src/Hydrator/Exception/UntypedObjectPropertyException.php +++ b/src/Exception/UntypedPropertyException.php @@ -12,8 +12,8 @@ namespace Sunrise\Hydrator\Exception; /** - * UntypedObjectPropertyException + * UntypedPropertyException */ -class UntypedObjectPropertyException extends InvalidObjectException +class UntypedPropertyException extends InvalidObjectException { } diff --git a/src/Hydrator.php b/src/Hydrator.php new file mode 100644 index 0000000..29eec21 --- /dev/null +++ b/src/Hydrator.php @@ -0,0 +1,697 @@ + + * @copyright Copyright (c) 2021, Anatoly Fenric + * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE + * @link https://github.com/sunrise-php/hydrator + */ + +namespace Sunrise\Hydrator; + +/** + * Import classes + */ +use Doctrine\Common\Annotations\SimpleAnnotationReader; +use Sunrise\Hydrator\Annotation\Alias; +use InvalidArgumentException; +use DateTimeInterface; +use ReflectionClass; +use ReflectionProperty; +use ReflectionNamedType; +use ReflectionUnionType; + +/** + * Import functions + */ +use function array_key_exists; +use function class_exists; +use function ctype_digit; +use function filter_var; +use function is_array; +use function is_bool; +use function is_float; +use function is_int; +use function is_object; +use function is_string; +use function is_subclass_of; +use function json_decode; +use function json_last_error; +use function json_last_error_msg; +use function sprintf; +use function strtotime; + +/** + * Import constants + */ +use const FILTER_NULL_ON_FAILURE; +use const FILTER_VALIDATE_BOOLEAN; +use const FILTER_VALIDATE_FLOAT; +use const FILTER_VALIDATE_INT; +use const JSON_ERROR_NONE; +use const PHP_MAJOR_VERSION; + +/** + * Hydrator + */ +class Hydrator implements HydratorInterface +{ + + /** + * @var SimpleAnnotationReader|null + */ + private $annotationReader = null; + + /** + * Constructor of the class + */ + public function __construct() + { + if (PHP_MAJOR_VERSION < 8) { + // @codeCoverageIgnoreStart + $this->useAnnotations(); + // @codeCoverageIgnoreEnd + } + } + + /** + * Enables support for annotations + * + * @return self + */ + public function useAnnotations() : self + { + if (isset($this->annotationReader)) { + return $this; + } + + if (class_exists(SimpleAnnotationReader::class)) { + $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader(); + $this->annotationReader->addNamespace(Annotation::class); + } + + return $this; + } + + /** + * {@inheritdoc} + * + * @throws Exception\UntypedPropertyException + * If one of the object properties isn't typed. + * + * @throws Exception\UnsupportedPropertyTypeException + * If one of the object properties contains an unsupported type. + * + * @throws Exception\MissingRequiredValueException + * If the given data doesn't contain required value. + * + * @throws Exception\InvalidValueException + * If the given data contains an invalid value. + */ + public function hydrate($object, array $data) : object + { + $object = $this->initializeObject($object); + + $class = new ReflectionClass($object); + $properties = $class->getProperties(); + foreach ($properties as $property) { + // statical properties cannot be hydrated... + if ($property->isStatic()) { + continue; + } + + $property->setAccessible(true); + + if (!$property->hasType()) { + throw new Exception\UntypedPropertyException(sprintf( + 'The <%s.%s> property is not typed.', + $class->getShortName(), + $property->getName() + )); + } + + if ($property->getType() instanceof ReflectionUnionType) { + throw new Exception\UnsupportedPropertyTypeException(sprintf( + 'The <%s.%s> property contains an unsupported type <%s>.', + $class->getShortName(), + $property->getName(), + \implode('|', \array_map(function (ReflectionNamedType $type) : string { + return $type->getName(); + }, $property->getType()->getTypes())) + )); + } + + $key = $property->getName(); + if (!array_key_exists($key, $data)) { + $alias = $this->getPropertyAlias($property); + if (isset($alias)) { + $key = $alias->value; + } + } + + if (!array_key_exists($key, $data)) { + if (!$property->isInitialized($object)) { + throw new Exception\MissingRequiredValueException(sprintf( + 'The <%s.%s> property is required.', + $class->getShortName(), + $property->getName() + )); + } + + continue; + } + + $this->hydrateProperty($object, $class, $property, $property->getType(), $data[$key]); + } + + return $object; + } + + /** + * {@inheritdoc} + * + * @throws InvalidArgumentException + * If the given JSON cannot be decoded. + */ + public function hydrateWithJson($object, string $json) : object + { + json_decode(''); // reset previous error... + $data = (array) json_decode($json, true); + if (JSON_ERROR_NONE <> json_last_error()) { + throw new InvalidArgumentException(sprintf( + 'Unable to decode JSON: %s', + json_last_error_msg() + )); + } + + return $this->hydrate($object, $data); + } + + /** + * Initializes the given object + * + * @param object|class-string $object + * + * @return object + * + * @throws InvalidArgumentException + * If the given object cannot be initialized. + */ + private function initializeObject($object) : object + { + if (is_object($object)) { + return $object; + } + + if (!is_string($object) || !class_exists($object)) { + throw new InvalidArgumentException(sprintf( + 'The method %s::hydrate() expects an object or name of an existing class.', + __CLASS__ + )); + } + + $class = new ReflectionClass($object); + $constructor = $class->getConstructor(); + if (isset($constructor) && $constructor->getNumberOfRequiredParameters() > 0) { + throw new InvalidArgumentException(sprintf( + 'The object %s cannot be hydrated because its constructor has required parameters.', + $class->getName() + )); + } + + return $class->newInstance(); + } + + /** + * Gets an alias for the given property + * + * @param ReflectionProperty $property + * + * @return Alias|null + */ + private function getPropertyAlias(ReflectionProperty $property) : ?Alias + { + if (PHP_MAJOR_VERSION >= 8) { + $attributes = $property->getAttributes(Alias::class); + if (isset($attributes[0])) { + return $attributes[0]->newInstance(); + } + } + + if (isset($this->annotationReader)) { + $annotation = $this->annotationReader->getPropertyAnnotation($property, Alias::class); + if (isset($annotation)) { + return $annotation; + } + } + + return null; + } + + /** + * Hydrates the given property with the given value + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + * + * @throws Exception\UnsupportedPropertyTypeException + * If the given property contains an unsupported type. + */ + private function hydrateProperty( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (null === $value) { + $this->hydratePropertyWithNull($object, $class, $property, $type); + return; + } + + if ('bool' === $type->getName()) { + $this->hydratePropertyWithBooleanValue($object, $class, $property, $type, $value); + return; + } + + if ('int' === $type->getName()) { + $this->hydratePropertyWithIntegerNumber($object, $class, $property, $type, $value); + return; + } + + if ('float' === $type->getName()) { + $this->hydratePropertyWithNumber($object, $class, $property, $type, $value); + return; + } + + if ('string' === $type->getName()) { + $this->hydratePropertyWithString($object, $class, $property, $type, $value); + return; + } + + if ('array' === $type->getName()) { + $this->hydratePropertyWithArray($object, $class, $property, $type, $value); + return; + } + + if ('object' === $type->getName()) { + $this->hydratePropertyWithObject($object, $class, $property, $type, $value); + return; + } + + if (is_subclass_of($type->getName(), DateTimeInterface::class)) { + $this->hydratePropertyWithTimestamp($object, $class, $property, $type, $value); + return; + } + + if (is_subclass_of($type->getName(), ObjectCollectionInterface::class)) { + $this->hydratePropertyWithManyAssociations($object, $class, $property, $type, $value); + return; + } + + if (class_exists($type->getName())) { + $this->hydratePropertyWithOneAssociation($object, $class, $property, $type, $value); + return; + } + + throw new Exception\UnsupportedPropertyTypeException(sprintf( + 'The <%s.%s> property contains an unsupported type <%s>.', + $class->getShortName(), + $property->getName(), + $type->getName() + )); + } + + /** + * Hydrates the given property with null + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithNull( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type + ) : void { + if (!$type->allowsNull()) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property cannot accept null.', + $class->getShortName(), + $property->getName() + )); + } + + $property->setValue($object, null); + } + + /** + * Hydrates the given property with the given boolean value + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithBooleanValue( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (!is_bool($value)) { + // if the value isn't boolean, then we will use filter_var, because it will give us the ability to specify + // boolean values as strings. this behavior is great for html forms. details at: + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L273 + $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + + if (!isset($value)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts a boolean value only.', + $class->getShortName(), + $property->getName() + )); + } + } + + $property->setValue($object, $value); + } + + /** + * Hydrates the given property with the given integer number + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithIntegerNumber( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (!is_int($value)) { + // it's senseless to convert the value type if it's not a number, so we will use filter_var to correct + // converting the value type to int. also remember that string numbers must be between PHP_INT_MIN and + // PHP_INT_MAX, otherwise the result will be null. this behavior is great for html forms. details at: + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197 + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94 + $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); + + if (!isset($value)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts an integer number only.', + $class->getShortName(), + $property->getName() + )); + } + } + + $property->setValue($object, $value); + } + + /** + * Hydrates the given property with the given number + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithNumber( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (!is_float($value)) { + // it's senseless to convert the value type if it's not a number, so we will use filter_var to correct + // converting the value type to float. this behavior is great for html forms. details at: + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L342 + $value = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE); + + if (!isset($value)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts a number only.', + $class->getShortName(), + $property->getName() + )); + } + } + + $property->setValue($object, $value); + } + + /** + * Hydrates the given property with the given string + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithString( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (!is_string($value)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts a string only.', + $class->getShortName(), + $property->getName() + )); + } + + $property->setValue($object, $value); + } + + /** + * Hydrates the given property with the given array + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithArray( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (!is_array($value)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts an array only.', + $class->getShortName(), + $property->getName() + )); + } + + $property->setValue($object, $value); + } + + /** + * Hydrates the given property with the given object + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithObject( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (!is_object($value)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts an object only.', + $class->getShortName(), + $property->getName() + )); + } + + $property->setValue($object, $value); + } + + /** + * Hydrates the given property with the given timestamp + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithTimestamp( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + $target = $type->getName(); + + if (is_int($value) || ctype_digit($value)) { + $property->setValue($object, (new $target)->setTimestamp($value)); + return; + } + + if (is_string($value) && false !== strtotime($value)) { + $property->setValue($object, new $target($value)); + return; + } + + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts a valid date-time string or a timestamp only.', + $class->getShortName(), + $property->getName() + )); + } + + /** + * Hydrates the given property with the given many associations + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithManyAssociations( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (!is_array($value)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts an array only.', + $class->getShortName(), + $property->getName() + )); + } + + $target = $type->getName(); + $collection = new $target(); + foreach ($value as $key => $item) { + if (!is_array($item)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts an array with arrays only.', + $class->getShortName(), + $property->getName() + )); + } + + $collection->add($key, $this->hydrate($collection->getItemClassName(), $item)); + } + + $property->setValue($object, $collection); + } + + /** + * Hydrates the given property with the given one association + * + * @param object $object + * @param ReflectionClass $class + * @param ReflectionProperty $property + * @param ReflectionNamedType $type + * @param mixed $value + * + * @return void + * + * @throws Exception\InvalidValueException + * If the given value isn't valid. + */ + private function hydratePropertyWithOneAssociation( + object $object, + ReflectionClass $class, + ReflectionProperty $property, + ReflectionNamedType $type, + $value + ) : void { + if (!is_array($value)) { + throw new Exception\InvalidValueException(sprintf( + 'The <%s.%s> property accepts an array only.', + $class->getShortName(), + $property->getName() + )); + } + + $property->setValue($object, $this->hydrate($type->getName(), $value)); + } +} diff --git a/src/Hydrator/EnumerableObject.php b/src/Hydrator/EnumerableObject.php deleted file mode 100644 index eb57568..0000000 --- a/src/Hydrator/EnumerableObject.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * EnumerableObject - */ -abstract class EnumerableObject implements EnumerableObjectInterface -{ - - /** - * The enum value - * - * @var mixed - */ - protected $value; - - /** - * Constructor of the class - * - * @param mixed $value - */ - public function __construct($value) - { - $this->value = $value; - } - - /** - * {@inheritdoc} - */ - public function getValue() - { - return $this->value; - } -} diff --git a/src/Hydrator/EnumerableObjectInterface.php b/src/Hydrator/EnumerableObjectInterface.php deleted file mode 100644 index 7926e21..0000000 --- a/src/Hydrator/EnumerableObjectInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * EnumerableObjectInterface - */ -interface EnumerableObjectInterface -{ - - /** - * Gets the enum value - * - * @return mixed - */ - public function getValue(); -} diff --git a/src/Hydrator/HydrableObjectCollection.php b/src/Hydrator/HydrableObjectCollection.php deleted file mode 100644 index cdf4dc1..0000000 --- a/src/Hydrator/HydrableObjectCollection.php +++ /dev/null @@ -1,65 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * Import classes - */ -use ArrayIterator; -use InvalidArgumentException; -use Traversable; - -/** - * Import functions - */ -use function get_called_class; -use function sprintf; - -/** - * HydrableObjectCollection - */ -class HydrableObjectCollection implements HydrableObjectCollectionInterface -{ - - /** - * The collection objects - * - * @var HydrableObjectInterface[] - */ - private array $objects = []; - - /** - * {@inheritDoc} - */ - final public function add(HydrableObjectInterface $object) - { - $type = static::T; - - if (!($object instanceof $type)) { - throw new InvalidArgumentException(sprintf( - 'The <%s> collection must contain only the <%s> objects.', - get_called_class(), - $type, - )); - } - - $this->objects[] = $object; - } - - /** - * {@inheritDoc} - */ - final public function getIterator() : Traversable - { - return new ArrayIterator($this->objects); - } -} diff --git a/src/Hydrator/HydrableObjectCollectionInterface.php b/src/Hydrator/HydrableObjectCollectionInterface.php deleted file mode 100644 index 330d113..0000000 --- a/src/Hydrator/HydrableObjectCollectionInterface.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * Import classes - */ -use IteratorAggregate; - -/** - * HydrableObjectCollectionInterface - */ -interface HydrableObjectCollectionInterface extends IteratorAggregate -{ - - /** - * The type for objects in the collection - * - * @var string - */ - public const T = HydrableObjectInterface::class; - - /** - * Adds the given object to the collection - * - * @param HydrableObjectInterface $object - * - * @return void - */ - public function add(HydrableObjectInterface $object); -} diff --git a/src/Hydrator/HydrableObjectInterface.php b/src/Hydrator/HydrableObjectInterface.php deleted file mode 100644 index ff4fecd..0000000 --- a/src/Hydrator/HydrableObjectInterface.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * HydrableObjectInterface - */ -interface HydrableObjectInterface -{ -} diff --git a/src/Hydrator/Hydrator.php b/src/Hydrator/Hydrator.php deleted file mode 100644 index 3204a78..0000000 --- a/src/Hydrator/Hydrator.php +++ /dev/null @@ -1,574 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * Import classes - */ -use Doctrine\Common\Annotations\SimpleAnnotationReader; -use ArrayAccess; -use DateTimeInterface; -use ReflectionClass; -use ReflectionProperty; -use ReflectionNamedType; - -/** - * Import functions - */ -use function array_key_exists; -use function class_exists; -use function constant; -use function ctype_digit; -use function defined; -use function in_array; -use function is_array; -use function is_int; -use function is_scalar; -use function is_string; -use function is_subclass_of; -use function json_decode; -use function json_last_error; -use function json_last_error_msg; -use function sprintf; -use function strtotime; - -/** - * Import constants - */ -use const JSON_ERROR_NONE; - -/** - * Hydrator - */ -class Hydrator implements HydratorInterface -{ - - /** - * @var SimpleAnnotationReader - */ - private $annotationReader; - - /** - * Constructor of the class - */ - public function __construct() - { - if (class_exists(SimpleAnnotationReader::class)) { - $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader(); - $this->annotationReader->addNamespace(Annotation::class); - } - } - - /** - * {@inheritDoc} - * - * @throws Exception\MissingRequiredValueException - * If the given data does not contain required value. - * * data error. - * - * @throws Exception\InvalidValueException - * If the given data contains an invalid value. - * * data error. - * - * @throws Exception\UntypedObjectPropertyException - * If one of the properties of the given object is not typed. - * * DTO error. - * - * @throws Exception\UnsupportedObjectPropertyTypeException - * If one of the properties of the given object contains an unsupported type. - * * DTO error. - */ - public function hydrate(HydrableObjectInterface $object, array $data) : HydrableObjectInterface - { - $class = new ReflectionClass($object); - $properties = $class->getProperties(); - foreach ($properties as $property) { - $property->setAccessible(true); - - if ($property->isStatic()) { - continue; - } - - $key = $property->getName(); - - if (isset($this->annotationReader) && !array_key_exists($key, $data)) { - $alias = $this->annotationReader->getPropertyAnnotation($property, Annotation\Alias::class); - if ($alias instanceof Annotation\Alias) { - $key = $alias->value; - } - } - - if (!array_key_exists($key, $data)) { - if (!$property->isInitialized($object)) { - throw new Exception\MissingRequiredValueException(sprintf( - 'The <%s.%s> property is required.', - $class->getShortName(), - $property->getName(), - )); - } - - continue; - } - - if (!$property->hasType()) { - throw new Exception\UntypedObjectPropertyException(sprintf( - 'The <%s.%s> property is not typed.', - $class->getShortName(), - $property->getName(), - )); - } - - $this->hydrateProperty($object, $class, $property, $property->getType(), $data[$key]); - } - - return $object; - } - - /** - * Hydrates the given property with the given value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\UnsupportedObjectPropertyTypeException - * If the given property contains an unsupported type. - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydrateProperty( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (null === $value) { - $this->hydratePropertyWithNull($object, $class, $property, $type); - return; - } - - if (in_array($type->getName(), ['bool', 'int', 'float', 'string'])) { - $this->hydratePropertyWithScalar($object, $class, $property, $type, $value); - return; - } - - if (Json::class === $type->getName()) { - $this->hydratePropertyWithJson($object, $class, $property, $type, $value); - return; - } - - if ('array' === $type->getName() || is_subclass_of($type->getName(), ArrayAccess::class)) { - $this->hydratePropertyWithArray($object, $class, $property, $type, $value); - return; - } - - if (is_subclass_of($type->getName(), DateTimeInterface::class)) { - $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value); - return; - } - - if (is_subclass_of($type->getName(), EnumerableObjectInterface::class)) { - $this->hydratePropertyWithEnumerableValue($object, $class, $property, $type, $value); - return; - } - - if (is_subclass_of($type->getName(), JsonableObjectInterface::class)) { - $this->hydratePropertyWithJsonOneToOneAssociation($object, $class, $property, $type, $value); - return; - } - - if (is_subclass_of($type->getName(), HydrableObjectInterface::class)) { - $this->hydratePropertyWithOneToOneAssociation($object, $class, $property, $type, $value); - return; - } - - if (is_subclass_of($type->getName(), HydrableObjectCollectionInterface::class)) { - $this->hydratePropertyWithOneToManyAssociation($object, $class, $property, $type, $value); - return; - } - - throw new Exception\UnsupportedObjectPropertyTypeException(sprintf( - 'The <%s.%s> property contains the <%s> unhydrable type.', - $class->getShortName(), - $property->getName(), - $type->getName(), - )); - } - - /** - * Hydrates the given property with null - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydratePropertyWithNull( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type - ) : void { - if (!$type->allowsNull()) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property does not support null.', - $class->getShortName(), - $property->getName(), - )); - } - - $property->setValue($object, null); - } - - /** - * Hydrates the given property with the given scalar value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydratePropertyWithScalar( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (!is_scalar($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts a scalar value.', - $class->getShortName(), - $property->getName(), - )); - } - - switch ($type->getName()) { - case 'bool': - $property->setValue($object, (bool) $value); - break; - case 'int': - $property->setValue($object, (int) $value); - break; - case 'float': - $property->setValue($object, (float) $value); - break; - case 'string': - $property->setValue($object, (string) $value); - break; - } - } - - /** - * Hydrates the given property with the given json value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydratePropertyWithJson( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (!is_string($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts a string.', - $class->getShortName(), - $property->getName(), - )); - } - - json_decode(''); // reset previous error... - $value = (array) json_decode($value, true); - if (JSON_ERROR_NONE <> json_last_error()) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts valid JSON data (%s).', - $class->getShortName(), - $property->getName(), - json_last_error_msg(), - )); - } - - $property->setValue($object, new Json($value)); - } - - /** - * Hydrates the given property with the given array value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydratePropertyWithArray( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (!is_array($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts an array.', - $class->getShortName(), - $property->getName(), - )); - } - - if ('array' === $type->getName()) { - $property->setValue($object, $value); - return; - } - - $arrayClassName = $type->getName(); - $array = new $arrayClassName(); - foreach ($value as $offset => $element) { - $array->offsetSet($offset, $element); - } - - $property->setValue($object, $array); - } - - /** - * Hydrates the given property with the given enumerable value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydratePropertyWithEnumerableValue( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (!is_int($value) && !is_string($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts an integer or a string.', - $class->getShortName(), - $property->getName(), - )); - } - - // support for integer cases... - if (is_int($value) || ctype_digit($value)) { - $value = '_' . $value; - } - - $enum = $type->getName(); - $constant = sprintf('%s::%s', $enum, $value); - if (!defined($constant)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts one of the <%s> enum values.', - $class->getShortName(), - $property->getName(), - (new ReflectionClass($enum))->getShortName(), - )); - } - - $property->setValue($object, new $enum(constant($constant))); - } - - /** - * Hydrates the given property with the given date-time value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydratePropertyWithDateTime( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (!is_string($value) || false === strtotime($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts a valid date-time string.', - $class->getShortName(), - $property->getName(), - )); - } - - $dateTimeClassName = $type->getName(); - $dateTime = new $dateTimeClassName($value); - $property->setValue($object, $dateTime); - } - - /** - * Hydrates the given property with the given JSON one-to-one value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - public function hydratePropertyWithJsonOneToOneAssociation( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (!is_string($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts a string.', - $class->getShortName(), - $property->getName(), - )); - } - - json_decode(''); // reset previous error... - $value = (array) json_decode($value, true); - if (JSON_ERROR_NONE <> json_last_error()) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts valid JSON data (%s).', - $class->getShortName(), - $property->getName(), - json_last_error_msg(), - )); - } - - $this->hydratePropertyWithOneToOneAssociation($object, $class, $property, $type, $value); - } - - /** - * Hydrates the given property with the given one-to-one value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydratePropertyWithOneToOneAssociation( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (!is_array($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts an array.', - $class->getShortName(), - $property->getName(), - )); - } - - $childObjectClassName = $type->getName(); - $childObject = new $childObjectClassName(); - $this->hydrate($childObject, $value); - $property->setValue($object, $childObject); - } - - /** - * Hydrates the given property with the given one-to-many value - * - * @param HydrableObjectInterface $object - * @param ReflectionClass $class - * @param ReflectionProperty $property - * @param ReflectionNamedType $type - * @param mixed $value - * - * @return void - * - * @throws Exception\InvalidValueException - * If the given value isn't valid. - */ - private function hydratePropertyWithOneToManyAssociation( - HydrableObjectInterface $object, - ReflectionClass $class, - ReflectionProperty $property, - ReflectionNamedType $type, - $value - ) : void { - if (!is_array($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property only accepts an array.', - $class->getShortName(), - $property->getName(), - )); - } - - $objectCollectionClassName = $type->getName(); - $objectCollection = new $objectCollectionClassName(); - $collectionObjectClassName = $objectCollectionClassName::T; - foreach ($value as $item) { - $collectionObject = new $collectionObjectClassName(); - $this->hydrate($collectionObject, (array) $item); - $objectCollection->add($collectionObject); - } - - $property->setValue($object, $objectCollection); - } -} diff --git a/src/Hydrator/HydratorInterface.php b/src/Hydrator/HydratorInterface.php deleted file mode 100644 index 7da8639..0000000 --- a/src/Hydrator/HydratorInterface.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * HydratorInterface - */ -interface HydratorInterface -{ - - /** - * Hydrates the given object with the given data - * - * @param HydrableObjectInterface $object - * @param array $data - * - * @return HydrableObjectInterface - * The given object after hydration. - * - * @throws Exception\HydrationException - * If any error occurred during the hydration process. - */ - public function hydrate(HydrableObjectInterface $object, array $data) : HydrableObjectInterface; -} diff --git a/src/Hydrator/Json.php b/src/Hydrator/Json.php deleted file mode 100644 index 403125f..0000000 --- a/src/Hydrator/Json.php +++ /dev/null @@ -1,79 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * Import classes - */ -use ArrayAccess; -use JsonSerializable; - -/** - * Import functions - */ -use function array_key_exists; - -/** - * Json - */ -class Json implements ArrayAccess, JsonSerializable -{ - protected array $data; - - /** - * Constructor of the class - */ - public function __construct(array $data) - { - $this->data = $data; - } - - /** - * {@inheritdoc} - */ - public function offsetExists($offset) : bool - { - return array_key_exists($offset, $this->data); - } - - /** - * {@inheritdoc} - */ - public function offsetGet($offset) - { - return $this->data[$offset] ?? null; - } - - /** - * {@inheritdoc} - */ - public function offsetSet($offset, $value) : void - { - $this->data[$offset] = $value; - } - - /** - * {@inheritdoc} - */ - public function offsetUnset($offset) : void - { - unset($this->data[$offset]); - } - - /** - * {@inheritdoc} - */ - public function jsonSerialize() : array - { - return $this->data; - } -} diff --git a/src/Hydrator/JsonableObjectInterface.php b/src/Hydrator/JsonableObjectInterface.php deleted file mode 100644 index b0d1570..0000000 --- a/src/Hydrator/JsonableObjectInterface.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @copyright Copyright (c) 2021, Anatoly Fenric - * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE - * @link https://github.com/sunrise-php/hydrator - */ - -namespace Sunrise\Hydrator; - -/** - * JsonableObjectInterface - */ -interface JsonableObjectInterface extends HydrableObjectInterface -{ -} diff --git a/src/HydratorInterface.php b/src/HydratorInterface.php new file mode 100644 index 0000000..234c547 --- /dev/null +++ b/src/HydratorInterface.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2021, Anatoly Fenric + * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE + * @link https://github.com/sunrise-php/hydrator + */ + +namespace Sunrise\Hydrator; + +/** + * HydratorInterface + */ +interface HydratorInterface +{ + + /** + * Hydrates the given object with the given data + * + * @param object|class-string $object + * @param array $data + * + * @return object + * + * @throws Exception\HydrationException + * If the given object cannot be hydrated. + */ + public function hydrate($object, array $data) : object; + + /** + * Hydrates the given object with the given JSON + * + * @param object|class-string $object + * @param string $json + * + * @return object + * + * @throws Exception\HydrationException + * If the given object cannot be hydrated. + */ + public function hydrateWithJson($object, string $json) : object; +} diff --git a/src/ObjectCollection.php b/src/ObjectCollection.php new file mode 100644 index 0000000..0975e11 --- /dev/null +++ b/src/ObjectCollection.php @@ -0,0 +1,98 @@ + + * @copyright Copyright (c) 2021, Anatoly Fenric + * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE + * @link https://github.com/sunrise-php/hydrator + */ + +namespace Sunrise\Hydrator; + +/** + * Import classes + */ +use InvalidArgumentException; +use RuntimeException; + +/** + * Import functions + */ +use function sprintf; + +/** + * ObjectCollection + */ +abstract class ObjectCollection implements ObjectCollectionInterface +{ + + /** + * The type of objects in the collection + * + * @var class-string + * + * @template T + */ + public const T = null; + + /** + * The collection objects + * + * @var array + */ + private $objects = []; + + /** + * {@inheritdoc} + * + * @throws RuntimeException + * If the called class doesn't contain the T constant. + */ + final public function getItemClassName() : string + { + if (null === static::T) { + throw new RuntimeException(sprintf( + 'The <%s> collection must contain the constant.', + static::class + )); + } + + return static::T; + } + + /** + * {@inheritdoc} + */ + final public function add($key, object $object) : void + { + $type = $this->getItemClassName(); + + if (!($object instanceof $type)) { + throw new InvalidArgumentException(sprintf( + 'The <%s> collection must contain the <%s> objects only.', + static::class, + $type + )); + } + + $this->objects[$key] = $object; + } + + /** + * {@inheritdoc} + */ + final public function get($key) : ?object + { + return $this->objects[$key] ?? null; + } + + /** + * {@inheritdoc} + */ + final public function all() : array + { + return $this->objects; + } +} diff --git a/src/ObjectCollectionInterface.php b/src/ObjectCollectionInterface.php new file mode 100644 index 0000000..3bf79fd --- /dev/null +++ b/src/ObjectCollectionInterface.php @@ -0,0 +1,62 @@ + + * @copyright Copyright (c) 2021, Anatoly Fenric + * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE + * @link https://github.com/sunrise-php/hydrator + */ + +namespace Sunrise\Hydrator; + +/** + * Import classes + */ +use InvalidArgumentException; + +/** + * ObjectCollectionInterface + */ +interface ObjectCollectionInterface +{ + + /** + * Gets the type of objects in the collection + * + * @return class-string + * + * @template T + */ + public function getItemClassName() : string; + + /** + * Adds the given object to the collection by the given key + * + * @param int|string $key + * @param T $object + * + * @return void + * + * @throws InvalidArgumentException + * If the given object cannot be added to the collection. + */ + public function add($key, object $object) : void; + + /** + * Gets an object from the collection by the given key + * + * @param int|string $key + * + * @return T|null + */ + public function get($key) : ?object; + + /** + * Gets all objects of the collection + * + * @return array + */ + public function all() : array; +} From aba4eca86847794328c4871e4fb9e3f001f28e7d Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 12:01:57 +0500 Subject: [PATCH 03/18] update editorconfig --- .editorconfig | 3 --- 1 file changed, 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index 27e2667..91aebb8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,8 +11,5 @@ indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true -[*.md] -trim_trailing_whitespace = false - [*.yml] indent_size = 2 From 8fa5aff1517a5f6467898ce5aa8943c96820ab35 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 12:02:12 +0500 Subject: [PATCH 04/18] update scrutinizer.yml --- .scrutinizer.yml | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 07e435e..1e6a96f 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,21 +1,16 @@ build: + environment: + php: + version: '8.0' nodes: analysis: - environment: - php: - version: '8.0' tests: override: - php-scrutinizer-run coverage: - environment: - php: - version: '8.0' - ini: - 'xdebug.mode': 'coverage' tests: override: - - command: php vendor/bin/phpunit --coverage-clover coverage.xml + - command: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-clover coverage.xml coverage: file: coverage.xml format: clover From 4123d469889443972d6599c1cd5161ee35a97bc6 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 12:02:21 +0500 Subject: [PATCH 05/18] update phpunit.xml.dist --- phpunit.xml.dist | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 247d1b8..a800850 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,15 +1,12 @@ - + ./src - + ./tests/ From d1ea9069c9fdc5bb6c4b6cf231ad64a274529a25 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 12:02:30 +0500 Subject: [PATCH 06/18] update composer.json --- composer.json | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index da7f4a9..bebae94 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "fenric", "sunrise", "hydrator", + "data-mapper", "mapper", "DTO", "php7", @@ -23,24 +24,28 @@ "php": "^7.4|^8.0" }, "require-dev": { - "phpunit/phpunit": "9.5.3", - "sunrise/coding-standard": "1.0.0", - "doctrine/annotations": "^1.12.1" + "phpunit/phpunit": "~9.5.0", + "sunrise/coding-standard": "~1.0.0", + "doctrine/annotations": "^1.6.0" }, "autoload": { "psr-4": { - "Sunrise\\Hydrator\\": "src/Hydrator" + "Sunrise\\Hydrator\\": "src/" } }, "autoload-dev": { "psr-4": { - "Sunrise\\Hydrator\\Tests\\": "tests/Hydrator" + "Sunrise\\Hydrator\\Tests\\Fixtures\\": "tests/fixtures/" } }, "scripts": { "test": [ "phpcs", - "XDEBUG_MODE=coverage phpunit --colors=always --coverage-text" + "XDEBUG_MODE=coverage phpunit --coverage-text --colors=always" + ], + "build": [ + "phpdoc -d src/ -t phpdoc/", + "XDEBUG_MODE=coverage phpunit --coverage-html coverage/" ] } } From c3a9264b38e7da167cafc702b0ea0f0e50482c34 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 12:02:40 +0500 Subject: [PATCH 07/18] update gitignore --- .gitignore | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 86bfd55..ac89ebd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ -.php_cs.cache -.phpunit.result.cache -composer.lock -coverage.xml -phpbench.json -phpcs.xml -phpunit.xml -vendor/ +/.php_cs.cache +/.phpunit.result.cache +/composer.lock +/coverage.xml +/phpbench.json +/phpcs.xml +/phpunit.xml +/vendor/ From 8831bd174d5c91b60ffacf912da50312bcbbf882 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 12:02:55 +0500 Subject: [PATCH 08/18] update circleci.yml --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 62d05c5..3e81443 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -10,16 +10,16 @@ jobs: steps: - checkout - run: php -v - - run: composer install --no-interaction --prefer-source - - run: php vendor/bin/phpunit --colors=always + - run: composer install --no-interaction --no-suggest --prefer-source + - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text php80: docker: - image: circleci/php:8.0-cli-node-browsers steps: - checkout - run: php -v - - run: composer install --no-interaction --prefer-source - - run: php vendor/bin/phpunit --colors=always + - run: composer install --no-interaction --no-suggest --prefer-source + - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text workflows: version: 2 build: From 8ee3c13d62927b80072487ce4db2c572518d86f3 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 13:35:47 +0500 Subject: [PATCH 09/18] release v2 --- src/Hydrator.php | 56 ++++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/src/Hydrator.php b/src/Hydrator.php index 29eec21..ad47c6e 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -17,7 +17,6 @@ use Doctrine\Common\Annotations\SimpleAnnotationReader; use Sunrise\Hydrator\Annotation\Alias; use InvalidArgumentException; -use DateTimeInterface; use ReflectionClass; use ReflectionProperty; use ReflectionNamedType; @@ -59,6 +58,20 @@ class Hydrator implements HydratorInterface { + /** + * @var array + */ + private const PROPERTY_HYDRATOR_MAP = [ + 'bool' => 'hydratePropertyWithBooleanValue', + 'int' => 'hydratePropertyWithIntegerNumber', + 'float' => 'hydratePropertyWithNumber', + 'string' => 'hydratePropertyWithString', + 'array' => 'hydratePropertyWithArray', + 'object' => 'hydratePropertyWithObject', + 'DateTime' => 'hydratePropertyWithTimestamp', + 'DateTimeImmutable' => 'hydratePropertyWithTimestamp', + ]; + /** * @var SimpleAnnotationReader|null */ @@ -134,12 +147,9 @@ public function hydrate($object, array $data) : object if ($property->getType() instanceof ReflectionUnionType) { throw new Exception\UnsupportedPropertyTypeException(sprintf( - 'The <%s.%s> property contains an unsupported type <%s>.', + 'The <%s.%s> property contains an union type that is not supported.', $class->getShortName(), - $property->getName(), - \implode('|', \array_map(function (ReflectionNamedType $type) : string { - return $type->getName(); - }, $property->getType()->getTypes())) + $property->getName() )); } @@ -279,38 +289,8 @@ private function hydrateProperty( return; } - if ('bool' === $type->getName()) { - $this->hydratePropertyWithBooleanValue($object, $class, $property, $type, $value); - return; - } - - if ('int' === $type->getName()) { - $this->hydratePropertyWithIntegerNumber($object, $class, $property, $type, $value); - return; - } - - if ('float' === $type->getName()) { - $this->hydratePropertyWithNumber($object, $class, $property, $type, $value); - return; - } - - if ('string' === $type->getName()) { - $this->hydratePropertyWithString($object, $class, $property, $type, $value); - return; - } - - if ('array' === $type->getName()) { - $this->hydratePropertyWithArray($object, $class, $property, $type, $value); - return; - } - - if ('object' === $type->getName()) { - $this->hydratePropertyWithObject($object, $class, $property, $type, $value); - return; - } - - if (is_subclass_of($type->getName(), DateTimeInterface::class)) { - $this->hydratePropertyWithTimestamp($object, $class, $property, $type, $value); + if (isset(self::PROPERTY_HYDRATOR_MAP[$type->getName()])) { + $this->{self::PROPERTY_HYDRATOR_MAP[$type->getName()]}($object, $class, $property, $type, $value); return; } From e06465bf3921e34eed129dd8a9af552526f37087 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 13:35:58 +0500 Subject: [PATCH 10/18] improve tests --- tests/HydratorTest.php | 305 ++---------------- tests/ObjectCollectionTest.php | 39 +-- tests/fixtures/{BarDto.php => Bar.php} | 2 +- ...BarDtoCollection.php => BarCollection.php} | 4 +- tests/fixtures/BazDto.php | 17 - tests/fixtures/Foo.php | 34 ++ tests/fixtures/FooDto.php | 29 -- ...pertyTypeDto.php => UnionPropertyType.php} | 4 +- tests/fixtures/UnsupportedPropertyType.php | 10 + ...pedPropertyDto.php => UntypedProperty.php} | 2 +- 10 files changed, 98 insertions(+), 348 deletions(-) rename tests/fixtures/{BarDto.php => Bar.php} (84%) rename tests/fixtures/{BarDtoCollection.php => BarCollection.php} (57%) delete mode 100644 tests/fixtures/BazDto.php create mode 100644 tests/fixtures/Foo.php delete mode 100644 tests/fixtures/FooDto.php rename tests/fixtures/{WithUnsupportedPropertyTypeDto.php => UnionPropertyType.php} (51%) create mode 100644 tests/fixtures/UnsupportedPropertyType.php rename tests/fixtures/{WithUntypedPropertyDto.php => UntypedProperty.php} (74%) diff --git a/tests/HydratorTest.php b/tests/HydratorTest.php index 7128b8b..aff5876 100644 --- a/tests/HydratorTest.php +++ b/tests/HydratorTest.php @@ -1,25 +1,16 @@ -assertInstanceOf(HydratorInterface::class, $hydrator); } - /** - * @return void - */ public function testHydrate() : void { - $data = [ - 'static' => 'foo', - 'nullable' => null, - 'bool' => false, - 'int' => 0, - 'float' => 0.0, - 'string' => 'foo', - 'dateTime' => '2005-08-15T15:52:01.000+00:00', - 'barDto' => [ - 'value' => 'foo', - ], - 'barDtoCollection' => [ - [ - 'value' => 'foo', - ], - [ - 'value' => 'bar', - ], - ], - 'simpleArray' => [ - 'foo', - 'bar', - ], - 'alias' => 'value', - ]; - - $object = new Fixtures\FooDto(); - $hydrator = new Hydrator(); - $hydrator->hydrate($object, $data); - - $this->assertSame('default value', $object::$static); - $this->assertSame('default value', $object->valuable); + $data = []; + $data['statical'] = '813ea72c-6763-4596-a4d6-b478efed61bb'; + $data['nullable'] = null; + $data['required'] = '9f5c273e-1dca-4c2d-ac81-7d6b03b169f4'; + $data['boolean'] = true; + $data['integer'] = 42; + $data['number'] = 123.45; + $data['string'] = 'db7614d4-0a81-437b-b2cf-c536ad229c97'; + $data['array'] = ['foo' => 'bar']; + $data['object'] = (object) ['foo' => 'bar']; + $data['dateTime'] = '2038-01-19 03:14:08'; + $data['dateTimeImmutable'] = '2038-01-19 03:14:08'; + $data['bar'] = ['value' => '9898fb3b-ffb0-406c-bda6-b516423abde7']; + $data['barCollection'][] = ['value' => 'd85c17b6-6e2c-4e2d-9eba-e1dd59b75fe3']; + $data['barCollection'][] = ['value' => '5a8019aa-1c15-4c7c-8beb-1783c3d8996b']; + $data['non-normalized'] = 'f76c4656-431a-4337-9ba9-5440611b37f1'; + + $object = (new Hydrator)->hydrate(Fixtures\Foo::class, $data); + + $this->assertNotSame($data['statical'], $object::$statical); $this->assertSame($data['nullable'], $object->nullable); - $this->assertSame($data['bool'], $object->bool); - $this->assertSame($data['int'], $object->int); - $this->assertSame($data['float'], $object->float); + $this->assertSame($data['required'], $object->required); + $this->assertSame($data['boolean'], $object->boolean); + $this->assertSame($data['integer'], $object->integer); + $this->assertSame($data['number'], $object->number); $this->assertSame($data['string'], $object->string); - $this->assertSame($data['dateTime'], $object->dateTime->format(DateTimeInterface::RFC3339_EXTENDED)); - $this->assertSame($data['barDto']['value'], $object->barDto->value); - $this->assertSame($data['barDtoCollection'][0]['value'], $object->barDtoCollection->get(0)->value); - $this->assertSame($data['barDtoCollection'][1]['value'], $object->barDtoCollection->get(1)->value); - $this->assertSame($data['simpleArray'], $object->simpleArray); - $this->assertSame($data['alias'], $object->hidden); - } - - /** - * @return void - */ - public function testMissingRequiredValueException() : void - { - $object = new Fixtures\BarDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\MissingRequiredValueException::class); - $this->expectExceptionMessage('The property is required.'); - - $hydrator->hydrate($object, [ - ]); - } - - /** - * @return void - */ - public function testUntypedPropertyException() : void - { - $object = new Fixtures\WithUntypedPropertyDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\UntypedPropertyException::class); - $this->expectExceptionMessage('The property is not typed.'); - - $hydrator->hydrate($object, [ - 'value' => 'foo', - ]); - } - - /** - * @return void - */ - public function testUnsupportedPropertyTypeException() : void - { - $object = new Fixtures\WithUnsupportedPropertyTypeDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\UnsupportedPropertyTypeException::class); - $this->expectExceptionMessage('The property ' . - 'contains an unsupported type .'); - - $hydrator->hydrate($object, [ - 'value' => 'foo', - ]); - } - - /** - * @return void - */ - public function testInvalidValueExceptionForNonNullableProperty() : void - { - $object = new Fixtures\BazDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property cannot accept null.'); - - $hydrator->hydrate($object, [ - 'nonNullable' => null, - ]); - } - - /** - * @param mixed $nonScalarValue - * - * @return void - * - * @dataProvider nonScalarDataProvider - */ - public function testInvalidValueExceptionForScalarProperty($nonScalarValue) : void - { - $object = new Fixtures\BazDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property accepts a string only.'); - - $hydrator->hydrate($object, [ - 'scalar' => $nonScalarValue, - ]); - } - - /** - * @param mixed $nonArrayValue - * - * @return void - * - * @dataProvider nonArrayDataProvider - */ - public function testInvalidValueExceptionForArrayProperty($nonArrayValue) : void - { - $object = new Fixtures\BazDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property accepts an array only.'); - - $hydrator->hydrate($object, [ - 'array' => $nonArrayValue, - ]); - } - - /** - * @param mixed $nonDateTimeValue - * - * @return void - * - * @dataProvider nonDateTimeDataProvider - */ - public function testInvalidValueExceptionForDateTimeProperty($nonDateTimeValue) : void - { - $object = new Fixtures\BazDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage( - 'The property accepts a valid date-time string or a timestamp only.' - ); - - $hydrator->hydrate($object, [ - 'dateTime' => $nonDateTimeValue, - ]); - } - - /** - * @param mixed $nonArrayValue - * - * @return void - * - * @dataProvider nonArrayDataProvider - */ - public function testInvalidValueExceptionForOneToOneProperty($nonArrayValue) : void - { - $object = new Fixtures\BazDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property accepts an array only.'); - - $hydrator->hydrate($object, [ - 'oneToOne' => $nonArrayValue, - ]); - } - - /** - * @param mixed $nonArrayValue - * - * @return void - * - * @dataProvider nonArrayDataProvider - */ - public function testInvalidValueExceptionForOneToManyProperty($nonArrayValue) : void - { - $object = new Fixtures\BazDto(); - $hydrator = new Hydrator(); - - $this->expectException(Exception\InvalidValueException::class); - $this->expectExceptionMessage('The property accepts an array only.'); - - $hydrator->hydrate($object, [ - 'oneToMany' => $nonArrayValue, - ]); - } - - /** - * @return array - */ - public function nonScalarDataProvider() : array - { - return [ - [[]], - [new \stdClass], - [function () { - }], - [\STDOUT], - ]; - } - - /** - * @return array - */ - public function nonArrayDataProvider() : array - { - return [ - [true], - [1], - [1.1], - [''], - [new \stdClass], - [function () { - }], - [\STDOUT], - ]; - } - - /** - * @return array - */ - public function nonDateTimeDataProvider() : array - { - return [ - [true], - [1.1], - [''], - ['non-date-time-string'], - [new \stdClass], - [function () { - }], - [\STDOUT], - ]; + $this->assertSame($data['array'], $object->array); + $this->assertSame($data['object'], $object->object); + $this->assertSame($data['dateTime'], $object->dateTime->format('Y-m-d H:i:s')); + $this->assertSame($data['dateTimeImmutable'], $object->dateTimeImmutable->format('Y-m-d H:i:s')); + $this->assertSame($data['bar']['value'], $object->bar->value); + $this->assertSame($data['barCollection'][0]['value'], $object->barCollection->get(0)->value); + $this->assertSame($data['barCollection'][1]['value'], $object->barCollection->get(1)->value); + $this->assertSame($data['non-normalized'], $object->normalized); } } diff --git a/tests/ObjectCollectionTest.php b/tests/ObjectCollectionTest.php index 030fd16..74b38a2 100644 --- a/tests/ObjectCollectionTest.php +++ b/tests/ObjectCollectionTest.php @@ -1,43 +1,33 @@ -assertInstanceOf(ObjectCollectionInterface::class, $collection); } - /** - * @return void - */ public function testAdd() : void { $store = [ - new Fixtures\BarDto(), - new Fixtures\BarDto(), - new Fixtures\BarDto(), + new Fixtures\Bar(), + new Fixtures\Bar(), + new Fixtures\Bar(), ]; - $collection = new Fixtures\BarDtoCollection(); + $collection = new Fixtures\BarCollection(); $collection->add(0, $store[0]); $collection->add(1, $store[1]); @@ -46,17 +36,14 @@ public function testAdd() : void $this->assertSame($store, $collection->all()); } - /** - * @return void - */ public function testUnexpectedObject() : void { - $collection = new Fixtures\BarDtoCollection(); + $collection = new Fixtures\BarCollection(); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The <' . Fixtures\BarDtoCollection::class . '> collection ' . - 'must contain the <' . Fixtures\BarDto::class . '> objects only.'); + $this->expectExceptionMessage('The <' . Fixtures\BarCollection::class . '> collection ' . + 'must contain the <' . Fixtures\Bar::class . '> objects only.'); - $collection->add(0, new Fixtures\BazDto()); + $collection->add(0, new Fixtures\Foo()); } } diff --git a/tests/fixtures/BarDto.php b/tests/fixtures/Bar.php similarity index 84% rename from tests/fixtures/BarDto.php rename to tests/fixtures/Bar.php index bb0922f..75546ff 100644 --- a/tests/fixtures/BarDto.php +++ b/tests/fixtures/Bar.php @@ -4,7 +4,7 @@ namespace Sunrise\Hydrator\Tests\Fixtures; -final class BarDto +final class Bar { public string $value; } diff --git a/tests/fixtures/BarDtoCollection.php b/tests/fixtures/BarCollection.php similarity index 57% rename from tests/fixtures/BarDtoCollection.php rename to tests/fixtures/BarCollection.php index 748f1da..7effd1b 100644 --- a/tests/fixtures/BarDtoCollection.php +++ b/tests/fixtures/BarCollection.php @@ -6,7 +6,7 @@ use Sunrise\Hydrator\ObjectCollection; -final class BarDtoCollection extends ObjectCollection +final class BarCollection extends ObjectCollection { - public const T = BarDto::class; + public const T = Bar::class; } diff --git a/tests/fixtures/BazDto.php b/tests/fixtures/BazDto.php deleted file mode 100644 index 808fc4c..0000000 --- a/tests/fixtures/BazDto.php +++ /dev/null @@ -1,17 +0,0 @@ - Date: Fri, 22 Oct 2021 17:04:42 +0500 Subject: [PATCH 11/18] release v2 --- src/Annotation/Alias.php | 2 - src/Exception/InvalidValueException.php | 42 ++++++++++++++ src/Hydrator.php | 76 +++++++++++-------------- src/ObjectCollection.php | 4 +- 4 files changed, 76 insertions(+), 48 deletions(-) diff --git a/src/Annotation/Alias.php b/src/Annotation/Alias.php index 6d7f41f..fa01568 100644 --- a/src/Annotation/Alias.php +++ b/src/Annotation/Alias.php @@ -17,8 +17,6 @@ use Attribute; /** - * A property alias - * * @Annotation * * @Target({"PROPERTY"}) diff --git a/src/Exception/InvalidValueException.php b/src/Exception/InvalidValueException.php index 946947d..5893b5e 100644 --- a/src/Exception/InvalidValueException.php +++ b/src/Exception/InvalidValueException.php @@ -11,9 +11,51 @@ namespace Sunrise\Hydrator\Exception; +/** + * Import classes + */ +use ReflectionProperty; +use Throwable; + /** * InvalidValueException */ class InvalidValueException extends HydrationException { + + /** + * The problem property + * + * @var ReflectionProperty + */ + private $property; + + /** + * Constructor of the class + * + * @param ReflectionProperty $property + * @param string $message + * @param int $code + * @param Throwable|null $previous + */ + public function __construct( + ReflectionProperty $property, + string $message, + int $code = 0, + ?Throwable $previous = null + ) { + $this->property = $property; + + parent::__construct($message, $code, $previous); + } + + /** + * Gets the problem property + * + * @return ReflectionProperty + */ + public function getProperty() : ReflectionProperty + { + return $this->property; + } } diff --git a/src/Hydrator.php b/src/Hydrator.php index ad47c6e..79e1b22 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -77,18 +77,6 @@ class Hydrator implements HydratorInterface */ private $annotationReader = null; - /** - * Constructor of the class - */ - public function __construct() - { - if (PHP_MAJOR_VERSION < 8) { - // @codeCoverageIgnoreStart - $this->useAnnotations(); - // @codeCoverageIgnoreEnd - } - } - /** * Enables support for annotations * @@ -139,7 +127,7 @@ public function hydrate($object, array $data) : object if (!$property->hasType()) { throw new Exception\UntypedPropertyException(sprintf( - 'The <%s.%s> property is not typed.', + 'The %s.%s property is not typed.', $class->getShortName(), $property->getName() )); @@ -147,7 +135,7 @@ public function hydrate($object, array $data) : object if ($property->getType() instanceof ReflectionUnionType) { throw new Exception\UnsupportedPropertyTypeException(sprintf( - 'The <%s.%s> property contains an union type that is not supported.', + 'The %s.%s property contains an union type that is not supported.', $class->getShortName(), $property->getName() )); @@ -163,8 +151,8 @@ public function hydrate($object, array $data) : object if (!array_key_exists($key, $data)) { if (!$property->isInitialized($object)) { - throw new Exception\MissingRequiredValueException(sprintf( - 'The <%s.%s> property is required.', + throw new Exception\MissingRequiredValueException($property, sprintf( + 'The %s.%s property is required.', $class->getShortName(), $property->getName() )); @@ -305,7 +293,7 @@ private function hydrateProperty( } throw new Exception\UnsupportedPropertyTypeException(sprintf( - 'The <%s.%s> property contains an unsupported type <%s>.', + 'The %s.%s property contains an unsupported type %s.', $class->getShortName(), $property->getName(), $type->getName() @@ -332,8 +320,8 @@ private function hydratePropertyWithNull( ReflectionNamedType $type ) : void { if (!$type->allowsNull()) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property cannot accept null.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property cannot accept null.', $class->getShortName(), $property->getName() )); @@ -370,8 +358,8 @@ private function hydratePropertyWithBooleanValue( $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); if (!isset($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts a boolean value only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts a boolean value only.', $class->getShortName(), $property->getName() )); @@ -411,8 +399,8 @@ private function hydratePropertyWithIntegerNumber( $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); if (!isset($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts an integer number only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts an integer number only.', $class->getShortName(), $property->getName() )); @@ -450,8 +438,8 @@ private function hydratePropertyWithNumber( $value = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE); if (!isset($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts a number only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts a number only.', $class->getShortName(), $property->getName() )); @@ -483,8 +471,8 @@ private function hydratePropertyWithString( $value ) : void { if (!is_string($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts a string only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts a string only.', $class->getShortName(), $property->getName() )); @@ -515,8 +503,8 @@ private function hydratePropertyWithArray( $value ) : void { if (!is_array($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts an array only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts an array only.', $class->getShortName(), $property->getName() )); @@ -547,8 +535,8 @@ private function hydratePropertyWithObject( $value ) : void { if (!is_object($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts an object only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts an object only.', $class->getShortName(), $property->getName() )); @@ -578,20 +566,20 @@ private function hydratePropertyWithTimestamp( ReflectionNamedType $type, $value ) : void { - $target = $type->getName(); + $prototype = $type->getName(); if (is_int($value) || ctype_digit($value)) { - $property->setValue($object, (new $target)->setTimestamp($value)); + $property->setValue($object, (new $prototype)->setTimestamp((int) $value)); return; } if (is_string($value) && false !== strtotime($value)) { - $property->setValue($object, new $target($value)); + $property->setValue($object, new $prototype($value)); return; } - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts a valid date-time string or a timestamp only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts a valid date-time string or a timestamp only.', $class->getShortName(), $property->getName() )); @@ -619,19 +607,19 @@ private function hydratePropertyWithManyAssociations( $value ) : void { if (!is_array($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts an array only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts an array only.', $class->getShortName(), $property->getName() )); } - $target = $type->getName(); - $collection = new $target(); + $prototype = $type->getName(); + $collection = new $prototype(); foreach ($value as $key => $item) { if (!is_array($item)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts an array with arrays only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts an array with arrays only.', $class->getShortName(), $property->getName() )); @@ -665,8 +653,8 @@ private function hydratePropertyWithOneAssociation( $value ) : void { if (!is_array($value)) { - throw new Exception\InvalidValueException(sprintf( - 'The <%s.%s> property accepts an array only.', + throw new Exception\InvalidValueException($property, sprintf( + 'The %s.%s property accepts an array only.', $class->getShortName(), $property->getName() )); diff --git a/src/ObjectCollection.php b/src/ObjectCollection.php index 0975e11..04c2a4b 100644 --- a/src/ObjectCollection.php +++ b/src/ObjectCollection.php @@ -54,7 +54,7 @@ final public function getItemClassName() : string { if (null === static::T) { throw new RuntimeException(sprintf( - 'The <%s> collection must contain the constant.', + 'The %s collection must contain the T constant.', static::class )); } @@ -71,7 +71,7 @@ final public function add($key, object $object) : void if (!($object instanceof $type)) { throw new InvalidArgumentException(sprintf( - 'The <%s> collection must contain the <%s> objects only.', + 'The %s collection can contain the %s objects only.', static::class, $type )); From 87297a812577fcdfd11e9c8c0afef4ea5ce6a1e2 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 17:04:57 +0500 Subject: [PATCH 12/18] improve tests --- tests/HydratorTest.php | 260 +++++++++++++++++- tests/ObjectCollectionTest.php | 15 +- .../fixtures/ObjectWithAnnotatedProperty.php | 12 + tests/fixtures/ObjectWithArrayProperty.php | 10 + tests/fixtures/ObjectWithAssociation.php | 10 + tests/fixtures/ObjectWithAssociations.php | 10 + tests/fixtures/ObjectWithBooleanProperty.php | 10 + tests/fixtures/ObjectWithIntegerProperty.php | 10 + tests/fixtures/ObjectWithNumberProperty.php | 10 + tests/fixtures/ObjectWithObjectProperty.php | 10 + tests/fixtures/ObjectWithRequiredProperty.php | 10 + tests/fixtures/ObjectWithStringProperty.php | 10 + .../fixtures/ObjectWithTimestampProperty.php | 12 + ...pe.php => ObjectWithUnionPropertyType.php} | 2 +- ... => ObjectWithUnsupportedPropertyType.php} | 2 +- ...erty.php => ObjectWithUntypedProperty.php} | 2 +- tests/fixtures/UninitializableObject.php | 12 + tests/fixtures/UntypedCollection.php | 11 + 18 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/ObjectWithAnnotatedProperty.php create mode 100644 tests/fixtures/ObjectWithArrayProperty.php create mode 100644 tests/fixtures/ObjectWithAssociation.php create mode 100644 tests/fixtures/ObjectWithAssociations.php create mode 100644 tests/fixtures/ObjectWithBooleanProperty.php create mode 100644 tests/fixtures/ObjectWithIntegerProperty.php create mode 100644 tests/fixtures/ObjectWithNumberProperty.php create mode 100644 tests/fixtures/ObjectWithObjectProperty.php create mode 100644 tests/fixtures/ObjectWithRequiredProperty.php create mode 100644 tests/fixtures/ObjectWithStringProperty.php create mode 100644 tests/fixtures/ObjectWithTimestampProperty.php rename tests/fixtures/{UnionPropertyType.php => ObjectWithUnionPropertyType.php} (73%) rename tests/fixtures/{UnsupportedPropertyType.php => ObjectWithUnsupportedPropertyType.php} (70%) rename tests/fixtures/{UntypedProperty.php => ObjectWithUntypedProperty.php} (72%) create mode 100644 tests/fixtures/UninitializableObject.php create mode 100644 tests/fixtures/UntypedCollection.php diff --git a/tests/HydratorTest.php b/tests/HydratorTest.php index aff5876..40081b7 100644 --- a/tests/HydratorTest.php +++ b/tests/HydratorTest.php @@ -8,6 +8,7 @@ use Sunrise\Hydrator\Exception; use Sunrise\Hydrator\Hydrator; use Sunrise\Hydrator\HydratorInterface; +use InvalidArgumentException; class HydratorTest extends TestCase { @@ -37,7 +38,10 @@ public function testHydrate() : void $data['barCollection'][] = ['value' => '5a8019aa-1c15-4c7c-8beb-1783c3d8996b']; $data['non-normalized'] = 'f76c4656-431a-4337-9ba9-5440611b37f1'; - $object = (new Hydrator)->hydrate(Fixtures\Foo::class, $data); + $object = (new Hydrator) + ->useAnnotations() + ->useAnnotations() // CC + ->hydrate(new Fixtures\Foo(), $data); $this->assertNotSame($data['statical'], $object::$statical); $this->assertSame($data['nullable'], $object->nullable); @@ -55,4 +59,258 @@ public function testHydrate() : void $this->assertSame($data['barCollection'][1]['value'], $object->barCollection->get(1)->value); $this->assertSame($data['non-normalized'], $object->normalized); } + + public function testHydrateUndefinedObject() : void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The method ' . Hydrator::class . '::hydrate() ' . + 'expects an object or name of an existing class.'); + + (new Hydrator)->hydrate('Undefined', []); + } + + public function testHydrateUninitializableObject() : void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The object ' . Fixtures\UninitializableObject::class . ' ' . + 'cannot be hydrated because its constructor has required parameters.'); + + (new Hydrator)->hydrate(Fixtures\UninitializableObject::class, []); + } + + public function testHydrateWithJson() : void + { + $json = '{"value": "4c1e3453-7b76-4d5d-b4b8-bc6b0afcd835"}'; + + $object = (new Hydrator)->useAnnotations()->hydrateWithJson(Fixtures\Bar::class, $json); + + $this->assertSame($object->value, '4c1e3453-7b76-4d5d-b4b8-bc6b0afcd835'); + } + + public function testHydrateWithInvalidJson() : void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to decode JSON: Syntax error'); + + (new Hydrator)->useAnnotations()->hydrateWithJson(Fixtures\Bar::class, '!'); + } + + public function testUntypedProperty() : void + { + $this->expectException(Exception\UntypedPropertyException::class); + $this->expectExceptionMessage('The ObjectWithUntypedProperty.value property is not typed.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithUntypedProperty::class, []); + } + + public function testUnsupportedPropertyType() : void + { + $this->expectException(Exception\UnsupportedPropertyTypeException::class); + $this->expectExceptionMessage('The ObjectWithUnsupportedPropertyType.value property ' . + 'contains an unsupported type iterable.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithUnsupportedPropertyType::class, [ + 'value' => 'b25c08e8-4771-42c0-b01b-fe7fd3689602', + ]); + } + + public function testUnionPropertyType() : void + { + $this->expectException(Exception\UnsupportedPropertyTypeException::class); + $this->expectExceptionMessage('The ObjectWithUnionPropertyType.value property ' . + 'contains an union type that is not supported.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithUnionPropertyType::class, []); + } + + public function testRequiredProperty() : void + { + $this->expectException(Exception\MissingRequiredValueException::class); + $this->expectExceptionMessage('The ObjectWithRequiredProperty.value property ' . + 'is required.'); + + try { + (new Hydrator)->hydrate(Fixtures\ObjectWithRequiredProperty::class, []); + } catch (Exception\MissingRequiredValueException $e) { + $this->assertSame('value', $e->getProperty()->getName()); + + throw $e; + } + } + + public function testAnnotatedProperty() : void + { + $data = [ + 'non-normalized-value' => '87019bbd-643b-45b8-94f8-0abd56be9851', + ]; + + $object = (new Hydrator)->useAnnotations()->hydrate(Fixtures\ObjectWithAnnotatedProperty::class, $data); + + $this->assertSame($object->value, '87019bbd-643b-45b8-94f8-0abd56be9851'); + } + + public function testUnnullableProperty() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The Bar.value property cannot accept null.'); + + (new Hydrator)->hydrate(Fixtures\Bar::class, [ + 'value' => null, + ]); + } + + public function testHydratePropertyWithStringBooleanValue() : void + { + $object = (new Hydrator)->hydrate(Fixtures\ObjectWithBooleanProperty::class, [ + 'value' => 'yes', + ]); + + $this->assertSame(true, $object->value); + } + + public function testHydratePropertyWithStringIntegerNumber() : void + { + $object = (new Hydrator)->hydrate(Fixtures\ObjectWithIntegerProperty::class, [ + 'value' => '42', + ]); + + $this->assertSame(42, $object->value); + } + + public function testHydratePropertyWithStringableNumber() : void + { + $object = (new Hydrator)->hydrate(Fixtures\ObjectWithNumberProperty::class, [ + 'value' => '123.45', + ]); + + $this->assertSame(123.45, $object->value); + } + + public function testHydratePropertyWithIntegerTimestamp() : void + { + $object = (new Hydrator)->hydrate(Fixtures\ObjectWithTimestampProperty::class, [ + 'value' => 1262304000, + ]); + + $this->assertSame('2010-01-01', $object->value->format('Y-m-d')); + } + + public function testHydratePropertyWithStringIntegerTimestamp() : void + { + $object = (new Hydrator)->hydrate(Fixtures\ObjectWithTimestampProperty::class, [ + 'value' => '1262304000', + ]); + + $this->assertSame('2010-01-01', $object->value->format('Y-m-d')); + } + + public function testHydratePropertyWithStringDateTime() : void + { + $object = (new Hydrator)->hydrate(Fixtures\ObjectWithTimestampProperty::class, [ + 'value' => '2010-01-01', + ]); + + $this->assertSame('2010-01-01', $object->value->format('Y-m-d')); + } + + public function testHydratePropertyWithInvalidBooleanValue() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithBooleanProperty.value property accepts a boolean value only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithBooleanProperty::class, [ + 'value' => [], + ]); + } + + public function testHydratePropertyWithInvalidIntegerNumber() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithIntegerProperty.value property accepts an integer number only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithIntegerProperty::class, [ + 'value' => [], + ]); + } + + public function testHydratePropertyWithInvalidNumber() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithNumberProperty.value property accepts a number only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithNumberProperty::class, [ + 'value' => [], + ]); + } + + public function testHydratePropertyWithInvalidString() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithStringProperty.value property accepts a string only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithStringProperty::class, [ + 'value' => [], + ]); + } + + public function testHydratePropertyWithInvalidArray() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithArrayProperty.value property accepts an array only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithArrayProperty::class, [ + 'value' => 0, + ]); + } + + public function testHydratePropertyWithInvalidObject() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithObjectProperty.value property accepts an object only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithObjectProperty::class, [ + 'value' => 0, + ]); + } + + public function testHydratePropertyWithInvalidTimestamp() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithTimestampProperty.value property ' . + 'accepts a valid date-time string or a timestamp only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithTimestampProperty::class, [ + 'value' => [], + ]); + } + + public function testHydratePropertyWithInvalidAssociation() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithAssociation.value property accepts an array only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithAssociation::class, [ + 'value' => 0, + ]); + } + + public function testHydratePropertyWithInvalidAssociations() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithAssociations.value property accepts an array only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithAssociations::class, [ + 'value' => 0, + ]); + } + + public function testHydratePropertyWithInvalidOneOfAssociations() : void + { + $this->expectException(Exception\InvalidValueException::class); + $this->expectExceptionMessage('The ObjectWithAssociations.value property accepts an array with arrays only.'); + + (new Hydrator)->hydrate(Fixtures\ObjectWithAssociations::class, [ + 'value' => [0], + ]); + } } diff --git a/tests/ObjectCollectionTest.php b/tests/ObjectCollectionTest.php index 74b38a2..ac48873 100644 --- a/tests/ObjectCollectionTest.php +++ b/tests/ObjectCollectionTest.php @@ -36,13 +36,24 @@ public function testAdd() : void $this->assertSame($store, $collection->all()); } + public function testUntypedCollection() : void + { + $collection = new Fixtures\UntypedCollection(); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The ' . Fixtures\UntypedCollection::class . ' collection ' . + 'must contain the T constant.'); + + $collection->add(0, new Fixtures\Bar()); + } + public function testUnexpectedObject() : void { $collection = new Fixtures\BarCollection(); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('The <' . Fixtures\BarCollection::class . '> collection ' . - 'must contain the <' . Fixtures\Bar::class . '> objects only.'); + $this->expectExceptionMessage('The ' . Fixtures\BarCollection::class . ' collection ' . + 'can contain the ' . Fixtures\Bar::class . ' objects only.'); $collection->add(0, new Fixtures\Foo()); } diff --git a/tests/fixtures/ObjectWithAnnotatedProperty.php b/tests/fixtures/ObjectWithAnnotatedProperty.php new file mode 100644 index 0000000..dd6d394 --- /dev/null +++ b/tests/fixtures/ObjectWithAnnotatedProperty.php @@ -0,0 +1,12 @@ + Date: Fri, 22 Oct 2021 17:19:22 +0500 Subject: [PATCH 13/18] release v2 --- src/Hydrator.php | 2 +- src/ObjectCollection.php | 6 ++---- src/ObjectCollectionInterface.php | 10 ++++------ 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Hydrator.php b/src/Hydrator.php index 79e1b22..9c7aca3 100644 --- a/src/Hydrator.php +++ b/src/Hydrator.php @@ -90,7 +90,7 @@ public function useAnnotations() : self if (class_exists(SimpleAnnotationReader::class)) { $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader(); - $this->annotationReader->addNamespace(Annotation::class); + $this->annotationReader->addNamespace('Sunrise\Hydrator\Annotation'); } return $this; diff --git a/src/ObjectCollection.php b/src/ObjectCollection.php index 04c2a4b..3d33cfc 100644 --- a/src/ObjectCollection.php +++ b/src/ObjectCollection.php @@ -31,16 +31,14 @@ abstract class ObjectCollection implements ObjectCollectionInterface /** * The type of objects in the collection * - * @var class-string - * - * @template T + * @var class-string */ public const T = null; /** * The collection objects * - * @var array + * @var array */ private $objects = []; diff --git a/src/ObjectCollectionInterface.php b/src/ObjectCollectionInterface.php index 3bf79fd..fae97fa 100644 --- a/src/ObjectCollectionInterface.php +++ b/src/ObjectCollectionInterface.php @@ -25,9 +25,7 @@ interface ObjectCollectionInterface /** * Gets the type of objects in the collection * - * @return class-string - * - * @template T + * @return class-string */ public function getItemClassName() : string; @@ -35,7 +33,7 @@ public function getItemClassName() : string; * Adds the given object to the collection by the given key * * @param int|string $key - * @param T $object + * @param object $object * * @return void * @@ -49,14 +47,14 @@ public function add($key, object $object) : void; * * @param int|string $key * - * @return T|null + * @return object|null */ public function get($key) : ?object; /** * Gets all objects of the collection * - * @return array + * @return array */ public function all() : array; } From 0fb1fa98a6c0469c43c8913e8617e9564f9d9eb0 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 17:34:47 +0500 Subject: [PATCH 14/18] update README.md --- README.md | 133 ++++++++++++++++++++++++------------------------------ 1 file changed, 59 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 2ddabb3..aadbdd8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![Build Status](https://circleci.com/gh/sunrise-php/hydrator.svg?style=shield)](https://circleci.com/gh/sunrise-php/hydrator) [![Code Coverage](https://scrutinizer-ci.com/g/sunrise-php/hydrator/badges/coverage.png?b=main)](https://scrutinizer-ci.com/g/sunrise-php/hydrator/?branch=main) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sunrise-php/hydrator/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/sunrise-php/hydrator/?branch=main) [![Total Downloads](https://poser.pugx.org/sunrise/hydrator/downloads?format=flat)](https://packagist.org/packages/sunrise/hydrator) [![Latest Stable Version](https://poser.pugx.org/sunrise/hydrator/v/stable?format=flat)](https://packagist.org/packages/sunrise/hydrator) [![License](https://poser.pugx.org/sunrise/hydrator/license?format=flat)](https://packagist.org/packages/sunrise/hydrator) @@ -14,104 +13,90 @@ ## Installation ```bash -composer require 'sunrise/hydrator:^1.2' +composer require 'sunrise/hydrator:^2.0' ``` ## How to use? ```php -$payload = [ - 'nullable' => null, - 'bool' => true, - 'int' => 1, - 'float' => 1.1, - 'string' => 'foo', - 'array' => [], - 'dateTime' => '2005-08-15T15:52:01.000+00:00', - 'barDto' => [ - 'value' => 'foo', - ], - 'barDtoCollection' => [ - [ - 'value' => 'foo', - ], - [ - 'value' => 'bar', - ], - ], - 'enumerableValue' => 'FOO', -]; +$object = (new \Sunrise\Hydrator\Hydrator)->hydrate(Foo::class, $data); + +var_dump($object); ``` ```php -use Sunrise\Hydrator\HydrableObjectInterface; -use ArrayIterator; -use DateTimeImmutable; - -final class FooDto implements HydrableObjectInterface +final class Foo { - public string $optional = 'default value'; - public ?string $nullable; - public bool $bool; - public int $int; - public float $float; - public string $string; - public ArrayIterator $array; - public DateTimeImmutable $dateTime; - public BarDto $barDto; - public BarDtoCollection $barDtoCollection; - public TestEnumerableObject $enumerableValue; -} -``` + // statical properties will ignored + public static string $statical = '50f4e382-2858-4991-b045-a121004cec80'; -```php -use Sunrise\Hydrator\HydrableObjectInterface; + private ?string $nullable = 'ed0110a9-01ac-4f75-a205-223c98d2d2b5'; + private string $valuable = '5bf11aa0-08b3-4429-a6d7-4ebf6d70919c'; + private string $required; -final class BarDto implements HydrableObjectInterface -{ - public string $value; -} -``` + private bool $boolean; // also accepts strings (1, on, yes, etc.) + private int $integer; // also accepts string numbers + private float $number; // also accepts string numbers + private string $string; + private array $array; + private object $object; -```php -use Sunrise\Hydrator\HydrableObjectCollection; + private \DateTime $dateTime; // accepts timestamps and string date-time + private \DateTimeImmutable $dateTimeImmutable; // accepts timestamps and string date-time -final class BarDtoCollection extends HydrableObjectCollection -{ - public const T = BarDto::class; + private Bar $bar; + private BarCollection $barCollection; + + /** + * @Alias("non-normalized") + */ + #[Alias('non-normalized')] + private string $normalized; + + // getters... } ``` ```php -use Sunrise\Hydrator\EnumerableObject; - -final class TestEnumerableObject extends EnumerableObject +final class Bar { - public const FOO = 'foo'; + public string $value; } ``` ```php -use Sunrise\Hydrator\Hydrator; - -$dto = new FooDto(); - -(new Hydrator)->hydrate($dto, $payload); - -var_dump($dto); +final class BarCollection extends ObjectCollection +{ + public const T = Bar::class; +} ``` -### Property aliases - ```php -final class FooDto implements HydrableObjectInterface -{ - - /** - * @Alias("snake_case") - */ - public string $camelCase; -} +$data = [ + 'statical' => '813ea72c-6763-4596-a4d6-b478efed61bb', + 'nullable' => null, + 'required' => '9f5c273e-1dca-4c2d-ac81-7d6b03b169f4', + 'boolean' => true, + 'integer' => 42, + 'number' => 123.45, + 'string' => 'db7614d4-0a81-437b-b2cf-c536ad229c97', + 'array' => ['foo' => 'bar'], + 'object' => (object) ['foo' => 'bar'], + 'dateTime' => '2038-01-19 03:14:08', + 'dateTimeImmutable' => '2038-01-19 03:14:08', + 'bar' => [ + 'value' => '9898fb3b-ffb0-406c-bda6-b516423abde7', + ], + 'barCollection' => [ + [ + 'value' => 'd85c17b6-6e2c-4e2d-9eba-e1dd59b75fe3', + ], + [ + 'value' => '5a8019aa-1c15-4c7c-8beb-1783c3d8996b', + ], + ], + 'non-normalized' => 'f76c4656-431a-4337-9ba9-5440611b37f1', +]; ``` --- From 8fefe3981b8dbcf833ce26e8e3d862ae44edfadb Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 18:51:03 +0500 Subject: [PATCH 15/18] update README.md --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aadbdd8..020483f 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,13 @@ composer require 'sunrise/hydrator:^2.0' ## How to use? ```php +// hydrate an object with array: $object = (new \Sunrise\Hydrator\Hydrator)->hydrate(Foo::class, $data); +// or you can hydrate the object with JSON: +$object = (new \Sunrise\Hydrator\Hydrator)->hydrate(Foo::class, $json); + +// output the result: var_dump($object); ``` @@ -44,8 +49,8 @@ final class Foo private \DateTime $dateTime; // accepts timestamps and string date-time private \DateTimeImmutable $dateTimeImmutable; // accepts timestamps and string date-time - private Bar $bar; - private BarCollection $barCollection; + private Bar $bar; // see bellow... + private BarCollection $barCollection; // see bellow... /** * @Alias("non-normalized") @@ -65,8 +70,11 @@ final class Bar ``` ```php +use Sunrise\Hydrator\ObjectCollection; + final class BarCollection extends ObjectCollection { + // the collection will contain only the specified objects public const T = Bar::class; } ``` From 026963225c44ef13916c9ebe472e71aabecd913f Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 18:51:43 +0500 Subject: [PATCH 16/18] release v2 --- src/Exception/InvalidValueException.php | 4 +++- src/ObjectCollection.php | 2 +- src/ObjectCollectionInterface.php | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Exception/InvalidValueException.php b/src/Exception/InvalidValueException.php index 5893b5e..3e13f2e 100644 --- a/src/Exception/InvalidValueException.php +++ b/src/Exception/InvalidValueException.php @@ -44,6 +44,8 @@ public function __construct( int $code = 0, ?Throwable $previous = null ) { + $property->setAccessible(false); + $this->property = $property; parent::__construct($message, $code, $previous); @@ -54,7 +56,7 @@ public function __construct( * * @return ReflectionProperty */ - public function getProperty() : ReflectionProperty + final public function getProperty() : ReflectionProperty { return $this->property; } diff --git a/src/ObjectCollection.php b/src/ObjectCollection.php index 3d33cfc..c645ead 100644 --- a/src/ObjectCollection.php +++ b/src/ObjectCollection.php @@ -38,7 +38,7 @@ abstract class ObjectCollection implements ObjectCollectionInterface /** * The collection objects * - * @var array + * @var array */ private $objects = []; diff --git a/src/ObjectCollectionInterface.php b/src/ObjectCollectionInterface.php index fae97fa..f0747e5 100644 --- a/src/ObjectCollectionInterface.php +++ b/src/ObjectCollectionInterface.php @@ -32,7 +32,7 @@ public function getItemClassName() : string; /** * Adds the given object to the collection by the given key * - * @param int|string $key + * @param array-key $key * @param object $object * * @return void @@ -45,7 +45,7 @@ public function add($key, object $object) : void; /** * Gets an object from the collection by the given key * - * @param int|string $key + * @param array-key $key * * @return object|null */ @@ -54,7 +54,7 @@ public function get($key) : ?object; /** * Gets all objects of the collection * - * @return array + * @return array */ public function all() : array; } From ea2161135d96b2e62fa177192ec7c7b9907a5b90 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 18:58:33 +0500 Subject: [PATCH 17/18] update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 020483f..8dc66dd 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![Build Status](https://circleci.com/gh/sunrise-php/hydrator.svg?style=shield)](https://circleci.com/gh/sunrise-php/hydrator) [![Code Coverage](https://scrutinizer-ci.com/g/sunrise-php/hydrator/badges/coverage.png?b=main)](https://scrutinizer-ci.com/g/sunrise-php/hydrator/?branch=main) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/sunrise-php/hydrator/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/sunrise-php/hydrator/?branch=main) [![Total Downloads](https://poser.pugx.org/sunrise/hydrator/downloads?format=flat)](https://packagist.org/packages/sunrise/hydrator) [![Latest Stable Version](https://poser.pugx.org/sunrise/hydrator/v/stable?format=flat)](https://packagist.org/packages/sunrise/hydrator) [![License](https://poser.pugx.org/sunrise/hydrator/license?format=flat)](https://packagist.org/packages/sunrise/hydrator) From d9aee29d59e0360e608972d4fafef4e3871dd8bc Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 22 Oct 2021 18:58:42 +0500 Subject: [PATCH 18/18] improve tests --- tests/HydratorTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/HydratorTest.php b/tests/HydratorTest.php index 40081b7..ce08905 100644 --- a/tests/HydratorTest.php +++ b/tests/HydratorTest.php @@ -116,6 +116,11 @@ public function testUnsupportedPropertyType() : void public function testUnionPropertyType() : void { + if (8 > \PHP_MAJOR_VERSION) { + $this->markTestSkipped('PHP 8 is required...'); + return; + } + $this->expectException(Exception\UnsupportedPropertyTypeException::class); $this->expectExceptionMessage('The ObjectWithUnionPropertyType.value property ' . 'contains an union type that is not supported.');