From 4aec2b0ecb151581b7734e247d27c3a46e92e1bb Mon Sep 17 00:00:00 2001 From: matapatos Date: Tue, 15 Jul 2025 08:41:34 +0100 Subject: [PATCH] feat: Support numbered arguments --- src/Validatable.php | 5 +-- src/Validator.php | 15 +++++---- tests/Integration/ValidateCallableTest.php | 38 ++++++++++++++++++++++ 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/Validatable.php b/src/Validatable.php index 8edf39c..d75b24e 100644 --- a/src/Validatable.php +++ b/src/Validatable.php @@ -8,6 +8,7 @@ namespace Attributes\Validation; +use ArrayAccess; use Attributes\Validation\Exceptions\ValidationException; interface Validatable @@ -21,12 +22,12 @@ interface Validatable public function validate(array $data, object $model): object; /** - * @param array $data - Data to be validated + * @param array|ArrayAccess $data - Data to be validated * @param callable $call - Callable to be validated * * @returns array - Arguments in a sequence order for the given function * * @throws ValidationException - If the validation fails */ - public function validateCallable(array $data, callable $call): array; + public function validateCallable(array|ArrayAccess $data, callable $call): array; } diff --git a/src/Validator.php b/src/Validator.php index 9770261..5b673da 100644 --- a/src/Validator.php +++ b/src/Validator.php @@ -4,6 +4,7 @@ namespace Attributes\Validation; +use ArrayAccess; use Attributes\Options; use Attributes\Options\Exceptions\InvalidOptionException; use Attributes\Validation\Exceptions\ContextPropertyException; @@ -51,7 +52,7 @@ public function __construct(?PropertyValidator $validator = null, bool $stopFirs /** * Validates a given data according to a given model * - * @param array $data - Data to validate + * @param array|ArrayAccess $data - Data to validate * @param string|object $model - Model to validate against * @return object - Model populated with the validated data * @@ -60,7 +61,7 @@ public function __construct(?PropertyValidator $validator = null, bool $stopFirs * @throws ReflectionException * @throws InvalidOptionException */ - public function validate(array $data, string|object $model): object + public function validate(array|ArrayAccess $data, string|object $model): object { $currentLevel = $this->context->getOptional('internal.recursionLevel', 0); $maxRecursionLevel = $this->context->getOptional('internal.maxRecursionLevel', 30); @@ -127,7 +128,7 @@ public function validate(array $data, string|object $model): object /** * Validates a given data according to a given model * - * @param array $data - Data to validate + * @param array|ArrayAccess $data - Data to validate * @param callable $call - Callable to validate data against * @return array - Returns an array with the necessary arguments for the callable * @@ -136,14 +137,14 @@ public function validate(array $data, string|object $model): object * @throws ReflectionException * @throws InvalidOptionException */ - public function validateCallable(array $data, callable $call): array + public function validateCallable(array|ArrayAccess $data, callable $call): array { $arguments = []; $reflectionFunction = new ReflectionFunction($call); $errorInfo = $this->context->getOptional(ErrorHolder::class) ?: new ErrorHolder($this->context); $this->context->set(ErrorHolder::class, $errorInfo, override: true); $defaultAliasGenerator = $this->getDefaultAliasGenerator($reflectionFunction); - foreach ($reflectionFunction->getParameters() as $parameter) { + foreach ($reflectionFunction->getParameters() as $index => $parameter) { if (! $this->isToValidate($parameter)) { continue; } @@ -152,7 +153,7 @@ public function validateCallable(array $data, callable $call): array $aliasName = $this->getAliasName($parameter, $defaultAliasGenerator); $this->context->push('internal.currentProperty', $propertyName); - if (! array_key_exists($aliasName, $data)) { + if (! array_key_exists($aliasName, $data) && ! array_key_exists($index, $data)) { if (! $parameter->isDefaultValueAvailable()) { try { $errorInfo->addError("Missing required argument '$aliasName'"); @@ -166,7 +167,7 @@ public function validateCallable(array $data, callable $call): array continue; } - $propertyValue = $data[$aliasName]; + $propertyValue = $data[$index] ?? $data[$aliasName]; $property = new Property($parameter, $propertyValue); $this->context->set(Property::class, $property, override: true); diff --git a/tests/Integration/ValidateCallableTest.php b/tests/Integration/ValidateCallableTest.php index d602c5a..5bcd16b 100644 --- a/tests/Integration/ValidateCallableTest.php +++ b/tests/Integration/ValidateCallableTest.php @@ -138,3 +138,41 @@ ]], ]) ->group('validator', 'validate-callable', 'error-handling'); + +// Numbered arguments + +test('Numbered arguments - validate callable', function (bool $isStrict) { + $validator = new Validator(strict: $isStrict); + $rawData = [ + 'fullName' => 'My full name', + 1 => [ + 'my_post' => [ + 'postId' => 1, + 'myTitle' => 'My Post Title', + ], + ], + 'profile' => [], // Numbered arguments should have precedence + 'default' => 5, + ]; + $call = function (string $fullName, Models\Complex\Profile $profile, int $default = 1) { + return 'success'; + }; + $args = $validator->validateCallable($rawData, $call); + expect($args) + ->toBeArray() + ->toHaveCount(3) + ->toMatchArray([ + 'fullName' => 'My full name', + 'default' => 5, + ]) + ->and($args['profile']) + ->toBeInstanceOf(Models\Complex\Profile::class) + ->and($args['profile']->my_post) + ->toBeInstanceOf(Models\Complex\Post::class) + ->and($args['profile']->my_post->my_post_id) + ->toBe(1) + ->and($args['profile']->my_post->my_title) + ->toBe('My Post Title'); +}) + ->with([true, false]) + ->group('validator', 'validate-callable', 'numbered-arguments');