no message

This commit is contained in:
BarboraFleg
2019-04-04 18:34:40 +02:00
commit ce48ca4a21
589 changed files with 82184 additions and 0 deletions
+34
View File
@@ -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"
}
}
}
+33
View File
@@ -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:
+60
View File
@@ -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)
+304
View File
@@ -0,0 +1,304 @@
Nette Schema
************
[![Downloads this Month](https://img.shields.io/packagist/dm/nette/schema.svg)](https://packagist.org/packages/nette/schema)
[![Build Status](https://travis-ci.org/nette/schema.svg?branch=master)](https://travis-ci.org/nette/schema)
[![Coverage Status](https://coveralls.io/repos/github/nette/schema/badge.svg?branch=master)](https://coveralls.io/github/nette/schema?branch=master)
[![Latest Stable Version](https://poser.pugx.org/nette/schema/v/stable)](https://github.com/nette/schema/releases)
[![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](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
View File
@@ -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];
}
}
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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));
}
}
}
+175
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+35
View File
@@ -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;
}
}