├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── docs ├── doc.md ├── document.md ├── formatter.md ├── loader.md ├── schema.md └── tokenizer.md └── src ├── BaseDocument.php ├── Document.php ├── Formatter.php ├── Helpers ├── Error.php ├── Finder.php ├── Format │ ├── BaseFormat.php │ ├── Copier.php │ ├── Orderer.php │ └── Pruner.php ├── Patch │ ├── Builder.php │ └── Target.php ├── Patcher.php └── Utils.php ├── Loader.php ├── Schema ├── Cache.php ├── Comparer.php ├── Constraint │ ├── ArrayConstraint.php │ ├── BaseConstraint.php │ ├── CommonConstraint.php │ ├── ConstraintInterface.php │ ├── EnumConstraint.php │ ├── FormatChecker.php │ ├── ItemsConstraint.php │ ├── Manager.php │ ├── MaxMinConstraint.php │ ├── NumberConstraint.php │ ├── ObjectConstraint.php │ ├── OfConstraint.php │ ├── PropertiesConstraint.php │ ├── SpecificConstraint.php │ ├── StringConstraint.php │ └── TypeConstraint.php ├── DataChecker.php ├── JsonTypes.php ├── Resolver.php ├── Store.php ├── ValidationException.php └── Validator.php └── Tokenizer.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [2.1.0] - 2023-04-17 4 | * BC Break: `document->toJson()` can now return null on failure 5 | * Fixed: JSON encode exceptions are now caught and the error captured 6 | * Fixed: Invalid schema exceptions are now caught and the error captured 7 | * Fixed: Issue when adding a new schema and not resetting the validator 8 | 9 | ## [2.0.0] - 2023-04-10 10 | * Added: Major refactor for PHP 7.4 upwards 11 | 12 | ## [1.1.0] - 2016-01-05 13 | * Updated test version to php7 and fixed failing json-decoding test 14 | * Updated structure to use PSR4 autoloader 15 | 16 | ## [1.0.2] = 2016-01-04 17 | * Updated test versions to PHP 5.6 and HHVM 18 | * Multiple code-style fixes 19 | * Fixed bug validating against arbitrarily large integers 20 | ([PR2](https://github.com/johnstevenson/json-works/pull/2)). 21 | Thanks [aoberoi](https://github.com/aoberoi) 22 | * Improved string handling in Document load functions and added tests 23 | 24 | ## [1.0.1] - 2013-04-25 25 | * Fixed newline/format bugs in Utils:dataToJson and added tests 26 | * Added JohnStevenson\JsonWorks\Schema\ValidationException 27 | 28 | ## [1.0.0] - 2013-04-22 29 | 30 | * Initial stable release. 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2023 John Stevenson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Json-Works 2 | 3 | 4 | [![packagist](https://img.shields.io/packagist/v/johnstevenson/json-works)](https://packagist.org/packages/johnstevenson/json-works) 5 | [![Continuous Integration](https://github.com/johnstevenson/json-works/actions/workflows/continuous-integration.yml/badge.svg?branch=main)](https://github.com/johnstevenson/json-works/actions?query=branch:main) 6 | ![license](https://img.shields.io/github/license/johnstevenson/json-works.svg) 7 | ![php](https://img.shields.io/packagist/php-v/johnstevenson/json-works?colorB=8892BF) 8 | 9 | A PHP library to create, edit, query and validate [JSON][json]. 10 | 11 | ## Installation 12 | 13 | Install the latest version with: 14 | 15 | ```bash 16 | $ composer require johnstevenson/json-works 17 | ``` 18 | 19 | ## Requirements 20 | 21 | * PHP 7.4 minimum, although using the latest PHP version is highly recommended. 22 | 23 | ## Usage 24 | 25 | The library is intended to be used with complex json structures, or with json data that needs 26 | validation. Full usage information is available in the [documentation](docs/doc.md). 27 | 28 | * [Overview](#overview) 29 | * [Validation](#validation) 30 | 31 | ### Overview 32 | Json-Works allows you to create, edit and query json data using [JSON Pointer][pointer] syntax. For 33 | example: 34 | 35 | ```php 36 | addValue('/path/to/array/-', ['firstName'=> 'Fred', 'lastName' => 'Blogg']); 40 | 41 | // prettyPrint 42 | $json = $document->toJson(true); 43 | ``` 44 | 45 | which will give you the following json: 46 | 47 | ```json 48 | { 49 | "path": { 50 | "to": { 51 | "array": [ 52 | { 53 | "firstName": "Fred", 54 | "lastName": "Blogg" 55 | } 56 | ] 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | You can query this value by calling: 63 | 64 | ```php 65 | $person = $document->getValue('/path/to/array/0'); 66 | ``` 67 | 68 | and update it with: 69 | 70 | ```php 71 | $document->addValue('/path/to/array/0/lastName', 'Bloggs'); 72 | ``` 73 | and move it with: 74 | 75 | ```php 76 | $document->moveValue('/path/to/array/0', '/users/-'); 77 | 78 | $document->tidy(); 79 | $json = $document->toJson(true); 80 | ``` 81 | 82 | to end up with: 83 | 84 | ```json 85 | { 86 | "users": [ 87 | { 88 | "firstName": "Fred", 89 | "lastName": "Bloggs" 90 | } 91 | ] 92 | } 93 | ``` 94 | 95 | then delete it with: 96 | 97 | ```php 98 | $document->deleteValue('/users/0'); 99 | ``` 100 | 101 | ### Validation 102 | 103 | Json-Works includes an implementation of [JSON Schema][schema], version 4, which allows you to 104 | validate json data. If the document contains invalid or missing value data, the validation will fail 105 | with the error in `$document->getError()`. 106 | 107 | ```php 108 | $document = new JohnStevenson\JsonWorks\Document(); 109 | 110 | $document->loadData('path/to/data.json'); 111 | $document->loadScheme('path/to/schema.json'); 112 | 113 | if (!$document->validate()) { 114 | $error = $document->getError(); 115 | } 116 | ``` 117 | 118 | You can also validate data whilst building a document. The following example schema describes an 119 | array containing objects whose properties are all required and whose types are defined. 120 | 121 | ```jsonc 122 | // schemas can be very simple 123 | { 124 | "items": { 125 | "properties": { 126 | "firstName": { "type": "string" }, 127 | "lastName": { "type": "string" } 128 | }, 129 | "required": [ "firstName", "lastName" ] 130 | } 131 | } 132 | ``` 133 | 134 | Now you can check if your data is valid: 135 | 136 | ```php 137 | $document->loadSchema($schema); 138 | $document->addValue('/-', ['firstName'=> 'Fred']); 139 | 140 | if (!$document->validate()) { 141 | $error = $document->getError(); 142 | # "Property: '/0'. Error: is missing required property 'lastName'" 143 | } 144 | ``` 145 | 146 | Without a schema, any value can be added anywhere. 147 | 148 | ## License 149 | 150 | Json-Works is licensed under the MIT License - see the `LICENSE` file for details. 151 | 152 | [json]: https://www.rfc-editor.org/rfc/rfc8259 153 | [pointer]: https://www.rfc-editor.org/rfc/rfc6901 154 | [schema]: https://json-schema.org/ 155 | [composer]: https://getcomposer.org 156 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "johnstevenson/json-works", 3 | "description": "Create, edit, query and validate json", 4 | "keywords": ["json", "schema", "validator", "builder"], 5 | "homepage": "http://github.com/johnstevenson/json-works", 6 | "type": "library", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "John Stevenson", 11 | "email": "john-stevenson@blueyonder.co.uk" 12 | } 13 | ], 14 | "require": { 15 | "php": "^7.4 || ^8.0", 16 | "composer/pcre": "^3.1" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "JohnStevenson\\JsonWorks\\": "src/" 21 | } 22 | }, 23 | "autoload-dev": { 24 | "psr-4": { 25 | "JsonWorks\\Tests\\": "tests/" 26 | } 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^9.6.5", 30 | "phpstan/phpstan": "^1.0", 31 | "phpstan/phpstan-strict-rules": "^1.1" 32 | }, 33 | "scripts": { 34 | "test": "@php vendor/bin/phpunit", 35 | "phpstan": "@php vendor/bin/phpstan analyse" 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-main": "2.2-dev" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /docs/doc.md: -------------------------------------------------------------------------------- 1 | # Json-Works Documentation 2 | 3 | Json-Works is a PHP library that allows you to create, edit, query and validate json data. The main 4 | foundation is the [Document class](document.md) and this section contains most of the information 5 | you need to start using the library. 6 | 7 | * **[Document class](document.md)** 8 | * **[Formatter class](formatter.md)** 9 | * **[Loader class](formatter.md)** 10 | * **[Tokenizer class](formatter.md)** 11 | * **[Schema](schema.md)** 12 | 13 | -------------------------------------------------------------------------------- /docs/document.md: -------------------------------------------------------------------------------- 1 | # Document 2 | 3 | This class is the foundation of Json-Works, allowing you to load, create, query, edit, validate and 4 | output json data. 5 | 6 | ```php 7 | firstName = 'Fred'; 53 | $data->lastName = 'Bloggs'; 54 | 55 | # or using an array ... 56 | $data = ['firstName' => 'Fred', 'lastName' => 'Bloggs']; 57 | 58 | # or using a class 59 | $data = new PersonClass('Fred', 'Bloggs'); 60 | 61 | # Add the data to an existing document ... 62 | $document->addValue($path, $data); 63 | 64 | # or load it into a new document 65 | $document->loadData($data); 66 | 67 | # $document->getData() returns an unreferenced stdClass object 68 | ``` 69 | 70 | ## Json Build and Query 71 | These methods all take at least one *$path* parameter (see 72 | [Paths And Pointers](#paths-and-pointers)) which can either be an array or a string. 73 | 74 | * if it is an array, all items are treated as un-encoded and will be built into a single encoded 75 | path. 76 | * if it is a string, the value is treated as a single path that has already been encoded. 77 | 78 | By using an array you can leave the encoding to Json-Works. In the example below we want to 79 | reference a nested element named `with/slash`, which obviously needs encoding. 80 | 81 | ```php 82 | addValue($path, ...); 91 | ``` 92 | 93 | All methods, except *getValue()*, return a bool to indicate if they were successful. In failure 94 | cases the error is in `$document->getError()`. 95 | 96 | * [addValue()](#addvalue) 97 | * [copyValue()](#copyvalue) 98 | * [deleteValue()](#deletevalue) 99 | * [getValue()](#getvalue) 100 | * [hasValue()](#hasvalue) 101 | * [moveValue()](#movevalue) 102 | 103 | 104 | ### addValue 105 | bool **addValue** ( mixed `$path`, mixed `$value` ) 106 | 107 | Returns true if the value is added to *$path*. If *$path* does not exists, Json-Works will try and 108 | create it, returning false if it is unable to do so with the error in `$document->getError()`. 109 | 110 | If you wish to create objects that have array-like keys (digits or `-`) then you must create the 111 | base object first (or it must already exist) otherwise an array will be created or an error will 112 | occur. 113 | 114 | ```php 115 | addValue('prop1/0', 'myValue'); 117 | # creates an array at prop1: [ "myValue" ] 118 | 119 | $document->addValue('prop1', new \stdClass()); 120 | $document->addValue('prop1/0', 'myValue'); 121 | # creates an object at prop1: { "0": "myValue" } 122 | ``` 123 | 124 | ### copyValue 125 | bool **copyValue** ( mixed `$fromPath`, mixed `$toPath` ) 126 | 127 | Returns true if the value at *$fromPath* is copied to *$toPath*. 128 | 129 | ### deleteValue 130 | bool **deleteValue** ( mixed `$path` ) 131 | 132 | Returns true if the value at *$path* is deleted. 133 | 134 | ### getValue 135 | mixed **getValue** ( mixed `$path`, [ mixed `$default` = null ] ) 136 | 137 | Returns the value found at *$path*, or *$default*. 138 | 139 | ### hasValue 140 | bool **hasValue** ( mixed `$path`, mixed `&$value` ) 141 | 142 | Returns true if a value is found at *$path*, placing it in *$value*. Note that *$value* will be null 143 | if the function returns false. 144 | 145 | ### moveValue 146 | bool **moveValue** ( mixed `$fromPath`, mixed `$toPath` ) 147 | 148 | Returns true if the value at *$fromPath* is firstly copied to *$toPath*, then deleted from 149 | *$fromPath*. 150 | 151 | *Back to:* [Contents](#contents) 152 | 153 | ## Json Format 154 | These methods format the document data. Json-Works does not call them automatically so their usage 155 | is left to the discretion of the implementation. The [Formatter class][formatter] is used 156 | internally. 157 | 158 | * [tidy()](#tidy) 159 | * [toJson()](#tojson) 160 | 161 | ### tidy 162 | void **tidy** ( [ bool `$order` = false ] ) 163 | 164 | Removes empty objects and arrays from the data. If *$order* is true and a schema has been loaded the 165 | function re-orders the data using the schema content. 166 | 167 | ### toJson 168 | ?string **toJson** ( bool `$pretty` ) 169 | 170 | Returns a json-encoded string of `$document->data`. If *$pretty* is true, the output will be 171 | pretty-printed. Returns null on failure with the error in `$document->getError()`. 172 | 173 | *Back to:* [Contents](#contents) 174 | 175 | ## Load Data 176 | These methods load either json data or a json schema. The [Loader class][loader] is used internally. 177 | Input can either be: 178 | 179 | * a json string, requiring a successful call to _json_decode_. 180 | * a filename, requiring a successful call to _file_get_contents_ then _json_decode_. 181 | * a PHP data type specific to the method. 182 | 183 | * [loadData()](#loaddata) 184 | * [loadSchema()](#loadschema) 185 | 186 | ### loadData 187 | void **loadData** ( mixed `$data` ) 188 | 189 | Accepts most PHP data types because a JSON text is not technically restricted to objects and arrays. 190 | On success the *$data* is copied and stored internally. It is accessible using 191 | `$document->getData()`. Throws a *RuntimeException* if the data is a resource. 192 | 193 | ### loadSchema 194 | void **loadSchema** ( mixed `$schema` ) 195 | 196 | Throws a `RuntimeException` if *$schema* does not result in a PHP object when processed. 197 | 198 | *Back to:* [Contents](#contents) 199 | 200 | ## Schema Validation 201 | Json-Works provides an implementation of JSON Schema version 4. Please read [Schema][schema] for 202 | more details. 203 | 204 | ### validate 205 | bool **validate** () 206 | 207 | Returns the result of validating the data against the loaded schema. If false is returned the error 208 | will be in `$document->getError()`. If no schema has been loaded this method will always return 209 | true. 210 | 211 | 212 | ```php 213 | $document->loadData('path/to/data.json'); 214 | $document->loadScheme('path/to/schema.json'); 215 | 216 | if (!$document->validate()) { 217 | $error = $document->getError(); 218 | } 219 | ``` 220 | 221 | *Back to:* [Contents](#contents) 222 | 223 | [pointer]: https://www.rfc-editor.org/rfc/rfc6901 224 | [formatter]: formatter.md 225 | [loader]: loader.md 226 | [schema]: schema.md 227 | [tokenizer]: tokenizer.md 228 | -------------------------------------------------------------------------------- /docs/formatter.md: -------------------------------------------------------------------------------- 1 | # Formatter 2 | 3 | This class provides methods to manipulate array, object or json data. 4 | 5 | ```php 6 | 'Fred', 'lastName' => 'Bloggs']; 27 | 28 | $result = $formatter->copy($data); 29 | 30 | $fred = $result->firstName; 31 | $bloggs = $result->lastName; 32 | ``` 33 | 34 | ### order 35 | mixed **order** ( mixed `$data`, stdClass `$schema` ) 36 | 37 | Returns an unreferenced copy of *$data*, with object properties re-ordered using the order found in 38 | *$schema*. This is illustrated in the example below, which uses json-notation for PHP objects. 39 | 40 | ```php 41 | order($data, $schema); 60 | 61 | # ordered $data 62 | { 63 | "prop1": "value 1", 64 | "prop2": {}, 65 | "prop3": "value 3" 66 | } 67 | ``` 68 | 69 | Note that the ordering is fairly simplistic. Only the *properties* and *items* keywords are searched 70 | in the schema, and only the property names listed are ordered. This means that property names 71 | appearing within an *anyOf* schema, for example, will not be discovered or ordered. Any property 72 | names not discovered or listed in the schema will be positioned after any ordered elements. 73 | 74 | ### prune 75 | mixed **prune** ( mixed `$data` ) 76 | 77 | Returns an unreferenced copy of *$data*, having removed any empty object properties or arrays. This 78 | is illustrated in the example below, which uses json-notation for PHP objects. 79 | 80 | ```php 81 | prune($data); 92 | 93 | # pruned $data 94 | { 95 | "prop1": "value 1", 96 | "prop3": "value 3", 97 | "prop5": 5 98 | } 99 | ``` 100 | 101 | ### toJson 102 | string **toJson** ( mixed `$data`, int `$options` ) 103 | 104 | Returns a json-encoded string of *$data*. Throws a *RuntimeException* on failure. 105 | -------------------------------------------------------------------------------- /docs/loader.md: -------------------------------------------------------------------------------- 1 | # Loader 2 | 3 | This class provides methods for processing input data. 4 | 5 | 6 | ```php 7 | add('', 'keyname'); 29 | # /keyname 30 | 31 | $result = $tokenizer->add('/prop1', 'name/with/slash'); 32 | # /prop1/name~1with~1slash 33 | 34 | $result = $tokenizer->add('/prop1', 'name~with~tilde'); 35 | # /prop1/name~0with~0tilde 36 | 37 | $result = $tokenizer->add('/prop1/prop2', ''); 38 | # /prop1/prop2/ (this represents an empty key property of prop2) 39 | ``` 40 | 41 | ### decode 42 | array **decode** ( string `$path` ) 43 | 44 | Returns an array of decoded elements from an encoded JSON Pointer *$path*. Each element is decoded 45 | by replacing all `~1` sequences with a forward-slash, then replacing all `~0` sequences with a 46 | tilde. 47 | 48 | ```php 49 | decode('/keyname'); 51 | # ['keyname'] 52 | 53 | $result = $tokenizer->decode('/prop1/name~1with~1slash'); 54 | # ['prop1', 'name/with/slash'] 55 | 56 | $result = $tokenizer->decode('/prop1/name~0with~0tilde'); 57 | # ['prop1', 'name~with~tilde'] 58 | 59 | $result = $tokenizer->decode('/prop1/prop2/'); 60 | # ['prop1', 'prop2', ''] 61 | ``` 62 | 63 | ### encode 64 | string **encode** ( string | array `$path` ) 65 | 66 | Returns an encoded JSON Pointer from *$path*, which must either be a single string element, or an 67 | array of path elements. Uses [add()](#add) internally. 68 | 69 | ```php 70 | encode('keyname'); 72 | # /keyname 73 | 74 | $result = $tokenizer->encode(['prop1', 'prop2', 'name/with/slash']); 75 | # /prop1/prop2/name~1with~1slash 76 | ``` 77 | 78 | ### encodeKey 79 | string **encodeKey** ( string `$key` ) 80 | 81 | Encodes and returns *$key*. All tilde characters are replaced with `~0`, then all forward-slashes 82 | are replaced with `~1`. 83 | 84 | ```php 85 | encodeKey('keyname'); 87 | # keyname 88 | 89 | $result = $tokenizer->encodeKey('name/with/slash'); 90 | # name~1with~1slash 91 | 92 | $result = $tokenizer->encodeKey('name~with~tilde'); 93 | # name~0with~0tilde 94 | ``` 95 | 96 | [pointer]: https://www.rfc-editor.org/rfc/rfc6901 97 | -------------------------------------------------------------------------------- /src/BaseDocument.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Schema\Validator; 17 | 18 | /** 19 | * A class for loading, formatting and validating json data. * 20 | * @api 21 | */ 22 | class BaseDocument 23 | { 24 | /** @var mixed */ 25 | protected $data; 26 | protected string $error = ''; 27 | protected ?stdClass $schema = null; 28 | 29 | protected Formatter $formatter; 30 | protected ?Validator $validator = null; 31 | 32 | public function __construct() 33 | { 34 | $this->formatter = new Formatter(); 35 | } 36 | 37 | /** 38 | * @param mixed $data 39 | */ 40 | public function loadData($data): void 41 | { 42 | $loader = new Loader(); 43 | $this->data = $loader->getData($data); 44 | } 45 | 46 | /** 47 | * @param mixed $data 48 | */ 49 | public function loadSchema($data): void 50 | { 51 | $loader = new Loader(); 52 | $this->schema = $loader->getSchema($data); 53 | $this->validator = null; 54 | } 55 | 56 | public function tidy(bool $order = false): void 57 | { 58 | $this->data = $this->formatter->prune($this->data); 59 | 60 | if ($order && $this->schema !== null) { 61 | $this->data = $this->formatter->order($this->data, $this->schema); 62 | } 63 | } 64 | 65 | /** 66 | * @return mixed $data 67 | */ 68 | public function getData() 69 | { 70 | return $this->data; 71 | } 72 | 73 | public function getError(): string 74 | { 75 | return $this->error; 76 | } 77 | 78 | public function toJson(bool $pretty): ?string 79 | { 80 | $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; 81 | $options |= $pretty ? JSON_PRETTY_PRINT : 0; 82 | 83 | try { 84 | $result = $this->formatter->toJson($this->data, $options); 85 | } catch (\RuntimeException $e) { 86 | $result = null; 87 | $this->error = $e->getMessage(); 88 | } 89 | 90 | return $result; 91 | } 92 | 93 | public function validate(): bool 94 | { 95 | if ($this->schema === null) { 96 | return true; 97 | } 98 | 99 | if ($this->validator === null) { 100 | $this->validator = new Validator($this->schema); 101 | } 102 | 103 | try { 104 | if (!$result = $this->validator->check($this->data)) { 105 | $this->error = $this->validator->getLastError(); 106 | } 107 | } catch (\RuntimeException $e) { 108 | $result = false; 109 | $this->error = $e->getMessage(); 110 | } 111 | 112 | return $result; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Document.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks; 13 | 14 | use JohnStevenson\JsonWorks\Helpers\Patcher; 15 | use JohnStevenson\JsonWorks\Helpers\Finder; 16 | 17 | 18 | /** 19 | * A class for Querying and manipulating json data. * 20 | * @api 21 | */ 22 | class Document extends BaseDocument 23 | { 24 | private Finder $finder; 25 | 26 | public function __construct() 27 | { 28 | parent::__construct(); 29 | $this->finder = new Finder(); 30 | } 31 | 32 | /** 33 | * Adds an element to the data 34 | * 35 | * @param string|array $path 36 | * @param mixed $value 37 | */ 38 | public function addValue($path, $value): bool 39 | { 40 | $this->error = ''; 41 | $path = $this->getPath($path, '$path'); 42 | $value = $this->formatter->copy($value); 43 | $patcher = new Patcher(); 44 | 45 | if (!$result = $patcher->add($this->data, $path, $value)) { 46 | $this->error = $patcher->getError(); 47 | } 48 | 49 | return $result; 50 | } 51 | 52 | /** 53 | * @param string|array $fromPath 54 | * @param string|array $toPath 55 | */ 56 | public function copyValue($fromPath, $toPath): bool 57 | { 58 | $fromPath = $this->getPath($fromPath, '$fromPath'); 59 | $toPath = $this->getPath($toPath, '$toPath'); 60 | 61 | return $this->doMove($fromPath, $toPath, false); 62 | } 63 | 64 | /** 65 | * @param string|array $path 66 | */ 67 | public function deleteValue($path): bool 68 | { 69 | $path = $this->getPath($path, '$path'); 70 | $patcher = new Patcher(); 71 | 72 | if (!$result = $patcher->remove($this->data, $path)) { 73 | $this->error = $patcher->getError(); 74 | } 75 | 76 | return $result; 77 | } 78 | 79 | /** 80 | * @param string|array $path 81 | * @param mixed $default 82 | * @return mixed 83 | */ 84 | public function getValue($path, $default = null) 85 | { 86 | $path = $this->getPath($path, '$path'); 87 | 88 | if (!$this->hasValue($path, $value)) { 89 | $value = $default; 90 | } 91 | 92 | $this->error = ''; 93 | 94 | return $value; 95 | } 96 | 97 | /** 98 | * @param string|array $path 99 | * @param mixed $value 100 | */ 101 | public function hasValue($path, &$value): bool 102 | { 103 | $path = $this->getPath($path, '$path'); 104 | $value = null; 105 | $this->error = $error = ''; 106 | 107 | if ($result = $this->finder->find($path, $this->data, $element, $error)) { 108 | $value = $this->formatter->copy($element); 109 | } else { 110 | $this->error = $error; 111 | } 112 | 113 | return $result; 114 | } 115 | 116 | /** 117 | * @param string|array $fromPath 118 | * @param string|array $toPath 119 | */ 120 | public function moveValue($fromPath, $toPath): bool 121 | { 122 | $fromPath = $this->getPath($fromPath, '$fromPath'); 123 | $toPath = $this->getPath($toPath, '$toPath'); 124 | 125 | return $this->doMove($fromPath, $toPath, true); 126 | } 127 | 128 | private function doMove(string $fromPath, string $toPath, bool $delete): bool 129 | { 130 | $result = false; 131 | $this->error = ''; 132 | 133 | if ($this->hasValue($fromPath, $value)) { 134 | if ($result = $this->addValue($toPath, $value)) { 135 | if ($delete) { 136 | $this->deleteValue($fromPath); 137 | } 138 | } 139 | } 140 | 141 | return $result; 142 | } 143 | 144 | /** 145 | * @param string|array $value 146 | */ 147 | private function getPath($value, string $varName): string 148 | { 149 | if (is_string($value)) { 150 | return $value; 151 | } 152 | 153 | $tokenizer = new Tokenizer(); 154 | return $tokenizer->encode($value); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/Formatter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Format\Copier; 17 | use JohnStevenson\JsonWorks\Helpers\Format\Orderer; 18 | use JohnStevenson\JsonWorks\Helpers\Format\Pruner; 19 | use JohnStevenson\JsonWorks\Helpers\Utils; 20 | 21 | /** 22 | * A class for manipulating array, object or json data. 23 | * @api 24 | */ 25 | class Formatter 26 | { 27 | private ?Copier $copier = null; 28 | private ?Orderer $orderer = null; 29 | private ?Pruner $pruner = null; 30 | 31 | /** 32 | * Returns an unreferenced copy of the data 33 | * 34 | * @param mixed $data 35 | * @return mixed 36 | */ 37 | public function copy($data) 38 | { 39 | if ($this->copier === null) { 40 | $this->copier = new Copier(); 41 | } 42 | 43 | return $this->copier->run($data); 44 | } 45 | 46 | /** 47 | * Reorders object properties using the schema order 48 | * 49 | * @param mixed $data 50 | * @param stdClass|null $schema 51 | * @return mixed An unreferenced copy of the ordered data 52 | */ 53 | public function order($data, $schema) 54 | { 55 | if ($this->orderer === null) { 56 | $this->orderer = new Orderer(); 57 | } 58 | 59 | $data = $this->copy($data); 60 | 61 | return $this->orderer->run($data, $schema); 62 | } 63 | 64 | /** 65 | * Removes empty objects and arrays from the data 66 | * 67 | * @param mixed $data 68 | * @return mixed An unreferenced copy of the pruned data 69 | */ 70 | public function prune($data) 71 | { 72 | if ($this->pruner === null) { 73 | $this->pruner = new Pruner(); 74 | } 75 | 76 | return $this->pruner->run($data); 77 | } 78 | 79 | /** 80 | * @param mixed $data 81 | */ 82 | public function toJson($data, int $options): string 83 | { 84 | return Utils::jsonEncode($data, $options); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Helpers/Error.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers; 13 | 14 | use JohnStevenson\JsonWorks\Helpers\Error; 15 | 16 | /** 17 | * A class for formatting and setting error messages 18 | */ 19 | class Error 20 | { 21 | public const ERR_NOT_FOUND = 'ERR_NOT_FOUND'; 22 | public const ERR_PATH_KEY = 'ERR_PATH_KEY'; 23 | public const ERR_BAD_INPUT = 'ERR_BAD_INPUT'; 24 | public const ERR_VALIDATE = 'ERR_VALIDATE'; 25 | 26 | /** 27 | * Returns a formatted error message 28 | */ 29 | public function get(string $code, string $msg): string 30 | { 31 | $caption = $this->codeGetCaption($code); 32 | 33 | if ($caption !== null) { 34 | $error = sprintf('%s: %s [%s]', $code, $caption, $msg); 35 | } else { 36 | $error = sprintf('%s: %s', $code, $msg); 37 | } 38 | 39 | return $error; 40 | } 41 | 42 | /** 43 | * Returns an error caption 44 | */ 45 | protected function codeGetCaption(string $code): ?string 46 | { 47 | $result = null; 48 | 49 | switch ($code) { 50 | case self::ERR_NOT_FOUND: 51 | $result = 'Unable to find resource'; 52 | break; 53 | case self::ERR_PATH_KEY: 54 | $result = 'Invalid path key'; 55 | break; 56 | case self::ERR_BAD_INPUT: 57 | $result = 'Invalid input'; 58 | break; 59 | } 60 | 61 | return $result; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Helpers/Finder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Error; 17 | use JohnStevenson\JsonWorks\Helpers\Patch\Target; 18 | 19 | /** 20 | * A class for finding a value using JSON Pointers 21 | */ 22 | class Finder 23 | { 24 | /** @var mixed */ 25 | protected $element; 26 | protected Target $target; 27 | 28 | /** 29 | * Returns true if an element is found 30 | * 31 | * @param mixed $data 32 | * @param mixed $element Set by method if found 33 | * @param string $error Set by method 34 | */ 35 | public function find(string $path, $data, &$element, &$error): bool 36 | { 37 | $target = new Target($path, $error); 38 | 39 | if ($result = $this->get($data, $target)) { 40 | $element = $target->element; 41 | } 42 | 43 | return $result; 44 | } 45 | 46 | /** 47 | * Returns true if an element is found 48 | * 49 | * @param mixed $data 50 | */ 51 | public function get(&$data, Target $target): bool 52 | { 53 | $this->element =& $data; 54 | $this->target = $target; 55 | 56 | if ($target->invalid) { 57 | return false; 58 | } 59 | 60 | $found = Utils::arrayIsEmpty($target->tokens); 61 | 62 | if (!$found) { 63 | $found = $this->search($target->tokens); 64 | } 65 | 66 | $target->setResult($found, $this->element); 67 | 68 | return $found; 69 | } 70 | 71 | /** 72 | * Returns true if the element is found 73 | * 74 | * @param array $tokens Modified by method 75 | */ 76 | protected function search(array &$tokens): bool 77 | { 78 | // tokens is guaranteed not empty 79 | while (Utils::arrayNotEmpty($tokens)) { 80 | $token = $tokens[0]; 81 | 82 | if (count($tokens) === 1) { 83 | $this->target->parent =& $this->element; 84 | $this->target->childKey = $token; 85 | } 86 | 87 | if (!$this->findContainer($token)) { 88 | return false; 89 | } 90 | 91 | $token = array_shift($tokens); 92 | if ($token === null) { 93 | break; 94 | } 95 | 96 | $this->target->setFoundPath($token); 97 | } 98 | 99 | return true; 100 | } 101 | 102 | /** 103 | * Returns true if a token is found at the current data root 104 | * 105 | * A reference to the value is placed in $this->element 106 | */ 107 | protected function findContainer(string $token): bool 108 | { 109 | $found = false; 110 | 111 | if (is_object($this->element)) { 112 | $found = $this->findObject($token); 113 | } elseif (is_array($this->element)) { 114 | 115 | if ($token !== '-') { 116 | $found = $this->findArray($token); 117 | } 118 | } 119 | 120 | return $found; 121 | } 122 | 123 | /** 124 | * Returns true if the token is an existing array key 125 | * 126 | * Sets $this->element to reference the value 127 | */ 128 | protected function findArray(string $token): bool 129 | { 130 | if (!$this->isArrayKey($token, $index)) { 131 | $this->target->setError(Error::ERR_PATH_KEY); 132 | return false; 133 | } 134 | 135 | if (is_array($this->element) && array_key_exists($index, $this->element)) { 136 | $this->element = &$this->element[$index]; 137 | return true; 138 | } 139 | 140 | return false; 141 | } 142 | 143 | /** 144 | * Returns true if the token is an existing object property key 145 | * 146 | * Sets $this->element to reference the value 147 | */ 148 | protected function findObject(string $token): bool 149 | { 150 | if ($this->element instanceof stdClass 151 | && property_exists($this->element, $token) 152 | ) { 153 | $this->element = &$this->element->$token; 154 | return true; 155 | } 156 | 157 | return false; 158 | } 159 | 160 | /** 161 | * Returns true if the token is a valid array key 162 | * 163 | * @param mixed $index Set to an integer on success 164 | */ 165 | protected function isArrayKey(string $token, &$index): bool 166 | { 167 | $result = Utils::isMatch('/^((0)|([1-9]\d*))$/', $token); 168 | 169 | if ($result) { 170 | $index = (int) $token; 171 | } 172 | 173 | return $result; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Helpers/Format/BaseFormat.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers\Format; 13 | 14 | /** 15 | * A base class to be extended by data formatters 16 | */ 17 | class BaseFormat 18 | { 19 | /** 20 | * Returns true if the data is an object or array 21 | * 22 | * The $asObject param is set and reports if $data is either an object or an 23 | * associative array 24 | * 25 | * @param object|array|mixed $data The data to check 26 | */ 27 | protected function isContainer($data, ?bool &$asObject): bool 28 | { 29 | if ($asObject = is_object($data)) { 30 | return true; 31 | } 32 | 33 | if (is_array($data)) { 34 | $asObject = $this->isAssociative($data); 35 | return true; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | /** 42 | * Determines if an array is associative 43 | * 44 | * @param array $data 45 | */ 46 | protected function isAssociative(array $data): bool 47 | { 48 | if (function_exists('array_is_list')) { 49 | return !array_is_list($data); 50 | } 51 | 52 | foreach ($data as $key => $value) { 53 | if ($key !== (int) $key) { 54 | return true; 55 | } 56 | } 57 | 58 | return false; 59 | } 60 | 61 | /** 62 | * Casts a value as an object if required 63 | * 64 | * @param object|array $data 65 | * @return object|array 66 | */ 67 | protected function formatContainer($data, bool $asObject) 68 | { 69 | return $asObject ? (object) $data: $data; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Helpers/Format/Copier.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers\Format; 13 | 14 | /** 15 | * A class to return an unreferenced copy of the data, with an optional callback 16 | */ 17 | class Copier extends BaseFormat 18 | { 19 | /** 20 | * Returns an unreferenced copy of the data 21 | * 22 | * @internal 23 | * @param object|array|mixed $data 24 | * @return object|array|mixed 25 | */ 26 | public function run($data) 27 | { 28 | $isObject = null; 29 | 30 | if ($this->isContainer($data, $asObject)) { 31 | // for phpstan 32 | if (is_object($data) || is_array($data)) { 33 | return $this->copyContainer($data, $asObject); 34 | } 35 | } 36 | 37 | return $data; 38 | } 39 | 40 | /** 41 | * Recursively copies an object or array 42 | * 43 | * @param object|array $data The data to copy 44 | * @return object|array An unreferenced copy 45 | */ 46 | protected function copyContainer($data, bool $asObject) 47 | { 48 | $result = []; 49 | 50 | if (is_object($data)) { 51 | $data = get_object_vars($data); 52 | } 53 | 54 | foreach ($data as $key => $value) { 55 | $result[$key] = $this->run($value); 56 | } 57 | 58 | return $this->formatContainer($result, $asObject); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Helpers/Format/Orderer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers\Format; 13 | 14 | use \stdClass; 15 | 16 | /** 17 | * A class to order elements based on their order in a schema 18 | */ 19 | class Orderer 20 | { 21 | /** 22 | * Reorders object properties using the schema order 23 | * 24 | * @internal 25 | * @param mixed $data 26 | * @return mixed 27 | */ 28 | public function run($data, ?stdClass $schema) 29 | { 30 | $properties = $this->objectWithSchema($data, $schema); 31 | 32 | if ($properties !== null && ($data instanceof stdClass)) { 33 | return $this->orderObject($data, $properties); 34 | } 35 | 36 | $items = $this->arrayWithSchema($data, $schema); 37 | 38 | if ($items !== null && is_array($data)) { 39 | return $this->orderArray($data, $items); 40 | } 41 | 42 | return $data; 43 | } 44 | 45 | /** 46 | * Returns schema items if they are relevant and exist 47 | * 48 | * @param mixed $data 49 | */ 50 | protected function arrayWithSchema($data, ?stdClass $schema): ?stdClass 51 | { 52 | if (!is_array($data)) { 53 | return null; 54 | } 55 | 56 | $items = $this->getProperties($schema, 'items'); 57 | return $items ?? new stdClass(); 58 | } 59 | 60 | /** 61 | * Returns schema properties if they are relevant and exist 62 | * 63 | * @param mixed $data 64 | */ 65 | protected function objectWithSchema($data, ?stdClass $schema): ?stdClass 66 | { 67 | if (!is_object($data)) { 68 | return null; 69 | } 70 | 71 | $properties = $this->getProperties($schema, 'properties'); 72 | 73 | if ($properties === null) { 74 | $properties = $this->getPropertiesFromData($data); 75 | } 76 | 77 | return $properties; 78 | } 79 | 80 | /** 81 | * Returns schema properties if valid, otherwise null 82 | * 83 | */ 84 | protected function getProperties(?stdClass $schema, string $key): ?stdClass 85 | { 86 | if (!isset($schema->$key)) { 87 | return null; 88 | } 89 | 90 | return ($schema->$key instanceof stdClass) ? $schema->$key : null; 91 | } 92 | 93 | /** 94 | * Creates schema properties from ordered data properties 95 | * 96 | * @param object $data 97 | */ 98 | protected function getPropertiesFromData($data): stdClass 99 | { 100 | $result = new stdClass(); 101 | 102 | $keys = array_keys((array) $data); 103 | sort($keys, SORT_NATURAL | SORT_FLAG_CASE); 104 | 105 | foreach ($keys as $key) { 106 | $result->$key = new stdClass(); 107 | } 108 | 109 | return $result; 110 | } 111 | 112 | /** 113 | * Orders an array using the schema items properties 114 | * 115 | * @param array $data 116 | * @return array 117 | */ 118 | protected function orderArray(array $data, stdClass $schema): array 119 | { 120 | $result = []; 121 | 122 | foreach ($data as $item) { 123 | $result[] = $this->run($item, $schema); 124 | } 125 | 126 | return $result; 127 | } 128 | 129 | /** 130 | * Orders an object using the schema properties 131 | */ 132 | protected function orderObject(stdClass $data, stdClass $properties): stdClass 133 | { 134 | $result = []; 135 | 136 | /** @var stdClass $schema */ 137 | foreach (get_object_vars($properties) as $key => $schema) { 138 | if (property_exists($data, $key)) { 139 | $result[$key] = $this->run($data->$key, $schema); 140 | unset($data->$key); 141 | } 142 | } 143 | 144 | return (object) array_merge($result, (array) $data); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Helpers/Format/Pruner.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers\Format; 13 | 14 | use JohnStevenson\JsonWorks\Helpers\Utils; 15 | 16 | /** 17 | * A class to remove empty objects and arrays from data 18 | */ 19 | class Pruner extends BaseFormat 20 | { 21 | protected bool $keep; 22 | 23 | /** 24 | * Removes empty objects and arrays from the data 25 | * 26 | * @internal 27 | * @param mixed $data The data to prune 28 | * @return mixed An unreferenced copy of the pruned data 29 | */ 30 | public function run($data) 31 | { 32 | $this->keep = true; 33 | 34 | if ($this->isContainer($data, $asObject)) { 35 | $container = is_object($data) ? get_object_vars($data) : (array) $data; 36 | 37 | return $this->pruneContainer($container, $asObject); 38 | } 39 | 40 | return $data; 41 | } 42 | 43 | /** 44 | * Recursively removes empty objects and arrays from the container 45 | * 46 | * @param array $container The data container to prune 47 | * @return object|array An unreferenced copy of the pruned container 48 | */ 49 | protected function pruneContainer(array $container, bool $asObject) 50 | { 51 | $result = []; 52 | 53 | foreach ($container as $key => $value) { 54 | $value = $this->run($value); 55 | 56 | if ($this->keep) { 57 | $result[$key] = $value; 58 | } 59 | } 60 | 61 | $this->keep = Utils::arrayNotEmpty($result); 62 | 63 | return $this->formatContainer($result, $asObject); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Helpers/Patch/Builder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers\Patch; 13 | 14 | use \InvalidArgumentException; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Error; 17 | use JohnStevenson\JsonWorks\Helpers\Patch\Target; 18 | use JohnStevenson\JsonWorks\Helpers\Utils; 19 | 20 | class Builder 21 | { 22 | /** @var mixed */ 23 | protected $data; 24 | 25 | /** @var mixed */ 26 | protected $element; 27 | 28 | protected Target $target; 29 | 30 | /** 31 | * Returns a new element to be added to the data 32 | * 33 | * @param Target $target 34 | * @param mixed $value 35 | * @return mixed 36 | */ 37 | public function make(Target $target, $value) 38 | { 39 | $this->data = $target->element; 40 | $this->element =& $this->data; 41 | $this->target = $target; 42 | 43 | $this->initData(); 44 | 45 | if (Utils::arrayIsEmpty($target->tokens)) { 46 | $this->data = $value; 47 | } else { 48 | $this->processTokens($target->tokens, $value); 49 | } 50 | 51 | return $this->data; 52 | } 53 | 54 | /** 55 | * Initializes the data 56 | */ 57 | protected function initData(): void 58 | { 59 | if (Utils::arrayNotEmpty($this->target->tokens)) { 60 | 61 | $key = $this->target->tokens[0]; 62 | 63 | if (is_null($this->data)) { 64 | // 1st pass: creating a root container 65 | // 2nd pass: recursing from statement below 66 | $this->createContainer($key); 67 | } else { 68 | $this->setTarget($key); 69 | $this->data = null; 70 | array_shift($this->target->tokens); 71 | $this->initData(); 72 | } 73 | } 74 | } 75 | 76 | /** 77 | * Builds new elements from the remaining tokens 78 | * 79 | * @param array $tokens 80 | * @param mixed $value The value to add to the final element 81 | */ 82 | protected function processTokens(array $tokens, $value): void 83 | { 84 | while (true) { 85 | $key = array_shift($tokens); 86 | 87 | if ($key === null) { 88 | break; 89 | } 90 | 91 | if (Utils::arrayNotEmpty($tokens)) { 92 | $this->addElement($key, $tokens[0]); 93 | } else { 94 | // No tokens left so set the value 95 | $this->setValue($key, $value); 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * Creates a new object or array 102 | * 103 | * The new container is created on the current root element and its type 104 | * depends on the nature of the key. 105 | */ 106 | protected function createContainer(string $key): void 107 | { 108 | if ($key === '-' || $key === '0') { 109 | $this->element = []; 110 | } else { 111 | $this->element = new \stdClass(); 112 | } 113 | } 114 | 115 | /** 116 | * Adds a new container member to an object or array 117 | */ 118 | protected function addElement(string $key, string $containerKey): void 119 | { 120 | if (is_array($this->element)) { 121 | $this->element[0] = null; 122 | $this->element =& $this->element[0]; 123 | 124 | } else { 125 | // @phpstan-ignore-next-line 126 | $this->element->$key = null; 127 | // @phpstan-ignore-next-line 128 | $this->element =& $this->element->$key; 129 | } 130 | 131 | $this->createContainer($containerKey); 132 | } 133 | 134 | /** 135 | * Sets external target data for incorporating the data 136 | */ 137 | protected function setTarget(string $key): void 138 | { 139 | if (is_array($this->data)) { 140 | $this->checkArrayKey(count($this->data), $key, $index); 141 | $this->target->setArray($index); 142 | 143 | } else { 144 | $this->target->setObject($key); 145 | } 146 | } 147 | 148 | /** 149 | * Sets an array item or an object property 150 | * 151 | * @param mixed $value 152 | */ 153 | protected function setValue(string $key, $value): void 154 | { 155 | if (is_array($this->element)) { 156 | $this->checkArrayKey(count($this->element), $key, $index); 157 | // @phpstan-ignore-next-line 158 | $this->element[$index] = $value; 159 | } else { 160 | // @phpstan-ignore-next-line 161 | $this->element->$key = $value; 162 | } 163 | } 164 | 165 | /** 166 | * Checks if an array key is valid and sets its index 167 | * 168 | * @param integer|null $index Set by method 169 | * @throws InvalidArgumentException 170 | */ 171 | protected function checkArrayKey(int $itemCount, string $key, ?int &$index): void 172 | { 173 | $result = Utils::isMatch('/^(?:(-)|(0)|([1-9]\d*))$/', $key); 174 | 175 | if ($result) { 176 | if ($key === '-') { 177 | $index = $itemCount; 178 | } else { 179 | $index = (int) $key; 180 | $result = $index <= $itemCount; 181 | } 182 | } 183 | 184 | if (!$result) { 185 | $this->target->setError(Error::ERR_PATH_KEY); 186 | throw new InvalidArgumentException($this->target->error); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Helpers/Patch/Target.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers\Patch; 13 | 14 | use JohnStevenson\JsonWorks\Helpers\Error; 15 | use JohnStevenson\JsonWorks\Helpers\Utils; 16 | use JohnStevenson\JsonWorks\Tokenizer; 17 | 18 | /** 19 | * A class for holding various properties when searching for or building data 20 | */ 21 | class Target 22 | { 23 | const TYPE_VALUE = 0; 24 | const TYPE_OBJECT = 1; 25 | const TYPE_ARRAY = 2; 26 | 27 | /** @var string|integer */ 28 | public $key = ''; 29 | 30 | /** @var array */ 31 | public $tokens = []; 32 | 33 | /** @var mixed */ 34 | public $element; 35 | 36 | public string $foundPath = ''; 37 | 38 | /** @var mixed */ 39 | public $parent; 40 | 41 | public bool $invalid = false; 42 | public int $type = self::TYPE_VALUE; 43 | public string $path = ''; 44 | public string $childKey = ''; 45 | public string $error = ''; 46 | public Tokenizer $tokenizer; 47 | 48 | /** 49 | * Constructor 50 | * 51 | */ 52 | public function __construct(string $path, string &$error) 53 | { 54 | $this->path = $path; 55 | $this->error =& $error; 56 | $this->tokenizer = new Tokenizer(); 57 | 58 | if (!$this->tokenizer->decode($this->path, $this->tokens)) { 59 | $this->invalid = true; 60 | $this->setError(Error::ERR_PATH_KEY); 61 | } 62 | } 63 | 64 | /** 65 | * Sets type and key for an array 66 | * 67 | * @param string|integer $index 68 | */ 69 | public function setArray($index): void 70 | { 71 | $this->type = self::TYPE_ARRAY; 72 | $this->key = (int) $index; 73 | } 74 | 75 | /** 76 | * Sets type and key for an object 77 | */ 78 | public function setObject(string $key): void 79 | { 80 | $this->type = self::TYPE_OBJECT; 81 | $this->key = $key; 82 | } 83 | 84 | /** 85 | * Sets or clears an error message 86 | */ 87 | public function setError(?string $code): void 88 | { 89 | $this->error = ''; 90 | 91 | if ($code !== null) { 92 | $error = new Error(); 93 | $this->error = $error->get($code, $this->path); 94 | $this->invalid = $code === Error::ERR_PATH_KEY; 95 | } 96 | } 97 | 98 | /** 99 | * Add a token to the found path 100 | */ 101 | public function setFoundPath(string $token): void 102 | { 103 | $this->foundPath = $this->tokenizer->add($this->foundPath, $token); 104 | } 105 | 106 | /** 107 | * Sets element and error if not already set 108 | * 109 | * @param mixed $element 110 | */ 111 | public function setResult(bool $found, &$element): void 112 | { 113 | $this->element =& $element; 114 | 115 | if (!$found && Utils::stringIsEmpty($this->error)) { 116 | $this->setError(Error::ERR_NOT_FOUND); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Helpers/Patcher.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers; 13 | 14 | use InvalidArgumentException; 15 | use JohnStevenson\JsonWorks\Helpers\Finder; 16 | use JohnStevenson\JsonWorks\Helpers\Patch\Builder; 17 | use JohnStevenson\JsonWorks\Helpers\Patch\Target; 18 | use JohnStevenson\JsonWorks\Helpers\Utils; 19 | 20 | /** 21 | * A class for building json 22 | */ 23 | class Patcher 24 | { 25 | protected string $error = ''; 26 | protected bool $jsonPatch; 27 | protected Builder $builder; 28 | protected Finder $finder; 29 | 30 | /** 31 | * Constructor 32 | * 33 | * If jsonPatch is set, elements will only be added to the root or an 34 | * existing element. See RFC 6902 (http://tools.ietf.org/html/rfc6902) 35 | */ 36 | public function __construct(bool $jsonPatch = false) 37 | { 38 | $this->jsonPatch = $jsonPatch; 39 | $this->builder = new Builder(); 40 | $this->finder = new Finder(); 41 | } 42 | 43 | /** 44 | * Adds an element to the data 45 | * 46 | * @param mixed $data 47 | * @param mixed $value 48 | */ 49 | public function add(&$data, string $path, $value): bool 50 | { 51 | if ($result = $this->getElement($data, $path, $value, $target)) { 52 | $result = $this->addData($target, $value); 53 | } 54 | 55 | return $result; 56 | } 57 | 58 | /** 59 | * Removes an element if found 60 | * 61 | * @param mixed $data 62 | */ 63 | public function remove(&$data, string $path): bool 64 | { 65 | if ($result = $this->find($data, $path, $target)) { 66 | 67 | if (0 === strlen($target->childKey)) { 68 | $data = null; 69 | } elseif (is_array($target->parent)) { 70 | array_splice($target->parent, (int) $target->childKey, 1); 71 | } else { 72 | // @phpstan-ignore-next-line 73 | unset($target->parent->{$target->childKey}); 74 | } 75 | } 76 | 77 | return $result; 78 | } 79 | 80 | /** 81 | * Replaces an element if found 82 | * 83 | * @param mixed $data 84 | * @param mixed $value 85 | */ 86 | public function replace(&$data, string $path, $value): bool 87 | { 88 | if ($result = $this->find($data, $path, $target)) { 89 | $result = $this->addData($target, $value); 90 | } 91 | 92 | return $result; 93 | } 94 | 95 | public function getError(): string 96 | { 97 | return $this->error; 98 | } 99 | 100 | /** 101 | * Adds or modifies the data 102 | * 103 | * @param mixed $value 104 | */ 105 | protected function addData(Target $target, $value): bool 106 | { 107 | $result = true; 108 | $error = ''; 109 | 110 | switch ($target->type) { 111 | case Target::TYPE_VALUE: 112 | $target->element = $value; 113 | break; 114 | case Target::TYPE_OBJECT: 115 | if (is_object($target->element)) { 116 | // @phpstan-ignore-next-line 117 | $target->element->{$target->key} = $value; 118 | } else { 119 | $error = sprintf("property '%s'", $target->key); 120 | } 121 | break; 122 | case Target::TYPE_ARRAY: 123 | if (is_array($target->element)) { 124 | // @phpstan-ignore-next-line 125 | array_splice($target->element, $target->key, 0, [$value]); 126 | } else { 127 | $error =sprintf("offset '%d'", $target->key); 128 | } 129 | break; 130 | } 131 | 132 | if (Utils::stringNotEmpty($error)) { 133 | $result = false; 134 | $type = gettype($target->element); 135 | $this->error = sprintf('Cannot assign %s to %s', $error, $type); 136 | } 137 | 138 | return $result; 139 | } 140 | 141 | /** 142 | * Returns true if we can create a new element 143 | */ 144 | protected function canBuild(Target $target): bool 145 | { 146 | return !$target->invalid && !($this->jsonPatch && Utils::arrayNotEmpty($target->tokens)); 147 | } 148 | 149 | /** 150 | * Returns true if an element is found 151 | * 152 | * @param mixed $data 153 | */ 154 | protected function find(&$data, string $path, ?Target &$target): bool 155 | { 156 | $target = new Target($path, $this->error); 157 | 158 | return $this->finder->get($data, $target); 159 | } 160 | 161 | /** 162 | * Returns true if an element is found or built 163 | * 164 | * @param mixed $data 165 | * @param mixed $value 166 | * @param Target $target Set by method 167 | * @return bool 168 | */ 169 | protected function getElement(&$data, string $path, &$value, &$target): bool 170 | { 171 | if ($this->find($data, $path, $target)) { 172 | 173 | if (is_array($target->parent)) { 174 | $target->setArray($target->childKey); 175 | $target->element =& $target->parent; 176 | } 177 | 178 | return true; 179 | } 180 | 181 | return $this->buildElement($target, $value); 182 | } 183 | 184 | /** 185 | * Return true if a new value has been built 186 | * 187 | * @param mixed $value 188 | * @return bool 189 | */ 190 | protected function buildElement(Target $target, &$value): bool 191 | { 192 | if ($result = $this->canBuild($target)) { 193 | 194 | try { 195 | $value = $this->builder->make($target, $value); 196 | } catch (InvalidArgumentException $e) { 197 | $result = false; 198 | } 199 | } 200 | 201 | return $result; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Helpers/Utils.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Helpers; 13 | 14 | use \stdClass; 15 | 16 | use Composer\Pcre\Preg; 17 | 18 | class Utils 19 | { 20 | public static function stringIsEmpty(string $value): bool 21 | { 22 | return strlen($value) === 0; 23 | } 24 | 25 | public static function stringNotEmpty(string $value): bool 26 | { 27 | return strlen($value) !== 0; 28 | } 29 | 30 | public static function stringIsJson(string $value): bool 31 | { 32 | $value = trim($value); 33 | $objectRegex = '#^\\{(?:.*)\\}$#s'; 34 | $arrayRegex = '#^\\[(?:.*)\\]$#s'; 35 | 36 | return (Preg::isMatch($objectRegex, $value) 37 | || Preg::isMatch($arrayRegex, $value)); 38 | } 39 | 40 | /** 41 | * @param non-empty-string $pattern 42 | * @param array $matches Set by method 43 | * @param-out array $matches 44 | */ 45 | public static function isMatch(string $pattern, string $value, ?array &$matches = null): bool 46 | { 47 | return Preg::isMatch($pattern, $value, $matches); 48 | } 49 | 50 | /** 51 | * @param array $value 52 | */ 53 | public static function arrayIsEmpty(array $value): bool 54 | { 55 | return count($value) === 0; 56 | } 57 | 58 | /** 59 | * @param array $value 60 | */ 61 | public static function arrayNotEmpty(array $value): bool 62 | { 63 | return count($value) > 0; 64 | } 65 | 66 | /** 67 | * @param mixed $data 68 | */ 69 | public static function jsonEncode($data, int $options = 0): string 70 | { 71 | $options = ($options & ~JSON_THROW_ON_ERROR); 72 | $result = json_encode($data, $options); 73 | 74 | if ($result === false) { 75 | throw new \RuntimeException(json_last_error_msg()); 76 | } 77 | 78 | return $result; 79 | } 80 | 81 | /** 82 | * @param mixed $value 83 | */ 84 | public static function getArgumentError(string $name, string $expected, $value): string 85 | { 86 | if (is_object($value)) { 87 | $type = get_class($value); 88 | } else { 89 | $type = gettype($value); 90 | } 91 | 92 | return sprintf("Argument '%s' expected '%s', got '%s'", $name, $expected, $type); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Loader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Error; 17 | use JohnStevenson\JsonWorks\Helpers\Utils; 18 | 19 | /** 20 | * A class for loading input data. 21 | * @api 22 | */ 23 | class Loader 24 | { 25 | /** 26 | * Processes input to be used as a document 27 | * 28 | * The input can be: 29 | * - a json string, passed to json_decode 30 | * - a .json filename, passed to file_get_contents then json_decode 31 | * - an object, class, array 32 | * 33 | * @param mixed $input 34 | * @return mixed 35 | */ 36 | public function getData($input) 37 | { 38 | $result = $this->processInput($input); 39 | $dataType = gettype($result); 40 | 41 | if ($dataType !== 'resource') { 42 | return $result; 43 | } 44 | 45 | throw new \RuntimeException($this->getInputError($dataType)); 46 | } 47 | 48 | /** 49 | * Processes input to be used as a JSON Patch 50 | * 51 | * The input can be: 52 | * - a json string, passed to json_decode 53 | * - a .json filename, passed to file_get_contents then json_decode 54 | * - an array 55 | * 56 | * The resulting data must be an array. 57 | * 58 | * @param mixed $input 59 | * @return array 60 | */ 61 | public function getPatch($input): array 62 | { 63 | $result = $this->processInput($input); 64 | 65 | if (is_array($result)) { 66 | return $result; 67 | } 68 | 69 | $dataType = gettype($result); 70 | throw new \RuntimeException($this->getInputError($dataType)); 71 | } 72 | 73 | /** 74 | * Processes input to be used as a schema 75 | * 76 | * The input can be: 77 | * - a json string, passed to json_decode 78 | * - a .json filename, passed to file_get_contents then json_decode 79 | * - an object 80 | * 81 | * The resulting data must be an object. 82 | * 83 | * @param mixed $input 84 | * @return stdClass 85 | */ 86 | public function getSchema($input) 87 | { 88 | $result = $this->processInput($input); 89 | 90 | if ($result instanceof stdClass) { 91 | return $result; 92 | } 93 | 94 | $dataType = gettype($result); 95 | throw new \RuntimeException($this->getInputError($dataType)); 96 | } 97 | 98 | /** 99 | * The main input processing method 100 | * 101 | * @param mixed $data 102 | * @return mixed 103 | */ 104 | private function processInput($data) 105 | { 106 | if (is_string($data)) { 107 | $data = $this->processStringInput($data); 108 | } else { 109 | $formatter = new Formatter(); 110 | $data = $formatter->copy($data); 111 | } 112 | 113 | return $data; 114 | } 115 | 116 | /** 117 | * Processes a file or raw json 118 | * 119 | * @return mixed 120 | */ 121 | private function processStringInput(string $input) 122 | { 123 | if ($this->isFile($input)) { 124 | $input = $this->getDataFromFile($input); 125 | } 126 | 127 | return $this->decodeJson($input); 128 | } 129 | 130 | private function isFile(string $input): bool 131 | { 132 | return pathinfo($input, PATHINFO_EXTENSION) === 'json'; 133 | } 134 | 135 | /** 136 | * Returns the contents of a file 137 | * 138 | * @throws \RuntimeException 139 | */ 140 | private function getDataFromFile(string $filename): string 141 | { 142 | $json = @file_get_contents($filename); 143 | 144 | if ($json === false) { 145 | $error = new Error(); 146 | throw new \RuntimeException($error->get(Error::ERR_NOT_FOUND, $filename)); 147 | } 148 | 149 | return $json; 150 | } 151 | 152 | /** 153 | * Decodes a json string 154 | * 155 | * This function allows a JSON text as per RFC 8259, except for an empty string 156 | * 157 | * @param string $value 158 | * @return mixed 159 | * @throws \RuntimeException 160 | */ 161 | private function decodeJson(string $value) 162 | { 163 | $result = $value; 164 | $errorMsg = null; 165 | 166 | if ($this->checkJson($value, $errorMsg)) { 167 | $result = json_decode($value); 168 | 169 | if (json_last_error() > 0) { 170 | $errorMsg = json_last_error_msg(); 171 | } 172 | } 173 | 174 | if ($errorMsg !== null) { 175 | throw new \RuntimeException($this->getJsonError($errorMsg)); 176 | } 177 | 178 | return $result; 179 | } 180 | 181 | private function checkJson(string &$value, ?string &$errorMsg): bool 182 | { 183 | $value = trim($value); 184 | 185 | if (Utils::stringIsJson($value)) { 186 | return true; 187 | } 188 | 189 | if (Utils::stringIsEmpty($value)) { 190 | $errorMsg = 'Syntax error'; 191 | } 192 | 193 | return false; 194 | } 195 | 196 | private function getInputError(string $dataType): string 197 | { 198 | $error = new Error(); 199 | 200 | return $error->get(Error::ERR_BAD_INPUT, $dataType); 201 | } 202 | 203 | /** 204 | * Returns a formatted json error message 205 | * 206 | */ 207 | private function getJsonError(string $errorMsg): string 208 | { 209 | $error = new Error(); 210 | $msg = sprintf('json error: %s', $errorMsg); 211 | return $error->get(Error::ERR_BAD_INPUT, $msg); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/Schema/Cache.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Finder; 17 | use JohnStevenson\JsonWorks\Helpers\Patch\Target; 18 | use JohnStevenson\JsonWorks\Helpers\Utils; 19 | use JohnStevenson\JsonWorks\Schema\DataChecker; 20 | 21 | class Cache 22 | { 23 | protected Store $store; 24 | protected DataChecker $dataChecker; 25 | protected Finder $finder; 26 | 27 | /** @var array */ 28 | protected array $parents; 29 | 30 | public function __construct(stdClass $schema) 31 | { 32 | $this->store = new Store; 33 | $this->store->addRoot('/', $schema); 34 | 35 | $this->dataChecker = new DataChecker; 36 | $this->finder = new Finder; 37 | } 38 | 39 | public function resolveRef(string $ref): stdClass 40 | { 41 | list($doc, $path) = $this->splitRef($ref); 42 | 43 | if ($this->store->hasRoot($doc)) { 44 | $this->parents = []; 45 | $schema = $this->resolve($ref); 46 | 47 | $childRef = $this->dataChecker->checkForRef($schema); 48 | 49 | if ($childRef !== null) { 50 | $error = $this->getRefError('Circular reference found', $ref); 51 | throw new \RuntimeException($error); 52 | } 53 | 54 | return $schema; 55 | } 56 | 57 | throw new \RuntimeException($this->getResolveError($ref)); 58 | } 59 | 60 | protected function resolve(string $ref): stdClass 61 | { 62 | list($doc, $path) = $this->splitRef($ref); 63 | 64 | $schema = $this->store->get($doc, $path, $data); 65 | if ($schema !== null) { 66 | return $schema; 67 | } 68 | 69 | $this->checkParents($ref); 70 | 71 | $schema = $this->find($ref, $doc, $path, $data); 72 | if ($schema !== null) { 73 | return $schema; 74 | } 75 | 76 | throw new \RuntimeException($this->getResolveError($ref)); 77 | } 78 | 79 | /** 80 | * @return array{0: string, 1: string} 81 | */ 82 | protected function splitRef(string $ref): array 83 | { 84 | list($doc, $path) = explode('#', $ref, 2); 85 | 86 | if (Utils::stringIsEmpty($doc)) { 87 | $doc = '/'; 88 | } 89 | 90 | if (Utils::stringIsEmpty($path)) { 91 | $path = '#'; 92 | } 93 | 94 | return [$doc, $path]; 95 | } 96 | 97 | protected function checkParents(string $ref): void 98 | { 99 | if (!in_array($ref, $this->parents, true)) { 100 | return; 101 | } 102 | 103 | $error = $this->getRefError('Circular reference found', $this->parents); 104 | throw new \RuntimeException($error); 105 | } 106 | 107 | protected function makeRef(string $doc, string $path): string 108 | { 109 | $doc = $doc !== '/' ? $doc : ''; 110 | 111 | return sprintf('%s#%s', $doc, $path); 112 | } 113 | 114 | /** 115 | * @param mixed $data 116 | */ 117 | protected function find(string $ref, string $doc, string $path, $data): ?stdClass 118 | { 119 | $error = ''; 120 | $target = new Target($path, $error); 121 | 122 | if ($this->finder->get($data, $target)) { 123 | if ($target->element instanceof stdClass) { 124 | return $this->processFoundSchema($ref, $target->element); 125 | } 126 | 127 | throw new \RuntimeException($this->getResolveError($ref)); 128 | } 129 | 130 | $childRef = $this->dataChecker->checkForRef($target->element); 131 | 132 | if ($childRef !== null) { 133 | $foundRef = $this->makeRef($doc, $target->foundPath); 134 | return $this->processFoundRef($ref, $foundRef, $childRef); 135 | } 136 | 137 | return null; 138 | } 139 | 140 | protected function processFoundSchema(string $ref, stdClass $schema): stdClass 141 | { 142 | $childRef = $this->dataChecker->checkForRef($schema); 143 | 144 | if ($childRef !== null) { 145 | $this->parents[] = $ref; 146 | $schema = $this->resolve($childRef); 147 | } 148 | 149 | $this->addRef($ref, $schema); 150 | 151 | return $schema; 152 | } 153 | 154 | protected function processFoundRef(string $ref, string $foundRef, string $childRef): stdClass 155 | { 156 | $this->parents[] = $foundRef; 157 | $schema = $this->resolve($childRef); 158 | 159 | $this->addRef($foundRef, $schema); 160 | 161 | // remove foundRef from parents 162 | $key = array_search($foundRef, $this->parents, true); 163 | unset($this->parents[$key]); 164 | 165 | return $this->resolve($ref); 166 | } 167 | 168 | protected function addRef(string $ref, stdclass $schema): void 169 | { 170 | list($doc, $path) = $this->splitRef($ref); 171 | 172 | if (!$this->store->add($doc, $path, $schema)) { 173 | throw new \RuntimeException($this->getRecursionError($ref)); 174 | } 175 | } 176 | 177 | protected function getResolveError(string $ref): string 178 | { 179 | return $this->getRefError('Unable to find $ref', $ref); 180 | } 181 | 182 | protected function getRecursionError(string $ref): string 183 | { 184 | return $this->getRefError('Recursion searching for $ref', $ref); 185 | } 186 | 187 | /** 188 | * @param string|array $ref 189 | */ 190 | protected function getRefError(string $caption, $ref): string 191 | { 192 | return sprintf('%s [%s]', $caption, implode(', ', (array) $ref)); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Schema/Comparer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema; 13 | 14 | class Comparer extends JsonTypes 15 | { 16 | /** 17 | * @param mixed $value1 18 | * @param mixed $value2 19 | */ 20 | public function equals($value1, $value2): bool 21 | { 22 | $type = $this->getGeneric($value1); 23 | 24 | if ($type !== $this->getGeneric($value2)) { 25 | return false; 26 | } 27 | 28 | if ($type === 'array') { 29 | // @phpstan-ignore-next-line 30 | return $this->equalsArray($value1, $value2); 31 | } 32 | 33 | if ($type === 'number') { 34 | // @phpstan-ignore-next-line 35 | return $this->equalsNumber($value1, $value2); 36 | } 37 | 38 | if ($type === 'object') { 39 | // @phpstan-ignore-next-line 40 | return $this->equalsObject($value1, $value2); 41 | } 42 | 43 | return $value1 === $value2; 44 | } 45 | 46 | /** 47 | * @param array $data 48 | */ 49 | public function uniqueArray(array $data): bool 50 | { 51 | $count = count($data); 52 | 53 | for ($i = 0; $i < $count; ++$i) { 54 | 55 | for ($j = $i + 1; $j < $count; ++$j) { 56 | if ($this->equals($data[$i], $data[$j])) { 57 | return false; 58 | } 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | 65 | /** 66 | * @param array $arr1 67 | * @param array $arr2 68 | */ 69 | protected function equalsArray(array $arr1, array $arr2): bool 70 | { 71 | $count = count($arr1); 72 | 73 | if ($count !== count($arr2)) { 74 | return false; 75 | } 76 | 77 | for ($i = 0; $i < $count; ++$i) { 78 | if (!$this->equals($arr1[$i], $arr2[$i])) { 79 | return false; 80 | } 81 | } 82 | 83 | return true; 84 | } 85 | 86 | /** 87 | * @param int|double|string $value1 88 | * @param int|double|string $value2 89 | */ 90 | protected function equalsNumber($value1, $value2): bool 91 | { 92 | return 0 === bccomp(strval($value1), strval($value2), 16); 93 | } 94 | 95 | /** 96 | * @param object $obj1 97 | * @param object $obj2 98 | */ 99 | protected function equalsObject($obj1, $obj2): bool 100 | { 101 | // get_object_vars fails on objects with digit keys 102 | if (count((array) $obj1) !== count((array) $obj2)) { 103 | return false; 104 | } 105 | 106 | foreach (get_object_vars($obj1) as $key => $value) { 107 | if (!$this->hasEqualProperty($obj2, $key, $value)) { 108 | return false; 109 | } 110 | } 111 | 112 | return true; 113 | } 114 | 115 | /** 116 | * @param object $object 117 | * @param mixed $value 118 | */ 119 | protected function hasEqualProperty($object, string $key, $value): bool 120 | { 121 | if (!property_exists($object, $key)) { 122 | return false; 123 | } 124 | 125 | // @phpstan-ignore-next-line 126 | return $this->equals($value, $object->$key); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Schema/Constraint/ArrayConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Schema\Comparer; 17 | use JohnStevenson\JsonWorks\Schema\Constraint\ItemsConstraint; 18 | use JohnStevenson\JsonWorks\Schema\Constraint\Manager; 19 | use JohnStevenson\JsonWorks\Schema\Constraint\MaxMinConstraint; 20 | use JohnStevenson\JsonWorks\Helpers\Utils; 21 | 22 | class ArrayConstraint extends BaseConstraint implements ConstraintInterface 23 | { 24 | protected Comparer $comparer; 25 | protected ItemsConstraint $items; 26 | protected MaxMinConstraint $maxMin; 27 | 28 | public function __construct(Manager $manager) 29 | { 30 | parent::__construct($manager); 31 | $this->comparer = new Comparer(); 32 | $this->items = new ItemsConstraint($manager); 33 | $this->maxMin = new MaxMinConstraint($manager); 34 | } 35 | 36 | /** 37 | * @param mixed $data 38 | * @param stdClass|array $schema 39 | */ 40 | public function validate($data, $schema, ?string $key = null): void 41 | { 42 | if (!is_array($data)) { 43 | $error = Utils::getArgumentError('$data', 'array', $data); 44 | throw new \InvalidArgumentException($error); 45 | } 46 | 47 | if (!($schema instanceof stdClass)) { 48 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 49 | throw new \InvalidArgumentException($error); 50 | } 51 | 52 | // max and min 53 | $this->checkMaxMin($data, $schema); 54 | 55 | // uniqueItems 56 | $this->checkUnique($data, $schema); 57 | 58 | $this->items->validate($data, $schema); 59 | } 60 | 61 | /** 62 | * @param array $data 63 | */ 64 | protected function checkMaxMin(array $data, stdClass $schema): void 65 | { 66 | // maxItems 67 | $this->maxMin->validate($data, $schema, 'maxItems'); 68 | 69 | // minItems 70 | $this->maxMin->validate($data, $schema, 'minItems'); 71 | } 72 | 73 | /** 74 | * @param array $data 75 | */ 76 | protected function checkUnique(array $data, stdClass $schema): void 77 | { 78 | if ($this->isUnique($schema)) { 79 | 80 | if (!$this->comparer->uniqueArray($data)) { 81 | $this->addError('contains duplicate values'); 82 | } 83 | } 84 | } 85 | 86 | protected function isUnique(stdClass $schema): bool 87 | { 88 | if ($this->getValue($schema, 'uniqueItems', $value, ['boolean'])) { 89 | return $value; 90 | } 91 | 92 | return false; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Schema/Constraint/BaseConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Tokenizer; 17 | use JohnStevenson\JsonWorks\Helpers\Utils; 18 | use JohnStevenson\JsonWorks\Schema\Constraint\Manager; 19 | use JohnStevenson\JsonWorks\Schema\ValidationException; 20 | 21 | abstract class BaseConstraint 22 | { 23 | protected Manager $manager; 24 | protected Tokenizer $tokenizer; 25 | 26 | public function __construct(Manager $manager) 27 | { 28 | $this->manager = $manager; 29 | $this->tokenizer = new Tokenizer(); 30 | } 31 | 32 | protected function addError(string $error): void 33 | { 34 | $path = $this->tokenizer->encode($this->manager->dataPath); 35 | 36 | if (Utils::stringIsEmpty($path)) { 37 | $path = '#'; 38 | } 39 | 40 | $this->manager->errors[] = sprintf("Property: '%s'. Error: %s", $path, $error); 41 | 42 | if ($this->manager->stopOnError) { 43 | throw new ValidationException(); 44 | } 45 | } 46 | 47 | /** 48 | * @param mixed $value Set by method 49 | * @param array|null $required 50 | */ 51 | public function getValue(stdClass $schema, string $key, &$value, ?array $required = null): bool 52 | { 53 | return $this->manager->getValue($schema, $key, $value, $required); 54 | } 55 | 56 | protected function formatError(string $expected, string $value): string 57 | { 58 | return $this->manager->dataChecker->formatError($expected, $value); 59 | } 60 | 61 | protected function matchPattern(string $pattern, string $string): bool 62 | { 63 | $regex = sprintf('#%s#', str_replace('#', '\\#', $pattern)); 64 | 65 | // suppress any warnings 66 | set_error_handler(static function (int $code, string $msg): bool { return true; }); 67 | 68 | try { 69 | $result = Utils::isMatch($regex, $string); 70 | restore_error_handler(); 71 | } catch (\Composer\Pcre\PcreException $e) { 72 | restore_error_handler(); 73 | $error = $this->formatError('valid regex', $pattern); 74 | throw new \RuntimeException($error); 75 | } 76 | 77 | return $result; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Schema/Constraint/CommonConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | use JohnStevenson\JsonWorks\Schema\DataChecker; 18 | 19 | class CommonConstraint extends BaseConstraint implements ConstraintInterface 20 | { 21 | protected DataChecker $dataChecker; 22 | 23 | public function __construct(Manager $manager) 24 | { 25 | parent::__construct($manager); 26 | $this->dataChecker = $this->manager->dataChecker; 27 | } 28 | 29 | /** 30 | * @param mixed $data 31 | * @param stdClass|array $schema 32 | */ 33 | public function validate($data, $schema, ?string $key = null): void 34 | { 35 | if (!($schema instanceof stdClass)) { 36 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 37 | throw new \InvalidArgumentException($error); 38 | } 39 | 40 | $this->run($data, $schema); 41 | } 42 | 43 | /** 44 | * @param mixed $data 45 | */ 46 | protected function run($data, stdClass $schema): void 47 | { 48 | $common = [ 49 | 'enum' => 'array', 50 | 'type' => ['array', 'string'], 51 | 'allOf' => 'array', 52 | 'anyOf' => 'array', 53 | 'oneOf' => 'array', 54 | 'not' => 'object' 55 | ]; 56 | 57 | foreach (get_object_vars($schema) as $key => $subSchema) { 58 | if (isset($common[$key])) { 59 | $required = (array) $common[$key]; 60 | $this->getValue($schema, $key, $subSchema, $required); 61 | $this->checkSchema($subSchema, $key); 62 | $this->check($data, $subSchema, $key); 63 | } 64 | } 65 | } 66 | 67 | /** 68 | * @param mixed $schema 69 | */ 70 | protected function checkSchema(&$schema, string $key): void 71 | { 72 | if ($key === 'type') { 73 | $schema = (array) $schema; 74 | } 75 | 76 | if ($key !== 'not' && is_array($schema)) { 77 | $this->dataChecker->checkArray($schema, $key); 78 | } 79 | } 80 | 81 | /** 82 | * @param mixed $data 83 | * @param array $subSchema 84 | */ 85 | protected function check($data, $subSchema, string $key): void 86 | { 87 | switch ($key) { 88 | case 'enum': 89 | $class = EnumConstraint::class; 90 | break; 91 | case 'type': 92 | $class = TypeConstraint::class; 93 | break; 94 | default: 95 | $class = OfConstraint::class; 96 | break; 97 | } 98 | 99 | $validator =$this->manager->factory($class); 100 | $validator->validate($data, $subSchema, $key); 101 | 102 | /* 103 | if ($name === 'of') { 104 | $validator->validate($data, $subSchema, $key); 105 | } else { 106 | $validator->validate($data, $subSchema); 107 | } 108 | */ 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Schema/Constraint/ConstraintInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | interface ConstraintInterface 17 | { 18 | public function __construct(Manager $manager); 19 | 20 | /** 21 | * @param mixed $data 22 | * @param stdClass|array $schema 23 | */ 24 | public function validate($data, $schema, ?string $key = null): void; 25 | } 26 | -------------------------------------------------------------------------------- /src/Schema/Constraint/EnumConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | use JohnStevenson\JsonWorks\Schema\Comparer; 18 | use JohnStevenson\JsonWorks\Schema\Constraint\Manager; 19 | 20 | class EnumConstraint extends BaseConstraint implements ConstraintInterface 21 | { 22 | protected Comparer $comparer; 23 | 24 | public function __construct(Manager $manager) 25 | { 26 | parent::__construct($manager); 27 | $this->comparer = new Comparer(); 28 | } 29 | 30 | /** 31 | * @param mixed $data 32 | * @param stdClass|array $schema 33 | */ 34 | public function validate($data, $schema, ?string $key = null): void 35 | { 36 | if (!is_array($schema)) { 37 | $error = Utils::getArgumentError('$schema', 'array', $schema); 38 | throw new \InvalidArgumentException($error); 39 | } 40 | 41 | foreach ($schema as $value) { 42 | if ($this->comparer->equals($value, $data)) { 43 | return; 44 | } 45 | } 46 | 47 | $error = sprintf("value not found in enum '%s'", json_encode($schema)); 48 | $this->addError($error); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Schema/Constraint/FormatChecker.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use JohnStevenson\JsonWorks\Helpers\Utils; 15 | 16 | class FormatChecker extends BaseConstraint 17 | { 18 | public function check(string $data, string $format): void 19 | { 20 | if (!$this->checkKnownFormat($data, $format)) { 21 | $error = sprintf("Unknown format '%s'", $format); 22 | $this->addError($error); 23 | } 24 | } 25 | 26 | protected function checkKnownFormat(string $data, string $format): bool 27 | { 28 | if ($format === 'date-time') { 29 | $this->checkDateTime($data, $format); 30 | return true; 31 | } 32 | 33 | if ($format === 'email') { 34 | $this->checkEmail($data, $format); 35 | return true; 36 | } 37 | 38 | if ($format === 'hostname') { 39 | $this->checkHostname($data, $format); 40 | return true; 41 | } 42 | 43 | if ($format === 'ipv4' || $format === 'ipv6') { 44 | $this->checkIp($data, $format); 45 | return true; 46 | } 47 | 48 | if ($format === 'uri') { 49 | $this->checkUri($data, $format); 50 | return true; 51 | } 52 | 53 | return false; 54 | } 55 | 56 | protected function checkDateTime(string $data, string $format): void 57 | { 58 | $regex = '/^\d{4}-\d{2}-\d{2}[T| ]\d{2}:\d{2}:\d{2}(\.\d{1})?(Z|[\+|-]\d{2}:\d{2})?$/i'; 59 | 60 | if (!Utils::isMatch($regex, $data) || false === strtotime($data)) { 61 | $this->setError($data, $format); 62 | } 63 | } 64 | 65 | protected function checkEmail(string $data, string $format): void 66 | { 67 | $this->filter($data, $format, FILTER_VALIDATE_EMAIL); 68 | } 69 | 70 | protected function checkHostname(string $data, string $format): void 71 | { 72 | $regex = '/^[_a-z]+\.([_a-z]+\.?)+$/i'; 73 | 74 | if (!Utils::isMatch($regex, $data) || strlen($data) > 255) { 75 | $this->setError($data, $format); 76 | } 77 | } 78 | 79 | protected function checkIp(string $data, string $format): void 80 | { 81 | $flags = $format === 'ipv4' ? FILTER_FLAG_IPV4 : FILTER_FLAG_IPV6; 82 | $this->filter($data, $format, FILTER_VALIDATE_IP, $flags); 83 | } 84 | 85 | protected function checkUri(string $data, string $format): void 86 | { 87 | $this->filter($data, $format, FILTER_VALIDATE_URL); 88 | } 89 | 90 | protected function filter(string $data, string $format, int $filter, int $flags = 0): void 91 | { 92 | $flags |= FILTER_NULL_ON_FAILURE; 93 | 94 | if (null === filter_var($data, $filter, $flags)) { 95 | $this->setError($data, $format); 96 | } 97 | } 98 | 99 | protected function setError(string $data, string $format): void 100 | { 101 | $error = sprintf("Invalid %s '%s'", $format, $data); 102 | $this->addError($error); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Schema/Constraint/ItemsConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | 18 | class ItemsConstraint extends BaseConstraint implements ConstraintInterface 19 | { 20 | /** 21 | * @param mixed $data 22 | * @param stdClass|array $schema 23 | */ 24 | public function validate($data, $schema, ?string $key = null): void 25 | { 26 | if (!is_array($data)) { 27 | $error = Utils::getArgumentError('$data', 'array', $data); 28 | throw new \InvalidArgumentException($error); 29 | } 30 | 31 | if (!($schema instanceof stdClass)) { 32 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 33 | throw new \InvalidArgumentException($error); 34 | } 35 | 36 | list($items, $additional) = $this->getItemValues($schema); 37 | 38 | if ($items instanceof stdClass) { 39 | $this->validateObjectItems($data, $items); 40 | return; 41 | } 42 | 43 | if (is_array($items)) { 44 | $this->checkArrayItems($data, $items, $additional); 45 | $this->validateArrayItems($data, $items, $additional); 46 | } 47 | } 48 | 49 | /** 50 | * Returns items and additionalItems values 51 | * 52 | * @return array{0: array|object, 1: object|boolean|null} 53 | */ 54 | protected function getItemValues(stdClass $schema): array 55 | { 56 | $items = null; 57 | $this->getValue($schema, 'items', $items, ['array', 'object']); 58 | 59 | if ($items === null) { 60 | $items = []; 61 | } 62 | 63 | $additional = null; 64 | $this->getValue($schema, 'additionalItems', $additional, ['boolean', 'object']); 65 | 66 | return [$items, $additional]; 67 | } 68 | 69 | /** 70 | * @param array $data 71 | */ 72 | protected function validateObjectItems(array $data, stdClass $schema): void 73 | { 74 | $key = 0; 75 | 76 | foreach ($data as $value) { 77 | $this->manager->validate($value, $schema, strval($key)); 78 | ++$key; 79 | } 80 | } 81 | 82 | /** 83 | * @param array $data 84 | * @param array $items 85 | * @param object|boolean|null $additional 86 | */ 87 | protected function checkArrayItems(array $data, array $items, $additional): void 88 | { 89 | if (false === $additional) { 90 | if (count($data) > count($items)) { 91 | $this->addError('contains more elements than are allowed'); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * @param array $data 98 | * @param array $items 99 | * @param object|boolean|null $additional 100 | */ 101 | protected function validateArrayItems(array $data, array $items, $additional): void 102 | { 103 | $key = 0; 104 | $itemsCount = count($items); 105 | 106 | foreach ($data as $value) { 107 | 108 | if ($key < $itemsCount) { 109 | /** @var array $item */ 110 | $item = $items[$key]; 111 | $this->manager->validate($value, $item, strval($key)); 112 | } else { 113 | 114 | if ($additional instanceof stdClass) { 115 | $this->validateObjectItems(array_slice($data, $key), $additional); 116 | } 117 | break; 118 | } 119 | 120 | ++$key; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Schema/Constraint/Manager.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Schema\DataChecker; 17 | use JohnStevenson\JsonWorks\Schema\Resolver; 18 | 19 | class Manager 20 | { 21 | /** @var array */ 22 | public array $dataPath; 23 | 24 | /** @var array */ 25 | public array $errors; 26 | 27 | /** @var array */ 28 | protected $constraints; 29 | 30 | public bool $stopOnError; 31 | public DataChecker $dataChecker; 32 | 33 | protected Resolver $resolver; 34 | 35 | public function __construct(Resolver $resolver, bool $stopOnError) 36 | { 37 | $this->resolver = $resolver; 38 | $this->stopOnError = $stopOnError; 39 | 40 | $this->dataPath = []; 41 | $this->errors = []; 42 | $this->constraints = []; 43 | $this->dataChecker = new DataChecker(); 44 | } 45 | 46 | /** 47 | * @param mixed $data 48 | * @param stdClass|array $schema 49 | */ 50 | public function validate($data, $schema, ?string $key = null): void 51 | { 52 | $schema = $this->setValue($schema); 53 | 54 | if ($this->dataChecker->isEmptySchema($schema)) { 55 | return; 56 | } 57 | 58 | $this->dataPath[] = strval($key); 59 | 60 | // Check commmon types first 61 | if ($this->checkCommonTypes($data, $schema)) { 62 | $constraint = $this->factory(SpecificConstraint::class); 63 | $constraint->validate($data, $schema); 64 | } 65 | 66 | array_pop($this->dataPath); 67 | } 68 | 69 | /** 70 | * @template T of ConstraintInterface 71 | * @param class-string $class 72 | */ 73 | public function factory(string $class): ConstraintInterface 74 | { 75 | if (!isset($this->constraints[$class])) { 76 | $this->constraints[$class] = new $class($this); 77 | } 78 | 79 | return $this->constraints[$class]; 80 | } 81 | 82 | /** 83 | * Fetches a value from the schema 84 | * 85 | * @param mixed $value 86 | * @param array|null $required 87 | * @throws \RuntimeException 88 | */ 89 | public function getValue(stdClass $schema, string $key, &$value, ?array $required = null): bool 90 | { 91 | $result = property_exists($schema, $key); 92 | 93 | if ($result) { 94 | $value = $this->setValue($schema->$key); 95 | $this->dataChecker->checkType($value, $required); 96 | } 97 | 98 | return $result; 99 | } 100 | 101 | /** 102 | * @param mixed $data 103 | * @param stdClass|array $schema 104 | */ 105 | protected function checkCommonTypes($data, $schema): bool 106 | { 107 | $errorCount = count($this->errors); 108 | 109 | $constraint = $this->factory(CommonConstraint::class); 110 | $constraint->validate($data, $schema); 111 | 112 | return count($this->errors) === $errorCount; 113 | } 114 | 115 | /** 116 | * @param stdClass|array $schema 117 | * @return stdClass|array 118 | */ 119 | protected function setValue($schema) 120 | { 121 | $ref = $this->dataChecker->checkForRef($schema); 122 | 123 | if ($ref !== null) { 124 | $schema = $this->resolver->getRef($ref); 125 | $this->dataChecker->checkType($schema, ['object']); 126 | } 127 | 128 | return $schema; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Schema/Constraint/MaxMinConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | 18 | class MaxMinConstraint extends BaseConstraint implements ConstraintInterface 19 | { 20 | protected bool $max; 21 | protected bool $length; 22 | protected string $caption; 23 | 24 | /** 25 | * @param mixed $data 26 | * @param stdClass|array $schema 27 | */ 28 | public function validate($data, $schema, ?string $key = null): void 29 | { 30 | if (!($schema instanceof stdClass)) { 31 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 32 | throw new \InvalidArgumentException($error); 33 | } 34 | 35 | if (!is_string($key)) { 36 | $error = Utils::getArgumentError('$key', 'string', $key); 37 | throw new \InvalidArgumentException($error); 38 | } 39 | 40 | if (!$this->getInteger($schema, $key, $value)) { 41 | return; 42 | } 43 | 44 | $this->setValues($data, $key); 45 | 46 | if ($this->length && is_string($data)) { 47 | $count = mb_strlen($data); 48 | } else { 49 | $count = count((array) $data); 50 | } 51 | 52 | if (!$this->compare($count, $value)) { 53 | $this->setError($count, $value); 54 | } 55 | } 56 | 57 | /** 58 | * Sets protected values 59 | * 60 | * @param mixed $data 61 | */ 62 | protected function setValues($data, string $key): void 63 | { 64 | $this->max = Utils::isMatch('/^max/', $key); 65 | 66 | if ($this->length = Utils::isMatch('/Length$/', $key)) { 67 | $this->caption = 'characters'; 68 | } else { 69 | $this->caption = is_object($data) ? 'properties' : 'elements'; 70 | } 71 | } 72 | 73 | /** 74 | * Returns true if a valid integer is found in the schema 75 | * 76 | * @param int $value Set by method 77 | * @throws \RuntimeException 78 | */ 79 | protected function getInteger(stdClass $schema, string $key, &$value): bool 80 | { 81 | if (!$this->getValue($schema, $key, $value, ['integer'])) { 82 | return false; 83 | } 84 | 85 | if ($value >= 0) { 86 | return true; 87 | } 88 | 89 | $error = $this->formatError('>= 0', (string) $value); 90 | throw new \RuntimeException($error); 91 | } 92 | 93 | protected function compare(int $count, int $value): bool 94 | { 95 | return $this->max ? $count <= $value : $count >= $value; 96 | } 97 | 98 | protected function setError(int $count, int $value): void 99 | { 100 | if ($this->max) { 101 | $error = "has too many %s [%d], maximum is '%d'"; 102 | } else { 103 | $error = "has too few %s [%d], minimum is '%d'"; 104 | } 105 | 106 | $this->addError(sprintf($error, $this->caption, $count, $value)); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Schema/Constraint/NumberConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | 18 | class NumberConstraint extends BaseConstraint implements ConstraintInterface 19 | { 20 | /** 21 | * @param mixed $data 22 | * @param stdClass|array $schema 23 | */ 24 | public function validate($data, $schema, ?string $key = null): void 25 | { 26 | if (!is_int($data) && !is_float($data)) { 27 | $error = Utils::getArgumentError('$data', 'int|float', $data); 28 | throw new \InvalidArgumentException($error); 29 | } 30 | 31 | if (!($schema instanceof stdClass)) { 32 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 33 | throw new \InvalidArgumentException($error); 34 | } 35 | 36 | // maximum 37 | $this->checkMaxMin($data, $schema, 'maximum', true); 38 | 39 | // minimum 40 | $this->checkMaxMin($data, $schema, 'minimum', false); 41 | 42 | // multipleOf 43 | $this->checkMultipleOf($data, $schema); 44 | } 45 | 46 | /** 47 | * @param int|float $data 48 | */ 49 | protected function checkMaxMin($data, stdClass $schema, string $key, bool $max): void 50 | { 51 | $exclusiveKey = sprintf('exclusive%s', ucfirst($key)); 52 | $exclusive = $this->getExclusive($schema, $exclusiveKey); 53 | 54 | if ($this->getNumber($schema, $key, false, $value)) { 55 | $this->compare($data, $value, $exclusive, $max); 56 | 57 | } elseif ($exclusive) { 58 | $error = $this->formatError($key, ''); 59 | throw new \RuntimeException($error); 60 | } 61 | } 62 | 63 | /** 64 | * @param int|float $data 65 | */ 66 | protected function checkMultipleOf($data, stdClass $schema): void 67 | { 68 | if (!$this->getNumber($schema, 'multipleOf', true, $multipleOf)) { 69 | return; 70 | } 71 | 72 | $quotient = bcdiv(strval($data), strval($multipleOf), 16); 73 | $intqt = (int) $quotient; 74 | 75 | if (bccomp(strval($quotient), strval($intqt), 16) !== 0) { 76 | $error = sprintf("value must be a multiple of '%f'", $multipleOf); 77 | $this->addError($error); 78 | } 79 | } 80 | 81 | protected function getExclusive(stdClass $schema, string $key): bool 82 | { 83 | return $this->getValue($schema, $key, $value, ['boolean']); 84 | } 85 | 86 | /** 87 | * @param int|float $value Set by method 88 | */ 89 | protected function getNumber(stdClass $schema, string $key, bool $positiveNonZero, &$value): bool 90 | { 91 | if (!$this->getValue($schema, $key, $value, ['number', 'integer'])) { 92 | return false; 93 | } 94 | 95 | if (!$positiveNonZero || $value > 0) { 96 | return true; 97 | } 98 | 99 | $error = $this->formatError('> 0', (string) $value); 100 | throw new \RuntimeException($error); 101 | } 102 | 103 | /** 104 | * @param int|float $data 105 | * @param int|float $value 106 | */ 107 | protected function compare($data, $value, bool $exclusive, bool $max): void 108 | { 109 | if ($this->precisionCompare($data, $value, $exclusive, $max)) { 110 | return; 111 | } 112 | 113 | // format the error 114 | $caption = $max ? 'less' : 'greater'; 115 | $equals = $exclusive ? 'or equal to ' : ''; 116 | $error = sprintf("value must be %s than %s'%f'", $caption, $equals, $value); 117 | 118 | $this->addError($error); 119 | } 120 | 121 | /** 122 | * @param int|double $data 123 | * @param int|float $value 124 | */ 125 | protected function precisionCompare($data, $value, bool $exclusive, bool $max): bool 126 | { 127 | $comp = bccomp(strval($data), strval($value), 16); 128 | 129 | if ($max) { 130 | $result = $exclusive ? $comp < 0 : $comp <= 0; 131 | } else { 132 | $result = $exclusive ? $comp > 0 : $comp >= 0; 133 | } 134 | 135 | return $result; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Schema/Constraint/ObjectConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | use JohnStevenson\JsonWorks\Schema\Constraint\Manager; 18 | use JohnStevenson\JsonWorks\Schema\Constraint\MaxMinConstraint; 19 | use JohnStevenson\JsonWorks\Schema\Constraint\PropertiesConstraint; 20 | 21 | class ObjectConstraint extends BaseConstraint implements ConstraintInterface 22 | { 23 | protected MaxMinConstraint $maxMin; 24 | protected PropertiesConstraint $properties; 25 | 26 | public function __construct(Manager $manager) 27 | { 28 | parent::__construct($manager); 29 | $this->maxMin = new MaxMinConstraint($manager); 30 | $this->properties = new PropertiesConstraint($manager); 31 | } 32 | 33 | /** 34 | * @param mixed $data 35 | * @param stdClass|array $schema 36 | */ 37 | public function validate($data, $schema, ?string $key = null): void 38 | { 39 | if (!is_object($data)) { 40 | $error = Utils::getArgumentError('$data', 'object', $data); 41 | throw new \InvalidArgumentException($error); 42 | } 43 | 44 | if (!($schema instanceof stdClass)) { 45 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 46 | throw new \InvalidArgumentException($error); 47 | } 48 | 49 | // max and min 50 | $this->checkMaxMin($data, $schema); 51 | 52 | // required 53 | $this->checkRequired($data, $schema); 54 | 55 | $this->properties->validate($data, $schema); 56 | } 57 | 58 | protected function checkMaxMin(object $data, stdClass $schema): void 59 | { 60 | // maxProperties 61 | $this->maxMin->validate($data, $schema, 'maxProperties'); 62 | 63 | // minProperties 64 | $this->maxMin->validate($data, $schema, 'minProperties'); 65 | } 66 | 67 | protected function checkRequired(object $data, stdClass $schema): void 68 | { 69 | if (!$this->getValue($schema, 'required', $value, ['array'])) { 70 | return; 71 | } 72 | 73 | $this->manager->dataChecker->checkArray($value, 'required'); 74 | 75 | foreach ($value as $name) { 76 | if (!property_exists($data, $name)) { 77 | $this->addError(sprintf("is missing required property '%s'", $name)); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Schema/Constraint/OfConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | use JohnStevenson\JsonWorks\Schema\ValidationException; 18 | 19 | class OfConstraint extends BaseConstraint implements ConstraintInterface 20 | { 21 | /** 22 | * @param mixed $data 23 | * @param stdClass|array $schema 24 | */ 25 | public function validate($data, $schema, ?string $key = null): void 26 | { 27 | if (!is_string($key)) { 28 | $error = Utils::getArgumentError('$key', 'string', $key); 29 | throw new \InvalidArgumentException($error); 30 | } 31 | 32 | $matchFirst = in_array($key, ['anyOf', 'not'], true); 33 | $schemas = is_array($schema) ? $schema : [$schema]; 34 | $matches = $this->getMatches($data, $schemas, $matchFirst); 35 | 36 | if (!$this->checkResult($key, $matches, count($schemas))) { 37 | $this->addError($this->getError($key)); 38 | } 39 | } 40 | 41 | /** 42 | * @param mixed $data 43 | * @param array $schemas 44 | */ 45 | protected function getMatches($data, array $schemas, bool $matchFirst): int 46 | { 47 | $result = 0; 48 | 49 | foreach ($schemas as $subSchema) { 50 | // type check 51 | if (!($subSchema instanceof stdClass)) { 52 | $error = Utils::getArgumentError('$subSchema', 'stdClass', $subSchema); 53 | throw new \InvalidArgumentException($error); 54 | } 55 | 56 | if ($this->testChild($data, $subSchema)) { 57 | ++$result; 58 | 59 | if ($matchFirst) { 60 | break; 61 | } 62 | } 63 | } 64 | 65 | return $result; 66 | } 67 | 68 | /** 69 | * @param mixed $data 70 | */ 71 | protected function testChild($data, stdClass $schema): bool 72 | { 73 | $currentStop = $this->manager->stopOnError; 74 | $this->manager->stopOnError = true; 75 | 76 | try { 77 | $this->manager->validate($data, $schema); 78 | $result = true; 79 | } catch (ValidationException $e) { 80 | $result = false; 81 | array_pop($this->manager->errors); 82 | } 83 | 84 | $this->manager->stopOnError = $currentStop; 85 | 86 | return $result; 87 | } 88 | 89 | protected function checkResult(string $key, int $matches, int $schemaCount): bool 90 | { 91 | switch ($key) { 92 | case 'allOf': 93 | return $matches === $schemaCount; 94 | case 'anyOf': 95 | return $matches !== 0; 96 | case 'oneOf': 97 | return $matches === 1; 98 | } 99 | 100 | return $matches === 0; 101 | } 102 | 103 | protected function getError(string $key): string 104 | { 105 | if ($key === 'not') { 106 | return 'must not validate against this schema'; 107 | } 108 | 109 | return sprintf("does not match '%s' schema requirements", $key); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Schema/Constraint/PropertiesConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | 18 | class PropertiesConstraint extends BaseConstraint implements ConstraintInterface 19 | { 20 | /** @var array */ 21 | protected array $children; 22 | 23 | /** 24 | * @param mixed $data 25 | * @param stdClass|array $schema 26 | */ 27 | public function validate($data, $schema, ?string $key = null): void 28 | { 29 | if (!($schema instanceof stdClass)) { 30 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 31 | throw new \InvalidArgumentException($error); 32 | } 33 | 34 | $additional = $this->getAdditional($schema); 35 | $this->children = []; 36 | 37 | $this->checkProperties($data, $schema, $additional); 38 | 39 | foreach ($this->children as $child) { 40 | $this->manager->validate($child['data'], $child['schema'], $child['key']); 41 | } 42 | } 43 | 44 | /** 45 | * @return object|bool 46 | */ 47 | protected function getAdditional(stdClass $schema) 48 | { 49 | $this->getValue($schema, 'additionalProperties', $value, ['object', 'boolean']); 50 | 51 | return $value; 52 | } 53 | 54 | /** 55 | * @param mixed $data 56 | * @param object|bool $additional 57 | */ 58 | protected function checkProperties($data, stdClass $schema, $additional): void 59 | { 60 | $set = (array) $data; 61 | 62 | $this->parseProperties($schema, $set); 63 | $this->parsePatternProperties($schema, $set); 64 | 65 | if (false === $additional && Utils::arrayNotEmpty($set)) { 66 | $this->addError('contains unspecified additional properties'); 67 | } 68 | 69 | $this->mergeAdditional($set, $additional); 70 | } 71 | 72 | /** 73 | * @param array $set 74 | */ 75 | protected function parseProperties(stdClass $schema, array &$set): void 76 | { 77 | if (!$this->getSchemaProperties($schema, 'properties', $props)) { 78 | return; 79 | } 80 | 81 | foreach ($props as $key => $subSchema) { 82 | if (array_key_exists($key, $set)) { 83 | $this->addChild($set[$key], $subSchema, $key); 84 | unset($set[$key]); 85 | } 86 | } 87 | } 88 | 89 | /** 90 | * @param array $set 91 | */ 92 | protected function parsePatternProperties(stdClass $schema, array &$set): void 93 | { 94 | if (!$this->getSchemaProperties($schema, 'patternProperties', $props)) { 95 | return; 96 | } 97 | 98 | foreach ($props as $regex => $subSchema) { 99 | $this->checkPattern($regex, $subSchema, $set); 100 | } 101 | } 102 | 103 | /** 104 | * @param mixed $value Set by method 105 | */ 106 | protected function getSchemaProperties(stdClass $schema, string $key, &$value): bool 107 | { 108 | if ($result = $this->getValue($schema, $key, $value, ['object'])) { 109 | $this->manager->dataChecker->checkContainerTypes($value, 'object'); 110 | } 111 | 112 | return $result; 113 | } 114 | 115 | /** 116 | * @param array $set 117 | * @param object|bool $additional 118 | */ 119 | protected function mergeAdditional(array $set, $additional): void 120 | { 121 | if ($additional instanceof stdClass) { 122 | 123 | foreach ($set as $key => $data) { 124 | $this->addChild($data, $additional, $key); 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * @param array $set 131 | */ 132 | protected function checkPattern(string $regex, stdClass $schema, array &$set): void 133 | { 134 | $copy = $set; 135 | 136 | foreach ($copy as $key => $value) { 137 | 138 | $matchKey = $key !== '_empty_' ? $key : ''; 139 | 140 | if ($this->matchPattern($regex, $matchKey)) { 141 | $this->addChild($value, $schema, $key); 142 | unset($set[$key]); 143 | } 144 | } 145 | } 146 | 147 | /** 148 | * @param mixed $data 149 | */ 150 | protected function addChild($data, stdClass $schema, string $key): void 151 | { 152 | $this->children[] = [ 153 | 'data' => $data, 154 | 'schema' => $schema, 155 | 'key' => $key 156 | ]; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Schema/Constraint/SpecificConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | use JohnStevenson\JsonWorks\Schema\JsonTypes; 18 | use JohnStevenson\JsonWorks\Schema\Constraint\Manager; 19 | 20 | class SpecificConstraint extends BaseConstraint implements ConstraintInterface 21 | { 22 | protected JsonTypes $jsonTypes; 23 | 24 | public function __construct(Manager $manager) 25 | { 26 | parent::__construct($manager); 27 | $this->jsonTypes = new JsonTypes(); 28 | } 29 | 30 | /** 31 | * @param mixed $data 32 | * @param stdClass|array $schema 33 | */ 34 | public function validate($data, $schema, ?string $key = null): void 35 | { 36 | if (!($schema instanceof stdClass)) { 37 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 38 | throw new \InvalidArgumentException($error); 39 | } 40 | 41 | $dataType = $this->getInstanceType($data); 42 | $class = null; 43 | 44 | if (Utils::stringIsEmpty($dataType)) { 45 | return; 46 | } 47 | 48 | switch ($dataType) { 49 | case 'array': 50 | $class = ArrayConstraint::class; 51 | break; 52 | case 'number': 53 | $class = NumberConstraint::class; 54 | break; 55 | case 'object': 56 | $class = ObjectConstraint::class; 57 | break; 58 | case 'string': 59 | $class = StringConstraint::class; 60 | break; 61 | } 62 | 63 | if ($class === null) { 64 | throw new \RuntimeException('Unknown constraint: '.$dataType); 65 | } 66 | 67 | $validator = $this->manager->factory($class); 68 | $validator->validate($data, $schema); 69 | } 70 | 71 | /** 72 | * @param mixed $data 73 | */ 74 | protected function getInstanceType($data): string 75 | { 76 | $result = $this->jsonTypes->getGeneric($data); 77 | 78 | if (in_array($result, ['boolean', 'null'], true)) { 79 | $result = ''; 80 | } 81 | 82 | return $result; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Schema/Constraint/StringConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | 18 | class StringConstraint extends BaseConstraint implements ConstraintInterface 19 | { 20 | protected MaxMinConstraint $maxMin; 21 | protected FormatChecker $formatChecker; 22 | 23 | public function __construct(Manager $manager) 24 | { 25 | parent::__construct($manager); 26 | $this->maxMin = new MaxMinConstraint($manager); 27 | $this->formatChecker = new FormatChecker($manager); 28 | } 29 | 30 | /** 31 | * @param mixed $data 32 | * @param stdClass|array $schema 33 | */ 34 | public function validate($data, $schema, ?string $key = null): void 35 | { 36 | if (!is_string($data)) { 37 | $error = Utils::getArgumentError('$data', 'string', $data); 38 | throw new \InvalidArgumentException($error); 39 | } 40 | 41 | if (!($schema instanceof stdClass)) { 42 | $error = Utils::getArgumentError('$schema', 'sdtClass', $schema); 43 | throw new \InvalidArgumentException($error); 44 | } 45 | 46 | // maxLength 47 | $this->maxMin->validate($data, $schema, 'maxLength'); 48 | 49 | // minLength 50 | $this->maxMin->validate($data, $schema, 'minLength'); 51 | 52 | // format 53 | if ($this->getString($schema, 'format', $format)) { 54 | $this->formatChecker->check($data, $format); 55 | } 56 | 57 | // pattern 58 | if ($this->getString($schema, 'pattern', $pattern)) { 59 | $this->checkPattern($data, $pattern); 60 | } 61 | } 62 | 63 | protected function checkPattern(string $data, string $pattern): void 64 | { 65 | if (!$this->matchPattern($pattern, $data)) { 66 | $this->addError(sprintf('does not match pattern: %s', $pattern)); 67 | } 68 | } 69 | 70 | /** 71 | * @param mixed $value Set by method 72 | */ 73 | protected function getString(stdClass $schema, string $key, &$value): bool 74 | { 75 | return $this->getValue($schema, $key, $value, ['string']); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Schema/Constraint/TypeConstraint.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema\Constraint; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | use JohnStevenson\JsonWorks\Schema\JsonTypes; 18 | 19 | class TypeConstraint extends BaseConstraint implements ConstraintInterface 20 | { 21 | protected jsonTypes $jsonTypes; 22 | 23 | /** @var array */ 24 | protected array $types; 25 | 26 | public function __construct(Manager $manager) 27 | { 28 | parent::__construct($manager); 29 | $this->jsonTypes = new JsonTypes(); 30 | 31 | $this->types = [ 32 | 'array', 33 | 'boolean', 34 | 'integer', 35 | 'null', 36 | 'number', 37 | 'object', 38 | 'string' 39 | ]; 40 | } 41 | 42 | /** 43 | * @param mixed $data 44 | * @param stdClass|array $schema 45 | */ 46 | public function validate($data, $schema, ?string $key = null): void 47 | { 48 | if (!is_array($schema)) { 49 | $error = Utils::getArgumentError('$schema', 'string', $schema); 50 | throw new \InvalidArgumentException($error); 51 | } 52 | 53 | if (Utils::arrayIsEmpty($schema)) { 54 | return; 55 | } 56 | 57 | $this->checkSchema($schema); 58 | 59 | foreach ($schema as $type) { 60 | // type check 61 | if (!is_string($type)) { 62 | $error = Utils::getArgumentError('$type', 'string', $type); 63 | throw new \InvalidArgumentException($error); 64 | } 65 | 66 | if ($this->jsonTypes->checkType($data, $type)) { 67 | return; 68 | } 69 | } 70 | 71 | $error = sprintf("value must be of type '%s'", implode('|', $schema)); 72 | $this->addError($error); 73 | } 74 | 75 | /** 76 | * @param array $schema 77 | */ 78 | protected function checkSchema(array $schema): void 79 | { 80 | $unknown = array_diff($schema, $this->types); 81 | 82 | if (Utils::arrayNotEmpty($unknown)) { 83 | $error = $this->formatError(implode('|', $this->types), implode('', $unknown)); 84 | throw new \RuntimeException($error); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Schema/DataChecker.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema; 13 | 14 | use \stdClass; 15 | 16 | use JohnStevenson\JsonWorks\Helpers\Utils; 17 | use JohnStevenson\JsonWorks\Schema\Comparer; 18 | 19 | class DataChecker 20 | { 21 | protected Comparer $comparer; 22 | 23 | public function __construct() 24 | { 25 | $this->comparer = new Comparer(); 26 | } 27 | 28 | /** 29 | * @param mixed $value 30 | * @param array|null $required 31 | */ 32 | public function checkType($value, ?array $required): void 33 | { 34 | $type = $this->comparer->getSpecific($value); 35 | 36 | if ($required !== null) { 37 | if (!in_array($type, $required, true)) { 38 | $error = $this->formatError(implode('|', $required), $type); 39 | throw new \RuntimeException($error); 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * @param array $schema 46 | */ 47 | public function checkArray(array $schema, string $key): void 48 | { 49 | if ($key !== 'type') { 50 | $this->checkArrayCount($schema); 51 | } 52 | 53 | $this->checkArrayValues($schema, $key); 54 | } 55 | 56 | /** 57 | * @param object|array $schema 58 | */ 59 | public function checkContainerTypes($schema, string $type): void 60 | { 61 | $container = is_object($schema) ? get_object_vars($schema) : $schema; 62 | 63 | foreach ($container as $value) { 64 | if (!$this->comparer->checkType($value, $type)) { 65 | $error = $this->formatError($type, 'mixed'); 66 | throw new \RuntimeException($error); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * @param mixed $schema 73 | */ 74 | public function isEmptySchema($schema): bool 75 | { 76 | $this->checkType($schema, ['object']); 77 | 78 | return Utils::arrayIsEmpty((array) $schema); 79 | } 80 | 81 | /** 82 | * @param mixed $schema 83 | */ 84 | public function checkForRef($schema): ?string 85 | { 86 | $result = null; 87 | 88 | if (!($schema instanceof stdClass) || !property_exists($schema, '$ref')) { 89 | return $result; 90 | } 91 | 92 | $result = $schema->{'$ref'}; 93 | 94 | if (!is_string($result)) { 95 | $error = $this->formatError('string', gettype($result)); 96 | throw new \RuntimeException($error); 97 | } 98 | 99 | return $result; 100 | } 101 | 102 | public function formatError(string $expected, string $value): string 103 | { 104 | return sprintf( 105 | "Invalid schema value: expected '%s', got '%s'", 106 | $expected, 107 | $value 108 | ); 109 | } 110 | 111 | /** 112 | * @param array $schema 113 | */ 114 | protected function checkArrayCount(array $schema): void 115 | { 116 | if (count($schema) <= 0) { 117 | $error = $this->formatError('> 0', '0'); 118 | throw new \RuntimeException($error); 119 | } 120 | } 121 | 122 | /** 123 | * @param array $schema 124 | */ 125 | protected function checkArrayValues(array $schema, string $key): void 126 | { 127 | if (in_array($key, ['enum', 'type', 'required'], true)) { 128 | $this->checkUnique($schema); 129 | } 130 | 131 | if ($key !== 'enum') { 132 | $type = in_array($key, ['type', 'required'], true) ? 'string' : 'object'; 133 | $this->checkContainerTypes($schema, $type); 134 | } 135 | } 136 | 137 | /** 138 | * @param array $schema 139 | */ 140 | protected function checkUnique(array $schema): void 141 | { 142 | if (!$this->comparer->uniqueArray($schema)) { 143 | $error = $this->formatError('unique', 'duplicates'); 144 | throw new \RuntimeException($error); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Schema/JsonTypes.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema; 13 | 14 | class JsonTypes 15 | { 16 | /** 17 | * Returns the variable type 18 | * 19 | * Numeric variables are reported as numbers 20 | * 21 | * @param mixed $value 22 | */ 23 | public function getGeneric($value): string 24 | { 25 | $result = $this->getSpecific($value); 26 | 27 | return $result === 'integer' ? 'number' : $result; 28 | } 29 | 30 | /** 31 | * Returns the variable type 32 | * 33 | * Numeric variables are reported as either numbers or integers 34 | * 35 | * @param mixed $value 36 | */ 37 | public function getSpecific($value): string 38 | { 39 | $result = strtolower(gettype($value)); 40 | 41 | if ($result === 'double') { 42 | $result = $this->isInteger($value) ? 'integer' : 'number'; 43 | } 44 | 45 | return $result; 46 | } 47 | 48 | /** 49 | * Returns true if a value is the generic type 50 | * 51 | * @param mixed $value 52 | */ 53 | public function checkType($value, string $type): bool 54 | { 55 | if ($type === 'integer') { 56 | return $this->isInteger($value); 57 | } 58 | 59 | if ($type === 'number') { 60 | return $this->isNumber($value); 61 | } 62 | 63 | return $type === $this->getGeneric($value); 64 | } 65 | 66 | /** 67 | * Returns true if array values are the same type 68 | * 69 | * @param array $data 70 | */ 71 | public function arrayOfType(array $data, string $type): bool 72 | { 73 | foreach ($data as $value) { 74 | 75 | if (!$this->checkType($value, $type)) { 76 | return false; 77 | } 78 | } 79 | 80 | return true; 81 | } 82 | 83 | /** 84 | * Returns true if a value is an integer 85 | * 86 | * Large integers may be stored as a float (Issue:1). Note that the data 87 | * may have been truncated to fit a 64-bit PHP_MAX_INT 88 | * 89 | * @param mixed $value 90 | */ 91 | protected function isInteger($value): bool 92 | { 93 | return is_integer($value) || (is_float($value) && abs($value) >= PHP_INT_MAX); 94 | } 95 | 96 | /** 97 | * Returns true if a value is a json number 98 | * 99 | * @param mixed $value 100 | */ 101 | protected function isNumber($value): bool 102 | { 103 | return is_float($value) || is_integer($value); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Schema/Resolver.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema; 13 | 14 | use \stdClass; 15 | 16 | class Resolver 17 | { 18 | protected Cache $cache; 19 | 20 | public function __construct(stdClass $schema) 21 | { 22 | $this->cache = new Cache($schema); 23 | } 24 | 25 | public function getRef(string $ref): stdClass 26 | { 27 | return $this->cache->resolveRef($ref); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Schema/Store.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema; 13 | 14 | use \stdClass; 15 | 16 | class Store 17 | { 18 | /** @var array> */ 19 | protected array $data = []; 20 | 21 | public function add(string $doc, string $path, stdClass $schema): bool 22 | { 23 | if (!$this->matchPath($doc, $path, $partPath)) { 24 | $this->data[$doc][$path] = $schema; 25 | return true; 26 | } 27 | 28 | return $path !== $partPath; 29 | } 30 | 31 | /** 32 | * Adds the schema to the root and returns false if it already existed 33 | */ 34 | public function addRoot(string $doc, stdClass $schema): bool 35 | { 36 | $found = $this->hasRoot($doc); 37 | 38 | if (!$found) { 39 | $this->data[$doc] = ['#' => $schema]; 40 | } 41 | 42 | return !$found; 43 | } 44 | 45 | public function hasRoot(string $doc): bool 46 | { 47 | return isset($this->data[$doc]); 48 | } 49 | 50 | /** 51 | * Returns a schema if found, otherwise sets $data 52 | * 53 | * @param string $path 54 | * @param mixed $data Set by method 55 | */ 56 | public function get(string $doc, string &$path, &$data): ?stdClass 57 | { 58 | if (isset($this->data[$doc][$path])) { 59 | return $this->data[$doc][$path]; 60 | } 61 | 62 | $data = isset($this->data[$doc]) ? $this->getData($doc, $path) : null; 63 | 64 | return null; 65 | } 66 | 67 | /** 68 | * @return mixed 69 | */ 70 | protected function getData(string $doc, string &$path) 71 | { 72 | if (!$this->matchPath($doc, $path, $partPath)) { 73 | return $this->data[$doc]['#']; 74 | } else { 75 | $path = substr($path, strlen($partPath)); 76 | return $this->data[$doc][$partPath]; 77 | } 78 | } 79 | 80 | protected function matchPath(string $doc, string $path, ?string &$partPath): bool 81 | { 82 | foreach ($this->data[$doc] as $key => $dummy) { 83 | if (0 === strpos($path.'/', $key.'/')) { 84 | $partPath = $key; 85 | return true; 86 | } 87 | } 88 | 89 | return false; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Schema/ValidationException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema; 13 | 14 | class ValidationException extends \Exception 15 | { 16 | } 17 | -------------------------------------------------------------------------------- /src/Schema/Validator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks\Schema; 13 | 14 | use stdClass; 15 | 16 | use JohnStevenson\JsonWorks\BaseDocument; 17 | use JohnStevenson\JsonWorks\Loader; 18 | use JohnStevenson\JsonWorks\Helpers\Utils; 19 | use JohnStevenson\JsonWorks\Schema\Resolver; 20 | use JohnStevenson\JsonWorks\Schema\ValidationException; 21 | use JohnStevenson\JsonWorks\Schema\Constraint\Manager; 22 | 23 | class Validator 24 | { 25 | /** @var array */ 26 | protected array $errors = []; 27 | protected stdClass $schema; 28 | protected bool $stopOnError = false; 29 | 30 | protected Loader $loader; 31 | protected Resolver $resolver; 32 | 33 | public function __construct(stdClass $schema) 34 | { 35 | $this->loader = new Loader; 36 | $this->schema = $this->loader->getSchema($schema); 37 | $this->resolver = new Resolver($this->schema); 38 | } 39 | 40 | /** 41 | * @param mixed $data 42 | */ 43 | public function check($data): bool 44 | { 45 | $data = $this->getData($data); 46 | $manager = new Manager($this->resolver, $this->stopOnError); 47 | 48 | try { 49 | 50 | $manager->validate($data, $this->schema); 51 | } catch (ValidationException $e) { 52 | // The exception is thrown to stop validation 53 | } 54 | 55 | $this->errors = $manager->errors; 56 | 57 | return Utils::arrayIsEmpty($this->errors); 58 | } 59 | 60 | /** 61 | * @return array 62 | */ 63 | public function getErrors(): array 64 | { 65 | return $this->errors; 66 | } 67 | 68 | public function getLastError(): string 69 | { 70 | return $this->errors[0] ?? ''; 71 | } 72 | 73 | public function setStopOnError(bool $value): void 74 | { 75 | $this->stopOnError = $value; 76 | } 77 | 78 | /** 79 | * @param mixed $data 80 | * @return mixed 81 | */ 82 | protected function getData($data) 83 | { 84 | if ($data instanceof BaseDocument) { 85 | return $data->getData(); 86 | } 87 | 88 | return $this->loader->getData($data); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Tokenizer.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace JohnStevenson\JsonWorks; 13 | 14 | use JohnStevenson\JsonWorks\Helpers\Utils; 15 | 16 | /** 17 | * A class for creating and manipulating JSON Pointers. 18 | * @api 19 | */ 20 | class Tokenizer 21 | { 22 | /** 23 | * Adds a token to an existing JSON Pointer 24 | * 25 | * @param string $pointer The existing JSON Pointer 26 | * @param string $token The token to add 27 | * @return string The new JSON Pointer 28 | */ 29 | public function add(string $pointer, string $token): string 30 | { 31 | $encoded = $this->encodeToken($token); 32 | 33 | return $pointer.'/'.$encoded; 34 | } 35 | 36 | /** 37 | * Splits a JSON Pointer into individual tokens 38 | * 39 | * @param string $pointer The JSON Pointer to split 40 | * @param array $tokens Placeholder for decoded tokens 41 | * @return bool If the pointer is valid 42 | */ 43 | public function decode(string $pointer, &$tokens): bool 44 | { 45 | if (Utils::stringNotEmpty($pointer) && $pointer[0] !== '/') { 46 | return false; 47 | } 48 | 49 | $tokens = explode('/', $pointer); 50 | array_shift($tokens); 51 | 52 | foreach ($tokens as $key => $value) { 53 | $tokens[$key] = $this->processToken($value); 54 | } 55 | 56 | return true; 57 | } 58 | 59 | /** 60 | * Creates a JSON Pointer from a string or an array of tokens 61 | * 62 | * @param string|array $tokens 63 | * @return string The encoded JSON Pointer 64 | */ 65 | public function encode($tokens): string 66 | { 67 | $result = ''; 68 | foreach ((array) $tokens as $index => $value) { 69 | // skip empty first token 70 | if ($index === 0 && Utils::stringIsEmpty($value)) { 71 | continue; 72 | } 73 | $result = $this->add($result, $value); 74 | } 75 | 76 | return $result; 77 | } 78 | 79 | /** 80 | * Encodes a JSON Pointer token 81 | * 82 | * @param string $token 83 | * @return string The encoded JSON Pointer 84 | */ 85 | public function encodeToken(string $token): string 86 | { 87 | return str_replace('/', '~1', str_replace('~', '~0', strval($token))); 88 | } 89 | 90 | /** 91 | * Returns a correctly formatted token 92 | * 93 | * @param string $token 94 | * @return string 95 | */ 96 | private function processToken(string $token): string 97 | { 98 | return str_replace('~0', '~', str_replace('~1', '/', $token)); 99 | } 100 | } 101 | --------------------------------------------------------------------------------