diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d47bd7..65b07f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,37 @@ +VERSION 3.2.0 +------------- +Release date: 2020-04-02 + + - added ValidationBuilder + +VERSION 3.1.1 +------------- +Release date: 2020-03-27 + + - added "autoload-dev" to composer.json (thank you @ordago) + - added missing extension "ext-json" to composer.json (thank you @ordago) + +VERSION 3.1.0 +------------- +Release date: 2020-03-26 + + - use SchemaInterface instead of Schema in StrictValidationFromSchema and ValidationFromSchema + - added validator expectKeyToBeObject() and expectKeysToBeObject() + +VERSION 3.0.0 +------------- +Release date: 2020-03-21 + + - introducing schema which add a new way to define your validations structure + - added Schema, SchemaInterface, SchemaCompiler and SchemaCompilerInterface + - added ArrayValidationExceptionInterface to help catching any validation exceptions + - added StrictValidationFromSchema + - rewritten StrictValidation and added AbstractValidation + - added Validation, ValidationFromDefinition and ValidationFromSchema + which act almost like as Strict* classes but without exceptions + - fixed bug with ValidationDefinition where same method validation were not stacked but overwritten + - added ValidationDefinitionExecutor + VERSION 2.0.0 ------------- Release date: 2020-02-24 diff --git a/README.md b/README.md index e8d8b51..f06b3e5 100644 --- a/README.md +++ b/README.md @@ -8,23 +8,55 @@ composer require peak/array-validation -## Usage +## What is this? -General validation (stateless) +This component help you to validate array structure by: + +- validating the type of any key values +- ensuring a data structure with expected keys requirements +- preventing structure pollution by allowing only a set of keys + +This is especially useful when dealing with json data request, before using the data, you must validate his content so +you can afterward check the value of those keys with your business logic without worrying about the type or presence of any key value. + +# How to use +##### 8 Usages + +## 1- General validation "à la carte" (stateless) ```php $validator = new Validator(); -if ($validator->expectExactlyKeys($array, ['key1', 'key2', 'key3']) === true) { +if ($validator->expectExactlyKeys($data, $keys) === true) { // ... } ``` -Advanced validation with fluent interface. +## 2- Validation with fluent interface (stateful) ```php +$data = [ // data + 'tags' => [], + 'name' => 'foobar' +]; +$validation = new Validation($data); -$validation = new StrictValidation($array); +$validation + ->expectExactlyKeys(['tags', 'name']) + ->expectKeyToBeArray('tags'); + ->expectKeyToBeString('name'); + +if ($validation->hasErrors()) { + // $lastError = $validation->getLastError(); + // $errors = $validation->getErrors(); +} +``` + +## 3- Strict validation with fluent interface (stateful) + +```php + +$validation = new StrictValidation($data); // will throw an exception if any of tests below fail $validation @@ -35,7 +67,113 @@ $validation ->expectKeyToBeBoolean('isPrivate') ->expectKeyToBeArray('tags'); -// if we reach this point, it means the array is +// if we reach this point, it means the array structure is // valid according to the validation rules above. +``` + +## 4- Create a ValidationDefinition for later usage + +```php +$vDef = new ValidationDefinition(); +$vDef + ->expectOnlyKeys(['title', 'content', 'description']) + ->expectAtLeastKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content', 'description']); + +$validation = new ValidationFromDefinition($vDef, $data); + +if ($validation->hasErrors()) { + // $validation->getErrors(); +} + +``` + +## 5- Create a validation Schema for later usage + +Schema is just another way to write validation definitions. This format is ideal when you want to store schemas in file (ex: json, php array file, yml, etc.) + +```php + +$mySchema = [ + 'title' => [ + 'type' => 'string', + 'required' => true + ], + 'content' => [ + 'type' => 'string', + 'nullable' => true, + ], +]; + +$schema = new Schema(new SchemaCompiler(), $mySchema, 'mySchemaName'); + +$validation = new ValidationFromSchema($schema, $data); + +if ($validation->hasErrors()) { + // $validation->getErrors(); +} +``` + + + +## 6- Strict validation using ValidationDefinition + +```php +// all validation definitions are executed at object creation and an exception is thrown if any of tests failed +new StrictValidationFromDefinition($validationDefinition, $arrayToValidate); +``` + +## 7- Strict validation using Schema + +```php +// all validation definitions are executed at object creation and an exception is thrown if any of tests failed +new StrictValidationFromSchema($schema, $arrayToValidate); +``` + +## 8- Validation and Strict Validation with ValidationBuilder + +```php +$validation = new ValidationBuilder(); +$validation + ->expectOnlyKeys(['title', 'content', 'description']) + ->expectAtLeastKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content', 'description']); + +if ($validation->validate($data) === false) { + // $validation->getErrors(); + // $validation->getLastError(); +} +``` + +and with strict validation: + +```php +// will throw an exception if any of tests fail +$validation->strictValidate($data); +``` + +# Validation methods +```php + +interface ValidationInterface +{ + public function expectExactlyKeys(array $keys); + public function expectOnlyOneFromKeys( array $keys); + public function expectAtLeastKeys(array $keys); + public function expectOnlyKeys(array $keys); + public function expectNKeys(int $n); + public function expectKeyToBeArray(string $key, bool $acceptNull = false); + public function expectKeysToBeArray(array $keys, bool $acceptNull = false); + public function expectKeyToBeInteger(string $key, bool $acceptNull = false); + public function expectKeysToBeInteger(array $keys, bool $acceptNull = false); + public function expectKeyToBeFloat(string $key, bool $acceptNull = false); + public function expectKeysToBeFloat(array $keys, bool $acceptNull = false); + public function expectKeyToBeString(string $key, bool $acceptNull = false); + public function expectKeysToBeString(array $keys, bool $acceptNull = false); + public function expectKeyToBeBoolean(string $key, bool $acceptNull = false); + public function expectKeysToBeBoolean(array $keys, bool $acceptNull = false); + public function expectKeyToBeObject(string $key, bool $acceptNull = false); + public function expectKeysToBeObject(array $keys, bool $acceptNull = false); +} ``` \ No newline at end of file diff --git a/composer.json b/composer.json index 0e01e86..7eae421 100644 --- a/composer.json +++ b/composer.json @@ -5,15 +5,22 @@ "license": "MIT", "type": "library", "require": { - "php": ">=7.2" + "php": ">=7.2", + "symfony/polyfill-php73": "^1.14", + "ext-json": "*" }, "require-dev": { - "phpunit/phpunit": "^8.2", - "phpstan/phpstan": "^0.11" + "phpunit/phpunit": "^9.0", + "phpstan/phpstan": "^0.12" }, "autoload": { "psr-4": { "Peak\\ArrayValidation\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } } -} \ No newline at end of file +} diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..afcd42b --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + checkMissingIterableValueType: false + paths: + - src diff --git a/phpunit.xml b/phpunit.xml index 2d23c38..fcfbb9e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,8 @@ @@ -18,5 +21,4 @@ - \ No newline at end of file diff --git a/src/AbstractValidation.php b/src/AbstractValidation.php new file mode 100644 index 0000000..38b5846 --- /dev/null +++ b/src/AbstractValidation.php @@ -0,0 +1,69 @@ + + */ + protected $errors = []; + + /** + * @var array + */ + protected $errorMessages = [ + 'expectedN' => '{dataName}invalid data, expected {nExpected} element{nExpectedPlural}, received {nReceived} element{nReceivedPlural}', + 'expected' => '{dataName}invalid data, expected {expectedType} [{keysExpected}], received [{keysReceived}]', + 'type' => '{dataName}invalid type for key [{key}], type {expectedType} is expected', + ]; + + /** + * @param string $type + * @param array $context + * @return string + */ + protected function getErrorMessage(string $type, array $context): string + { + $message = $this->errorMessages[$type]; + $replace = []; + + foreach ($context as $key => $val) { + // check that the value can be casted to string + if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { + $replace['{' . $key . '}'] = $val; + } + } + return strtr($message, $replace); + } + + /** + * @return string|null + */ + public function getLastError(): ?string + { + $lastKey = array_key_last($this->errors); + if ($lastKey === null) { + return $lastKey; + } + return $this->errors[$lastKey]; + } + + /** + * @return bool + */ + public function hasErrors(): bool + { + return !empty($this->errors); + } + + /** + * @return array + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/src/Exception/ArrayValidationExceptionInterface.php b/src/Exception/ArrayValidationExceptionInterface.php new file mode 100644 index 0000000..405b5d7 --- /dev/null +++ b/src/Exception/ArrayValidationExceptionInterface.php @@ -0,0 +1,9 @@ +compiler = $compiler; + $this->schema = $schema; + $this->schemaName = $schemaName; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->schemaName; + } + + /** + * @return ValidationDefinition + * @throws Exception\InvalidStructureException + * @throws Exception\InvalidTypeException + */ + public function compile(): ValidationDefinition + { + return $this->compiler->compileSchema($this->schema); + } + + /** + * @inheritDoc + * @return array + */ + public function jsonSerialize() + { + return $this->schema; + } +} diff --git a/src/SchemaCompiler.php b/src/SchemaCompiler.php new file mode 100644 index 0000000..c04cfd1 --- /dev/null +++ b/src/SchemaCompiler.php @@ -0,0 +1,81 @@ + $fieldDefinition) { + if (is_string($fieldName) && is_array($fieldDefinition)) { + $this->handleFieldDefinition($fieldName, $fieldDefinition, $definition); + } + } + + // handle "required" + $this->handleRequiredFields($schema, $definition); + + return $definition; + } + + /** + * @param string $fieldName + * @param array $fieldDefinition + * @param ValidationDefinition $definition + * @throws Exception\InvalidStructureException + * @throws Exception\InvalidTypeException + */ + private function handleFieldDefinition(string $fieldName, array $fieldDefinition, ValidationDefinition $definition): void + { + (new StrictValidation($fieldDefinition, 'compile.schema.field.' . $fieldName)) + ->expectOnlyKeys([ + 'comment', 'type', 'nullable', 'required', 'default', 'values' + ]) + ->expectKeysToBeBoolean([ + 'nullable', 'required' + ]); + + // handle "type" and "nullable" + if (isset($fieldDefinition['type'])) { + $method = 'expectKeyToBe' . ucfirst($fieldDefinition['type']); + $definition->$method($fieldName, $fieldDefinition['nullable'] ?? false); + } + } + + /** + * @param array $schema + * @param ValidationDefinition $definition + */ + private function handleRequiredFields(array $schema, ValidationDefinition $definition): void + { + $definition->expectOnlyKeys(array_keys($schema)); + $atLeastKeys = []; + $fieldCount = 0; + $requiredFieldCount = 0; + foreach ($schema as $field => $fieldDef) { + if (is_array($fieldDef)) { + ++$fieldCount; + if (isset($fieldDef['required']) && $fieldDef['required'] === true) { + $atLeastKeys[] = $field; + ++$requiredFieldCount; + } + } + } + + if ($requiredFieldCount == $fieldCount) { + $definition->expectExactlyKeys($atLeastKeys); + } else { + $definition->expectAtLeastKeys($atLeastKeys); + } + } +} diff --git a/src/SchemaCompilerInterface.php b/src/SchemaCompilerInterface.php new file mode 100644 index 0000000..84e5f93 --- /dev/null +++ b/src/SchemaCompilerInterface.php @@ -0,0 +1,16 @@ + '{dataName}invalid data, expected {nExpected} element(s), received {nReceived} element(s)', - 'expected' => '{dataName}invalid data, expected {expectedType} [{keysExpected}], received [{keysReceived}]', - 'type' => '{dataName}invalid type for key [{key}], type {expectedType} is expected', - ]; - - /** - * StrictValidation constructor. - * @param array $data - * @param string|null $dataName - * @param Validator|null $validator - */ - public function __construct(array $data, string $dataName = null, Validator $validator = null) - { - $this->data = $data; - $this->dataName = $dataName; - if (!isset($validator)) { - $validator = new Validator(); - } - $this->validator = $validator; - } - /** * @param array $keys * @return $this @@ -56,18 +16,8 @@ public function __construct(array $data, string $dataName = null, Validator $val */ public function expectExactlyKeys(array $keys) { - if ($this->validator->expectExactlyKeys($this->data, $keys) === false) { - $keysReceived = array_keys($this->data); - natsort($keys); - natsort($keysReceived); - $message = $this->getErrorMessage('expected', [ - 'expectedType' => 'exactly keys', - 'keysExpected' => implode(', ', $keys), - 'keysReceived' => implode(', ', $keysReceived) - ]); - throw new InvalidStructureException($message); - } - return $this; + parent::expectExactlyKeys($keys); + return $this->checkStructureErrors(); } /** @@ -77,18 +27,8 @@ public function expectExactlyKeys(array $keys) */ public function expectAtLeastKeys(array $keys) { - if ($this->validator->expectAtLeastKeys($this->data, $keys) === false) { - $keysReceived = array_keys($this->data); - natsort($keys); - natsort($keysReceived); - $message = $this->getErrorMessage('expected', [ - 'expectedType' => 'at least keys', - 'keysExpected' => implode(', ', $keys), - 'keysReceived' => implode(', ', $keysReceived) - ]); - throw new InvalidStructureException($message); - } - return $this; + parent::expectAtLeastKeys($keys); + return $this->checkStructureErrors(); } /** @@ -98,18 +38,8 @@ public function expectAtLeastKeys(array $keys) */ public function expectOnlyKeys(array $keys) { - if ($this->validator->expectOnlyKeys($this->data, $keys) === false) { - $keysReceived = array_keys($this->data); - natsort($keys); - natsort($keysReceived); - $message = $this->getErrorMessage('expected', [ - 'expectedType' => 'only keys', - 'keysExpected' => implode(', ', $keys), - 'keysReceived' => implode(', ', $keysReceived) - ]); - throw new InvalidStructureException($message); - } - return $this; + parent::expectOnlyKeys($keys); + return $this->checkStructureErrors(); } /** @@ -119,18 +49,8 @@ public function expectOnlyKeys(array $keys) */ public function expectOnlyOneFromKeys(array $keys) { - if ($this->validator->expectOnlyOneFromKeys($this->data, $keys) === false) { - $keysReceived = array_keys($this->data); - natsort($keys); - natsort($keysReceived); - $message = $this->getErrorMessage('expected', [ - 'expectedType' => 'only one of keys', - 'keysExpected' => implode(', ', $keys), - 'keysReceived' => implode(', ', $keysReceived) - ]); - throw new InvalidStructureException($message); - } - return $this; + parent::expectOnlyOneFromKeys($keys); + return $this->checkStructureErrors(); } /** @@ -140,17 +60,8 @@ public function expectOnlyOneFromKeys(array $keys) */ public function expectNKeys(int $n) { - if ($this->validator->expectNKeys($this->data, $n) === false) { - $keysReceived = array_keys($this->data); - natsort($keysReceived); - $message = $this->getErrorMessage('expectedN', [ - 'expectedType' => 'only N keys', - 'nExpected' => $n, - 'nReceived' => count($keysReceived) - ]); - throw new InvalidStructureException($message); - } - return $this; + parent::expectNKeys($n); + return $this->checkStructureErrors(); } /** @@ -161,14 +72,8 @@ public function expectNKeys(int $n) */ public function expectKeyToBeArray(string $key, bool $acceptNull = false) { - if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeArray($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'array', - ]); - throw new InvalidTypeException($message); - } - return $this; + parent::expectKeyToBeArray($key, $acceptNull); + return $this->checkTypeErrors(); } /** @@ -179,14 +84,8 @@ public function expectKeyToBeArray(string $key, bool $acceptNull = false) */ public function expectKeyToBeInteger(string $key, bool $acceptNull = false) { - if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeInteger($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'integer', - ]); - throw new InvalidTypeException($message); - } - return $this; + parent::expectKeyToBeInteger($key, $acceptNull); + return $this->checkTypeErrors(); } /** @@ -197,14 +96,8 @@ public function expectKeyToBeInteger(string $key, bool $acceptNull = false) */ public function expectKeyToBeFloat(string $key, bool $acceptNull = false) { - if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeFloat($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'float', - ]); - throw new InvalidTypeException($message); - } - return $this; + parent::expectKeyToBeFloat($key, $acceptNull); + return $this->checkTypeErrors(); } /** @@ -215,14 +108,8 @@ public function expectKeyToBeFloat(string $key, bool $acceptNull = false) */ public function expectKeyToBeString(string $key, bool $acceptNull = false) { - if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeString($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'string', - ]); - throw new InvalidTypeException($message); - } - return $this; + parent::expectKeyToBeString($key, $acceptNull); + return $this->checkTypeErrors(); } /** @@ -233,20 +120,14 @@ public function expectKeyToBeString(string $key, bool $acceptNull = false) */ public function expectKeyToBeBoolean(string $key, bool $acceptNull = false) { - if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeBoolean($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'boolean', - ]); - throw new InvalidTypeException($message); - } - return $this; + parent::expectKeyToBeBoolean($key, $acceptNull); + return $this->checkTypeErrors(); } /** * @param array $keys * @param bool $acceptNull - * @return $this + * @return $this|Validation * @throws InvalidTypeException */ public function expectKeysToBeString(array $keys, bool $acceptNull = false) @@ -260,7 +141,7 @@ public function expectKeysToBeString(array $keys, bool $acceptNull = false) /** * @param array $keys * @param bool $acceptNull - * @return $this + * @return $this|Validation * @throws InvalidTypeException */ public function expectKeysToBeInteger(array $keys, bool $acceptNull = false) @@ -274,7 +155,7 @@ public function expectKeysToBeInteger(array $keys, bool $acceptNull = false) /** * @param array $keys * @param bool $acceptNull - * @return $this + * @return $this|Validation * @throws InvalidTypeException */ public function expectKeysToBeFloat(array $keys, bool $acceptNull = false) @@ -288,7 +169,7 @@ public function expectKeysToBeFloat(array $keys, bool $acceptNull = false) /** * @param array $keys * @param bool $acceptNull - * @return $this + * @return $this|Validation * @throws InvalidTypeException */ public function expectKeysToBeBoolean(array $keys, bool $acceptNull = false) @@ -302,7 +183,7 @@ public function expectKeysToBeBoolean(array $keys, bool $acceptNull = false) /** * @param array $keys * @param bool $acceptNull - * @return $this + * @return $this|Validation * @throws InvalidTypeException */ public function expectKeysToBeArray(array $keys, bool $acceptNull = false) @@ -314,36 +195,26 @@ public function expectKeysToBeArray(array $keys, bool $acceptNull = false) } /** - * @return string|null + * @return $this + * @throws InvalidStructureException */ - private function getExceptionDataName(): ?string + protected function checkStructureErrors() { - if (isset($this->dataName)) { - return '['. $this->dataName.'] '; + if ($this->hasErrors()) { + throw new InvalidStructureException($this->getLastError() ?? ''); } - return null; + return $this; } /** - * @param $type - * @param array $context - * @return string + * @return $this + * @throws InvalidTypeException */ - private function getErrorMessage($type, array $context): string + protected function checkTypeErrors() { - $message = $this->messages[$type]; - $context = array_merge(['dataName' => $this->getExceptionDataName()], $context); - $replace = []; - - foreach ($context as $key => $val) { - // check that the value can be casted to string - if (!is_array($val) && (!is_object($val) || method_exists($val, '__toString'))) { - if (isset($fn)) { - $val = $fn($val); - } - $replace['{' . $key . '}'] = $val; - } + if ($this->hasErrors()) { + throw new InvalidTypeException($this->getLastError() ?? ''); } - return strtr($message, $replace); + return $this; } } diff --git a/src/StrictValidationFromDefinition.php b/src/StrictValidationFromDefinition.php index 80b0897..cb41f38 100644 --- a/src/StrictValidationFromDefinition.php +++ b/src/StrictValidationFromDefinition.php @@ -6,30 +6,31 @@ class StrictValidationFromDefinition extends StrictValidation { + /** * @var ValidationDefinition */ - private $arrayValidationDefinition; + private $validationDefinition; /** * StrictArrayValidatorFromDefinition constructor. - * @param ValidationDefinition $arrayValidationDefinition + * @param ValidationDefinition $validationDefinition * @param array $data * @param string|null $dataName * @param Validator|null $arrayValidation */ public function __construct( - ValidationDefinition $arrayValidationDefinition, + ValidationDefinition $validationDefinition, array $data, string $dataName = null, Validator $arrayValidation = null ) { - $this->arrayValidationDefinition = $arrayValidationDefinition; + $this->validationDefinition = $validationDefinition; parent::__construct($data, $dataName, $arrayValidation); - $validations = $arrayValidationDefinition->getValidations(); - foreach ($validations as $name => $args) { - call_user_func_array([$this, $name], $args); - } + (new ValidationDefinitionExecutor())->execute( + $validationDefinition, + $this + ); } -} \ No newline at end of file +} diff --git a/src/StrictValidationFromSchema.php b/src/StrictValidationFromSchema.php new file mode 100644 index 0000000..5e705ce --- /dev/null +++ b/src/StrictValidationFromSchema.php @@ -0,0 +1,31 @@ +getName(), $arrayValidation); + + $validationDefinition = $schema->compile(); + + (new ValidationDefinitionExecutor())->execute( + $validationDefinition, + $this + ); + } +} diff --git a/src/Validation.php b/src/Validation.php new file mode 100644 index 0000000..98b7396 --- /dev/null +++ b/src/Validation.php @@ -0,0 +1,331 @@ +data = $data; + $this->dataName = $dataName; + if (!isset($validator)) { + $validator = new Validator(); + } + $this->validator = $validator; + } + + /** + * @param array $keys + * @return $this + */ + public function expectExactlyKeys(array $keys) + { + if ($this->validator->expectExactlyKeys($this->data, $keys) === false) { + $keysReceived = array_keys($this->data); + natsort($keys); + natsort($keysReceived); + $this->errors[] = $this->getErrorMessage('expected', [ + 'expectedType' => 'exactly keys', + 'keysExpected' => implode(', ', $keys), + 'keysReceived' => implode(', ', $keysReceived) + ]); + } + return $this; + } + + /** + * @param array $keys + * @return $this + */ + public function expectAtLeastKeys(array $keys) + { + if ($this->validator->expectAtLeastKeys($this->data, $keys) === false) { + $keysReceived = array_keys($this->data); + natsort($keys); + natsort($keysReceived); + $this->errors[] = $this->getErrorMessage('expected', [ + 'expectedType' => 'at least keys', + 'keysExpected' => implode(', ', $keys), + 'keysReceived' => implode(', ', $keysReceived) + ]); + } + return $this; + } + + /** + * @param array $keys + * @return $this + */ + public function expectOnlyKeys(array $keys) + { + if ($this->validator->expectOnlyKeys($this->data, $keys) === false) { + $keysReceived = array_keys($this->data); + natsort($keys); + natsort($keysReceived); + $this->errors[] = $this->getErrorMessage('expected', [ + 'expectedType' => 'only keys', + 'keysExpected' => implode(', ', $keys), + 'keysReceived' => implode(', ', $keysReceived) + ]); + } + return $this; + } + + /** + * @param array $keys + * @return $this + */ + public function expectOnlyOneFromKeys(array $keys) + { + if ($this->validator->expectOnlyOneFromKeys($this->data, $keys) === false) { + $keysReceived = array_keys($this->data); + natsort($keys); + natsort($keysReceived); + $this->errors[] = $this->getErrorMessage('expected', [ + 'expectedType' => 'only one of keys', + 'keysExpected' => implode(', ', $keys), + 'keysReceived' => implode(', ', $keysReceived) + ]); + } + return $this; + } + + /** + * @param int $n + * @return $this + */ + public function expectNKeys(int $n) + { + if ($this->validator->expectNKeys($this->data, $n) === false) { + $keysReceived = array_keys($this->data); + natsort($keysReceived); + $this->errors[] = $this->getErrorMessage('expectedN', [ + 'expectedType' => 'only N keys', + 'nExpected' => $n, + 'nExpectedPlural' => $n > 1 ? 's' : '', + 'nReceivedPlural' => count($keysReceived) > 1 ? 's' : '', + 'nReceived' => count($keysReceived) + ]); + } + return $this; + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeArray(string $key, bool $acceptNull = false) + { + if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeArray($this->data, $key, $acceptNull) === false) { + $this->errors[] = $this->getErrorMessage('type', [ + 'key' => $key, + 'expectedType' => 'array', + ]); + } + return $this; + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeInteger(string $key, bool $acceptNull = false) + { + if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeInteger($this->data, $key, $acceptNull) === false) { + $this->errors[] = $this->getErrorMessage('type', [ + 'key' => $key, + 'expectedType' => 'integer', + ]); + } + return $this; + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeFloat(string $key, bool $acceptNull = false) + { + if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeFloat($this->data, $key, $acceptNull) === false) { + $this->errors[] = $this->getErrorMessage('type', [ + 'key' => $key, + 'expectedType' => 'float', + ]); + } + return $this; + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeString(string $key, bool $acceptNull = false) + { + if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeString($this->data, $key, $acceptNull) === false) { + $this->errors[] = $this->getErrorMessage('type', [ + 'key' => $key, + 'expectedType' => 'string', + ]); + } + return $this; + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeBoolean(string $key, bool $acceptNull = false) + { + if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeBoolean($this->data, $key, $acceptNull) === false) { + $this->errors[] = $this->getErrorMessage('type', [ + 'key' => $key, + 'expectedType' => 'boolean', + ]); + } + return $this; + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeObject(string $key, bool $acceptNull = false) + { + if (array_key_exists($key, $this->data) && $this->validator->expectKeyToBeObject($this->data, $key, $acceptNull) === false) { + $this->errors[] = $this->getErrorMessage('type', [ + 'key' => $key, + 'expectedType' => 'object', + ]); + } + return $this; + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeString(array $keys, bool $acceptNull = false) + { + foreach ($keys as $key) { + $this->expectKeyToBeString($key, $acceptNull); + } + return $this; + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeInteger(array $keys, bool $acceptNull = false) + { + foreach ($keys as $key) { + $this->expectKeyToBeInteger($key, $acceptNull); + } + return $this; + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeFloat(array $keys, bool $acceptNull = false) + { + foreach ($keys as $key) { + $this->expectKeyToBeFloat($key, $acceptNull); + } + return $this; + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeBoolean(array $keys, bool $acceptNull = false) + { + foreach ($keys as $key) { + $this->expectKeyToBeBoolean($key, $acceptNull); + } + return $this; + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeArray(array $keys, bool $acceptNull = false) + { + foreach ($keys as $key) { + $this->expectKeyToBeArray($key, $acceptNull); + } + return $this; + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeObject(array $keys, bool $acceptNull = false) + { + foreach ($keys as $key) { + $this->expectKeyToBeObject($key, $acceptNull); + } + return $this; + } + + /** + * @return string|null + */ + protected function getExceptionDataName(): ?string + { + if (isset($this->dataName)) { + return '['. $this->dataName.'] '; + } + return null; + } + + /** + * @param string $type + * @param array $context + * @return string + */ + protected function getErrorMessage(string $type, array $context): string + { + $context = array_merge(['dataName' => $this->getExceptionDataName()], $context); + return parent::getErrorMessage($type, $context); + } +} diff --git a/src/ValidationBuilder.php b/src/ValidationBuilder.php new file mode 100644 index 0000000..0b77d75 --- /dev/null +++ b/src/ValidationBuilder.php @@ -0,0 +1,56 @@ +errors = $validation->getErrors(); + $this->lastError = $validation->getLastError(); + return !$validation->hasErrors(); + } + + /** + * @param array $data + * @throw InvalidStructureException + * @throw InvalidTypeException + */ + public function strictValidate(array $data): void + { + new StrictValidationFromDefinition($this, $data); + } + + /** + * @return array + */ + public function getErrors(): array + { + return $this->errors; + } + + /** + * @return string|null + */ + public function getLastError(): ?string + { + return $this->lastError; + } +} diff --git a/src/ValidationDefinition.php b/src/ValidationDefinition.php index 659f872..e1018c7 100644 --- a/src/ValidationDefinition.php +++ b/src/ValidationDefinition.php @@ -4,27 +4,31 @@ namespace Peak\ArrayValidation; -class ValidationDefinition implements ValidationInterface +use \JsonSerializable; + +class ValidationDefinition implements ValidationInterface, JsonSerializable { /** * @var array */ private $validations = [ - 'expectExactlyKeys' => null, - 'expectOnlyOneFromKeys' => null, - 'expectAtLeastKeys' => null, - 'expectOnlyKeys' => null, - 'expectNKeys' => null, - 'expectKeyToBeArray' => null, - 'expectKeysToBeArray' => null, - 'expectKeyToBeInteger' => null, - 'expectKeysToBeInteger' => null, - 'expectKeyToBeFloat' => null, - 'expectKeysToBeFloat' => null, - 'expectKeyToBeString' => null, - 'expectKeysToBeString' => null, - 'expectKeyToBeBoolean' => null, - 'expectKeysToBeBoolean' => null, + 'expectExactlyKeys' => [], + 'expectOnlyOneFromKeys' => [], + 'expectAtLeastKeys' => [], + 'expectOnlyKeys' => [], + 'expectNKeys' => [], + 'expectKeyToBeArray' => [], + 'expectKeysToBeArray' => [], + 'expectKeyToBeInteger' => [], + 'expectKeysToBeInteger' => [], + 'expectKeyToBeFloat' => [], + 'expectKeysToBeFloat' => [], + 'expectKeyToBeString' => [], + 'expectKeysToBeString' => [], + 'expectKeyToBeBoolean' => [], + 'expectKeysToBeBoolean' => [], + 'expectKeyToBeObject' => [], + 'expectKeysToBeObject' => [], ]; /** @@ -34,100 +38,195 @@ public function getValidations(): array { $validations = []; foreach ($this->validations as $name => $args) { - if (null !== $args) { + if (!empty($args)) { $validations[$name] = $args; } } return $validations; } + /** + * @param array $keys + * @return $this + */ public function expectExactlyKeys(array $keys) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param array $keys + * @return $this + */ public function expectOnlyOneFromKeys(array $keys) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param array $keys + * @return $this + */ public function expectAtLeastKeys(array $keys) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param array $keys + * @return $this + */ public function expectOnlyKeys(array $keys) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param int $n + * @return $this + */ public function expectNKeys(int $n) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ public function expectKeyToBeArray(string $key, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ public function expectKeysToBeArray(array $keys, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ public function expectKeyToBeInteger(string $key, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ public function expectKeysToBeInteger(array $keys, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ public function expectKeyToBeFloat(string $key, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ public function expectKeysToBeFloat(array $keys, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ public function expectKeyToBeString(string $key, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ public function expectKeysToBeString(array $keys, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ public function expectKeyToBeBoolean(string $key, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); - return $this; + return $this->addValidation(__FUNCTION__, func_get_args()); } + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ public function expectKeysToBeBoolean(array $keys, bool $acceptNull = false) { - $this->validations[__FUNCTION__] = func_get_args(); + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeObject(string $key, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeObject(array $keys, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @inheritDoc + * @return array + */ + public function jsonSerialize() + { + return $this->getValidations(); + } + + /** + * @param string $validationName + * @param array $validationArgs + * @return $this + */ + protected function addValidation(string $validationName, array $validationArgs) + { + $this->validations[$validationName][] = $validationArgs; return $this; } } diff --git a/src/ValidationDefinitionExecutor.php b/src/ValidationDefinitionExecutor.php new file mode 100644 index 0000000..0ff1a57 --- /dev/null +++ b/src/ValidationDefinitionExecutor.php @@ -0,0 +1,23 @@ +getValidations(); + foreach ($validations as $name => $multipleArgs) { + foreach ($multipleArgs as $args) { + $callable = [$validation, $name]; + if (is_callable($callable)) { + call_user_func_array($callable, $args); + } + } + } + } +} \ No newline at end of file diff --git a/src/ValidationFromDefinition.php b/src/ValidationFromDefinition.php new file mode 100644 index 0000000..ddc791b --- /dev/null +++ b/src/ValidationFromDefinition.php @@ -0,0 +1,35 @@ +validationDefinition = $validationDefinition; + parent::__construct($data, $dataName, $arrayValidation); + + (new ValidationDefinitionExecutor())->execute( + $validationDefinition, + $this + ); + } +} diff --git a/src/ValidationFromSchema.php b/src/ValidationFromSchema.php new file mode 100644 index 0000000..2330da0 --- /dev/null +++ b/src/ValidationFromSchema.php @@ -0,0 +1,30 @@ +getName(), $arrayValidation); + + $validationDefinition = $schema->compile(); + (new ValidationDefinitionExecutor())->execute( + $validationDefinition, + $this + ); + } +} diff --git a/src/ValidationInterface.php b/src/ValidationInterface.php index 20b9374..ed66f1e 100644 --- a/src/ValidationInterface.php +++ b/src/ValidationInterface.php @@ -1,5 +1,7 @@ internalTypeValidation('is_object', $array, $key, $acceptNull); + } + + /** + * @param array $array + * @param array $keys + * @param bool $acceptNull + * @return bool + */ + public function expectKeysToBeObject(array $array, array $keys, bool $acceptNull = false): bool + { + foreach ($keys as $key) { + if ($this->expectKeyToBeObject($array, $key, $acceptNull) === false) { + return false; + } + } + return true; + } + /** * @param string $method * @param array $array @@ -235,4 +262,7 @@ private function internalTypeValidation(string $method, array $array, string $ke ($acceptNull && ($array[$key] !== null && !$method($array[$key]))) ); } + + + } diff --git a/src/ValidatorInterface.php b/src/ValidatorInterface.php index 6897071..263c02a 100644 --- a/src/ValidatorInterface.php +++ b/src/ValidatorInterface.php @@ -1,5 +1,7 @@ expectAtLeastKeys(['title', 'content']) - ->expectExactlyKeys(['title', 'content']) - ->expectOnlyKeys(['title', 'content']) - ->expectKeysToBeString(['title', 'content'], true); - - $validations = $arrayDefinition->getValidations(); - $this->assertTrue(is_array($validations)); - $this->assertTrue(count($validations) === 4); - } - - public function testStrictArrayValidatorFromDefinition() - { - $arrayDefinition = new ValidationDefinition(); - $arrayDefinition - ->expectAtLeastKeys(['title', 'content']) - ->expectExactlyKeys(['title', 'content']) - ->expectOnlyKeys(['title', 'content']) - ->expectKeysToBeString(['title', 'content'], true); - - $strictValidator = new StrictValidationFromDefinition($arrayDefinition, [ - 'title' => '', - 'content' => '', - ]); - - $this->assertTrue(true); - - $this->expectException(InvalidTypeException::class); - $strictValidator = new StrictValidationFromDefinition($arrayDefinition, [ - 'title' => '', - 'content' => 1, - ]); - } -} diff --git a/tests/SchemaCompilerTest.php b/tests/SchemaCompilerTest.php new file mode 100644 index 0000000..3516d62 --- /dev/null +++ b/tests/SchemaCompilerTest.php @@ -0,0 +1,72 @@ + 'category.schema', + 'field1' => [ + 'type' => 'array', + 'required' => true + ], + 'field2' => [ + 'type' => 'string', + 'required' => true + ], + ], + [ // data + 'field1' => [], + 'field2' => 'strong' + ] + ], + [ + [ // schema + 'name' => 'post.schema', + 'field1' => [ + 'type' => 'array', + 'required' => true + ], + 'field2' => [ + 'type' => 'string', + 'required' => false, + 'nullable' => true, + ], + ], + [ // data + 'field1' => [], + 'field2' => null + ] + ], + ]; + } + + /** + * @dataProvider dataProvider + * @param $schema + * @param $data + * @throws InvalidStructureException + * @throws InvalidTypeException + */ + public function testExpectExactlyKeys($schema, $data) + { + $def = new SchemaCompiler(); + $validationDefinition = $def->compileSchema($schema); + $this->assertInstanceOf(ValidationInterface::class, $validationDefinition); + new StrictValidationFromDefinition($validationDefinition, $data); + } +} diff --git a/tests/SchemaTest.php b/tests/SchemaTest.php new file mode 100644 index 0000000..beb4fe1 --- /dev/null +++ b/tests/SchemaTest.php @@ -0,0 +1,80 @@ + [ + 'type' => 'array', + 'required' => true + ], + 'field2' => [ + 'type' => 'string', + 'required' => true + ], + ], + [ // data + 'field1' => [], + 'field2' => 'strong' + ] + ], + [ + [ // schema + 'field1' => [ + 'type' => 'array', + 'required' => true + ], + 'field2' => [ + 'type' => 'string', + 'required' => false, + 'nullable' => true, + ], + ], + [ // data + 'field1' => [], + 'field2' => null + ] + ], + ]; + } + + /** + * @dataProvider dataProvider + * @param $schema + * @param $data + * @throws InvalidStructureException + * @throws InvalidTypeException + */ + public function testExpectExactlyKeys($schema, $data) + { + $compiler = new SchemaCompiler(); + $schema = new Schema($compiler, $schema, 'myName'); + $validationDefinition = $schema->compile(); + $this->assertInstanceOf(ValidationInterface::class, $validationDefinition); + $this->assertTrue($schema->getName() === 'myName'); + new StrictValidationFromDefinition($validationDefinition, $data); + } + + public function testJsonSerialize() + { + $compiler = new SchemaCompiler(); + $schema = new Schema($compiler, ['schema'], 'myName'); + $this->assertTrue(json_encode($schema) === '["schema"]'); + } +} diff --git a/tests/StrictArrayValidatorTest.php b/tests/StrictValidationTest.php similarity index 98% rename from tests/StrictArrayValidatorTest.php rename to tests/StrictValidationTest.php index 6a73018..6287cd3 100644 --- a/tests/StrictArrayValidatorTest.php +++ b/tests/StrictValidationTest.php @@ -2,12 +2,14 @@ declare(strict_types=1); +namespace Tests; + use Peak\ArrayValidation\Exception\InvalidStructureException; use Peak\ArrayValidation\Exception\InvalidTypeException; use Peak\ArrayValidation\StrictValidation; use PHPUnit\Framework\TestCase; -class StrictArrayValidatorTest extends TestCase +class StrictValidationTest extends TestCase { private $data1 = [ 'title' => 'foo', diff --git a/tests/ValidationBuilderTest.php b/tests/ValidationBuilderTest.php new file mode 100644 index 0000000..abcbdf9 --- /dev/null +++ b/tests/ValidationBuilderTest.php @@ -0,0 +1,72 @@ +expectNKeys(2) + ->expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content'], true); + + $data = [ + 'title' => 'test', + 'content' => 'test', + ]; + + $validationPass = $validation->validate($data); + $this->assertTrue($validationPass); + $this->assertTrue($validation->getErrors() === []); + $this->assertTrue($validation->getLastError() === null); + } + + public function testStrictValidate() + { + $validation = new ValidationBuilder(); + $validation + ->expectNKeys(2) + ->expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content'], true); + + $data = [ + 'title' => 'test', + 'content' => 'test', + ]; + + $validation->strictValidate($data); + $this->assertTrue(true); + } + + public function testStrictValidateException() + { + $validation = new ValidationBuilder(); + $validation + ->expectNKeys(2) + ->expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content'], true); + + $data = [ + 'title' => 1, + 'content' => 'test', + ]; + + $this->expectException(ArrayValidationExceptionInterface::class); + $validation->strictValidate($data); + } + +} diff --git a/tests/ValidationDefinitionTest.php b/tests/ValidationDefinitionTest.php new file mode 100644 index 0000000..2323bdf --- /dev/null +++ b/tests/ValidationDefinitionTest.php @@ -0,0 +1,147 @@ +expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content'], true); + + $validations = $arrayDefinition->getValidations(); + $this->assertTrue(is_array($validations)); + $this->assertTrue(count($validations) === 4); + } + + public function testGetDefinition2() + { + $arrayDefinition = new ValidationDefinition(); + + $arrayDefinition + ->expectOnlyKeys(['title', 'content', 'description', 'id', 'number', 'amount', 'fields', 'isPrivate', 'person']) + ->expectOnlyOneFromKeys(['title']) + ->expectKeysToBeInteger(['id']) + ->expectKeyToBeInteger('number') + ->expectKeyToBeFloat('amount') + ->expectKeysToBeFloat(['amount']) + ->expectKeysToBeArray(['fields']) + ->expectKeyToBeArray('fields') + ->expectKeyToBeBoolean('isPrivate') + ->expectKeysToBeBoolean(['isPrivate']) + ->expectKeysToBeString(['title', 'content'], true) + ->expectKeyToBeString('title', true) + ->expectKeyToBeString('content', true) + ->expectKeyToBeString('description', false) + ->expectKeyToBeObject('person', true) + ->expectKeysToBeObject(['person']); + + $validations = $arrayDefinition->getValidations(); + + $this->assertTrue(is_array($validations)); + $this->assertTrue(isset($validations['expectKeyToBeString'][0][0])); + $this->assertTrue($validations['expectKeyToBeString'][0][0] === 'title'); + $this->assertTrue($validations['expectKeyToBeString'][0][1]); + + $this->assertTrue(isset($validations['expectKeyToBeString'][1][0])); + $this->assertTrue($validations['expectKeyToBeString'][1][0] === 'content'); + $this->assertTrue($validations['expectKeyToBeString'][1][1]); + + $this->assertTrue(isset($validations['expectKeyToBeString'][2][0])); + $this->assertTrue($validations['expectKeyToBeString'][2][0] === 'description'); + $this->assertFalse($validations['expectKeyToBeString'][2][1]); + + $this->assertTrue(isset($validations['expectKeyToBeObject'][0][0])); + $this->assertTrue($validations['expectKeyToBeObject'][0][0] === 'person'); + $this->assertTrue($validations['expectKeyToBeObject'][0][1]); + } + + public function testStrictArrayValidatorFromDefinition() + { + $arrayDefinition = new ValidationDefinition(); + $arrayDefinition + ->expectNKeys(2) + ->expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content'], true); + + $strictValidator = new StrictValidationFromDefinition($arrayDefinition, [ + 'title' => '', + 'content' => '', + ]); + + $this->assertTrue(true); + + $this->expectException(InvalidTypeException::class); + $strictValidator = new StrictValidationFromDefinition($arrayDefinition, [ + 'title' => '', + 'content' => 1, + ]); + } + + /** + * @throws InvalidTypeException + * @throws InvalidStructureException + */ + public function testStrictArrayValidatorFromSchema() + { + $schemaArray = [ + 'title' => [ + 'type' => 'string', + 'required' => true + ], + 'content' => [ + 'type' => 'string', + 'nullable' => true, + ], + ]; + + $schema = new Schema( + new SchemaCompiler(), + $schemaArray, + 'mySchemaName' + ); + + $arrayToValidate = [ + 'title' => '', + 'content' => null, + ]; + + // will throw an exception if any of tests failed, + // this should pass + new StrictValidationFromSchema($schema, $arrayToValidate); + $this->assertTrue(true); + + // this should not pass + $this->expectException(InvalidTypeException::class); + new StrictValidationFromSchema($schema, [ + 'title' => '', + 'content' => 1, + ]); + + } + + public function testJsonSerialize() + { + $definition = new ValidationDefinition(); + $definition->expectOnlyKeys(['title', 'content']); + $this->assertTrue(json_encode($definition) === '{"expectOnlyKeys":[[["title","content"]]]}'); + } +} diff --git a/tests/ValidationFromDefinitionTest.php b/tests/ValidationFromDefinitionTest.php new file mode 100644 index 0000000..408b2a2 --- /dev/null +++ b/tests/ValidationFromDefinitionTest.php @@ -0,0 +1,37 @@ +expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content'], true); + + $data = [ + 'item1' => 45.1, + 'item2' => 36.5, + 'item3' => [], + 'item4' => null, + 'item5' => 99, + ]; + + $validation = new ValidationFromDefinition($validationDefinition, $data); + + $this->assertTrue($validation->hasErrors()); + $this->assertTrue(count($validation->getErrors()) === 3); + } + +} diff --git a/tests/ValidationFromSchemaTest.php b/tests/ValidationFromSchemaTest.php new file mode 100644 index 0000000..790f0c8 --- /dev/null +++ b/tests/ValidationFromSchemaTest.php @@ -0,0 +1,48 @@ + [ + 'type' => 'string', + 'required' => true + ], + 'content' => [ + 'type' => 'string', + 'nullable' => true, + ], + ]; + + $schema = new Schema( + new SchemaCompiler(), + $schemaArray, + 'mySchemaName' + ); + + $arrayToValidate = [ + 'title' => '', + 'content' => null, + ]; + + // this should not pass + $validation = new ValidationFromSchema($schema, [ + 'title' => '', + 'content' => 1, + ]); + + $this->assertTrue($validation->hasErrors()); + $this->assertTrue(count($validation->getErrors()) == 1); + } + +} diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php new file mode 100644 index 0000000..6dd1c05 --- /dev/null +++ b/tests/ValidationTest.php @@ -0,0 +1,119 @@ + [], 'field2' => 'text', 'field3' => 1, 'field4'=> true, 'field5' => 1.2, 'field6' => new StdClass()], // data + null, // dataName + [ // validation rules + 'expectOnlyKeys' => [ ['field1', 'field2', 'field3', 'field4', 'field5', 'field6'] ], + 'expectExactlyKeys' => [ ['field1', 'field2', 'field3', 'field4', 'field5', 'field6'] ], + 'expectKeyToBeArray' => ['field1'], + 'expectKeyToBeString' => ['field2'], + 'expectKeyToBeInteger' => ['field3'], + 'expectKeyToBeBoolean' => ['field4'], + 'expectKeyToBeFloat' => ['field5'], + 'expectKeyToBeObject' => ['field6'], + + 'expectKeysToBeArray' => [['field1']], + 'expectKeysToBeString' => [['field2']], + 'expectKeysToBeInteger' => [['field3']], + 'expectKeysToBeBoolean' => [['field4']], + 'expectKeysToBeFloat' => [['field5']], + 'expectKeysToBeObject' => [['field6']], + ], + false, // has error(s) + 0, // number of error(s) + [] // error(s) messages + ], + + // this one will test types errors + [ + ['field6' => [], 'field5' => 'text', 'field4' => 1, 'field3'=> true, 'field2' => 1.2, 'field1' => new StdClass()], // data + 'myValidation', // dataName + [ // validation rules + 'expectKeyToBeArray' => ['field1'], + 'expectKeyToBeString' => ['field2'], + 'expectKeyToBeInteger' => ['field3'], + 'expectKeyToBeBoolean' => ['field4'], + 'expectKeyToBeFloat' => ['field5'], + 'expectKeyToBeObject' => ['field6'], + ], + true, // has error(s) + 6, // number of error(s) + [ + '[myValidation] invalid type for key [field1], type array is expected', + '[myValidation] invalid type for key [field2], type string is expected', + '[myValidation] invalid type for key [field3], type integer is expected', + '[myValidation] invalid type for key [field4], type boolean is expected', + '[myValidation] invalid type for key [field5], type float is expected', + '[myValidation] invalid type for key [field6], type object is expected', + ] // error(s) messages + ], + + //this one will tests structure errors + [ + ['field6' => 'im here!', 'field7' => 'foo'], // data + null, // dataName + [ // validation rules + 'expectOnlyKeys' => [ ['field1', 'field2', 'field3', 'field4', 'field5', 'field6'] ], + 'expectExactlyKeys' => [ ['field1', 'field2', 'field3', 'field4', 'field5', 'field6'] ], + 'expectAtLeastKeys' => [ ['field1'] ], + 'expectOnlyOneFromKeys' => [ ['field1'] ], + 'expectNKeys' => [1] + ], + true, // has error(s) + 5, // number of error(s) + [ + 'invalid data, expected only keys [field1, field2, field3, field4, field5, field6], received [field6, field7]', + 'invalid data, expected exactly keys [field1, field2, field3, field4, field5, field6], received [field6, field7]', + 'invalid data, expected at least keys [field1], received [field6, field7]', + 'invalid data, expected only one of keys [field1], received [field6, field7]', + 'invalid data, expected 1 element, received 2 elements' + ] // error(s) messages + ], + ]; + } + + /** + * @dataProvider dataProvider + * @param array $data + * @param string|null $dataName + * @param array $validationMethods + * @param bool $hasErrors + * @param int $errorsCount + * @param array $errorsMsg + */ + public function testScenarios(array $data, ?string $dataName, array $validationMethods, bool $hasErrors, int $errorsCount, array $errorsMsg) + { + $validation = new Validation($data, $dataName); + + foreach ($validationMethods as $methodName => $methodArgs) { + call_user_func_array([$validation, $methodName], $methodArgs); + } + + $this->assertTrue($validation->hasErrors() === $hasErrors); + $this->assertTrue(count($validation->getErrors()) === $errorsCount); + $this->assertTrue($validation->getErrors() === $errorsMsg); + + if ($errorsCount > 0) { + $errors = $validation->getErrors(); + $this->assertTrue($validation->getLastError() === $errors[array_key_last($errors)]); + } else { + $this->assertTrue($validation->getLastError() === null); + } + } +} diff --git a/tests/ArrayValidationTest.php b/tests/ValidatorTest.php similarity index 89% rename from tests/ArrayValidationTest.php rename to tests/ValidatorTest.php index 72eff64..6d79dba 100644 --- a/tests/ArrayValidationTest.php +++ b/tests/ValidatorTest.php @@ -2,10 +2,14 @@ declare(strict_types=1); +namespace Tests; + +use DateTime; use \PHPUnit\Framework\TestCase; use \Peak\ArrayValidation\Validator; +use stdClass; -class ArrayValidationTest extends TestCase +class ValidatorTest extends TestCase { private $data1 = [ 'title' => 'foo', @@ -173,4 +177,20 @@ public function testIsString() $this->assertTrue($validation->expectKeysToBeString($data, ['item1', 'item2', 'item4'], true)); $this->assertFalse($validation->expectKeysToBeString($data, ['item1', 'item2', 'item3', 'item4'], true)); } + + public function testIsObject() + { + $data = [ + 'item1' => new stdClass(), + 'item2' => new DateTime(), + 'item3' => null, + ]; + $validation = new Validator(); + $this->assertTrue($validation->expectKeyToBeObject($data, 'item1', false)); + $this->assertFalse($validation->expectKeyToBeObject($data, 'item3', false)); + $this->assertTrue($validation->expectKeyToBeObject($data, 'item3', true)); + $this->assertTrue($validation->expectKeysToBeObject($data, ['item1', 'item2'], false)); + $this->assertFalse($validation->expectKeysToBeObject($data, ['item1', 'item2', 'item3'], false)); + $this->assertTrue($validation->expectKeysToBeObject($data, ['item1', 'item2', 'item3'], true)); + } }