no message
This commit is contained in:
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "nette/schema",
|
||||
"description": "📐 Nette Schema: validating data structures against a given Schema.",
|
||||
"keywords": ["nette", "config"],
|
||||
"homepage": "https://nette.org",
|
||||
"license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Grudl",
|
||||
"homepage": "https://davidgrudl.com"
|
||||
},
|
||||
{
|
||||
"name": "Nette Community",
|
||||
"homepage": "https://nette.org/contributors"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.1",
|
||||
"nette/utils": "^3.0.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^2.2",
|
||||
"tracy/tracy": "^2.3"
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": ["src/"]
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0-dev"
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
How to contribute & use the issue tracker
|
||||
=========================================
|
||||
|
||||
Nette welcomes your contributions. There are several ways to help out:
|
||||
|
||||
* Create an issue on GitHub, if you have found a bug
|
||||
* Write test cases for open bug issues
|
||||
* Write fixes for open bug/feature issues, preferably with test cases included
|
||||
* Contribute to the [documentation](https://nette.org/en/writing)
|
||||
|
||||
Issues
|
||||
------
|
||||
|
||||
Please **do not use the issue tracker to ask questions**. We will be happy to help you
|
||||
on [Nette forum](https://forum.nette.org) or chat with us on [Gitter](https://gitter.im/nette/nette).
|
||||
|
||||
A good bug report shouldn't leave others needing to chase you up for more
|
||||
information. Please try to be as detailed as possible in your report.
|
||||
|
||||
**Feature requests** are welcome. But take a moment to find out whether your idea
|
||||
fits with the scope and aims of the project. It's up to *you* to make a strong
|
||||
case to convince the project's developers of the merits of this feature.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
If you'd like to contribute, please take a moment to read [the contributing guide](https://nette.org/en/contributing).
|
||||
|
||||
The best way to propose a feature is to discuss your ideas on [Nette forum](https://forum.nette.org) before implementing them.
|
||||
|
||||
Please do not fix whitespace, format code, or make a purely cosmetic patch.
|
||||
|
||||
Thanks! :heart:
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
Licenses
|
||||
========
|
||||
|
||||
Good news! You may use Nette Framework under the terms of either
|
||||
the New BSD License or the GNU General Public License (GPL) version 2 or 3.
|
||||
|
||||
The BSD License is recommended for most projects. It is easy to understand and it
|
||||
places almost no restrictions on what you can do with the framework. If the GPL
|
||||
fits better to your project, you can use the framework under this license.
|
||||
|
||||
You don't have to notify anyone which license you are using. You can freely
|
||||
use Nette Framework in commercial projects as long as the copyright header
|
||||
remains intact.
|
||||
|
||||
Please be advised that the name "Nette Framework" is a protected trademark and its
|
||||
usage has some limitations. So please do not use word "Nette" in the name of your
|
||||
project or top-level domain, and choose a name that stands on its own merits.
|
||||
If your stuff is good, it will not take long to establish a reputation for yourselves.
|
||||
|
||||
|
||||
New BSD License
|
||||
---------------
|
||||
|
||||
Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of "Nette Framework" nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
This software is provided by the copyright holders and contributors "as is" and
|
||||
any express or implied warranties, including, but not limited to, the implied
|
||||
warranties of merchantability and fitness for a particular purpose are
|
||||
disclaimed. In no event shall the copyright owner or contributors be liable for
|
||||
any direct, indirect, incidental, special, exemplary, or consequential damages
|
||||
(including, but not limited to, procurement of substitute goods or services;
|
||||
loss of use, data, or profits; or business interruption) however caused and on
|
||||
any theory of liability, whether in contract, strict liability, or tort
|
||||
(including negligence or otherwise) arising in any way out of the use of this
|
||||
software, even if advised of the possibility of such damage.
|
||||
|
||||
|
||||
GNU General Public License
|
||||
--------------------------
|
||||
|
||||
GPL licenses are very very long, so instead of including them here we offer
|
||||
you URLs with full text:
|
||||
|
||||
- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html)
|
||||
- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html)
|
||||
Vendored
+304
@@ -0,0 +1,304 @@
|
||||
Nette Schema
|
||||
************
|
||||
|
||||
[](https://packagist.org/packages/nette/schema)
|
||||
[](https://travis-ci.org/nette/schema)
|
||||
[](https://coveralls.io/github/nette/schema?branch=master)
|
||||
[](https://github.com/nette/schema/releases)
|
||||
[](https://github.com/nette/schema/blob/master/license.md)
|
||||
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
Handy library for validating data structures against a given Schema.
|
||||
|
||||
Documentation can be found on the [website](https://doc.nette.org/schema).
|
||||
|
||||
If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you!
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
The recommended way to install is via Composer:
|
||||
|
||||
```
|
||||
composer require nette/schema
|
||||
```
|
||||
|
||||
It requires PHP version 7.1 and supports PHP up to 7.4.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
```php
|
||||
$processor = new Nette\Schema\Processor;
|
||||
|
||||
try {
|
||||
$normalized = $processor->process($schema, $data);
|
||||
} catch (Nette\Schema\ValidationException $e) {
|
||||
echo 'Data are not valid: ' . $e->getMessage();
|
||||
}
|
||||
|
||||
// in case of error it throws Nette\Schema\ValidationException
|
||||
|
||||
```
|
||||
|
||||
Defining schema
|
||||
---------------
|
||||
|
||||
```php
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
$schema = Expect::structure([
|
||||
'processRefund' => Expect::bool(),
|
||||
'refundAmount' => Expect::int(),
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'processRefund' => true,
|
||||
'refundAmount' => 17,
|
||||
];
|
||||
|
||||
$normalized = $processor->process($schema, $data); // validates!
|
||||
```
|
||||
|
||||
If you're validating data passed, you can cast strings and booleans to the expected types defined by your schema:
|
||||
|
||||
```php
|
||||
$schema = Expect::structure([
|
||||
'processRefund' => Expect::scalar()->castTo('bool'),
|
||||
'refundAmount' => Expect::scalar()->castTo('int'),
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'processRefund' => 1,
|
||||
'refundAmount' => '17',
|
||||
];
|
||||
|
||||
$normalized = $processor->process($schema, $data); // validates!
|
||||
|
||||
is_bool($normalized->processRefund); // true
|
||||
is_int($normalized->refundAmount); // true
|
||||
```
|
||||
|
||||
If your schema contains default values, you can have these automatically applied during validation (by default, all properties are optional and have default value `null`):
|
||||
|
||||
```php
|
||||
$schema = Expect::structure([
|
||||
'processRefund' => Expect::bool()->default(true), // or Expect::bool(true)
|
||||
'refundAmount' => Expect::int(),
|
||||
]);
|
||||
|
||||
$data = [
|
||||
'refundAmount' => 17,
|
||||
];
|
||||
|
||||
// validates, and sets defaults for missing properties
|
||||
$normalized = $processor->process($schema, $data);
|
||||
|
||||
// $normalized->processRefund === true;
|
||||
```
|
||||
|
||||
|
||||
Arrays of items
|
||||
---------------
|
||||
|
||||
Array where string keys are allowed:
|
||||
|
||||
```php
|
||||
$schema = Expect::arrayOf('string');
|
||||
|
||||
$processor->process($schema, ['key1' => 'a', 'key2' => 'b']); // validates
|
||||
$processor->process($schema, ['key' => 123]); // error: The option 'key' expects to be string, int 123 given.
|
||||
```
|
||||
|
||||
Indexed array with only numeric keys:
|
||||
|
||||
```php
|
||||
$schema = Expect::listOf('string');
|
||||
|
||||
$processor->process($schema, ['a', 'b']); // validates
|
||||
$processor->process($schema, ['key' => 'a']); // error, unexpected 'key'
|
||||
```
|
||||
|
||||
Enumerated values and anyOf()
|
||||
-----------------------------
|
||||
|
||||
The `anyOf()` is used to restrict a value to a fixed set of values or subschemes. It must be an array with at least one element:
|
||||
|
||||
```php
|
||||
$schema = Expect::listOf(
|
||||
Expect::anyOf('a', true, null)
|
||||
);
|
||||
|
||||
$processor->process($schema, ['a', true, null, 'a']); // validates
|
||||
$processor->process($schema, ['a', false]); // error: The option '1' expects to be 'a'|true|null, false given.
|
||||
```
|
||||
|
||||
Elements can be schema:
|
||||
|
||||
```php
|
||||
$schema = Expect::listOf(
|
||||
Expect::anyOf(Expect::string(), true, null)
|
||||
);
|
||||
|
||||
$processor->process($schema, ['foo', true, null, 'bar']); // validates
|
||||
$processor->process($schema, [123]); // error: The option '0' expects to be string|true|null, 123 given.
|
||||
```
|
||||
|
||||
Structures
|
||||
----------
|
||||
|
||||
Structures are objects with defined keys. Each of these key => pairs is conventionally referred to as a “property”.
|
||||
|
||||
By default, all properties are optional and have default value `null`. You can define mandatory properties via `required()`:
|
||||
|
||||
```php
|
||||
$schema = Expect::structure([
|
||||
'required' => Expect::string()->required(),
|
||||
'optional' => Expect::string(), // default is null
|
||||
]);
|
||||
|
||||
$processor->process($schema, ['optional' => '']); // error: option 'required' is missing
|
||||
$processor->process($schema, ['required' => 'foo']); // validates, returns (object) ['required' => 'foo', 'optional' => null]
|
||||
```
|
||||
|
||||
You can define nullable properties via `nullable()`:
|
||||
|
||||
```php
|
||||
$schema = Expect::structure([
|
||||
'optional' => Expect::string(),
|
||||
'nullable' => Expect::string()->nullable(),
|
||||
]);
|
||||
|
||||
$processor->process($schema, ['optional' => null]); // error: 'optional' expects to be string, null given.
|
||||
$processor->process($schema, ['nullable' => null]); // validates, returns (object) ['optional' => null, 'nullable' => null]
|
||||
```
|
||||
|
||||
By default, providing additional properties is forbidden:
|
||||
|
||||
```php
|
||||
$schema = Expect::structure([
|
||||
'key' => Expect::string(),
|
||||
]);
|
||||
|
||||
$processor->process($schema, ['additional' => 1]); // error: Unexpected option 'additional'
|
||||
```
|
||||
|
||||
The `otherItems()` is used to control the handling of extra stuff, that is, properties whose names are not listed in `Expect::structure()`:
|
||||
|
||||
```php
|
||||
$schema = Expect::structure([
|
||||
'key' => Expect::string(),
|
||||
])->otherItems(Expect::int());
|
||||
|
||||
$processor->process($schema, ['additional' => 1]); // validates
|
||||
```
|
||||
|
||||
Size and ranges
|
||||
---------------
|
||||
|
||||
The number of properties on an object can be restricted using the `min()` and `max()`:
|
||||
|
||||
```php
|
||||
// array, at least 10 items, maximum 20 items
|
||||
$schema = Expect::array()->min(10)->max(20);
|
||||
```
|
||||
|
||||
The length of a string can be constrained using the `min()` and `max()`:
|
||||
|
||||
```php
|
||||
// string, at least 10 characters long, maximum 20 characters
|
||||
$schema = Expect::string()->min(10)->max(20);
|
||||
```
|
||||
|
||||
Ranges of numbers are specified using a combination of `min()` and `max()`:
|
||||
|
||||
```php
|
||||
// at least 10, maximum 20 characters
|
||||
$schema = Expect::int()->min(10)->max(20);
|
||||
```
|
||||
|
||||
Data mapping to objects
|
||||
-----------------------
|
||||
|
||||
Schema can be generated from class:
|
||||
|
||||
```php
|
||||
class Config {
|
||||
/** @var string */
|
||||
public $dsn;
|
||||
|
||||
/** @var string|null */
|
||||
public $user;
|
||||
|
||||
/** @var string|null */
|
||||
public $password;
|
||||
|
||||
/** @var bool */
|
||||
public $debugger = true;
|
||||
}
|
||||
|
||||
$schema = Expect::from(new Config);
|
||||
|
||||
$data = [
|
||||
'dsn' => 'sqlite',
|
||||
'user' => 'root'
|
||||
];
|
||||
|
||||
$normalized = $processor->process($schema, $data);
|
||||
// $normalized is Config class
|
||||
// $normalized->dsn === 'sqlite'
|
||||
// $normalized->user === 'root'
|
||||
// $normalized->password === null
|
||||
// $normalized->debugger === true
|
||||
```
|
||||
|
||||
You can even use PHP 7.4 notation:
|
||||
|
||||
|
||||
```php
|
||||
class Config {
|
||||
public string $dsn;
|
||||
public ?string $user;
|
||||
public ?string $password;
|
||||
public bool $debugger = true;
|
||||
}
|
||||
|
||||
$schema = Expect::from(new Config);
|
||||
```
|
||||
|
||||
Or use anonymous class:
|
||||
|
||||
```php
|
||||
$schema = Expect::from(new class {
|
||||
public string $dsn;
|
||||
public ?string $user;
|
||||
public ?string $password;
|
||||
public bool $debugger = true;
|
||||
});
|
||||
```
|
||||
|
||||
Custom normalization
|
||||
--------------------
|
||||
|
||||
```php
|
||||
$schema = Expect::arrayOf('string')
|
||||
->before(function ($v) { return explode(' ', $v); });
|
||||
|
||||
$normalized = $processor->process($schema, 'a b c'); // validates and returns ['a', 'b', 'c']
|
||||
```
|
||||
|
||||
Custom constraints
|
||||
------------------
|
||||
|
||||
```php
|
||||
$schema = Expect::arrayOf('string')
|
||||
->assert(function ($v) { return count($v) % 2 === 0; }); // count must be even number
|
||||
|
||||
$processor->process($schema, ['a', 'b']); // validates, 2 is even number
|
||||
$processor->process($schema, ['a', 'b', 'c']); // error, 3 is not even number
|
||||
```
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Context
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var bool */
|
||||
public $skipDefaults = false;
|
||||
|
||||
/** @var string[] */
|
||||
public $path = [];
|
||||
|
||||
/** @var \stdClass[] */
|
||||
public $errors = [];
|
||||
|
||||
/** @var array[] */
|
||||
public $dynamics = [];
|
||||
|
||||
|
||||
public function addError($message, $hint = null)
|
||||
{
|
||||
$this->errors[] = (object) ['message' => $message, 'path' => $this->path, 'hint' => $hint];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema;
|
||||
|
||||
|
||||
interface DynamicParameter
|
||||
{
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema\Elements;
|
||||
|
||||
use Nette;
|
||||
use Nette\Schema\Context;
|
||||
use Nette\Schema\Helpers;
|
||||
use Nette\Schema\Schema;
|
||||
|
||||
|
||||
final class AnyOf implements Schema
|
||||
{
|
||||
use Base;
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var array */
|
||||
private $set;
|
||||
|
||||
|
||||
/**
|
||||
* @param mixed|Schema ...$set
|
||||
*/
|
||||
public function __construct(...$set)
|
||||
{
|
||||
$this->set = $set;
|
||||
}
|
||||
|
||||
|
||||
public function nullable(): self
|
||||
{
|
||||
$this->set[] = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function dynamic(): self
|
||||
{
|
||||
$this->set[] = new Type(Nette\Schema\DynamicParameter::class);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/********************* processing ****************d*g**/
|
||||
|
||||
|
||||
public function normalize($value, Context $context)
|
||||
{
|
||||
return $this->doNormalize($value, $context);
|
||||
}
|
||||
|
||||
|
||||
public function merge($value, $base)
|
||||
{
|
||||
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) {
|
||||
unset($value[Helpers::PREVENT_MERGING]);
|
||||
return $value;
|
||||
}
|
||||
return Helpers::merge($value, $base);
|
||||
}
|
||||
|
||||
|
||||
public function complete($value, Nette\Schema\Context $context)
|
||||
{
|
||||
$hints = $innerErrors = [];
|
||||
foreach ($this->set as $item) {
|
||||
if ($item instanceof Schema) {
|
||||
$dolly = new Context;
|
||||
$dolly->path = $context->path;
|
||||
$res = $item->complete($value, $dolly);
|
||||
if (!$dolly->errors) {
|
||||
return $this->doFinalize($res, $context);
|
||||
}
|
||||
foreach ($dolly->errors as $error) {
|
||||
if ($error->path !== $context->path || !$error->hint) {
|
||||
$innerErrors[] = $error;
|
||||
} else {
|
||||
$hints[] = $error->hint;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($item === $value) {
|
||||
return $this->doFinalize($value, $context);
|
||||
}
|
||||
$hints[] = static::formatValue($item);
|
||||
}
|
||||
}
|
||||
|
||||
if ($innerErrors) {
|
||||
$context->errors = array_merge($context->errors, $innerErrors);
|
||||
} else {
|
||||
$hints = implode('|', array_unique($hints));
|
||||
$context->addError("The option %path% expects to be $hints, " . static::formatValue($value) . ' given.');
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema\Elements;
|
||||
|
||||
use Nette;
|
||||
use Nette\Schema\Context;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
trait Base
|
||||
{
|
||||
/** @var bool */
|
||||
private $required = false;
|
||||
|
||||
/** @var mixed */
|
||||
private $default;
|
||||
|
||||
/** @var callable|null */
|
||||
private $before;
|
||||
|
||||
/** @var callable[] */
|
||||
private $asserts = [];
|
||||
|
||||
/** @var string|null */
|
||||
private $castTo;
|
||||
|
||||
|
||||
public function default($value): self
|
||||
{
|
||||
$this->default = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function required(): self
|
||||
{
|
||||
$this->required = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function before(callable $handler): self
|
||||
{
|
||||
$this->before = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function castTo(string $type): self
|
||||
{
|
||||
$this->castTo = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function assert(callable $handler): self
|
||||
{
|
||||
$this->asserts[] = $handler;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function completeDefault(Context $context)
|
||||
{
|
||||
if ($this->required) {
|
||||
$context->addError('The mandatory option %path% is missing.');
|
||||
return null;
|
||||
}
|
||||
return $this->default;
|
||||
}
|
||||
|
||||
|
||||
public function doNormalize($value, Context $context)
|
||||
{
|
||||
if ($this->before) {
|
||||
$value = ($this->before)($value);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
private function doValidate($value, string $expected, Context $context): bool
|
||||
{
|
||||
try {
|
||||
Nette\Utils\Validators::assert($value, $expected, 'option %path%');
|
||||
return true;
|
||||
} catch (Nette\Utils\AssertionException $e) {
|
||||
$context->addError($e->getMessage(), $expected);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function doFinalize($value, Context $context)
|
||||
{
|
||||
if ($this->castTo) {
|
||||
if (Nette\Utils\Reflection::isBuiltinType($this->castTo)) {
|
||||
settype($value, $this->castTo);
|
||||
} else {
|
||||
$value = Nette\Utils\Arrays::toObject($value, new $this->castTo);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->asserts as $i => $assert) {
|
||||
if (!$assert($value)) {
|
||||
$expected = is_string($assert) ? "$assert()" : "#$i";
|
||||
$context->addError("Failed assertion $expected for option %path% with value " . static::formatValue($value) . '.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
private static function formatValue($value): string
|
||||
{
|
||||
if (is_string($value)) {
|
||||
return "'$value'";
|
||||
} elseif (is_bool($value)) {
|
||||
return $value ? 'true' : 'false';
|
||||
} elseif (is_scalar($value)) {
|
||||
return (string) $value;
|
||||
} else {
|
||||
return strtolower(gettype($value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema\Elements;
|
||||
|
||||
use Nette;
|
||||
use Nette\Schema\Context;
|
||||
use Nette\Schema\Helpers;
|
||||
use Nette\Schema\Schema;
|
||||
|
||||
|
||||
final class Structure implements Schema
|
||||
{
|
||||
use Base;
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var Schema[] */
|
||||
private $items;
|
||||
|
||||
/** @var Schema|null for array|list */
|
||||
private $otherItems;
|
||||
|
||||
/** @var array */
|
||||
private $range = [null, null];
|
||||
|
||||
|
||||
/**
|
||||
* @param Schema[] $items
|
||||
*/
|
||||
public function __construct(array $items)
|
||||
{
|
||||
(function (Schema ...$items) {})(...array_values($items));
|
||||
$this->items = $items;
|
||||
$this->castTo = 'object';
|
||||
}
|
||||
|
||||
|
||||
public function default($value): self
|
||||
{
|
||||
throw new Nette\InvalidStateException('Structure cannot have default value.');
|
||||
}
|
||||
|
||||
|
||||
public function min(?float $min): self
|
||||
{
|
||||
$this->range[0] = $min;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function max(?float $max): self
|
||||
{
|
||||
$this->range[1] = $max;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|Schema $type
|
||||
*/
|
||||
public function otherItems($type = 'mixed'): self
|
||||
{
|
||||
$this->otherItems = $type instanceof Schema ? $type : new Type($type);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/********************* processing ****************d*g**/
|
||||
|
||||
|
||||
public function normalize($value, Context $context)
|
||||
{
|
||||
$value = $this->doNormalize($value, $context);
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $key => $val) {
|
||||
$itemSchema = $this->items[$key] ?? $this->otherItems;
|
||||
if ($itemSchema) {
|
||||
$context->path[] = $key;
|
||||
$value[$key] = $itemSchema->normalize($val, $context);
|
||||
array_pop($context->path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
public function merge($value, $base)
|
||||
{
|
||||
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) {
|
||||
unset($value[Helpers::PREVENT_MERGING]);
|
||||
$base = null;
|
||||
}
|
||||
|
||||
if (is_array($value) && is_array($base)) {
|
||||
$index = 0;
|
||||
foreach ($value as $key => $val) {
|
||||
if ($key === $index) {
|
||||
$base[] = $val;
|
||||
$index++;
|
||||
} elseif (array_key_exists($key, $base)) {
|
||||
$itemSchema = $this->items[$key] ?? $this->otherItems;
|
||||
$base[$key] = $itemSchema
|
||||
? $itemSchema->merge($val, $base[$key])
|
||||
: Helpers::merge($val, $base[$key]);
|
||||
} else {
|
||||
$base[$key] = $val;
|
||||
}
|
||||
}
|
||||
return $base;
|
||||
}
|
||||
|
||||
return Helpers::merge($value, $base);
|
||||
}
|
||||
|
||||
|
||||
public function complete($value, Nette\Schema\Context $context)
|
||||
{
|
||||
if ($value === null) {
|
||||
$value = []; // is unable to distinguish null from array in NEON
|
||||
} elseif (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
|
||||
$expected = 'array' . ($this->range === [null, null] ? '' : ':' . implode('..', $this->range));
|
||||
if (!$this->doValidate($value, $expected, $context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$errCount = count($context->errors);
|
||||
$items = $this->items;
|
||||
if ($extraKeys = array_keys(array_diff_key($value, $items))) {
|
||||
if ($this->otherItems) {
|
||||
$items += array_fill_keys($extraKeys, $this->otherItems);
|
||||
} else {
|
||||
$hint = Nette\Utils\ObjectHelpers::getSuggestion(array_map('strval', array_keys($items)), (string) $extraKeys[0]);
|
||||
$s = implode("', '", array_map(function ($key) use ($context) {
|
||||
return implode(' › ', array_merge($context->path, [$key]));
|
||||
}, $hint ? [$extraKeys[0]] : $extraKeys));
|
||||
$context->addError("Unexpected option '$s'" . ($hint ? ", did you mean '$hint'?" : '.'));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($items as $itemKey => $itemVal) {
|
||||
$context->path[] = $itemKey;
|
||||
if (array_key_exists($itemKey, $value)) {
|
||||
$value[$itemKey] = $itemVal->complete($value[$itemKey], $context);
|
||||
} else {
|
||||
$default = $itemVal->completeDefault($context); // checks required item
|
||||
if (!$context->skipDefaults) {
|
||||
$value[$itemKey] = $default;
|
||||
}
|
||||
}
|
||||
array_pop($context->path);
|
||||
}
|
||||
|
||||
if (count($context->errors) > $errCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->doFinalize($value, $context);
|
||||
}
|
||||
|
||||
|
||||
public function completeDefault(Context $context)
|
||||
{
|
||||
return $this->complete([], $context);
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema\Elements;
|
||||
|
||||
use Nette;
|
||||
use Nette\Schema\Context;
|
||||
use Nette\Schema\DynamicParameter;
|
||||
use Nette\Schema\Helpers;
|
||||
use Nette\Schema\Schema;
|
||||
|
||||
|
||||
final class Type implements Schema
|
||||
{
|
||||
use Base;
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var string */
|
||||
private $type;
|
||||
|
||||
/** @var Schema|null for arrays */
|
||||
private $items;
|
||||
|
||||
/** @var array */
|
||||
private $range = [null, null];
|
||||
|
||||
|
||||
public function __construct(string $type)
|
||||
{
|
||||
static $defaults = ['list' => [], 'array' => []];
|
||||
$this->type = $type;
|
||||
$this->default = strpos($type, '[]') ? [] : $defaults[$type] ?? null;
|
||||
}
|
||||
|
||||
|
||||
public function nullable(): self
|
||||
{
|
||||
$this->type .= '|null';
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function dynamic(): self
|
||||
{
|
||||
$this->type .= '|' . DynamicParameter::class;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function min(?float $min): self
|
||||
{
|
||||
$this->range[0] = $min;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function max(?float $max): self
|
||||
{
|
||||
$this->range[1] = $max;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|Schema $type
|
||||
*/
|
||||
public function items($type = 'mixed'): self
|
||||
{
|
||||
$this->items = $type instanceof Schema ? $type : new self($type);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/********************* processing ****************d*g**/
|
||||
|
||||
|
||||
public function normalize($value, Context $context)
|
||||
{
|
||||
$value = $this->doNormalize($value, $context);
|
||||
if (is_array($value) && $this->items) {
|
||||
foreach ($value as $key => $val) {
|
||||
$context->path[] = $key;
|
||||
$value[$key] = $this->items->normalize($val, $context);
|
||||
array_pop($context->path);
|
||||
}
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
public function merge($value, $base)
|
||||
{
|
||||
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) {
|
||||
unset($value[Helpers::PREVENT_MERGING]);
|
||||
return $value;
|
||||
}
|
||||
if (is_array($value) && is_array($base) && $this->items) {
|
||||
$index = 0;
|
||||
foreach ($value as $key => $val) {
|
||||
if ($key === $index) {
|
||||
$base[] = $val;
|
||||
$index++;
|
||||
} else {
|
||||
$base[$key] = array_key_exists($key, $base)
|
||||
? $this->items->merge($val, $base[$key])
|
||||
: $val;
|
||||
}
|
||||
}
|
||||
return $base;
|
||||
}
|
||||
|
||||
return Helpers::merge($value, $base);
|
||||
}
|
||||
|
||||
|
||||
public function complete($value, Context $context)
|
||||
{
|
||||
if ($value === null && is_array($this->default)) {
|
||||
$value = []; // is unable to distinguish null from array in NEON
|
||||
}
|
||||
|
||||
$expected = $this->type . ($this->range === [null, null] ? '' : ':' . implode('..', $this->range));
|
||||
if (!$this->doValidate($value, $expected, $context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($value instanceof DynamicParameter) {
|
||||
$context->dynamics[] = [$value, str_replace('|' . DynamicParameter::class, '', $expected)];
|
||||
}
|
||||
|
||||
if ($this->items) {
|
||||
$errCount = count($context->errors);
|
||||
foreach ($value as $key => $val) {
|
||||
$context->path[] = $key;
|
||||
$value[$key] = $this->items->complete($val, $context);
|
||||
array_pop($context->path);
|
||||
}
|
||||
if (count($context->errors) > $errCount) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$value = Helpers::merge($value, $this->default);
|
||||
return $this->doFinalize($value, $context);
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema;
|
||||
|
||||
use Nette;
|
||||
use Nette\Schema\Elements\AnyOf;
|
||||
use Nette\Schema\Elements\Structure;
|
||||
use Nette\Schema\Elements\Type;
|
||||
|
||||
|
||||
/**
|
||||
* Schema generator.
|
||||
*
|
||||
* @method static Type scalar($default = null)
|
||||
* @method static Type string($default = null)
|
||||
* @method static Type int($default = null)
|
||||
* @method static Type float($default = null)
|
||||
* @method static Type bool($default = null)
|
||||
* @method static Type null()
|
||||
* @method static Type array()
|
||||
* @method static Type list()
|
||||
* @method static Type mixed()
|
||||
* @method static Type email($default = null)
|
||||
*/
|
||||
final class Expect
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
public static function __callStatic(string $name, array $args): Type
|
||||
{
|
||||
$type = new Type($name);
|
||||
if ($args) {
|
||||
$type->default($args[0]);
|
||||
}
|
||||
return $type;
|
||||
}
|
||||
|
||||
|
||||
public static function type(string $type): Type
|
||||
{
|
||||
return new Type($type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param mixed|Schema ...$set
|
||||
*/
|
||||
public static function anyOf(...$set): AnyOf
|
||||
{
|
||||
return new AnyOf(...$set);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param Schema[] $items
|
||||
*/
|
||||
public static function structure(array $items): Structure
|
||||
{
|
||||
return new Structure($items);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param object $object
|
||||
*/
|
||||
public static function from($object, array $items = []): Structure
|
||||
{
|
||||
$ro = new \ReflectionObject($object);
|
||||
foreach ($ro->getProperties() as $prop) {
|
||||
$type = Helpers::getPropertyType($prop) ?? 'mixed';
|
||||
$item = &$items[$prop->getName()];
|
||||
if (!$item) {
|
||||
$item = new Type($type);
|
||||
if (PHP_VERSION_ID >= 70400 && !$prop->isInitialized($object)) {
|
||||
$item->required();
|
||||
} else {
|
||||
$def = $prop->getValue($object);
|
||||
if (is_object($def)) {
|
||||
$item = static::from($def);
|
||||
} elseif ($def === null && !Nette\Utils\Validators::is(null, $type)) {
|
||||
$item->required();
|
||||
} else {
|
||||
$item->default($def);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return (new Structure($items))->castTo($ro->getName());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|Schema $type
|
||||
*/
|
||||
public static function arrayOf($type): Type
|
||||
{
|
||||
return (new Type('array'))->items($type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|Schema $type
|
||||
*/
|
||||
public static function listOf($type): Type
|
||||
{
|
||||
return (new Type('list'))->items($type);
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema;
|
||||
|
||||
use Nette;
|
||||
use Nette\Utils\Reflection;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Helpers
|
||||
{
|
||||
use Nette\StaticClass;
|
||||
|
||||
public const PREVENT_MERGING = '_prevent_merging';
|
||||
|
||||
|
||||
/**
|
||||
* Merges dataset. Left has higher priority than right one.
|
||||
* @return array|string
|
||||
*/
|
||||
public static function merge($value, $base)
|
||||
{
|
||||
if (is_array($value) && isset($value[self::PREVENT_MERGING])) {
|
||||
unset($value[self::PREVENT_MERGING]);
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_array($value) && is_array($base)) {
|
||||
$index = 0;
|
||||
foreach ($value as $key => $val) {
|
||||
if ($key === $index) {
|
||||
$base[] = $val;
|
||||
$index++;
|
||||
} else {
|
||||
$base[$key] = static::merge($val, $base[$key] ?? null);
|
||||
}
|
||||
}
|
||||
return $base;
|
||||
|
||||
} elseif ($value === null && is_array($base)) {
|
||||
return $base;
|
||||
|
||||
} else {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function getPropertyType(\ReflectionProperty $prop): ?string
|
||||
{
|
||||
if ($type = Reflection::getPropertyType($prop)) {
|
||||
return ($prop->getType()->allowsNull() ? '?' : '') . $type;
|
||||
} elseif ($type = preg_replace('#\s.*#', '', (string) self::parseAnnotation($prop, 'var'))) {
|
||||
$class = Reflection::getPropertyDeclaringClass($prop);
|
||||
return preg_replace_callback('#[\w\\\\]+#', function ($m) use ($class) {
|
||||
return Reflection::expandClassName($m[0], $class);
|
||||
}, $type);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns an annotation value.
|
||||
*/
|
||||
public static function parseAnnotation(\Reflector $ref, string $name): ?string
|
||||
{
|
||||
if (!Reflection::areCommentsAvailable()) {
|
||||
throw new Nette\InvalidStateException('You have to enable phpDoc comments in opcode cache.');
|
||||
}
|
||||
$re = '#[\s*]@' . preg_quote($name, '#') . '(?=\s|$)(?:[ \t]+([^@\s]\S*))?#';
|
||||
if ($ref->getDocComment() && preg_match($re, trim($ref->getDocComment(), '/*'), $m)) {
|
||||
return $m[1] ?? '';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Schema validator.
|
||||
*/
|
||||
final class Processor
|
||||
{
|
||||
use Nette\SmartObject;
|
||||
|
||||
/** @var array */
|
||||
public $onNewContext = [];
|
||||
|
||||
/** @var bool */
|
||||
private $skipDefaults;
|
||||
|
||||
|
||||
public function skipDefaults(bool $value = true)
|
||||
{
|
||||
$this->skipDefaults = $value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes and validates data. Result is a clean completed data.
|
||||
* @return mixed
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function process(Schema $schema, $data)
|
||||
{
|
||||
$context = $this->createContext();
|
||||
$data = $schema->normalize($data, $context);
|
||||
$this->throwsErrors($context);
|
||||
$data = $schema->complete($data, $context);
|
||||
$this->throwsErrors($context);
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Normalizes and validates and merges multiple data. Result is a clean completed data.
|
||||
* @return mixed
|
||||
* @throws ValidationException
|
||||
*/
|
||||
public function processMultiple(Schema $schema, array $dataset)
|
||||
{
|
||||
$context = $this->createContext();
|
||||
$flatten = null;
|
||||
$first = true;
|
||||
foreach ($dataset as $data) {
|
||||
$data = $schema->normalize($data, $context);
|
||||
$this->throwsErrors($context);
|
||||
$flatten = $first ? $data : $schema->merge($data, $flatten);
|
||||
$first = false;
|
||||
}
|
||||
$data = $schema->complete($flatten, $context);
|
||||
$this->throwsErrors($context);
|
||||
return $data;
|
||||
}
|
||||
|
||||
|
||||
private function throwsErrors(Context $context): void
|
||||
{
|
||||
$messages = [];
|
||||
foreach ($context->errors as $error) {
|
||||
$pathStr = " '" . implode(' › ', $error->path) . "'";
|
||||
$messages[] = str_replace(' %path%', $error->path ? $pathStr : '', $error->message);
|
||||
}
|
||||
if ($messages) {
|
||||
throw new ValidationException($messages[0], $messages);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function createContext(): Context
|
||||
{
|
||||
$context = new Context;
|
||||
$context->skipDefaults = $this->skipDefaults;
|
||||
$this->onNewContext($context);
|
||||
return $context;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema;
|
||||
|
||||
|
||||
interface Schema
|
||||
{
|
||||
/**
|
||||
* Normalization.
|
||||
* @return mixed
|
||||
*/
|
||||
function normalize($value, Context $context);
|
||||
|
||||
/**
|
||||
* Merging.
|
||||
* @return mixed
|
||||
*/
|
||||
function merge($value, $base);
|
||||
|
||||
/**
|
||||
* Validation and finalization.
|
||||
* @return mixed
|
||||
*/
|
||||
function complete($value, Context $context);
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
function completeDefault(Context $context);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Nette Framework (https://nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Nette\Schema;
|
||||
|
||||
use Nette;
|
||||
|
||||
|
||||
/**
|
||||
* Validation error.
|
||||
*/
|
||||
class ValidationException extends Nette\InvalidStateException
|
||||
{
|
||||
/** @var array */
|
||||
private $messages;
|
||||
|
||||
|
||||
public function __construct(string $message, array $messages = [])
|
||||
{
|
||||
parent::__construct($message);
|
||||
$this->messages = $messages ?: [$message];
|
||||
}
|
||||
|
||||
|
||||
public function getMessages(): array
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user