diff --git a/CHANGELOG.md b/CHANGELOG.md index f76565b..65b07f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,55 @@ +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 + + - [BC] renamed method expectNElement() to expectNKeys() in ArrayValidation + - added expectNKeys() and expectOnlyOneFromKeys() to StrictArrayValidator + - added interfaces ArrayValidationInterface and ArrayValidatorInterface + - [BC] renamed all classes and interfaces with better name + - added ValidationDefinition and StrictValidationFromDefinition + +VERSION 1.1.0 +------------- +Release date: 2020-02-20 + + - added methods expectKeysToBeArray(), expectKeysToBeBoolean() and expectKeysToBeFloat() to StrictArrayValidator + - transformed ArrayValidationException to 2 more specific exceptions: InvalidStructureException and InvalidTypeException + - [BC] simplified and reordered constructor params of StrictArrayValidator + VERSION 1.0.0 ------------- Release date: 2020-02-07 diff --git a/README.md b/README.md index 6aee4cf..f06b3e5 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,172 @@ composer require peak/array-validation -## Usage \ No newline at end of file +## What is this? + +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($data, $keys) === true) { + // ... +} +``` + +## 2- Validation with fluent interface (stateful) + +```php +$data = [ // data + 'tags' => [], + 'name' => 'foobar' +]; +$validation = new Validation($data); + +$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 + ->expectOnlyKeys(['id', 'title', 'description', 'isPrivate', 'tags']) + ->expectAtLeastKeys(['id', 'title', 'description']) + ->expectKeyToBeInteger('id') + ->expectKeysToBeString(['title', 'description']) + ->expectKeyToBeBoolean('isPrivate') + ->expectKeyToBeArray('tags'); + +// 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/ArrayValidationException.php b/src/Exception/ArrayValidationException.php deleted file mode 100644 index d5f7228..0000000 --- a/src/Exception/ArrayValidationException.php +++ /dev/null @@ -1,7 +0,0 @@ -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 {expectedType} [{keysExpected}], received [{keysReceived}]', - 'type' => '{dataName}invalid type for key [{key}], type {expectedType} is expected', - ]; - - /** - * ArrayValidationHelper constructor. - * @param ArrayValidation $arrayValidation - * @param array $data - * @param string|null $dataName - */ - public function __construct(ArrayValidation $arrayValidation, array $data, string $dataName = null) - { - $this->arrayValidation = $arrayValidation; - $this->data = $data; - $this->dataName = $dataName; - } - - /** - * @param array $keysName - * @return $this - * @throws ArrayValidationException - */ - public function expectExactlyKeys(array $keysName) - { - if ($this->arrayValidation->expectExactlyKeys($this->data, $keysName) === false) { - $keysReceived = array_keys($this->data); - natsort($keysName); - natsort($keysReceived); - $message = $this->getErrorMessage('expected', [ - 'expectedType' => 'exactly keys', - 'keysExpected' => implode(', ', $keysName), - 'keysReceived' => implode(', ', $keysReceived) - ]); - throw new ArrayValidationException($message); - } - return $this; - } - - /** - * @param array $keysName - * @return $this - * @throws ArrayValidationException - */ - public function expectAtLeastKeys(array $keysName) - { - if ($this->arrayValidation->expectAtLeastKeys($this->data, $keysName) === false) { - $keysReceived = array_keys($this->data); - natsort($keysName); - natsort($keysReceived); - $message = $this->getErrorMessage('expected', [ - 'expectedType' => 'at least keys', - 'keysExpected' => implode(', ', $keysName), - 'keysReceived' => implode(', ', $keysReceived) - ]); - throw new ArrayValidationException($message); - } - return $this; - } - - /** - * @param array $keysName - * @return $this - * @throws ArrayValidationException - */ - public function expectOnlyKeys(array $keysName) - { - if ($this->arrayValidation->expectOnlyKeys($this->data, $keysName) === false) { - $keysReceived = array_keys($this->data); - natsort($keysName); - natsort($keysReceived); - $message = $this->getErrorMessage('expected', [ - 'expectedType' => 'only keys', - 'keysExpected' => implode(', ', $keysName), - 'keysReceived' => implode(', ', $keysReceived) - ]); - throw new ArrayValidationException($message); - } - return $this; - } - - /** - * @param string $key - * @param bool $acceptNull - * @return $this - * @throws ArrayValidationException - */ - public function expectKeyToBeArray(string $key, bool $acceptNull = false) - { - if (array_key_exists($key, $this->data) && $this->arrayValidation->expectKeyToBeArray($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'array', - ]); - throw new ArrayValidationException($message); - } - return $this; - } - - /** - * @param string $key - * @param bool $acceptNull - * @return $this - * @throws ArrayValidationException - */ - public function expectKeyToBeInteger(string $key, bool $acceptNull = false) - { - if (array_key_exists($key, $this->data) && $this->arrayValidation->expectKeyToBeInteger($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'integer', - ]); - throw new ArrayValidationException($message); - } - return $this; - } - - /** - * @param string $key - * @param bool $acceptNull - * @return $this - * @throws ArrayValidationException - */ - public function expectKeyToBeFloat(string $key, bool $acceptNull = false) - { - if (array_key_exists($key, $this->data) && $this->arrayValidation->expectKeyToBeFloat($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'float', - ]); - throw new ArrayValidationException($message); - } - return $this; - } - - /** - * @param string $key - * @param bool $acceptNull - * @return $this - * @throws ArrayValidationException - */ - public function expectKeyToBeString(string $key, bool $acceptNull = false) - { - if (array_key_exists($key, $this->data) && $this->arrayValidation->expectKeyToBeString($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'string', - ]); - throw new ArrayValidationException($message); - } - return $this; - } - - /** - * @param array $keys - * @param bool $acceptNull - * @return $this - * @throws ArrayValidationException - */ - 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 - * @throws ArrayValidationException - */ - public function expectKeysToBeString(array $keys, bool $acceptNull = false) - { - foreach ($keys as $key) { - $this->expectKeyToBeString($key, $acceptNull); - } - return $this; - } - - /** - * @param string $key - * @param bool $acceptNull - * @return $this - * @throws ArrayValidationException - */ - public function expectKeyToBeBoolean(string $key, bool $acceptNull = false) - { - if (array_key_exists($key, $this->data) && $this->arrayValidation->expectKeyToBeBoolean($this->data, $key, $acceptNull) === false) { - $message = $this->getErrorMessage('type', [ - 'key' => $key, - 'expectedType' => 'boolean', - ]); - throw new ArrayValidationException($message); - } - return $this; - } - - /** - * @return string|null - */ - private function getExceptionDataName(): ?string - { - if (isset($this->dataName)) { - return '['. $this->dataName.'] '; - } - return null; - } - - /** - * @param $type - * @param array $context - * @return string - */ - private function getErrorMessage($type, array $context): string - { - $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; - } - } - return strtr($message, $replace); - } -} diff --git a/src/StrictValidation.php b/src/StrictValidation.php new file mode 100644 index 0000000..98465de --- /dev/null +++ b/src/StrictValidation.php @@ -0,0 +1,220 @@ +checkStructureErrors(); + } + + /** + * @param array $keys + * @return $this + * @throws InvalidStructureException + */ + public function expectAtLeastKeys(array $keys) + { + parent::expectAtLeastKeys($keys); + return $this->checkStructureErrors(); + } + + /** + * @param array $keys + * @return $this + * @throws InvalidStructureException + */ + public function expectOnlyKeys(array $keys) + { + parent::expectOnlyKeys($keys); + return $this->checkStructureErrors(); + } + + /** + * @param array $keys + * @return $this + * @throws InvalidStructureException + */ + public function expectOnlyOneFromKeys(array $keys) + { + parent::expectOnlyOneFromKeys($keys); + return $this->checkStructureErrors(); + } + + /** + * @param int $n + * @return $this + * @throws InvalidStructureException + */ + public function expectNKeys(int $n) + { + parent::expectNKeys($n); + return $this->checkStructureErrors(); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + * @throws InvalidTypeException + */ + public function expectKeyToBeArray(string $key, bool $acceptNull = false) + { + parent::expectKeyToBeArray($key, $acceptNull); + return $this->checkTypeErrors(); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + * @throws InvalidTypeException + */ + public function expectKeyToBeInteger(string $key, bool $acceptNull = false) + { + parent::expectKeyToBeInteger($key, $acceptNull); + return $this->checkTypeErrors(); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + * @throws InvalidTypeException + */ + public function expectKeyToBeFloat(string $key, bool $acceptNull = false) + { + parent::expectKeyToBeFloat($key, $acceptNull); + return $this->checkTypeErrors(); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + * @throws InvalidTypeException + */ + public function expectKeyToBeString(string $key, bool $acceptNull = false) + { + parent::expectKeyToBeString($key, $acceptNull); + return $this->checkTypeErrors(); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + * @throws InvalidTypeException + */ + public function expectKeyToBeBoolean(string $key, bool $acceptNull = false) + { + parent::expectKeyToBeBoolean($key, $acceptNull); + return $this->checkTypeErrors(); + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this|Validation + * @throws InvalidTypeException + */ + 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|Validation + * @throws InvalidTypeException + */ + 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|Validation + * @throws InvalidTypeException + */ + 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|Validation + * @throws InvalidTypeException + */ + 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|Validation + * @throws InvalidTypeException + */ + public function expectKeysToBeArray(array $keys, bool $acceptNull = false) + { + foreach ($keys as $key) { + $this->expectKeyToBeArray($key, $acceptNull); + } + return $this; + } + + /** + * @return $this + * @throws InvalidStructureException + */ + protected function checkStructureErrors() + { + if ($this->hasErrors()) { + throw new InvalidStructureException($this->getLastError() ?? ''); + } + return $this; + } + + /** + * @return $this + * @throws InvalidTypeException + */ + protected function checkTypeErrors() + { + if ($this->hasErrors()) { + throw new InvalidTypeException($this->getLastError() ?? ''); + } + return $this; + } +} diff --git a/src/StrictValidationFromDefinition.php b/src/StrictValidationFromDefinition.php new file mode 100644 index 0000000..cb41f38 --- /dev/null +++ b/src/StrictValidationFromDefinition.php @@ -0,0 +1,36 @@ +validationDefinition = $validationDefinition; + parent::__construct($data, $dataName, $arrayValidation); + + (new ValidationDefinitionExecutor())->execute( + $validationDefinition, + $this + ); + } +} 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 new file mode 100644 index 0000000..e1018c7 --- /dev/null +++ b/src/ValidationDefinition.php @@ -0,0 +1,232 @@ + [], + 'expectOnlyOneFromKeys' => [], + 'expectAtLeastKeys' => [], + 'expectOnlyKeys' => [], + 'expectNKeys' => [], + 'expectKeyToBeArray' => [], + 'expectKeysToBeArray' => [], + 'expectKeyToBeInteger' => [], + 'expectKeysToBeInteger' => [], + 'expectKeyToBeFloat' => [], + 'expectKeysToBeFloat' => [], + 'expectKeyToBeString' => [], + 'expectKeysToBeString' => [], + 'expectKeyToBeBoolean' => [], + 'expectKeysToBeBoolean' => [], + 'expectKeyToBeObject' => [], + 'expectKeysToBeObject' => [], + ]; + + /** + * @return array + */ + public function getValidations(): array + { + $validations = []; + foreach ($this->validations as $name => $args) { + if (!empty($args)) { + $validations[$name] = $args; + } + } + return $validations; + } + + /** + * @param array $keys + * @return $this + */ + public function expectExactlyKeys(array $keys) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @return $this + */ + public function expectOnlyOneFromKeys(array $keys) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @return $this + */ + public function expectAtLeastKeys(array $keys) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @return $this + */ + public function expectOnlyKeys(array $keys) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param int $n + * @return $this + */ + public function expectNKeys(int $n) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeArray(string $key, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeArray(array $keys, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeInteger(string $key, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeInteger(array $keys, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeFloat(string $key, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeFloat(array $keys, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeString(string $key, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeString(array $keys, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param string $key + * @param bool $acceptNull + * @return $this + */ + public function expectKeyToBeBoolean(string $key, bool $acceptNull = false) + { + return $this->addValidation(__FUNCTION__, func_get_args()); + } + + /** + * @param array $keys + * @param bool $acceptNull + * @return $this + */ + public function expectKeysToBeBoolean(array $keys, bool $acceptNull = false) + { + 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 new file mode 100644 index 0000000..ed66f1e --- /dev/null +++ b/src/ValidationInterface.php @@ -0,0 +1,26 @@ +internalTypeValidation('is_array', $array, $key, $acceptNull); } + /** + * @param array $array + * @param array $keys + * @param bool $acceptNull + * @return bool + */ + public function expectKeysToBeArray(array $array, array $keys, bool $acceptNull = false): bool + { + foreach ($keys as $key) { + if ($this->expectKeyToBeArray($array, $key, $acceptNull) === false) { + return false; + } + } + return true; + } + /** * @param array $array * @param string $key @@ -107,6 +123,22 @@ public function expectKeyToBeInteger(array $array, string $key, bool $acceptNull return $this->internalTypeValidation('is_integer', $array, $key, $acceptNull); } + /** + * @param array $array + * @param array $keys + * @param bool $acceptNull + * @return bool + */ + public function expectKeysToBeInteger(array $array, array $keys, bool $acceptNull = false): bool + { + foreach ($keys as $key) { + if ($this->expectKeyToBeInteger($array, $key, $acceptNull) === false) { + return false; + } + } + return true; + } + /** * @param array $array * @param string $key @@ -118,6 +150,22 @@ public function expectKeyToBeString(array $array, string $key, bool $acceptNull return $this->internalTypeValidation('is_string', $array, $key, $acceptNull); } + /** + * @param array $array + * @param array $keys + * @param bool $acceptNull + * @return bool + */ + public function expectKeysToBeString(array $array, array $keys, bool $acceptNull = false): bool + { + foreach ($keys as $key) { + if ($this->expectKeyToBeString($array, $key, $acceptNull) === false) { + return false; + } + } + return true; + } + /** * @param array $array * @param string $key @@ -129,6 +177,22 @@ public function expectKeyToBeFloat(array $array, string $key, bool $acceptNull = return $this->internalTypeValidation('is_float', $array, $key, $acceptNull); } + /** + * @param array $array + * @param array $keys + * @param bool $acceptNull + * @return bool + */ + public function expectKeysToBeFloat(array $array, array $keys, bool $acceptNull = false): bool + { + foreach ($keys as $key) { + if ($this->expectKeyToBeFloat($array, $key, $acceptNull) === false) { + return false; + } + } + return true; + } + /** * @param array $array * @param string $key @@ -140,6 +204,49 @@ public function expectKeyToBeBoolean(array $array, string $key, bool $acceptNull return $this->internalTypeValidation('is_bool', $array, $key, $acceptNull); } + /** + * @param array $array + * @param array $keys + * @param bool $acceptNull + * @return bool + */ + public function expectKeysToBeBoolean(array $array, array $keys, bool $acceptNull = false): bool + { + foreach ($keys as $key) { + if ($this->expectKeyToBeBoolean($array, $key, $acceptNull) === false) { + return false; + } + } + return true; + } + + /** + * @param array $array + * @param string $key + * @param bool $acceptNull + * @return bool + */ + public function expectKeyToBeObject(array $array, string $key, bool $acceptNull = false): bool + { + return $this->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 @@ -155,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 new file mode 100644 index 0000000..263c02a --- /dev/null +++ b/src/ValidatorInterface.php @@ -0,0 +1,26 @@ + 'foo', - 'content' => 'foobar', - ]; - - private $data2 = [ - 'title' => 'foo' - ]; - - private $data3 = [ - 'title' => null, - 'views' => 45, - 'comments' => [] - ]; - - public function testExpectExactlyKeys() - { - $validation = new ArrayValidation(); - $this->assertTrue($validation->expectExactlyKeys($this->data1, ['title', 'content'])); - $this->assertFalse($validation->expectExactlyKeys($this->data1, ['title', 'content', 'extra'])); - $this->assertFalse($validation->expectExactlyKeys($this->data1, ['title'])); - $this->assertFalse($validation->expectExactlyKeys($this->data1, ['extra'])); - $this->assertFalse($validation->expectExactlyKeys($this->data1, [])); - } - - public function testExpectOnlyOneFromKeys() - { - $validation = new ArrayValidation(); - $this->assertTrue($validation->expectOnlyOneFromKeys($this->data2, ['title', 'extra'])); - $this->assertTrue($validation->expectOnlyOneFromKeys($this->data2, ['title'])); - $this->assertFalse($validation->expectOnlyOneFromKeys($this->data2, ['extra'])); - } - - public function testExpectAtLeastKeys() - { - $validation = new ArrayValidation(); - $this->assertTrue($validation->expectAtLeastKeys($this->data1, ['title'])); - $this->assertTrue($validation->expectAtLeastKeys($this->data1, ['content'])); - $this->assertTrue($validation->expectAtLeastKeys($this->data1, ['title', 'content'])); - $this->assertFalse($validation->expectAtLeastKeys($this->data1, ['extra'])); - $this->assertFalse($validation->expectAtLeastKeys($this->data1, ['title', 'content', 'extra'])); - } - - public function testExpectOnlyKeys() - { - $validation = new ArrayValidation(); - $this->assertTrue($validation->expectOnlyKeys($this->data1, ['title', 'content'])); - $this->assertTrue($validation->expectOnlyKeys($this->data1, ['title', 'content', 'extra'])); - $this->assertFalse($validation->expectOnlyKeys($this->data1, ['title'])); - } - - public function testExpectXElement() - { - $validation = new ArrayValidation(); - $this->assertTrue($validation->expectXElement($this->data1, 2)); - $this->assertFalse($validation->expectXElement($this->data1, 1)); - } - - public function testIsArray() - { - $validation = new ArrayValidation(); - $this->assertTrue($validation->expectKeyToBeArray($this->data3, 'comments', false)); - $this->assertFalse($validation->expectKeyToBeArray($this->data3, 'title', false)); - $this->assertTrue($validation->expectKeyToBeArray($this->data3, 'title', true)); - } -} 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/StrictArrayValidatorTest.php deleted file mode 100644 index 07b92d8..0000000 --- a/tests/StrictArrayValidatorTest.php +++ /dev/null @@ -1,60 +0,0 @@ - 'foo', - 'content' => 'foobar', - ]; - - private $data2 = [ - 'title' => 'foo' - ]; - - private $data3 = [ - 'title' => null, - 'views' => 45, - 'comments' => [] - ]; - - /** - * @throws ArrayValidationException - */ - public function testNoException() - { - $strictValidator = new StrictArrayValidator(new ArrayValidation(), $this->data1); - - $strictValidator - ->expectAtLeastKeys(['title', 'content']) - ->expectExactlyKeys(['title', 'content']) - ->expectOnlyKeys(['title', 'content']) - ->expectKeysToBeString(['title', 'content']); - - $this->assertTrue(true); - } - - /** - * @throws ArrayValidationException - */ - public function testWithException() - { - $this->expectException(ArrayValidationException::class); - - $strictValidator = new StrictArrayValidator(new ArrayValidation(), $this->data1, 'my data'); - - $strictValidator - ->expectAtLeastKeys(['title', 'content']) - ->expectExactlyKeys(['title', 'content']) - ->expectOnlyKeys(['title', 'content']) - ->expectKeysToBeInteger(['title', 'content']); - - $this->assertTrue(true); - } -} \ No newline at end of file diff --git a/tests/StrictValidationTest.php b/tests/StrictValidationTest.php new file mode 100644 index 0000000..6287cd3 --- /dev/null +++ b/tests/StrictValidationTest.php @@ -0,0 +1,205 @@ + 'foo', + 'content' => 'foobar', + ]; + + private $data2 = [ + 'id' => 1, + 'title' => 'foo', + 'isPrivate' => true, + 'tags' => ['tag1', ''], + 'money' => 15.55 + ]; + + private $data3 = [ + 'title' => null, + 'views' => 45, + 'comments' => [] + ]; + + /** + * @throws InvalidStructureException + * @throws InvalidTypeException + */ + public function testNoException1() + { + $strictValidator = new StrictValidation($this->data1); + + $strictValidator + ->expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeString(['title', 'content']); + + $this->assertTrue(true); + } + + /** + * @throws InvalidStructureException + * @throws InvalidTypeException + */ + public function testNoException2() + { + $strictValidator = new StrictValidation($this->data2); + + $strictValidator + ->expectOnlyKeys(['id', 'title', 'isPrivate', 'tags', 'money']) + ->expectKeysToBeInteger(['id']) + ->expectKeysToBeString(['title']) + ->expectKeysToBeBoolean(['isPrivate']) + ->expectKeysToBeArray(['tags']) + ->expectKeysToBeFloat(['money']); + + $this->assertTrue(true); + } + + /** + * @throws InvalidStructureException + */ + public function testOnlyKeysException() + { + $this->expectException(InvalidStructureException::class); + $strictValidator = new StrictValidation(['title' => 1]); + $strictValidator->expectOnlyKeys(['name']); + } + + /** + * @throws InvalidStructureException + */ + public function testNKeysException() + { + $this->expectException(InvalidStructureException::class); + $strictValidator = new StrictValidation(['title' => 1, 'description' => 'foo']); + $strictValidator->expectNKeys(1); + } + + /** + * @throws InvalidStructureException + */ + public function testNKeysNoException() + { + $strictValidator = new StrictValidation(['title' => 1]); + $strictValidator->expectNKeys(1); + $this->assertTrue(true); + } + + /** + * @throws InvalidStructureException + */ + public function testOnlyOneFromKeysException() + { + $this->expectException(InvalidStructureException::class); + $strictValidator = new StrictValidation(['title' => 'x', 'description' => 'foo']); + $strictValidator->expectOnlyOneFromKeys(['title', 'description']); + } + + /** + * @throws InvalidStructureException + */ + public function testExactlyKeysException() + { + $this->expectException(InvalidStructureException::class); + $strictValidator = new StrictValidation(['title' => 1]); + $strictValidator->expectExactlyKeys(['name']); + } + + /** + * @throws InvalidStructureException + */ + public function testOnlyOneFromKeysNoException() + { + $strictValidator = new StrictValidation(['title' => 1]); + $strictValidator->expectOnlyOneFromKeys(['name', 'title']); + $this->assertTrue(true); + } + + /** + * @throws InvalidTypeException + */ + public function testInvalidStringException() + { + $this->expectException(InvalidTypeException::class); + $strictValidator = new StrictValidation(['title' => 1]); + $strictValidator->expectKeyToBeString('title'); + } + + /** + * @throws InvalidTypeException + */ + public function testInvalidArrayException() + { + $this->expectException(InvalidTypeException::class); + $strictValidator = new StrictValidation(['tags' => 1]); + $strictValidator->expectKeyToBeArray('tags'); + } + /** + * @throws InvalidTypeException + */ + public function testInvalidBooleanException() + { + $this->expectException(InvalidTypeException::class); + $strictValidator = new StrictValidation(['isPrivate' => 1]); + $strictValidator->expectKeyToBeBoolean('isPrivate'); + } + + /** + * @throws InvalidTypeException + */ + public function testInvalidFloatException() + { + $this->expectException(InvalidTypeException::class); + $strictValidator = new StrictValidation(['money' => 1]); + $strictValidator->expectKeyToBeFloat('money'); + } + + /** + * @throws InvalidStructureException + * @throws InvalidTypeException + */ + public function testWithInvalidTypeException() + { + $this->expectException(InvalidTypeException::class); + + $strictValidator = new StrictValidation($this->data1); + + $strictValidator + ->expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeInteger(['title', 'content']); + + $this->assertTrue(true); + } + + /** + * @throws InvalidStructureException + * @throws InvalidTypeException + */ + public function testWithInvalidStructureException() + { + $this->expectException(InvalidStructureException::class); + + $strictValidator = new StrictValidation($this->data3, 'my data'); + + $strictValidator + ->expectAtLeastKeys(['title', 'content']) + ->expectExactlyKeys(['title', 'content']) + ->expectOnlyKeys(['title', 'content']) + ->expectKeysToBeInteger(['title', 'content']); + + $this->assertTrue(true); + } +} \ No newline at end of file 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/ValidatorTest.php b/tests/ValidatorTest.php new file mode 100644 index 0000000..6d79dba --- /dev/null +++ b/tests/ValidatorTest.php @@ -0,0 +1,196 @@ + 'foo', + 'content' => 'foobar', + ]; + + private $data2 = [ + 'title' => 'foo' + ]; + + private $data3 = [ + 'title' => null, + 'views' => 45, + 'comments' => [] + ]; + + public function testExpectExactlyKeys() + { + $validation = new Validator(); + $this->assertTrue($validation->expectExactlyKeys($this->data1, ['title', 'content'])); + $this->assertFalse($validation->expectExactlyKeys($this->data1, ['title', 'content', 'extra'])); + $this->assertFalse($validation->expectExactlyKeys($this->data1, ['title'])); + $this->assertFalse($validation->expectExactlyKeys($this->data1, ['extra'])); + $this->assertFalse($validation->expectExactlyKeys($this->data1, [])); + } + + public function testExpectOnlyOneFromKeys() + { + $validation = new Validator(); + $this->assertTrue($validation->expectOnlyOneFromKeys($this->data2, ['title', 'extra'])); + $this->assertTrue($validation->expectOnlyOneFromKeys($this->data2, ['title'])); + $this->assertFalse($validation->expectOnlyOneFromKeys($this->data2, ['extra'])); + } + + public function testExpectAtLeastKeys() + { + $validation = new Validator(); + $this->assertTrue($validation->expectAtLeastKeys($this->data1, ['title'])); + $this->assertTrue($validation->expectAtLeastKeys($this->data1, ['content'])); + $this->assertTrue($validation->expectAtLeastKeys($this->data1, ['title', 'content'])); + $this->assertFalse($validation->expectAtLeastKeys($this->data1, ['extra'])); + $this->assertFalse($validation->expectAtLeastKeys($this->data1, ['title', 'content', 'extra'])); + } + + public function testExpectOnlyKeys() + { + $validation = new Validator(); + $this->assertTrue($validation->expectOnlyKeys($this->data1, ['title', 'content'])); + $this->assertTrue($validation->expectOnlyKeys($this->data1, ['title', 'content', 'extra'])); + $this->assertFalse($validation->expectOnlyKeys($this->data1, ['title'])); + } + + public function testExpectNElement() + { + $validation = new Validator(); + $this->assertTrue($validation->expectNKeys($this->data1, 2)); + $this->assertFalse($validation->expectNKeys($this->data1, 1)); + } + + public function testIsArray() + { + $validation = new Validator(); + $this->assertTrue($validation->expectKeyToBeArray($this->data3, 'comments', false)); + $this->assertFalse($validation->expectKeyToBeArray($this->data3, 'title', false)); + $this->assertTrue($validation->expectKeyToBeArray($this->data3, 'title', true)); + + $data = [ + 'item1' => 45, + 'item2' => [], + 'item3' => [], + 'item4' => null, + ]; + $this->assertTrue($validation->expectKeysToBeArray($data, ['item2'], false)); + $this->assertTrue($validation->expectKeysToBeArray($data, ['item2', 'item3'], false)); + $this->assertFalse($validation->expectKeysToBeArray($data, ['item2', 'item3', 'item4'], false)); + $this->assertTrue($validation->expectKeysToBeArray($data, ['item2', 'item3', 'item4'], true)); + } + + public function testIsInt() + { + $data = [ + 'item1' => 45, + 'item2' => 36, + 'item3' => [], + 'item4' => null, + 'item5' => '', + ]; + $validation = new Validator(); + $this->assertTrue($validation->expectKeyToBeInteger($data, 'item1', false)); + $this->assertFalse($validation->expectKeyToBeInteger($data, 'item3', false)); + $this->assertFalse($validation->expectKeyToBeInteger($data, 'item4', false)); + $this->assertTrue($validation->expectKeyToBeInteger($data, 'item4', true)); + + + $this->assertTrue($validation->expectKeysToBeInteger($data, ['item1', 'item2'], false)); + $this->assertFalse($validation->expectKeysToBeInteger($data, ['item1', 'item2', 'item4'], false)); + $this->assertTrue($validation->expectKeysToBeInteger($data, ['item1', 'item2', 'item4'], true)); + $this->assertFalse($validation->expectKeysToBeInteger($data, ['item1', 'item2', 'item3', 'item4'], true)); + } + + public function testIsFloat() + { + $data = [ + 'item1' => 45.1, + 'item2' => 36.5, + 'item3' => [], + 'item4' => null, + 'item5' => 99, + ]; + $validation = new Validator(); + $this->assertTrue($validation->expectKeyToBeFloat($data, 'item1', false)); + $this->assertFalse($validation->expectKeyToBeFloat($data, 'item3', false)); + $this->assertFalse($validation->expectKeyToBeFloat($data, 'item4', false)); + $this->assertTrue($validation->expectKeyToBeFloat($data, 'item4', true)); + + + $this->assertTrue($validation->expectKeysToBeFloat($data, ['item1', 'item2'], false)); + $this->assertFalse($validation->expectKeysToBeFloat($data, ['item1', 'item2', 'item4'], false)); + $this->assertTrue($validation->expectKeysToBeFloat($data, ['item1', 'item2', 'item4'], true)); + $this->assertFalse($validation->expectKeysToBeFloat($data, ['item1', 'item2', 'item3', 'item4'], true)); + $this->assertFalse($validation->expectKeysToBeFloat($data, ['item1', 'item2', 'item5'], false)); + } + + public function testIsBool() + { + $data = [ + 'item1' => true, + 'item2' => false, + 'item3' => [], + 'item4' => null, + 'item5' => 1, + ]; + $validation = new Validator(); + $this->assertTrue($validation->expectKeyToBeBoolean($data, 'item1', false)); + $this->assertFalse($validation->expectKeyToBeBoolean($data, 'item3', false)); + $this->assertFalse($validation->expectKeyToBeBoolean($data, 'item5', false)); + $this->assertFalse($validation->expectKeyToBeBoolean($data, 'item4', false)); + $this->assertTrue($validation->expectKeyToBeBoolean($data, 'item4', true)); + + + $this->assertTrue($validation->expectKeysToBeBoolean($data, ['item1', 'item2'], false)); + $this->assertFalse($validation->expectKeysToBeBoolean($data, ['item1', 'item2', 'item4'], false)); + $this->assertTrue($validation->expectKeysToBeBoolean($data, ['item1', 'item2', 'item4'], true)); + $this->assertFalse($validation->expectKeysToBeBoolean($data, ['item1', 'item2', 'item3', 'item4'], true)); + } + + public function testIsString() + { + $data = [ + 'item1' => 'string', + 'item2' => 'string', + 'item3' => [], + 'item4' => null, + 'item5' => 2, + ]; + $validation = new Validator(); + $this->assertTrue($validation->expectKeyToBeString($data, 'item1', false)); + $this->assertFalse($validation->expectKeyToBeString($data, 'item3', false)); + $this->assertFalse($validation->expectKeyToBeString($data, 'item4', false)); + $this->assertTrue($validation->expectKeyToBeString($data, 'item4', true)); + + + $this->assertTrue($validation->expectKeysToBeString($data, ['item1', 'item2'], false)); + $this->assertFalse($validation->expectKeysToBeString($data, ['item1', 'item2', 'item4'], false)); + $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)); + } +}