Giter Site home page Giter Site logo

nette / schema Goto Github PK

View Code? Open in Web Editor NEW
847.0 26.0 26.0 128 KB

📐 Validating data structures against a given Schema.

Home Page: https://doc.nette.org/schema

License: Other

PHP 100.00%
nette nette-framework validation schema json data-structures php

schema's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

schema's Issues

Default value on i.e. string is not working as expected.

Version: v1.2.3

Bug Description

When expecting a string with a default value set, the default value is only returned when key does not exist in data array. I would expect it would return default value "string()" when I have specified null in property "test" in data array. The property "test" is not nullable on purpose, because default value should be returned instead.

$config = new Configuration([
    'test' => Expect::string('hey')
]);

$config->merge([
    'test' => null,
]);

$value = $config->get('test');

The exception I get is

Uncaught Nette\Schema\ValidationException: The item 'test' expects to be string, null given.

Expected Behavior

I would expect the variable $value would be equal to "hey".

I think the problem is in the loop at master:src/Schema/Elements/Structure.php:175.

Feature request: Unmark Context as internal

Class \Nette\Schema\Context is marked as @internal.

/**
* @internal
*/
final class Context

It causes that I cannot make custom validator object because the ->normalize() method requires Context instance in argument declared by \Nette\Schema\Schema interface.

PhpStorm is here firing inspection warning:

Class 'Context' is marked as @internal  
Inspection info: Reports the usages of the entities that are marked as @internal.
In most cases, such usages should be removed or replaced with other constructs.

In addition Context is not fully internal, because custom validators need it for for collecting validation errors. I didn't found possibility to do it with more clean way.

Example of use:
https://github.com/redbitcz/subreg-api-php/blob/60f377ac68f3c1871b926eca336f5eb8d3368455/src/Schema/DateTime.php#L44-L64

Suggest to feature

Unmark \Nette\Schema\Context as internal.

I understand the Context class has currently too open public interface which is potentially sensitive. Maybe move collecting errors to more specifics object which will not be @internal marked.

Merging default values doesn't work on non associative array.

Hi,

I encounter a bug with the merge function when the schema use a list of string.
Here is an example :

`
$schema = Expect::structure([
'paths' => Expect::arrayOf('string')->default(['path_1']),
]);

$data = ['paths' => ['path_1']];
`
after the processSchema() i got the following result :

['path_1', 'path_1']
but i expect the values to be merged, and only have ['path_1'] as a result. This wrong behavior is when the array has numeric keys. If a use an associative array (ex : ['default' => 'path_1']) the merge work fine and i got only on element for the result.

Keep up the good work, this library is amazing :)

Tuple validation

I know a tuple isn't a PHP structure, but you can still format and validate your data like it is. For instance, I want to validate a list of tuples:

[
  [date, date, [ints], int],
  [date, date, [ints], int],
  [date, date, [ints], int],
  // etc, 20 or 100 more lines, so I don't want assoc arrays (Expect::structure())
]

The outside would be a listOf(), but a list of what? Tuples. Numeric arrays with a set amount of items that have specific types: elements 0 and 1 are dates, element 2 is a listOf(int()), element 3 is an int().

Maybe something like

$schema = Expect::listOf(Expect::tuple(
  Custom::date(),
  Custom::date(),
  Expect::listOf(Expect::int()),
  Expect::int(),
));

String expectation with min(0) and a nullable flag does not accept null

Version: 1.0.2

Bug Description

String expectation with min(0) and a nullable flag does not accept null.

Steps To Reproduce

Expectation is defined as

$schema = Expect::structure([
    'position' => Expect::string()->min(0)->nullable(),
]);

When validating with ['position' => null], the error message raised is

"The option 'position' expects to be string or null in range 0.., null given."

Expected Behavior

The null value should be accepted according to the error message.

If failing for other reasons, null should be mentioned first in the error message for it to make more sense (only strings have "ranges")

Possible Solution

Nullable does not take precedence over string length constraint even when minimal string length is 0

Custom error message mapping

The error messages created by the library are quite nice (they contain path, expected type and actual values). However in usability in real world APIs (at least at our company) we need programatically to map these error messages to either our error codes (numbers or strings) or custom error messages for example to provide more details, links or just keep consistent with our documentation language.

Assert with custom message.

When you have more asserts for one value it is difficult to find which assert failed.

Same issue if you have more complex logic in your assert. For example you asserts relations between two fields of structure. In this case you get error like this
Failed assertion #0 for option 'data › items › 0' with value object. which is not so useful.

What do you think about this feature. May I prepare PR?

Definition generated from Schema

What about some kind of Definition generation direct from Schema? It can be useful for implement some business logic depend on defined schema. Maybe something like this:

Basic type:

$schema = Expect::string()->required()->nullable();
$definition = $schema->getDefinition();
$definition->getType();
$definition->isRequired();
$definition->isNullable();

Array:

$schema = Expect::structure([
    'name' => Expect::string()->required(),
    'surname' => Expect::string()
]);
$definition = $schema->getDefinition();
foreach ($definition->getItems() as $item) {
    $item->isRequired();
    ...
}

Example usage:

// Some kind of input processing in presenter
$input = (new UberUserInput('id', $_GET))->setSchema(Expect::integer()->required());

try {
    $input->validate();
} catch (...) { ... }
// Generating form for user input
$form = new Form();
$form->addText($input->getName());
if ($input->getSchemaDefinition()->isRequired()) {
    $form[$input->getName()]->setRequired();
}

String to DateTime returns \Nette\Utils\Arrays error

Bug Description

I am not sure, if I am using this right, but I am trying to convert input string to result DateTime object

Steps To Reproduce

https://fiddle.nette.org/nette/#4f2cd78abf

class Foo {
    public \DateTime $bar;
}

$processor = new \Nette\Schema\Processor;
$processor->process(
    \Nette\Schema\Expect::structure([
        'bar' => \Nette\Schema\Expect::string()->castTo('DateTime')
    ])->castTo(Foo::class),
    [
        'bar' => '2021-01-01',
    ]
);

Will result in the error:

Nette\Utils\Arrays::toObject(): Argument #1 ($array) must be of type iterable, string given, called in ...../vendor/nette/schema/src/Schema/Elements/Base.php on line 174

Expected Behavior

Input string will be successfully mapped to target class DateTime property.

Allow options to be marked as deprecated

Problem

I would like the ability to mark certain options as being 'deprecated'. These are elements that are still currently allowed but may be removed in future versions. Using a deprecated option should cause a silenced E_USER_DEPRECATED error to triggered when used.

Proposed implementation

Add a new method called deprecated() to the Base trait - something like this:

/**
 * Marks this option as deprecated
 *
 * @param ?string $message Optional deprecation message (any '%s' in the string will be replaced with the option path)
 *
 * @return $this
 */
public function deprecated(?string $message = null): self
{
    $this->deprecated = $message ?? "Option '%s' is deprecated";
    return $this;
}

Users can then flag options as deprecated like so:

 $schema = Expect::structure([
-    'foo' => Expect::string(),
+    'foo' => Expect::string()->deprecated('"%s" was deprecated in v2.1 and will be removed in v3.0; use "bar" instead'),
+    'bar' => Expect::string(),
 ]);

At some point during the complete() method call we'd raise a silenced deprecation error if any value was provided:

// TODO: Replace $valueWasProvided with the actual logic needed
if ($valueWasProvided && $this->deprecated !== null) {
    $s = implode("', '", array_map(function ($key) use ($context) {
        return implode(' › ', array_merge($context->path, [$key]));
    }, $hint ? [$extraKeys[0]] : $extraKeys));

   @trigger_error(sprintf($this->deprecated, $s), E_USER_DEPRECATED);
}

Why a silenced error?

The @trigger_error('...', E_USER_DEPRECATED) pattern is borrowed from Symfony and other projects:

Without the @-silencing operator, users would need to opt-out from deprecation notices. Silencing swaps this behavior and allows users to opt-in when they are ready to cope with them (by adding a custom error handler like the one used by the Web Debug Toolbar or by the PHPUnit bridge).

See https://symfony.com/doc/4.4/contributing/code/conventions.html#deprecating-code for more details.

Outstanding questions

  • Should an exception be thrown if a deprecated option lacks a default or is marked as required? I don't think so, but I'm not sure.
  • Should there be an alternate way to detect deprecated options? Symfony's approach works really well but not everyone uses custom error handlers that would detect these. I don't personally need an alternate method but perhaps we could record them in the Context too if that's desired?

Am I willing to implement this?

Yes - but I could use a little guidance on the best place to put the deprecation check and how to properly determine if a value is given or omitted.

arrayOf, listOf defaults are always in array

Version: 1.0.0

$schema = Expect::listOf('string')->default([
	'foo',
	'bar',
]);

$processor = new Processor();

$array = $processor->process($schema, [
	'foo',
	'bar'
]);

var_dump($array);

dumps:

array(4) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(3) "bar"
  [2]=>
  string(3) "foo"
  [3]=>
  string(3) "bar"
}
$schema = Expect::arrayOf('string')->default([
	'foo',
	'bar',
]);

$processor = new Processor();

$array = $processor->process($schema, [
	'foo',
	'bar'
]);

var_dump($array);

dumps:

array(4) {
  [0]=>
  string(3) "foo"
  [1]=>
  string(3) "bar"
  [2]=>
  string(3) "foo"
  [3]=>
  string(3) "bar"
}

Is it the right behavior?

Allow recursive schema validation in tree structure

I'm trying to solve recursive schema validation in menu library when you have tree structure of menu (submenu items).

Here's what I did as temporary solution:

final class MenuExtension extends CompilerExtension
{

	public function getConfigSchema(): Schema
	{
		return Expect::arrayOf(Expect::structure([
			'loader' => Expect::string(DefaultMenuLoader::class),
			'items' => Expect::array()->required(),
		]));
	}

	public function getItemSchema(): Schema
	{
		return Expect::structure([
			'title' => Expect::string(),
			'link' => Expect::string(),
			'items' => Expect::array(),
		]);
	}

	public function loadConfiguration(): void
	{
		$config = $this->getConfig();
		$builder = $this->getContainerBuilder();
		$processor = new Processor;

		foreach ($config as $menuName => $menu) {
			$container->addSetup('addMenu', [
				$this->loadMenuConfiguration($builder, $processor, $menuName, $menu),
			]);
		}
	}

	private function loadMenuConfiguration(
		ContainerBuilder $builder,
		Processor $processor,
		string $menuName,
		stdClass $config
	): ServiceDefinition {
		$loader = $config->loader;

		if (!Strings::startsWith($config->loader, '@')) {
			$loader = $builder->addDefinition($this->prefix('menu.'. $menuName. '.loader'))
				->setType($config->loader)
				->setAutowired(false);
		}

		if ($loader->getType() === DefaultMenuLoader::class) {
			$loader->setArguments([$this->normalizeMenuItems($processor, $config->items)]);
		}
	}

	private function normalizeMenuItems(Processor $processor, array $items): array
	{
		array_walk($items, function(array &$item, string $key) use ($processor): void {
			$item = $processor->process($this->getItemSchema(), $item);

			if ($item->title === null) {
				$item->title = $key;
			}

			$item->items = $this->normalizeMenuItems($processor, $item->items);
		});

		return $items;
	}

}

Full file is here

  • It would be nice if I could use something like Expect::schemaFactory([$this, 'getItemSchema']) so function getItemSchema() could be called recursively as long as submenu's are present.
  • It would be nice if I could get Processor in CompilerExtension so I don't need to create one when I have custom config.

Programmatically access path of field(s) with error(s)

I believe it is not so uncommon to use such library when validating API parameters which is inherently connected to reporting errors back to the caller.

In such situation it is not uncommon to provide path* within the request payload and nette/schema does that in way that is not directly usable to be passed back to the response as it is available only (as far as I investigated) to the exception message.

So it would be nice to be able to access the path programmatically.

  • Error response in our company is a array of errors with each having path, error message and error code.

Validator from class

The Expect::from is nice, but it does not really work for immutable objects like the following:

<?php

use Money\Money;

final class InvoiceItem {
	public function __construct(
		public readonly string $name,
		public readonly Money $price,
		public readonly int $amount,
	) {
	}
}
Version without readonly
<?php

use Money\Money;

final class InvoiceItem {
	public function __construct(
		private string $name,
		private Money $price,
		private int $amount,
	) {
	}

	public function getName(): string {
		return $this->name;
	}

	public function getPrice(): Money {
		return $this->price;
	}

	public function getAmount(): int {
		return $this->amount;
	}
}

It would be nice to have Expect::fromClass(InvoiceItem::class), that would check the constructor arguments instead of the public properties like Expect::from does.

I can try to implement this, if you think this is a good idea.

Warnings produced by PHP 8.3.2

Version: 1.2.5

Bug Description

PHP 8.3.2
produces below warnings
Warning: Private methods cannot be final as they are never overridden by other classes in /var/www/vendor/nette/schema/src/Schema/Helpers.php on line 19

Warning: Private methods cannot be final as they are never overridden by other classes in /var/www/vendor/nette/utils/src/Utils/Reflection.php on line 18

Warning: Private methods cannot be final as they are never overridden by other classes in /var/www/vendor/nette/utils/src/Utils/Validators.php on line 18

this is casued by @internal annotation in Schema/Helpers.php

Steps To Reproduce

Use the package with PHP 8.3.2

Expected Behavior

No warnings

Possible Solution

remove @internal annotation

Support custom cast function

It would like to be able to cast a value using a callable.

class Foo {
  public static function fromString($bar) {
    // some logic
  }
}

$schema = Expect::structure([
  'foo' => Expect::string()->castTo(fn ($v) => Foo::fromString($v));
  'foo2' => Expect::string()->castTo('Foo::fromString');
});

I'm willing to make a PR for this if you accept the feature to be integrated in the project.

Range with nullable

Version: 1.0.2

'limit' => Expect::int(null)->min(1)->nullable(),

When limit is null, exception throwed "The option 'limit' expects to be int or null in range 1.., null given."

Dump of variable $expected:

int|null:1..

expected:

int:1..|null

then everything is fine

Optional structure with required fields

Let's say I validate the data structure of a blog post. This blog post has a body and an author. And as we support anonymous comments, the user is NOT mandatory. The author is a structure as well and if it IS PROVIDED it MUST have an e-mail.

I have a schema like this:

- comment: structure
  - body: string (required)
  - author: structure (not required)
    - email: string (required)
    - name: string (not required)

At the moment I am unable to validate the data with Nette/Schema like that because if I mark the email as required I still get a validation exception even if the author (not required) is not provided at all.

My validation schema code is like:

Expect::structure([
    'body' => Expect::string()->required(),
    'author' => Expect::structure([
        'email' => Expect::string()->required(),
        'name' => Expect::string(),
    ]),
]);

And for data like this:

- body: Hello World!

I get The mandatory option 'author > email' is missing.. I get the same result even if I explicitly set the structure as not required like this: 'author' => Expect::structure([ ... ])->setRequired(false).

This example is obviously all made up to make things clear but I've hit this issue three times in different places. One of them being writing MongoDB extension for DI and trying to validate the driverOptions (see the context, structure inside structure, but context not mandatory)

Is this a bug? Is it a feature? Or am I missing something?

We are obviously able to work around this by explicitly checking the values in all sorts of places but it is a shame we have to pollute our codebase, especially when we streamlined it pretty much by implementing Nette\Schema in the first place :-)

Type error with Helpers::getCastStrategy and DTO with constructor sice v1.2.5

Version: v1.2.5

Bug Description

Since the v1.2.5 my code throws type error, because Helpers::getCastStrategy expects array or single value, but gets stdClass when using data object with constructor.

testArrayOfUsers
   TypeError: RBCB\Schema\User::__construct(): Argument #1 ($xxxx) must be of type ?string, stdClass given, called in 
   vendor/nette/schema/src/Schema/Helpers.php on line 186

Steps To Reproduce

$json = Json::decode($data);
foreach ($json->data as $user) {
       $schema = Expect::from(new User());
       $user = $processor->process($schema, $user);
       yield $user;
}

And the DTO

class User
{
    public function __construct(
        public ?string $yyyy = null,
        public ?string $xxxx = null,
        public ?string $zzz= null,
     .... 
}

Expected Behavior

Im unsure if I need to cast it manually, so let me know if I use the schema correctly or not.

Possible Solution

Editing the method_exists($type, '__construct') for supporting stdClass.

Possibility to use Schema to create an array/object of Schema's default values.

Intention

My intention is to use the Schema both to normalize the data and to obtain default values. In the second case, there is no need to input any data, just obtain an array of default values, if there are any.

My use case

I have several Personas, each of which can have very different parameters (saved as JSON in database).

The structure of these parameters is defined by the Persona's Schema.

I use the Persona's Schema to normalize these parameters entered by user's input.

To create a new Persona, however, I need to generate default values for its parameters from its Schema, so that I do not have to define them separately, because – why not use the existing Schema for this?

Example of my Schema for one of the Persona's:

Expect::structure([
	'nickname' => Expect::string()->default('Maniac'),
	'skills' => Expect::anyOf('low', 'medium', 'high')->default('low')->required(true)
]);

I need to use this Schema for normalization, which works great, but also to get the default values that are defined there. I need to get this result from this Schema:

[
   'nickname' => 'Maniac',
   'skills' => 'low'
]

When creating a new Persona, I encode this into JSON and save into database as default parameters of the Persona.

My idea is to have something like:

// normalized data
$normalized = $processor->process($schema, $data);

// default values
$defaults = $processor->getDefaults($schema);
// or
$defaults = $processor->process($schema);  // that is, no input data are specified, so it returns default values instead of normalized data

min/max/assert not applyed on default value

Hi,

It seems the controles like min/max or assert() are not appplyed on the default value.
This could be problematic if the default value is dynamic.

In my use case i should controle the application key is a hexa 64 chars, and by default we use the environement variable APP_KEY.

I also tryed to move the min/max/assert function after the chained ->default(), but the result is the same :(

return Expect::structure([ 'key' => Expect::xdigit()->min(64)->max(64)->default(env('APP_KEY')), ]);

Is that a bug or an "expected" behaviour ?

Keep up the good work.

Adding new PHP Versions as experimental

Hi,
i tried to start an early php 8.4 adoption but since i do not know where the tail will finally end, i stopped it.
The problem is that several nette packages are bound to a max php version.

To make changes independently i would suggest to add "experimental" support to the nette packages, this would it make easier to fix upcoming issues with php 7.4 or further php versions.

If something like in: https://github.com/ItsReddi/schema/pull/1/files (tests.yaml) would be available in all nette packages it is maybe easier for the community to fix upcoming issues.

False-positive security bug because of non-standard LICENSE.md file

Version: 1.2.1

Bug Description

Since you're not using a standard LICENSE.md format it makes things like Enlightn fail and say my project is using packages that I'm not legally allowed to use. Fix your license.

Steps To Reproduce

  1. laravel new app --jet (With --jet since it's Laravel Jetstream requiring this package in my case)
  2. composer require enlightn/enlightn
  3. php artisan englightn Enlightn will fail saying your package is illegal to use.

Expected Behavior

Use of a standard LICENSE.md format that doesn't make security checks fail.

Possible Solution

Use a standard LICENSE.md format...

Invalid documentation link

Version: master

Bug Description

In readme.md is this sentence Documentation can be found on the website.. "website" links to https://doc.nette.org/schema. But this page returns 404.

Schema from PHPSTAN array shape

We have an API that maps the URL to PHP method and the POST JSON data to method arguments, for example curl -XPOST https://mysite/api/device/add -d'{"id": 1, "data": [1,2,3]}' would call

class Device 
{
    /** @param array<int> $data */
    function add(int $id, array $data)
}

Thanks to PHP strict typing, it checks the type of basic types (int, string, booo) but it does not check content of arrays. We are already using PHPStan to pass static analysis tests and it would be cool to enforce the already existing rules via Nette\Schema.

Using reflection, I can get the phpdoc but there is no way (I think) that I could validate it using Nette/Schema. To accomplish that, a new method fromPhpstanArrayShape(string $shape) (with a much better name) would have to be created.

// this is just a sample with simple array shape but we sometimes use more coplicated ones as well
$schema = Expect::fromPhpstanArrayShape('array<int>'); // = Expect::arrayOf('int')

$data = [1, 'one', 2];

// this would throw an exception since the second element is not integer
$normalized = $processor->process($schema, $data); 

Do you think this is something that could be useful?

Add DateTime and DateTimeRange expectations

It would be handy to provide expectations for datetime too, typically validation & parsing in given format. Apart from format typically we use min and max validations as well as cross validations for intervals between two or more provided values.

Possibility to validate callback signature

Hi,

time to time I would like to validate callback signature and/or its return value. In some scenarios you setup hook and such hook invocation may throw TypeError lately because of incompatible signature. Another case is that you have to check type of returned value from callback in runtime. Both of such cases could be validated in setup time for example by:

Expect::signature(fn(int $level): string => '');

Such "signature validation" can by done by PHP typesystem itself by interface:

interface LogLevelTranslator
{
    function __invoke(int $level): string;
}

and it is probably cleaner way but too verbose.

Would you consider to merge such functionality? Or maybe into Nette\Utils\Validators?

Required with parameter to set to true or false.

It would be useful if required() method had fingerprint like this:

public function required(bool $required = true): self

Imagine you want to generate the schema programmatically either to require all options or to allow for only partial result. Imagine some create operation versus update operation where either I have to provide all fields or only ones I want to edit.

public static function getSchema(bool $partialAllowed) {
        return Expect::structure(
            [
                'name' => Expect::string()->required(!$partialAllowed),
            ]
        );
}

I am implementing restfull api, particularly post, put and patch methods and this would hugely declutter my code.

I will send PR shortly.

`Expect::anyOf` should not disable `before()`

Version: v1.0.2

Bug Description

before() helper is not called on things nested in Expect::anyOf.

Steps To Reproduce

test(function() { // normalization through anyOf
	$schema = Expect::anyOf(Expect::string()->before(function($v) {
		return (string) $v;
	}));
	Assert::same('1',  $schema->normalize(1, new Context));
});

Expected Behavior

before() should work consistently inside anyOf.

Access the $items property in the Structure class

Hi,

Nice piece of code. Do you think there is a way to access the $items property in the Structure::class ?
I use this value in 2 cases :

  1. Force the castTo('array') for all the structure and sub-structures.
  2. I need to enforce the key value used to be only in the range ['a-z0-9_'].

For the moment i do a reflection and i access the private property, but a public function getItems() could be usefull. Or perhaps an "iterator" inside the structure class.

What do you think about this feature, this could add more flexibility using this great library.

Keep up the good work.

Expect::structure() and normalization

Version: 1.0.0

Bug Description

We encountered a strange behavior when using normalization with Expect::structure(). It seemed that when specifying a before() method on a property inside the structure and passing an object to the Nette\Schema\Processor::process() method, it would not get normalized.

I managed to track it down to the Nette\Schema\Elements\Structure::normalize() method, specifically the is_array method in the condition, which was indeed the case:

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;
}

Steps To Reproduce

This code snippet reproduces the problem:

use Nette\Schema\Expect;
use Nette\Schema\Processor;
use Nette\Utils\ArrayHash;

// The example from https://doc.nette.org/en/3.0/schema#toc-custom-normalization, just wrapped it in a structure
$schema = Expect::structure([
    'data' => Expect::arrayOf('string')->before(function ($v) { return explode(' ', $v); })
]);

$values = ['data' => 'a b c'];
$processor = new Processor();

// Simple array
// stdClass { data => [ 'a', 'b', 'c' ] }
$arrayResult = $processor->process($schema, $values);

// Non-iterable class
// Nette\Schema\ValidationException: The option 'data' expects to be array, string 'a b c' given
$objectResult = $processor->process($schema, (object) $values);

// An ArrayHash, or any class that implements \Traversable
// Nette\Schema\ValidationException: The option 'data' expects to be array, string 'a b c' given
$traversableResult = $processor->process($schema, ArrayHash::from($values));

Expected Behavior

If the structure is an object, it's properties get properly normalized.

Possible Solution

I tried my best with rewriting the Nette\Schema\Elements\Structure::normalize() method to support objects, and I came up with this:

public function normalize($value, Context $context)
{
    $value = $this->doNormalize($value, $context);
    if (is_array($value) || is_object($value)) {
        // When non-iterable object is received, iterate through its public properties
        $properties = is_iterable($value) ? $value : get_object_vars($value);

        foreach ($properties as $key => $val) {
            $itemSchema = $this->items[$key] ?? $this->otherItems;
            if ($itemSchema) {
                $context->path[] = $key;

                if (is_object($value)) {
                    $value->{$key} = $itemSchema->normalize($val, $context);
                } else {
                    $value[$key] = $itemSchema->normalize($val, $context);
                }

                array_pop($context->path);
            }
        }
    }
    return $value;
}

With this modification, all of the examples I showed in Steps to Reproduce section work as expected:

use Nette\Schema\Expect;
use Nette\Schema\Processor;
use Nette\Utils\ArrayHash;

$schema = Expect::structure([
    'data' => Expect::arrayOf('string')->before(function ($v) { return explode(' ', $v); })
]);

$values = ['data' => 'a b c'];
$processor = new Processor();

// Simple array
$arrayResult = $processor->process($schema, $values);

// Non-iterable class
$objectResult = $processor->process($schema, (object) $values);

// An ArrayHash, or any class that implements \Traversable
$traversableResult = $processor->process($schema, ArrayHash::from($values));


dump($arrayResult);          // stdClass { data => [ 'a', 'b', 'c' ] }
dump($objectResult);         // stdClass { data => [ 'a', 'b', 'c' ] }
dump($traversableResult);    // stdClass { data => [ 'a', 'b', 'c' ] }

Edit: I created a PR in case the solution would be acceptable.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.