├── composer.json ├── license.md ├── readme.md └── src └── Schema ├── Context.php ├── DynamicParameter.php ├── Elements ├── AnyOf.php ├── Base.php ├── Structure.php └── Type.php ├── Expect.php ├── Helpers.php ├── MergeMode.php ├── Message.php ├── Processor.php ├── Schema.php └── ValidationException.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nette/schema", 3 | "description": "📐 Nette Schema: validating data structures against a given Schema.", 4 | "keywords": ["nette", "config"], 5 | "homepage": "https://nette.org", 6 | "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"], 7 | "authors": [ 8 | { 9 | "name": "David Grudl", 10 | "homepage": "https://davidgrudl.com" 11 | }, 12 | { 13 | "name": "Nette Community", 14 | "homepage": "https://nette.org/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": "8.1 - 8.4", 19 | "nette/utils": "^4.0" 20 | }, 21 | "require-dev": { 22 | "nette/tester": "^2.5.2", 23 | "tracy/tracy": "^2.8", 24 | "phpstan/phpstan-nette": "^2.0@stable" 25 | }, 26 | "autoload": { 27 | "classmap": ["src/"], 28 | "psr-4": { 29 | "Nette\\": "src" 30 | } 31 | }, 32 | "minimum-stability": "dev", 33 | "scripts": { 34 | "phpstan": "phpstan analyse", 35 | "tester": "tester tests -s" 36 | }, 37 | "extra": { 38 | "branch-alias": { 39 | "dev-master": "2.0-dev" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Licenses 2 | ======== 3 | 4 | Good news! You may use Nette Framework under the terms of either 5 | the New BSD License or the GNU General Public License (GPL) version 2 or 3. 6 | 7 | The BSD License is recommended for most projects. It is easy to understand and it 8 | places almost no restrictions on what you can do with the framework. If the GPL 9 | fits better to your project, you can use the framework under this license. 10 | 11 | You don't have to notify anyone which license you are using. You can freely 12 | use Nette Framework in commercial projects as long as the copyright header 13 | remains intact. 14 | 15 | Please be advised that the name "Nette Framework" is a protected trademark and its 16 | usage has some limitations. So please do not use word "Nette" in the name of your 17 | project or top-level domain, and choose a name that stands on its own merits. 18 | If your stuff is good, it will not take long to establish a reputation for yourselves. 19 | 20 | 21 | New BSD License 22 | --------------- 23 | 24 | Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com) 25 | All rights reserved. 26 | 27 | Redistribution and use in source and binary forms, with or without modification, 28 | are permitted provided that the following conditions are met: 29 | 30 | * Redistributions of source code must retain the above copyright notice, 31 | this list of conditions and the following disclaimer. 32 | 33 | * Redistributions in binary form must reproduce the above copyright notice, 34 | this list of conditions and the following disclaimer in the documentation 35 | and/or other materials provided with the distribution. 36 | 37 | * Neither the name of "Nette Framework" nor the names of its contributors 38 | may be used to endorse or promote products derived from this software 39 | without specific prior written permission. 40 | 41 | This software is provided by the copyright holders and contributors "as is" and 42 | any express or implied warranties, including, but not limited to, the implied 43 | warranties of merchantability and fitness for a particular purpose are 44 | disclaimed. In no event shall the copyright owner or contributors be liable for 45 | any direct, indirect, incidental, special, exemplary, or consequential damages 46 | (including, but not limited to, procurement of substitute goods or services; 47 | loss of use, data, or profits; or business interruption) however caused and on 48 | any theory of liability, whether in contract, strict liability, or tort 49 | (including negligence or otherwise) arising in any way out of the use of this 50 | software, even if advised of the possibility of such damage. 51 | 52 | 53 | GNU General Public License 54 | -------------------------- 55 | 56 | GPL licenses are very very long, so instead of including them here we offer 57 | you URLs with full text: 58 | 59 | - [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) 60 | - [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) 61 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Nette Schema 2 | ************ 3 | 4 | [![Downloads this Month](https://img.shields.io/packagist/dm/nette/schema.svg)](https://packagist.org/packages/nette/schema) 5 | [![Tests](https://github.com/nette/schema/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/schema/actions) 6 | [![Coverage Status](https://coveralls.io/repos/github/nette/schema/badge.svg?branch=master)](https://coveralls.io/github/nette/schema?branch=master) 7 | [![Latest Stable Version](https://poser.pugx.org/nette/schema/v/stable)](https://github.com/nette/schema/releases) 8 | [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/schema/blob/master/license.md) 9 | 10 | 11 | Introduction 12 | ============ 13 | 14 | A practical library for validation and normalization of data structures against a given schema with a smart & easy-to-understand API. 15 | 16 | Documentation can be found on the [website](https://doc.nette.org/schema). 17 | 18 | Installation: 19 | 20 | ```shell 21 | composer require nette/schema 22 | ``` 23 | 24 | It requires PHP version 8.1 and supports PHP up to 8.4. 25 | 26 | 27 | [Support Me](https://github.com/sponsors/dg) 28 | -------------------------------------------- 29 | 30 | Do you like Nette Schema? Are you looking forward to the new features? 31 | 32 | [![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg) 33 | 34 | Thank you! 35 | 36 | 37 | Basic Usage 38 | ----------- 39 | 40 | In variable `$schema` we have a validation schema (what exactly this means and how to create it we will say later) and in variable `$data` we have a data structure that we want to validate and normalize. This can be, for example, data sent by the user through an API, configuration file, etc. 41 | 42 | The task is handled by the [Nette\Schema\Processor](https://api.nette.org/schema/master/Nette/Schema/Processor.html) class, which processes the input and either returns normalized data or throws an [Nette\Schema\ValidationException](https://api.nette.org/schema/master/Nette/Schema/ValidationException.html) exception on error. 43 | 44 | ```php 45 | $processor = new Nette\Schema\Processor; 46 | 47 | try { 48 | $normalized = $processor->process($schema, $data); 49 | } catch (Nette\Schema\ValidationException $e) { 50 | echo 'Data is invalid: ' . $e->getMessage(); 51 | } 52 | ``` 53 | 54 | Method `$e->getMessages()` returns array of all message strings and `$e->getMessageObjects()` return all messages as [Nette\Schema\Message](https://api.nette.org/schema/master/Nette/Schema/Message.html) objects. 55 | 56 | 57 | Defining Schema 58 | --------------- 59 | 60 | And now let's create a schema. The class [Nette\Schema\Expect](https://api.nette.org/schema/master/Nette/Schema/Expect.html) is used to define it, we actually define expectations of what the data should look like. Let's say that the input data must be a structure (e.g. an array) containing elements `processRefund` of type bool and `refundAmount` of type int. 61 | 62 | ```php 63 | use Nette\Schema\Expect; 64 | 65 | $schema = Expect::structure([ 66 | 'processRefund' => Expect::bool(), 67 | 'refundAmount' => Expect::int(), 68 | ]); 69 | ``` 70 | 71 | We believe that the schema definition looks clear, even if you see it for the very first time. 72 | 73 | Lets send the following data for validation: 74 | 75 | ```php 76 | $data = [ 77 | 'processRefund' => true, 78 | 'refundAmount' => 17, 79 | ]; 80 | 81 | $normalized = $processor->process($schema, $data); // OK, it passes 82 | ``` 83 | 84 | The output, i.e. the value `$normalized`, is the object `stdClass`. If we want the output to be an array, we add a cast to schema `Expect::structure([...])->castTo('array')`. 85 | 86 | All elements of the structure are optional and have a default value `null`. Example: 87 | 88 | ```php 89 | $data = [ 90 | 'refundAmount' => 17, 91 | ]; 92 | 93 | $normalized = $processor->process($schema, $data); // OK, it passes 94 | // $normalized = {'processRefund' => null, 'refundAmount' => 17} 95 | ``` 96 | 97 | The fact that the default value is `null` does not mean that it would be accepted in the input data `'processRefund' => null`. No, the input must be boolean, i.e. only `true` or `false`. We would have to explicitly allow `null` via `Expect::bool()->nullable()`. 98 | 99 | An item can be made mandatory using `Expect::bool()->required()`. We change the default value to `false` using `Expect::bool()->default(false)` or shortly using `Expect::bool(false)`. 100 | 101 | And what if we wanted to accept `1` and `0` besides booleans? Then we list the allowed values, which we will also normalize to boolean: 102 | 103 | ```php 104 | $schema = Expect::structure([ 105 | 'processRefund' => Expect::anyOf(true, false, 1, 0)->castTo('bool'), 106 | 'refundAmount' => Expect::int(), 107 | ]); 108 | 109 | $normalized = $processor->process($schema, $data); 110 | is_bool($normalized->processRefund); // true 111 | ``` 112 | 113 | Now you know the basics of how the schema is defined and how the individual elements of the structure behave. We will now show what all the other elements can be used in defining a schema. 114 | 115 | 116 | Data Types: type() 117 | ------------------ 118 | 119 | All standard PHP data types can be listed in the schema: 120 | 121 | ```php 122 | Expect::string($default = null) 123 | Expect::int($default = null) 124 | Expect::float($default = null) 125 | Expect::bool($default = null) 126 | Expect::null() 127 | Expect::array($default = []) 128 | ``` 129 | 130 | And then all types [supported by the Validators](https://doc.nette.org/validators#toc-validation-rules) via `Expect::type('scalar')` or abbreviated `Expect::scalar()`. Also class or interface names are accepted, e.g. `Expect::type('AddressEntity')`. 131 | 132 | You can also use union notation: 133 | 134 | ```php 135 | Expect::type('bool|string|array') 136 | ``` 137 | 138 | The default value is always `null` except for `array` and `list`, where it is an empty array. (A list is an array indexed in ascending order of numeric keys from zero, that is, a non-associative array). 139 | 140 | 141 | Array of Values: arrayOf() listOf() 142 | ----------------------------------- 143 | 144 | The array is too general structure, it is more useful to specify exactly what elements it can contain. For example, an array whose elements can only be strings: 145 | 146 | ```php 147 | $schema = Expect::arrayOf('string'); 148 | 149 | $processor->process($schema, ['hello', 'world']); // OK 150 | $processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK 151 | $processor->process($schema, ['key' => 123]); // ERROR: 123 is not a string 152 | ``` 153 | 154 | The second parameter can be used to specify keys (since version 1.2): 155 | 156 | ```php 157 | $schema = Expect::arrayOf('string', 'int'); 158 | 159 | $processor->process($schema, ['hello', 'world']); // OK 160 | $processor->process($schema, ['a' => 'hello']); // ERROR: 'a' is not int 161 | ``` 162 | 163 | The list is an indexed array: 164 | 165 | ```php 166 | $schema = Expect::listOf('string'); 167 | 168 | $processor->process($schema, ['a', 'b']); // OK 169 | $processor->process($schema, ['a', 123]); // ERROR: 123 is not a string 170 | $processor->process($schema, ['key' => 'a']); // ERROR: is not a list 171 | $processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: is not a list 172 | ``` 173 | 174 | The parameter can also be a schema, so we can write: 175 | 176 | ```php 177 | Expect::arrayOf(Expect::bool()) 178 | ``` 179 | 180 | The default value is an empty array. If you specify a default value and call `mergeDefaults()`, it will be merged with the passed data. 181 | 182 | 183 | Enumeration: anyOf() 184 | -------------------- 185 | 186 | `anyOf()` is a set of values ​​or schemas that a value can be. Here's how to write an array of elements that can be either `'a'`, `true`, or `null`: 187 | 188 | ```php 189 | $schema = Expect::listOf( 190 | Expect::anyOf('a', true, null), 191 | ); 192 | 193 | $processor->process($schema, ['a', true, null, 'a']); // OK 194 | $processor->process($schema, ['a', false]); // ERROR: false does not belong there 195 | ``` 196 | 197 | The enumeration elements can also be schemas: 198 | 199 | ```php 200 | $schema = Expect::listOf( 201 | Expect::anyOf(Expect::string(), true, null), 202 | ); 203 | 204 | $processor->process($schema, ['foo', true, null, 'bar']); // OK 205 | $processor->process($schema, [123]); // ERROR 206 | ``` 207 | 208 | The `anyOf()` method accepts variants as individual parameters, not as array. To pass it an array of values, use the unpacking operator `anyOf(...$variants)`. 209 | 210 | The default value is `null`. Use the `firstIsDefault()` method to make the first element the default: 211 | 212 | ```php 213 | // default is 'hello' 214 | Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault(); 215 | ``` 216 | 217 | 218 | Structures 219 | ---------- 220 | 221 | Structures are objects with defined keys. Each of these key => value pairs is referred to as a "property": 222 | 223 | Structures accept arrays and objects and return objects `stdClass` (unless you change it with `castTo('array')`, etc.). 224 | 225 | By default, all properties are optional and have a default value of `null`. You can define mandatory properties using `required()`: 226 | 227 | ```php 228 | $schema = Expect::structure([ 229 | 'required' => Expect::string()->required(), 230 | 'optional' => Expect::string(), // the default value is null 231 | ]); 232 | 233 | $processor->process($schema, ['optional' => '']); 234 | // ERROR: option 'required' is missing 235 | 236 | $processor->process($schema, ['required' => 'foo']); 237 | // OK, returns {'required' => 'foo', 'optional' => null} 238 | ``` 239 | 240 | If you do not want to output properties with only a default value, use `skipDefaults()`: 241 | 242 | ```php 243 | $schema = Expect::structure([ 244 | 'required' => Expect::string()->required(), 245 | 'optional' => Expect::string(), 246 | ])->skipDefaults(); 247 | 248 | $processor->process($schema, ['required' => 'foo']); 249 | // OK, returns {'required' => 'foo'} 250 | ``` 251 | 252 | Although `null` is the default value of the `optional` property, it is not allowed in the input data (the value must be a string). Properties accepting `null` are defined using `nullable()`: 253 | 254 | ```php 255 | $schema = Expect::structure([ 256 | 'optional' => Expect::string(), 257 | 'nullable' => Expect::string()->nullable(), 258 | ]); 259 | 260 | $processor->process($schema, ['optional' => null]); 261 | // ERROR: 'optional' expects to be string, null given. 262 | 263 | $processor->process($schema, ['nullable' => null]); 264 | // OK, returns {'optional' => null, 'nullable' => null} 265 | ``` 266 | 267 | By default, there can be no extra items in the input data: 268 | 269 | ```php 270 | $schema = Expect::structure([ 271 | 'key' => Expect::string(), 272 | ]); 273 | 274 | $processor->process($schema, ['additional' => 1]); 275 | // ERROR: Unexpected item 'additional' 276 | ``` 277 | 278 | Which we can change with `otherItems()`. As a parameter, we will specify the schema for each extra element: 279 | 280 | ```php 281 | $schema = Expect::structure([ 282 | 'key' => Expect::string(), 283 | ])->otherItems(Expect::int()); 284 | 285 | $processor->process($schema, ['additional' => 1]); // OK 286 | $processor->process($schema, ['additional' => true]); // ERROR 287 | ``` 288 | 289 | 290 | Deprecations 291 | ------------ 292 | 293 | You can deprecate property using the `deprecated([string $message])` method. Deprecation notices are returned by `$processor->getWarnings()`: 294 | 295 | ```php 296 | $schema = Expect::structure([ 297 | 'old' => Expect::int()->deprecated('The item %path% is deprecated'), 298 | ]); 299 | 300 | $processor->process($schema, ['old' => 1]); // OK 301 | $processor->getWarnings(); // ["The item 'old' is deprecated"] 302 | ``` 303 | 304 | 305 | Ranges: min() max() 306 | ------------------- 307 | 308 | Use `min()` and `max()` to limit the number of elements for arrays: 309 | 310 | ```php 311 | // array, at least 10 items, maximum 20 items 312 | Expect::array()->min(10)->max(20); 313 | ``` 314 | 315 | For strings, limit their length: 316 | 317 | ```php 318 | // string, at least 10 characters long, maximum 20 characters 319 | Expect::string()->min(10)->max(20); 320 | ``` 321 | 322 | For numbers, limit their value: 323 | 324 | ```php 325 | // integer, between 10 and 20 inclusive 326 | Expect::int()->min(10)->max(20); 327 | ``` 328 | 329 | Of course, it is possible to mention only `min()`, or only `max()`: 330 | 331 | ```php 332 | // string, maximum 20 characters 333 | Expect::string()->max(20); 334 | ``` 335 | 336 | 337 | Regular Expressions: pattern() 338 | ------------------------------ 339 | 340 | Using `pattern()`, you can specify a regular expression which the **whole** input string must match (i.e. as if it were wrapped in characters `^` a `$`): 341 | 342 | ```php 343 | // just 9 digits 344 | Expect::string()->pattern('\d{9}'); 345 | ``` 346 | 347 | 348 | Custom Assertions: assert() 349 | --------------------------- 350 | 351 | You can add any other restrictions using `assert(callable $fn)`. 352 | 353 | ```php 354 | $countIsEven = fn($v) => count($v) % 2 === 0; 355 | 356 | $schema = Expect::arrayOf('string') 357 | ->assert($countIsEven); // the count must be even 358 | 359 | $processor->process($schema, ['a', 'b']); // OK 360 | $processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 is not even 361 | ``` 362 | 363 | Or 364 | 365 | ```php 366 | Expect::string()->assert('is_file'); // the file must exist 367 | ``` 368 | 369 | You can add your own description for each assertion. It will be part of the error message. 370 | 371 | ```php 372 | $schema = Expect::arrayOf('string') 373 | ->assert($countIsEven, 'Even items in array'); 374 | 375 | $processor->process($schema, ['a', 'b', 'c']); 376 | // Failed assertion "Even items in array" for item with value array. 377 | ``` 378 | 379 | The method can be called repeatedly to add multiple constraints. It can be intermixed with calls to `transform()` and `castTo()`. 380 | 381 | 382 | Transformation: transform() 383 | --------------------------- 384 | 385 | Successfully validated data can be modified using a custom function: 386 | 387 | ```php 388 | // conversion to uppercase: 389 | Expect::string()->transform(fn(string $s) => strtoupper($s)); 390 | ``` 391 | 392 | The method can be called repeatedly to add multiple transformations. It can be intermixed with calls to `assert()` and `castTo()`. The operations will be executed in the order in which they are declared: 393 | 394 | ```php 395 | Expect::type('string|int') 396 | ->castTo('string') 397 | ->assert('ctype_lower', 'All characters must be lowercased') 398 | ->transform(fn(string $s) => strtoupper($s)); // conversion to uppercase 399 | ``` 400 | 401 | The `transform()` method can both transform and validate the value simultaneously. This is often simpler and less redundant than chaining `transform()` and `assert()`. For this purpose, the function receives a [Nette\Schema\Context](https://api.nette.org/schema/master/Nette/Schema/Context.html) object with an `addError()` method, which can be used to add information about validation issues: 402 | 403 | ```php 404 | Expect::string() 405 | ->transform(function (string $s, Nette\Schema\Context $context) { 406 | if (!ctype_lower($s)) { 407 | $context->addError('All characters must be lowercased', 'my.case.error'); 408 | return null; 409 | } 410 | 411 | return strtoupper($s); 412 | }); 413 | ``` 414 | 415 | 416 | Casting: castTo() 417 | ----------------- 418 | 419 | Successfully validated data can be cast: 420 | 421 | ```php 422 | Expect::scalar()->castTo('string'); 423 | ``` 424 | 425 | In addition to native PHP types, you can also cast to classes. It distinguishes whether it is a simple class without a constructor or a class with a constructor. If the class has no constructor, an instance of it is created and all elements of the structure are written to its properties: 426 | 427 | ```php 428 | class Info 429 | { 430 | public bool $processRefund; 431 | public int $refundAmount; 432 | } 433 | 434 | Expect::structure([ 435 | 'processRefund' => Expect::bool(), 436 | 'refundAmount' => Expect::int(), 437 | ])->castTo(Info::class); 438 | 439 | // creates '$obj = new Info' and writes to $obj->processRefund and $obj->refundAmount 440 | ``` 441 | 442 | If the class has a constructor, the elements of the structure are passed as named parameters to the constructor: 443 | 444 | ```php 445 | class Info 446 | { 447 | public function __construct( 448 | public bool $processRefund, 449 | public int $refundAmount, 450 | ) { 451 | } 452 | } 453 | 454 | // creates $obj = new Info(processRefund: ..., refundAmount: ...) 455 | ``` 456 | 457 | Casting combined with a scalar parameter creates an object and passes the value as the sole parameter to the constructor: 458 | 459 | ```php 460 | Expect::string()->castTo(DateTime::class); 461 | // creates new DateTime(...) 462 | ``` 463 | 464 | 465 | Normalization: before() 466 | ----------------------- 467 | 468 | Prior to the validation itself, the data can be normalized using the method `before()`. As an example, let's have an element that must be an array of strings (eg `['a', 'b', 'c']`), but receives input in the form of a string `a b c`: 469 | 470 | ```php 471 | $explode = fn($v) => explode(' ', $v); 472 | 473 | $schema = Expect::arrayOf('string') 474 | ->before($explode); 475 | 476 | $normalized = $processor->process($schema, 'a b c'); 477 | // OK, returns ['a', 'b', 'c'] 478 | ``` 479 | 480 | 481 | Mapping to Objects: from() 482 | -------------------------- 483 | 484 | You can generate structure schema from the class. Example: 485 | 486 | ```php 487 | class Config 488 | { 489 | public string $name; 490 | public ?string $password; 491 | public bool $admin = false; 492 | } 493 | 494 | $schema = Expect::from(new Config); 495 | 496 | $data = [ 497 | 'name' => 'jeff', 498 | ]; 499 | 500 | $normalized = $processor->process($schema, $data); 501 | // $normalized instanceof Config 502 | // $normalized = {'name' => 'jeff', 'password' => null, 'admin' => false} 503 | ``` 504 | 505 | Anonymous classes are also supported: 506 | 507 | ```php 508 | $schema = Expect::from(new class { 509 | public string $name; 510 | public ?string $password; 511 | public bool $admin = false; 512 | }); 513 | ``` 514 | 515 | Because the information obtained from the class definition may not be sufficient, you can add a custom schema for the elements with the second parameter: 516 | 517 | ```php 518 | $schema = Expect::from(new Config, [ 519 | 'name' => Expect::string()->pattern('\w:.*'), 520 | ]); 521 | ``` 522 | -------------------------------------------------------------------------------- /src/Schema/Context.php: -------------------------------------------------------------------------------- 1 | isKey; 35 | return $this->errors[] = new Message($message, $code, $this->path, $variables); 36 | } 37 | 38 | 39 | public function addWarning(string $message, string $code, array $variables = []): Message 40 | { 41 | return $this->warnings[] = new Message($message, $code, $this->path, $variables); 42 | } 43 | 44 | 45 | /** @return \Closure(): bool */ 46 | public function createChecker(): \Closure 47 | { 48 | $count = count($this->errors); 49 | return fn(): bool => $count === count($this->errors); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Schema/DynamicParameter.php: -------------------------------------------------------------------------------- 1 | set = $set; 32 | } 33 | 34 | 35 | public function firstIsDefault(): self 36 | { 37 | $this->default = $this->set[0]; 38 | return $this; 39 | } 40 | 41 | 42 | public function nullable(): self 43 | { 44 | $this->set[] = null; 45 | return $this; 46 | } 47 | 48 | 49 | public function dynamic(): self 50 | { 51 | $this->set[] = new Type(Nette\Schema\DynamicParameter::class); 52 | return $this; 53 | } 54 | 55 | 56 | /********************* processing ****************d*g**/ 57 | 58 | 59 | public function normalize(mixed $value, Context $context): mixed 60 | { 61 | return $this->doNormalize($value, $context); 62 | } 63 | 64 | 65 | public function merge(mixed $value, mixed $base): mixed 66 | { 67 | return Helpers::merge($value, $base); 68 | } 69 | 70 | 71 | public function complete(mixed $value, Context $context): mixed 72 | { 73 | $isOk = $context->createChecker(); 74 | $value = $this->findAlternative($value, $context); 75 | $isOk() && $value = $this->doTransform($value, $context); 76 | return $isOk() ? $value : null; 77 | } 78 | 79 | 80 | private function findAlternative(mixed $value, Context $context): mixed 81 | { 82 | $expecteds = $innerErrors = []; 83 | foreach ($this->set as $item) { 84 | if ($item instanceof Schema) { 85 | $dolly = new Context; 86 | $dolly->path = $context->path; 87 | $res = $item->complete($item->normalize($value, $dolly), $dolly); 88 | if (!$dolly->errors) { 89 | $context->warnings = array_merge($context->warnings, $dolly->warnings); 90 | return $res; 91 | } 92 | 93 | foreach ($dolly->errors as $error) { 94 | if ($error->path !== $context->path || empty($error->variables['expected'])) { 95 | $innerErrors[] = $error; 96 | } else { 97 | $expecteds[] = $error->variables['expected']; 98 | } 99 | } 100 | } else { 101 | if ($item === $value) { 102 | return $value; 103 | } 104 | 105 | $expecteds[] = Nette\Schema\Helpers::formatValue($item); 106 | } 107 | } 108 | 109 | if ($innerErrors) { 110 | $context->errors = array_merge($context->errors, $innerErrors); 111 | } else { 112 | $context->addError( 113 | 'The %label% %path% expects to be %expected%, %value% given.', 114 | Nette\Schema\Message::TypeMismatch, 115 | [ 116 | 'value' => $value, 117 | 'expected' => implode('|', array_unique($expecteds)), 118 | ], 119 | ); 120 | } 121 | 122 | return null; 123 | } 124 | 125 | 126 | public function completeDefault(Context $context): mixed 127 | { 128 | if ($this->required) { 129 | $context->addError( 130 | 'The mandatory item %path% is missing.', 131 | Nette\Schema\Message::MissingItem, 132 | ); 133 | return null; 134 | } 135 | 136 | if ($this->default instanceof Schema) { 137 | return $this->default->completeDefault($context); 138 | } 139 | 140 | return $this->default; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Schema/Elements/Base.php: -------------------------------------------------------------------------------- 1 | default = $value; 36 | return $this; 37 | } 38 | 39 | 40 | public function required(bool $state = true): self 41 | { 42 | $this->required = $state; 43 | return $this; 44 | } 45 | 46 | 47 | public function before(callable $handler): self 48 | { 49 | $this->before = $handler; 50 | return $this; 51 | } 52 | 53 | 54 | public function castTo(string $type): self 55 | { 56 | return $this->transform(Helpers::getCastStrategy($type)); 57 | } 58 | 59 | 60 | public function transform(callable $handler): self 61 | { 62 | $this->transforms[] = $handler; 63 | return $this; 64 | } 65 | 66 | 67 | public function assert(callable $handler, ?string $description = null): self 68 | { 69 | $expected = $description ?: (is_string($handler) ? "$handler()" : '#' . count($this->transforms)); 70 | return $this->transform(function ($value, Context $context) use ($handler, $description, $expected) { 71 | if ($handler($value)) { 72 | return $value; 73 | } 74 | $context->addError( 75 | 'Failed assertion ' . ($description ? "'%assertion%'" : '%assertion%') . ' for %label% %path% with value %value%.', 76 | Nette\Schema\Message::FailedAssertion, 77 | ['value' => $value, 'assertion' => $expected], 78 | ); 79 | }); 80 | } 81 | 82 | 83 | /** Marks as deprecated */ 84 | public function deprecated(string $message = 'The item %path% is deprecated.'): self 85 | { 86 | $this->deprecated = $message; 87 | return $this; 88 | } 89 | 90 | 91 | public function completeDefault(Context $context): mixed 92 | { 93 | if ($this->required) { 94 | $context->addError( 95 | 'The mandatory item %path% is missing.', 96 | Nette\Schema\Message::MissingItem, 97 | ); 98 | return null; 99 | } 100 | 101 | return $this->default; 102 | } 103 | 104 | 105 | public function doNormalize(mixed $value, Context $context): mixed 106 | { 107 | if ($this->before) { 108 | $value = ($this->before)($value); 109 | } 110 | 111 | return $value; 112 | } 113 | 114 | 115 | private function doDeprecation(Context $context): void 116 | { 117 | if ($this->deprecated !== null) { 118 | $context->addWarning( 119 | $this->deprecated, 120 | Nette\Schema\Message::Deprecated, 121 | ); 122 | } 123 | } 124 | 125 | 126 | private function doTransform(mixed $value, Context $context): mixed 127 | { 128 | $isOk = $context->createChecker(); 129 | foreach ($this->transforms as $handler) { 130 | $value = $handler($value, $context); 131 | if (!$isOk()) { 132 | return null; 133 | } 134 | } 135 | return $value; 136 | } 137 | 138 | 139 | /** @deprecated use Nette\Schema\Validators::validateType() */ 140 | private function doValidate(mixed $value, string $expected, Context $context): bool 141 | { 142 | $isOk = $context->createChecker(); 143 | Helpers::validateType($value, $expected, $context); 144 | return $isOk(); 145 | } 146 | 147 | 148 | /** @deprecated use Nette\Schema\Validators::validateRange() */ 149 | private static function doValidateRange(mixed $value, array $range, Context $context, string $types = ''): bool 150 | { 151 | $isOk = $context->createChecker(); 152 | Helpers::validateRange($value, $range, $context, $types); 153 | return $isOk(); 154 | } 155 | 156 | 157 | /** @deprecated use doTransform() */ 158 | private function doFinalize(mixed $value, Context $context): mixed 159 | { 160 | return $this->doTransform($value, $context); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/Schema/Elements/Structure.php: -------------------------------------------------------------------------------- 1 | items = $shape; 40 | $this->castTo('object'); 41 | $this->required = true; 42 | } 43 | 44 | 45 | public function default(mixed $value): self 46 | { 47 | throw new Nette\InvalidStateException('Structure cannot have default value.'); 48 | } 49 | 50 | 51 | public function min(?int $min): self 52 | { 53 | $this->range[0] = $min; 54 | return $this; 55 | } 56 | 57 | 58 | public function max(?int $max): self 59 | { 60 | $this->range[1] = $max; 61 | return $this; 62 | } 63 | 64 | 65 | public function otherItems(string|Schema $type = 'mixed'): self 66 | { 67 | $this->otherItems = $type instanceof Schema ? $type : new Type($type); 68 | return $this; 69 | } 70 | 71 | 72 | public function skipDefaults(bool $state = true): self 73 | { 74 | $this->skipDefaults = $state; 75 | return $this; 76 | } 77 | 78 | 79 | public function extend(array|self $shape): self 80 | { 81 | $shape = $shape instanceof self ? $shape->items : $shape; 82 | return new self(array_merge($this->items, $shape)); 83 | } 84 | 85 | 86 | public function getShape(): array 87 | { 88 | return $this->items; 89 | } 90 | 91 | 92 | /********************* processing ****************d*g**/ 93 | 94 | 95 | public function normalize(mixed $value, Context $context): mixed 96 | { 97 | $value = $this->doNormalize($value, $context); 98 | if (is_object($value)) { 99 | $value = (array) $value; 100 | } 101 | 102 | if (is_array($value)) { 103 | foreach ($value as $key => $val) { 104 | $itemSchema = $this->items[$key] ?? $this->otherItems; 105 | if ($itemSchema) { 106 | $context->path[] = $key; 107 | $value[$key] = $itemSchema->normalize($val, $context); 108 | array_pop($context->path); 109 | } 110 | } 111 | } 112 | 113 | return $value; 114 | } 115 | 116 | 117 | public function merge(mixed $value, mixed $base): mixed 118 | { 119 | if (is_array($value) && isset($value[Helpers::PreventMerging])) { 120 | unset($value[Helpers::PreventMerging]); 121 | $base = null; 122 | } 123 | 124 | if (is_array($value) && is_array($base)) { 125 | $index = $this->otherItems === null ? null : 0; 126 | foreach ($value as $key => $val) { 127 | if ($key === $index) { 128 | $base[] = $val; 129 | $index++; 130 | } else { 131 | $base[$key] = array_key_exists($key, $base) && ($itemSchema = $this->items[$key] ?? $this->otherItems) 132 | ? $itemSchema->merge($val, $base[$key]) 133 | : $val; 134 | } 135 | } 136 | 137 | return $base; 138 | } 139 | 140 | return $value ?? $base; 141 | } 142 | 143 | 144 | public function complete(mixed $value, Context $context): mixed 145 | { 146 | if ($value === null) { 147 | $value = []; // is unable to distinguish null from array in NEON 148 | } 149 | 150 | $this->doDeprecation($context); 151 | 152 | $isOk = $context->createChecker(); 153 | Helpers::validateType($value, 'array', $context); 154 | $isOk() && Helpers::validateRange($value, $this->range, $context); 155 | $isOk() && $this->validateItems($value, $context); 156 | $isOk() && $value = $this->doTransform($value, $context); 157 | return $isOk() ? $value : null; 158 | } 159 | 160 | 161 | private function validateItems(array &$value, Context $context): void 162 | { 163 | $items = $this->items; 164 | if ($extraKeys = array_keys(array_diff_key($value, $items))) { 165 | if ($this->otherItems) { 166 | $items += array_fill_keys($extraKeys, $this->otherItems); 167 | } else { 168 | $keys = array_map('strval', array_keys($items)); 169 | foreach ($extraKeys as $key) { 170 | $hint = Nette\Utils\Helpers::getSuggestion($keys, (string) $key); 171 | $context->addError( 172 | 'Unexpected item %path%' . ($hint ? ", did you mean '%hint%'?" : '.'), 173 | Nette\Schema\Message::UnexpectedItem, 174 | ['hint' => $hint], 175 | )->path[] = $key; 176 | } 177 | } 178 | } 179 | 180 | foreach ($items as $itemKey => $itemVal) { 181 | $context->path[] = $itemKey; 182 | if (array_key_exists($itemKey, $value)) { 183 | $value[$itemKey] = $itemVal->complete($value[$itemKey], $context); 184 | } else { 185 | $default = $itemVal->completeDefault($context); // checks required item 186 | if (!$context->skipDefaults && !$this->skipDefaults) { 187 | $value[$itemKey] = $default; 188 | } 189 | } 190 | 191 | array_pop($context->path); 192 | } 193 | } 194 | 195 | 196 | public function completeDefault(Context $context): mixed 197 | { 198 | return $this->required 199 | ? $this->complete([], $context) 200 | : null; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Schema/Elements/Type.php: -------------------------------------------------------------------------------- 1 | [], 'array' => []]; 37 | $this->type = $type; 38 | $this->default = strpos($type, '[]') ? [] : $defaults[$type] ?? null; 39 | } 40 | 41 | 42 | public function nullable(): self 43 | { 44 | $this->type = 'null|' . $this->type; 45 | return $this; 46 | } 47 | 48 | 49 | /** @deprecated mergeDefaults is disabled by default */ 50 | public function mergeDefaults(bool $state = true): self 51 | { 52 | if ($state === true) { 53 | trigger_error(__METHOD__ . '() is deprecated and will be removed in the next major version.', E_USER_DEPRECATED); 54 | } 55 | $this->merge = $state; 56 | return $this; 57 | } 58 | 59 | 60 | public function mergeMode(MergeMode $mode): self 61 | { 62 | $this->mergeMode = $mode; 63 | return $this; 64 | } 65 | 66 | 67 | public function dynamic(): self 68 | { 69 | $this->type = DynamicParameter::class . '|' . $this->type; 70 | return $this; 71 | } 72 | 73 | 74 | public function min(?float $min): self 75 | { 76 | $this->range[0] = $min; 77 | return $this; 78 | } 79 | 80 | 81 | public function max(?float $max): self 82 | { 83 | $this->range[1] = $max; 84 | return $this; 85 | } 86 | 87 | 88 | /** 89 | * @internal use arrayOf() or listOf() 90 | */ 91 | public function items(string|Schema $valueType = 'mixed', string|Schema|null $keyType = null): self 92 | { 93 | $this->itemsValue = $valueType instanceof Schema 94 | ? $valueType 95 | : new self($valueType); 96 | $this->itemsKey = $keyType instanceof Schema || $keyType === null 97 | ? $keyType 98 | : new self($keyType); 99 | return $this; 100 | } 101 | 102 | 103 | public function pattern(?string $pattern): self 104 | { 105 | $this->pattern = $pattern; 106 | return $this; 107 | } 108 | 109 | 110 | /********************* processing ****************d*g**/ 111 | 112 | 113 | public function normalize(mixed $value, Context $context): mixed 114 | { 115 | $value = $this->doNormalize($value, $context); 116 | if (is_array($value) && $this->itemsValue) { 117 | $res = []; 118 | foreach ($value as $key => $val) { 119 | $context->path[] = $key; 120 | $context->isKey = true; 121 | $key = $this->itemsKey 122 | ? $this->itemsKey->normalize($key, $context) 123 | : $key; 124 | $context->isKey = false; 125 | $res[$key] = $this->itemsValue->normalize($val, $context); 126 | array_pop($context->path); 127 | } 128 | 129 | $value = $res; 130 | } 131 | 132 | return $value; 133 | } 134 | 135 | 136 | public function merge(mixed $value, mixed $base): mixed 137 | { 138 | if ($this->mergeMode === MergeMode::Replace) { 139 | return $value; 140 | } 141 | 142 | if (is_array($value) && is_array($base)) { 143 | $index = $this->mergeMode === MergeMode::OverwriteKeys ? null : 0; 144 | foreach ($value as $key => $val) { 145 | if ($key === $index) { 146 | $base[] = $val; 147 | $index++; 148 | } else { 149 | $base[$key] = array_key_exists($key, $base) && $this->itemsValue 150 | ? $this->itemsValue->merge($val, $base[$key]) 151 | : $val; 152 | } 153 | } 154 | 155 | return $base; 156 | } 157 | 158 | return $value === null && is_array($base) ? $base : $value; 159 | } 160 | 161 | 162 | public function complete(mixed $value, Context $context): mixed 163 | { 164 | if ($value === null && is_array($this->default)) { 165 | $value = []; // is unable to distinguish null from array in NEON 166 | } 167 | 168 | $this->doDeprecation($context); 169 | 170 | $isOk = $context->createChecker(); 171 | Helpers::validateType($value, $this->type, $context); 172 | $isOk() && Helpers::validateRange($value, $this->range, $context, $this->type); 173 | $isOk() && $value !== null && $this->pattern !== null && Helpers::validatePattern($value, $this->pattern, $context); 174 | $isOk() && is_array($value) && $this->validateItems($value, $context); 175 | $isOk() && $this->merge && $value = Helpers::merge($value, $this->default); 176 | $isOk() && $value = $this->doTransform($value, $context); 177 | if (!$isOk()) { 178 | return null; 179 | } 180 | 181 | if ($value instanceof DynamicParameter) { 182 | $expected = $this->type . ($this->range === [null, null] ? '' : ':' . implode('..', $this->range)); 183 | $context->dynamics[] = [$value, str_replace(DynamicParameter::class . '|', '', $expected), $context->path]; 184 | } 185 | return $value; 186 | } 187 | 188 | 189 | private function validateItems(array &$value, Context $context): void 190 | { 191 | if (!$this->itemsValue) { 192 | return; 193 | } 194 | 195 | $res = []; 196 | foreach ($value as $key => $val) { 197 | $context->path[] = $key; 198 | $context->isKey = true; 199 | $key = $this->itemsKey ? $this->itemsKey->complete($key, $context) : $key; 200 | $context->isKey = false; 201 | $res[$key] = $this->itemsValue->complete($val, $context); 202 | array_pop($context->path); 203 | } 204 | $value = $res; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Schema/Expect.php: -------------------------------------------------------------------------------- 1 | default($args[0]); 39 | } 40 | 41 | return $type; 42 | } 43 | 44 | 45 | public static function type(string $type): Type 46 | { 47 | return new Type($type); 48 | } 49 | 50 | 51 | public static function anyOf(mixed ...$set): AnyOf 52 | { 53 | return new AnyOf(...$set); 54 | } 55 | 56 | 57 | /** 58 | * @param Schema[] $shape 59 | */ 60 | public static function structure(array $shape): Structure 61 | { 62 | return new Structure($shape); 63 | } 64 | 65 | 66 | public static function from(object|string $object, array $items = []): Structure 67 | { 68 | $ro = new \ReflectionClass($object); 69 | $props = $ro->hasMethod('__construct') 70 | ? $ro->getMethod('__construct')->getParameters() 71 | : $ro->getProperties(); 72 | 73 | foreach ($props as $prop) { 74 | \assert($prop instanceof \ReflectionProperty || $prop instanceof \ReflectionParameter); 75 | if ($item = &$items[$prop->getName()]) { 76 | continue; 77 | } 78 | 79 | $item = new Type($propType = (string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed')); 80 | if (class_exists($propType)) { 81 | $item = static::from($propType); 82 | } 83 | 84 | $hasDefault = match (true) { 85 | $prop instanceof \ReflectionParameter => $prop->isOptional(), 86 | is_object($object) => $prop->isInitialized($object), 87 | default => $prop->hasDefaultValue(), 88 | }; 89 | if ($hasDefault) { 90 | $default = match (true) { 91 | $prop instanceof \ReflectionParameter => $prop->getDefaultValue(), 92 | is_object($object) => $prop->getValue($object), 93 | default => $prop->getDefaultValue(), 94 | }; 95 | if (is_object($default)) { 96 | $item = static::from($default); 97 | } else { 98 | $item->default($default); 99 | } 100 | } else { 101 | $item->required(); 102 | } 103 | } 104 | 105 | return (new Structure($items))->castTo($ro->getName()); 106 | } 107 | 108 | 109 | /** 110 | * @param mixed[] $shape 111 | */ 112 | public static function array(?array $shape = []): Structure|Type 113 | { 114 | return Nette\Utils\Arrays::first($shape ?? []) instanceof Schema 115 | ? (new Structure($shape))->castTo('array') 116 | : (new Type('array'))->default($shape); 117 | } 118 | 119 | 120 | public static function arrayOf(string|Schema $valueType, string|Schema|null $keyType = null): Type 121 | { 122 | return (new Type('array'))->items($valueType, $keyType); 123 | } 124 | 125 | 126 | public static function listOf(string|Schema $type): Type 127 | { 128 | return (new Type('list'))->items($type); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Schema/Helpers.php: -------------------------------------------------------------------------------- 1 | $val) { 38 | if ($key === $index) { 39 | $base[] = $val; 40 | $index++; 41 | } else { 42 | $base[$key] = static::merge($val, $base[$key] ?? null); 43 | } 44 | } 45 | 46 | return $base; 47 | 48 | } elseif ($value === null && is_array($base)) { 49 | return $base; 50 | 51 | } else { 52 | return $value; 53 | } 54 | } 55 | 56 | 57 | public static function formatValue(mixed $value): string 58 | { 59 | if ($value instanceof DynamicParameter) { 60 | return 'dynamic'; 61 | } elseif (is_object($value)) { 62 | return 'object ' . $value::class; 63 | } elseif (is_string($value)) { 64 | return "'" . Nette\Utils\Strings::truncate($value, 15, '...') . "'"; 65 | } elseif (is_scalar($value)) { 66 | return var_export($value, return: true); 67 | } else { 68 | return get_debug_type($value); 69 | } 70 | } 71 | 72 | 73 | public static function validateType(mixed $value, string $expected, Context $context): void 74 | { 75 | if (!Nette\Utils\Validators::is($value, $expected)) { 76 | $expected = str_replace(DynamicParameter::class . '|', '', $expected); 77 | $expected = str_replace(['|', ':'], [' or ', ' in range '], $expected); 78 | $context->addError( 79 | 'The %label% %path% expects to be %expected%, %value% given.', 80 | Message::TypeMismatch, 81 | ['value' => $value, 'expected' => $expected], 82 | ); 83 | } 84 | } 85 | 86 | 87 | public static function validateRange(mixed $value, array $range, Context $context, string $types = ''): void 88 | { 89 | if (is_array($value) || is_string($value)) { 90 | [$length, $label] = is_array($value) 91 | ? [count($value), 'items'] 92 | : (in_array('unicode', explode('|', $types), true) 93 | ? [Nette\Utils\Strings::length($value), 'characters'] 94 | : [strlen($value), 'bytes']); 95 | 96 | if (!self::isInRange($length, $range)) { 97 | $context->addError( 98 | "The length of %label% %path% expects to be in range %expected%, %length% $label given.", 99 | Message::LengthOutOfRange, 100 | ['value' => $value, 'length' => $length, 'expected' => implode('..', $range)], 101 | ); 102 | } 103 | } elseif ((is_int($value) || is_float($value)) && !self::isInRange($value, $range)) { 104 | $context->addError( 105 | 'The %label% %path% expects to be in range %expected%, %value% given.', 106 | Message::ValueOutOfRange, 107 | ['value' => $value, 'expected' => implode('..', $range)], 108 | ); 109 | } 110 | } 111 | 112 | 113 | public static function isInRange(mixed $value, array $range): bool 114 | { 115 | return ($range[0] === null || $value >= $range[0]) 116 | && ($range[1] === null || $value <= $range[1]); 117 | } 118 | 119 | 120 | public static function validatePattern(string $value, string $pattern, Context $context): void 121 | { 122 | if (!preg_match("\x01^(?:$pattern)$\x01Du", $value)) { 123 | $context->addError( 124 | "The %label% %path% expects to match pattern '%pattern%', %value% given.", 125 | Message::PatternMismatch, 126 | ['value' => $value, 'pattern' => $pattern], 127 | ); 128 | } 129 | } 130 | 131 | 132 | public static function getCastStrategy(string $type): \Closure 133 | { 134 | if (Nette\Utils\Reflection::isBuiltinType($type)) { 135 | return static function ($value) use ($type) { 136 | settype($value, $type); 137 | return $value; 138 | }; 139 | } elseif (method_exists($type, '__construct')) { 140 | return static fn($value) => is_array($value) || $value instanceof \stdClass 141 | ? new $type(...(array) $value) 142 | : new $type($value); 143 | } else { 144 | return static fn($value) => Nette\Utils\Arrays::toObject((array) $value, new $type); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Schema/MergeMode.php: -------------------------------------------------------------------------------- 1 | message = $message; 78 | $this->code = $code; 79 | $this->path = $path; 80 | $this->variables = $variables; 81 | } 82 | 83 | 84 | public function toString(): string 85 | { 86 | $vars = $this->variables; 87 | $vars['label'] = empty($vars['isKey']) ? 'item' : 'key of item'; 88 | $vars['path'] = $this->path 89 | ? "'" . implode("\u{a0}›\u{a0}", $this->path) . "'" 90 | : null; 91 | $vars['value'] = Helpers::formatValue($vars['value'] ?? null); 92 | 93 | return preg_replace_callback('~( ?)%(\w+)%~', function ($m) use ($vars) { 94 | [, $space, $key] = $m; 95 | return $vars[$key] === null ? '' : $space . $vars[$key]; 96 | }, $this->message) ?? throw new Nette\InvalidStateException(preg_last_error_msg()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Schema/Processor.php: -------------------------------------------------------------------------------- 1 | skipDefaults = $value; 28 | } 29 | 30 | 31 | /** 32 | * Normalizes and validates data. Result is a clean completed data. 33 | * @throws ValidationException 34 | */ 35 | public function process(Schema $schema, mixed $data): mixed 36 | { 37 | $this->createContext(); 38 | $data = $schema->normalize($data, $this->context); 39 | $this->throwsErrors(); 40 | $data = $schema->complete($data, $this->context); 41 | $this->throwsErrors(); 42 | return $data; 43 | } 44 | 45 | 46 | /** 47 | * Normalizes and validates and merges multiple data. Result is a clean completed data. 48 | * @throws ValidationException 49 | */ 50 | public function processMultiple(Schema $schema, array $dataset): mixed 51 | { 52 | $this->createContext(); 53 | $flatten = null; 54 | $first = true; 55 | foreach ($dataset as $data) { 56 | $data = $schema->normalize($data, $this->context); 57 | $this->throwsErrors(); 58 | $flatten = $first ? $data : $schema->merge($data, $flatten); 59 | $first = false; 60 | } 61 | 62 | $data = $schema->complete($flatten, $this->context); 63 | $this->throwsErrors(); 64 | return $data; 65 | } 66 | 67 | 68 | /** 69 | * @return string[] 70 | */ 71 | public function getWarnings(): array 72 | { 73 | $res = []; 74 | foreach ($this->context->warnings as $message) { 75 | $res[] = $message->toString(); 76 | } 77 | 78 | return $res; 79 | } 80 | 81 | 82 | private function throwsErrors(): void 83 | { 84 | if ($this->context->errors) { 85 | throw new ValidationException(null, $this->context->errors); 86 | } 87 | } 88 | 89 | 90 | private function createContext(): void 91 | { 92 | $this->context = new Context; 93 | $this->context->skipDefaults = $this->skipDefaults; 94 | Nette\Utils\Arrays::invoke($this->onNewContext, $this->context); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Schema/Schema.php: -------------------------------------------------------------------------------- 1 | toString()); 30 | $this->messages = $messages; 31 | } 32 | 33 | 34 | /** 35 | * @return string[] 36 | */ 37 | public function getMessages(): array 38 | { 39 | $res = []; 40 | foreach ($this->messages as $message) { 41 | $res[] = $message->toString(); 42 | } 43 | 44 | return $res; 45 | } 46 | 47 | 48 | /** 49 | * @return Message[] 50 | */ 51 | public function getMessageObjects(): array 52 | { 53 | return $this->messages; 54 | } 55 | } 56 | --------------------------------------------------------------------------------