You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

441 lines
13 KiB

3 years ago
  1. Nette Schema
  2. ************
  3. [![Downloads this Month](https://img.shields.io/packagist/dm/nette/schema.svg)](https://packagist.org/packages/nette/schema)
  4. [![Tests](https://github.com/nette/schema/workflows/Tests/badge.svg?branch=master)](https://github.com/nette/schema/actions)
  5. [![Coverage Status](https://coveralls.io/repos/github/nette/schema/badge.svg?branch=master)](https://coveralls.io/github/nette/schema?branch=master)
  6. [![Latest Stable Version](https://poser.pugx.org/nette/schema/v/stable)](https://github.com/nette/schema/releases)
  7. [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/schema/blob/master/license.md)
  8. Introduction
  9. ============
  10. A practical library for validation and normalization of data structures against a given schema with a smart & easy-to-understand API.
  11. Documentation can be found on the [website](https://doc.nette.org/schema).
  12. Installation:
  13. ```shell
  14. composer require nette/schema
  15. ```
  16. It requires PHP version 7.1 and supports PHP up to 8.0.
  17. [Support Me](https://github.com/sponsors/dg)
  18. --------------------------------------------
  19. Do you like Nette DI? Are you looking forward to the new features?
  20. [![Buy me a coffee](https://files.nette.org/icons/donation-3.svg)](https://github.com/sponsors/dg)
  21. Thank you!
  22. Basic Usage
  23. -----------
  24. 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.
  25. The task is handled by the [Nette\Schema\Processor](https://api.nette.org/3.0/Nette/Schema/Processor.html) class, which processes the input and either returns normalized data or throws an [Nette\Schema\ValidationException](https://api.nette.org/3.0/Nette/Schema/ValidationException.html) exception on error.
  26. ```php
  27. $processor = new Nette\Schema\Processor;
  28. try {
  29. $normalized = $processor->process($schema, $data);
  30. } catch (Nette\Schema\ValidationException $e) {
  31. echo 'Data is invalid: ' . $e->getMessage();
  32. }
  33. ```
  34. Method `$e->getMessages()` returns array of all message strings and `$e->getMessageObjects()` return all messages as [Nette\Schema\Message](https://api.nette.org/3.1/Nette/Schema/Message.html) objects.
  35. Defining Schema
  36. ---------------
  37. And now let's create a schema. The class [Nette\Schema\Expect](https://api.nette.org/3.0/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.
  38. ```php
  39. use Nette\Schema\Expect;
  40. $schema = Expect::structure([
  41. 'processRefund' => Expect::bool(),
  42. 'refundAmount' => Expect::int(),
  43. ]);
  44. ```
  45. We believe that the schema definition looks clear, even if you see it for the very first time.
  46. Lets send the following data for validation:
  47. ```php
  48. $data = [
  49. 'processRefund' => true,
  50. 'refundAmount' => 17,
  51. ];
  52. $normalized = $processor->process($schema, $data); // OK, it passes
  53. ```
  54. 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')`.
  55. All elements of the structure are optional and have a default value `null`. Example:
  56. ```php
  57. $data = [
  58. 'refundAmount' => 17,
  59. ];
  60. $normalized = $processor->process($schema, $data); // OK, it passes
  61. // $normalized = {'processRefund' => null, 'refundAmount' => 17}
  62. ```
  63. 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()`.
  64. 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)`.
  65. 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:
  66. ```php
  67. $schema = Expect::structure([
  68. 'processRefund' => Expect::anyOf(true, false, 1, 0)->castTo('bool'),
  69. 'refundAmount' => Expect::int(),
  70. ]);
  71. $normalized = $processor->process($schema, $data);
  72. is_bool($normalized->processRefund); // true
  73. ```
  74. 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.
  75. Data Types: type()
  76. ------------------
  77. All standard PHP data types can be listed in the schema:
  78. ```php
  79. Expect::string($default = null)
  80. Expect::int($default = null)
  81. Expect::float($default = null)
  82. Expect::bool($default = null)
  83. Expect::null()
  84. Expect::array($default = [])
  85. ```
  86. 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')`.
  87. You can also use union notation:
  88. ```php
  89. Expect::type('bool|string|array')
  90. ```
  91. 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).
  92. Array of Values: arrayOf() listOf()
  93. -----------------------------------
  94. 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:
  95. ```php
  96. $schema = Expect::arrayOf('string');
  97. $processor->process($schema, ['hello', 'world']); // OK
  98. $processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
  99. $processor->process($schema, ['key' => 123]); // ERROR: 123 is not a string
  100. ```
  101. The list is an indexed array:
  102. ```php
  103. $schema = Expect::listOf('string');
  104. $processor->process($schema, ['a', 'b']); // OK
  105. $processor->process($schema, ['a', 123]); // ERROR: 123 is not a string
  106. $processor->process($schema, ['key' => 'a']); // ERROR: is not a list
  107. $processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: is not a list
  108. ```
  109. The parameter can also be a schema, so we can write:
  110. ```php
  111. Expect::arrayOf(Expect::bool())
  112. ```
  113. The default value is an empty array. If you specify default value, it will be merged with the passed data. This can be disabled using `mergeDefaults(false)`.
  114. Enumeration: anyOf()
  115. --------------------
  116. `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`:
  117. ```php
  118. $schema = Expect::listOf(
  119. Expect::anyOf('a', true, null)
  120. );
  121. $processor->process($schema, ['a', true, null, 'a']); // OK
  122. $processor->process($schema, ['a', false]); // ERROR: false does not belong there
  123. ```
  124. The enumeration elements can also be schemas:
  125. ```php
  126. $schema = Expect::listOf(
  127. Expect::anyOf(Expect::string(), true, null)
  128. );
  129. $processor->process($schema, ['foo', true, null, 'bar']); // OK
  130. $processor->process($schema, [123]); // ERROR
  131. ```
  132. The default value is `null`.
  133. Structures
  134. ----------
  135. Structures are objects with defined keys. Each of these key => value pairs is referred to as a "property":
  136. Structures accept arrays and objects and return objects `stdClass` (unless you change it with `castTo('array')`, etc.).
  137. By default, all properties are optional and have a default value of `null`. You can define mandatory properties using `required()`:
  138. ```php
  139. $schema = Expect::structure([
  140. 'required' => Expect::string()->required(),
  141. 'optional' => Expect::string(), // the default value is null
  142. ]);
  143. $processor->process($schema, ['optional' => '']);
  144. // ERROR: item 'required' is missing
  145. $processor->process($schema, ['required' => 'foo']);
  146. // OK, returns {'required' => 'foo', 'optional' => null}
  147. ```
  148. 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()`:
  149. ```php
  150. $schema = Expect::structure([
  151. 'optional' => Expect::string(),
  152. 'nullable' => Expect::string()->nullable(),
  153. ]);
  154. $processor->process($schema, ['optional' => null]);
  155. // ERROR: 'optional' expects to be string, null given.
  156. $processor->process($schema, ['nullable' => null]);
  157. // OK, returns {'optional' => null, 'nullable' => null}
  158. ```
  159. By default, there can be no extra items in the input data:
  160. ```php
  161. $schema = Expect::structure([
  162. 'key' => Expect::string(),
  163. ]);
  164. $processor->process($schema, ['additional' => 1]);
  165. // ERROR: Unexpected item 'additional'
  166. ```
  167. Which we can change with `otherItems()`. As a parameter, we will specify the schema for each extra element:
  168. ```php
  169. $schema = Expect::structure([
  170. 'key' => Expect::string(),
  171. ])->otherItems(Expect::int());
  172. $processor->process($schema, ['additional' => 1]); // OK
  173. $processor->process($schema, ['additional' => true]); // ERROR
  174. ```
  175. Deprecations
  176. ------------
  177. You can deprecate property using the `deprecated([string $message])` method. Deprecation notices are returned by `$processor->getWarnings()` (since v1.1):
  178. ```php
  179. $schema = Expect::structure([
  180. 'old' => Expect::int()->deprecated('The item %path% is deprecated'),
  181. ]);
  182. $processor->process($schema, ['old' => 1]); // OK
  183. $processor->getWarnings(); // ["The item 'old' is deprecated"]
  184. ```
  185. Ranges: min() max()
  186. -------------------
  187. Use `min()` and `max()` to limit the number of elements for arrays:
  188. ```php
  189. // array, at least 10 items, maximum 20 items
  190. Expect::array()->min(10)->max(20);
  191. ```
  192. For strings, limit their length:
  193. ```php
  194. // string, at least 10 characters long, maximum 20 characters
  195. Expect::string()->min(10)->max(20);
  196. ```
  197. For numbers, limit their value:
  198. ```php
  199. // integer, between 10 and 20 inclusive
  200. Expect::int()->min(10)->max(20);
  201. ```
  202. Of course, it is possible to mention only `min()`, or only `max()`:
  203. ```php
  204. // string, maximum 20 characters
  205. Expect::string()->max(20);
  206. ```
  207. Regular Expressions: pattern()
  208. ------------------------------
  209. 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 `$`):
  210. ```php
  211. // just 9 digits
  212. Expect::string()->pattern('\d{9}');
  213. ```
  214. Custom Assertions: assert()
  215. ---------------------------
  216. You can add any other restrictions using `assert(callable $fn)`.
  217. ```php
  218. $countIsEven = function ($v) { return count($v) % 2 === 0; };
  219. $schema = Expect::arrayOf('string')
  220. ->assert($countIsEven); // the count must be even
  221. $processor->process($schema, ['a', 'b']); // OK
  222. $processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 is not even
  223. ```
  224. Or
  225. ```php
  226. Expect::string()->assert('is_file'); // the file must exist
  227. ```
  228. You can add your own description for each assertions. It will be part of the error message.
  229. ```php
  230. $schema = Expect::arrayOf('string')
  231. ->assert($countIsEven, 'Even items in array');
  232. $processor->process($schema, ['a', 'b', 'c']);
  233. // Failed assertion "Even items in array" for item with value array.
  234. ```
  235. The method can be called repeatedly to add more assertions.
  236. Mapping to Objects: from()
  237. --------------------------
  238. You can generate structure schema from the class. Example:
  239. ```php
  240. class Config
  241. {
  242. /** @var string */
  243. public $name;
  244. /** @var string|null */
  245. public $password;
  246. /** @var bool */
  247. public $admin = false;
  248. }
  249. $schema = Expect::from(new Config);
  250. $data = [
  251. 'name' => 'jeff',
  252. ];
  253. $normalized = $processor->process($schema, $data);
  254. // $normalized instanceof Config
  255. // $normalized = {'name' => 'jeff', 'password' => null, 'admin' => false}
  256. ```
  257. If you are using PHP 7.4 or higher, you can use native types:
  258. ```php
  259. class Config
  260. {
  261. public string $name;
  262. public ?string $password;
  263. public bool $admin = false;
  264. }
  265. $schema = Expect::from(new Config);
  266. ```
  267. Anonymous classes are also supported:
  268. ```php
  269. $schema = Expect::from(new class {
  270. public string $name;
  271. public ?string $password;
  272. public bool $admin = false;
  273. });
  274. ```
  275. 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:
  276. ```php
  277. $schema = Expect::from(new Config, [
  278. 'name' => Expect::string()->pattern('\w:.*'),
  279. ]);
  280. ```
  281. Casting: castTo()
  282. -----------------
  283. Successfully validated data can be cast:
  284. ```php
  285. Expect::scalar()->castTo('string');
  286. ```
  287. In addition to native PHP types, you can also cast to classes:
  288. ```php
  289. Expect::scalar()->castTo('AddressEntity');
  290. ```
  291. Normalization: before()
  292. -----------------------
  293. 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`:
  294. ```php
  295. $explode = function ($v) { return explode(' ', $v); };
  296. $schema = Expect::arrayOf('string')
  297. ->before($explode);
  298. $normalized = $processor->process($schema, 'a b c');
  299. // OK, returns ['a', 'b', 'c']
  300. ```