From d56b1c2c145473822c4d562218fe3d94b6140f98 Mon Sep 17 00:00:00 2001 From: matapatos Date: Tue, 3 Jun 2025 08:17:10 +0100 Subject: [PATCH] feat: Support array of types --- composer.lock | 12 +- src/{ErrorInfo.php => ErrorHolder.php} | 23 +- src/Exceptions/BaseException.php | 12 +- src/Property.php | 10 +- src/Types/ArrArr.php | 10 + src/Types/ArrayOf.php | 19 ++ src/Types/BoolArr.php | 10 + src/Types/FloatArr.php | 10 + src/Types/IntArr.php | 10 + src/Types/ObjArr.php | 10 + src/Types/StrArr.php | 10 + src/Validator.php | 6 +- src/Validators/AttributesValidator.php | 4 +- src/Validators/ChainValidator.php | 4 +- src/Validators/TypeHintValidator.php | 7 +- src/Validators/Types/ArrayObject.php | 113 ++++++++ tests/Benchmark/Benchmark.php | 21 ++ tests/Integration/ArrayObjectTest.php | 243 ++++++++++++++++++ tests/Integration/ErrorHandlingTest.php | 79 +++--- .../Integration/UnionAndIntersectionTest.php | 39 +++ tests/Models/Complex/ArrayObject.php | 11 + 21 files changed, 581 insertions(+), 82 deletions(-) rename src/{ErrorInfo.php => ErrorHolder.php} (78%) create mode 100644 src/Types/ArrArr.php create mode 100644 src/Types/ArrayOf.php create mode 100644 src/Types/BoolArr.php create mode 100644 src/Types/FloatArr.php create mode 100644 src/Types/IntArr.php create mode 100644 src/Types/ObjArr.php create mode 100644 src/Types/StrArr.php create mode 100644 src/Validators/Types/ArrayObject.php create mode 100644 tests/Integration/ArrayObjectTest.php create mode 100644 tests/Models/Complex/ArrayObject.php diff --git a/composer.lock b/composer.lock index 7359f13..2636100 100644 --- a/composer.lock +++ b/composer.lock @@ -570,16 +570,16 @@ }, { "name": "filp/whoops", - "version": "2.18.0", + "version": "2.18.1", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e" + "reference": "8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", - "reference": "a7de6c3c6c3c022f5cfc337f8ede6a14460cf77e", + "url": "https://api.github.com/repos/filp/whoops/zipball/8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26", + "reference": "8fcc6a862f2e7b94eb4221fd0819ddba3d30ab26", "shasum": "" }, "require": { @@ -629,7 +629,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.18.0" + "source": "https://github.com/filp/whoops/tree/2.18.1" }, "funding": [ { @@ -637,7 +637,7 @@ "type": "github" } ], - "time": "2025-03-15T12:00:00+00:00" + "time": "2025-06-03T18:56:14+00:00" }, { "name": "jean85/pretty-package-versions", diff --git a/src/ErrorInfo.php b/src/ErrorHolder.php similarity index 78% rename from src/ErrorInfo.php rename to src/ErrorHolder.php index 5c6ddaf..5086b36 100644 --- a/src/ErrorInfo.php +++ b/src/ErrorHolder.php @@ -30,7 +30,7 @@ use Exception; use Respect\Validation\Exceptions\NestedValidationException as RespectNestedValidationException; -class ErrorInfo +class ErrorHolder { private Context $context; @@ -62,18 +62,19 @@ public function hasErrors(): bool public function addError(Exception|string $error): void { $propertyPath = $this->context->getOptional('internal.currentProperty', []); - $errors = &$this->errors; - foreach ($propertyPath as $property) { - if (! isset($errors[$property])) { - $errors[$property] = []; - } - - $errors = &$errors[$property]; - } + $fullPropertyPath = implode('.', $propertyPath); if ($error instanceof RespectNestedValidationException) { - $errors += array_values($error->getMessages()); + foreach (array_values($error->getMessages()) as $message) { + $this->errors[] = [ + 'field' => $fullPropertyPath, + 'reason' => $message, + ]; + } } else { - $errors[] = is_string($error) ? $error : $error->getMessage(); + $this->errors[] = [ + 'field' => $fullPropertyPath, + 'reason' => is_string($error) ? $error : $error->getMessage(), + ]; } if ($this->context->get('option.stopFirstError')) { diff --git a/src/Exceptions/BaseException.php b/src/Exceptions/BaseException.php index 353f11d..14b7e04 100644 --- a/src/Exceptions/BaseException.php +++ b/src/Exceptions/BaseException.php @@ -4,22 +4,22 @@ namespace Attributes\Validation\Exceptions; -use Attributes\Validation\ErrorInfo; +use Attributes\Validation\ErrorHolder; use Exception; use Throwable; abstract class BaseException extends Exception { - private ?ErrorInfo $result; + private ?ErrorHolder $errorHolder; - public function __construct(string $message, ?ErrorInfo $result = null, ?Throwable $previous = null) + public function __construct(string $message, ?ErrorHolder $errorHolder = null, ?Throwable $previous = null) { - $this->result = $result; + $this->errorHolder = $errorHolder; parent::__construct($message, 0, $previous); } - public function getInfo(): ?ErrorInfo + public function getErrors(): array { - return $this->result; + return $this->errorHolder ? $this->errorHolder->getErrors() : []; } } diff --git a/src/Property.php b/src/Property.php index b8711eb..a324292 100644 --- a/src/Property.php +++ b/src/Property.php @@ -12,13 +12,10 @@ class Property private mixed $value; - private string $modelClass; - - public function __construct(ReflectionProperty $property, mixed $value, string $modelClass) + public function __construct(ReflectionProperty $property, mixed $value) { $this->property = $property; $this->value = $value; - $this->modelClass = $modelClass; } public function getName(): string @@ -40,9 +37,4 @@ public function getReflection(): ReflectionProperty { return $this->property; } - - public function getModelClass(): string - { - return $this->modelClass; - } } diff --git a/src/Types/ArrArr.php b/src/Types/ArrArr.php new file mode 100644 index 0000000..0f6a3d2 --- /dev/null +++ b/src/Types/ArrArr.php @@ -0,0 +1,10 @@ +context->getOptional(ErrorInfo::class) ?: new ErrorInfo($this->context); - $this->context->set(ErrorInfo::class, $errorInfo, override: true); + $errorInfo = $this->context->getOptional(ErrorHolder::class) ?: new ErrorHolder($this->context); + $this->context->set(ErrorHolder::class, $errorInfo, override: true); $defaultAliasGenerator = $this->getDefaultAliasGenerator($reflectionClass); foreach ($reflectionClass->getProperties() as $reflectionProperty) { $propertyName = $reflectionProperty->getName(); @@ -89,7 +89,7 @@ public function validate(array $data, string|object $model): object } $propertyValue = $data[$aliasName]; - $property = new Property($reflectionProperty, $propertyValue, $validModel::class); + $property = new Property($reflectionProperty, $propertyValue); $this->context->set(Property::class, $property, override: true); try { diff --git a/src/Validators/AttributesValidator.php b/src/Validators/AttributesValidator.php index e0561ae..1f3c74f 100644 --- a/src/Validators/AttributesValidator.php +++ b/src/Validators/AttributesValidator.php @@ -5,7 +5,7 @@ namespace Attributes\Validation\Validators; use Attributes\Validation\Context; -use Attributes\Validation\ErrorInfo; +use Attributes\Validation\ErrorHolder; use Attributes\Validation\Exceptions\ContextPropertyException; use Attributes\Validation\Property; use ReflectionAttribute; @@ -33,7 +33,7 @@ public function validate(Property $property, Context $context): void return; } - $errorInfo = $context->get(ErrorInfo::class); + $errorInfo = $context->get(ErrorHolder::class); foreach ($allAttributes as $attribute) { $className = $attribute->getName(); if ($className == Rules\DateTime::class) { diff --git a/src/Validators/ChainValidator.php b/src/Validators/ChainValidator.php index 7022463..d8bd59f 100644 --- a/src/Validators/ChainValidator.php +++ b/src/Validators/ChainValidator.php @@ -5,7 +5,7 @@ namespace Attributes\Validation\Validators; use Attributes\Validation\Context; -use Attributes\Validation\ErrorInfo; +use Attributes\Validation\ErrorHolder; use Attributes\Validation\Exceptions\ContinueValidationException; use Attributes\Validation\Exceptions\ValidationException; use Attributes\Validation\Property; @@ -25,7 +25,7 @@ class ChainValidator implements PropertyValidator */ public function validate(Property $property, Context $context): void { - $errorInfo = $context->get(ErrorInfo::class); + $errorInfo = $context->get(ErrorHolder::class); foreach ($this->allValidators as $validator) { try { $validator->validate($property, $context); diff --git a/src/Validators/TypeHintValidator.php b/src/Validators/TypeHintValidator.php index a6b8726..4849f2d 100644 --- a/src/Validators/TypeHintValidator.php +++ b/src/Validators/TypeHintValidator.php @@ -4,6 +4,7 @@ namespace Attributes\Validation\Validators; +use ArrayObject; use Attributes\Validation\Context; use Attributes\Validation\Exceptions\ContextPropertyException; use Attributes\Validation\Exceptions\ValidationException; @@ -138,8 +139,8 @@ private function getDefaultRules(): array 'enum' => new TypeValidators\RawEnum, 'null' => new TypeValidators\RawNull, DateTime::class => new TypeValidators\DateTime, - DateTimeInterface::class => new TypeValidators\DateTime, 'interface' => new TypeValidators\StrictType, + ArrayObject::class => new TypeValidators\ArrayObject, 'default' => new TypeValidators\AnyClass, ]; } @@ -156,6 +157,10 @@ public function getTypeValidator(ReflectionNamedType|ReflectionType $propertyTyp $typeHintName = $propertyType->getName(); $typeName = isset($this->typeHintRules[$typeHintName]) ? $typeHintName : 'default'; if ($typeName == 'default') { + if (is_subclass_of($typeHintName, ArrayObject::class)) { + return $this->typeHintRules[ArrayObject::class]; + } + if (enum_exists($typeHintName)) { return $this->typeHintRules['enum']; } diff --git a/src/Validators/Types/ArrayObject.php b/src/Validators/Types/ArrayObject.php new file mode 100644 index 0000000..2875508 --- /dev/null +++ b/src/Validators/Types/ArrayObject.php @@ -0,0 +1,113 @@ +getValue(); + $typeHint = $context->get('property.typeHint'); + if ($allValues instanceof $typeHint) { + return; + } + + v::arrayVal()->assert($allValues); + + if (! property_exists($typeHint, 'type')) { + $property->setValue(new $typeHint((array) $allValues)); + + return; + } + + $this->assertLengthRules($property); + + $eachRules = $this->getEachRules($property); + + $reflectionProperty = new ReflectionProperty($typeHint, 'type'); + $typeHintValidator = $context->get(TypeHintValidator::class); + $typeProperty = new Property($reflectionProperty, null); + $arrayObject = new $typeHint; + foreach ($allValues as $value) { + $typeProperty->setValue($value); + $typeHintValidator->validate($typeProperty, $context); + $value = $typeProperty->getValue(); + foreach ($eachRules as $rule) { + $rule->assert($value); + } + $reflectionProperty->setValue($arrayObject, $value); // This is to cast a value if necessary + $arrayObject[] = $reflectionProperty->getValue($arrayObject); + } + + $property->setValue($arrayObject); + } + + /** + * Retrieves all associated Rules\Each to speed up validation + * + * @return array + */ + private function getEachRules(Property $property): array + { + $reflection = $property->getReflection(); + $allEachAttributes = $reflection->getAttributes(Rules\Each::class); + if (! $allEachAttributes) { + return []; + } + + $eachRules = []; + foreach ($allEachAttributes as $attribute) { + $eachRules[] = $attribute->getArguments()[0]; + } + + return $eachRules; + } + + /** + * Ensures that the length of the array matches the correct boundaries to avoid spending unnecessary CPU cycles + * + * @throws ReflectionException + * @throws RespectValidationException + */ + private function assertLengthRules(Property $property): void + { + $reflection = $property->getReflection(); + $allLengthAttributes = $reflection->getAttributes(Rules\Length::class); + if (! $allLengthAttributes) { + return; + } + + $reflectionClass = new ReflectionClass(Rules\Length::class); + foreach ($allLengthAttributes as $attribute) { + $rule = $reflectionClass->newInstanceArgs($attribute->getArguments()); + $rule->assert($property->getValue()); + } + } +} diff --git a/tests/Benchmark/Benchmark.php b/tests/Benchmark/Benchmark.php index d660513..3a9be2e 100644 --- a/tests/Benchmark/Benchmark.php +++ b/tests/Benchmark/Benchmark.php @@ -3,6 +3,7 @@ namespace Attributes\Validation\Tests\Benchmark; use Attributes\Validation\Tests\Models as Models; +use Attributes\Validation\Types\IntArr; use Attributes\Validation\Validator; use Generator; use PhpBench\Attributes as Bench; @@ -39,6 +40,17 @@ public function benchUnions() $validator->validate($data, new Models\Complex\BasicUnion); } + #[Bench\Iterations(100)] + #[Bench\ParamProviders(['getSampleArray'])] + public function benchArrays(array $params) + { + $validator = new Validator; + $validator->validate(['value' => $params['data']], new class + { + public IntArr $value; + }); + } + #[Bench\Iterations(100)] public function benchNestedAndAttributes() { @@ -64,4 +76,13 @@ public function getStrictOption(): Generator yield 'strict' => ['isStrict' => true]; yield 'loose' => ['isStrict' => false]; } + + public function getSampleArray(): Generator + { + $numbers = []; + for ($i = 0; $i < 100; $i++) { + $numbers[$i] = $i; + } + yield 'numbers' => ['data' => $numbers]; + } } diff --git a/tests/Integration/ArrayObjectTest.php b/tests/Integration/ArrayObjectTest.php new file mode 100644 index 0000000..9d42f7b --- /dev/null +++ b/tests/Integration/ArrayObjectTest.php @@ -0,0 +1,243 @@ +validate(['value' => $allRawValues], new class + { + public Types\BoolArr $value; + }); + $expectedValue = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE); + $allExpectedValues = array_fill(0, 10, $expectedValue); + expect($model) + ->toBeObject() + ->and((array) $model->value) + ->toMatchArray($allExpectedValues); +}) + ->with('bool') + ->group('validator', 'array-object', 'bool'); + +test('Invalid array of bool\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $validator->validate(['value' => $allRawValues], new class + { + public Types\BoolArr $value; + }); +}) + ->with('invalid bool') + ->throws(ValidationException::class, 'Invalid data') + ->group('validator', 'array-object', 'bool'); + +// Array of integer's + +test('Valid array of int\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $model = $validator->validate(['value' => $allRawValues], new class + { + public Types\IntArr $value; + }); + $expectedValue = (int) $value; + $allExpectedValues = array_fill(0, 10, $expectedValue); + expect($model) + ->toBeObject() + ->and((array) $model->value) + ->toMatchArray($allExpectedValues); +}) + ->with('integer') + ->group('validator', 'array-object', 'integer'); + +test('Invalid array of integer\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $validator->validate(['value' => $allRawValues], new class + { + public Types\IntArr $value; + }); +}) + ->with('invalid integer') + ->throws(ValidationException::class, 'Invalid data') + ->group('validator', 'array-object', 'integer'); + +// Array of float's + +test('Valid array of float\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $model = $validator->validate(['value' => $allRawValues], new class + { + public Types\FloatArr $value; + }); + $expectedValue = (float) $value; + $allExpectedValues = array_fill(0, 10, $expectedValue); + expect($model) + ->toBeObject() + ->and((array) $model->value) + ->toMatchArray($allExpectedValues); +}) + ->with('float') + ->group('validator', 'array-object', 'float'); + +test('Invalid array of float\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $validator->validate(['value' => $allRawValues], new class + { + public Types\FloatArr $value; + }); +}) + ->with('invalid float') + ->throws(ValidationException::class, 'Invalid data') + ->group('validator', 'array-object', 'float'); + +// Array of string's + +test('Valid array of string\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $model = $validator->validate(['value' => $allRawValues], new class + { + public Types\StrArr $value; + }); + $expectedValue = (string) $value; + $allExpectedValues = array_fill(0, 10, $expectedValue); + expect($model) + ->toBeObject() + ->and((array) $model->value) + ->toMatchArray($allExpectedValues); +}) + ->with('float') + ->group('validator', 'array-object', 'string'); + +test('Invalid array of string\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $validator->validate(['value' => $allRawValues], new class + { + public Types\StrArr $value; + }); +}) + ->with('invalid string') + ->throws(ValidationException::class, 'Invalid data') + ->group('validator', 'array-object', 'string'); + +// Array of array's + +test('Valid array of array\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $model = $validator->validate(['value' => $allRawValues], new class + { + public Types\ArrArr $value; + }); + $expectedValue = (array) $value; + $allExpectedValues = array_fill(0, 10, $expectedValue); + expect($model) + ->toBeObject() + ->and((array) $model->value) + ->toMatchArray($allExpectedValues); +}) + ->with('array') + ->group('validator', 'array-object', 'array'); + +test('Invalid array of array\'s', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $validator->validate(['value' => $allRawValues], new class + { + public Types\ArrArr $value; + }); +}) + ->with('invalid array') + ->throws(ValidationException::class, 'Invalid data') + ->group('validator', 'array-object', 'array'); + +// Array of objects + +test('Valid array of objects', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $model = $validator->validate(['value' => $allRawValues], new class + { + public Types\ObjArr $value; + }); + $expectedValue = (object) $value; + $allExpectedValues = array_fill(0, 10, $expectedValue); + expect($model) + ->toBeObject() + ->and((array) $model->value) + ->toMatchArray($allExpectedValues); +}) + ->with('object') + ->group('validator', 'array-object', 'object'); + +test('Invalid array of objects', function (mixed $value) { + $validator = new Validator; + $allRawValues = array_fill(0, 10, $value); + $validator->validate(['value' => $allRawValues], new class + { + public Types\ObjArr $value; + }); + var_dump($value); +}) + ->with('invalid object') + ->throws(ValidationException::class, 'Invalid data') + ->group('validator', 'array-object', 'object'); + +// Custom object + +test('Valid array of a custom class', function () { + $validator = new Validator; + $allRawPosts = []; + for ($i = 0; $i < 10; $i++) { + $allRawPosts[] = ['id' => $i, 'title' => 'My title']; + } + class PostsArr extends Types\ArrayOf + { + private Post $type; + } + + $model = $validator->validate(['value' => $allRawPosts], new class + { + public PostsArr $value; + }); + + expect($model) + ->toBeObject() + ->and($model->value) + ->toHaveCount(10) + ->toContainOnlyInstancesOf(Post::class); +}) + ->group('validator', 'array-object', 'custom-class'); + +test('Invalid array of a custom class', function (mixed $value) { + $validator = new Validator; + + $validator->validate(['value' => $value], new class + { + public PostsArr $value; + }); +}) + ->with('invalid object') + ->throws(ValidationException::class, 'Invalid data') + ->group('validator', 'array-object', 'custom-class'); diff --git a/tests/Integration/ErrorHandlingTest.php b/tests/Integration/ErrorHandlingTest.php index 1f90b40..0af6088 100644 --- a/tests/Integration/ErrorHandlingTest.php +++ b/tests/Integration/ErrorHandlingTest.php @@ -44,15 +44,17 @@ } catch (ValidationException $e) { expect($e->getMessage()) ->toBe('Invalid data') - ->and($e->getInfo()->getErrors()) + ->and($e->getErrors()) ->toBeArray() ->toBe([ - 'bool' => ['"invalid" must be a boolean value'], - 'int' => ['"invalid" must be a finite number', '"invalid" must be numeric'], - 'float' => ['"invalid" must be a finite number', '"invalid" must be a float number'], - 'string' => ['`{ "invalid" }` must be a string'], - 'array' => ['"invalid" must be an array value'], - 'object' => ['"invalid" must be an array value'], + ['field' => 'bool', 'reason' => '"invalid" must be a boolean value'], + ['field' => 'int', 'reason' => '"invalid" must be a finite number'], + ['field' => 'int', 'reason' => '"invalid" must be numeric'], + ['field' => 'float', 'reason' => '"invalid" must be a finite number'], + ['field' => 'float', 'reason' => '"invalid" must be a float number'], + ['field' => 'string', 'reason' => '`{ "invalid" }` must be a string'], + ['field' => 'array', 'reason' => '"invalid" must be an array value'], + ['field' => 'object', 'reason' => '"invalid" must be an array value'], ]); } }) @@ -77,17 +79,13 @@ } catch (ValidationException $e) { expect($e->getMessage()) ->toBe('Invalid data') - ->and($e->getInfo()->getErrors()) + ->and($e->getErrors()) ->toBeArray() ->toBe([ - 'profile' => [ - 'lastName' => ['`{ "profile.lastName" }` must be a string'], - 'post' => [ - 'title' => ['`{ "profile.post.title" }` must be a string'], - ], - ], - 'userType' => ['"userType" must be in `{ "admin", "moderator", "guest" }`'], - 'createdAt' => ['"createdAt" must be a valid date/time in the format "2005-12-30T01:02:03+00:00"'], + ['field' => 'profile.lastName', 'reason' => '`{ "profile.lastName" }` must be a string'], + ['field' => 'profile.post.title', 'reason' => '`{ "profile.post.title" }` must be a string'], + ['field' => 'userType', 'reason' => '"userType" must be in `{ "admin", "moderator", "guest" }`'], + ['field' => 'createdAt', 'reason' => '"createdAt" must be a valid date/time in the format "2005-12-30T01:02:03+00:00"'], ]); } }) @@ -123,15 +121,17 @@ } catch (ValidationException $e) { expect($e->getMessage()) ->toBe('Invalid data') - ->and($e->getInfo()->getErrors()) + ->and($e->getErrors()) ->toBeArray() ->toBe([ - 'bool' => ['"invalid" must be of type boolean'], - 'int' => ['"invalid" must be of type integer', '"invalid" must be a finite number'], - 'float' => ['"invalid" must be of type float', '"invalid" must be a finite number'], - 'string' => ['`{ "invalid" }` must be of type string'], - 'array' => ['"invalid" must be of type array'], - 'object' => ['"invalid" must be an array value'], + ['field' => 'bool', 'reason' => '"invalid" must be of type boolean'], + ['field' => 'int', 'reason' => '"invalid" must be of type integer'], + ['field' => 'int', 'reason' => '"invalid" must be a finite number'], + ['field' => 'float', 'reason' => '"invalid" must be of type float'], + ['field' => 'float', 'reason' => '"invalid" must be a finite number'], + ['field' => 'string', 'reason' => '`{ "invalid" }` must be of type string'], + ['field' => 'array', 'reason' => '"invalid" must be of type array'], + ['field' => 'object', 'reason' => '"invalid" must be an array value'], ]); } }) @@ -156,17 +156,13 @@ } catch (ValidationException $e) { expect($e->getMessage()) ->toBe('Invalid data') - ->and($e->getInfo()->getErrors()) + ->and($e->getErrors()) ->toBeArray() ->toBe([ - 'profile' => [ - 'lastName' => ['`{ "profile.lastName" }` must be of type string'], - 'post' => [ - 'title' => ['`{ "profile.post.title" }` must be of type string'], - ], - ], - 'userType' => ['"userType" must be in `{ "admin", "moderator", "guest" }`'], - 'createdAt' => ['"createdAt" must be a valid date/time in the format "2005-12-30T01:02:03+00:00"'], + ['field' => 'profile.lastName', 'reason' => '`{ "profile.lastName" }` must be of type string'], + ['field' => 'profile.post.title', 'reason' => '`{ "profile.post.title" }` must be of type string'], + ['field' => 'userType', 'reason' => '"userType" must be in `{ "admin", "moderator", "guest" }`'], + ['field' => 'createdAt', 'reason' => '"createdAt" must be a valid date/time in the format "2005-12-30T01:02:03+00:00"'], ]); } }) @@ -200,12 +196,12 @@ public object $object; }); } catch (ValidationException $e) { - $expectedErrors = $isStrict ? ['"invalid" must be of type boolean'] : ['"invalid" must be a boolean value']; + $expectedError = $isStrict ? '"invalid" must be of type boolean' : '"invalid" must be a boolean value'; expect($e->getMessage()) ->toBe('Invalid data') - ->and($e->getInfo()->getErrors()) + ->and($e->getErrors()) ->toBeArray() - ->toBe(['bool' => $expectedErrors]); + ->toBe([['field' => 'bool', 'reason' => $expectedError]]); } }) ->with([true, false]) @@ -228,16 +224,15 @@ try { $validator->validate($rawData, new Models\User); } catch (ValidationException $e) { - $expectedErrors = $isStrict ? ['`{ "profile.lastName" }` must be of type string'] : ['`{ "profile.lastName" }` must be a string']; + $expectedError = $isStrict ? '`{ "profile.lastName" }` must be of type string' : '`{ "profile.lastName" }` must be a string'; expect($e->getMessage()) ->toBe('Invalid data') - ->and($e->getInfo()->getErrors()) + ->and($e->getErrors()) ->toBeArray() - ->toBe([ - 'profile' => [ - 'lastName' => $expectedErrors, - ], - ]); + ->toBe([[ + 'field' => 'profile.lastName', + 'reason' => $expectedError, + ]]); } }) ->with([true, false]) diff --git a/tests/Integration/UnionAndIntersectionTest.php b/tests/Integration/UnionAndIntersectionTest.php index 76801e7..24451d6 100644 --- a/tests/Integration/UnionAndIntersectionTest.php +++ b/tests/Integration/UnionAndIntersectionTest.php @@ -14,6 +14,8 @@ use Attributes\Validation\Exceptions\ValidationException; use Attributes\Validation\Tests\Models\Complex as Models; +use Attributes\Validation\Types\IntArr; +use Attributes\Validation\Types\StrArr; use Attributes\Validation\Validator; use DateTime; use stdClass; @@ -116,6 +118,43 @@ ->with([1, -10, 10e10, 1.1, '100', '10.28', '-98e2', '2050-12-06T00:00:03+00:00', new DateTime]) ->group('validator', 'union'); +test('Union with int/IntArr', function ($value) { + $validator = new Validator; + $model = $validator->validate(['value' => $value], new class + { + public int|IntArr $value; + }); + expect($model) + ->toBeObject(); + + if (is_numeric($value)) { + expect($model->value)->toBe((int) $value); + + return; + } + + expect($model->value) + ->toHaveCount(count($value)) + ->each + ->toBeInt(); +}) + ->with([1, -10, 10e10, 1.1, '100', '10.28', '-98e2', [[1, 2, 3]], [[0.22, '90', 19]]]) + ->group('validator', 'union'); + +test('Union with IntArr/StrArr', function ($value) { + $validator = new Validator; + $model = $validator->validate(['value' => $value], new class + { + public IntArr|StrArr $value; + }); + expect($model) + ->toBeObject() + ->and($model->value) + ->toMatchArray($value); +}) + ->with([[[1, 2, 3]], [['hello', 'bro', 'another']]]) + ->group('validator', 'union', 'hey'); + /*** Intersection ***/ test('Intersection with Logger&Formatter', function () { diff --git a/tests/Models/Complex/ArrayObject.php b/tests/Models/Complex/ArrayObject.php new file mode 100644 index 0000000..a2e8eb1 --- /dev/null +++ b/tests/Models/Complex/ArrayObject.php @@ -0,0 +1,11 @@ +