New contact form with friendlycaptcha

This commit is contained in:
Jorit Tijsen committed 2026-02-24 14:04:04 +01:00
1 parent 6786ac7ce1
commit 3b4030113b
80 files changed
+2860 -1918

No files matched your search

+15 -11
View File
@@ -9,30 +9,26 @@ declare(strict_types=1);
namespace Nette\Schema;
use Nette;
use function count;
final class Context
{
use Nette\SmartObject;
/** @var bool */
public $skipDefaults = false;
public bool $skipDefaults = false;
/** @var string[] */
public $path = [];
public array $path = [];
/** @var bool */
public $isKey = false;
public bool $isKey = false;
/** @var Message[] */
public $errors = [];
public array $errors = [];
/** @var Message[] */
public $warnings = [];
public array $warnings = [];
/** @var array[] */
public $dynamics = [];
public array $dynamics = [];
public function addError(string $message, string $code, array $variables = []): Message
@@ -46,4 +42,12 @@ final class Context
{
return $this->warnings[] = new Message($message, $code, $this->path, $variables);
}
/** @return \Closure(): bool */
public function createChecker(): \Closure
{
$count = count($this->errors);
return fn(): bool => $count === count($this->errors);
}
}
+25 -18
View File
@@ -13,21 +13,17 @@ use Nette;
use Nette\Schema\Context;
use Nette\Schema\Helpers;
use Nette\Schema\Schema;
use function array_merge, array_unique, implode, is_array;
final class AnyOf implements Schema
{
use Base;
use Nette\SmartObject;
/** @var array */
private $set;
private array $set;
/**
* @param mixed|Schema ...$set
*/
public function __construct(...$set)
public function __construct(mixed ...$set)
{
if (!$set) {
throw new Nette\InvalidStateException('The enumeration must not be empty.');
@@ -61,16 +57,16 @@ final class AnyOf implements Schema
/********************* processing ****************d*g**/
public function normalize($value, Context $context)
public function normalize(mixed $value, Context $context): mixed
{
return $this->doNormalize($value, $context);
}
public function merge($value, $base)
public function merge(mixed $value, mixed $base): mixed
{
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) {
unset($value[Helpers::PREVENT_MERGING]);
if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PreventMerging]);
return $value;
}
@@ -78,7 +74,16 @@ final class AnyOf implements Schema
}
public function complete($value, Context $context)
public function complete(mixed $value, Context $context): mixed
{
$isOk = $context->createChecker();
$value = $this->findAlternative($value, $context);
$isOk() && $value = $this->doTransform($value, $context);
return $isOk() ? $value : null;
}
private function findAlternative(mixed $value, Context $context): mixed
{
$expecteds = $innerErrors = [];
foreach ($this->set as $item) {
@@ -88,7 +93,7 @@ final class AnyOf implements Schema
$res = $item->complete($item->normalize($value, $dolly), $dolly);
if (!$dolly->errors) {
$context->warnings = array_merge($context->warnings, $dolly->warnings);
return $this->doFinalize($res, $context);
return $res;
}
foreach ($dolly->errors as $error) {
@@ -100,7 +105,7 @@ final class AnyOf implements Schema
}
} else {
if ($item === $value) {
return $this->doFinalize($value, $context);
return $value;
}
$expecteds[] = Nette\Schema\Helpers::formatValue($item);
@@ -112,22 +117,24 @@ final class AnyOf implements Schema
} else {
$context->addError(
'The %label% %path% expects to be %expected%, %value% given.',
Nette\Schema\Message::TYPE_MISMATCH,
Nette\Schema\Message::TypeMismatch,
[
'value' => $value,
'expected' => implode('|', array_unique($expecteds)),
]
],
);
}
return null;
}
public function completeDefault(Context $context)
public function completeDefault(Context $context): mixed
{
if ($this->required) {
$context->addError(
'The mandatory item %path% is missing.',
Nette\Schema\Message::MISSING_ITEM
Nette\Schema\Message::MissingItem,
);
return null;
}
+62 -99
View File
@@ -11,6 +11,8 @@ namespace Nette\Schema\Elements;
use Nette;
use Nette\Schema\Context;
use Nette\Schema\Helpers;
use function count, is_string;
/**
@@ -18,26 +20,18 @@ use Nette\Schema\Context;
*/
trait Base
{
/** @var bool */
private $required = false;
private bool $required = false;
private mixed $default = null;
/** @var mixed */
private $default;
/** @var callable|null */
/** @var ?callable */
private $before;
/** @var array[] */
private $asserts = [];
/** @var string|null */
private $castTo;
/** @var string|null */
private $deprecated;
/** @var callable[] */
private array $transforms = [];
private ?string $deprecated = null;
public function default($value): self
public function default(mixed $value): self
{
$this->default = $value;
return $this;
@@ -60,15 +54,30 @@ trait Base
public function castTo(string $type): self
{
$this->castTo = $type;
return $this->transform(Helpers::getCastStrategy($type));
}
public function transform(callable $handler): self
{
$this->transforms[] = $handler;
return $this;
}
public function assert(callable $handler, ?string $description = null): self
{
$this->asserts[] = [$handler, $description];
return $this;
$expected = $description ?: (is_string($handler) ? "$handler()" : '#' . count($this->transforms));
return $this->transform(function ($value, Context $context) use ($handler, $description, $expected) {
if ($handler($value)) {
return $value;
}
$context->addError(
'Failed assertion ' . ($description ? "'%assertion%'" : '%assertion%') . ' for %label% %path% with value %value%.',
Nette\Schema\Message::FailedAssertion,
['value' => $value, 'assertion' => $expected],
);
});
}
@@ -80,12 +89,12 @@ trait Base
}
public function completeDefault(Context $context)
public function completeDefault(Context $context): mixed
{
if ($this->required) {
$context->addError(
'The mandatory item %path% is missing.',
Nette\Schema\Message::MISSING_ITEM
Nette\Schema\Message::MissingItem,
);
return null;
}
@@ -94,7 +103,7 @@ trait Base
}
public function doNormalize($value, Context $context)
public function doNormalize(mixed $value, Context $context): mixed
{
if ($this->before) {
$value = ($this->before)($value);
@@ -109,92 +118,46 @@ trait Base
if ($this->deprecated !== null) {
$context->addWarning(
$this->deprecated,
Nette\Schema\Message::DEPRECATED
Nette\Schema\Message::Deprecated,
);
}
}
private function doValidate($value, string $expected, Context $context): bool
private function doTransform(mixed $value, Context $context): mixed
{
if (!Nette\Utils\Validators::is($value, $expected)) {
$expected = str_replace(['|', ':'], [' or ', ' in range '], $expected);
$context->addError(
'The %label% %path% expects to be %expected%, %value% given.',
Nette\Schema\Message::TYPE_MISMATCH,
['value' => $value, 'expected' => $expected]
);
return false;
}
return true;
}
private function doValidateRange($value, array $range, Context $context, string $types = ''): bool
{
if (is_array($value) || is_string($value)) {
[$length, $label] = is_array($value)
? [count($value), 'items']
: (in_array('unicode', explode('|', $types), true)
? [Nette\Utils\Strings::length($value), 'characters']
: [strlen($value), 'bytes']);
if (!self::isInRange($length, $range)) {
$context->addError(
"The length of %label% %path% expects to be in range %expected%, %length% $label given.",
Nette\Schema\Message::LENGTH_OUT_OF_RANGE,
['value' => $value, 'length' => $length, 'expected' => implode('..', $range)]
);
return false;
}
} elseif ((is_int($value) || is_float($value)) && !self::isInRange($value, $range)) {
$context->addError(
'The %label% %path% expects to be in range %expected%, %value% given.',
Nette\Schema\Message::VALUE_OUT_OF_RANGE,
['value' => $value, 'expected' => implode('..', $range)]
);
return false;
}
return true;
}
private function isInRange($value, array $range): bool
{
return ($range[0] === null || $value >= $range[0])
&& ($range[1] === null || $value <= $range[1]);
}
private function doFinalize($value, Context $context)
{
if ($this->castTo) {
if (Nette\Utils\Reflection::isBuiltinType($this->castTo)) {
settype($value, $this->castTo);
} else {
$object = new $this->castTo;
foreach ($value as $k => $v) {
$object->$k = $v;
}
$value = $object;
$isOk = $context->createChecker();
foreach ($this->transforms as $handler) {
$value = $handler($value, $context);
if (!$isOk()) {
return null;
}
}
foreach ($this->asserts as $i => [$handler, $description]) {
if (!$handler($value)) {
$expected = $description ?: (is_string($handler) ? "$handler()" : "#$i");
$context->addError(
'Failed assertion ' . ($description ? "'%assertion%'" : '%assertion%') . ' for %label% %path% with value %value%.',
Nette\Schema\Message::FAILED_ASSERTION,
['value' => $value, 'assertion' => $expected]
);
return;
}
}
return $value;
}
/** @deprecated use Nette\Schema\Validators::validateType() */
private function doValidate(mixed $value, string $expected, Context $context): bool
{
$isOk = $context->createChecker();
Helpers::validateType($value, $expected, $context);
return $isOk();
}
/** @deprecated use Nette\Schema\Validators::validateRange() */
private static function doValidateRange(mixed $value, array $range, Context $context, string $types = ''): bool
{
$isOk = $context->createChecker();
Helpers::validateRange($value, $range, $context, $types);
return $isOk();
}
/** @deprecated use doTransform() */
private function doFinalize(mixed $value, Context $context): mixed
{
return $this->doTransform($value, $context);
}
}
+53 -50
View File
@@ -13,39 +13,37 @@ use Nette;
use Nette\Schema\Context;
use Nette\Schema\Helpers;
use Nette\Schema\Schema;
use function array_diff_key, array_fill_keys, array_key_exists, array_keys, array_map, array_merge, array_pop, array_values, is_array, is_object;
final class Structure implements Schema
{
use Base;
use Nette\SmartObject;
/** @var Schema[] */
private $items;
private array $items;
/** @var Schema|null for array|list */
private $otherItems;
/** for array|list */
private ?Schema $otherItems = null;
/** @var array{?int, ?int} */
private $range = [null, null];
/** @var bool */
private $skipDefaults = false;
private array $range = [null, null];
private bool $skipDefaults = false;
/**
* @param Schema[] $items
* @param Schema[] $shape
*/
public function __construct(array $items)
public function __construct(array $shape)
{
(function (Schema ...$items) {})(...array_values($items));
$this->items = $items;
$this->castTo = 'object';
(function (Schema ...$items) {})(...array_values($shape));
$this->items = $shape;
$this->castTo('object');
$this->required = true;
}
public function default($value): self
public function default(mixed $value): self
{
throw new Nette\InvalidStateException('Structure cannot have default value.');
}
@@ -65,10 +63,7 @@ final class Structure implements Schema
}
/**
* @param string|Schema $type
*/
public function otherItems($type = 'mixed'): self
public function otherItems(string|Schema $type = 'mixed'): self
{
$this->otherItems = $type instanceof Schema ? $type : new Type($type);
return $this;
@@ -82,13 +77,26 @@ final class Structure implements Schema
}
public function extend(array|self $shape): self
{
$shape = $shape instanceof self ? $shape->items : $shape;
return new self(array_merge($this->items, $shape));
}
public function getShape(): array
{
return $this->items;
}
/********************* processing ****************d*g**/
public function normalize($value, Context $context)
public function normalize(mixed $value, Context $context): mixed
{
if ($prevent = (is_array($value) && isset($value[Helpers::PREVENT_MERGING]))) {
unset($value[Helpers::PREVENT_MERGING]);
if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) {
unset($value[Helpers::PreventMerging]);
}
$value = $this->doNormalize($value, $context);
@@ -107,7 +115,7 @@ final class Structure implements Schema
}
if ($prevent) {
$value[Helpers::PREVENT_MERGING] = true;
$value[Helpers::PreventMerging] = true;
}
}
@@ -115,37 +123,34 @@ final class Structure implements Schema
}
public function merge($value, $base)
public function merge(mixed $value, mixed $base): mixed
{
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) {
unset($value[Helpers::PREVENT_MERGING]);
if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PreventMerging]);
$base = null;
}
if (is_array($value) && is_array($base)) {
$index = 0;
$index = $this->otherItems === null ? null : 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;
$base[$key] = array_key_exists($key, $base) && ($itemSchema = $this->items[$key] ?? $this->otherItems)
? $itemSchema->merge($val, $base[$key])
: $val;
}
}
return $base;
}
return Helpers::merge($value, $base);
return $value ?? $base;
}
public function complete($value, Context $context)
public function complete(mixed $value, Context $context): mixed
{
if ($value === null) {
$value = []; // is unable to distinguish null from array in NEON
@@ -153,13 +158,17 @@ final class Structure implements Schema
$this->doDeprecation($context);
if (!$this->doValidate($value, 'array', $context)
|| !$this->doValidateRange($value, $this->range, $context)
) {
return;
}
$isOk = $context->createChecker();
Helpers::validateType($value, 'array', $context);
$isOk() && Helpers::validateRange($value, $this->range, $context);
$isOk() && $this->validateItems($value, $context);
$isOk() && $value = $this->doTransform($value, $context);
return $isOk() ? $value : null;
}
$errCount = count($context->errors);
private function validateItems(array &$value, Context $context): void
{
$items = $this->items;
if ($extraKeys = array_keys(array_diff_key($value, $items))) {
if ($this->otherItems) {
@@ -167,11 +176,11 @@ final class Structure implements Schema
} else {
$keys = array_map('strval', array_keys($items));
foreach ($extraKeys as $key) {
$hint = Nette\Utils\ObjectHelpers::getSuggestion($keys, (string) $key);
$hint = Nette\Utils\Helpers::getSuggestion($keys, (string) $key);
$context->addError(
'Unexpected item %path%' . ($hint ? ", did you mean '%hint%'?" : '.'),
Nette\Schema\Message::UNEXPECTED_ITEM,
['hint' => $hint]
Nette\Schema\Message::UnexpectedItem,
['hint' => $hint],
)->path[] = $key;
}
}
@@ -190,16 +199,10 @@ final class Structure implements Schema
array_pop($context->path);
}
if (count($context->errors) > $errCount) {
return;
}
return $this->doFinalize($value, $context);
}
public function completeDefault(Context $context)
public function completeDefault(Context $context): mixed
{
return $this->required
? $this->complete([], $context)
+46 -67
View File
@@ -9,35 +9,25 @@ 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;
use function array_key_exists, array_pop, implode, is_array, str_replace, strpos;
final class Type implements Schema
{
use Base;
use Nette\SmartObject;
/** @var string */
private $type;
/** @var Schema|null for arrays */
private $itemsValue;
/** @var Schema|null for arrays */
private $itemsKey;
private string $type;
private ?Schema $itemsValue = null;
private ?Schema $itemsKey = null;
/** @var array{?float, ?float} */
private $range = [null, null];
/** @var string|null */
private $pattern;
/** @var bool */
private $merge = true;
private array $range = [null, null];
private ?string $pattern = null;
private bool $merge = true;
public function __construct(string $type)
@@ -84,11 +74,9 @@ final class Type implements Schema
/**
* @param string|Schema $valueType
* @param string|Schema|null $keyType
* @internal use arrayOf() or listOf()
*/
public function items($valueType = 'mixed', $keyType = null): self
public function items(string|Schema $valueType = 'mixed', string|Schema|null $keyType = null): self
{
$this->itemsValue = $valueType instanceof Schema
? $valueType
@@ -110,10 +98,10 @@ final class Type implements Schema
/********************* processing ****************d*g**/
public function normalize($value, Context $context)
public function normalize(mixed $value, Context $context): mixed
{
if ($prevent = (is_array($value) && isset($value[Helpers::PREVENT_MERGING]))) {
unset($value[Helpers::PREVENT_MERGING]);
if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) {
unset($value[Helpers::PreventMerging]);
}
$value = $this->doNormalize($value, $context);
@@ -134,17 +122,17 @@ final class Type implements Schema
}
if ($prevent && is_array($value)) {
$value[Helpers::PREVENT_MERGING] = true;
$value[Helpers::PreventMerging] = true;
}
return $value;
}
public function merge($value, $base)
public function merge(mixed $value, mixed $base): mixed
{
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) {
unset($value[Helpers::PREVENT_MERGING]);
if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PreventMerging]);
return $value;
}
@@ -168,11 +156,11 @@ final class Type implements Schema
}
public function complete($value, Context $context)
public function complete(mixed $value, Context $context): mixed
{
$merge = $this->merge;
if (is_array($value) && isset($value[Helpers::PREVENT_MERGING])) {
unset($value[Helpers::PREVENT_MERGING]);
if (is_array($value) && isset($value[Helpers::PreventMerging])) {
unset($value[Helpers::PreventMerging]);
$merge = false;
}
@@ -182,49 +170,40 @@ final class Type implements Schema
$this->doDeprecation($context);
if (!$this->doValidate($value, $this->type, $context)
|| !$this->doValidateRange($value, $this->range, $context, $this->type)
) {
return;
}
if ($value !== null && $this->pattern !== null && !preg_match("\x01^(?:$this->pattern)$\x01Du", $value)) {
$context->addError(
"The %label% %path% expects to match pattern '%pattern%', %value% given.",
Nette\Schema\Message::PATTERN_MISMATCH,
['value' => $value, 'pattern' => $this->pattern]
);
return;
$isOk = $context->createChecker();
Helpers::validateType($value, $this->type, $context);
$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() && $value = $this->doTransform($value, $context);
if (!$isOk()) {
return null;
}
if ($value instanceof DynamicParameter) {
$expected = $this->type . ($this->range === [null, null] ? '' : ':' . implode('..', $this->range));
$context->dynamics[] = [$value, str_replace(DynamicParameter::class . '|', '', $expected)];
$context->dynamics[] = [$value, str_replace(DynamicParameter::class . '|', '', $expected), $context->path];
}
return $value;
}
private function validateItems(array &$value, Context $context): void
{
if (!$this->itemsValue) {
return;
}
if ($this->itemsValue) {
$errCount = count($context->errors);
$res = [];
foreach ($value as $key => $val) {
$context->path[] = $key;
$context->isKey = true;
$key = $this->itemsKey ? $this->itemsKey->complete($key, $context) : $key;
$context->isKey = false;
$res[$key] = $this->itemsValue->complete($val, $context);
array_pop($context->path);
}
if (count($context->errors) > $errCount) {
return null;
}
$value = $res;
$res = [];
foreach ($value as $key => $val) {
$context->path[] = $key;
$context->isKey = true;
$key = $this->itemsKey ? $this->itemsKey->complete($key, $context) : $key;
$context->isKey = false;
$res[$key ?? ''] = $this->itemsValue->complete($val, $context);
array_pop($context->path);
}
if ($merge) {
$value = Helpers::merge($value, $this->default);
}
return $this->doFinalize($value, $context);
$value = $res;
}
}
+27 -27
View File
@@ -13,6 +13,7 @@ use Nette;
use Nette\Schema\Elements\AnyOf;
use Nette\Schema\Elements\Structure;
use Nette\Schema\Elements\Type;
use function is_object;
/**
@@ -24,7 +25,6 @@ use Nette\Schema\Elements\Type;
* @method static Type float($default = null)
* @method static Type bool($default = null)
* @method static Type null()
* @method static Type array($default = [])
* @method static Type list($default = [])
* @method static Type mixed($default = null)
* @method static Type email($default = null)
@@ -32,8 +32,6 @@ use Nette\Schema\Elements\Type;
*/
final class Expect
{
use Nette\SmartObject;
public static function __callStatic(string $name, array $args): Type
{
$type = new Type($name);
@@ -51,39 +49,35 @@ final class Expect
}
/**
* @param mixed|Schema ...$set
*/
public static function anyOf(...$set): AnyOf
public static function anyOf(mixed ...$set): AnyOf
{
return new AnyOf(...$set);
}
/**
* @param Schema[] $items
* @param Schema[] $shape
*/
public static function structure(array $items): Structure
public static function structure(array $shape): Structure
{
return new Structure($items);
return new Structure($shape);
}
/**
* @param object $object
*/
public static function from($object, array $items = []): Structure
public static function from(object $object, array $items = []): Structure
{
$ro = new \ReflectionObject($object);
foreach ($ro->getProperties() as $prop) {
$type = Helpers::getPropertyType($prop) ?? 'mixed';
$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 (PHP_VERSION_ID >= 70400 && !$prop->isInitialized($object)) {
$item->required();
} else {
$def = $prop->getValue($object);
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)) {
@@ -91,6 +85,8 @@ final class Expect
} else {
$item->default($def);
}
} else {
$item->required();
}
}
}
@@ -100,19 +96,23 @@ final class Expect
/**
* @param string|Schema $valueType
* @param string|Schema|null $keyType
* @param mixed[] $shape
*/
public static function arrayOf($valueType, $keyType = null): Type
public static function array(?array $shape = []): Structure|Type
{
return Nette\Utils\Arrays::first($shape ?? []) instanceof Schema
? (new Structure($shape))->castTo('array')
: (new Type('array'))->default($shape);
}
public static function arrayOf(string|Schema $valueType, string|Schema|null $keyType = null): Type
{
return (new Type('array'))->items($valueType, $keyType);
}
/**
* @param string|Schema $type
*/
public static function listOf($type): Type
public static function listOf(string|Schema $type): Type
{
return (new Type('list'))->items($type);
}
+95 -21
View File
@@ -11,6 +11,7 @@ namespace Nette\Schema;
use Nette;
use Nette\Utils\Reflection;
use function count, explode, get_debug_type, implode, in_array, is_array, is_float, is_int, is_object, is_scalar, is_string, method_exists, preg_match, preg_quote, preg_replace, preg_replace_callback, settype, str_replace, strlen, trim, var_export;
/**
@@ -20,17 +21,16 @@ final class Helpers
{
use Nette\StaticClass;
public const PREVENT_MERGING = '_prevent_merging';
public const PreventMerging = '_prevent_merging';
/**
* Merges dataset. Left has higher priority than right one.
* @return array|string
*/
public static function merge($value, $base)
public static function merge(mixed $value, mixed $base): mixed
{
if (is_array($value) && isset($value[self::PREVENT_MERGING])) {
unset($value[self::PREVENT_MERGING]);
if (is_array($value) && isset($value[self::PreventMerging])) {
unset($value[self::PreventMerging]);
return $value;
}
@@ -56,17 +56,16 @@ final class Helpers
}
public static function getPropertyType(\ReflectionProperty $prop): ?string
public static function getPropertyType(\ReflectionProperty|\ReflectionParameter $prop): ?string
{
if (!class_exists(Nette\Utils\Type::class)) {
throw new Nette\NotSupportedException('Expect::from() requires nette/utils 3.x');
} elseif ($type = Nette\Utils\Type::fromReflection($prop)) {
if ($type = Nette\Utils\Type::fromReflection($prop)) {
return (string) $type;
} elseif ($type = preg_replace('#\s.*#', '', (string) self::parseAnnotation($prop, 'var'))) {
} elseif (
($prop instanceof \ReflectionProperty)
&& ($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 preg_replace_callback('#[\w\\\]+#', fn($m) => Reflection::expandClassName($m[0], $class), $type);
}
return null;
@@ -92,19 +91,94 @@ final class Helpers
}
/**
* @param mixed $value
*/
public static function formatValue($value): string
public static function formatValue(mixed $value): string
{
if (is_object($value)) {
return 'object ' . get_class($value);
if ($value instanceof DynamicParameter) {
return 'dynamic';
} elseif (is_object($value)) {
return 'object ' . $value::class;
} elseif (is_string($value)) {
return "'" . Nette\Utils\Strings::truncate($value, 15, '...') . "'";
} elseif (is_scalar($value)) {
return var_export($value, true);
return var_export($value, return: true);
} else {
return strtolower(gettype($value));
return get_debug_type($value);
}
}
public static function validateType(mixed $value, string $expected, Context $context): void
{
if (!Nette\Utils\Validators::is($value, $expected)) {
$expected = str_replace(DynamicParameter::class . '|', '', $expected);
$expected = str_replace(['|', ':'], [' or ', ' in range '], $expected);
$context->addError(
'The %label% %path% expects to be %expected%, %value% given.',
Message::TypeMismatch,
['value' => $value, 'expected' => $expected],
);
}
}
public static function validateRange(mixed $value, array $range, Context $context, string $types = ''): void
{
if (is_array($value) || is_string($value)) {
[$length, $label] = is_array($value)
? [count($value), 'items']
: (in_array('unicode', explode('|', $types), true)
? [Nette\Utils\Strings::length($value), 'characters']
: [strlen($value), 'bytes']);
if (!self::isInRange($length, $range)) {
$context->addError(
"The length of %label% %path% expects to be in range %expected%, %length% $label given.",
Message::LengthOutOfRange,
['value' => $value, 'length' => $length, 'expected' => implode('..', $range)],
);
}
} elseif ((is_int($value) || is_float($value)) && !self::isInRange($value, $range)) {
$context->addError(
'The %label% %path% expects to be in range %expected%, %value% given.',
Message::ValueOutOfRange,
['value' => $value, 'expected' => implode('..', $range)],
);
}
}
public static function isInRange(mixed $value, array $range): bool
{
return ($range[0] === null || $value >= $range[0])
&& ($range[1] === null || $value <= $range[1]);
}
public static function validatePattern(string $value, string $pattern, Context $context): void
{
if (!preg_match("\x01^(?:$pattern)$\x01Du", $value)) {
$context->addError(
"The %label% %path% expects to match pattern '%pattern%', %value% given.",
Message::PatternMismatch,
['value' => $value, 'pattern' => $pattern],
);
}
}
public static function getCastStrategy(string $type): \Closure
{
if (Nette\Utils\Validators::isBuiltinType($type)) {
return static function ($value) use ($type) {
settype($value, $type);
return $value;
};
} elseif (method_exists($type, '__construct')) {
return static fn($value) => is_array($value) || $value instanceof \stdClass
? new $type(...(array) $value)
: new $type($value);
} else {
return static fn($value) => Nette\Utils\Arrays::toObject((array) $value, new $type);
}
}
}
+38 -18
View File
@@ -10,47 +10,67 @@ declare(strict_types=1);
namespace Nette\Schema;
use Nette;
use function implode, preg_last_error_msg, preg_replace_callback;
final class Message
{
use Nette\SmartObject;
/** variables: {value: mixed, expected: string} */
public const TypeMismatch = 'schema.typeMismatch';
/** variables: {value: mixed, expected: string} */
public const TYPE_MISMATCH = 'schema.typeMismatch';
/** variables: {value: mixed, expected: string} */
public const VALUE_OUT_OF_RANGE = 'schema.valueOutOfRange';
public const ValueOutOfRange = 'schema.valueOutOfRange';
/** variables: {value: mixed, length: int, expected: string} */
public const LENGTH_OUT_OF_RANGE = 'schema.lengthOutOfRange';
public const LengthOutOfRange = 'schema.lengthOutOfRange';
/** variables: {value: string, pattern: string} */
public const PATTERN_MISMATCH = 'schema.patternMismatch';
public const PatternMismatch = 'schema.patternMismatch';
/** variables: {value: mixed, assertion: string} */
public const FAILED_ASSERTION = 'schema.failedAssertion';
public const FailedAssertion = 'schema.failedAssertion';
/** no variables */
public const MISSING_ITEM = 'schema.missingItem';
public const MissingItem = 'schema.missingItem';
/** variables: {hint: string} */
public const UNEXPECTED_ITEM = 'schema.unexpectedItem';
public const UnexpectedItem = 'schema.unexpectedItem';
/** no variables */
public const DEPRECATED = 'schema.deprecated';
public const Deprecated = 'schema.deprecated';
/** @var string */
public $message;
/** @deprecated use Message::TypeMismatch */
public const TYPE_MISMATCH = self::TypeMismatch;
/** @var string */
public $code;
/** @deprecated use Message::ValueOutOfRange */
public const VALUE_OUT_OF_RANGE = self::ValueOutOfRange;
/** @deprecated use Message::LengthOutOfRange */
public const LENGTH_OUT_OF_RANGE = self::LengthOutOfRange;
/** @deprecated use Message::PatternMismatch */
public const PATTERN_MISMATCH = self::PatternMismatch;
/** @deprecated use Message::FailedAssertion */
public const FAILED_ASSERTION = self::FailedAssertion;
/** @deprecated use Message::MissingItem */
public const MISSING_ITEM = self::MissingItem;
/** @deprecated use Message::UnexpectedItem */
public const UNEXPECTED_ITEM = self::UnexpectedItem;
/** @deprecated use Message::Deprecated */
public const DEPRECATED = self::Deprecated;
public string $message;
public string $code;
/** @var string[] */
public $path;
public array $path;
/** @var string[] */
public $variables;
public array $variables;
public function __construct(string $message, string $code, array $path, array $variables = [])
@@ -74,6 +94,6 @@ final class Message
return preg_replace_callback('~( ?)%(\w+)%~', function ($m) use ($vars) {
[, $space, $key] = $m;
return $vars[$key] === null ? '' : $space . $vars[$key];
}, $this->message);
}, $this->message) ?? throw new Nette\InvalidStateException(preg_last_error_msg());
}
}
+8 -17
View File
@@ -17,19 +17,12 @@ use Nette;
*/
final class Processor
{
use Nette\SmartObject;
/** @var array */
public $onNewContext = [];
/** @var Context|null */
private $context;
/** @var bool */
private $skipDefaults;
public array $onNewContext = [];
private Context $context;
private bool $skipDefaults = false;
public function skipDefaults(bool $value = true)
public function skipDefaults(bool $value = true): void
{
$this->skipDefaults = $value;
}
@@ -37,10 +30,9 @@ final class Processor
/**
* Normalizes and validates data. Result is a clean completed data.
* @return mixed
* @throws ValidationException
*/
public function process(Schema $schema, $data)
public function process(Schema $schema, mixed $data): mixed
{
$this->createContext();
$data = $schema->normalize($data, $this->context);
@@ -53,10 +45,9 @@ final class Processor
/**
* Normalizes and validates and merges multiple data. Result is a clean completed data.
* @return mixed
* @throws ValidationException
*/
public function processMultiple(Schema $schema, array $dataset)
public function processMultiple(Schema $schema, array $dataset): mixed
{
$this->createContext();
$flatten = null;
@@ -96,10 +87,10 @@ final class Processor
}
private function createContext()
private function createContext(): void
{
$this->context = new Context;
$this->context->skipDefaults = $this->skipDefaults;
$this->onNewContext($this->context);
Nette\Utils\Arrays::invoke($this->onNewContext, $this->context);
}
}
+3 -3
View File
@@ -16,19 +16,19 @@ interface Schema
* Normalization.
* @return mixed
*/
function normalize($value, Context $context);
function normalize(mixed $value, Context $context);
/**
* Merging.
* @return mixed
*/
function merge($value, $base);
function merge(mixed $value, mixed $base);
/**
* Validation and finalization.
* @return mixed
*/
function complete($value, Context $context);
function complete(mixed $value, Context $context);
/**
* @return mixed
+1 -1
View File
@@ -18,7 +18,7 @@ use Nette;
class ValidationException extends Nette\InvalidStateException
{
/** @var Message[] */
private $messages;
private array $messages;
/**