├── .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 | [](https://travis-ci.org/webmozart/json)
5 | [](https://ci.appveyor.com/project/webmozart/json/branch/master)
6 | [](https://scrutinizer-ci.com/g/webmozart/json/?branch=master)
7 | [](https://packagist.org/packages/webmozart/json)
8 | [](https://packagist.org/packages/webmozart/json)
9 | [](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 |
--------------------------------------------------------------------------------