Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
"dev-master": "2.0-dev"
}
}
}
28 changes: 6 additions & 22 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Introduction

A practical library for validation and normalization of data structures against a given schema with a smart & easy-to-understand API.

Documentation can be found on the [website](https://doc.nette.org/schema).
Documentation can be found on the [website](https://doc.nette.org/en/schema).

Installation:

Expand Down Expand Up @@ -127,7 +127,7 @@ Expect::null()
Expect::array($default = [])
```

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')`.
And then all types [supported by the Validators](https://doc.nette.org/validators#toc-expected-types) via `Expect::type('scalar')` or abbreviated `Expect::scalar()`. Also class or interface names are accepted, e.g. `Expect::type('AddressEntity')`.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Validation rules heading doesn't exist on the docs site, so I figured I'd link to the closest thing on the page.


You can also use union notation:

Expand Down Expand Up @@ -177,7 +177,7 @@ The parameter can also be a schema, so we can write:
Expect::arrayOf(Expect::bool())
```

The default value is an empty array. If you specify a default value, it will be merged with the passed data. This can be disabled using `mergeDefaults(false)`.
The default value is an empty array. If you specify a default value and call `mergeDefaults()`, it will be merged with the passed data.


Enumeration: anyOf()
Expand Down Expand Up @@ -486,12 +486,9 @@ You can generate structure schema from the class. Example:
```php
class Config
{
/** @var string */
public $name;
/** @var string|null */
public $password;
/** @var bool */
public $admin = false;
public string $name;
public ?string $password;
public bool $admin = false;
}

$schema = Expect::from(new Config);
Expand All @@ -505,19 +502,6 @@ $normalized = $processor->process($schema, $data);
// $normalized = {'name' => 'jeff', 'password' => null, 'admin' => false}
```

If you are using PHP 7.4 or higher, you can use native types:

```php
class Config
{
public string $name;
public ?string $password;
public bool $admin = false;
}

$schema = Expect::from(new Config);
```

Anonymous classes are also supported:

```php
Expand Down
5 changes: 0 additions & 5 deletions src/Schema/Elements/AnyOf.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,6 @@ public function normalize(mixed $value, Context $context): mixed

public function merge(mixed $value, mixed $base): mixed
{
if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PreventMerging]);
return $value;
}

return Helpers::merge($value, $base);
}

Expand Down
8 changes: 0 additions & 8 deletions src/Schema/Elements/Structure.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,6 @@ public function getShape(): array

public function normalize(mixed $value, Context $context): mixed
{
if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) {
unset($value[Helpers::PreventMerging]);
}

$value = $this->doNormalize($value, $context);
if (is_object($value)) {
$value = (array) $value;
Expand All @@ -112,10 +108,6 @@ public function normalize(mixed $value, Context $context): mixed
array_pop($context->path);
}
}

if ($prevent) {
$value[Helpers::PreventMerging] = true;
}
}

return $value;
Expand Down
42 changes: 20 additions & 22 deletions src/Schema/Elements/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Nette\Schema\Context;
use Nette\Schema\DynamicParameter;
use Nette\Schema\Helpers;
use Nette\Schema\MergeMode;
use Nette\Schema\Schema;


Expand All @@ -26,7 +27,8 @@ final class Type implements Schema
/** @var array{?float, ?float} */
private array $range = [null, null];
private ?string $pattern = null;
private bool $merge = true;
private bool $merge = false;
private MergeMode $mergeMode = MergeMode::AppendKeys;


public function __construct(string $type)
Expand All @@ -44,13 +46,24 @@ public function nullable(): self
}


/** @deprecated mergeDefaults is disabled by default */
public function mergeDefaults(bool $state = true): self
{
if ($state === true) {
trigger_error(__METHOD__ . '() is deprecated and will be removed in the next major version.', E_USER_DEPRECATED);
}
$this->merge = $state;
return $this;
}


public function mergeMode(MergeMode $mode): self
{
$this->mergeMode = $mode;
return $this;
}


public function dynamic(): self
{
$this->type = DynamicParameter::class . '|' . $this->type;
Expand Down Expand Up @@ -99,10 +112,6 @@ public function pattern(?string $pattern): self

public function normalize(mixed $value, Context $context): mixed
{
if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) {
unset($value[Helpers::PreventMerging]);
}

$value = $this->doNormalize($value, $context);
if (is_array($value) && $this->itemsValue) {
$res = [];
Expand All @@ -120,29 +129,24 @@ public function normalize(mixed $value, Context $context): mixed
$value = $res;
}

if ($prevent && is_array($value)) {
$value[Helpers::PreventMerging] = true;
}

return $value;
}


public function merge(mixed $value, mixed $base): mixed
{
if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PreventMerging]);
if ($this->mergeMode === MergeMode::Replace) {
return $value;
}

if (is_array($value) && is_array($base) && $this->itemsValue) {
$index = 0;
if (is_array($value) && is_array($base)) {
$index = $this->mergeMode === MergeMode::OverwriteKeys ? null : 0;
foreach ($value as $key => $val) {
if ($key === $index) {
$base[] = $val;
$index++;
} else {
$base[$key] = array_key_exists($key, $base)
$base[$key] = array_key_exists($key, $base) && $this->itemsValue
? $this->itemsValue->merge($val, $base[$key])
: $val;
}
Expand All @@ -151,18 +155,12 @@ public function merge(mixed $value, mixed $base): mixed
return $base;
}

return Helpers::merge($value, $base);
return $value === null && is_array($base) ? $base : $value;
}


public function complete(mixed $value, Context $context): mixed
{
$merge = $this->merge;
if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PreventMerging]);
$merge = false;
}

if ($value === null && is_array($this->default)) {
$value = []; // is unable to distinguish null from array in NEON
}
Expand All @@ -174,7 +172,7 @@ public function complete(mixed $value, Context $context): mixed
$isOk() && Helpers::validateRange($value, $this->range, $context, $this->type);
$isOk() && $value !== null && $this->pattern !== null && Helpers::validatePattern($value, $this->pattern, $context);
$isOk() && is_array($value) && $this->validateItems($value, $context);
$isOk() && $merge && $value = Helpers::merge($value, $this->default);
$isOk() && $this->merge && $value = Helpers::merge($value, $this->default);
$isOk() && $value = $this->doTransform($value, $context);
if (!$isOk()) {
return null;
Expand Down
44 changes: 28 additions & 16 deletions src/Schema/Expect.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,30 +63,42 @@ public static function structure(array $shape): Structure
}


public static function from(object $object, array $items = []): Structure
public static function from(object|string $object, array $items = []): Structure
{
$ro = new \ReflectionObject($object);
$ro = new \ReflectionClass($object);
$props = $ro->hasMethod('__construct')
? $ro->getMethod('__construct')->getParameters()
: $ro->getProperties();

foreach ($props as $prop) {
$item = &$items[$prop->getName()];
if (!$item) {
$type = Helpers::getPropertyType($prop) ?? 'mixed';
$item = new Type($type);
if ($prop instanceof \ReflectionProperty ? $prop->isInitialized($object) : $prop->isOptional()) {
$def = ($prop instanceof \ReflectionProperty ? $prop->getValue($object) : $prop->getDefaultValue());
if (is_object($def)) {
$item = static::from($def);
} elseif ($def === null && !Nette\Utils\Validators::is(null, $type)) {
$item->required();
} else {
$item->default($def);
}
\assert($prop instanceof \ReflectionProperty || $prop instanceof \ReflectionParameter);
if ($item = &$items[$prop->getName()]) {
continue;
}

$item = new Type($propType = (string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed'));
if (class_exists($propType)) {
$item = static::from($propType);
}

$hasDefault = match (true) {
$prop instanceof \ReflectionParameter => $prop->isOptional(),
is_object($object) => $prop->isInitialized($object),
default => $prop->hasDefaultValue(),
};
if ($hasDefault) {
$default = match (true) {
$prop instanceof \ReflectionParameter => $prop->getDefaultValue(),
is_object($object) => $prop->getValue($object),
default => $prop->getDefaultValue(),
};
if (is_object($default)) {
$item = static::from($default);
} else {
$item->required();
$item->default($default);
}
} else {
$item->required();
}
}

Expand Down
36 changes: 0 additions & 36 deletions src/Schema/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
namespace Nette\Schema;

use Nette;
use Nette\Utils\Reflection;


/**
Expand Down Expand Up @@ -55,41 +54,6 @@ public static function merge(mixed $value, mixed $base): mixed
}


public static function getPropertyType(\ReflectionProperty|\ReflectionParameter $prop): ?string
{
if ($type = Nette\Utils\Type::fromReflection($prop)) {
return (string) $type;
} elseif (
($prop instanceof \ReflectionProperty)
&& ($type = preg_replace('#\s.*#', '', (string) self::parseAnnotation($prop, 'var')))
) {
$class = Reflection::getPropertyDeclaringClass($prop);
return preg_replace_callback('#[\w\\\\]+#', fn($m) => Reflection::expandClassName($m[0], $class), $type);
}

return null;
}


/**
* Returns an annotation value.
* @param \ReflectionProperty $ref
*/
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;
}


public static function formatValue(mixed $value): string
{
if ($value instanceof DynamicParameter) {
Expand Down
23 changes: 23 additions & 0 deletions src/Schema/MergeMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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;


enum MergeMode: int
{
/** Replaces all items with the last one. */
case Replace = 0;

/** Overwrites existing keys. */
case OverwriteKeys = 1;

/** Overwrites existing keys and appends new indexed elements. */
case AppendKeys = 2;
}
14 changes: 4 additions & 10 deletions src/Schema/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,18 @@ interface Schema
{
/**
* Normalization.
* @return mixed
*/
function normalize(mixed $value, Context $context);
function normalize(mixed $value, Context $context): mixed;

/**
* Merging.
* @return mixed
*/
function merge(mixed $value, mixed $base);
function merge(mixed $value, mixed $base): mixed;

/**
* Validation and finalization.
* @return mixed
*/
function complete(mixed $value, Context $context);
function complete(mixed $value, Context $context): mixed;

/**
* @return mixed
*/
function completeDefault(Context $context);
function completeDefault(Context $context): mixed;
}
Loading