├── src ├── Exception │ ├── MaximumDepthExceededException.php │ ├── ConstraintNotFoundException.php │ └── InvalidSchemaException.php ├── ConstraintInterface.php ├── Constraint │ └── DraftFour │ │ ├── Format │ │ └── FormatExtensionInterface.php │ │ ├── Not.php │ │ ├── MinItems.php │ │ ├── MaxItems.php │ │ ├── UniqueItems.php │ │ ├── Pattern.php │ │ ├── MaxProperties.php │ │ ├── MinProperties.php │ │ ├── Enum.php │ │ ├── Required.php │ │ ├── AllOf.php │ │ ├── MinLength.php │ │ ├── AnyOf.php │ │ ├── MaxLength.php │ │ ├── Maximum.php │ │ ├── Minimum.php │ │ ├── ExclusiveMaximum.php │ │ ├── OneOf.php │ │ ├── MultipleOf.php │ │ ├── PatternProperties.php │ │ ├── ExclusiveMinimum.php │ │ ├── Properties.php │ │ ├── Items.php │ │ ├── Dependencies.php │ │ ├── AdditionalItems.php │ │ ├── Type.php │ │ ├── AdditionalProperties.php │ │ └── Format.php ├── RuleSet │ ├── RuleSetContainer.php │ └── DraftFour.php ├── Assert.php ├── functions.php ├── ValidationError.php └── Validator.php ├── .editorconfig ├── LICENSE.md ├── CONTRIBUTING.md ├── composer.json ├── README.md └── CHANGELOG.md /src/Exception/MaximumDepthExceededException.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | 21 | $subValidator = $validator->makeSubSchemaValidator($value, $parameter); 22 | if ($subValidator->passes()) { 23 | return error('The data must not match the schema.', $validator); 24 | } 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/MinItems.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | Assert::nonNegative($parameter, self::KEYWORD, $validator->getSchemaPath()); 21 | 22 | if (!is_array($value) || count($value) >= $parameter) { 23 | return null; 24 | } 25 | 26 | return error('The array must contain at least {parameter} items.', $validator); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/MaxItems.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | Assert::nonNegative($parameter, self::KEYWORD, $validator->getSchemaPath()); 21 | 22 | if (!is_array($value) || count($value) <= $parameter) { 23 | return null; 24 | } 25 | 26 | return error('The array must contain less than {parameter} items.', $validator); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/UniqueItems.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | 21 | if (!is_array($value) || $parameter === false) { 22 | return null; 23 | } 24 | 25 | if (count($value) === count(array_unique(array_map('serialize', $value)))) { 26 | return null; 27 | } 28 | 29 | return error('The array must be unique.', $validator); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Pattern.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 21 | 22 | if (!is_string($value)) { 23 | return null; 24 | } 25 | 26 | if (preg_match(JsonGuard\delimit_pattern($parameter), $value) === 1) { 27 | return null; 28 | } 29 | 30 | return error('The string must match the pattern {parameter}.', $validator); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/MaxProperties.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | Assert::nonNegative($parameter, self::KEYWORD, $validator->getSchemaPath()); 21 | 22 | if (!is_object($value) || count(get_object_vars($value)) <= $parameter) { 23 | return null; 24 | } 25 | 26 | return error('The object must contain less than {parameter} properties.', $validator); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/MinProperties.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | Assert::nonNegative($parameter, self::KEYWORD, $validator->getSchemaPath()); 21 | 22 | if (!is_object($value) || count(get_object_vars($value)) >= $parameter) { 23 | return null; 24 | } 25 | 26 | return error('The object must contain at least {parameter} properties.', $validator); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Enum.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | 21 | if (is_object($value)) { 22 | foreach ($parameter as $i) { 23 | if (is_object($i) && $value == $i) { 24 | return null; 25 | } 26 | } 27 | } else { 28 | if (in_array($value, $parameter, true)) { 29 | return null; 30 | } 31 | } 32 | 33 | return error('The value must be one of: {parameter}', $validator); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Required.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | Assert::notEmpty($parameter, self::KEYWORD, $validator->getSchemaPath()); 21 | 22 | if (!is_object($value)) { 23 | return null; 24 | } 25 | 26 | $actualProperties = array_keys(get_object_vars($value)); 27 | $missing = array_diff($parameter, $actualProperties); 28 | if (count($missing)) { 29 | return error('The object must contain the properties {cause}.', $validator) 30 | ->withCause(array_values($missing)); 31 | } 32 | 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/AllOf.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | Assert::notEmpty($parameter, self::KEYWORD, $validator->getSchemaPath()); 21 | 22 | $errors = []; 23 | 24 | foreach ($parameter as $key => $schema) { 25 | $subValidator = $validator->makeSubSchemaValidator( 26 | $value, 27 | $schema, 28 | $validator->getDataPath(), 29 | pointer_push($validator->getSchemaPath(), $key) 30 | ); 31 | $errors = array_merge($errors, $subValidator->errors()); 32 | } 33 | 34 | return $errors; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/MinLength.php: -------------------------------------------------------------------------------- 1 | charset = $charset; 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function validate($value, $parameter, Validator $validator) 32 | { 33 | Assert::type($parameter, 'number', self::KEYWORD, $validator->getSchemaPath()); 34 | Assert::nonNegative($parameter, self::KEYWORD, $validator->getSchemaPath()); 35 | 36 | if (!is_string($value) || JsonGuard\strlen($value, $this->charset) >= $parameter) { 37 | return null; 38 | } 39 | 40 | return error('The string must be at least {parameter} characters long.', $validator); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/AnyOf.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 21 | Assert::notEmpty($parameter, self::KEYWORD, $validator->getSchemaPath()); 22 | 23 | foreach ($parameter as $key => $schema) { 24 | $subValidator = $validator->makeSubSchemaValidator( 25 | $value, 26 | $schema, 27 | $validator->getDataPath(), 28 | pointer_push($validator->getSchemaPath(), $key) 29 | ); 30 | if ($subValidator->passes()) { 31 | return null; 32 | } 33 | } 34 | return error('The data must match one of the schemas.', $validator); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/MaxLength.php: -------------------------------------------------------------------------------- 1 | charset = $charset; 26 | } 27 | 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function validate($value, $parameter, Validator $validator) 33 | { 34 | Assert::type($parameter, 'number', self::KEYWORD, $validator->getSchemaPath()); 35 | Assert::nonNegative($parameter, self::KEYWORD, $validator->getSchemaPath()); 36 | 37 | if (!is_string($value) || JsonGuard\strlen($value, $this->charset) <= $parameter) { 38 | return null; 39 | } 40 | 41 | return error('The string must be less than {parameter} characters long.', $validator); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2016, Matthew Allan (matthew.james.allan@gmail.com) and the JSON Guard contributors 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Maximum.php: -------------------------------------------------------------------------------- 1 | precision = $precision; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function validate($value, $parameter, Validator $validator) 31 | { 32 | Assert::type($parameter, 'number', self::KEYWORD, $validator->getSchemaPath()); 33 | 34 | if (isset($validator->getSchema()->exclusiveMaximum) && $validator->getSchema()->exclusiveMaximum === true) { 35 | return; 36 | } 37 | 38 | if (!is_numeric($value) || 39 | bccomp($value, $parameter, $this->precision) === -1 || bccomp($value, $parameter, $this->precision) === 0) { 40 | return null; 41 | } 42 | 43 | return error('The number must be less than {parameter}.', $validator); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Minimum.php: -------------------------------------------------------------------------------- 1 | precision = $precision; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function validate($value, $parameter, Validator $validator) 31 | { 32 | Assert::type($parameter, 'number', self::KEYWORD, $validator->getSchemaPath()); 33 | 34 | if (isset($validator->getSchema()->exclusiveMinimum) && $validator->getSchema()->exclusiveMinimum === true) { 35 | return null; 36 | } 37 | 38 | if (!is_numeric($value) || 39 | bccomp($value, $parameter, $this->precision) === 1 || bccomp($value, $parameter, $this->precision) === 0) { 40 | return null; 41 | } 42 | 43 | return error('The number must be at least {parameter}.', $validator); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/ExclusiveMaximum.php: -------------------------------------------------------------------------------- 1 | precision = $precision; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function validate($value, $parameter, Validator $validator) 31 | { 32 | Assert::type($parameter, 'boolean', self::KEYWORD, $validator->getSchemaPath()); 33 | Assert::hasProperty($validator->getSchema(), 'maximum', self::KEYWORD, $validator->getSchemaPath()); 34 | 35 | if ($parameter !== true) { 36 | return null; 37 | } 38 | 39 | if (!is_numeric($value) || bccomp($value, $validator->getSchema()->maximum, $this->precision) === -1) { 40 | return null; 41 | } 42 | 43 | return error('The number must be less than {parameter}.', $validator); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/OneOf.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 21 | Assert::notEmpty($parameter, self::KEYWORD, $validator->getSchemaPath()); 22 | 23 | $passed = 0; 24 | foreach ($parameter as $key => $schema) { 25 | $subValidator = $validator->makeSubSchemaValidator( 26 | $value, 27 | $schema, 28 | $validator->getDataPath(), 29 | pointer_push($validator->getSchemaPath(), $key) 30 | ); 31 | if ($subValidator->passes()) { 32 | $passed++; 33 | } 34 | } 35 | if ($passed !== 1) { 36 | return error('The data must match exactly one of the schemas.', $validator); 37 | } 38 | 39 | return null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/MultipleOf.php: -------------------------------------------------------------------------------- 1 | precision = $precision; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function validate($value, $parameter, Validator $validator) 31 | { 32 | Assert::type($parameter, 'number', self::KEYWORD, $validator->getSchemaPath()); 33 | Assert::nonNegative($parameter, self::KEYWORD, $validator->getSchemaPath()); 34 | 35 | if (!is_numeric($value)) { 36 | return null; 37 | } 38 | 39 | $quotient = bcdiv($value, $parameter, $this->precision); 40 | $mod = bcsub($quotient, floor($quotient), $this->precision); 41 | if (bccomp($mod, 0, $this->precision) === 0) { 42 | return null; 43 | } 44 | 45 | return error('The number must be a multiple of {parameter}.', $validator); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/PatternProperties.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 21 | 22 | if (!is_object($value)) { 23 | return null; 24 | } 25 | 26 | $errors = []; 27 | foreach ($parameter as $property => $schema) { 28 | $matches = JsonGuard\properties_matching_pattern($property, $value); 29 | foreach ($matches as $match) { 30 | $subValidator = $validator->makeSubSchemaValidator( 31 | $value->$match, 32 | $schema, 33 | pointer_push($validator->getDataPath(), $match), 34 | pointer_push($validator->getSchemaPath(), $property) 35 | ); 36 | $errors = array_merge($errors, $subValidator->errors()); 37 | } 38 | } 39 | return $errors; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/ExclusiveMinimum.php: -------------------------------------------------------------------------------- 1 | precision = $precision; 25 | } 26 | 27 | /** 28 | * @param mixed $value 29 | * @param mixed $parameter 30 | * @param Validator $validator 31 | * 32 | * @return \League\JsonGuard\ValidationError|null 33 | */ 34 | public function validate($value, $parameter, Validator $validator) 35 | { 36 | Assert::type($parameter, 'boolean', self::KEYWORD, $validator->getSchemaPath()); 37 | Assert::hasProperty($validator->getSchema(), 'minimum', self::KEYWORD, $validator->getSchemaPath()); 38 | 39 | if ($parameter !== true) { 40 | return null; 41 | } 42 | 43 | if (!is_numeric($value) || bccomp($value, $validator->getSchema()->minimum, $this->precision) === 1) { 44 | return null; 45 | } 46 | 47 | return error('The number must be greater than {parameter}.', $validator); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Properties.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | 21 | if (!is_object($value)) { 22 | return null; 23 | } 24 | 25 | // Iterate through the properties and create a new validator for that property's schema and data. 26 | $errors = []; 27 | foreach ($parameter as $property => $schema) { 28 | if (is_object($value) && property_exists($value, $property)) { 29 | $subValidator = $validator->makeSubSchemaValidator( 30 | $value->$property, 31 | $schema, 32 | pointer_push($validator->getDataPath(), $property), 33 | pointer_push($validator->getSchemaPath(), $property) 34 | ); 35 | if ($subValidator->fails()) { 36 | $errors = array_merge($errors, $subValidator->errors()); 37 | } 38 | } 39 | } 40 | 41 | return $errors; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/thephpleague/json-guard). 6 | 7 | If you plan to implement a new feature please open an issue first. The core library aims to fully support the JSON Schema standard. Features that are not part of the JSON Schema specification are unlikely to be merged. 8 | 9 | ## Pull Requests 10 | 11 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 12 | 13 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 14 | 15 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 16 | 17 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 18 | 19 | - **Create feature branches** - Don't ask us to pull from your master branch. 20 | 21 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 22 | 23 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 24 | 25 | 26 | ## Running Tests 27 | 28 | ``` bash 29 | $ composer test 30 | ``` 31 | 32 | ## Checking Code Style 33 | 34 | ```bash 35 | composer cs 36 | ``` 37 | 38 | 39 | **Happy coding**! 40 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Items.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 20 | 21 | if (!is_array($value)) { 22 | return null; 23 | } 24 | 25 | $errors = []; 26 | foreach ($value as $key => $itemValue) { 27 | $schema = self::getSchema($parameter, $key); 28 | 29 | // Additional items are allowed by default, 30 | // so there might not be a schema for this. 31 | if (is_null($schema)) { 32 | continue; 33 | } 34 | 35 | $subValidator = $validator->makeSubSchemaValidator( 36 | $itemValue, 37 | $schema, 38 | pointer_push($validator->getDataPath(), $key), 39 | pointer_push($validator->getSchemaPath(), $key) 40 | ); 41 | $errors = array_merge($errors, $subValidator->errors()); 42 | } 43 | 44 | return $errors ?: null; 45 | } 46 | 47 | /** 48 | * @param $parameter 49 | * @param $key 50 | * 51 | * @return mixed 52 | */ 53 | private static function getSchema($parameter, $key) 54 | { 55 | if (is_object($parameter)) { 56 | // list validation 57 | return $parameter; 58 | } elseif (is_array($parameter) && array_key_exists($key, $parameter)) { 59 | // tuple validation 60 | return $parameter[$key]; 61 | } 62 | 63 | return null; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Dependencies.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 21 | 22 | $errors = []; 23 | foreach ($parameter as $property => $dependencies) { 24 | if (!is_object($value) || !property_exists($value, $property)) { 25 | continue; 26 | } 27 | 28 | if (is_array($dependencies)) { 29 | $errors = array_merge( 30 | $errors, 31 | array_filter(array_map(function ($dependency) use ($value, $validator) { 32 | if (!in_array($dependency, array_keys(get_object_vars($value)), true)) { 33 | return error('The object must contain the dependent property {cause}.', $validator) 34 | ->withCause($dependency); 35 | } 36 | }, $dependencies)) 37 | ); 38 | } elseif (is_object($dependencies)) { 39 | $errors = array_merge( 40 | $errors, 41 | $validator->makeSubSchemaValidator( 42 | $value, 43 | $dependencies, 44 | $validator->getDataPath(), 45 | pointer_push($validator->getSchemaPath(), $property) 46 | )->errors() 47 | ); 48 | } 49 | } 50 | 51 | return $errors; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/RuleSet/RuleSetContainer.php: -------------------------------------------------------------------------------- 1 | $rule) { 24 | $this->set($keyword, $rule); 25 | } 26 | } 27 | 28 | public function get($keyword) 29 | { 30 | if (!$this->has($keyword)) { 31 | throw ConstraintNotFoundException::forRule($keyword); 32 | } 33 | 34 | return $this->rules[$keyword]($this); 35 | } 36 | 37 | public function has($keyword) 38 | { 39 | return isset($this->rules[$keyword]); 40 | } 41 | 42 | /** 43 | * Adds a rule to the container. 44 | * 45 | * @param string $keyword Identifier of the entry. 46 | * @param \Closure|string $factory The closure to invoke when this entry is resolved or the FQCN. 47 | * The closure will be given this container as the only 48 | * argument when invoked. 49 | */ 50 | public function set($keyword, $factory) 51 | { 52 | if (!(is_string($factory) || $factory instanceof \Closure)) { 53 | throw new \InvalidArgumentException( 54 | sprintf('Expected a string or Closure, got %s', gettype($keyword)) 55 | ); 56 | } 57 | 58 | $this->rules[$keyword] = function ($container) use ($factory) { 59 | static $object; 60 | 61 | if (is_null($object)) { 62 | $object = is_string($factory) ? new $factory() : $factory($container); 63 | } 64 | 65 | return $object; 66 | }; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "league/json-guard", 3 | "type": "library", 4 | "description": "A validator for JSON using json-schema.", 5 | "keywords": [ 6 | "Validation", 7 | "json", 8 | "schema", 9 | "json-schema", 10 | "json-schema.org" 11 | ], 12 | "homepage": "https://github.com/thephpleague/json-guard", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Matthew Allan", 17 | "email": "matthew.james.allan@gmail.com", 18 | "homepage": "https://mattallan.org", 19 | "role": "Developer" 20 | } 21 | ], 22 | "repositories": [ 23 | { 24 | "type": "package", 25 | "package": { 26 | "name": "json-schema/JSON-Schema-Test-Suite", 27 | "version": "1.2.0", 28 | "source": { 29 | "url": "https://github.com/json-schema/JSON-Schema-Test-Suite", 30 | "type": "git", 31 | "reference": "1.2.0" 32 | } 33 | } 34 | } 35 | ], 36 | "require": { 37 | "php": ">=5.6.0", 38 | "psr/container": "^1.0", 39 | "ext-bcmath": "*" 40 | }, 41 | "require-dev": { 42 | "phpunit/phpunit" : "^4.8.35", 43 | "scrutinizer/ocular": "~1.1", 44 | "squizlabs/php_codesniffer": "~2.3", 45 | "json-schema/JSON-Schema-Test-Suite": "1.2.0", 46 | "league/json-reference": "^1.1.0", 47 | "ext-curl": "*", 48 | "peterpostmann/fileuri": "^1.0" 49 | }, 50 | "suggest": { 51 | "league/json-reference": "Required to use schemas containing JSON references" 52 | }, 53 | "autoload": { 54 | "psr-4": { 55 | "League\\JsonGuard\\": "src" 56 | }, 57 | "files": [ 58 | "src/functions.php" 59 | ] 60 | }, 61 | "autoload-dev": { 62 | "psr-4": { 63 | "League\\JsonGuard\\Test\\": "tests", 64 | "League\\JsonGuard\\Bench\\": "tests/benchmarks" 65 | } 66 | }, 67 | "scripts": { 68 | "test": "phpunit", 69 | "test-server": "php -S localhost:1234 -t ./vendor/json-schema/JSON-Schema-Test-Suite/remotes/", 70 | "cs": "phpcs --standard=psr2 src/", 71 | "bench": "phpbench run ./tests/benchmarks --report=default" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Assert.php: -------------------------------------------------------------------------------- 1 | = 0) { 38 | return; 39 | } 40 | 41 | throw InvalidSchemaException::negativeValue( 42 | $value, 43 | $keyword, 44 | $pointer 45 | ); 46 | } 47 | 48 | /** 49 | * Validate a value is one of the allowed types. 50 | * 51 | * @param mixed $value 52 | * @param array|string $choices 53 | * @param string $keyword 54 | * @param string $pointer 55 | * 56 | * @throws InvalidSchemaException 57 | */ 58 | public static function type($value, $choices, $keyword, $pointer) 59 | { 60 | $actualType = gettype($value); 61 | $choices = is_array($choices) ? $choices : [$choices]; 62 | 63 | if (in_array($actualType, $choices) || 64 | (is_json_number($value) && in_array('number', $choices))) { 65 | return; 66 | } 67 | 68 | throw InvalidSchemaException::invalidParameterType( 69 | $actualType, 70 | $choices, 71 | $keyword, 72 | $pointer 73 | ); 74 | } 75 | 76 | /** 77 | * @param object $schema 78 | * @param string $property 79 | * @param string $keyword 80 | * @param string $pointer 81 | */ 82 | public static function hasProperty($schema, $property, $keyword, $pointer) 83 | { 84 | if (isset($schema->$property)) { 85 | return; 86 | } 87 | 88 | throw InvalidSchemaException::missingProperty( 89 | $property, 90 | $keyword, 91 | $pointer 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/AdditionalItems.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 21 | 22 | if (!is_array($value) || $parameter === true) { 23 | return null; 24 | } 25 | 26 | if (!is_array($items = self::getItems($validator->getSchema()))) { 27 | return null; 28 | } 29 | 30 | if ($parameter === false) { 31 | return self::validateAdditionalItemsWhenNotAllowed($value, $items, $validator); 32 | } elseif (is_object($parameter)) { 33 | $additionalItems = array_slice($value, count($items)); 34 | 35 | return self::validateAdditionalItemsAgainstSchema($additionalItems, $parameter, $validator); 36 | } 37 | } 38 | 39 | /** 40 | * @param object $schema 41 | * 42 | * @return mixed 43 | */ 44 | private static function getItems($schema) 45 | { 46 | return property_exists($schema, Items::KEYWORD) ? $schema->items : null; 47 | } 48 | 49 | /** 50 | * @param array $items 51 | * @param object $schema 52 | * @param Validator $validator 53 | * 54 | * @return array 55 | */ 56 | private static function validateAdditionalItemsAgainstSchema($items, $schema, Validator $validator) 57 | { 58 | $errors = []; 59 | foreach ($items as $key => $item) { 60 | $subValidator = $validator->makeSubSchemaValidator( 61 | $item, 62 | $schema, 63 | pointer_push($validator->getDataPath(), $key) 64 | ); 65 | $errors = array_merge($errors, $subValidator->errors()); 66 | } 67 | 68 | return $errors; 69 | } 70 | 71 | /** 72 | * @param array $value 73 | * @param array $items 74 | * @param \League\JsonGuard\Validator $validator 75 | * 76 | * @return \League\JsonGuard\ValidationError 77 | */ 78 | private static function validateAdditionalItemsWhenNotAllowed($value, $items, Validator $validator) 79 | { 80 | if (count($value) > count($items)) { 81 | return error('The array must not contain additional items.', $validator); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON Guard 2 | 3 | [![Software License][ico-license]](LICENSE.md) 4 | [![Build Status][ico-travis]][link-travis] 5 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer] 6 | [![Quality Score][ico-code-quality]][link-code-quality] 7 | [![Documentation][ico-docs]][link-docs] 8 | 9 | # Unmaintained! 10 | 11 | This project is no longer maintained. Recommended alternatives: 12 | 13 | - [opis/json-schema](https://github.com/opis/json-schema) 14 | - [swaggest/php-json-schema](https://github.com/swaggest/php-json-schema) 15 | 16 | ----- 17 | 18 | This package is a validator for [JSON Schema](http://json-schema.org/). It fully supports draft 4 of the specification. 19 | 20 | Notable Features: 21 | 22 | - Passes the entire [draft 4 JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite). 23 | - Fully customizable with custom rule sets. 24 | - Helpful error messages with JSON Pointers. 25 | 26 | ## Install 27 | 28 | ### Via Composer 29 | 30 | ```bash 31 | composer require league/json-guard 32 | ``` 33 | 34 | ## Usage 35 | 36 | Complete documentation is available [here](http://json-guard.thephpleague.com/). 37 | 38 | ## Change log 39 | 40 | Please see [CHANGELOG](CHANGELOG.md) for more information about what has changed recently. 41 | 42 | ## Testing 43 | 44 | You need to run a web server while testing. 45 | 46 | ```bash 47 | $ composer test-server 48 | ``` 49 | 50 | Once the server is running, you can run the test suite. 51 | 52 | ``` bash 53 | $ composer test 54 | ``` 55 | 56 | ## Contributing 57 | 58 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 59 | 60 | ## Security 61 | 62 | If you discover any security related issues, please email matthew.james.allan@gmail.com instead of using the issue tracker. 63 | 64 | ## Credits 65 | 66 | - [Matt Allan][link-author] 67 | - [All Contributors][link-contributors] 68 | 69 | ## License 70 | 71 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 72 | 73 | [link-travis]: https://travis-ci.org/thephpleague/json-guard 74 | [link-scrutinizer]: https://scrutinizer-ci.com/g/thephpleague/json-guard/code-structure 75 | [link-code-quality]: https://scrutinizer-ci.com/g/thephpleague/json-guard 76 | [link-docs]: http://json-guard.thephpleague.com/ 77 | [link-author]: https://github.com/thephpleague 78 | [link-contributors]: ../../contributors 79 | 80 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 81 | [ico-travis]: https://img.shields.io/travis/thephpleague/json-guard/master.svg?style=flat-square 82 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/thephpleague/json-guard.svg?style=flat-square 83 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/thephpleague/json-guard.svg?style=flat-square 84 | [ico-docs]: https://img.shields.io/badge/Docs-Latest-brightgreen.svg?style=flat-square 85 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Type.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 21 | 22 | if (is_array($type)) { 23 | return $this->anyType($value, $type, $validator); 24 | } 25 | 26 | switch ($type) { 27 | case 'object': 28 | return $this->validateType($value, 'is_object', $validator); 29 | case 'array': 30 | return $this->validateType($value, 'is_array', $validator); 31 | case 'boolean': 32 | return $this->validateType($value, 'is_bool', $validator); 33 | case 'null': 34 | return $this->validateType($value, 'is_null', $validator); 35 | case 'number': 36 | return $this->validateType( 37 | $value, 38 | 'League\JsonGuard\is_json_number', 39 | $validator 40 | ); 41 | case 'integer': 42 | return $this->validateType( 43 | $value, 44 | 'League\JsonGuard\is_json_integer', 45 | $validator 46 | ); 47 | case 'string': 48 | return $this->validateType($value, 'is_string', $validator); 49 | } 50 | } 51 | 52 | /** 53 | * @param mixed $value 54 | * @param callable $callable 55 | * @param \League\JsonGuard\Validator $validator 56 | * 57 | * @return \League\JsonGuard\ValidationError|null 58 | * 59 | */ 60 | private function validateType($value, callable $callable, Validator $validator) 61 | { 62 | if (call_user_func($callable, $value) === true) { 63 | return null; 64 | } 65 | 66 | return error('The data must be a(n) {parameter}.', $validator); 67 | } 68 | 69 | /** 70 | * @param mixed $value 71 | * @param array $choices 72 | * 73 | * @param Validator $validator 74 | * 75 | * @return ValidationError|null 76 | */ 77 | private function anyType($value, array $choices, Validator $validator) 78 | { 79 | foreach ($choices as $type) { 80 | $error = $this->validate($value, $type, $validator); 81 | if (is_null($error)) { 82 | return null; 83 | } 84 | } 85 | 86 | return error('The data must be one of {parameter}.', $validator); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/AdditionalProperties.php: -------------------------------------------------------------------------------- 1 | getSchemaPath()); 22 | 23 | if (!is_object($value)) { 24 | return null; 25 | } 26 | 27 | $diff = self::getDiff($value, $validator->getSchema()); 28 | 29 | if (count($diff) === 0) { 30 | return null; 31 | } 32 | 33 | if ($parameter === false) { 34 | return error('The object must not contain additional properties ({cause}).', $validator) 35 | ->withCause($diff); 36 | } elseif (is_object($parameter)) { 37 | // If additionalProperties is an object it's a schema, 38 | // so validate all additional properties against it. 39 | $errors = []; 40 | foreach ($diff as $property) { 41 | $subValidator = $validator->makeSubSchemaValidator( 42 | $value->$property, 43 | $parameter, 44 | pointer_push($validator->getDataPath(), $property) 45 | ); 46 | $errors = array_merge($errors, $subValidator->errors()); 47 | } 48 | 49 | return $errors; 50 | } 51 | } 52 | 53 | /** 54 | * Get the properties in $value which are not in $schema 'properties' or matching 'patternProperties'. 55 | * 56 | * @param object $value 57 | * @param object $schema 58 | * 59 | * @return array 60 | */ 61 | private static function getDiff($value, $schema) 62 | { 63 | if (property_exists($schema, Properties::KEYWORD)) { 64 | $definedProperties = array_keys(get_object_vars($schema->properties)); 65 | } else { 66 | $definedProperties = []; 67 | } 68 | 69 | $actualProperties = array_keys(get_object_vars($value)); 70 | $diff = array_diff($actualProperties, $definedProperties); 71 | 72 | // The diff doesn't account for patternProperties, so lets filter those out too. 73 | if (property_exists($schema, PatternProperties::KEYWORD)) { 74 | foreach ($schema->patternProperties as $property => $schema) { 75 | $matches = JsonGuard\properties_matching_pattern($property, $diff); 76 | $diff = array_diff($diff, $matches); 77 | } 78 | 79 | return $diff; 80 | } 81 | 82 | return $diff; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/RuleSet/DraftFour.php: -------------------------------------------------------------------------------- 1 | AdditionalItems::class, 42 | AdditionalProperties::KEYWORD => AdditionalProperties::class, 43 | AllOf::KEYWORD => AllOf::class, 44 | Anyof::KEYWORD => AnyOf::class, 45 | Dependencies::KEYWORD => Dependencies::class, 46 | Enum::KEYWORD => Enum::class, 47 | ExclusiveMaximum::KEYWORD => ExclusiveMaximum::class, 48 | ExclusiveMinimum::KEYWORD => ExclusiveMinimum::class, 49 | Format::KEYWORD => Format::class, 50 | Items::KEYWORD => Items::class, 51 | Maximum::KEYWORD => Maximum::class, 52 | MaxItems::KEYWORD => MaxItems::class, 53 | MaxLength::KEYWORD => MaxLength::class, 54 | MaxProperties::KEYWORD => MaxProperties::class, 55 | Minimum::KEYWORD => Minimum::class, 56 | MinItems::KEYWORD => MinItems::class, 57 | MinLength::KEYWORD => MinLength::class, 58 | MinProperties::KEYWORD => MinProperties::class, 59 | MultipleOf::KEYWORD => MultipleOf::class, 60 | Not::KEYWORD => Not::class, 61 | OneOF::KEYWORD => OneOf::class, 62 | Pattern::KEYWORD => Pattern::class, 63 | PatternProperties::KEYWORD => PatternProperties::class, 64 | Properties::KEYWORD => Properties::class, 65 | Required::KEYWORD => Required::class, 66 | Type::KEYWORD => Type::class, 67 | UniqueItems::KEYWORD => UniqueItems::class, 68 | ]; 69 | 70 | public function __construct(array $rules = []) 71 | { 72 | parent::__construct(array_merge(self::DEFAULT_RULES, $rules)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Exception/InvalidSchemaException.php: -------------------------------------------------------------------------------- 1 | keyword = $keyword; 27 | $this->pointer = $pointer; 28 | } 29 | 30 | /** 31 | * @param string $actualType 32 | * @param array $allowedTypes 33 | * @param string $keyword 34 | * @param string $pointer 35 | * 36 | * @return \League\JsonGuard\Exception\InvalidSchemaException 37 | */ 38 | public static function invalidParameterType($actualType, array $allowedTypes, $keyword, $pointer) 39 | { 40 | $message = sprintf( 41 | 'Value has type "%s" but must be one of: "%s"', 42 | $actualType, 43 | implode(', ', $allowedTypes) 44 | ); 45 | 46 | return new self($message, $keyword, $pointer); 47 | } 48 | 49 | /** 50 | * @param string $actualParameter 51 | * @param array $allowedParameter 52 | * @param string $keyword 53 | * @param string $pointer 54 | * 55 | * @return \League\JsonGuard\Exception\InvalidSchemaException 56 | */ 57 | public static function invalidParameter($actualParameter, array $allowedParameter, $keyword, $pointer) 58 | { 59 | $message = sprintf( 60 | 'Value is "%s" but must be one of: "%s"', 61 | $actualParameter, 62 | implode(', ', $allowedParameter) 63 | ); 64 | 65 | return new self($message, $keyword, $pointer); 66 | } 67 | 68 | /** 69 | * @param integer $value 70 | * @param string $keyword 71 | * @param string $pointer 72 | * 73 | * @return \League\JsonGuard\Exception\InvalidSchemaException 74 | */ 75 | public static function negativeValue($value, $keyword, $pointer) 76 | { 77 | $message = sprintf( 78 | 'Integer value "%d" must be greater than, or equal to, 0', 79 | $value 80 | ); 81 | 82 | return new self($message, $keyword, $pointer); 83 | } 84 | 85 | /** 86 | * @param string $keyword 87 | * @param string $pointer 88 | * 89 | * @return \League\JsonGuard\Exception\InvalidSchemaException 90 | */ 91 | public static function emptyArray($keyword, $pointer) 92 | { 93 | return new self( 94 | 'Array must have at least one element', 95 | $keyword, 96 | $pointer 97 | ); 98 | } 99 | 100 | /** 101 | * @param string $property 102 | * @param string $keyword 103 | * @param string $pointer 104 | * 105 | * @return \League\JsonGuard\Exception\InvalidSchemaException 106 | */ 107 | public static function missingProperty($property, $keyword, $pointer) 108 | { 109 | $message = sprintf( 110 | 'The schema must contain the property %s', 111 | $property 112 | ); 113 | 114 | return new self($message, $keyword, $pointer); 115 | } 116 | 117 | /** 118 | * @return string 119 | */ 120 | public function getKeyword() 121 | { 122 | return $this->keyword; 123 | } 124 | 125 | /** 126 | * @return string 127 | */ 128 | public function getPointer() 129 | { 130 | return $this->pointer; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | getCurrentKeyword(), 18 | $validator->getCurrentParameter(), 19 | $validator->getData(), 20 | $validator->getDataPath(), 21 | $validator->getSchema(), 22 | $validator->getSchemaPath() 23 | ); 24 | } 25 | 26 | /** 27 | * @param string $string 28 | * @param string $charset 29 | * 30 | * @return int 31 | */ 32 | function strlen($string, $charset = 'UTF-8') 33 | { 34 | if (function_exists('iconv_strlen')) { 35 | return iconv_strlen($string, $charset); 36 | } 37 | 38 | if (function_exists('mb_strlen')) { 39 | return mb_strlen($string, $charset); 40 | } 41 | 42 | if (function_exists('utf8_decode') && $charset === 'UTF-8') { 43 | $string = utf8_decode($string); 44 | } 45 | 46 | return \strlen($string); 47 | } 48 | 49 | /** 50 | * Returns the string representation of a value. 51 | * 52 | * @param mixed $value 53 | * @return string 54 | */ 55 | function as_string($value) 56 | { 57 | switch (true) { 58 | case is_scalar($value): 59 | $result = (string) $value; 60 | break; 61 | case is_resource($value): 62 | $result = ''; 63 | break; 64 | default: 65 | $result = (string) json_encode($value, JSON_UNESCAPED_SLASHES); 66 | } 67 | 68 | if (\strlen($result) > 100) { 69 | $result = substr($result, 0, 97) . '...'; 70 | } 71 | 72 | return $result; 73 | } 74 | 75 | /** 76 | * Get the properties matching $pattern from the $data. 77 | * 78 | * @param string $pattern 79 | * @param array|object $data 80 | * @return array 81 | */ 82 | function properties_matching_pattern($pattern, $data) 83 | { 84 | // If an object is supplied, extract an array of the property names. 85 | if (is_object($data)) { 86 | $data = array_keys(get_object_vars($data)); 87 | } 88 | 89 | return preg_grep(delimit_pattern($pattern), $data); 90 | } 91 | 92 | /** 93 | * Delimit a regular expression pattern. 94 | * 95 | * The regular expression syntax used for JSON schema is ECMA 262, from Javascript, 96 | * and does not use delimiters. Since the PCRE functions do, this function will 97 | * delimit a pattern and escape the delimiter if found in the pattern. 98 | * 99 | * @see http://json-schema.org/latest/json-schema-validation.html#anchor6 100 | * @see http://php.net/manual/en/regexp.reference.delimiters.php 101 | * 102 | * @param string $pattern 103 | * 104 | * @return string 105 | */ 106 | function delimit_pattern($pattern) 107 | { 108 | return '/' . str_replace('/', '\\/', $pattern) . '/'; 109 | } 110 | 111 | /** 112 | * Determines if the value is an integer or an integer that was cast to a string 113 | * because it is larger than PHP_INT_MAX. 114 | * 115 | * @param mixed $value 116 | * @return boolean 117 | */ 118 | function is_json_integer($value) 119 | { 120 | if (is_string($value) && \strlen($value) && $value[0] === '-') { 121 | $value = substr($value, 1); 122 | } 123 | 124 | return is_int($value) || (is_string($value) && ctype_digit($value) && bccomp($value, PHP_INT_MAX) === 1); 125 | } 126 | 127 | /** 128 | * Determines if the value is a number. A number is a float, integer, or a number that was cast 129 | * to a string because it is larger than PHP_INT_MAX. 130 | * 131 | * @param mixed $value 132 | * 133 | * @return boolean 134 | */ 135 | function is_json_number($value) 136 | { 137 | return is_float($value) || is_json_integer($value); 138 | } 139 | 140 | /** 141 | * Push a segment onto the given JSON Pointer. 142 | * 143 | * @param string $pointer 144 | * @param string[] $segments 145 | * 146 | * @return string 147 | * 148 | */ 149 | function pointer_push($pointer, ...$segments) 150 | { 151 | $segments = array_map(function ($segment) { 152 | $segment = str_replace('~', '~0', $segment); 153 | return str_replace('/', '~1', $segment); 154 | }, $segments); 155 | 156 | return ($pointer !== '/' ? $pointer : '') . '/' . implode('/', $segments); 157 | } 158 | -------------------------------------------------------------------------------- /src/ValidationError.php: -------------------------------------------------------------------------------- 1 | message = $message; 76 | $this->keyword = $keyword; 77 | $this->parameter = $parameter; 78 | $this->data = $data; 79 | $this->dataPath = $dataPath; 80 | $this->schema = $schema; 81 | $this->schemaPath = $schemaPath; 82 | } 83 | 84 | /** 85 | * Get the human readable error message for this error. 86 | * 87 | * @return string 88 | */ 89 | public function getMessage() 90 | { 91 | if ($this->interpolatedMessage === null) { 92 | $this->interpolatedMessage = $this->interpolate($this->message, $this->getContext()); 93 | } 94 | 95 | return $this->interpolatedMessage; 96 | } 97 | 98 | /** 99 | * @return string 100 | */ 101 | public function getKeyword() 102 | { 103 | return $this->keyword; 104 | } 105 | 106 | /** 107 | * @return mixed 108 | */ 109 | public function getParameter() 110 | { 111 | return $this->parameter; 112 | } 113 | 114 | /** 115 | * @return mixed 116 | */ 117 | public function getData() 118 | { 119 | return $this->data; 120 | } 121 | 122 | /** 123 | * @return string 124 | */ 125 | public function getDataPath() 126 | { 127 | return $this->dataPath; 128 | } 129 | 130 | /** 131 | * @return object 132 | */ 133 | public function getSchema() 134 | { 135 | return $this->schema; 136 | } 137 | 138 | /** 139 | * @return string 140 | */ 141 | public function getSchemaPath() 142 | { 143 | return $this->schemaPath; 144 | } 145 | 146 | /** 147 | * Get the cause of the error. The cause is either the the value itself 148 | * or the subset of the value that failed validation. For example, the 149 | * cause of a failed minimum constraint would be the number itself, while 150 | * the cause of a failed additionalProperties constraint would be the 151 | * additional properties in the value that are not allowed. 152 | * 153 | * @return mixed 154 | */ 155 | public function getCause() 156 | { 157 | return $this->cause !== null ? $this->cause : $this->data; 158 | } 159 | 160 | /** 161 | * @param $cause 162 | * 163 | * @return \League\JsonGuard\ValidationError 164 | */ 165 | public function withCause($cause) 166 | { 167 | $error = clone $this; 168 | $error->cause = $cause; 169 | 170 | return $error; 171 | } 172 | 173 | /** 174 | * Get the context that applied to the failed assertion. 175 | * 176 | * @return string[] 177 | */ 178 | public function getContext() 179 | { 180 | if ($this->context === null) { 181 | $this->context = array_map( 182 | 'League\JsonGuard\as_string', 183 | [ 184 | self::KEYWORD => $this->keyword, 185 | self::PARAMETER => $this->parameter, 186 | self::DATA => $this->data, 187 | self::DATA_PATH => $this->dataPath, 188 | self::SCHEMA => $this->schema, 189 | self::SCHEMA_PATH => $this->schemaPath, 190 | self::CAUSE => $this->getCause(), 191 | ] 192 | ); 193 | } 194 | 195 | return $this->context; 196 | } 197 | 198 | /** 199 | * @return array 200 | */ 201 | public function toArray() 202 | { 203 | return [ 204 | self::MESSAGE => $this->getMessage(), 205 | self::KEYWORD => $this->keyword, 206 | self::PARAMETER => $this->parameter, 207 | self::DATA => $this->data, 208 | self::DATA_PATH => $this->dataPath, 209 | self::SCHEMA => $this->schema, 210 | self::SCHEMA_PATH => $this->schemaPath, 211 | self::CAUSE => $this->getCause(), 212 | ]; 213 | } 214 | 215 | /** 216 | * @inheritdoc 217 | */ 218 | public function jsonSerialize() 219 | { 220 | return $this->toArray(); 221 | } 222 | 223 | /** 224 | * Interpolate the context values into the message placeholders. 225 | * 226 | * @param string $message 227 | * @param array $context 228 | * 229 | * @return string 230 | */ 231 | private function interpolate($message, array $context = []) 232 | { 233 | $replace = []; 234 | foreach ($context as $key => $val) { 235 | $replace['{' . $key . '}'] = $val; 236 | } 237 | 238 | return strtr($message, $replace); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/Constraint/DraftFour/Format.php: -------------------------------------------------------------------------------- 1 | \d{4})-(?0[1-9]|1[0-2])-(?0[1-9]|[12][0-9]|3[01])' . 'T' . 21 | '(?[01][0-9]|2[0-3]):(?[0-5][0-9]):(?[0-5][0-9]|60)(?\.[0-9]+)?' . 22 | '(Z|(\+|-)(?[01][0-9]|2[0-3]):(?[0-5][0-9]))$/i'; 23 | 24 | /** 25 | * @internal 26 | */ 27 | const HOST_NAME_PATTERN = '/^[_a-z]+\.([_a-z]+\.?)+$/i'; 28 | 29 | /** 30 | * @internal 31 | * 32 | * @var string[] 33 | */ 34 | const KNOWN_FORMATS = ['date-time', 'uri', 'email', 'ipv4', 'ipv6','hostname']; 35 | 36 | /** 37 | * @var \League\JsonGuard\Constraint\DraftFour\Format\FormatExtensionInterface[] 38 | */ 39 | private $extensions = []; 40 | 41 | /** 42 | * @var bool 43 | */ 44 | private $ignoreUnknownFormats = true; 45 | 46 | /** 47 | * Any custom format extensions to use, indexed by the format name. 48 | * 49 | * @param array \League\JsonGuard\Constraint\DraftFour\Format\FormatExtensionInterface[] $extensions 50 | * @param bool $ignoreUnknownFormats 51 | */ 52 | public function __construct(array $extensions = [], $ignoreUnknownFormats = true) 53 | { 54 | foreach ($extensions as $format => $extension) { 55 | $this->addExtension($format, $extension); 56 | } 57 | 58 | $this->ignoreUnknownFormats = $ignoreUnknownFormats; 59 | } 60 | 61 | /** 62 | * Add a custom format extension. 63 | * 64 | * @param string $format 65 | * @param \League\JsonGuard\Constraint\DraftFour\Format\FormatExtensionInterface $extension 66 | */ 67 | public function addExtension($format, FormatExtensionInterface $extension) 68 | { 69 | $this->extensions[$format] = $extension; 70 | } 71 | 72 | /** 73 | * Define if unknown formats shall be ignored 74 | * 75 | * @param boolean 76 | */ 77 | public function setIgnoreUnknownFormats($ignoreUnknownFormats) 78 | { 79 | $this->ignoreUnknownFormats = $ignoreUnknownFormats; 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | public function validate($value, $parameter, Validator $validator) 86 | { 87 | Assert::type($parameter, 'string', self::KEYWORD, $validator->getSchemaPath()); 88 | 89 | if (isset($this->extensions[$parameter])) { 90 | return $this->extensions[$parameter]->validate($value, $validator); 91 | } 92 | 93 | switch ($parameter) { 94 | case 'date-time': 95 | return self::validateRegex( 96 | $value, 97 | self::DATE_TIME_PATTERN, 98 | $validator 99 | ); 100 | case 'uri': 101 | return self::validateFilter( 102 | $value, 103 | FILTER_VALIDATE_URL, 104 | null, 105 | $validator 106 | ); 107 | case 'email': 108 | return self::validateFilter( 109 | $value, 110 | FILTER_VALIDATE_EMAIL, 111 | null, 112 | $validator 113 | ); 114 | case 'ipv4': 115 | return self::validateFilter( 116 | $value, 117 | FILTER_VALIDATE_IP, 118 | FILTER_FLAG_IPV4, 119 | $validator 120 | ); 121 | case 'ipv6': 122 | return self::validateFilter( 123 | $value, 124 | FILTER_VALIDATE_IP, 125 | FILTER_FLAG_IPV6, 126 | $validator 127 | ); 128 | case 'hostname': 129 | return self::validateRegex( 130 | $value, 131 | self::HOST_NAME_PATTERN, 132 | $validator 133 | ); 134 | default: 135 | if (!$this->ignoreUnknownFormats) { 136 | throw InvalidSchemaException::invalidParameter( 137 | $parameter, 138 | array_merge(self::KNOWN_FORMATS, array_keys($this->extensions)), 139 | self::KEYWORD, 140 | $validator->getSchemaPath() 141 | ); 142 | } 143 | } 144 | } 145 | 146 | /** 147 | * @param mixed $value 148 | * @param string $pattern 149 | * @param \League\JsonGuard\Validator $validator 150 | * 151 | * @return \League\JsonGuard\ValidationError|null 152 | */ 153 | private static function validateRegex($value, $pattern, Validator $validator) 154 | { 155 | if (!is_string($value) || preg_match($pattern, $value) === 1) { 156 | return null; 157 | } 158 | 159 | return error('The value {data} must match the format {parameter}.', $validator); 160 | } 161 | 162 | /** 163 | * @param mixed $value 164 | * @param int $filter 165 | * @param mixed $options 166 | * @param \League\JsonGuard\Validator $validator 167 | * 168 | * @return \League\JsonGuard\ValidationError|null 169 | */ 170 | private static function validateFilter($value, $filter, $options, Validator $validator) 171 | { 172 | if (!is_string($value) || filter_var($value, $filter, $options) !== false) { 173 | return null; 174 | } 175 | 176 | // This workaround allows otherwise valid protocol relative urls to pass. 177 | // @see https://bugs.php.net/bug.php?id=72301 178 | if ($filter === FILTER_VALIDATE_URL && is_string($value) && strpos($value, '//') === 0) { 179 | if (filter_var('http:' . $value, $filter, $options) !== false) { 180 | return null; 181 | } 182 | } 183 | 184 | return error('The value must match the format {parameter}.', $validator); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | resolve(); 87 | } 88 | 89 | $this->data = $data; 90 | $this->schema = $schema; 91 | $this->ruleSet = $ruleSet ?: new DraftFour(); 92 | } 93 | 94 | /** 95 | * @return boolean 96 | * 97 | * @throws \League\JsonGuard\Exception\InvalidSchemaException 98 | * @throws \League\JsonGuard\Exception\MaximumDepthExceededException 99 | */ 100 | public function fails() 101 | { 102 | return !$this->passes(); 103 | } 104 | 105 | /** 106 | * @return boolean 107 | * 108 | * @throws \League\JsonGuard\Exception\InvalidSchemaException 109 | * @throws \League\JsonGuard\Exception\MaximumDepthExceededException 110 | */ 111 | public function passes() 112 | { 113 | return empty($this->errors()); 114 | } 115 | 116 | /** 117 | * Get a collection of errors. 118 | * 119 | * @return ValidationError[] 120 | * 121 | * @throws \League\JsonGuard\Exception\InvalidSchemaException 122 | * @throws \League\JsonGuard\Exception\MaximumDepthExceededException 123 | */ 124 | public function errors() 125 | { 126 | $this->validate(); 127 | 128 | return $this->errors; 129 | } 130 | 131 | /** 132 | * Set the maximum allowed depth data will be validated until. 133 | * If the data exceeds the stack depth an exception is thrown. 134 | * 135 | * @param int $maxDepth 136 | * 137 | * @return $this 138 | */ 139 | public function setMaxDepth($maxDepth) 140 | { 141 | $this->maxDepth = $maxDepth; 142 | 143 | return $this; 144 | } 145 | 146 | /** 147 | * @return \Psr\Container\ContainerInterface 148 | */ 149 | public function getRuleSet() 150 | { 151 | return $this->ruleSet; 152 | } 153 | 154 | /** 155 | * @return string 156 | */ 157 | public function getDataPath() 158 | { 159 | return $this->dataPath; 160 | } 161 | 162 | /** 163 | * @return mixed 164 | */ 165 | public function getData() 166 | { 167 | return $this->data; 168 | } 169 | 170 | /** 171 | * @return object 172 | */ 173 | public function getSchema() 174 | { 175 | return $this->schema; 176 | } 177 | 178 | /** 179 | * @return string 180 | */ 181 | public function getSchemaPath() 182 | { 183 | return pointer_push($this->baseSchemaPath, $this->currentKeyword); 184 | } 185 | 186 | /** 187 | * @return string 188 | */ 189 | public function getCurrentKeyword() 190 | { 191 | return $this->currentKeyword; 192 | } 193 | 194 | /** 195 | * @return mixed 196 | */ 197 | public function getCurrentParameter() 198 | { 199 | return $this->currentParameter; 200 | } 201 | 202 | /** 203 | * Create a new sub-validator. 204 | * 205 | * @param mixed $data 206 | * @param object $schema 207 | * @param string|null $dataPath 208 | * @param string|null $schemaPath 209 | * 210 | * @return Validator 211 | */ 212 | public function makeSubSchemaValidator($data, $schema, $dataPath = null, $schemaPath = null) 213 | { 214 | $validator = new Validator($data, $schema, $this->ruleSet); 215 | 216 | $validator->dataPath = $dataPath ?: $this->dataPath; 217 | $validator->baseSchemaPath = $schemaPath ?: $this->getSchemaPath(); 218 | $validator->maxDepth = $this->maxDepth; 219 | $validator->depth = $this->depth + 1; 220 | 221 | return $validator; 222 | } 223 | 224 | /** 225 | * Validate the data and collect the errors. 226 | */ 227 | private function validate() 228 | { 229 | if ($this->hasValidated) { 230 | return; 231 | } 232 | 233 | $this->checkDepth(); 234 | 235 | foreach ($this->schema as $rule => $parameter) { 236 | $this->currentKeyword = $rule; 237 | $this->currentParameter = $parameter; 238 | $this->mergeErrors($this->validateRule($rule, $parameter)); 239 | $this->currentKeyword = $this->currentParameter = null; 240 | } 241 | 242 | $this->hasValidated = true; 243 | } 244 | 245 | /** 246 | * Keep track of how many levels deep we have validated. 247 | * This is to prevent a really deeply nested JSON 248 | * structure from causing the validator to continue 249 | * validating for an incredibly long time. 250 | * 251 | * @throws \League\JsonGuard\Exception\MaximumDepthExceededException 252 | */ 253 | private function checkDepth() 254 | { 255 | if ($this->depth > $this->maxDepth) { 256 | throw new MaximumDepthExceededException(); 257 | } 258 | } 259 | 260 | /** 261 | * Validate the data using the given rule and parameter. 262 | * 263 | * @param string $keyword 264 | * @param mixed $parameter 265 | * 266 | * @return null|ValidationError|ValidationError[] 267 | */ 268 | private function validateRule($keyword, $parameter) 269 | { 270 | if (!$this->ruleSet->has($keyword)) { 271 | return null; 272 | } 273 | 274 | return $this->ruleSet->get($keyword)->validate($this->data, $parameter, $this); 275 | } 276 | 277 | /** 278 | * Merge the errors with our error collection. 279 | * 280 | * @param ValidationError[]|ValidationError|null $errors 281 | */ 282 | private function mergeErrors($errors) 283 | { 284 | if (is_null($errors)) { 285 | return; 286 | } 287 | 288 | $errors = is_array($errors) ? $errors : [$errors]; 289 | $this->errors = array_merge($this->errors, $errors); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## Unreleased 4 | 5 | ### Fixed 6 | 7 | * Fixed many instances of the date-time validation rejecting valid values and allowing invalid values. 8 | 9 | ### Changed 10 | 11 | * @peterpostmann added the `ignoreUnknownFormats` option to the format constraint. If false the validator will throw an `InvalidSchemaException` for formats that can't be validated. 12 | 13 | ## 1.0.1 - 2017-05-03 14 | 15 | ## Fixed 16 | 17 | * Fixed the type validation to stop rejecting numeric strings larger than PHP_INT_MAX. This was originally implemented to prevent integers decoded with JSON_BIGINT_AS_STRING passing string validation but caused false negatives. If you need to prevent numeric strings you should add a pattern constraint. Contributed by @akeeman. 18 | 19 | ## 1.0.0 - 2017-04-29 20 | 21 | 1.0.0 is a complete rewrite. Please review the library and update your code accordingly. 22 | 23 | ### Fixed 24 | 25 | * Fixed the date-time validation to not allow invalid date-times in some cases. 26 | 27 | ### Changed 28 | 29 | #### General 30 | 31 | * Classes not meant to be extended have been marked final. 32 | 33 | #### Dependencies 34 | 35 | * Support was dropped for PHP 5.5. 36 | * HHVM is not actively supported anymore. 37 | * bcmatch is now a required extension. 38 | 39 | #### Separate Packages 40 | 41 | Starting with the 1.0 release json-guard is maintained as two separate packages - a JSON Schema validator implementation and a JSON Reference implementation. 42 | 43 | You will need to require both `league/json-guard` and `league/json-reference` if you are using JSON references. 44 | 45 | #### Dereferencing 46 | 47 | * The Dereferencer does not use JSON Schema draft 4 scope resolution rules (`id`) by default anymore. See [the scope resolution documentation](json-reference/scope-resolution) for more info. 48 | * Loaders are now registered with a loader manager. See [the loader documentation](json-reference/loaders) for more info. 49 | 50 | #### Constraints 51 | 52 | * All constraints now implement a single interface. See `League\JsonGuard\Constraint` for more info. If you are using custom constraints you should update them to match the new signature. 53 | * All draft 4 constraints were moved to the `League\JsonGuard\Constraint\DraftFour` namespace. 54 | * All constraints use dependency injection for configuration. This includes the precision used for minimum, maximum, and their exclusive variants and the charset used for minimumLength and maximumLength. 55 | * Custom format extensions are now registered with the format constraint directly. 56 | 57 | #### Rule Sets 58 | 59 | * The rule set interface was dropped in favor of the PSR-11 container interface. Custom rule sets can extend the `League\JsonGuard\RuleSet\RuleSetContainer` to make implementation easier. 60 | * The default rule set now uses the same instance each time instead of creating a new instance. 61 | 62 | #### Errors 63 | 64 | * Error messages no longer implement array access. 65 | * The error message 'value' has been renamed to 'data' and 'pointer' has been renamed to 'data_path'. 66 | * The data path for data at the root path will now return '/', not ''. 67 | * All error messages now return the same context. See `League\JsonGuard\ValidationError` for more info. 68 | * The error message constructor now requires (message, keyword, parameter, data, data path, schema, schema path). You can optionally set a cause. The `League\JsonGuard\error` function can be used to make building errors easier. 69 | * The data pointer will correctly return '/' instead of '' for errors in the root document. 70 | * The error messages have been rewritten to use consistent wording and do not include the value in the message. 71 | * The error context will truncate any strings over 100 characters. 72 | 73 | ### Removed 74 | 75 | * Removed the SubSchemaValidatorFactory interface. 76 | * Removed the the RuleSet interface. 77 | * Removed the Comparator. 78 | * Removed Pointer Parser. 79 | 80 | ## 0.5.1 - 2016-11-28 81 | 82 | ### Fixed 83 | 84 | * Fixed a bug where the context was being encoded as a string twice, resulting in extra quotes around parameters in the error message. 85 | * Updated incorrect docblock types for ValidationError keyword. 86 | 87 | ## 0.5.0 - 2016-11-28 88 | 89 | ### Changed 90 | 91 | * ValidationError "constraints" were replaced with "context". 92 | * Any calls to `ValidationError@getConstraint` need to be changed to `ValidationError@getContext`. 93 | * If you are using the `ArrayAccess` interface for `ValidationError` you need to replace any usage of the `constraints` key with `context`. 94 | * Unlike the old constraints array, every entry in the context array is a string. This makes implementing your own error messages a lot easier. 95 | * ValidationError "code" was replaced with "keyword". 96 | * Each validation error will now return a string keyword instead of a numeric code. 97 | * The League\JsonGuard\ErrorCode class was removed. 98 | * Any calls to `ValidationError@getCode` need to be changed to `ValidationError@getKeyword`. 99 | * If you are using the `ArrayAccess` interface for `ValidationError` you need to replace any usage of the `code` key with `keyword`. 100 | * Instead of there being a different code for every format failure, they just return the keyword 'format'. 101 | * Invalid schemas now throw an InvalidSchemaException. 102 | * Dereferencer@getLoader is now public. 103 | 104 | ### Fixed 105 | 106 | * Type number was passing for numeric strings when it should not have been. 107 | 108 | ### Added 109 | 110 | * Added a `getLoaders` method to the Dereferencer which returns all loaders. 111 | 112 | ## 0.4.0 - 2016-11-03 113 | 114 | ### Changed 115 | 116 | * The dereferencer now lazily resolves external references. 117 | * You can now use pointers when using the file loader, I.E. 'file://my-schema.json#/some/property'. 118 | 119 | ### Fixed 120 | 121 | * Fixed a bug where non string values passed to `format` would fail when they should pass. 122 | * Massive improvements to URI resolution. Now using sabre/uri (BSD-3) to resolve reference URIs. 123 | * Fixed a bug where the dereferencer would try to resolve ids that were not strings. 124 | 125 | ## 0.3.3 - 2016-08-22 126 | 127 | ### Fixed 128 | 129 | * Fixed a bug that caused a Segmentation fault on a system where mbstring and intl extensions were missing by @msarca 130 | * Avoid PHP notice on empty integer fields by @gbirke 131 | * Fixed a bug where properties with the name `$ref` were considered a reference by @ribeiropaulor 132 | * The dereferencer was fixed to resolve relative references when the parent schema does not have an `id`. 133 | * Fixed a bug where absolute references in nested IDs were appended to the current resolution scope instead of replacing it. 134 | 135 | ### Added 136 | 137 | * It is now possible to pass a path with a reference fragment to the dereferencer. 138 | * Added the dependency constraint to the dependencies error. 139 | 140 | ## 0.3.2 - 2016-07-26 141 | 142 | ### Fixed 143 | 144 | * the type : integer constraint now passes for valid negative integers that are larger than PHP_INT_MAX, and does not pass for numeric strings that are not larger than PHP_INT_MAX. 145 | * The date-time format constraint was fixed to only pass if the date is RFC3339 instead of all of ISO 8601. 146 | * The uri format constraint now passes for valid protocol relative URIs. 147 | * Fixed a bug where custom format extensions only worked for the first level of data and were not used for nested objects. 148 | * Minimum and Maximum comparisons will now work for numbers larger than PHP_INT_MAX if ext-bcmath is installed. 149 | * Fixed a bug where a custom ruleset was not being used past the first level of data in a nested object. 150 | 151 | ### Added 152 | 153 | A Comparator class was added so that the rest of the code doesn't have to constantly check if bccomp is avaiable. You can specify the precision to use for comparisons by calling Comparator::setScale(). 154 | 155 | ### Changed 156 | 157 | The validator now passes version 1.2.0 of the official test suite. 158 | 159 | ### Removed 160 | 161 | * Setters used when creating sub-schema validators were removed, since they are not necessary. These were marked @internal so this should not be a breaking change. 162 | 163 | ## 0.3.1 - 2016-06-28 164 | 165 | ### Fixed 166 | 167 | * The required constraint was not checking the type of the data. It now correctly ignores the data if it isn't an object. A fix was also added to the official JSON Schema Test Suite. 168 | 169 | ## 0.3.0 - 2016-05-11 170 | 171 | ### Fixed 172 | 173 | * The type constraint was failing for string checks if the bcmath extension wasn't installed. It now passes if the value is a string and bcmath isn't installed. 174 | * Pointers are now escaped for error messages. 175 | * The JSON Pointer now properly handles setting a new element in arrays. 176 | * Fixed a bug where the DraftFour rule set was not throwing an exception when trying to get a missing rule. 177 | 178 | ### Changed 179 | 180 | * The `ErrorCode` constants class is now marked as final. 181 | * All function names are now snake cased. 182 | 183 | ## 0.2.1 - 2016-05-08 184 | 185 | ### Changed 186 | 187 | * The loaders now `json_decode` with the option `JSON_BIGINT_AS_STRING` by default. This allows validating numbers larger than `PHP_INT_MAX` properly. 188 | 189 | ### Fixed 190 | 191 | * The dereferencer wasn't resolving references nested under properties that contained a slash character. The JSON Pointer used internally is now escaped so that properties containing a slash character will dereference properly. 192 | 193 | ## 0.2.0 - 2016-05-04 194 | 195 | ### Added 196 | 197 | * Added an ArrayLoader and ChainableLoader. The ChainableLoader was added to allow using multiple loaders for the same prefix. The ArrayLoader is mostly for testing, when you want to return paths from an in memory array. 198 | 199 | ### Changed 200 | 201 | * Only validate once when `passes`, `fails`, or `errors` is called instead of re-validating for each call. This was causing a huge performance hit when validating nested schemas. The properties constraint made two calls which resulted in an exponential slow down for each level of nesting. 202 | * The `Ruleset` interface now requires throwing a `ConstraintNotFoundException` instead of returning null for missing constraints. 203 | * Moved to the League namespace. 204 | * The curl extension is now explicitly required for dev installs, since you need it to test the `CurlWebLoader`. 205 | * The default max depth was increased to 50. The validator can validate to that depth really quickly and it's high enough that it won't be reached with normal usage. 206 | * The tests were switched to load json-schema.org urls from memory since their site went down and the build started failing. 207 | --------------------------------------------------------------------------------