├── .composer-auth.json ├── .gitignore ├── .styleci.yml ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── appveyor.yml ├── composer.json ├── phpunit.xml.dist ├── res └── meta-schema.json ├── src ├── Conversion │ ├── ConversionFailedException.php │ └── JsonConverter.php ├── DecodingFailedException.php ├── EncodingFailedException.php ├── FileNotFoundException.php ├── IOException.php ├── InvalidSchemaException.php ├── JsonDecoder.php ├── JsonEncoder.php ├── JsonError.php ├── JsonValidator.php ├── Migration │ ├── JsonMigration.php │ ├── MigratingConverter.php │ ├── MigrationFailedException.php │ ├── MigrationManager.php │ └── UnsupportedVersionException.php ├── UriRetriever │ └── LocalUriRetriever.php ├── Validation │ └── ValidatingConverter.php ├── ValidationFailedException.php └── Versioning │ ├── CannotParseVersionException.php │ ├── CannotUpdateVersionException.php │ ├── JsonVersioner.php │ ├── SchemaUriVersioner.php │ └── VersionFieldVersioner.php └── tests ├── Fixtures ├── box.json.dist ├── invalid.json ├── schema-external-refs.json ├── schema-invalid.json ├── schema-no-object.json ├── schema-refs.json ├── schema.json ├── schema.phar ├── valid.json └── win-1258.json ├── JsonDecoderTest.php ├── JsonEncoderTest.php ├── JsonValidatorTest.php ├── Migration ├── MigratingConverterTest.php └── MigrationManagerTest.php ├── UriRetriever ├── Fixtures │ └── schema-1.0.json └── LocalUriRetrieverTest.php ├── Validation └── ValidatingConverterTest.php └── Versioning ├── SchemaUriVersionerTest.php └── VersionFieldVersionerTest.php /.composer-auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "github-oauth": { 3 | "github.com": "PLEASE DO NOT USE THIS TOKEN IN YOUR OWN PROJECTS/FORKS", 4 | "github.com": "This token is reserved for testing the webmozart/* repositories", 5 | "github.com": "a9debbffdd953ee9b3b82dbc3b807cde2086bb86" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: symfony 2 | 3 | enabled: 4 | - ordered_use 5 | - strict 6 | 7 | disabled: 8 | - empty_return 9 | - phpdoc_annotation_without_dot # This is still buggy: https://github.com/symfony/symfony/pull/19198 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | cache: 10 | directories: 11 | - $HOME/.composer/cache/files 12 | 13 | matrix: 14 | include: 15 | - php: 5.3 16 | - php: 5.4 17 | - php: 5.5 18 | - php: 5.6 19 | - php: hhvm 20 | - php: nightly 21 | - php: 7.0 22 | env: COVERAGE=yes 23 | - php: 7.0 24 | env: COMPOSER_FLAGS='--prefer-lowest --prefer-stable' 25 | allow_failures: 26 | - php: hhvm 27 | - php: nightly 28 | fast_finish: true 29 | 30 | before_install: 31 | - if [[ $TRAVIS_PHP_VERSION != hhvm && $COVERAGE != yes ]]; then phpenv config-rm xdebug.ini; fi; 32 | - if [[ $TRAVIS_REPO_SLUG = webmozart/json ]]; then cp .composer-auth.json ~/.composer/auth.json; fi; 33 | - composer self-update 34 | 35 | install: composer update $COMPOSER_FLAGS --prefer-dist --no-interaction 36 | 37 | script: if [[ $COVERAGE = yes ]]; then vendor/bin/phpunit --verbose --coverage-clover=coverage.clover; else vendor/bin/phpunit --verbose; fi 38 | 39 | after_script: if [[ $COVERAGE = yes ]]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | * 1.3.0 (@release_date@) 5 | 6 | * added `JsonConverter` and `ConversionException` 7 | * added `MigratingConverter` to migrate JSON objects between different versions 8 | * added `ValidatingConverter` to validate converted JSON against schemas 9 | * added `JsonVersioner` and implementations `VersionFieldVersioner` and 10 | `SchemaUriVersioner` 11 | * added support for `$ref` to external schema 12 | * added support for validation against `$schema` property 13 | * added `LocalUriRetriever` 14 | * added support for empty properties before PHP 7.1 15 | 16 | * 1.2.2 (2016-01-14) 17 | 18 | * fixed loading of schemas from PHARs 19 | 20 | * 1.2.1 (2016-01-14) 21 | 22 | * bumped justinrainbow/json-schema to 1.6 to fix "pattern-properties" with 23 | slashes 24 | 25 | * 1.2.0 (2015-01-02) 26 | 27 | * added support for `$ref` in schemas 28 | 29 | * 1.1.1 (2015-12-28) 30 | 31 | * fixed PHP 7 compatibility 32 | 33 | * 1.1.0 (2015-12-11) 34 | 35 | * added `IOException` and better error handling in `JsonEncoder::encodeFile()` 36 | and `JsonDecoder::decodeFile()` 37 | * `JsonEncoder::encodeFile()` now creates missing directories on demand 38 | * `JsonEncoder` now throws an exception on all PHP versions when binary values 39 | are passed 40 | * added support for disabled slash escaping on PHP below 5.4 41 | 42 | * 1.0.2 (2015-08-11) 43 | 44 | * fixed decoding of `null` 45 | 46 | * 1.0.1 (2015-06-04) 47 | 48 | * fixed detection of the JSONC library in `JsonDecoder::decodeJson()` 49 | 50 | * 1.0.0 (2015-03-19) 51 | 52 | * flipped `$data` and `$file` arguments of `JsonEncoder::encodeFile()` 53 | 54 | * 1.0.0-beta (2015-01-12) 55 | 56 | * renamed `SchemaException` to `InvalidSchemaException` 57 | * changed `JsonValidator::validate()` to return the discovered errors instead 58 | of throwing an exception 59 | 60 | * 1.0.0-alpha1 (2014-12-03) 61 | 62 | * first alpha release 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Bernhard Schussek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Webmozart JSON 2 | ============== 3 | 4 | [![Build Status](https://travis-ci.org/webmozart/json.svg?branch=master)](https://travis-ci.org/webmozart/json) 5 | [![Build status](https://ci.appveyor.com/api/projects/status/icccqc0aq1molo96/branch/master?svg=true)](https://ci.appveyor.com/project/webmozart/json/branch/master) 6 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/webmozart/json/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/webmozart/json/?branch=master) 7 | [![Latest Stable Version](https://poser.pugx.org/webmozart/json/v/stable.svg)](https://packagist.org/packages/webmozart/json) 8 | [![Total Downloads](https://poser.pugx.org/webmozart/json/downloads.svg)](https://packagist.org/packages/webmozart/json) 9 | [![Dependency Status](https://www.versioneye.com/php/webmozart:json/1.2.2/badge.svg)](https://www.versioneye.com/php/webmozart:json/1.2.2) 10 | 11 | Latest release: [1.2.2](https://packagist.org/packages/webmozart/json#1.2.2) 12 | 13 | A robust wrapper for `json_encode()`/`json_decode()` that normalizes their 14 | behavior across PHP versions, throws meaningful exceptions and supports schema 15 | validation by default. 16 | 17 | Installation 18 | ------------ 19 | 20 | Use [Composer] to install the package: 21 | 22 | ~~~ 23 | $ composer require webmozart/json 24 | ~~~ 25 | 26 | Encoding 27 | -------- 28 | 29 | Use the [`JsonEncoder`] to encode data as JSON: 30 | 31 | ~~~php 32 | use Webmozart\Json\JsonEncoder; 33 | 34 | $encoder = new JsonEncoder(); 35 | 36 | // Store JSON in string 37 | $string = $encoder->encode($data); 38 | 39 | // Store JSON in file 40 | $encoder->encodeFile($data, '/path/to/file.json'); 41 | ~~~ 42 | 43 | By default, the [JSON schema] stored in the `$schema` property of the JSON 44 | document is used to validate the file. You can also pass the path to the schema 45 | in the last optional argument of both methods: 46 | 47 | ~~~php 48 | use Webmozart\Json\ValidationFailedException; 49 | 50 | try { 51 | $string = $encoder->encode($data, '/path/to/schema.json'); 52 | } catch (ValidationFailedException $e) { 53 | // data did not match schema 54 | } 55 | ~~~ 56 | 57 | Decoding 58 | -------- 59 | 60 | Use the [`JsonDecoder`] to decode a JSON string/file: 61 | 62 | ~~~php 63 | use Webmozart\Json\JsonDecoder; 64 | 65 | $decoder = new JsonDecoder(); 66 | 67 | // Read JSON string 68 | $data = $decoder->decode($string); 69 | 70 | // Read JSON file 71 | $data = $decoder->decodeFile('/path/to/file.json'); 72 | ~~~ 73 | 74 | Like [`JsonEncoder`], the decoder accepts the path to a JSON schema in the last 75 | optional argument of its methods: 76 | 77 | ~~~php 78 | use Webmozart\Json\ValidationFailedException; 79 | 80 | try { 81 | $data = $decoder->decodeFile('/path/to/file.json', '/path/to/schema.json'); 82 | } catch (ValidationFailedException $e) { 83 | // data did not match schema 84 | } 85 | ~~~ 86 | 87 | Validation 88 | ---------- 89 | 90 | Sometimes it is necessary to separate the steps of encoding/decoding JSON data 91 | and validating it against a schema. In this case, you can omit the schema 92 | argument during encoding/decoding and use the [`JsonValidator`] to validate the 93 | data manually later on: 94 | 95 | ~~~php 96 | use Webmozart\Json\JsonDecoder; 97 | use Webmozart\Json\JsonValidator; 98 | use Webmozart\Json\ValidationFailedException; 99 | 100 | $decoder = new JsonDecoder(); 101 | $validator = new JsonValidator(); 102 | 103 | $data = $decoder->decodeFile('/path/to/file.json'); 104 | 105 | // process $data... 106 | 107 | $errors = $validator->validate($data, '/path/to/schema.json'); 108 | 109 | if (count($errors) > 0) { 110 | // data did not match schema 111 | } 112 | ~~~ 113 | 114 | Note: This does not work if you use the `$schema` property to set the schema 115 | (see next section). If that property is set, the schema is always used for 116 | validation during encoding and decoding. 117 | 118 | Schemas 119 | ------- 120 | 121 | You are encouraged to store the schema of your JSON documents in the 122 | `$schema` property: 123 | 124 | ~~~json 125 | { 126 | "$schema": "http://example.org/schemas/1.0/schema" 127 | } 128 | ~~~ 129 | 130 | The utilities in this package will load the schema from the URL and use it for 131 | validating the document. Obviously, this has a hit on performance and depends on 132 | the availability of the server and an internet connection. Hence you are 133 | encouraged to ship the schema with your package. Use the [`LocalUriRetriever`] 134 | to map the URL to your local schema file: 135 | 136 | ~~~php 137 | $uriRetriever = new UriRetriever(); 138 | $uriRetriever->setUriRetriever(new LocalUriRetriever( 139 | // base directory 140 | __DIR__.'/../res/schemas', 141 | // list of schema mappings 142 | array( 143 | 'http://example.org/schemas/1.0/schema' => 'schema-1.0.json', 144 | ) 145 | )); 146 | 147 | $validator = new JsonValidator(null, $uriRetriever); 148 | $encoder = new JsonEncoder($validator); 149 | $decoder = new JsonDecoder($validator); 150 | 151 | // ... 152 | ~~~ 153 | 154 | Conversion 155 | ---------- 156 | 157 | You can implement [`JsonConverter`] to encapsulate the conversion of objects 158 | from and to JSON structures in a single class: 159 | 160 | ~~~php 161 | use stdClass; 162 | use Webmozart\Json\Conversion\JsonConverter; 163 | 164 | class ConfigFileJsonConverter implements JsonConverter 165 | { 166 | const SCHEMA = 'http://example.org/schemas/1.0/schema'; 167 | 168 | public function toJson($configFile, array $options = array()) 169 | { 170 | $jsonData = new stdClass(); 171 | $jsonData->{'$schema'} = self::SCHEMA; 172 | 173 | if (null !== $configFile->getApplicationName()) { 174 | $jsonData->application = $configFile->getApplicationName(); 175 | } 176 | 177 | // ... 178 | 179 | return $jsonData; 180 | } 181 | 182 | public function fromJson($jsonData, array $options = array()) 183 | { 184 | $configFile = new ConfigFile(); 185 | 186 | if (isset($jsonData->application)) { 187 | $configFile->setApplicationName($jsonData->application); 188 | } 189 | 190 | // ... 191 | 192 | return $configFile; 193 | } 194 | } 195 | ~~~ 196 | 197 | Loading and dumping `ConfigFile` objects is very simple now: 198 | 199 | ~~~php 200 | $converter = new ConfigFileJsonConverter(); 201 | 202 | // Load config.json as ConfigFile object 203 | $jsonData = $decoder->decodeFile('/path/to/config.json'); 204 | $configFile = $converter->fromJson($jsonData); 205 | 206 | // Save ConfigFile object as config.json 207 | $jsonData = $converter->toJson($configFile); 208 | $encoder->encodeFile($jsonData, '/path/to/config.json'); 209 | ~~~ 210 | 211 | You can automate the schema validation of your `ConfigFile` by wrapping the 212 | converter in a `ValidatingConverter`: 213 | 214 | ~~~php 215 | use Webmozart\Json\Validation\ValidatingConverter; 216 | 217 | $converter = new ValidatingConverter(new ConfigFileJsonConverter()); 218 | ~~~ 219 | 220 | You can also validate against an explicit schema by passing the schema to the 221 | `ValidatingConverter`: 222 | 223 | ~~~php 224 | use Webmozart\Json\Validation\ValidatingConverter; 225 | 226 | $converter = new ValidatingConverter( 227 | new ConfigFileJsonConverter(), 228 | __DIR__.'/../res/schema/config-schema.json' 229 | ); 230 | ~~~ 231 | 232 | Versioning and Migration 233 | ------------------------ 234 | 235 | When you continuously develop an application, you will enter the situation that 236 | you need to change your JSON schemas. Updating JSON files to match their 237 | changed schemas can be challenging and time consuming. This package supports a 238 | versioning mechanism to automate this migration. 239 | 240 | Imagine `config.json` files in three different versions: 1.0, 2.0 and 3.0. 241 | The name of a key changed between those versions: 242 | 243 | config.json (version 1.0) 244 | 245 | ~~~json 246 | { 247 | "$schema": "http://example.org/schemas/1.0/schema", 248 | "application": "Hello world!" 249 | } 250 | ~~~ 251 | 252 | config.json (version 2.0) 253 | 254 | ~~~json 255 | { 256 | "$schema": "http://example.org/schemas/2.0/schema", 257 | "application.name": "Hello world!" 258 | } 259 | ~~~ 260 | 261 | config.json (version 3.0) 262 | 263 | ~~~json 264 | { 265 | "$schema": "http://example.org/schemas/3.0/schema", 266 | "application": { 267 | "name": "Hello world!" 268 | } 269 | } 270 | ~~~ 271 | 272 | You can support files in any of these versions by implementing: 273 | 274 | 1. A converter compatible with the latest version (e.g. 3.0) 275 | 276 | 2. Migrations that migrate older versions to newer versions (e.g. 1.0 to 277 | 2.0 and 2.0 to 3.0. 278 | 279 | Let's look at an example of a `ConfigFileJsonConverter` for version 3.0: 280 | 281 | ~~~php 282 | use stdClass; 283 | use Webmozart\Json\Conversion\JsonConverter; 284 | 285 | class ConfigFileJsonConverter implements JsonConverter 286 | { 287 | const SCHEMA = 'http://example.org/schemas/3.0/schema'; 288 | 289 | public function toJson($configFile, array $options = array()) 290 | { 291 | $jsonData = new stdClass(); 292 | $jsonData->{'$schema'} = self::SCHEMA; 293 | 294 | if (null !== $configFile->getApplicationName()) { 295 | $jsonData->application = new stdClass(); 296 | $jsonData->application->name = $configFile->getApplicationName(); 297 | } 298 | 299 | // ... 300 | 301 | return $jsonData; 302 | } 303 | 304 | public function fromJson($jsonData, array $options = array()) 305 | { 306 | $configFile = new ConfigFile(); 307 | 308 | if (isset($jsonData->application->name)) { 309 | $configFile->setApplicationName($jsonData->application->name); 310 | } 311 | 312 | // ... 313 | 314 | return $configFile; 315 | } 316 | } 317 | ~~~ 318 | 319 | This converter can be used as described in the previous section. However, 320 | it can only be used with `config.json` files in version 3.0. 321 | 322 | We can add support for older files by implementing the [`JsonMigration`] 323 | interface. This interface contains four methods: 324 | 325 | * `getSourceVersion()`: returns the source version of the migration 326 | * `getTargetVersion()`: returns the target version of the migration 327 | * `up(stdClass $jsonData)`: migrates from the source to the target version 328 | * `down(stdClass $jsonData)`: migrates from the target to the source version 329 | 330 | ~~~php 331 | use Webmozart\Json\Migration\JsonMigration; 332 | 333 | class ConfigFileJson20To30Migration implements JsonMigration 334 | { 335 | const SOURCE_SCHEMA = 'http://example.org/schemas/2.0/schema'; 336 | 337 | const TARGET_SCHEMA = 'http://example.org/schemas/3.0/schema'; 338 | 339 | public function getSourceVersion() 340 | { 341 | return '2.0'; 342 | } 343 | 344 | public function getTargetVersion() 345 | { 346 | return '3.0'; 347 | } 348 | 349 | public function up(stdClass $jsonData) 350 | { 351 | $jsonData->{'$schema'} = self::TARGET_SCHEMA; 352 | 353 | if (isset($jsonData->{'application.name'})) { 354 | $jsonData->application = new stdClass(); 355 | $jsonData->application->name = $jsonData->{'application.name'}; 356 | 357 | unset($jsonData->{'application.name'}); 358 | ) 359 | } 360 | 361 | public function down(stdClass $jsonData) 362 | { 363 | $jsonData->{'$schema'} = self::SOURCE_SCHEMA; 364 | 365 | if (isset($jsonData->application->name)) { 366 | $jsonData->{'application.name'} = $jsonData->application->name; 367 | 368 | unset($jsonData->application); 369 | ) 370 | } 371 | } 372 | ~~~ 373 | 374 | With a list of such migrations, we can create a `MigratingConverter` that 375 | decorates our `ConfigFileJsonConverter`: 376 | 377 | ~~~php 378 | use Webmozart\Json\Migration\MigratingConverter; 379 | use Webmozart\Json\Migration\MigrationManager; 380 | 381 | // Written for version 3.0 382 | $converter = new ConfigFileJsonConverter(); 383 | 384 | // Support for older versions. The order of migrations does not matter. 385 | $migrationManager = new MigrationManager(array( 386 | new ConfigFileJson10To20Migration(), 387 | new ConfigFileJson20To30Migration(), 388 | )); 389 | 390 | // Decorate the converter 391 | $converter = new MigratingConverter($converter, $migrationManager); 392 | ~~~ 393 | 394 | The resulting converter is able to load and dump JSON files in any of the 395 | versions 1.0, 2.0 and 3.0. 396 | 397 | ~~~php 398 | // Loads a file in version 1.0, 2.0 or 3.0 399 | $jsonData = $decoder->decodeFile('/path/to/config.json'); 400 | $configFile = $converter->fromJson($jsonData); 401 | 402 | // Writes the file in the latest version by default (3.0) 403 | $jsonData = $converter->toJson($configFile); 404 | $encoder->encodeFile($jsonData, '/path/to/config.json'); 405 | 406 | // Writes the file in a specific version 407 | $jsonData = $converter->toJson($configFile, array( 408 | 'targetVersion' => '2.0', 409 | )); 410 | $encoder->encodeFile($jsonData, '/path/to/config.json'); 411 | ~~~ 412 | 413 | ### Validation of Different Versions 414 | 415 | If you want to add schema validation, wrap your encoder into a 416 | `ValidatingConverter`. You can wrap both the inner and the outer converter 417 | to make sure that both the JSON before and after running the migrations complies 418 | to the corresponding schemas. 419 | 420 | ~~~php 421 | // Written for version 3.0 422 | $converter = new ConfigFileJsonConverter(); 423 | 424 | // Decorate to validate against the schema at version 3.0 425 | $converter = new ValidatingConverter($converter); 426 | 427 | // Decorate to support different versions 428 | $converter = new MigratingConverter($converter, $migrationManager); 429 | 430 | // Decorate to validate against the old schema 431 | $converter = new ValidatingConverter($converter); 432 | ~~~ 433 | 434 | If you store the version in a `version` field (see below) and want to use a 435 | custom schema depending on that version, you can pass schema paths or closures 436 | for resolving the schema paths: 437 | 438 | ~~~php 439 | // Written for version 3.0 440 | $converter = new ConfigFileJsonConverter(); 441 | 442 | // Decorate to validate against the schema at version 3.0 443 | $converter = new ValidatingConverter($converter, __DIR__.'/../res/schema/config-schema-3.0.json'); 444 | 445 | // Decorate to support different versions 446 | $converter = new MigratingConverter($converter, $migrationManager); 447 | 448 | // Decorate to validate against the old schema 449 | $converter = new ValidatingConverter($converter, function ($jsonData) { 450 | return __DIR__.'/../res/schema/config-schema-'.$jsonData->version.'.json' 451 | }); 452 | ~~~ 453 | 454 | ### Using Custom Schema Versioning 455 | 456 | By default, the version of the schema is stored in the schema name: 457 | 458 | ~~~json 459 | { 460 | "$schema": "http://example.com/schemas/1.0/my-schema" 461 | } 462 | ~~~ 463 | 464 | The version must be enclosed by slashes. Appending the version to the schema, 465 | for example, won't work: 466 | 467 | ~~~json 468 | { 469 | "$schema": "http://example.com/schemas/my-schema-1.0" 470 | } 471 | ~~~ 472 | 473 | You can however customize the format of the schema URI by creating a 474 | `SchemaUriVersioner` with a custom regular expression: 475 | 476 | ~~~php 477 | use Webmozart\Json\Versioning\SchemaUriVersioner; 478 | 479 | $versioner = new SchemaUriVersioner('~(?<=-)\d+\.\d+(?=$)~'); 480 | 481 | $migrationManager = new MigrationManager(array( 482 | // migrations... 483 | ), $versioner); 484 | 485 | // ... 486 | ~~~ 487 | 488 | The regular expression must match the version only. Make sure to wrap 489 | characters before and after the version in look-around assertions (`(?<=...)`, 490 | `(?=...)`). 491 | 492 | ### Storing the Version in a Field 493 | 494 | Instead of storing the version in the schema URI, you could also store it in 495 | a separate field. For example, the field "version": 496 | 497 | ~~~json 498 | { 499 | "version": "1.0" 500 | } 501 | ~~~ 502 | 503 | This use case is supported by the `VersionFieldVersioner` class: 504 | 505 | ~~~php 506 | use Webmozart\Json\Versioning\VersionFieldVersioner; 507 | 508 | $versioner = new VersionFieldVersioner(); 509 | 510 | $migrationManager = new MigrationManager(array( 511 | // migrations... 512 | ), $versioner); 513 | 514 | // ... 515 | ~~~ 516 | 517 | The constructor of `VersionFieldVersioner` optionally accepts a custom field 518 | name used to store the version. The default field name is "version". 519 | 520 | Authors 521 | ------- 522 | 523 | * [Bernhard Schussek] a.k.a. [@webmozart] 524 | * [The Community Contributors] 525 | 526 | Contribute 527 | ---------- 528 | 529 | Contributions to the package are always welcome! 530 | 531 | * Report any bugs or issues you find on the [issue tracker]. 532 | * You can grab the source code at the package's [Git repository]. 533 | 534 | Support 535 | ------- 536 | 537 | If you are having problems, send a mail to bschussek@gmail.com or shout out to 538 | [@webmozart] on Twitter. 539 | 540 | License 541 | ------- 542 | 543 | All contents of this package are licensed under the [MIT license]. 544 | 545 | [Composer]: https://getcomposer.org 546 | [Bernhard Schussek]: http://webmozarts.com 547 | [The Community Contributors]: https://github.com/webmozart/json/graphs/contributors 548 | [issue tracker]: https://github.com/webmozart/json/issues 549 | [Git repository]: https://github.com/webmozart/json 550 | [@webmozart]: https://twitter.com/webmozart 551 | [MIT license]: LICENSE 552 | [JSON schema]: http://json-schema.org 553 | [`JsonEncoder`]: src/JsonEncoder.php 554 | [`JsonDecoder`]: src/JsonDecoder.php 555 | [`JsonValidator`]: src/JsonValidator.php 556 | [`JsonConverter`]: src/Conversion/JsonConverter.php 557 | [`JsonMigration`]: src/Migration/JsonMigration.php 558 | [`LocalUriRetriever`]: src/UriRetriever/LocalUriRetriever.php 559 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | platform: x86 3 | clone_folder: c:\projects\webmozart\json 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | cache: 10 | - c:\php -> appveyor.yml 11 | 12 | init: 13 | - SET PATH=c:\php;%PATH% 14 | - SET COMPOSER_NO_INTERACTION=1 15 | - SET PHP=1 16 | 17 | install: 18 | - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php) 19 | - cd c:\php 20 | - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-7.0.0-nts-Win32-VC14-x86.zip 21 | - IF %PHP%==1 7z x php-7.0.0-nts-Win32-VC14-x86.zip -y >nul 22 | - IF %PHP%==1 del /Q *.zip 23 | - IF %PHP%==1 echo @php %%~dp0composer.phar %%* > composer.bat 24 | - IF %PHP%==1 copy /Y php.ini-development php.ini 25 | - IF %PHP%==1 echo max_execution_time=1200 >> php.ini 26 | - IF %PHP%==1 echo date.timezone="UTC" >> php.ini 27 | - IF %PHP%==1 echo extension_dir=ext >> php.ini 28 | - IF %PHP%==1 echo extension=php_curl.dll >> php.ini 29 | - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini 30 | - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini 31 | - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini 32 | - appveyor DownloadFile https://getcomposer.org/composer.phar 33 | - cd c:\projects\webmozart\json 34 | - mkdir %APPDATA%\Composer 35 | - IF %APPVEYOR_REPO_NAME%==webmozart/json copy /Y .composer-auth.json %APPDATA%\Composer\auth.json 36 | - composer update --prefer-dist --no-progress --ansi 37 | 38 | test_script: 39 | - cd c:\projects\webmozart\json 40 | - vendor\bin\phpunit.bat --verbose 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webmozart/json", 3 | "description": "A robust JSON decoder/encoder with support for schema validation.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Bernhard Schussek", 8 | "email": "bschussek@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^5.3.3|^7.0", 13 | "justinrainbow/json-schema": "^2.0", 14 | "seld/jsonlint": "^1.0", 15 | "webmozart/assert": "^1.0", 16 | "webmozart/path-util": "^2.3" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^4.6", 20 | "sebastian/version": "^1.0.1", 21 | "symfony/filesystem": "^2.5" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "Webmozart\\Json\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "Webmozart\\Json\\Tests\\": "tests/" 31 | } 32 | }, 33 | "extra": { 34 | "branch-alias": { 35 | "dev-master": "1.3-dev" 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | 12 | 13 | ./src/ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /res/meta-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://json-schema.org/draft-04/schema#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "description": "Core schema meta-schema", 5 | "definitions": { 6 | "schemaArray": { 7 | "type": "array", 8 | "minItems": 1, 9 | "items": { "$ref": "#" } 10 | }, 11 | "positiveInteger": { 12 | "type": "integer", 13 | "minimum": 0 14 | }, 15 | "positiveIntegerDefault0": { 16 | "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] 17 | }, 18 | "simpleTypes": { 19 | "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ] 20 | }, 21 | "stringArray": { 22 | "type": "array", 23 | "items": { "type": "string" }, 24 | "minItems": 1, 25 | "uniqueItems": true 26 | } 27 | }, 28 | "type": "object", 29 | "properties": { 30 | "id": { 31 | "type": "string" 32 | }, 33 | "$schema": { 34 | "type": "string", 35 | "format": "uri" 36 | }, 37 | "title": { 38 | "type": "string" 39 | }, 40 | "description": { 41 | "type": "string" 42 | }, 43 | "default": {}, 44 | "multipleOf": { 45 | "type": "number", 46 | "minimum": 0, 47 | "exclusiveMinimum": true 48 | }, 49 | "maximum": { 50 | "type": "number" 51 | }, 52 | "exclusiveMaximum": { 53 | "type": "boolean", 54 | "default": false 55 | }, 56 | "minimum": { 57 | "type": "number" 58 | }, 59 | "exclusiveMinimum": { 60 | "type": "boolean", 61 | "default": false 62 | }, 63 | "maxLength": { "$ref": "#/definitions/positiveInteger" }, 64 | "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, 65 | "pattern": { 66 | "type": "string", 67 | "format": "regex" 68 | }, 69 | "additionalItems": { 70 | "anyOf": [ 71 | { "type": "boolean" }, 72 | { "$ref": "#" } 73 | ], 74 | "default": {} 75 | }, 76 | "items": { 77 | "anyOf": [ 78 | { "$ref": "#" }, 79 | { "$ref": "#/definitions/schemaArray" } 80 | ], 81 | "default": {} 82 | }, 83 | "maxItems": { "$ref": "#/definitions/positiveInteger" }, 84 | "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, 85 | "uniqueItems": { 86 | "type": "boolean", 87 | "default": false 88 | }, 89 | "maxProperties": { "$ref": "#/definitions/positiveInteger" }, 90 | "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" }, 91 | "required": { "$ref": "#/definitions/stringArray" }, 92 | "additionalProperties": { 93 | "anyOf": [ 94 | { "type": "boolean" }, 95 | { "$ref": "#" } 96 | ], 97 | "default": {} 98 | }, 99 | "definitions": { 100 | "type": "object", 101 | "additionalProperties": { "$ref": "#" }, 102 | "default": {} 103 | }, 104 | "properties": { 105 | "type": "object", 106 | "additionalProperties": { "$ref": "#" }, 107 | "default": {} 108 | }, 109 | "patternProperties": { 110 | "type": "object", 111 | "additionalProperties": { "$ref": "#" }, 112 | "default": {} 113 | }, 114 | "dependencies": { 115 | "type": "object", 116 | "additionalProperties": { 117 | "anyOf": [ 118 | { "$ref": "#" }, 119 | { "$ref": "#/definitions/stringArray" } 120 | ] 121 | } 122 | }, 123 | "enum": { 124 | "type": "array", 125 | "minItems": 1, 126 | "uniqueItems": true 127 | }, 128 | "type": { 129 | "anyOf": [ 130 | { "$ref": "#/definitions/simpleTypes" }, 131 | { 132 | "type": "array", 133 | "items": { "$ref": "#/definitions/simpleTypes" }, 134 | "minItems": 1, 135 | "uniqueItems": true 136 | } 137 | ] 138 | }, 139 | "allOf": { "$ref": "#/definitions/schemaArray" }, 140 | "anyOf": { "$ref": "#/definitions/schemaArray" }, 141 | "oneOf": { "$ref": "#/definitions/schemaArray" }, 142 | "not": { "$ref": "#" } 143 | }, 144 | "dependencies": { 145 | "exclusiveMaximum": [ "maximum" ], 146 | "exclusiveMinimum": [ "minimum" ] 147 | }, 148 | "default": {} 149 | } 150 | -------------------------------------------------------------------------------- /src/Conversion/ConversionFailedException.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 Webmozart\Json\Conversion; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown when the conversion of data to/from JSON fails. 18 | * 19 | * @since 1.3 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class ConversionFailedException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/Conversion/JsonConverter.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 Webmozart\Json\Conversion; 13 | 14 | /** 15 | * Converts data to and from JSON. 16 | * 17 | * @since 1.3 18 | * 19 | * @author Bernhard Schussek 20 | */ 21 | interface JsonConverter 22 | { 23 | /** 24 | * Converts an implementation-specific data structure to JSON. 25 | * 26 | * @param mixed $data The data to convert 27 | * @param array $options Additional implementation-specific conversion options 28 | * 29 | * @return mixed The JSON data. Pass this data to a {@link JsonEncoder} to 30 | * generate a JSON string 31 | * 32 | * @throws ConversionFailedException If the conversion fails 33 | */ 34 | public function toJson($data, array $options = array()); 35 | 36 | /** 37 | * Converts JSON to an implementation-specific data structure. 38 | * 39 | * @param mixed $jsonData The JSON data. Use a {@link JsonDecoder} to 40 | * convert a JSON string to this data structure 41 | * @param array $options Additional implementation-specific conversion options 42 | * 43 | * @return mixed The converted data 44 | * 45 | * @throws ConversionFailedException If the conversion fails 46 | */ 47 | public function fromJson($jsonData, array $options = array()); 48 | } 49 | -------------------------------------------------------------------------------- /src/DecodingFailedException.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 Webmozart\Json; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown when a JSON string cannot be decoded. 18 | * 19 | * @since 1.0 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class DecodingFailedException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/EncodingFailedException.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 Webmozart\Json; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown when data cannot be encoded as JSON. 18 | * 19 | * @since 1.0 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class EncodingFailedException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/FileNotFoundException.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 Webmozart\Json; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown when a file was not found. 18 | * 19 | * @since 1.0 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class FileNotFoundException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/IOException.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 Webmozart\Json; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown when read/write errors on the filesystem occur. 18 | * 19 | * @since 1.1 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class IOException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/InvalidSchemaException.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 Webmozart\Json; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown a JSON schema cannot be loaded. 18 | * 19 | * @since 1.0 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class InvalidSchemaException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/JsonDecoder.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 Webmozart\Json; 13 | 14 | use Seld\JsonLint\JsonParser; 15 | use Seld\JsonLint\ParsingException; 16 | 17 | /** 18 | * Decodes JSON strings/files and validates against a JSON schema. 19 | * 20 | * @since 1.0 21 | * 22 | * @author Bernhard Schussek 23 | */ 24 | class JsonDecoder 25 | { 26 | /** 27 | * Decode a JSON value as PHP object. 28 | */ 29 | const OBJECT = 0; 30 | 31 | /** 32 | * Decode a JSON value as associative array. 33 | */ 34 | const ASSOC_ARRAY = 1; 35 | 36 | /** 37 | * Decode a JSON value as float. 38 | */ 39 | const FLOAT = 2; 40 | 41 | /** 42 | * Decode a JSON value as string. 43 | */ 44 | const STRING = 3; 45 | 46 | /** 47 | * @var JsonValidator 48 | */ 49 | private $validator; 50 | 51 | /** 52 | * @var int 53 | */ 54 | private $objectDecoding = self::OBJECT; 55 | 56 | /** 57 | * @var int 58 | */ 59 | private $bigIntDecoding = self::FLOAT; 60 | 61 | /** 62 | * @var int 63 | */ 64 | private $maxDepth = 512; 65 | 66 | /** 67 | * Creates a new decoder. 68 | * 69 | * @param null|JsonValidator $validator 70 | */ 71 | public function __construct(JsonValidator $validator = null) 72 | { 73 | $this->validator = $validator ?: new JsonValidator(); 74 | } 75 | 76 | /** 77 | * Decodes and validates a JSON string. 78 | * 79 | * If a schema is passed, the decoded object is validated against that 80 | * schema. The schema may be passed as file path or as object returned from 81 | * `JsonDecoder::decodeFile($schemaFile)`. 82 | * 83 | * You can adjust the decoding with {@link setObjectDecoding()}, 84 | * {@link setBigIntDecoding()} and {@link setMaxDepth()}. 85 | * 86 | * Schema validation is not supported when objects are decoded as 87 | * associative arrays. 88 | * 89 | * @param string $json The JSON string 90 | * @param string|object $schema The schema file or object 91 | * 92 | * @return mixed The decoded value 93 | * 94 | * @throws DecodingFailedException If the JSON string could not be decoded 95 | * @throws ValidationFailedException If the decoded string fails schema 96 | * validation 97 | * @throws InvalidSchemaException If the schema is invalid 98 | */ 99 | public function decode($json, $schema = null) 100 | { 101 | if (self::ASSOC_ARRAY === $this->objectDecoding && null !== $schema) { 102 | throw new \InvalidArgumentException( 103 | 'Schema validation is not supported when objects are decoded '. 104 | 'as associative arrays. Call '. 105 | 'JsonDecoder::setObjectDecoding(JsonDecoder::JSON_OBJECT) to fix.' 106 | ); 107 | } 108 | 109 | $decoded = $this->decodeJson($json); 110 | 111 | if (null !== $schema) { 112 | $errors = $this->validator->validate($decoded, $schema); 113 | 114 | if (count($errors) > 0) { 115 | throw ValidationFailedException::fromErrors($errors); 116 | } 117 | } 118 | 119 | return $decoded; 120 | } 121 | 122 | /** 123 | * Decodes and validates a JSON file. 124 | * 125 | * @param string $path The path to the JSON file 126 | * @param string|object $schema The schema file or object 127 | * 128 | * @return mixed The decoded file 129 | * 130 | * @throws FileNotFoundException If the file was not found 131 | * @throws DecodingFailedException If the file could not be decoded 132 | * @throws ValidationFailedException If the decoded file fails schema 133 | * validation 134 | * @throws InvalidSchemaException If the schema is invalid 135 | * 136 | * @see decode 137 | */ 138 | public function decodeFile($path, $schema = null) 139 | { 140 | if (!file_exists($path)) { 141 | throw new FileNotFoundException(sprintf( 142 | 'The file %s does not exist.', 143 | $path 144 | )); 145 | } 146 | 147 | $errorMessage = null; 148 | $errorCode = 0; 149 | 150 | set_error_handler(function ($errno, $errstr) use (&$errorMessage, &$errorCode) { 151 | $errorMessage = $errstr; 152 | $errorCode = $errno; 153 | }); 154 | 155 | $content = file_get_contents($path); 156 | 157 | restore_error_handler(); 158 | 159 | if (null !== $errorMessage) { 160 | if (false !== $pos = strpos($errorMessage, '): ')) { 161 | // cut "file_get_contents(%path%):" to make message more readable 162 | $errorMessage = substr($errorMessage, $pos + 3); 163 | } 164 | 165 | throw new IOException(sprintf( 166 | 'Could not read %s: %s (%s)', 167 | $path, 168 | $errorMessage, 169 | $errorCode 170 | ), $errorCode); 171 | } 172 | 173 | try { 174 | return $this->decode($content, $schema); 175 | } catch (DecodingFailedException $e) { 176 | // Add the file name to the exception 177 | throw new DecodingFailedException(sprintf( 178 | 'An error happened while decoding %s: %s', 179 | $path, 180 | $e->getMessage() 181 | ), $e->getCode(), $e); 182 | } catch (ValidationFailedException $e) { 183 | // Add the file name to the exception 184 | throw new ValidationFailedException(sprintf( 185 | "Validation of %s failed:\n%s", 186 | $path, 187 | $e->getErrorsAsString() 188 | ), $e->getErrors(), $e->getCode(), $e); 189 | } catch (InvalidSchemaException $e) { 190 | // Add the file name to the exception 191 | throw new InvalidSchemaException(sprintf( 192 | 'An error happened while decoding %s: %s', 193 | $path, 194 | $e->getMessage() 195 | ), $e->getCode(), $e); 196 | } 197 | } 198 | 199 | /** 200 | * Returns the maximum recursion depth. 201 | * 202 | * A depth of zero means that objects are not allowed. A depth of one means 203 | * only one level of objects or arrays is allowed. 204 | * 205 | * @return int The maximum recursion depth 206 | */ 207 | public function getMaxDepth() 208 | { 209 | return $this->maxDepth; 210 | } 211 | 212 | /** 213 | * Sets the maximum recursion depth. 214 | * 215 | * If the depth is exceeded during decoding, an {@link DecodingnFailedException} 216 | * will be thrown. 217 | * 218 | * A depth of zero means that objects are not allowed. A depth of one means 219 | * only one level of objects or arrays is allowed. 220 | * 221 | * @param int $maxDepth The maximum recursion depth 222 | * 223 | * @throws \InvalidArgumentException If the depth is not an integer greater 224 | * than or equal to zero 225 | */ 226 | public function setMaxDepth($maxDepth) 227 | { 228 | if (!is_int($maxDepth)) { 229 | throw new \InvalidArgumentException(sprintf( 230 | 'The maximum depth should be an integer. Got: %s', 231 | is_object($maxDepth) ? get_class($maxDepth) : gettype($maxDepth) 232 | )); 233 | } 234 | 235 | if ($maxDepth < 1) { 236 | throw new \InvalidArgumentException(sprintf( 237 | 'The maximum depth should 1 or greater. Got: %s', 238 | $maxDepth 239 | )); 240 | } 241 | 242 | $this->maxDepth = $maxDepth; 243 | } 244 | 245 | /** 246 | * Returns the decoding of JSON objects. 247 | * 248 | * @return int One of the constants {@link JSON_OBJECT} and {@link ASSOC_ARRAY} 249 | */ 250 | public function getObjectDecoding() 251 | { 252 | return $this->objectDecoding; 253 | } 254 | 255 | /** 256 | * Sets the decoding of JSON objects. 257 | * 258 | * By default, JSON objects are decoded as instances of {@link \stdClass}. 259 | * 260 | * @param int $decoding One of the constants {@link JSON_OBJECT} and {@link ASSOC_ARRAY} 261 | * 262 | * @throws \InvalidArgumentException If the passed decoding is invalid 263 | */ 264 | public function setObjectDecoding($decoding) 265 | { 266 | if (self::OBJECT !== $decoding && self::ASSOC_ARRAY !== $decoding) { 267 | throw new \InvalidArgumentException(sprintf( 268 | 'Expected JsonDecoder::JSON_OBJECT or JsonDecoder::ASSOC_ARRAY. '. 269 | 'Got: %s', 270 | $decoding 271 | )); 272 | } 273 | 274 | $this->objectDecoding = $decoding; 275 | } 276 | 277 | /** 278 | * Returns the decoding of big integers. 279 | * 280 | * @return int One of the constants {@link FLOAT} and {@link JSON_STRING} 281 | */ 282 | public function getBigIntDecoding() 283 | { 284 | return $this->bigIntDecoding; 285 | } 286 | 287 | /** 288 | * Sets the decoding of big integers. 289 | * 290 | * By default, big integers are decoded as floats. 291 | * 292 | * @param int $decoding One of the constants {@link FLOAT} and {@link JSON_STRING} 293 | * 294 | * @throws \InvalidArgumentException If the passed decoding is invalid 295 | */ 296 | public function setBigIntDecoding($decoding) 297 | { 298 | if (self::FLOAT !== $decoding && self::STRING !== $decoding) { 299 | throw new \InvalidArgumentException(sprintf( 300 | 'Expected JsonDecoder::FLOAT or JsonDecoder::JSON_STRING. '. 301 | 'Got: %s', 302 | $decoding 303 | )); 304 | } 305 | 306 | $this->bigIntDecoding = $decoding; 307 | } 308 | 309 | private function decodeJson($json) 310 | { 311 | $assoc = self::ASSOC_ARRAY === $this->objectDecoding; 312 | 313 | if (PHP_VERSION_ID >= 50400 && !defined('JSON_C_VERSION')) { 314 | $options = self::STRING === $this->bigIntDecoding ? JSON_BIGINT_AS_STRING : 0; 315 | 316 | $decoded = json_decode($json, $assoc, $this->maxDepth, $options); 317 | } else { 318 | $decoded = json_decode($json, $assoc, $this->maxDepth); 319 | } 320 | 321 | // Data could not be decoded 322 | if (null === $decoded && 'null' !== $json) { 323 | $parser = new JsonParser(); 324 | $e = $parser->lint($json); 325 | 326 | if ($e instanceof ParsingException) { 327 | throw new DecodingFailedException(sprintf( 328 | 'The JSON data could not be decoded: %s.', 329 | $e->getMessage() 330 | ), 0, $e); 331 | } 332 | 333 | // $e is null if json_decode() failed, but the linter did not find 334 | // any problems. Happens for example when the max depth is exceeded. 335 | throw new DecodingFailedException(sprintf( 336 | 'The JSON data could not be decoded: %s.', 337 | JsonError::getLastErrorMessage() 338 | ), json_last_error()); 339 | } 340 | 341 | return $decoded; 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/JsonEncoder.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 Webmozart\Json; 13 | 14 | /** 15 | * Encodes data as JSON. 16 | * 17 | * @since 1.0 18 | * 19 | * @author Bernhard Schussek 20 | */ 21 | class JsonEncoder 22 | { 23 | /** 24 | * Encode a value as JSON array. 25 | */ 26 | const JSON_ARRAY = 1; 27 | 28 | /** 29 | * Encode a value as JSON object. 30 | */ 31 | const JSON_OBJECT = 2; 32 | 33 | /** 34 | * Encode a value as JSON string. 35 | */ 36 | const JSON_STRING = 3; 37 | 38 | /** 39 | * Encode a value as JSON integer or float. 40 | */ 41 | const JSON_NUMBER = 4; 42 | 43 | /** 44 | * @var JsonValidator 45 | */ 46 | private $validator; 47 | 48 | /** 49 | * @var int 50 | */ 51 | private $arrayEncoding = self::JSON_ARRAY; 52 | 53 | /** 54 | * @var int 55 | */ 56 | private $numericEncoding = self::JSON_STRING; 57 | 58 | /** 59 | * @var bool 60 | */ 61 | private $gtLtEscaped = false; 62 | 63 | /** 64 | * @var bool 65 | */ 66 | private $ampersandEscaped = false; 67 | 68 | /** 69 | * @var bool 70 | */ 71 | private $singleQuoteEscaped = false; 72 | 73 | /** 74 | * @var bool 75 | */ 76 | private $doubleQuoteEscaped = false; 77 | 78 | /** 79 | * @var bool 80 | */ 81 | private $slashEscaped = true; 82 | 83 | /** 84 | * @var bool 85 | */ 86 | private $unicodeEscaped = true; 87 | 88 | /** 89 | * @var bool 90 | */ 91 | private $prettyPrinting = false; 92 | 93 | /** 94 | * @var bool 95 | */ 96 | private $terminatedWithLineFeed = false; 97 | 98 | /** 99 | * @var int 100 | */ 101 | private $maxDepth = 512; 102 | 103 | /** 104 | * Creates a new encoder. 105 | * 106 | * @param null|JsonValidator $validator 107 | */ 108 | public function __construct(JsonValidator $validator = null) 109 | { 110 | $this->validator = $validator ?: new JsonValidator(); 111 | } 112 | 113 | /** 114 | * Encodes data as JSON. 115 | * 116 | * If a schema is passed, the value is validated against that schema before 117 | * encoding. The schema may be passed as file path or as object returned 118 | * from `JsonDecoder::decodeFile($schemaFile)`. 119 | * 120 | * You can adjust the decoding with the various setters in this class. 121 | * 122 | * @param mixed $data The data to encode 123 | * @param string|object $schema The schema file or object 124 | * 125 | * @return string The JSON string 126 | * 127 | * @throws EncodingFailedException If the data could not be encoded 128 | * @throws ValidationFailedException If the data fails schema validation 129 | * @throws InvalidSchemaException If the schema is invalid 130 | */ 131 | public function encode($data, $schema = null) 132 | { 133 | if (null !== $schema) { 134 | $errors = $this->validator->validate($data, $schema); 135 | 136 | if (count($errors) > 0) { 137 | throw ValidationFailedException::fromErrors($errors); 138 | } 139 | } 140 | 141 | $options = 0; 142 | 143 | if (self::JSON_OBJECT === $this->arrayEncoding) { 144 | $options |= JSON_FORCE_OBJECT; 145 | } 146 | 147 | if (self::JSON_NUMBER === $this->numericEncoding) { 148 | $options |= JSON_NUMERIC_CHECK; 149 | } 150 | 151 | if ($this->gtLtEscaped) { 152 | $options |= JSON_HEX_TAG; 153 | } 154 | 155 | if ($this->ampersandEscaped) { 156 | $options |= JSON_HEX_AMP; 157 | } 158 | 159 | if ($this->singleQuoteEscaped) { 160 | $options |= JSON_HEX_APOS; 161 | } 162 | 163 | if ($this->doubleQuoteEscaped) { 164 | $options |= JSON_HEX_QUOT; 165 | } 166 | 167 | if (PHP_VERSION_ID >= 50400) { 168 | if (!$this->slashEscaped) { 169 | $options |= JSON_UNESCAPED_SLASHES; 170 | } 171 | 172 | if (!$this->unicodeEscaped) { 173 | $options |= JSON_UNESCAPED_UNICODE; 174 | } 175 | 176 | if ($this->prettyPrinting) { 177 | $options |= JSON_PRETTY_PRINT; 178 | } 179 | } 180 | 181 | if (PHP_VERSION_ID < 71000) { 182 | // PHP before 7.1 decodes empty properties as "_empty_". Make 183 | // sure the encoding of these properties works again. 184 | if (is_object($data) && isset($data->{'_empty_'})) { 185 | $data = (array) $data; 186 | } 187 | 188 | if (is_array($data) && isset($data['_empty_'])) { 189 | // Maintain key order 190 | $keys = array_keys($data); 191 | $keys[array_search('_empty_', $keys, true)] = ''; 192 | $data = array_combine($keys, $data); 193 | } 194 | } 195 | 196 | if (PHP_VERSION_ID >= 50500) { 197 | $maxDepth = $this->maxDepth; 198 | 199 | // We subtract 1 from the max depth to make JsonDecoder and 200 | // JsonEncoder consistent. json_encode() and json_decode() behave 201 | // differently for their depth values. See the test cases for 202 | // examples. 203 | // HHVM does not have this inconsistency. 204 | if (!defined('HHVM_VERSION')) { 205 | --$maxDepth; 206 | } 207 | 208 | $encoded = json_encode($data, $options, $maxDepth); 209 | } else { 210 | $encoded = json_encode($data, $options); 211 | } 212 | 213 | if (PHP_VERSION_ID < 50400 && !$this->slashEscaped) { 214 | // PHP below 5.4 does not allow to turn off slash escaping. Let's 215 | // unescape slashes manually. 216 | $encoded = str_replace('\\/', '/', $encoded); 217 | } 218 | 219 | if (JSON_ERROR_NONE !== json_last_error()) { 220 | throw new EncodingFailedException(sprintf( 221 | 'The data could not be encoded as JSON: %s', 222 | JsonError::getLastErrorMessage() 223 | ), json_last_error()); 224 | } 225 | 226 | if ($this->terminatedWithLineFeed) { 227 | $encoded .= "\n"; 228 | } 229 | 230 | return $encoded; 231 | } 232 | 233 | /** 234 | * Encodes data into a JSON file. 235 | * 236 | * @param mixed $data The data to encode 237 | * @param string $path The path where the JSON file will be stored 238 | * @param string|object $schema The schema file or object 239 | * 240 | * @throws EncodingFailedException If the data could not be encoded 241 | * @throws ValidationFailedException If the data fails schema validation 242 | * @throws InvalidSchemaException If the schema is invalid 243 | * 244 | * @see encode 245 | */ 246 | public function encodeFile($data, $path, $schema = null) 247 | { 248 | if (!file_exists($dir = dirname($path))) { 249 | mkdir($dir, 0777, true); 250 | } 251 | 252 | try { 253 | // Right now, it's sufficient to just write the file. In the future, 254 | // this will diff existing files with the given data and only do 255 | // in-place modifications where necessary. 256 | $content = $this->encode($data, $schema); 257 | } catch (EncodingFailedException $e) { 258 | // Add the file name to the exception 259 | throw new EncodingFailedException(sprintf( 260 | 'An error happened while encoding %s: %s', 261 | $path, 262 | $e->getMessage() 263 | ), $e->getCode(), $e); 264 | } catch (ValidationFailedException $e) { 265 | // Add the file name to the exception 266 | throw new ValidationFailedException(sprintf( 267 | "Validation failed while encoding %s:\n%s", 268 | $path, 269 | $e->getErrorsAsString() 270 | ), $e->getErrors(), $e->getCode(), $e); 271 | } catch (InvalidSchemaException $e) { 272 | // Add the file name to the exception 273 | throw new InvalidSchemaException(sprintf( 274 | 'An error happened while encoding %s: %s', 275 | $path, 276 | $e->getMessage() 277 | ), $e->getCode(), $e); 278 | } 279 | 280 | $errorMessage = null; 281 | $errorCode = 0; 282 | 283 | set_error_handler(function ($errno, $errstr) use (&$errorMessage, &$errorCode) { 284 | $errorMessage = $errstr; 285 | $errorCode = $errno; 286 | }); 287 | 288 | file_put_contents($path, $content); 289 | 290 | restore_error_handler(); 291 | 292 | if (null !== $errorMessage) { 293 | if (false !== $pos = strpos($errorMessage, '): ')) { 294 | // cut "file_put_contents(%path%):" to make message more readable 295 | $errorMessage = substr($errorMessage, $pos + 3); 296 | } 297 | 298 | throw new IOException(sprintf( 299 | 'Could not write %s: %s (%s)', 300 | $path, 301 | $errorMessage, 302 | $errorCode 303 | ), $errorCode); 304 | } 305 | } 306 | 307 | /** 308 | * Returns the encoding of non-associative arrays. 309 | * 310 | * @return int One of the constants {@link JSON_OBJECT} and {@link JSON_ARRAY} 311 | */ 312 | public function getArrayEncoding() 313 | { 314 | return $this->arrayEncoding; 315 | } 316 | 317 | /** 318 | * Sets the encoding of non-associative arrays. 319 | * 320 | * By default, non-associative arrays are decoded as JSON arrays. 321 | * 322 | * @param int $encoding One of the constants {@link JSON_OBJECT} and {@link JSON_ARRAY} 323 | * 324 | * @throws \InvalidArgumentException If the passed encoding is invalid 325 | */ 326 | public function setArrayEncoding($encoding) 327 | { 328 | if (self::JSON_ARRAY !== $encoding && self::JSON_OBJECT !== $encoding) { 329 | throw new \InvalidArgumentException(sprintf( 330 | 'Expected JsonEncoder::JSON_ARRAY or JsonEncoder::JSON_OBJECT. '. 331 | 'Got: %s', 332 | $encoding 333 | )); 334 | } 335 | 336 | $this->arrayEncoding = $encoding; 337 | } 338 | 339 | /** 340 | * Returns the encoding of numeric strings. 341 | * 342 | * @return int One of the constants {@link JSON_STRING} and {@link JSON_NUMBER} 343 | */ 344 | public function getNumericEncoding() 345 | { 346 | return $this->numericEncoding; 347 | } 348 | 349 | /** 350 | * Sets the encoding of numeric strings. 351 | * 352 | * By default, non-associative arrays are decoded as JSON strings. 353 | * 354 | * @param int $encoding One of the constants {@link JSON_STRING} and {@link JSON_NUMBER} 355 | * 356 | * @throws \InvalidArgumentException If the passed encoding is invalid 357 | */ 358 | public function setNumericEncoding($encoding) 359 | { 360 | if (self::JSON_NUMBER !== $encoding && self::JSON_STRING !== $encoding) { 361 | throw new \InvalidArgumentException(sprintf( 362 | 'Expected JsonEncoder::JSON_NUMBER or JsonEncoder::JSON_STRING. '. 363 | 'Got: %s', 364 | $encoding 365 | )); 366 | } 367 | 368 | $this->numericEncoding = $encoding; 369 | } 370 | 371 | /** 372 | * Returns whether ampersands (&) are escaped. 373 | * 374 | * If `true`, ampersands will be escaped as "\u0026". 375 | * 376 | * By default, ampersands are not escaped. 377 | * 378 | * @return bool Whether ampersands are escaped 379 | */ 380 | public function isAmpersandEscaped() 381 | { 382 | return $this->ampersandEscaped; 383 | } 384 | 385 | /** 386 | * Sets whether ampersands (&) should be escaped. 387 | * 388 | * If `true`, ampersands will be escaped as "\u0026". 389 | * 390 | * By default, ampersands are not escaped. 391 | * 392 | * @param bool $enabled Whether ampersands should be escaped 393 | */ 394 | public function setEscapeAmpersand($enabled) 395 | { 396 | $this->ampersandEscaped = $enabled; 397 | } 398 | 399 | /** 400 | * Returns whether double quotes (") are escaped. 401 | * 402 | * If `true`, double quotes will be escaped as "\u0022". 403 | * 404 | * By default, double quotes are not escaped. 405 | * 406 | * @return bool Whether double quotes are escaped 407 | */ 408 | public function isDoubleQuoteEscaped() 409 | { 410 | return $this->doubleQuoteEscaped; 411 | } 412 | 413 | /** 414 | * Sets whether double quotes (") should be escaped. 415 | * 416 | * If `true`, double quotes will be escaped as "\u0022". 417 | * 418 | * By default, double quotes are not escaped. 419 | * 420 | * @param bool $enabled Whether double quotes should be escaped 421 | */ 422 | public function setEscapeDoubleQuote($enabled) 423 | { 424 | $this->doubleQuoteEscaped = $enabled; 425 | } 426 | 427 | /** 428 | * Returns whether single quotes (') are escaped. 429 | * 430 | * If `true`, single quotes will be escaped as "\u0027". 431 | * 432 | * By default, single quotes are not escaped. 433 | * 434 | * @return bool Whether single quotes are escaped 435 | */ 436 | public function isSingleQuoteEscaped() 437 | { 438 | return $this->singleQuoteEscaped; 439 | } 440 | 441 | /** 442 | * Sets whether single quotes (") should be escaped. 443 | * 444 | * If `true`, single quotes will be escaped as "\u0027". 445 | * 446 | * By default, single quotes are not escaped. 447 | * 448 | * @param bool $enabled Whether single quotes should be escaped 449 | */ 450 | public function setEscapeSingleQuote($enabled) 451 | { 452 | $this->singleQuoteEscaped = $enabled; 453 | } 454 | 455 | /** 456 | * Returns whether forward slashes (/) are escaped. 457 | * 458 | * If `true`, forward slashes will be escaped as "\/". 459 | * 460 | * By default, forward slashes are not escaped. 461 | * 462 | * @return bool Whether forward slashes are escaped 463 | */ 464 | public function isSlashEscaped() 465 | { 466 | return $this->slashEscaped; 467 | } 468 | 469 | /** 470 | * Sets whether forward slashes (") should be escaped. 471 | * 472 | * If `true`, forward slashes will be escaped as "\/". 473 | * 474 | * By default, forward slashes are not escaped. 475 | * 476 | * @param bool $enabled Whether forward slashes should be escaped 477 | */ 478 | public function setEscapeSlash($enabled) 479 | { 480 | $this->slashEscaped = $enabled; 481 | } 482 | 483 | /** 484 | * Returns whether greater than/less than symbols (>, <) are escaped. 485 | * 486 | * If `true`, greater than will be escaped as "\u003E" and less than as 487 | * "\u003C". 488 | * 489 | * By default, greater than/less than symbols are not escaped. 490 | * 491 | * @return bool Whether greater than/less than symbols are escaped 492 | */ 493 | public function isGtLtEscaped() 494 | { 495 | return $this->gtLtEscaped; 496 | } 497 | 498 | /** 499 | * Sets whether greater than/less than symbols (>, <) should be escaped. 500 | * 501 | * If `true`, greater than will be escaped as "\u003E" and less than as 502 | * "\u003C". 503 | * 504 | * By default, greater than/less than symbols are not escaped. 505 | * 506 | * @param bool $enabled Whether greater than/less than should be escaped 507 | */ 508 | public function setEscapeGtLt($enabled) 509 | { 510 | $this->gtLtEscaped = $enabled; 511 | } 512 | 513 | /** 514 | * Returns whether unicode characters are escaped. 515 | * 516 | * If `true`, unicode characters will be escaped as hexadecimals strings. 517 | * For example, "ü" will be escaped as "\u00fc". 518 | * 519 | * By default, unicode characters are escaped. 520 | * 521 | * @return bool Whether unicode characters are escaped 522 | */ 523 | public function isUnicodeEscaped() 524 | { 525 | return $this->unicodeEscaped; 526 | } 527 | 528 | /** 529 | * Sets whether unicode characters should be escaped. 530 | * 531 | * If `true`, unicode characters will be escaped as hexadecimals strings. 532 | * For example, "ü" will be escaped as "\u00fc". 533 | * 534 | * By default, unicode characters are escaped. 535 | * 536 | * @param bool $enabled Whether unicode characters should be escaped 537 | */ 538 | public function setEscapeUnicode($enabled) 539 | { 540 | $this->unicodeEscaped = $enabled; 541 | } 542 | 543 | /** 544 | * Returns whether JSON strings are formatted for better readability. 545 | * 546 | * If `true`, line breaks will be added after object properties and array 547 | * entries. Each new nesting level will be indented by four spaces. 548 | * 549 | * By default, pretty printing is not enabled. 550 | * 551 | * @return bool Whether JSON strings are formatted 552 | */ 553 | public function isPrettyPrinting() 554 | { 555 | return $this->prettyPrinting; 556 | } 557 | 558 | /** 559 | * Sets whether JSON strings should be formatted for better readability. 560 | * 561 | * If `true`, line breaks will be added after object properties and array 562 | * entries. Each new nesting level will be indented by four spaces. 563 | * 564 | * By default, pretty printing is not enabled. 565 | * 566 | * @param bool $prettyPrinting Whether JSON strings should be formatted 567 | */ 568 | public function setPrettyPrinting($prettyPrinting) 569 | { 570 | $this->prettyPrinting = $prettyPrinting; 571 | } 572 | 573 | /** 574 | * Returns whether JSON strings are terminated with a line feed. 575 | * 576 | * By default, JSON strings are not terminated with a line feed. 577 | * 578 | * @return bool Whether JSON strings are terminated with a line feed 579 | */ 580 | public function isTerminatedWithLineFeed() 581 | { 582 | return $this->terminatedWithLineFeed; 583 | } 584 | 585 | /** 586 | * Sets whether JSON strings should be terminated with a line feed. 587 | * 588 | * By default, JSON strings are not terminated with a line feed. 589 | * 590 | * @param bool $enabled Whether JSON strings should be terminated with a 591 | * line feed 592 | */ 593 | public function setTerminateWithLineFeed($enabled) 594 | { 595 | $this->terminatedWithLineFeed = $enabled; 596 | } 597 | 598 | /** 599 | * Returns the maximum recursion depth. 600 | * 601 | * A depth of zero means that objects are not allowed. A depth of one means 602 | * only one level of objects or arrays is allowed. 603 | * 604 | * @return int The maximum recursion depth 605 | */ 606 | public function getMaxDepth() 607 | { 608 | return $this->maxDepth; 609 | } 610 | 611 | /** 612 | * Sets the maximum recursion depth. 613 | * 614 | * If the depth is exceeded during encoding, an {@link EncodingFailedException} 615 | * will be thrown. 616 | * 617 | * A depth of zero means that objects are not allowed. A depth of one means 618 | * only one level of objects or arrays is allowed. 619 | * 620 | * @param int $maxDepth The maximum recursion depth 621 | * 622 | * @throws \InvalidArgumentException If the depth is not an integer greater 623 | * than or equal to zero 624 | */ 625 | public function setMaxDepth($maxDepth) 626 | { 627 | if (!is_int($maxDepth)) { 628 | throw new \InvalidArgumentException(sprintf( 629 | 'The maximum depth should be an integer. Got: %s', 630 | is_object($maxDepth) ? get_class($maxDepth) : gettype($maxDepth) 631 | )); 632 | } 633 | 634 | if ($maxDepth < 1) { 635 | throw new \InvalidArgumentException(sprintf( 636 | 'The maximum depth should 1 or greater. Got: %s', 637 | $maxDepth 638 | )); 639 | } 640 | 641 | $this->maxDepth = $maxDepth; 642 | } 643 | } 644 | -------------------------------------------------------------------------------- /src/JsonError.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 Webmozart\Json; 13 | 14 | /** 15 | * @since 1.0 16 | * 17 | * @author Bernhard Schussek 18 | */ 19 | class JsonError 20 | { 21 | /** 22 | * User-land implementation of `json_last_error_msg()` for PHP < 5.5. 23 | * 24 | * @return string The last JSON error message 25 | */ 26 | public static function getLastErrorMessage() 27 | { 28 | return self::getErrorMessage(json_last_error()); 29 | } 30 | 31 | /** 32 | * Returns the error message of a JSON error code. 33 | * 34 | * @param int $error The error code 35 | * 36 | * @return string The error message 37 | */ 38 | public static function getErrorMessage($error) 39 | { 40 | switch ($error) { 41 | case JSON_ERROR_NONE: 42 | return 'JSON_ERROR_NONE'; 43 | case JSON_ERROR_DEPTH: 44 | return 'JSON_ERROR_DEPTH'; 45 | case JSON_ERROR_STATE_MISMATCH: 46 | return 'JSON_ERROR_STATE_MISMATCH'; 47 | case JSON_ERROR_CTRL_CHAR: 48 | return 'JSON_ERROR_CTRL_CHAR'; 49 | case JSON_ERROR_SYNTAX: 50 | return 'JSON_ERROR_SYNTAX'; 51 | case JSON_ERROR_UTF8: 52 | return 'JSON_ERROR_UTF8'; 53 | } 54 | 55 | if (version_compare(PHP_VERSION, '5.5.0', '>=')) { 56 | switch ($error) { 57 | case JSON_ERROR_RECURSION: 58 | return 'JSON_ERROR_RECURSION'; 59 | case JSON_ERROR_INF_OR_NAN: 60 | return 'JSON_ERROR_INF_OR_NAN'; 61 | case JSON_ERROR_UNSUPPORTED_TYPE: 62 | return 'JSON_ERROR_UNSUPPORTED_TYPE'; 63 | } 64 | } 65 | 66 | return 'JSON_ERROR_UNKNOWN'; 67 | } 68 | 69 | private function __construct() 70 | { 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/JsonValidator.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 Webmozart\Json; 13 | 14 | use JsonSchema\Exception\InvalidArgumentException; 15 | use JsonSchema\Exception\ResourceNotFoundException; 16 | use JsonSchema\RefResolver; 17 | use JsonSchema\Uri\UriResolver; 18 | use JsonSchema\Uri\UriRetriever; 19 | use JsonSchema\UriResolverInterface; 20 | use JsonSchema\Validator; 21 | use Webmozart\PathUtil\Path; 22 | 23 | /** 24 | * Validates decoded JSON values against a JSON schema. 25 | * 26 | * This class is a wrapper for {@link Validator} that adds exceptions and 27 | * validation of schema files. A few edge cases that are not handled by 28 | * {@link Validator} are handled by this class. 29 | * 30 | * @since 1.0 31 | * 32 | * @author Bernhard Schussek 33 | */ 34 | class JsonValidator 35 | { 36 | /** 37 | * The schema used for validating schemas. 38 | * 39 | * @var object|null 40 | */ 41 | private $metaSchema; 42 | 43 | /** 44 | * Validator instance used for validation. 45 | * 46 | * @var Validator 47 | */ 48 | private $validator; 49 | 50 | /** 51 | * Reference resolver. 52 | * 53 | * @var RefResolver 54 | */ 55 | private $resolver; 56 | 57 | /** 58 | * JsonValidator constructor. 59 | * 60 | * @param Validator|null $validator JsonSchema\Validator 61 | * instance to use 62 | * @param UriRetriever|null $uriRetriever The retriever for fetching 63 | * JSON schemas 64 | * @param UriResolverInterface|null $uriResolver The resolver for URIs 65 | */ 66 | public function __construct(Validator $validator = null, UriRetriever $uriRetriever = null, UriResolverInterface $uriResolver = null) 67 | { 68 | $this->validator = $validator ?: new Validator(); 69 | $this->resolver = new RefResolver($uriRetriever ?: new UriRetriever(), $uriResolver ?: new UriResolver()); 70 | } 71 | 72 | /** 73 | * Validates JSON data against a schema. 74 | * 75 | * The schema may be passed as file path or as object returned from 76 | * `json_decode($schemaFile)`. 77 | * 78 | * @param mixed $data The decoded JSON data 79 | * @param string|object|null $schema The schema file or object. If `null`, 80 | * the validator will look for a `$schema` 81 | * property 82 | * 83 | * @return string[] The errors found during validation. Returns an empty 84 | * array if no errors were found 85 | * 86 | * @throws InvalidSchemaException If the schema is invalid 87 | */ 88 | public function validate($data, $schema = null) 89 | { 90 | if (null === $schema && isset($data->{'$schema'})) { 91 | $schema = $data->{'$schema'}; 92 | } 93 | 94 | if (is_string($schema)) { 95 | $schema = $this->loadSchema($schema); 96 | } elseif (is_object($schema)) { 97 | $this->assertSchemaValid($schema); 98 | } else { 99 | throw new InvalidSchemaException(sprintf( 100 | 'The schema must be given as string, object or in the "$schema" '. 101 | 'property of the JSON data. Got: %s', 102 | is_object($schema) ? get_class($schema) : gettype($schema) 103 | )); 104 | } 105 | 106 | $this->validator->reset(); 107 | 108 | try { 109 | $this->validator->check($data, $schema); 110 | } catch (InvalidArgumentException $e) { 111 | throw new InvalidSchemaException(sprintf( 112 | 'The schema is invalid: %s', 113 | $e->getMessage() 114 | ), 0, $e); 115 | } 116 | 117 | $errors = array(); 118 | 119 | if (!$this->validator->isValid()) { 120 | $errors = (array) $this->validator->getErrors(); 121 | 122 | foreach ($errors as $key => $error) { 123 | $prefix = $error['property'] ? $error['property'].': ' : ''; 124 | $errors[$key] = $prefix.$error['message']; 125 | } 126 | } 127 | 128 | return $errors; 129 | } 130 | 131 | private function assertSchemaValid($schema) 132 | { 133 | if (null === $this->metaSchema) { 134 | // The meta schema is obviously not validated. If we 135 | // validate it against itself, we have an endless recursion 136 | $this->metaSchema = json_decode(file_get_contents(__DIR__.'/../res/meta-schema.json')); 137 | } 138 | 139 | if ($schema === $this->metaSchema) { 140 | return; 141 | } 142 | 143 | $errors = $this->validate($schema, $this->metaSchema); 144 | 145 | if (count($errors) > 0) { 146 | throw new InvalidSchemaException(sprintf( 147 | "The schema is invalid:\n%s", 148 | implode("\n", $errors) 149 | )); 150 | } 151 | } 152 | 153 | private function loadSchema($file) 154 | { 155 | // Retrieve schema and cache in UriRetriever 156 | $file = Path::canonicalize($file); 157 | 158 | // Add file:// scheme if necessary 159 | if (false === strpos($file, '://')) { 160 | $file = 'file://'.$file; 161 | } 162 | 163 | // Resolve references to other schemas 164 | try { 165 | $schema = $this->resolver->resolve($file); 166 | } catch (ResourceNotFoundException $e) { 167 | throw new InvalidSchemaException(sprintf( 168 | 'The schema %s does not exist.', 169 | $file 170 | ), 0, $e); 171 | } 172 | 173 | try { 174 | $this->assertSchemaValid($schema); 175 | } catch (InvalidSchemaException $e) { 176 | throw new InvalidSchemaException(sprintf( 177 | 'An error occurred while loading the schema %s: %s', 178 | $file, 179 | $e->getMessage() 180 | ), 0, $e); 181 | } 182 | 183 | return $schema; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Migration/JsonMigration.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 Webmozart\Json\Migration; 13 | 14 | use stdClass; 15 | 16 | /** 17 | * Migrates a JSON object between versions. 18 | * 19 | * The JSON object is expected to have the property "version" set. 20 | * 21 | * @since 1.3 22 | * 23 | * @author Bernhard Schussek 24 | */ 25 | interface JsonMigration 26 | { 27 | /** 28 | * Returns the version of the JSON object that this migration expects. 29 | * 30 | * @return string The version string 31 | */ 32 | public function getSourceVersion(); 33 | 34 | /** 35 | * Returns the version of the JSON object that this migration upgrades to. 36 | * 37 | * @return string The version string 38 | */ 39 | public function getTargetVersion(); 40 | 41 | /** 42 | * Upgrades a JSON object from the source to the target version. 43 | * 44 | * @param stdClass $data The JSON object of the package file 45 | */ 46 | public function up(stdClass $data); 47 | 48 | /** 49 | * Reverts a JSON object from the target to the source version. 50 | * 51 | * @param stdClass $data The JSON object of the package file 52 | */ 53 | public function down(stdClass $data); 54 | } 55 | -------------------------------------------------------------------------------- /src/Migration/MigratingConverter.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 Webmozart\Json\Migration; 13 | 14 | use stdClass; 15 | use Webmozart\Json\Conversion\ConversionFailedException; 16 | use Webmozart\Json\Conversion\JsonConverter; 17 | 18 | /** 19 | * A decorator for a {@link JsonCoverter} that migrates JSON objects. 20 | * 21 | * This decorator supports JSON objects in different versions. The decorated 22 | * converter can be written for a specific version. Any other version can be 23 | * supported by supplying a {@link MigrationManager} that is able to migrate 24 | * a JSON object in that version to the version required by the decorated 25 | * converter. 26 | * 27 | * You need to pass the decorated converter and the migration manager to the 28 | * constructor: 29 | * 30 | * ~~~php 31 | * // Written for version 3.0 32 | * $converter = ConfigFileConverter(); 33 | * 34 | * // Support older versions of the file 35 | * $migrationManager = new MigrationManager(array( 36 | * new ConfigFile10To20Migration(), 37 | * new ConfigFile20To30Migration(), 38 | * )); 39 | * 40 | * // Decorate the converter 41 | * $converter = new MigratingConverter($converter, '3.0', $migrationManager); 42 | * ~~~ 43 | * 44 | * You can load JSON data in any version with the method {@link fromJson()}. If 45 | * the "version" property of the JSON object is different than the version 46 | * supported by the decorated converter, the JSON object is migrated to the 47 | * required version. 48 | * 49 | * ~~~php 50 | * $jsonDecoder = new JsonDecoder(); 51 | * $configFile = $converter->fromJson($jsonDecoder->decode($json)); 52 | * ~~~ 53 | * 54 | * You can also dump data as JSON object with {@link toJson()}: 55 | * 56 | * ~~~php 57 | * $jsonEncoder = new JsonEncoder(); 58 | * $jsonEncoder->encode($converter->toJson($configFile)); 59 | * ~~~ 60 | * 61 | * By default, data is dumped in the current version. If you want to dump the 62 | * data in a specific version, pass the "targetVersion" option: 63 | * 64 | * ~~~php 65 | * $jsonEncoder->encode($converter->toJson($configFile, array( 66 | * 'targetVersion' => '2.0', 67 | * ))); 68 | * ~~~ 69 | * 70 | * @since 1.3 71 | * 72 | * @author Bernhard Schussek 73 | */ 74 | class MigratingConverter implements JsonConverter 75 | { 76 | /** 77 | * @var JsonConverter 78 | */ 79 | private $innerConverter; 80 | 81 | /** 82 | * @var MigrationManager 83 | */ 84 | private $migrationManager; 85 | 86 | /** 87 | * @var string 88 | */ 89 | private $currentVersion; 90 | 91 | /** 92 | * @var string[] 93 | */ 94 | private $knownVersions; 95 | 96 | /** 97 | * Creates the converter. 98 | * 99 | * @param JsonConverter $innerConverter The decorated converter 100 | * @param string $currentVersion The version that the decorated 101 | * converter is compatible with 102 | * @param MigrationManager $migrationManager The manager for migrating JSON data 103 | */ 104 | public function __construct(JsonConverter $innerConverter, $currentVersion, MigrationManager $migrationManager) 105 | { 106 | $this->innerConverter = $innerConverter; 107 | $this->migrationManager = $migrationManager; 108 | $this->currentVersion = $currentVersion; 109 | $this->knownVersions = $this->migrationManager->getKnownVersions(); 110 | 111 | if (!in_array($currentVersion, $this->knownVersions, true)) { 112 | $this->knownVersions[] = $currentVersion; 113 | usort($this->knownVersions, 'version_compare'); 114 | } 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function toJson($data, array $options = array()) 121 | { 122 | $targetVersion = isset($options['targetVersion']) 123 | ? $options['targetVersion'] 124 | : $this->currentVersion; 125 | 126 | $this->assertVersionSupported($targetVersion); 127 | 128 | $jsonData = $this->innerConverter->toJson($data, $options); 129 | 130 | $this->assertObject($jsonData); 131 | 132 | $jsonData->version = $this->currentVersion; 133 | 134 | if ($jsonData->version !== $targetVersion) { 135 | $this->migrate($jsonData, $targetVersion); 136 | $jsonData->version = $targetVersion; 137 | } 138 | 139 | return $jsonData; 140 | } 141 | 142 | /** 143 | * {@inheritdoc} 144 | */ 145 | public function fromJson($jsonData, array $options = array()) 146 | { 147 | $this->assertObject($jsonData); 148 | $this->assertVersionIsset($jsonData); 149 | $this->assertVersionSupported($jsonData->version); 150 | 151 | if ($jsonData->version !== $this->currentVersion) { 152 | $this->migrationManager->migrate($jsonData, $this->currentVersion); 153 | } 154 | 155 | return $this->innerConverter->fromJson($jsonData, $options); 156 | } 157 | 158 | private function migrate(stdClass $jsonData, $targetVersion) 159 | { 160 | try { 161 | $this->migrationManager->migrate($jsonData, $targetVersion); 162 | } catch (MigrationFailedException $e) { 163 | throw new ConversionFailedException(sprintf( 164 | 'Could not migrate the JSON data: %s', 165 | $e->getMessage() 166 | ), 0, $e); 167 | } 168 | } 169 | 170 | private function assertVersionSupported($version) 171 | { 172 | if (!in_array($version, $this->knownVersions, true)) { 173 | throw UnsupportedVersionException::forVersion($version, $this->knownVersions); 174 | } 175 | } 176 | 177 | private function assertObject($jsonData) 178 | { 179 | if (!$jsonData instanceof stdClass) { 180 | throw new ConversionFailedException(sprintf( 181 | 'Expected an instance of stdClass, got: %s', 182 | is_object($jsonData) ? get_class($jsonData) : gettype($jsonData) 183 | )); 184 | } 185 | } 186 | 187 | private function assertVersionIsset(stdClass $jsonData) 188 | { 189 | if (!isset($jsonData->version)) { 190 | throw new ConversionFailedException('Could not find a "version" property.'); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Migration/MigrationFailedException.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 Webmozart\Json\Migration; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown when a migration fails. 18 | * 19 | * @since 1.3 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class MigrationFailedException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/Migration/MigrationManager.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 Webmozart\Json\Migration; 13 | 14 | use stdClass; 15 | use Webmozart\Assert\Assert; 16 | use Webmozart\Json\Versioning\JsonVersioner; 17 | use Webmozart\Json\Versioning\SchemaUriVersioner; 18 | 19 | /** 20 | * Migrates a JSON object between different versions. 21 | * 22 | * The JSON object is expected to have the property "version" set. 23 | * 24 | * @since 1.3 25 | * 26 | * @author Bernhard Schussek 27 | */ 28 | class MigrationManager 29 | { 30 | /** 31 | * @var JsonVersioner 32 | */ 33 | private $versioner; 34 | 35 | /** 36 | * @var JsonMigration[] 37 | */ 38 | private $migrationsBySourceVersion = array(); 39 | 40 | /** 41 | * @var JsonMigration[] 42 | */ 43 | private $migrationsByTargetVersion = array(); 44 | 45 | /** 46 | * @var string[] 47 | */ 48 | private $knownVersions = array(); 49 | 50 | /** 51 | * Creates a new migration manager. 52 | * 53 | * @param JsonMigration[] $migrations The migrations migrating a JSON 54 | * object between individual versions 55 | * @param JsonVersioner|null $versioner The versioner that should be used 56 | */ 57 | public function __construct(array $migrations, JsonVersioner $versioner = null) 58 | { 59 | Assert::allIsInstanceOf($migrations, __NAMESPACE__.'\JsonMigration'); 60 | 61 | $this->versioner = $versioner ?: new SchemaUriVersioner(); 62 | 63 | foreach ($migrations as $migration) { 64 | $this->migrationsBySourceVersion[$migration->getSourceVersion()] = $migration; 65 | $this->migrationsByTargetVersion[$migration->getTargetVersion()] = $migration; 66 | $this->knownVersions[] = $migration->getSourceVersion(); 67 | $this->knownVersions[] = $migration->getTargetVersion(); 68 | } 69 | 70 | $this->knownVersions = array_unique($this->knownVersions); 71 | 72 | uksort($this->migrationsBySourceVersion, 'version_compare'); 73 | uksort($this->migrationsByTargetVersion, 'version_compare'); 74 | usort($this->knownVersions, 'version_compare'); 75 | } 76 | 77 | /** 78 | * Migrates a JSON object to the given version. 79 | * 80 | * @param stdClass $data The JSON object 81 | * @param string $targetVersion The version string 82 | */ 83 | public function migrate(stdClass $data, $targetVersion) 84 | { 85 | $sourceVersion = $this->versioner->parseVersion($data); 86 | 87 | if (version_compare($targetVersion, $sourceVersion, '>')) { 88 | $this->up($data, $sourceVersion, $targetVersion); 89 | } elseif (version_compare($targetVersion, $sourceVersion, '<')) { 90 | $this->down($data, $sourceVersion, $targetVersion); 91 | } 92 | } 93 | 94 | /** 95 | * Returns all versions known to the manager. 96 | * 97 | * @return string[] The known version strings 98 | */ 99 | public function getKnownVersions() 100 | { 101 | return $this->knownVersions; 102 | } 103 | 104 | private function up($data, $sourceVersion, $targetVersion) 105 | { 106 | while (version_compare($sourceVersion, $targetVersion, '<')) { 107 | if (!isset($this->migrationsBySourceVersion[$sourceVersion])) { 108 | throw new MigrationFailedException(sprintf( 109 | 'No migration found to upgrade from version %s to %s.', 110 | $sourceVersion, 111 | $targetVersion 112 | )); 113 | } 114 | 115 | $migration = $this->migrationsBySourceVersion[$sourceVersion]; 116 | 117 | // Final version too high 118 | if (version_compare($migration->getTargetVersion(), $targetVersion, '>')) { 119 | throw new MigrationFailedException(sprintf( 120 | 'No migration found to upgrade from version %s to %s.', 121 | $sourceVersion, 122 | $targetVersion 123 | )); 124 | } 125 | 126 | $migration->up($data); 127 | 128 | $this->versioner->updateVersion($data, $migration->getTargetVersion()); 129 | 130 | $sourceVersion = $migration->getTargetVersion(); 131 | } 132 | } 133 | 134 | private function down($data, $sourceVersion, $targetVersion) 135 | { 136 | while (version_compare($sourceVersion, $targetVersion, '>')) { 137 | if (!isset($this->migrationsByTargetVersion[$sourceVersion])) { 138 | throw new MigrationFailedException(sprintf( 139 | 'No migration found to downgrade from version %s to %s.', 140 | $sourceVersion, 141 | $targetVersion 142 | )); 143 | } 144 | 145 | $migration = $this->migrationsByTargetVersion[$sourceVersion]; 146 | 147 | // Final version too low 148 | if (version_compare($migration->getSourceVersion(), $targetVersion, '<')) { 149 | throw new MigrationFailedException(sprintf( 150 | 'No migration found to downgrade from version %s to %s.', 151 | $sourceVersion, 152 | $targetVersion 153 | )); 154 | } 155 | 156 | $migration->down($data); 157 | 158 | $this->versioner->updateVersion($data, $migration->getSourceVersion()); 159 | 160 | $sourceVersion = $migration->getSourceVersion(); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Migration/UnsupportedVersionException.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 Webmozart\Json\Migration; 13 | 14 | use Exception; 15 | use Webmozart\Json\Conversion\ConversionFailedException; 16 | 17 | /** 18 | * Thrown when a version is not supported. 19 | * 20 | * @since 1.3 21 | * 22 | * @author Bernhard Schussek 23 | */ 24 | class UnsupportedVersionException extends ConversionFailedException 25 | { 26 | /** 27 | * Creates an exception for an unknown version. 28 | * 29 | * @param string $version The version that caused the 30 | * exception 31 | * @param string[] $knownVersions The known versions 32 | * @param Exception|null $cause The exception that caused this 33 | * exception 34 | * 35 | * @return static The created exception 36 | */ 37 | public static function forVersion($version, array $knownVersions, Exception $cause = null) 38 | { 39 | usort($knownVersions, 'version_compare'); 40 | 41 | return new static(sprintf( 42 | 'Cannot process JSON at version %s. The supported versions '. 43 | 'are %s.', 44 | $version, 45 | implode(', ', $knownVersions) 46 | ), 0, $cause); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/UriRetriever/LocalUriRetriever.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 Webmozart\Json\UriRetriever; 13 | 14 | use JsonSchema\Uri\Retrievers\FileGetContents; 15 | use JsonSchema\Uri\Retrievers\UriRetrieverInterface; 16 | use Webmozart\PathUtil\Path; 17 | 18 | /** 19 | * @since 1.3 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class LocalUriRetriever implements UriRetrieverInterface 24 | { 25 | /** 26 | * @var string[] 27 | */ 28 | private $mappings; 29 | 30 | /** 31 | * @var string 32 | */ 33 | private $baseDir; 34 | 35 | /** 36 | * @var UriRetrieverInterface 37 | */ 38 | private $filesystemRetriever; 39 | 40 | /** 41 | * @var UriRetrieverInterface 42 | */ 43 | private $fallbackRetriever; 44 | 45 | /** 46 | * @var UriRetrieverInterface 47 | */ 48 | private $lastUsedRetriever; 49 | 50 | public function __construct($baseDir = null, array $mappings = array(), UriRetrieverInterface $fallbackRetriever = null) 51 | { 52 | $this->baseDir = $baseDir ? Path::canonicalize($baseDir) : null; 53 | $this->mappings = $mappings; 54 | $this->filesystemRetriever = new FileGetContents(); 55 | $this->fallbackRetriever = $fallbackRetriever ?: $this->filesystemRetriever; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function retrieve($uri) 62 | { 63 | if (isset($this->mappings[$uri])) { 64 | $uri = $this->mappings[$uri]; 65 | 66 | if (Path::isLocal($uri)) { 67 | $uri = 'file://'.($this->baseDir ? Path::makeAbsolute($uri, $this->baseDir) : $uri); 68 | } 69 | 70 | $this->lastUsedRetriever = $this->filesystemRetriever; 71 | 72 | return $this->filesystemRetriever->retrieve($uri); 73 | } 74 | 75 | $this->lastUsedRetriever = $this->fallbackRetriever; 76 | 77 | return $this->fallbackRetriever->retrieve($uri); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getContentType() 84 | { 85 | return $this->lastUsedRetriever ? $this->lastUsedRetriever->getContentType() : null; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Validation/ValidatingConverter.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 Webmozart\Json\Validation; 13 | 14 | use Webmozart\Json\Conversion\ConversionFailedException; 15 | use Webmozart\Json\Conversion\JsonConverter; 16 | use Webmozart\Json\InvalidSchemaException; 17 | use Webmozart\Json\JsonValidator; 18 | 19 | /** 20 | * A decorator for a {@link JsonCoverter} that validates the JSON data. 21 | * 22 | * Pass the path to the schema to the constructor: 23 | * 24 | * ~~~php 25 | * $converter = ConfigFileConverter(); 26 | * 27 | * // Decorate the converter 28 | * $converter = new ValidatingConverter($converter, __DIR__.'/schema.json'); 29 | * ~~~ 30 | * 31 | * Whenever you load or dump data as JSON, the JSON structure is validated 32 | * against the schema: 33 | * 34 | * ~~~php 35 | * $jsonDecoder = new JsonDecoder(); 36 | * $configFile = $converter->fromJson($jsonDecoder->decode($json)); 37 | * 38 | * $jsonEncoder = new JsonEncoder(); 39 | * $jsonEncoder->encode($converter->toJson($configFile)); 40 | * ~~~ 41 | * 42 | * If you want to dynamically determine the path to the schema file, pass a 43 | * callable instead of the string. This is especially useful when versioning 44 | * your JSON data: 45 | * 46 | * ~~~php 47 | * $converter = ConfigFileConverter(); 48 | * 49 | * // Calculate the schema path based on the "version" key in the JSON object 50 | * $getSchemaPath = function ($jsonData) { 51 | * return __DIR__.'/schema-'.$jsonData->version.'.json'; 52 | * } 53 | * 54 | * // Decorate the converter 55 | * $converter = new ValidatingConverter($converter, $getSchemaPath); 56 | * ~~~ 57 | * 58 | * @since 1.3 59 | * 60 | * @author Bernhard Schussek 61 | */ 62 | class ValidatingConverter implements JsonConverter 63 | { 64 | /** 65 | * @var JsonConverter 66 | */ 67 | private $innerConverter; 68 | 69 | /** 70 | * @var mixed 71 | */ 72 | private $schema; 73 | 74 | /** 75 | * @var JsonValidator 76 | */ 77 | private $jsonValidator; 78 | 79 | /** 80 | * Creates the converter. 81 | * 82 | * @param JsonConverter $innerConverter The decorated converter 83 | * @param string|callable|null $schema The path to the schema file 84 | * or a callable for calculating 85 | * the path dynamically for a 86 | * given JSON data. If `null`, 87 | * the schema is taken from the 88 | * `$schema` property of the 89 | * JSON data 90 | * @param JsonValidator $jsonValidator The JSON validator (optional) 91 | */ 92 | public function __construct(JsonConverter $innerConverter, $schema = null, JsonValidator $jsonValidator = null) 93 | { 94 | $this->innerConverter = $innerConverter; 95 | $this->schema = $schema; 96 | $this->jsonValidator = $jsonValidator ?: new JsonValidator(); 97 | } 98 | 99 | /** 100 | * {@inheritdoc} 101 | */ 102 | public function toJson($data, array $options = array()) 103 | { 104 | $jsonData = $this->innerConverter->toJson($data, $options); 105 | 106 | $this->validate($jsonData); 107 | 108 | return $jsonData; 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function fromJson($jsonData, array $options = array()) 115 | { 116 | $this->validate($jsonData); 117 | 118 | return $this->innerConverter->fromJson($jsonData, $options); 119 | } 120 | 121 | private function validate($jsonData) 122 | { 123 | $schema = $this->schema; 124 | 125 | if (is_callable($schema)) { 126 | $schema = $schema($jsonData); 127 | } 128 | 129 | try { 130 | $errors = $this->jsonValidator->validate($jsonData, $schema); 131 | } catch (InvalidSchemaException $e) { 132 | throw new ConversionFailedException(sprintf( 133 | 'An error occurred while loading the JSON schema (%s): %s', 134 | is_string($schema) ? '"'.$schema.'"' : gettype($schema), 135 | $e->getMessage() 136 | ), 0, $e); 137 | } 138 | 139 | if (count($errors) > 0) { 140 | throw new ConversionFailedException(sprintf( 141 | "The passed JSON did not match the schema:\n%s", 142 | implode("\n", $errors) 143 | )); 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/ValidationFailedException.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 Webmozart\Json; 13 | 14 | /** 15 | * Thrown when a JSON file contains invalid JSON. 16 | * 17 | * @since 1.0 18 | * 19 | * @author Bernhard Schussek 20 | */ 21 | class ValidationFailedException extends \Exception 22 | { 23 | private $errors; 24 | 25 | public static function fromErrors(array $errors = array(), $code = 0, \Exception $previous = null) 26 | { 27 | return new static(sprintf( 28 | "Validation of the JSON data failed:\n%s", 29 | implode("\n", $errors) 30 | ), $errors, $code, $previous); 31 | } 32 | 33 | public function __construct($message = '', array $errors = array(), $code = 0, \Exception $previous = null) 34 | { 35 | $this->errors = $errors; 36 | 37 | parent::__construct($message, $code, $previous); 38 | } 39 | 40 | /** 41 | * @return array 42 | */ 43 | public function getErrors() 44 | { 45 | return $this->errors; 46 | } 47 | 48 | /** 49 | * @return array 50 | */ 51 | public function getErrorsAsString() 52 | { 53 | return implode("\n", $this->errors); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Versioning/CannotParseVersionException.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 Webmozart\Json\Versioning; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown when a version cannot be parsed. 18 | * 19 | * @since 1.3 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class CannotParseVersionException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/Versioning/CannotUpdateVersionException.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 Webmozart\Json\Versioning; 13 | 14 | use RuntimeException; 15 | 16 | /** 17 | * Thrown when a version cannot be updated. 18 | * 19 | * @since 1.3 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class CannotUpdateVersionException extends RuntimeException 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /src/Versioning/JsonVersioner.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 Webmozart\Json\Versioning; 13 | 14 | use stdClass; 15 | 16 | /** 17 | * Parses and updates the version of a JSON object. 18 | * 19 | * @since 1.3 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | interface JsonVersioner 24 | { 25 | /** 26 | * Parses and returns the version of a JSON object. 27 | * 28 | * @param stdClass $jsonData The JSON object 29 | * 30 | * @return string The version 31 | * 32 | * @throws CannotParseVersionException If the version cannot be parsed 33 | */ 34 | public function parseVersion(stdClass $jsonData); 35 | 36 | /** 37 | * Updates the version of a JSON object. 38 | * 39 | * @param stdClass $jsonData The JSON object 40 | * @param string $version The version to set 41 | * 42 | * @throws CannotUpdateVersionException If the version cannot be updated 43 | */ 44 | public function updateVersion(stdClass $jsonData, $version); 45 | } 46 | -------------------------------------------------------------------------------- /src/Versioning/SchemaUriVersioner.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 Webmozart\Json\Versioning; 13 | 14 | use stdClass; 15 | 16 | /** 17 | * Expects the version to be set in the "$schema" field of a JSON object. 18 | * 19 | * @since 1.3 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class SchemaUriVersioner implements JsonVersioner 24 | { 25 | /** 26 | * The default pattern used to extract the version of a schema ID. 27 | */ 28 | const DEFAULT_PATTERN = '~(?<=/)\d+\.\d+(?=/)~'; 29 | 30 | /** 31 | * @var string 32 | */ 33 | private $pattern; 34 | 35 | public function __construct($pattern = self::DEFAULT_PATTERN) 36 | { 37 | $this->pattern = $pattern; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function parseVersion(stdClass $jsonData) 44 | { 45 | if (!isset($jsonData->{'$schema'})) { 46 | throw new CannotParseVersionException('Cannot find "$schema" property in JSON object.'); 47 | } 48 | 49 | $schema = $jsonData->{'$schema'}; 50 | 51 | if (!preg_match($this->pattern, $schema, $matches)) { 52 | throw new CannotParseVersionException(sprintf( 53 | 'Cannot find version of schema "%s" (pattern: "%s")', 54 | $schema, 55 | $this->pattern 56 | )); 57 | } 58 | 59 | return $matches[0]; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function updateVersion(stdClass $jsonData, $version) 66 | { 67 | if (!isset($jsonData->{'$schema'})) { 68 | throw new CannotUpdateVersionException('Cannot find "$schema" property in JSON object.'); 69 | } 70 | 71 | $previousSchema = $jsonData->{'$schema'}; 72 | $newSchema = preg_replace($this->pattern, $version, $previousSchema, -1, $count); 73 | 74 | if (1 !== $count) { 75 | throw new CannotUpdateVersionException(sprintf( 76 | 'Cannot update version of schema "%s" (pattern: "%s"): %s', 77 | $previousSchema, 78 | $this->pattern, 79 | $count < 1 ? 'Not found' : 'Found more than once' 80 | )); 81 | } 82 | 83 | $jsonData->{'$schema'} = $newSchema; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Versioning/VersionFieldVersioner.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 Webmozart\Json\Versioning; 13 | 14 | use stdClass; 15 | 16 | /** 17 | * Expects the version to be set in the "version" field of a JSON object. 18 | * 19 | * @since 1.3 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class VersionFieldVersioner implements JsonVersioner 24 | { 25 | /** 26 | * @var string 27 | */ 28 | private $fieldName; 29 | 30 | /** 31 | * Creates a new versioner. 32 | * 33 | * @param string $fieldName The name of the version field 34 | */ 35 | public function __construct($fieldName = 'version') 36 | { 37 | $this->fieldName = (string) $fieldName; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function parseVersion(stdClass $jsonData) 44 | { 45 | if (!isset($jsonData->{$this->fieldName})) { 46 | throw new CannotParseVersionException(sprintf( 47 | 'Cannot find "%s" property in JSON object.', 48 | $this->fieldName 49 | )); 50 | } 51 | 52 | return $jsonData->{$this->fieldName}; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function updateVersion(stdClass $jsonData, $version) 59 | { 60 | $jsonData->{$this->fieldName} = $version; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/Fixtures/box.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "schema.json" 4 | ], 5 | "compactors": [ 6 | "Herrera\\Box\\Compactor\\Json", 7 | "Herrera\\Box\\Compactor\\Php" 8 | ], 9 | "compression": "GZ", 10 | "output": "schema.phar", 11 | "chmod": "0755", 12 | "stub": true 13 | } 14 | -------------------------------------------------------------------------------- /tests/Fixtures/invalid.json: -------------------------------------------------------------------------------- 1 | "string" 2 | -------------------------------------------------------------------------------- /tests/Fixtures/schema-external-refs.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://webmozart.io/fixtures/schema-external-refs#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "type": "object", 5 | "patternProperties": { 6 | "^[a-zA-Z]": { 7 | "oneOf": [ 8 | { "$ref": "http://webmozart.io/fixtures/schema-refs#/definitions/stringDefinition" }, 9 | { "$ref": "http://webmozart.io/fixtures/schema-refs#/definitions/booleanDefinition" } 10 | ] 11 | } 12 | }, 13 | "additionalProperties": false 14 | } 15 | -------------------------------------------------------------------------------- /tests/Fixtures/schema-invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://webmozart.io/fixtures/schema-invalid#", 3 | "$schema": 12345 4 | } 5 | -------------------------------------------------------------------------------- /tests/Fixtures/schema-no-object.json: -------------------------------------------------------------------------------- 1 | "foobar" 2 | -------------------------------------------------------------------------------- /tests/Fixtures/schema-refs.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://webmozart.io/fixtures/schema-refs#", 3 | "$schema": "http://json-schema.org/draft-04/schema#", 4 | "type": "object", 5 | "definitions": { 6 | "stringDefinition": { 7 | "type": "string" 8 | }, 9 | "booleanDefinition": { 10 | "type": "boolean" 11 | } 12 | }, 13 | "patternProperties": { 14 | "^[a-zA-Z]": { 15 | "oneOf": [ 16 | { "$ref": "#/definitions/stringDefinition" }, 17 | { "$ref": "#/definitions/booleanDefinition" } 18 | ] 19 | } 20 | }, 21 | "additionalProperties": false 22 | } 23 | -------------------------------------------------------------------------------- /tests/Fixtures/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "http://webmozart.io/fixtures/schema#", 3 | "type": "object" 4 | } 5 | -------------------------------------------------------------------------------- /tests/Fixtures/schema.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmozart/json/d823428254474fc26aa499aebbda1315e2fedf3a/tests/Fixtures/schema.phar -------------------------------------------------------------------------------- /tests/Fixtures/valid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Bernhard" 3 | } 4 | -------------------------------------------------------------------------------- /tests/Fixtures/win-1258.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webmozart/json/d823428254474fc26aa499aebbda1315e2fedf3a/tests/Fixtures/win-1258.json -------------------------------------------------------------------------------- /tests/JsonDecoderTest.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 Webmozart\Json\Tests; 13 | 14 | use Webmozart\Json\JsonDecoder; 15 | 16 | /** 17 | * @since 1.0 18 | * 19 | * @author Bernhard Schussek 20 | */ 21 | class JsonDecoderTest extends \PHPUnit_Framework_TestCase 22 | { 23 | /** 24 | * @var JsonDecoder 25 | */ 26 | private $decoder; 27 | 28 | private $fixturesDir; 29 | 30 | private $schemaFile; 31 | 32 | private $schemaObject; 33 | 34 | protected function setUp() 35 | { 36 | $this->decoder = new JsonDecoder(); 37 | $this->fixturesDir = __DIR__.'/Fixtures'; 38 | $this->schemaFile = $this->fixturesDir.'/schema.json'; 39 | $this->schemaObject = json_decode(file_get_contents($this->schemaFile)); 40 | } 41 | 42 | public function testDecode() 43 | { 44 | $data = $this->decoder->decode('{ "name": "Bernhard" }'); 45 | 46 | $this->assertInstanceOf('\stdClass', $data); 47 | $this->assertObjectHasAttribute('name', $data); 48 | $this->assertSame('Bernhard', $data->name); 49 | } 50 | 51 | public function testDecodeEmptyObject() 52 | { 53 | $data = $this->decoder->decode('{}'); 54 | 55 | $this->assertEquals((object) array(), $data); 56 | $this->assertInstanceOf('\stdClass', $data); 57 | } 58 | 59 | public function testDecodeNull() 60 | { 61 | $data = $this->decoder->decode('null'); 62 | 63 | $this->assertNull($data); 64 | } 65 | 66 | public function testDecodeNestedEmptyObject() 67 | { 68 | $data = $this->decoder->decode('{ "empty": {} }'); 69 | 70 | $this->assertEquals((object) array('empty' => (object) array()), $data); 71 | $this->assertInstanceOf('\stdClass', $data); 72 | $this->assertInstanceOf('\stdClass', $data->empty); 73 | } 74 | 75 | public function testDecodeWithSchemaFile() 76 | { 77 | $data = $this->decoder->decode('{ "name": "Bernhard" }', $this->schemaFile); 78 | 79 | $this->assertInstanceOf('\stdClass', $data); 80 | $this->assertObjectHasAttribute('name', $data); 81 | $this->assertSame('Bernhard', $data->name); 82 | } 83 | 84 | public function testDecodeWithSchemaObject() 85 | { 86 | $data = $this->decoder->decode('{ "name": "Bernhard" }', $this->schemaObject); 87 | 88 | $this->assertInstanceOf('\stdClass', $data); 89 | $this->assertObjectHasAttribute('name', $data); 90 | $this->assertSame('Bernhard', $data->name); 91 | } 92 | 93 | /** 94 | * @expectedException \Webmozart\Json\ValidationFailedException 95 | */ 96 | public function testDecodeFailsIfValidationFailsWithSchemaFile() 97 | { 98 | $this->decoder->decode('"foobar"', $this->schemaFile); 99 | } 100 | 101 | /** 102 | * @expectedException \Webmozart\Json\ValidationFailedException 103 | */ 104 | public function testDecodeFailsIfValidationFailsWithSchemaObject() 105 | { 106 | $this->decoder->decode('"foobar"', $this->schemaObject); 107 | } 108 | 109 | public function testDecodeUtf8() 110 | { 111 | $data = $this->decoder->decode('{"name":"B\u00e9rnhard"}'); 112 | 113 | $this->assertEquals((object) array('name' => 'Bérnhard'), $data); 114 | } 115 | 116 | /** 117 | * JSON_ERROR_UTF8. 118 | * 119 | * @expectedException \Webmozart\Json\DecodingFailedException 120 | * @expectedExceptionCode 5 121 | */ 122 | public function testDecodeFailsIfNotUtf8() 123 | { 124 | if (defined('JSON_C_VERSION')) { 125 | $this->markTestSkipped('This error is not reported when using JSONC.'); 126 | } 127 | 128 | $win1258 = file_get_contents($this->fixturesDir.'/win-1258.json'); 129 | 130 | $this->decoder->decode($win1258); 131 | } 132 | 133 | public function testDecodeObjectAsObject() 134 | { 135 | $this->decoder->setObjectDecoding(JsonDecoder::OBJECT); 136 | 137 | $decoded = $this->decoder->decode('{ "name": "Bernhard" }'); 138 | 139 | $this->assertEquals((object) array('name' => 'Bernhard'), $decoded); 140 | } 141 | 142 | public function testDecodeObjectAsArray() 143 | { 144 | $this->decoder->setObjectDecoding(JsonDecoder::ASSOC_ARRAY); 145 | 146 | $decoded = $this->decoder->decode('{ "name": "Bernhard" }'); 147 | 148 | $this->assertEquals(array('name' => 'Bernhard'), $decoded); 149 | } 150 | 151 | public function testDecodeEmptyArrayKey() 152 | { 153 | $data = array('' => 'Bernhard'); 154 | 155 | $this->decoder->setObjectDecoding(JsonDecoder::ASSOC_ARRAY); 156 | 157 | $this->assertEquals($data, $this->decoder->decode('{"":"Bernhard"}')); 158 | } 159 | 160 | public function testDecodeEmptyProperty() 161 | { 162 | if (version_compare(PHP_VERSION, '7.1.0', '<')) { 163 | $this->markTestSkipped('PHP >= 7.1.0 only'); 164 | 165 | return; 166 | } 167 | 168 | $data = (object) array('' => 'Bernhard'); 169 | 170 | $this->assertEquals($data, $this->decoder->decode('{"":"Bernhard"}')); 171 | } 172 | 173 | public function testDecodeMagicEmptyPropertyAfter71() 174 | { 175 | if (version_compare(PHP_VERSION, '7.1.0', '<')) { 176 | $this->markTestSkipped('PHP >= 7.1.0 only'); 177 | 178 | return; 179 | } 180 | 181 | $data = (object) array('_empty_' => 'Bernhard'); 182 | 183 | $this->assertEquals($data, $this->decoder->decode('{"_empty_":"Bernhard"}')); 184 | } 185 | 186 | public function testDecodeMagicEmptyPropertyBefore71() 187 | { 188 | if (version_compare(PHP_VERSION, '7.1.0', '>=')) { 189 | $this->markTestSkipped('PHP < 7.1.0 only'); 190 | 191 | return; 192 | } 193 | 194 | $data = (object) array('a' => 'b', '_empty_' => 'Bernhard', 'c' => 'd'); 195 | 196 | $this->assertEquals($data, $this->decoder->decode('{"a":"b","":"Bernhard","c":"d"}')); 197 | } 198 | 199 | public function provideInvalidObjectDecoding() 200 | { 201 | return array( 202 | array(JsonDecoder::STRING), 203 | array(JsonDecoder::FLOAT), 204 | array(1234), 205 | ); 206 | } 207 | 208 | /** 209 | * @dataProvider provideInvalidObjectDecoding 210 | * @expectedException \InvalidArgumentException 211 | */ 212 | public function testFailIfInvalidObjectDecoding($invalidDecoding) 213 | { 214 | $this->decoder->setObjectDecoding($invalidDecoding); 215 | } 216 | 217 | /** 218 | * @expectedException \InvalidArgumentException 219 | */ 220 | public function testSchemaNotSupportedForArrays() 221 | { 222 | $this->decoder->setObjectDecoding(JsonDecoder::ASSOC_ARRAY); 223 | 224 | $this->decoder->decode('{ "name": "Bernhard" }', $this->schemaObject); 225 | } 226 | 227 | /** 228 | * JSON_ERROR_DEPTH. 229 | * 230 | * @expectedException \Webmozart\Json\DecodingFailedException 231 | * @expectedExceptionCode 1 232 | */ 233 | public function testMaxDepth1Exceeded() 234 | { 235 | $this->decoder->setMaxDepth(1); 236 | 237 | $this->decoder->decode('{ "name": "Bernhard" }'); 238 | } 239 | 240 | public function testMaxDepth1NotExceeded() 241 | { 242 | $this->decoder->setMaxDepth(1); 243 | 244 | $this->assertSame('Bernhard', $this->decoder->decode('"Bernhard"')); 245 | } 246 | 247 | /** 248 | * JSON_ERROR_DEPTH. 249 | * 250 | * @expectedException \Webmozart\Json\DecodingFailedException 251 | * @expectedExceptionCode 1 252 | */ 253 | public function testMaxDepth2Exceeded() 254 | { 255 | $this->decoder->setMaxDepth(2); 256 | 257 | $this->decoder->decode('{ "key": { "name": "Bernhard" } }'); 258 | } 259 | 260 | public function testMaxDepth2NotExceeded() 261 | { 262 | $this->decoder->setMaxDepth(2); 263 | 264 | $decoded = $this->decoder->decode('{ "name": "Bernhard" }'); 265 | 266 | $this->assertEquals((object) array('name' => 'Bernhard'), $decoded); 267 | } 268 | 269 | /** 270 | * @expectedException \InvalidArgumentException 271 | */ 272 | public function testMaxDepthMustBeInteger() 273 | { 274 | $this->decoder->setMaxDepth('foo'); 275 | } 276 | 277 | /** 278 | * @expectedException \InvalidArgumentException 279 | */ 280 | public function testMaxDepthMustBeOneOrGreater() 281 | { 282 | $this->decoder->setMaxDepth(0); 283 | } 284 | 285 | public function testDecodeBigIntAsFloat() 286 | { 287 | $this->decoder->setBigIntDecoding(JsonDecoder::FLOAT); 288 | 289 | $decoded = $this->decoder->decode('12312512423531123'); 290 | 291 | $this->assertEquals(12312512423531123.0, $decoded); 292 | } 293 | 294 | public function testDecodeBigIntAsString() 295 | { 296 | $this->decoder->setBigIntDecoding(JsonDecoder::STRING); 297 | 298 | $decoded = $this->decoder->decode('12312512423531123'); 299 | 300 | $this->assertEquals('12312512423531123', $decoded); 301 | } 302 | 303 | public function provideInvalidBigIntDecoding() 304 | { 305 | return array( 306 | array(JsonDecoder::OBJECT), 307 | array(JsonDecoder::ASSOC_ARRAY), 308 | array(1234), 309 | ); 310 | } 311 | 312 | /** 313 | * @dataProvider provideInvalidBigIntDecoding 314 | * @expectedException \InvalidArgumentException 315 | */ 316 | public function testFailIfInvalidBigIntDecoding($invalidDecoding) 317 | { 318 | $this->decoder->setBigIntDecoding($invalidDecoding); 319 | } 320 | 321 | public function testDecodeFile() 322 | { 323 | $data = $this->decoder->decodeFile($this->fixturesDir.'/valid.json'); 324 | 325 | $this->assertInstanceOf('\stdClass', $data); 326 | $this->assertObjectHasAttribute('name', $data); 327 | $this->assertSame('Bernhard', $data->name); 328 | } 329 | 330 | public function testDecodeFileFailsIfNotReadable() 331 | { 332 | if ('\\' === DIRECTORY_SEPARATOR) { 333 | $this->markTestSkipped('Cannot deny read access on Windows.'); 334 | } 335 | 336 | $tempFile = tempnam(sys_get_temp_dir(), 'JsonDecoderTest'); 337 | file_put_contents($tempFile, file_get_contents($this->fixturesDir.'/valid.json')); 338 | 339 | chmod($tempFile, 0000); 340 | 341 | // Test that the file name is present in the output. 342 | $this->setExpectedException( 343 | '\Webmozart\Json\IOException', 344 | $tempFile 345 | ); 346 | 347 | $this->decoder->decodeFile($tempFile); 348 | } 349 | 350 | /** 351 | * Test that the file name is present in the output. 352 | * 353 | * @expectedException \Webmozart\Json\FileNotFoundException 354 | * @expectedExceptionMessage bogus.json 355 | */ 356 | public function testDecodeFileFailsIfNotFound() 357 | { 358 | $this->decoder->decodeFile($this->fixturesDir.'/bogus.json'); 359 | } 360 | 361 | /** 362 | * Test that the file name is present in the output. 363 | * 364 | * @expectedException \Webmozart\Json\ValidationFailedException 365 | * @expectedExceptionMessage invalid.json 366 | */ 367 | public function testDecodeFileFailsIfValidationFailsWithSchemaFile() 368 | { 369 | $this->decoder->decodeFile($this->fixturesDir.'/invalid.json', $this->schemaFile); 370 | } 371 | 372 | /** 373 | * Test that the file name is present in the output. 374 | * 375 | * @expectedException \Webmozart\Json\ValidationFailedException 376 | * @expectedExceptionMessage invalid.json 377 | */ 378 | public function testDecodeFileFailsIfValidationFailsWithSchemaObject() 379 | { 380 | $this->decoder->decodeFile($this->fixturesDir.'/invalid.json', $this->schemaObject); 381 | } 382 | 383 | /** 384 | * Test that the file name is present in the output. 385 | * 386 | * @expectedException \Webmozart\Json\DecodingFailedException 387 | * @expectedExceptionMessage win-1258.json 388 | * @expectedExceptionCode 5 389 | */ 390 | public function testDecodeFileFailsIfNotUtf8() 391 | { 392 | if (defined('JSON_C_VERSION')) { 393 | $this->markTestSkipped('This error is not reported when using JSONC.'); 394 | } 395 | 396 | $this->decoder->decodeFile($this->fixturesDir.'/win-1258.json'); 397 | } 398 | 399 | /** 400 | * Test that the file name is present in the output. 401 | * 402 | * @expectedException \Webmozart\Json\InvalidSchemaException 403 | * @expectedExceptionMessage valid.json 404 | */ 405 | public function testDecodeFileFailsIfSchemaInvalid() 406 | { 407 | $this->decoder->decodeFile($this->fixturesDir.'/valid.json', 'bogus.json'); 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /tests/JsonEncoderTest.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 Webmozart\Json\Tests; 13 | 14 | use Symfony\Component\Filesystem\Filesystem; 15 | use Webmozart\Json\JsonEncoder; 16 | 17 | /** 18 | * @since 1.0 19 | * 20 | * @author Bernhard Schussek 21 | */ 22 | class JsonEncoderTest extends \PHPUnit_Framework_TestCase 23 | { 24 | const BINARY_INPUT = "\xff\xf0"; 25 | 26 | /** 27 | * @var JsonEncoder 28 | */ 29 | private $encoder; 30 | 31 | private $fixturesDir; 32 | 33 | private $schemaFile; 34 | 35 | private $schemaObject; 36 | 37 | private $tempDir; 38 | 39 | private $tempFile; 40 | 41 | public function provideValues() 42 | { 43 | return array( 44 | array(0, '0'), 45 | array(1, '1'), 46 | array(1234, '1234'), 47 | array('a', '"a"'), 48 | array('b', '"b"'), 49 | array('a/b', '"a\/b"'), 50 | array(12.34, '12.34'), 51 | array(true, 'true'), 52 | array(false, 'false'), 53 | array(null, 'null'), 54 | array(array(1, 2, 3, 4), '[1,2,3,4]'), 55 | array(array('foo' => 'bar', 'baz' => 'bam'), '{"foo":"bar","baz":"bam"}'), 56 | array((object) array('foo' => 'bar', 'baz' => 'bam'), '{"foo":"bar","baz":"bam"}'), 57 | ); 58 | } 59 | 60 | protected function setUp() 61 | { 62 | while (false === @mkdir($this->tempDir = sys_get_temp_dir().'/webmozart-JsonEncoderTest'.rand(10000, 99999), 0777, true)) { 63 | } 64 | 65 | $this->encoder = new JsonEncoder(); 66 | $this->fixturesDir = __DIR__.'/Fixtures'; 67 | $this->schemaFile = $this->fixturesDir.'/schema.json'; 68 | $this->schemaObject = json_decode(file_get_contents($this->schemaFile)); 69 | $this->tempFile = $this->tempDir.'/data.json'; 70 | } 71 | 72 | protected function tearDown() 73 | { 74 | $filesystem = new Filesystem(); 75 | 76 | // Ensure all files in the directory are writable before removing 77 | $filesystem->chmod($this->tempDir, 0755, 0000, true); 78 | $filesystem->remove($this->tempDir); 79 | } 80 | 81 | /** 82 | * @dataProvider provideValues 83 | */ 84 | public function testEncode($value, $json) 85 | { 86 | $this->assertSame($json, $this->encoder->encode($value)); 87 | } 88 | 89 | public function testEncodeWithSchemaFile() 90 | { 91 | $data = (object) array('name' => 'Bernhard'); 92 | 93 | $this->assertSame('{"name":"Bernhard"}', $this->encoder->encode($data, $this->schemaFile)); 94 | } 95 | 96 | public function testEncodeWithSchemaObject() 97 | { 98 | $data = (object) array('name' => 'Bernhard'); 99 | 100 | $this->assertSame('{"name":"Bernhard"}', $this->encoder->encode($data, $this->schemaObject)); 101 | } 102 | 103 | /** 104 | * @expectedException \Webmozart\Json\ValidationFailedException 105 | */ 106 | public function testEncodeFailsIfValidationFailsWithSchemaFile() 107 | { 108 | $this->encoder->encode('foobar', $this->schemaFile); 109 | } 110 | 111 | /** 112 | * @expectedException \Webmozart\Json\ValidationFailedException 113 | */ 114 | public function testEncodeFailsIfValidationFailsWithSchemaObject() 115 | { 116 | $this->encoder->encode('foobar', $this->schemaObject); 117 | } 118 | 119 | public function testEncodeUtf8() 120 | { 121 | $data = (object) array('name' => 'Bérnhard'); 122 | 123 | $this->assertSame('{"name":"B\u00e9rnhard"}', $this->encoder->encode($data)); 124 | } 125 | 126 | /** 127 | * JSON_ERROR_UTF8. 128 | * 129 | * @expectedException \Webmozart\Json\EncodingFailedException 130 | * @expectedExceptionCode 5 131 | */ 132 | public function testEncodeFailsIfNonUtf8() 133 | { 134 | if (version_compare(PHP_VERSION, '5.5.0', '<')) { 135 | $this->markTestSkipped('PHP >= 5.5.0 only'); 136 | 137 | return; 138 | } 139 | 140 | $this->encoder->encode(file_get_contents($this->fixturesDir.'/win-1258.json')); 141 | } 142 | 143 | /** 144 | * JSON_ERROR_UTF8. 145 | * 146 | * @expectedException \Webmozart\Json\EncodingFailedException 147 | * @expectedExceptionCode 5 148 | */ 149 | public function testEncodeFailsForBinaryValue() 150 | { 151 | $this->encoder->encode(self::BINARY_INPUT); 152 | } 153 | 154 | public function testEncodeEmptyArrayKey() 155 | { 156 | $data = array('' => 'Bernhard'); 157 | 158 | $this->assertSame('{"":"Bernhard"}', $this->encoder->encode($data)); 159 | } 160 | 161 | public function testEncodeEmptyProperty() 162 | { 163 | if (version_compare(PHP_VERSION, '7.1.0', '<')) { 164 | $this->markTestSkipped('PHP >= 7.1.0 only'); 165 | 166 | return; 167 | } 168 | 169 | $data = (object) array('' => 'Bernhard'); 170 | 171 | $this->assertSame('{"":"Bernhard"}', $this->encoder->encode($data)); 172 | } 173 | 174 | public function testEncodeMagicEmptyPropertyAfter71() 175 | { 176 | if (version_compare(PHP_VERSION, '7.1.0', '<')) { 177 | $this->markTestSkipped('PHP >= 7.1.0 only'); 178 | 179 | return; 180 | } 181 | 182 | $data = (object) array('_empty_' => 'Bernhard'); 183 | 184 | $this->assertSame('{"_empty_":"Bernhard"}', $this->encoder->encode($data)); 185 | } 186 | 187 | public function testEncodeMagicEmptyPropertyBefore71() 188 | { 189 | if (version_compare(PHP_VERSION, '7.1.0', '>=')) { 190 | $this->markTestSkipped('PHP < 7.1.0 only'); 191 | 192 | return; 193 | } 194 | 195 | $data = (object) array('a' => 'b', '_empty_' => 'Bernhard', 'c' => 'd'); 196 | 197 | $this->assertSame('{"a":"b","":"Bernhard","c":"d"}', $this->encoder->encode($data)); 198 | } 199 | 200 | public function testEncodeArrayAsArray() 201 | { 202 | $data = array('one', 'two'); 203 | 204 | $this->encoder->setArrayEncoding(JsonEncoder::JSON_ARRAY); 205 | 206 | $this->assertSame('["one","two"]', $this->encoder->encode($data)); 207 | } 208 | 209 | public function testEncodeArrayAsObject() 210 | { 211 | $data = array('one', 'two'); 212 | 213 | $this->encoder->setArrayEncoding(JsonEncoder::JSON_OBJECT); 214 | 215 | $this->assertSame('{"0":"one","1":"two"}', $this->encoder->encode($data)); 216 | } 217 | 218 | public function provideInvalidArrayEncoding() 219 | { 220 | return array( 221 | array(JsonEncoder::JSON_NUMBER), 222 | array(JsonEncoder::JSON_STRING), 223 | array(1234), 224 | ); 225 | } 226 | 227 | /** 228 | * @dataProvider provideInvalidArrayEncoding 229 | * @expectedException \InvalidArgumentException 230 | */ 231 | public function testFailIfInvalidArrayEncoding($invalidEncoding) 232 | { 233 | $this->encoder->setArrayEncoding($invalidEncoding); 234 | } 235 | 236 | public function testEncodeNumericAsString() 237 | { 238 | $data = '12345'; 239 | 240 | $this->encoder->setNumericEncoding(JsonEncoder::JSON_STRING); 241 | 242 | $this->assertSame('"12345"', $this->encoder->encode($data)); 243 | } 244 | 245 | public function testEncodeIntegerStringAsInteger() 246 | { 247 | $data = '12345'; 248 | 249 | $this->encoder->setNumericEncoding(JsonEncoder::JSON_NUMBER); 250 | 251 | $this->assertSame('12345', $this->encoder->encode($data)); 252 | } 253 | 254 | public function testEncodeIntegerFloatAsFloat() 255 | { 256 | $data = '123.45'; 257 | 258 | $this->encoder->setNumericEncoding(JsonEncoder::JSON_NUMBER); 259 | 260 | $this->assertSame('123.45', $this->encoder->encode($data)); 261 | } 262 | 263 | public function provideInvalidNumericEncoding() 264 | { 265 | return array( 266 | array(JsonEncoder::JSON_ARRAY), 267 | array(JsonEncoder::JSON_OBJECT), 268 | array(1234), 269 | ); 270 | } 271 | 272 | /** 273 | * @dataProvider provideInvalidNumericEncoding 274 | * @expectedException \InvalidArgumentException 275 | */ 276 | public function testFailIfInvalidNumericEncoding($invalidEncoding) 277 | { 278 | $this->encoder->setNumericEncoding($invalidEncoding); 279 | } 280 | 281 | public function testGtLtEscaoed() 282 | { 283 | $this->encoder->setEscapeGtLt(true); 284 | 285 | $this->assertSame('"\u003C\u003E"', $this->encoder->encode('<>')); 286 | } 287 | 288 | public function testGtLtNotEscaoed() 289 | { 290 | $this->encoder->setEscapeGtLt(false); 291 | 292 | $this->assertSame('"<>"', $this->encoder->encode('<>')); 293 | } 294 | 295 | public function testAmpersandEscaped() 296 | { 297 | $this->encoder->setEscapeAmpersand(true); 298 | 299 | $this->assertSame('"\u0026"', $this->encoder->encode('&')); 300 | } 301 | 302 | public function testAmpersandNotEscaped() 303 | { 304 | $this->encoder->setEscapeAmpersand(false); 305 | 306 | $this->assertSame('"&"', $this->encoder->encode('&')); 307 | } 308 | 309 | public function testSingleQuoteEscaped() 310 | { 311 | $this->encoder->setEscapeSingleQuote(true); 312 | 313 | $this->assertSame('"\u0027"', $this->encoder->encode("'")); 314 | } 315 | 316 | public function testSingleQuoteNotEscaped() 317 | { 318 | $this->encoder->setEscapeSingleQuote(false); 319 | 320 | $this->assertSame('"\'"', $this->encoder->encode("'")); 321 | } 322 | 323 | public function testDoubleQuoteEscaped() 324 | { 325 | $this->encoder->setEscapeDoubleQuote(true); 326 | 327 | $this->assertSame('"\u0022"', $this->encoder->encode('"')); 328 | } 329 | 330 | public function testDoubleQuoteNotEscaped() 331 | { 332 | $this->encoder->setEscapeDoubleQuote(false); 333 | 334 | $this->assertSame('"\""', $this->encoder->encode('"')); 335 | } 336 | 337 | public function testSlashEscaped() 338 | { 339 | $this->encoder->setEscapeSlash(true); 340 | 341 | $this->assertSame('"\\/"', $this->encoder->encode('/')); 342 | } 343 | 344 | public function testSlashNotEscaped() 345 | { 346 | $this->encoder->setEscapeSlash(false); 347 | 348 | $this->assertSame('"/"', $this->encoder->encode('/')); 349 | } 350 | 351 | public function testUnicodeEscaped() 352 | { 353 | $this->encoder->setEscapeUnicode(true); 354 | 355 | $this->assertSame('"\u00fc"', $this->encoder->encode('ü')); 356 | } 357 | 358 | public function testUnicodeNotEscaped() 359 | { 360 | if (version_compare(PHP_VERSION, '5.4.0', '<')) { 361 | $this->markTestSkipped('PHP >= 5.4.0 only'); 362 | 363 | return; 364 | } 365 | 366 | $this->encoder->setEscapeUnicode(false); 367 | 368 | $this->assertSame('"ü"', $this->encoder->encode('ü')); 369 | } 370 | 371 | /** 372 | * JSON_ERROR_DEPTH. 373 | * 374 | * @expectedException \Webmozart\Json\EncodingFailedException 375 | * @expectedExceptionCode 1 376 | */ 377 | public function testMaxDepth1Exceeded() 378 | { 379 | if (version_compare(PHP_VERSION, '5.5.0', '<')) { 380 | $this->markTestSkipped('PHP >= 5.5.0 only'); 381 | 382 | return; 383 | } 384 | 385 | $this->encoder->setMaxDepth(1); 386 | 387 | $this->encoder->encode((object) array('name' => 'Bernhard')); 388 | } 389 | 390 | public function testMaxDepth1NotExceeded() 391 | { 392 | $this->encoder->setMaxDepth(1); 393 | 394 | $this->assertSame('"Bernhard"', $this->encoder->encode('Bernhard')); 395 | } 396 | 397 | /** 398 | * JSON_ERROR_DEPTH. 399 | * 400 | * @expectedException \Webmozart\Json\EncodingFailedException 401 | * @expectedExceptionCode 1 402 | */ 403 | public function testMaxDepth2Exceeded() 404 | { 405 | if (version_compare(PHP_VERSION, '5.5.0', '<')) { 406 | $this->markTestSkipped('PHP >= 5.5.0 only'); 407 | 408 | return; 409 | } 410 | 411 | $this->encoder->setMaxDepth(2); 412 | 413 | $this->encoder->encode((object) array('key' => (object) array('name' => 'Bernhard'))); 414 | } 415 | 416 | public function testMaxDepth2NotExceeded() 417 | { 418 | $this->encoder->setMaxDepth(2); 419 | 420 | $this->assertSame('{"name":"Bernhard"}', $this->encoder->encode((object) array('name' => 'Bernhard'))); 421 | } 422 | 423 | /** 424 | * @expectedException \InvalidArgumentException 425 | */ 426 | public function testMaxDepthMustBeInteger() 427 | { 428 | $this->encoder->setMaxDepth('foo'); 429 | } 430 | 431 | /** 432 | * @expectedException \InvalidArgumentException 433 | */ 434 | public function testMaxDepthMustBeOneOrGreater() 435 | { 436 | $this->encoder->setMaxDepth(0); 437 | } 438 | 439 | public function testPrettyPrinting() 440 | { 441 | if (version_compare(PHP_VERSION, '5.4.0', '<')) { 442 | $this->markTestSkipped('PHP >= 5.4.0 only'); 443 | 444 | return; 445 | } 446 | 447 | $this->encoder->setPrettyPrinting(true); 448 | 449 | $this->assertSame("{\n \"name\": \"Bernhard\"\n}", $this->encoder->encode((object) array('name' => 'Bernhard'))); 450 | } 451 | 452 | public function testNoPrettyPrinting() 453 | { 454 | $this->encoder->setPrettyPrinting(false); 455 | 456 | $this->assertSame('{"name":"Bernhard"}', $this->encoder->encode((object) array('name' => 'Bernhard'))); 457 | } 458 | 459 | public function testTerminateWithLineFeed() 460 | { 461 | $this->encoder->setTerminateWithLineFeed(true); 462 | 463 | $this->assertSame('{"name":"Bernhard"}'."\n", $this->encoder->encode((object) array('name' => 'Bernhard'))); 464 | } 465 | 466 | public function testDoNotTerminateWithLineFeed() 467 | { 468 | $this->encoder->setTerminateWithLineFeed(false); 469 | 470 | $this->assertSame('{"name":"Bernhard"}', $this->encoder->encode((object) array('name' => 'Bernhard'))); 471 | } 472 | 473 | public function testEncodeFile() 474 | { 475 | $data = (object) array('name' => 'Bernhard'); 476 | 477 | $this->encoder->encodeFile($data, $this->tempFile); 478 | 479 | $this->assertFileExists($this->tempFile); 480 | $this->assertSame('{"name":"Bernhard"}', file_get_contents($this->tempFile)); 481 | } 482 | 483 | public function testEncodeFileCreatesMissingDirectories() 484 | { 485 | $data = (object) array('name' => 'Bernhard'); 486 | 487 | $this->encoder->encodeFile($data, $this->tempDir.'/sub/data.json'); 488 | 489 | $this->assertFileExists($this->tempDir.'/sub/data.json'); 490 | $this->assertSame('{"name":"Bernhard"}', file_get_contents($this->tempDir.'/sub/data.json')); 491 | } 492 | 493 | public function testEncodeFileFailsIfNotWritable() 494 | { 495 | $data = (object) array('name' => 'Bernhard'); 496 | 497 | touch($this->tempFile); 498 | chmod($this->tempFile, 0400); 499 | 500 | // Test that the file name is present in the output. 501 | $this->setExpectedException( 502 | '\Webmozart\Json\IOException', 503 | $this->tempFile 504 | ); 505 | 506 | $this->encoder->encodeFile($data, $this->tempFile); 507 | } 508 | 509 | public function testEncodeFileFailsIfValidationFailsWithSchemaFile() 510 | { 511 | // Test that the file name is present in the output. 512 | $this->setExpectedException( 513 | '\Webmozart\Json\ValidationFailedException', 514 | $this->tempFile 515 | ); 516 | 517 | $this->encoder->encodeFile('foobar', $this->tempFile, $this->schemaFile); 518 | } 519 | 520 | public function testEncodeFileFailsIfValidationFailsWithSchemaObject() 521 | { 522 | // Test that the file name is present in the output. 523 | $this->setExpectedException( 524 | '\Webmozart\Json\ValidationFailedException', 525 | $this->tempFile 526 | ); 527 | 528 | $this->encoder->encodeFile('foobar', $this->tempFile, $this->schemaObject); 529 | } 530 | 531 | public function testEncodeFileFailsIfNonUtf8() 532 | { 533 | if (version_compare(PHP_VERSION, '5.5.0', '<')) { 534 | $this->markTestSkipped('PHP >= 5.5.0 only'); 535 | 536 | return; 537 | } 538 | 539 | // Test that the file name is present in the output. 540 | $this->setExpectedException( 541 | '\Webmozart\Json\EncodingFailedException', 542 | $this->tempFile, 543 | 5 544 | ); 545 | 546 | $this->encoder->encodeFile(file_get_contents($this->fixturesDir.'/win-1258.json'), 547 | $this->tempFile); 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /tests/JsonValidatorTest.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 Webmozart\Json\Tests; 13 | 14 | use JsonSchema\Uri\Retrievers\PredefinedArray; 15 | use JsonSchema\Uri\UriRetriever; 16 | use Webmozart\Json\JsonValidator; 17 | 18 | /** 19 | * @since 1.0 20 | * 21 | * @author Bernhard Schussek 22 | */ 23 | class JsonValidatorTest extends \PHPUnit_Framework_TestCase 24 | { 25 | /** 26 | * @var JsonValidator 27 | */ 28 | private $validator; 29 | 30 | private $fixturesDir; 31 | 32 | private $schemaFile; 33 | 34 | private $schemaObject; 35 | 36 | protected function setUp() 37 | { 38 | $this->validator = new JsonValidator(); 39 | $this->fixturesDir = strtr(__DIR__.'/Fixtures', '\\', '/'); 40 | $this->schemaFile = $this->fixturesDir.'/schema.json'; 41 | $this->schemaObject = json_decode(file_get_contents($this->schemaFile)); 42 | } 43 | 44 | public function testValidateWithSchemaFile() 45 | { 46 | $errors = $this->validator->validate( 47 | (object) array('name' => 'Bernhard'), 48 | $this->schemaFile 49 | ); 50 | 51 | $this->assertCount(0, $errors); 52 | } 53 | 54 | public function testValidateWithSchemaFileInPhar() 55 | { 56 | // Work-around for https://bugs.php.net/bug.php?id=71368: 57 | // "format": "uri" validation removed for "id" field in meta-schema.json 58 | 59 | $errors = $this->validator->validate( 60 | (object) array('name' => 'Bernhard'), 61 | 'phar://'.$this->fixturesDir.'/schema.phar/schema.json' 62 | ); 63 | 64 | $this->assertCount(0, $errors); 65 | } 66 | 67 | public function testValidateWithSchemaObject() 68 | { 69 | $errors = $this->validator->validate( 70 | (object) array('name' => 'Bernhard'), 71 | $this->schemaObject 72 | ); 73 | 74 | $this->assertCount(0, $errors); 75 | } 76 | 77 | public function testValidateWithSchemaField() 78 | { 79 | $uriRetriever = new UriRetriever(); 80 | $uriRetriever->setUriRetriever(new PredefinedArray(array( 81 | 'http://webmozart.io/fixtures/schema' => file_get_contents(__DIR__.'/Fixtures/schema.json'), 82 | ))); 83 | 84 | $this->validator = new JsonValidator(null, $uriRetriever); 85 | 86 | $errors = $this->validator->validate((object) array( 87 | '$schema' => 'http://webmozart.io/fixtures/schema', 88 | 'name' => 'Bernhard', 89 | )); 90 | 91 | $this->assertCount(0, $errors); 92 | } 93 | 94 | public function testValidateWithReferences() 95 | { 96 | $errors = $this->validator->validate( 97 | (object) array('name' => 'Bernhard', 'has-coffee' => true), 98 | $this->fixturesDir.'/schema-refs.json' 99 | ); 100 | 101 | $this->assertCount(0, $errors); 102 | } 103 | 104 | public function testValidateWithExternalReferences() 105 | { 106 | $uriRetriever = new UriRetriever(); 107 | $uriRetriever->setUriRetriever(new PredefinedArray(array( 108 | 'http://webmozart.io/fixtures/schema-refs' => file_get_contents(__DIR__.'/Fixtures/schema-refs.json'), 109 | 'file://'.$this->fixturesDir.'/schema-external-refs.json' => file_get_contents(__DIR__.'/Fixtures/schema-external-refs.json'), 110 | ))); 111 | 112 | $this->validator = new JsonValidator(null, $uriRetriever); 113 | 114 | $errors = $this->validator->validate( 115 | (object) array('name' => 'Bernhard', 'has-coffee' => true), 116 | $this->fixturesDir.'/schema-external-refs.json' 117 | ); 118 | 119 | $this->assertCount(0, $errors); 120 | } 121 | 122 | public function testValidateFailsIfValidationFailsWithSchemaFile() 123 | { 124 | $errors = $this->validator->validate('foobar', $this->schemaFile); 125 | 126 | $this->assertCount(1, $errors); 127 | } 128 | 129 | public function testValidateFailsIfValidationFailsWithSchemaObject() 130 | { 131 | $errors = $this->validator->validate('foobar', $this->schemaObject); 132 | 133 | $this->assertCount(1, $errors); 134 | } 135 | 136 | public function testValidateFailsIfValidationFailsWithReferences() 137 | { 138 | $errors = $this->validator->validate( 139 | (object) array('name' => 'Bernhard', 'has-coffee' => null), 140 | $this->fixturesDir.'/schema-refs.json' 141 | ); 142 | 143 | $this->assertGreaterThan(1, count($errors)); 144 | } 145 | 146 | public function testValidateFailsIfValidationFailsWithExternalReferences() 147 | { 148 | $uriRetriever = new UriRetriever(); 149 | $uriRetriever->setUriRetriever(new PredefinedArray(array( 150 | 'http://webmozart.io/fixtures/schema-refs' => file_get_contents(__DIR__.'/Fixtures/schema-refs.json'), 151 | 'file://'.$this->fixturesDir.'/schema-external-refs.json' => file_get_contents(__DIR__.'/Fixtures/schema-external-refs.json'), 152 | ))); 153 | 154 | $this->validator = new JsonValidator(null, $uriRetriever); 155 | 156 | $errors = $this->validator->validate( 157 | (object) array('name' => 'Bernhard', 'has-coffee' => null), 158 | $this->fixturesDir.'/schema-external-refs.json' 159 | ); 160 | 161 | $this->assertGreaterThan(1, count($errors)); 162 | } 163 | 164 | /** 165 | * Test that the file name is mentioned in the output. 166 | * 167 | * @expectedException \Webmozart\Json\InvalidSchemaException 168 | * @expectedExceptionMessage bogus.json 169 | */ 170 | public function testValidateFailsIfSchemaFileNotFound() 171 | { 172 | $this->validator->validate((object) array('name' => 'Bernhard'), __DIR__.'/bogus.json'); 173 | } 174 | 175 | /** 176 | * @expectedException \Webmozart\Json\InvalidSchemaException 177 | */ 178 | public function testValidateFailsIfSchemaNeitherStringNorObject() 179 | { 180 | $this->validator->validate((object) array('name' => 'Bernhard'), 12345); 181 | } 182 | 183 | /** 184 | * @expectedException \Webmozart\Json\InvalidSchemaException 185 | */ 186 | public function testValidateFailsIfSchemaFileContainsNoObject() 187 | { 188 | $this->validator->validate( 189 | (object) array('name' => 'Bernhard'), 190 | $this->fixturesDir.'/schema-no-object.json' 191 | ); 192 | } 193 | 194 | /** 195 | * @expectedException \Webmozart\Json\InvalidSchemaException 196 | */ 197 | public function testValidateFailsIfSchemaFileInvalid() 198 | { 199 | $this->validator->validate( 200 | (object) array('name' => 'Bernhard'), 201 | $this->fixturesDir.'/schema-invalid.json' 202 | ); 203 | } 204 | 205 | /** 206 | * @expectedException \Webmozart\Json\InvalidSchemaException 207 | */ 208 | public function testValidateFailsIfSchemaObjectInvalid() 209 | { 210 | $this->validator->validate( 211 | (object) array('name' => 'Bernhard'), 212 | (object) array('id' => 12345) 213 | ); 214 | } 215 | 216 | /** 217 | * @expectedException \Webmozart\Json\InvalidSchemaException 218 | */ 219 | public function testValidateFailsIfInvalidSchemaNotRecognized() 220 | { 221 | // justinrainbow/json-schema cannot validate "anyOf", so the following 222 | // will load the schema successfully and fail when the file is validated 223 | // against the schema 224 | $this->validator->validate( 225 | (object) array('name' => 'Bernhard'), 226 | (object) array('type' => 12345) 227 | ); 228 | } 229 | 230 | /** 231 | * @expectedException \Webmozart\Json\InvalidSchemaException 232 | */ 233 | public function testValidateFailsIfMissingSchema() 234 | { 235 | $this->validator->validate( 236 | (object) array('name' => 'Bernhard') 237 | ); 238 | } 239 | 240 | /** 241 | * @expectedException \Webmozart\Json\InvalidSchemaException 242 | */ 243 | public function testValidateFailsIfInvalidSchemaType() 244 | { 245 | $this->validator->validate( 246 | (object) array('name' => 'Bernhard'), 247 | 1234 248 | ); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /tests/Migration/MigratingConverterTest.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 Webmozart\Json\Tests\Migration; 13 | 14 | use PHPUnit_Framework_Assert; 15 | use PHPUnit_Framework_MockObject_MockObject; 16 | use PHPUnit_Framework_TestCase; 17 | use stdClass; 18 | use Webmozart\Json\Conversion\JsonConverter; 19 | use Webmozart\Json\Migration\MigratingConverter; 20 | use Webmozart\Json\Migration\MigrationManager; 21 | 22 | /** 23 | * @since 1.3 24 | * 25 | * @author Bernhard Schussek 26 | */ 27 | class MigratingConverterTest extends PHPUnit_Framework_TestCase 28 | { 29 | /** 30 | * @var PHPUnit_Framework_MockObject_MockObject|JsonConverter 31 | */ 32 | private $innerConverter; 33 | 34 | /** 35 | * @var PHPUnit_Framework_MockObject_MockObject|MigrationManager 36 | */ 37 | private $migrationManager; 38 | 39 | /** 40 | * @var MigratingConverter 41 | */ 42 | private $converter; 43 | 44 | protected function setUp() 45 | { 46 | $this->migrationManager = $this->getMockBuilder('Webmozart\Json\Migration\MigrationManager') 47 | ->disableOriginalConstructor() 48 | ->getMock(); 49 | $this->migrationManager->expects($this->any()) 50 | ->method('getKnownVersions') 51 | ->willReturn(array('0.9', '1.0')); 52 | $this->innerConverter = $this->getMock('Webmozart\Json\Conversion\JsonConverter'); 53 | $this->converter = new MigratingConverter($this->innerConverter, '1.0', $this->migrationManager); 54 | } 55 | 56 | public function testToJsonDowngradesIfLowerVersion() 57 | { 58 | $options = array( 59 | 'inner_option' => 'value', 60 | 'targetVersion' => '0.9', 61 | ); 62 | 63 | $beforeMigration = (object) array( 64 | 'version' => '1.0', 65 | ); 66 | 67 | $afterMigration = (object) array( 68 | 'version' => '0.9', 69 | 'downgraded' => true, 70 | ); 71 | 72 | $this->innerConverter->expects($this->once()) 73 | ->method('toJson') 74 | ->with('DATA', $options) 75 | ->willReturn($beforeMigration); 76 | 77 | $this->migrationManager->expects($this->once()) 78 | ->method('migrate') 79 | ->willReturnCallback(function (stdClass $jsonData, $targetVersion) use ($beforeMigration) { 80 | // with() in combination with argument cloning doesn't work, 81 | // since we *want* to modify the original data (not the clone) below 82 | PHPUnit_Framework_Assert::assertEquals($beforeMigration, $jsonData); 83 | 84 | $jsonData->version = $targetVersion; 85 | $jsonData->downgraded = true; 86 | }); 87 | 88 | $this->assertEquals($afterMigration, $this->converter->toJson('DATA', $options)); 89 | } 90 | 91 | public function testToJsonDoesNotMigrateCurrentVersion() 92 | { 93 | $options = array( 94 | 'inner_option' => 'value', 95 | 'targetVersion' => '1.0', 96 | ); 97 | 98 | $jsonData = (object) array( 99 | 'version' => '1.0', 100 | ); 101 | 102 | $this->innerConverter->expects($this->once()) 103 | ->method('toJson') 104 | ->with('DATA', $options) 105 | ->willReturn($jsonData); 106 | 107 | $this->migrationManager->expects($this->never()) 108 | ->method('migrate'); 109 | 110 | $this->assertEquals($jsonData, $this->converter->toJson('DATA', $options)); 111 | } 112 | 113 | public function testToJsonDoesNotMigrateIfNoTargetVersion() 114 | { 115 | $options = array( 116 | 'inner_option' => 'value', 117 | ); 118 | 119 | $jsonData = (object) array( 120 | 'version' => '1.0', 121 | ); 122 | 123 | $this->innerConverter->expects($this->once()) 124 | ->method('toJson') 125 | ->with('DATA', $options) 126 | ->willReturn($jsonData); 127 | 128 | $this->migrationManager->expects($this->never()) 129 | ->method('migrate'); 130 | 131 | $this->assertEquals($jsonData, $this->converter->toJson('DATA', $options)); 132 | } 133 | 134 | /** 135 | * @expectedException \Webmozart\Json\Migration\UnsupportedVersionException 136 | */ 137 | public function testToJsonFailsIfTargetVersionTooHigh() 138 | { 139 | $this->converter->toJson('DATA', array('targetVersion' => '1.1')); 140 | } 141 | 142 | /** 143 | * @expectedException \Webmozart\Json\Conversion\ConversionFailedException 144 | */ 145 | public function testToJsonFailsIfNotAnObject() 146 | { 147 | $options = array( 148 | 'inner_option' => 'value', 149 | 'targetVersion' => '1.0', 150 | ); 151 | 152 | $this->innerConverter->expects($this->once()) 153 | ->method('toJson') 154 | ->with('DATA', $options) 155 | ->willReturn('foobar'); 156 | 157 | $this->migrationManager->expects($this->never()) 158 | ->method('migrate'); 159 | 160 | $this->converter->toJson('DATA', $options); 161 | } 162 | 163 | public function testFromJsonUpgradesIfVersionTooLow() 164 | { 165 | $options = array( 166 | 'inner_option' => 'value', 167 | ); 168 | 169 | $beforeMigration = (object) array( 170 | 'version' => '0.9', 171 | ); 172 | 173 | $afterMigration = (object) array( 174 | 'version' => '1.0', 175 | 'upgraded' => true, 176 | ); 177 | 178 | $this->migrationManager->expects($this->once()) 179 | ->method('migrate') 180 | ->willReturnCallback(function (stdClass $jsonData, $targetVersion) use ($beforeMigration) { 181 | PHPUnit_Framework_Assert::assertEquals($beforeMigration, $jsonData); 182 | 183 | $jsonData->version = $targetVersion; 184 | $jsonData->upgraded = true; 185 | }); 186 | 187 | $this->innerConverter->expects($this->once()) 188 | ->method('fromJson') 189 | ->with($afterMigration, $options) 190 | ->willReturn('DATA'); 191 | 192 | $result = $this->converter->fromJson(clone $beforeMigration, $options); 193 | 194 | $this->assertSame('DATA', $result); 195 | } 196 | 197 | public function testFromJsonDoesNotMigrateCurrentVersion() 198 | { 199 | $options = array( 200 | 'inner_option' => 'value', 201 | ); 202 | 203 | $jsonData = (object) array( 204 | 'version' => '1.0', 205 | ); 206 | 207 | $this->migrationManager->expects($this->never()) 208 | ->method('migrate'); 209 | 210 | $this->innerConverter->expects($this->once()) 211 | ->method('fromJson') 212 | ->with($jsonData, $options) 213 | ->willReturn('DATA'); 214 | 215 | $result = $this->converter->fromJson(clone $jsonData, $options); 216 | 217 | $this->assertSame('DATA', $result); 218 | } 219 | 220 | /** 221 | * @expectedException \Webmozart\Json\Migration\UnsupportedVersionException 222 | */ 223 | public function testFromJsonFailsIfSourceVersionTooHigh() 224 | { 225 | $jsonData = (object) array( 226 | 'version' => '1.1', 227 | ); 228 | 229 | $this->converter->fromJson($jsonData); 230 | } 231 | 232 | /** 233 | * @expectedException \Webmozart\Json\Conversion\ConversionFailedException 234 | */ 235 | public function testFromJsonFailsIfNotAnObject() 236 | { 237 | $this->migrationManager->expects($this->never()) 238 | ->method('migrate'); 239 | 240 | $this->innerConverter->expects($this->never()) 241 | ->method('fromJson'); 242 | 243 | $this->converter->fromJson('foobar'); 244 | } 245 | 246 | /** 247 | * @expectedException \Webmozart\Json\Conversion\ConversionFailedException 248 | */ 249 | public function testFromJsonFailsIfVersionIsMissing() 250 | { 251 | $this->migrationManager->expects($this->never()) 252 | ->method('migrate'); 253 | 254 | $this->innerConverter->expects($this->never()) 255 | ->method('fromJson'); 256 | 257 | $this->converter->fromJson((object) array()); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /tests/Migration/MigrationManagerTest.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 Webmozart\Json\Tests\Migration; 13 | 14 | use PHPUnit_Framework_Assert; 15 | use PHPUnit_Framework_MockObject_MockObject; 16 | use PHPUnit_Framework_TestCase; 17 | use stdClass; 18 | use Webmozart\Json\Migration\JsonMigration; 19 | use Webmozart\Json\Migration\MigrationManager; 20 | use Webmozart\Json\Versioning\JsonVersioner; 21 | 22 | /** 23 | * @since 1.3 24 | * 25 | * @author Bernhard Schussek 26 | */ 27 | class MigrationManagerTest extends PHPUnit_Framework_TestCase 28 | { 29 | /** 30 | * @var PHPUnit_Framework_MockObject_MockObject|JsonMigration 31 | */ 32 | private $migration1; 33 | 34 | /** 35 | * @var PHPUnit_Framework_MockObject_MockObject|JsonMigration 36 | */ 37 | private $migration2; 38 | 39 | /** 40 | * @var PHPUnit_Framework_MockObject_MockObject|JsonMigration 41 | */ 42 | private $migration3; 43 | 44 | /** 45 | * @var PHPUnit_Framework_MockObject_MockObject|JsonVersioner 46 | */ 47 | private $versioner; 48 | 49 | /** 50 | * @var MigrationManager 51 | */ 52 | private $manager; 53 | 54 | protected function setUp() 55 | { 56 | $this->migration1 = $this->createMigrationMock('0.8', '0.10'); 57 | $this->migration2 = $this->createMigrationMock('0.10', '1.0'); 58 | $this->migration3 = $this->createMigrationMock('1.0', '2.0'); 59 | $this->versioner = $this->getMock('Webmozart\Json\Versioning\JsonVersioner'); 60 | $this->manager = new MigrationManager(array( 61 | $this->migration1, 62 | $this->migration2, 63 | $this->migration3, 64 | ), $this->versioner); 65 | } 66 | 67 | public function testMigrateUp() 68 | { 69 | $data = (object) array('calls' => 0); 70 | 71 | $this->versioner->expects($this->once()) 72 | ->method('parseVersion') 73 | ->with($data) 74 | ->willReturn('0.8'); 75 | 76 | $this->versioner->expects($this->exactly(3)) 77 | ->method('updateVersion') 78 | ->withConsecutive( 79 | array($data, '0.10'), 80 | array($data, '1.0'), 81 | array($data, '2.0') 82 | ); 83 | 84 | $this->migration1->expects($this->once()) 85 | ->method('up') 86 | ->with($data) 87 | ->willReturnCallback(function (stdClass $data) { 88 | PHPUnit_Framework_Assert::assertSame(0, $data->calls); 89 | ++$data->calls; 90 | }); 91 | $this->migration2->expects($this->once()) 92 | ->method('up') 93 | ->with($data) 94 | ->willReturnCallback(function (stdClass $data) { 95 | PHPUnit_Framework_Assert::assertSame(1, $data->calls); 96 | ++$data->calls; 97 | }); 98 | $this->migration3->expects($this->once()) 99 | ->method('up') 100 | ->with($data) 101 | ->willReturnCallback(function (stdClass $data) { 102 | PHPUnit_Framework_Assert::assertSame(2, $data->calls); 103 | ++$data->calls; 104 | }); 105 | 106 | $this->manager->migrate($data, '2.0'); 107 | 108 | $this->assertSame(3, $data->calls); 109 | } 110 | 111 | public function testMigrateUpPartial() 112 | { 113 | $data = (object) array('calls' => 0); 114 | 115 | $this->versioner->expects($this->once()) 116 | ->method('parseVersion') 117 | ->with($data) 118 | ->willReturn('0.10'); 119 | 120 | $this->versioner->expects($this->once()) 121 | ->method('updateVersion') 122 | ->with($data, '1.0'); 123 | 124 | $this->migration1->expects($this->never()) 125 | ->method('up'); 126 | $this->migration2->expects($this->once()) 127 | ->method('up') 128 | ->with($data) 129 | ->willReturnCallback(function (stdClass $data) { 130 | PHPUnit_Framework_Assert::assertSame(0, $data->calls); 131 | ++$data->calls; 132 | }); 133 | $this->migration3->expects($this->never()) 134 | ->method('up'); 135 | 136 | $this->manager->migrate($data, '1.0'); 137 | 138 | $this->assertSame(1, $data->calls); 139 | } 140 | 141 | /** 142 | * @expectedException \Webmozart\Json\Migration\MigrationFailedException 143 | * @expectedExceptionMessage 0.5 144 | */ 145 | public function testMigrateUpFailsIfNoMigrationForSourceVersion() 146 | { 147 | $data = (object) array(); 148 | 149 | $this->versioner->expects($this->once()) 150 | ->method('parseVersion') 151 | ->with($data) 152 | ->willReturn('0.5'); 153 | 154 | $this->versioner->expects($this->never()) 155 | ->method('updateVersion'); 156 | 157 | $this->migration1->expects($this->never()) 158 | ->method('up'); 159 | $this->migration2->expects($this->never()) 160 | ->method('up'); 161 | $this->migration3->expects($this->never()) 162 | ->method('up'); 163 | 164 | $this->manager->migrate($data, '0.10'); 165 | } 166 | 167 | /** 168 | * @expectedException \Webmozart\Json\Migration\MigrationFailedException 169 | * @expectedExceptionMessage 1.2 170 | */ 171 | public function testMigrateUpFailsIfNoMigrationForTargetVersion() 172 | { 173 | $data = (object) array('calls' => 0); 174 | 175 | $this->versioner->expects($this->once()) 176 | ->method('parseVersion') 177 | ->with($data) 178 | ->willReturn('0.10'); 179 | 180 | $this->versioner->expects($this->once()) 181 | ->method('updateVersion') 182 | ->with($data, '1.0'); 183 | 184 | $this->migration1->expects($this->never()) 185 | ->method('up'); 186 | $this->migration2->expects($this->once()) 187 | ->method('up') 188 | ->willReturnCallback(function (stdClass $data) { 189 | PHPUnit_Framework_Assert::assertSame(0, $data->calls); 190 | ++$data->calls; 191 | }); 192 | $this->migration3->expects($this->never()) 193 | ->method('up'); 194 | 195 | $this->manager->migrate($data, '1.2'); 196 | } 197 | 198 | public function testMigrateDown() 199 | { 200 | $data = (object) array('calls' => 0); 201 | 202 | $this->versioner->expects($this->once()) 203 | ->method('parseVersion') 204 | ->with($data) 205 | ->willReturn('2.0'); 206 | 207 | $this->versioner->expects($this->exactly(3)) 208 | ->method('updateVersion') 209 | ->withConsecutive( 210 | array($data, '1.0'), 211 | array($data, '0.10'), 212 | array($data, '0.8') 213 | ); 214 | 215 | $this->migration3->expects($this->once()) 216 | ->method('down') 217 | ->with($data) 218 | ->willReturnCallback(function (stdClass $data) { 219 | PHPUnit_Framework_Assert::assertSame(0, $data->calls); 220 | ++$data->calls; 221 | }); 222 | $this->migration2->expects($this->once()) 223 | ->method('down') 224 | ->with($data) 225 | ->willReturnCallback(function (stdClass $data) { 226 | PHPUnit_Framework_Assert::assertSame(1, $data->calls); 227 | ++$data->calls; 228 | }); 229 | $this->migration1->expects($this->once()) 230 | ->method('down') 231 | ->with($data) 232 | ->willReturnCallback(function (stdClass $data) { 233 | PHPUnit_Framework_Assert::assertSame(2, $data->calls); 234 | ++$data->calls; 235 | }); 236 | 237 | $this->manager->migrate($data, '0.8'); 238 | 239 | $this->assertSame(3, $data->calls); 240 | } 241 | 242 | public function testMigrateDownPartial() 243 | { 244 | $data = (object) array('calls' => 0); 245 | 246 | $this->versioner->expects($this->once()) 247 | ->method('parseVersion') 248 | ->with($data) 249 | ->willReturn('1.0'); 250 | 251 | $this->versioner->expects($this->once()) 252 | ->method('updateVersion') 253 | ->with($data, '0.10'); 254 | 255 | $this->migration3->expects($this->never()) 256 | ->method('down'); 257 | $this->migration2->expects($this->once()) 258 | ->method('down') 259 | ->with($data) 260 | ->willReturnCallback(function (stdClass $data) { 261 | PHPUnit_Framework_Assert::assertSame(0, $data->calls); 262 | ++$data->calls; 263 | }); 264 | $this->migration1->expects($this->never()) 265 | ->method('down'); 266 | 267 | $this->manager->migrate($data, '0.10'); 268 | 269 | $this->assertSame(1, $data->calls); 270 | } 271 | 272 | /** 273 | * @expectedException \Webmozart\Json\Migration\MigrationFailedException 274 | * @expectedExceptionMessage 1.2 275 | */ 276 | public function testMigrateDownFailsIfNoMigrationForSourceVersion() 277 | { 278 | $data = (object) array(); 279 | 280 | $this->versioner->expects($this->once()) 281 | ->method('parseVersion') 282 | ->with($data) 283 | ->willReturn('1.2'); 284 | 285 | $this->versioner->expects($this->never()) 286 | ->method('updateVersion'); 287 | 288 | $this->migration3->expects($this->never()) 289 | ->method('down'); 290 | $this->migration2->expects($this->never()) 291 | ->method('down'); 292 | $this->migration1->expects($this->never()) 293 | ->method('down'); 294 | 295 | $this->manager->migrate($data, '1.0'); 296 | } 297 | 298 | /** 299 | * @expectedException \Webmozart\Json\Migration\MigrationFailedException 300 | * @expectedExceptionMessage 0.9 301 | */ 302 | public function testMigrateDownFailsIfNoMigrationForTargetVersion() 303 | { 304 | $data = (object) array('calls' => 0); 305 | 306 | $this->versioner->expects($this->once()) 307 | ->method('parseVersion') 308 | ->with($data) 309 | ->willReturn('1.0'); 310 | 311 | $this->versioner->expects($this->once()) 312 | ->method('updateVersion') 313 | ->with($data, '0.10'); 314 | 315 | $this->migration3->expects($this->never()) 316 | ->method('down'); 317 | $this->migration2->expects($this->once()) 318 | ->method('down') 319 | ->willReturnCallback(function (stdClass $data) { 320 | PHPUnit_Framework_Assert::assertSame(0, $data->calls); 321 | ++$data->calls; 322 | }); 323 | $this->migration1->expects($this->never()) 324 | ->method('down'); 325 | 326 | $this->manager->migrate($data, '0.9'); 327 | } 328 | 329 | public function testMigrateDoesNothingIfAlreadyCorrectVersion() 330 | { 331 | $data = (object) array(); 332 | 333 | $this->versioner->expects($this->once()) 334 | ->method('parseVersion') 335 | ->with($data) 336 | ->willReturn('0.10'); 337 | 338 | $this->versioner->expects($this->never()) 339 | ->method('updateVersion'); 340 | 341 | $this->migration1->expects($this->never()) 342 | ->method('up'); 343 | $this->migration2->expects($this->never()) 344 | ->method('up'); 345 | $this->migration3->expects($this->never()) 346 | ->method('up'); 347 | $this->migration1->expects($this->never()) 348 | ->method('down'); 349 | $this->migration2->expects($this->never()) 350 | ->method('down'); 351 | $this->migration3->expects($this->never()) 352 | ->method('down'); 353 | 354 | $this->manager->migrate($data, '0.10'); 355 | } 356 | 357 | public function testGetKnownVersions() 358 | { 359 | $this->assertSame(array('0.8', '0.10', '1.0', '2.0'), $this->manager->getKnownVersions()); 360 | } 361 | 362 | public function testGetKnownVersionsWithoutMigrations() 363 | { 364 | $this->manager = new MigrationManager(array(), $this->versioner); 365 | 366 | $this->assertSame(array(), $this->manager->getKnownVersions()); 367 | } 368 | 369 | /** 370 | * @param string $sourceVersion 371 | * @param string $targetVersion 372 | * 373 | * @return PHPUnit_Framework_MockObject_MockObject|JsonMigration 374 | */ 375 | private function createMigrationMock($sourceVersion, $targetVersion) 376 | { 377 | $mock = $this->getMock('Webmozart\Json\Migration\JsonMigration'); 378 | 379 | $mock->expects($this->any()) 380 | ->method('getSourceVersion') 381 | ->willReturn($sourceVersion); 382 | 383 | $mock->expects($this->any()) 384 | ->method('getTargetVersion') 385 | ->willReturn($targetVersion); 386 | 387 | return $mock; 388 | } 389 | } 390 | -------------------------------------------------------------------------------- /tests/UriRetriever/Fixtures/schema-1.0.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0" 3 | } 4 | -------------------------------------------------------------------------------- /tests/UriRetriever/LocalUriRetrieverTest.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 Webmozart\Json\Tests\UriRetriever; 13 | 14 | use Webmozart\Json\UriRetriever\LocalUriRetriever; 15 | 16 | /** 17 | * @author Bernhard Schussek 18 | */ 19 | class LocalUriRetrieverTest extends \PHPUnit_Framework_TestCase 20 | { 21 | const GITHUB_SCHEMA_URL = 'https://raw.githubusercontent.com/webmozart/json/be0e18a01f2ef720008a91d047f16de1dc30030c/tests/Fixtures/schema.json'; 22 | 23 | const GITHUB_SCHEMA_BODY = <<<'BODY' 24 | { 25 | "id": "http://webmozart.io/fixtures/schema#", 26 | "type": "object" 27 | } 28 | 29 | BODY; 30 | 31 | const GITHUB_SCHEMA_CONTENT_TYPE = 'text/plain; charset=utf-8'; 32 | 33 | public function testRetrieve() 34 | { 35 | $retriever = new LocalUriRetriever(__DIR__.'/Fixtures', array( 36 | 'http://my/schema/1.0' => 'schema-1.0.json', 37 | 'http://my/schema/2.0' => self::GITHUB_SCHEMA_URL, 38 | )); 39 | 40 | $schema1Body = file_get_contents(__DIR__.'/Fixtures/schema-1.0.json'); 41 | 42 | $this->assertSame($schema1Body, $retriever->retrieve('http://my/schema/1.0')); 43 | $this->assertNull($retriever->getContentType()); 44 | 45 | $this->assertSame(self::GITHUB_SCHEMA_BODY, $retriever->retrieve('http://my/schema/2.0')); 46 | $this->assertSame(self::GITHUB_SCHEMA_CONTENT_TYPE, $retriever->getContentType()); 47 | } 48 | 49 | public function testRetrieveLoadsUnmappedUrisFromFilesystemByDefault() 50 | { 51 | $retriever = new LocalUriRetriever(); 52 | 53 | $schema1Body = file_get_contents(__DIR__.'/Fixtures/schema-1.0.json'); 54 | 55 | $this->assertSame($schema1Body, $retriever->retrieve('file://'.__DIR__.'/Fixtures/schema-1.0.json')); 56 | $this->assertNull($retriever->getContentType()); 57 | } 58 | 59 | public function testRetrieveLoadsUnmappedUrisFromWebByDefault() 60 | { 61 | $retriever = new LocalUriRetriever(); 62 | 63 | $this->assertSame(self::GITHUB_SCHEMA_BODY, $retriever->retrieve(self::GITHUB_SCHEMA_URL)); 64 | $this->assertSame(self::GITHUB_SCHEMA_CONTENT_TYPE, $retriever->getContentType()); 65 | } 66 | 67 | public function testRetrievePassesUnmappedUrisToFallbackRetriever() 68 | { 69 | $fallbackRetriever = $this->getMock('JsonSchema\Uri\Retrievers\UriRetrieverInterface'); 70 | 71 | $fallbackRetriever->expects($this->at(0)) 72 | ->method('retrieve') 73 | ->with('http://my/schema/1.0') 74 | ->willReturn('FOOBAR'); 75 | 76 | $fallbackRetriever->expects($this->at(1)) 77 | ->method('getContentType') 78 | ->willReturn('content/type'); 79 | 80 | $retriever = new LocalUriRetriever(null, array(), $fallbackRetriever); 81 | 82 | $this->assertSame('FOOBAR', $retriever->retrieve('http://my/schema/1.0')); 83 | $this->assertSame('content/type', $retriever->getContentType()); 84 | } 85 | 86 | public function testGetContentTypeInitiallyReturnsNull() 87 | { 88 | $retriever = new LocalUriRetriever(); 89 | 90 | $this->assertNull($retriever->getContentType()); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/Validation/ValidatingConverterTest.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 Webmozart\Json\Tests\Validation; 13 | 14 | use PHPUnit_Framework_Assert; 15 | use PHPUnit_Framework_MockObject_MockObject; 16 | use PHPUnit_Framework_TestCase; 17 | use Webmozart\Json\Conversion\JsonConverter; 18 | use Webmozart\Json\InvalidSchemaException; 19 | use Webmozart\Json\JsonValidator; 20 | use Webmozart\Json\Validation\ValidatingConverter; 21 | 22 | /** 23 | * @since 1.3 24 | * 25 | * @author Bernhard Schussek 26 | */ 27 | class ValidatingConverterTest extends PHPUnit_Framework_TestCase 28 | { 29 | /** 30 | * @var PHPUnit_Framework_MockObject_MockObject|JsonConverter 31 | */ 32 | private $innerConverter; 33 | 34 | /** 35 | * @var PHPUnit_Framework_MockObject_MockObject|JsonValidator 36 | */ 37 | private $jsonValidator; 38 | 39 | /** 40 | * @var ValidatingConverter 41 | */ 42 | private $converter; 43 | 44 | protected function setUp() 45 | { 46 | $this->innerConverter = $this->getMock('Webmozart\Json\Conversion\JsonConverter'); 47 | $this->jsonValidator = $this->getMockBuilder('Webmozart\Json\JsonValidator') 48 | ->disableOriginalConstructor() 49 | ->getMock(); 50 | $this->converter = new ValidatingConverter( 51 | $this->innerConverter, 52 | '/path/to/schema', 53 | $this->jsonValidator 54 | ); 55 | } 56 | 57 | public function testToJson() 58 | { 59 | $options = array('option' => 'value'); 60 | 61 | $jsonData = (object) array( 62 | 'foo' => 'bar', 63 | ); 64 | 65 | $this->innerConverter->expects($this->once()) 66 | ->method('toJson') 67 | ->with('DATA', $options) 68 | ->willReturn($jsonData); 69 | 70 | $this->jsonValidator->expects($this->once()) 71 | ->method('validate') 72 | ->with($jsonData, '/path/to/schema'); 73 | 74 | $this->assertSame($jsonData, $this->converter->toJson('DATA', $options)); 75 | } 76 | 77 | public function testToJsonWithoutSchema() 78 | { 79 | $options = array('option' => 'value'); 80 | 81 | $jsonData = (object) array( 82 | 'foo' => 'bar', 83 | ); 84 | 85 | $this->converter = new ValidatingConverter( 86 | $this->innerConverter, 87 | null, 88 | $this->jsonValidator 89 | ); 90 | 91 | $this->innerConverter->expects($this->once()) 92 | ->method('toJson') 93 | ->with('DATA', $options) 94 | ->willReturn($jsonData); 95 | 96 | $this->jsonValidator->expects($this->once()) 97 | ->method('validate') 98 | ->with($jsonData, null); 99 | 100 | $this->assertSame($jsonData, $this->converter->toJson('DATA', $options)); 101 | } 102 | 103 | public function testToJsonRunsSchemaCallable() 104 | { 105 | $options = array('option' => 'value'); 106 | 107 | $jsonData = (object) array( 108 | 'foo' => 'bar', 109 | ); 110 | 111 | $this->innerConverter->expects($this->once()) 112 | ->method('toJson') 113 | ->with('DATA', $options) 114 | ->willReturn($jsonData); 115 | 116 | $this->jsonValidator->expects($this->once()) 117 | ->method('validate') 118 | ->with($jsonData, '/dynamic/schema'); 119 | 120 | $this->converter = new ValidatingConverter( 121 | $this->innerConverter, 122 | function ($data) use ($jsonData) { 123 | PHPUnit_Framework_Assert::assertSame($jsonData, $data); 124 | 125 | return '/dynamic/schema'; 126 | }, 127 | $this->jsonValidator 128 | ); 129 | 130 | $this->assertSame($jsonData, $this->converter->toJson('DATA', $options)); 131 | } 132 | 133 | public function testFromJson() 134 | { 135 | $options = array('option' => 'value'); 136 | 137 | $jsonData = (object) array( 138 | 'foo' => 'bar', 139 | ); 140 | 141 | $this->jsonValidator->expects($this->once()) 142 | ->method('validate') 143 | ->with($jsonData, '/path/to/schema'); 144 | 145 | $this->innerConverter->expects($this->once()) 146 | ->method('fromJson') 147 | ->with($jsonData, $options) 148 | ->willReturn('DATA'); 149 | 150 | $this->assertSame('DATA', $this->converter->fromJson($jsonData, $options)); 151 | } 152 | 153 | public function testFromJsonWithoutSchema() 154 | { 155 | $options = array('option' => 'value'); 156 | 157 | $jsonData = (object) array( 158 | 'foo' => 'bar', 159 | ); 160 | 161 | $this->converter = new ValidatingConverter( 162 | $this->innerConverter, 163 | null, 164 | $this->jsonValidator 165 | ); 166 | 167 | $this->jsonValidator->expects($this->once()) 168 | ->method('validate') 169 | ->with($jsonData, null); 170 | 171 | $this->innerConverter->expects($this->once()) 172 | ->method('fromJson') 173 | ->with($jsonData, $options) 174 | ->willReturn('DATA'); 175 | 176 | $this->assertSame('DATA', $this->converter->fromJson($jsonData, $options)); 177 | } 178 | 179 | public function testFromJsonRunsSchemaCallable() 180 | { 181 | $options = array('option' => 'value'); 182 | 183 | $jsonData = (object) array( 184 | 'foo' => 'bar', 185 | ); 186 | 187 | $this->jsonValidator->expects($this->once()) 188 | ->method('validate') 189 | ->with($jsonData, '/dynamic/schema'); 190 | 191 | $this->innerConverter->expects($this->once()) 192 | ->method('fromJson') 193 | ->with($jsonData, $options) 194 | ->willReturn('DATA'); 195 | 196 | $this->converter = new ValidatingConverter( 197 | $this->innerConverter, 198 | function ($data) use ($jsonData) { 199 | PHPUnit_Framework_Assert::assertSame($jsonData, $data); 200 | 201 | return '/dynamic/schema'; 202 | }, 203 | $this->jsonValidator 204 | ); 205 | 206 | $this->assertSame('DATA', $this->converter->fromJson($jsonData, $options)); 207 | } 208 | 209 | /** 210 | * @expectedException \Webmozart\Json\Conversion\ConversionFailedException 211 | */ 212 | public function testConvertSchemaExceptionToConversionException() 213 | { 214 | $options = array('option' => 'value'); 215 | 216 | $jsonData = (object) array( 217 | 'foo' => 'bar', 218 | ); 219 | 220 | $this->innerConverter->expects($this->once()) 221 | ->method('toJson') 222 | ->with('DATA', $options) 223 | ->willReturn($jsonData); 224 | 225 | $this->jsonValidator->expects($this->once()) 226 | ->method('validate') 227 | ->willThrowException(new InvalidSchemaException()); 228 | 229 | $this->converter->toJson('DATA', $options); 230 | } 231 | 232 | /** 233 | * @expectedException \Webmozart\Json\Conversion\ConversionFailedException 234 | */ 235 | public function testConvertValidationErrorsToConversionException() 236 | { 237 | $options = array('option' => 'value'); 238 | 239 | $jsonData = (object) array( 240 | 'foo' => 'bar', 241 | ); 242 | 243 | $this->innerConverter->expects($this->once()) 244 | ->method('toJson') 245 | ->with('DATA', $options) 246 | ->willReturn($jsonData); 247 | 248 | $this->jsonValidator->expects($this->once()) 249 | ->method('validate') 250 | ->willReturn(array( 251 | 'First error', 252 | 'Second error', 253 | )); 254 | 255 | $this->converter->toJson('DATA', $options); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /tests/Versioning/SchemaUriVersionerTest.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 Webmozart\Json\Tests\Versioning; 13 | 14 | use PHPUnit_Framework_TestCase; 15 | use Webmozart\Json\Versioning\SchemaUriVersioner; 16 | 17 | /** 18 | * @since 1.3 19 | * 20 | * @author Bernhard Schussek 21 | */ 22 | class SchemaUriVersionerTest extends PHPUnit_Framework_TestCase 23 | { 24 | /** 25 | * @var SchemaUriVersioner 26 | */ 27 | private $versioner; 28 | 29 | protected function setUp() 30 | { 31 | $this->versioner = new SchemaUriVersioner(); 32 | } 33 | 34 | public function testParseVersion() 35 | { 36 | $data = (object) array('$schema' => 'http://example.com/schemas/1.0/schema'); 37 | 38 | $this->assertSame('1.0', $this->versioner->parseVersion($data)); 39 | } 40 | 41 | /** 42 | * @expectedException \Webmozart\Json\Versioning\CannotParseVersionException 43 | */ 44 | public function testParseVersionFailsIfNotFound() 45 | { 46 | $data = (object) array('$schema' => 'http://example.com/schemas/1.0-schema'); 47 | 48 | $this->versioner->parseVersion($data); 49 | } 50 | 51 | public function testParseVersionWithCustomPattern() 52 | { 53 | $this->versioner = new SchemaUriVersioner('~(?<=/)\d+\.\d+(?=-)~'); 54 | 55 | $data = (object) array('$schema' => 'http://example.com/schemas/1.0-schema'); 56 | 57 | $this->assertSame('1.0', $this->versioner->parseVersion($data)); 58 | } 59 | 60 | /** 61 | * @expectedException \Webmozart\Json\Versioning\CannotParseVersionException 62 | */ 63 | public function testParseVersionFailsIfNoSchemaField() 64 | { 65 | $data = (object) array('foo' => 'bar'); 66 | 67 | $this->versioner->parseVersion($data); 68 | } 69 | 70 | public function testUpdateVersion() 71 | { 72 | $data = (object) array('$schema' => 'http://example.com/schemas/1.0/schema'); 73 | 74 | $this->versioner->updateVersion($data, '2.0'); 75 | 76 | $this->assertSame('http://example.com/schemas/2.0/schema', $data->{'$schema'}); 77 | } 78 | 79 | public function testUpdateVersionIgnoresCurrentVersion() 80 | { 81 | $data = (object) array('$schema' => 'http://example.com/schemas/1.0/schema'); 82 | 83 | $this->versioner->updateVersion($data, '1.0'); 84 | 85 | $this->assertSame('http://example.com/schemas/1.0/schema', $data->{'$schema'}); 86 | } 87 | 88 | /** 89 | * @expectedException \Webmozart\Json\Versioning\CannotUpdateVersionException 90 | */ 91 | public function testUpdateVersionFailsIfNotFound() 92 | { 93 | $data = (object) array('$schema' => 'http://example.com/schemas/1.0-schema'); 94 | 95 | $this->versioner->updateVersion($data, '2.0'); 96 | } 97 | 98 | /** 99 | * @expectedException \Webmozart\Json\Versioning\CannotUpdateVersionException 100 | */ 101 | public function testUpdateVersionFailsIfFoundMultipleTimes() 102 | { 103 | $data = (object) array('$schema' => 'http://example.com/1.0/schemas/1.0/schema'); 104 | 105 | $this->versioner->updateVersion($data, '2.0'); 106 | } 107 | 108 | public function testUpdateVersionCustomPattern() 109 | { 110 | $this->versioner = new SchemaUriVersioner('~(?<=/)\d+\.\d+(?=-)~'); 111 | 112 | $data = (object) array('$schema' => 'http://example.com/schemas/1.0-schema'); 113 | 114 | $this->versioner->updateVersion($data, '2.0'); 115 | 116 | $this->assertSame('http://example.com/schemas/2.0-schema', $data->{'$schema'}); 117 | } 118 | 119 | /** 120 | * @expectedException \Webmozart\Json\Versioning\CannotUpdateVersionException 121 | */ 122 | public function testUpdateVersionFailsIfNoSchemaField() 123 | { 124 | $data = (object) array('foo' => 'bar'); 125 | 126 | $this->versioner->updateVersion($data, '2.0'); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/Versioning/VersionFieldVersionerTest.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 Webmozart\Json\Tests\Versioning; 13 | 14 | use PHPUnit_Framework_TestCase; 15 | use Webmozart\Json\Versioning\VersionFieldVersioner; 16 | 17 | /** 18 | * @since 1.3 19 | * 20 | * @author Bernhard Schussek 21 | */ 22 | class VersionFieldVersionerTest extends PHPUnit_Framework_TestCase 23 | { 24 | /** 25 | * @var VersionFieldVersioner 26 | */ 27 | private $versioner; 28 | 29 | protected function setUp() 30 | { 31 | $this->versioner = new VersionFieldVersioner(); 32 | } 33 | 34 | public function testParseVersion() 35 | { 36 | $data = (object) array('version' => '1.0'); 37 | 38 | $this->assertSame('1.0', $this->versioner->parseVersion($data)); 39 | } 40 | 41 | public function testParseVersionCustomFieldName() 42 | { 43 | $this->versioner = new VersionFieldVersioner('foo'); 44 | 45 | $data = (object) array('foo' => '1.0'); 46 | 47 | $this->assertSame('1.0', $this->versioner->parseVersion($data)); 48 | } 49 | 50 | /** 51 | * @expectedException \Webmozart\Json\Versioning\CannotParseVersionException 52 | */ 53 | public function testParseVersionFailsIfNotFound() 54 | { 55 | $data = (object) array('foo' => 'bar'); 56 | 57 | $this->versioner->parseVersion($data); 58 | } 59 | 60 | public function testUpdateVersion() 61 | { 62 | $data = (object) array('version' => '1.0'); 63 | 64 | $this->versioner->updateVersion($data, '2.0'); 65 | 66 | $this->assertSame('2.0', $data->version); 67 | } 68 | 69 | public function testUpdateVersionCustomFieldName() 70 | { 71 | $this->versioner = new VersionFieldVersioner('foo'); 72 | 73 | $data = (object) array('foo' => '1.0'); 74 | 75 | $this->versioner->updateVersion($data, '2.0'); 76 | 77 | $this->assertSame('2.0', $data->foo); 78 | } 79 | 80 | public function testUpdateVersionCreatesFieldIfNotFound() 81 | { 82 | $data = (object) array('foo' => 'bar'); 83 | 84 | $this->versioner->updateVersion($data, '2.0'); 85 | 86 | $this->assertSame('2.0', $data->version); 87 | } 88 | } 89 | --------------------------------------------------------------------------------