Skip to content

Expect::structure() and normalization #9

@pavel-barton

Description

@pavel-barton

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions