diff --git a/README.md b/README.md index c87e6a9..5e59768 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,36 @@ If the value is valid, return `0`, otherwise `1`. ]) ``` +### The `required` property +You can mark any field as `required`, and if the value is not provided, then an automatic validation will happen for you (thus removing the need for you to weaken your validation callback with `null` types). You can set it to `true`, or you can provide an error array similar to the one returned by your validate callback: + +```php +//... +'updateThing' => new ValidatedFieldDefinition([ + 'type' => Types::thing(), + 'args' => [ + 'foo' => [ + 'required' => true, // if not provided, then an error of the form [1, 'foo is required'] will be returned. + 'validate' => function(string $foo) { + if(Foo::find($foo)) { + return 0; + } + return 1; + } + ], + 'bar' => [ + 'required' => [1, 'Oh, where is the bar?!'], + 'validate' => function(string $bar) { + if(Bar::find($bar)) { + return 0; + } + return 1; + } + ] + ] +]) +``` + If you want to return an error message, return an array with the message in the second bucket: ```php //... diff --git a/examples/01-basic-scalar-validation/README.md b/examples/01-basic-scalar-validation/README.md index 8cb96e7..294a23b 100644 --- a/examples/01-basic-scalar-validation/README.md +++ b/examples/01-basic-scalar-validation/README.md @@ -1,38 +1,38 @@ -# Simple scalar validation -The simplest possible type of user input validation. Mutation expects an `authorId` arg, and will respond with a nicely formatted error code and message if an author by that id doesn't exist. - -### Run locally -``` -php -S localhost:8000 ./index.php -``` - -### Install ChromeiQL plug-in for Chrome -1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) -2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button -3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types - -### Try mutation with valid input -``` -mutation { - deleteAuthor(id: 1) { - valid - result - } -} -``` - -### Try mutation with invalid input -``` -mutation { - deleteAuthor(id: 3) { - valid - result - suberrors { - id { - code - msg - } - } - } -} +# Simple scalar validation +The simplest possible type of user input validation. Mutation expects an `authorId` arg, and will respond with a nicely formatted error code and message if an author by that id doesn't exist. + +### Run locally +``` +php -S localhost:8000 ./index.php +``` + +### Install ChromeiQL plug-in for Chrome +1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) +2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button +3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types + +### Try mutation with valid input +``` +mutation { + deleteAuthor(id: 1) { + valid + result + } +} +``` + +### Try mutation with invalid input +``` +mutation { + deleteAuthor(id: 3) { + valid + result + suberrors { + id { + code + msg + } + } + } +} ``` \ No newline at end of file diff --git a/examples/02-custom-error-codes/README.md b/examples/02-custom-error-codes/README.md index c9198c0..1051014 100644 --- a/examples/02-custom-error-codes/README.md +++ b/examples/02-custom-error-codes/README.md @@ -1,44 +1,44 @@ -# Simple scalar validation -If you supply an `errorCodes` property on your `fields` or `args` definitions, then a custom, unique error code type will be created. - -### Run locally -``` -php -S localhost:8000 ./index.php -``` - -### Install ChromeiQL plug-in for Chrome -1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) -2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button -3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types - -### Try mutation with valid input -``` -mutation { - deleteAuthor(id: 1) { - valid - result - suberrors { - id { - code - msg - } - } - } -} -``` - -### Try mutation with invalid input -``` -mutation { - deleteAuthor(id: 3) { - valid - result - suberrors { - id { - code - msg - } - } - } -} +# Simple scalar validation +If you supply an `errorCodes` property on your `fields` or `args` definitions, then a custom, unique error code type will be created. + +### Run locally +``` +php -S localhost:8000 ./index.php +``` + +### Install ChromeiQL plug-in for Chrome +1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) +2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button +3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types + +### Try mutation with valid input +``` +mutation { + deleteAuthor(id: 1) { + valid + result + suberrors { + id { + code + msg + } + } + } +} +``` + +### Try mutation with invalid input +``` +mutation { + deleteAuthor(id: 3) { + valid + result + suberrors { + id { + code + msg + } + } + } +} ``` \ No newline at end of file diff --git a/examples/03-input-object-validation/README.md b/examples/03-input-object-validation/README.md index 71a0b33..d71aa02 100644 --- a/examples/03-input-object-validation/README.md +++ b/examples/03-input-object-validation/README.md @@ -1,82 +1,82 @@ -# Validation of InputObjects (or Objects) - -You can add `validate` properties to the fields of nested Objects or InputObjects (and those fields can themselves be of complex types with their own fields, and so on). This library will sniff them out and recursively build up a result type with a similarly nested structure. - - -### Run locally -``` -php -S localhost:8000 ./index.php -``` - -### Install ChromeiQL plug-in for Chrome -1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) -2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button -3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types - -### Try mutation with valid input -``` -mutation { - updateAuthor( - authorId: 1 - attributes: { - name: "Stephen King", - age: 47 - } - ) { - result { - id - name - } - suberrors { - attributes { - code - msg - suberrors { - age { - code - msg - } - name { - code - msg - } - } - } - } - } -} -``` - -### Try mutation with invalid input -``` -mutation { - updateAuthor( - authorId: 1 - attributes: { - name: "Edward John Moreton Drax Plunkett, 18th Baron of Dunsany", - age: -3 - } - ) { - result { - id - name - } - suberrors { - attributes { - code - msg - suberrors { - age { - code - msg - } - name { - code - msg - } - } - } - } - } -} +# Validation of InputObjects (or Objects) + +You can add `validate` properties to the fields of nested Objects or InputObjects (and those fields can themselves be of complex types with their own fields, and so on). This library will sniff them out and recursively build up a result type with a similarly nested structure. + + +### Run locally +``` +php -S localhost:8000 ./index.php +``` + +### Install ChromeiQL plug-in for Chrome +1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) +2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button +3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types + +### Try mutation with valid input +``` +mutation { + updateAuthor( + authorId: 1 + attributes: { + name: "Stephen King", + age: 47 + } + ) { + result { + id + name + } + suberrors { + attributes { + code + msg + suberrors { + age { + code + msg + } + name { + code + msg + } + } + } + } + } +} +``` + +### Try mutation with invalid input +``` +mutation { + updateAuthor( + authorId: 1 + attributes: { + name: "Edward John Moreton Drax Plunkett, 18th Baron of Dunsany", + age: -3 + } + ) { + result { + id + name + } + suberrors { + attributes { + code + msg + suberrors { + age { + code + msg + } + name { + code + msg + } + } + } + } + } +} ``` \ No newline at end of file diff --git a/examples/04-list-of-scalar-validation/README.md b/examples/04-list-of-scalar-validation/README.md index 095b02b..839e9f3 100644 --- a/examples/04-list-of-scalar-validation/README.md +++ b/examples/04-list-of-scalar-validation/README.md @@ -1,57 +1,57 @@ -# Validation of InputObjects (or Objects) - -You can add validate lists of things. You can specify a `validate` callback on the `ListOf` field itself, and also specify a `validateItem` callback to be applied to each item in the list. Any errors returned on the list items will each have an `index` property so you will know exactly which items were invalid. - - -### Run locally -``` -php -S localhost:8000 ./index.php -``` - -### Install ChromeiQL plug-in for Chrome -1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) -2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button -3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types - - -### Try mutation with valid input -``` -mutation { - savePhoneNumbers( - phoneNumbers: [ - "123-3456", - "867-5309" - ] - ) { - valid - suberrors { - phoneNumbers { - code - msg - path - } - } - } -} -``` - -### Try mutation with invalid input -``` -mutation { - savePhoneNumbers( - phoneNumbers: [ - "123-3456", - "xxx-3456" - ] - ) { - valid - suberrors { - phoneNumbers { - code - msg - path - } - } - } -} +# Validation of InputObjects (or Objects) + +You can add validate lists of things. You can specify a `validate` callback on the `ListOf` field itself, and also specify a `validateItem` callback to be applied to each item in the list. Any errors returned on the list items will each have an `index` property so you will know exactly which items were invalid. + + +### Run locally +``` +php -S localhost:8000 ./index.php +``` + +### Install ChromeiQL plug-in for Chrome +1. Install from [here](https://chrome.google.com/webstore/detail/chromeiql/fkkiamalmpiidkljmicmjfbieiclmeij?hl=en) +2. Enter "http://localhost:8000" in the text field at the top and press the "Set Endpoint" button +3. Be sure to inspect the "Docs" flyout to get familiar with the dynamically-generated types + + +### Try mutation with valid input +``` +mutation { + savePhoneNumbers( + phoneNumbers: [ + "123-3456", + "867-5309" + ] + ) { + valid + suberrors { + phoneNumbers { + code + msg + path + } + } + } +} +``` + +### Try mutation with invalid input +``` +mutation { + savePhoneNumbers( + phoneNumbers: [ + "123-3456", + "xxx-3456" + ] + ) { + valid + suberrors { + phoneNumbers { + code + msg + path + } + } + } +} ``` \ No newline at end of file diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 8f3ff5a..0e2f7af 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -1,18 +1,18 @@ - - - - - - - - - - - - src - tests - - - - - + + + + + + + + + + + + src + tests + + + + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index d6f4172..11319a2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -9,7 +9,7 @@ parameters: ignoreErrors: - "~Construct empty\\(\\) is not allowed\\. Use more strict comparison~" - "~(Method|Property) .+::.+(\\(\\))? (has parameter \\$\\w+ with no|has no return|has no) typehint specified~" - + treatPhpDocTypesAsCertain: false includes: - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon diff --git a/src/Type/Definition/UserErrorsType.php b/src/Type/Definition/UserErrorsType.php index 9870d48..3ff7732 100644 --- a/src/Type/Definition/UserErrorsType.php +++ b/src/Type/Definition/UserErrorsType.php @@ -85,7 +85,6 @@ protected function _buildInputObjectFields(InputObjectType $type, array $config, { $fields = []; foreach ($type->getFields() as $key => $field) { - /** @phpstan-var ValidatedFieldConfig */ $fieldConfig = $field->config; $fieldType = $this->_resolveType($field->config['type']); $newType = self::create( diff --git a/src/Type/Definition/ValidatedFieldDefinition.php b/src/Type/Definition/ValidatedFieldDefinition.php index e806156..e0c61ae 100644 --- a/src/Type/Definition/ValidatedFieldDefinition.php +++ b/src/Type/Definition/ValidatedFieldDefinition.php @@ -14,6 +14,8 @@ * typeSetter?: callable, * name?: string, * validName?: string, + * partial?: bool, + * required?: bool|array, * resultName?: string, * args: array, * resolve?: FieldResolver|null, @@ -200,21 +202,37 @@ protected function _validateInputObject(mixed $arg, mixed $value, array &$res, b /** * @phpstan-param InputObjectType $type - * @phpstan-param ValidatedFieldConfig $config + * @phpstan-param ValidatedFieldConfig $objectConfig * @param array $res */ - protected function _validateInputObjectFields(InputObjectType $type, array $config, mixed $value, array &$res, bool $isParentList = false): void + protected function _validateInputObjectFields(InputObjectType $type, array $objectConfig, mixed $value, array &$res, bool $isParentList = false): void { - $createSubErrors = UserErrorsType::needSuberrors($config, $isParentList); + $createSubErrors = UserErrorsType::needSuberrors($objectConfig, $isParentList); + $isPartial = $objectConfig['partial'] ?? true; $fields = $type->getFields(); - if (\is_array($value)) { - foreach ($value as $key => $subValue) { - $config = $fields[$key]->config; - $error = $this->_validate($config, $subValue); + foreach ($fields as $key => $field) { + $config = $field->config; - if (! empty($error)) { - $createSubErrors ? $res[UserErrorsType::SUBERRORS_NAME][$key] = $error : $res[$key] = $error; + $isKeyPresent = array_key_exists($key, $value); + $isRequired = $config['required'] ?? false; + if($isRequired && !isset($value[$key])) { + if ($isRequired === true) { + $error = ['error' => [1, "$key is required"]]; + } + else if (is_array($isRequired)) { + $error = ['error' => $isRequired]; + } + } + else if (!$isPartial || $isKeyPresent) { + $error = $this->_validate($config, $value[$key] ?? null); + } + + if (!empty($error)) { + if ($createSubErrors) { + $res[UserErrorsType::SUBERRORS_NAME][$key] = $error; + } else { + $res[$key] = $error; } } } diff --git a/tests/Type/ValidatedFieldDefinition/NonPartialValidation.php b/tests/Type/ValidatedFieldDefinition/NonPartialValidation.php new file mode 100644 index 0000000..3caf18e --- /dev/null +++ b/tests/Type/ValidatedFieldDefinition/NonPartialValidation.php @@ -0,0 +1,331 @@ + [ + 1 => ['firstName' => 'Wilson'], + 2 => ['firstName' => 'J.D.'], + 3 => ['firstName' => 'Diana'], + ], + ]; + + public function testInputObjectValidationOnFieldFail(): void + { + $this->_checkValidation( + new ValidatedFieldDefinition([ + 'name' => 'updateBook', + 'type' => Type::boolean(), + 'args' => [ + 'bookAttributes' => [ + 'partial' => false, + 'type' => function () { // lazy load + return new InputObjectType([ + 'name' => 'BookAttributes', + 'partial' => false, + 'fields' => [ + 'title' => [ + 'type' => Type::string(), + 'description' => 'Enter a book title, no more than 10 characters in length', + 'validate' => static function (string $title) { + if (\strlen($title) > 10) { + return [1, 'book title must be less than 10 characters']; + } + + return 0; + }, + ], + 'foo' => [ + 'type' => Type::string(), + 'description' => 'Provide a foo', + 'required' => true, + 'validate' => function (string $foo) { + if (strlen($foo) < 10) { + return [1, 'foo must be more than 10 characters']; + } + + return 0; + }, + ], + 'bar' => [ + 'type' => Type::string(), + 'description' => 'Provide a bar', + 'required' => [1, 'Oh, we absolutely must have a bar'], + 'validate' => function (string $foo) { + if (strlen($foo) < 10) { + return [1, 'bar must be more than 10 characters!']; + } + + return 0; + }, + ], + + 'author' => [ + 'type' => Type::id(), + 'description' => 'Provide a valid author id', + 'validate' => function (string $authorId) { + if (! isset($this->data['people'][$authorId])) { + return [1, 'We have no record of that author']; + } + + return 0; + }, + ], + ], + ]); + }, + ], + ], + 'resolve' => static function ($value): bool { + return ! $value; + }, + ]), + Utils::nowdoc(' + mutation UpdateBook( + $bookAttributes: BookAttributes + ) { + updateBook ( + bookAttributes: $bookAttributes + ) { + valid + suberrors { + bookAttributes { + title { + code + msg + } + author { + code + msg + } + foo { + code + msg + } + bar { + code + msg + } + } + } + result + } + } + '), + [ + 'bookAttributes' => [ + 'title' => 'The Catcher in the Rye', + 'author' => 4, + ], + ], + [ + 'valid' => false, + 'suberrors' => [ + 'bookAttributes' => [ + 'title' => [ + 'code' => 1, + 'msg' => 'book title must be less than 10 characters', + ], + 'foo' => [ + 'code' => 1, + 'msg' => 'foo is required', + ], + 'bar' => [ + 'code' => 1, + 'msg' => 'Oh, we absolutely must have a bar', + ], + 'author' => [ + 'code' => 1, + 'msg' => 'We have no record of that author', + ], + ], + ], + 'result' => null, + ] + ); + } + + public function testInputObjectSuberrorsValidationOnSelf(): void + { + $this->_checkValidation( + new ValidatedFieldDefinition([ + 'name' => 'updateBook', + 'type' => Type::boolean(), + 'args' => [ + 'bookAttributes' => [ + 'validate' => static function ($atts) { + return 0; + }, + 'type' => new InputObjectType([ + 'name' => 'BookAttributes', + 'fields' => [ + 'title' => [ + 'type' => Type::string(), + 'description' => 'Enter a book title, no more than 10 characters in length', + 'validate' => static function (string $title) { + if (\strlen($title) > 10) { + return [1, 'book title must be less than 10 characters']; + } + + return 0; + }, + ], + 'author' => [ + 'type' => Type::id(), + 'description' => 'Provide a valid author id', + 'validate' => function (string $authorId) { + if (! isset($this->data['people'][$authorId])) { + return [1, 'We have no record of that author']; + } + + return 0; + }, + ], + ], + ]), + ], + ], + 'resolve' => static function ($value): bool { + return ! $value; + }, + ]), + Utils::nowdoc(' + mutation UpdateBook( + $bookAttributes: BookAttributes + ) { + updateBook ( + bookAttributes: $bookAttributes + ) { + valid + suberrors { + bookAttributes { + suberrors { + title { + code + msg + } + author { + code + msg + } + } + } + } + result + } + } + '), + [ + 'bookAttributes' => [ + 'title' => 'The Catcher in the Rye', + 'author' => 4, + ], + ], + [ + 'valid' => false, + 'suberrors' => [ + 'bookAttributes' => [ + 'suberrors' => [ + 'title' => [ + 'code' => 1, + 'msg' => 'book title must be less than 10 characters', + ], + 'author' => [ + 'code' => 1, + 'msg' => 'We have no record of that author', + ], + ], + ], + ], + 'result' => null, + ] + ); + } + + public function testListOfInputObjectSuberrorsValidationOnChildField(): void + { + $this->_checkValidation( + new ValidatedFieldDefinition([ + 'name' => 'updateBook', + 'type' => Type::boolean(), + 'args' => [ + 'bookAttributes' => [ + 'validate' => static function () { + }, + 'type' => Type::listOf(new InputObjectType([ + 'name' => 'BookAttributes', + 'fields' => [ + 'title' => [ + 'type' => Type::string(), + 'description' => 'Enter a book title, no more than 10 characters in length', + 'validate' => static function (string $title) { + if (\strlen($title) > 10) { + return [1, 'book title must be less than 10 characters']; + } + + return 0; + }, + ], + ], + ])), + ], + ], + 'resolve' => static function ($value): bool { + return ! $value; + }, + ]), + Utils::nowdoc(' + mutation UpdateBook( + $bookAttributes: [BookAttributes] + ) { + updateBook ( + bookAttributes: $bookAttributes + ) { + valid + suberrors { + bookAttributes { + suberrors { + title { + code + msg + } + } + } + } + result + } + } + '), + [ + 'bookAttributes' => [[ + 'title' => 'The Catcher in the Rye', + ]], + ], + [ + 'valid' => false, + 'suberrors' => [ + 'bookAttributes' => [ + [ + 'suberrors' => [ + 'title' => [ + 'code' => 1, + 'msg' => 'book title must be less than 10 characters', + ], + ], + ], + ], + ], + 'result' => null, + ] + ); + } +}