Respect / Validation

The most awesome validation engine ever created for PHP
https://respect-validation.readthedocs.io
MIT License
5.76k stars 774 forks source link

Is there a way to get the full hierarchy of error for multidimensional arrays? #1358

Closed dimuska139 closed 2 years ago

dimuska139 commented 3 years ago

Hi,

I need to validate a data with the multidimensional structure:

Array
(
    [original_name] => Exiles
    [translations] => Array
        (
            [0] => Array
                (
                    [language] => ru
                    [title] => Exiles
                    [overview] => From the creators of the award winning...
                    [platforms] => Array
                        (
                            [0] => Array
                                (
                                    [id] => 8
                                    [minimum] => iPad 2
                                )
                            [1] => Array
                                (
                                    [id] => 7
                                    [minimum] => Android 4.2
                                )
                        )
                )
        )
)

My validation code is:

try {
            v::key('original_name', v::stringType())
                ->key('translations', v::arrayType()->each(
                    v::key('language', v::stringType())
                    ->key('platforms', v::arrayType()->each(
                        v::key('id', v::intType())
                            ->key('minimum', v::stringType())
                            ->key('recommended', v::optional(
                                v::stringType()
                            ))
                            ->key('release_date', v::allOf(
                                v::stringType(),
                                v::date('Y-m-d')
                            ))
                    ))
                )
            )->assert($bodyParams);
        } catch (ValidationException $exception) {
            print_r($exception->getMessages());
            die();
        }

The result of execution (if key minimum in any platforms doesn't exist) is:

Array
(
[translations] => minimum must be present
)

Is there a way to get the full hierarchy of error (including nesting)? Something like this:

Array
(
[translations] => [
 0 => [
     [platforms] => [
          1 => minimum must be present
     ]
]
)
alganet commented 2 years ago

This should work, as it is expressed on this test:

https://github.com/Respect/Validation/blob/master/tests/integration/get_messages.phpt

The ::create() calls here should not interfere, but it might be a bug. Can you check for me if putting them fixes the problem?

dimuska139 commented 2 years ago

@alganet If I understood you correctly, the test shows an example with associative arrays. But in translations and platforms i have a simple arrays with numerical keys (non-associatve arrays). I checked your example and wrote new code to validate my data. It shows:

Array
(
    [translations] => Array
        (
            [language] => language must be present
            [platforms] => platforms must be present
        )

)

But i expect to see something like this:

Array
(
    [translations] => Array
        (
            [0] => Array
                (
                    [platforms] => Array
                        (
                            [1] => Array
                                (
                                    [minimum] => minimum must be present
                                )
                        )
                )
        )
)

My code sample:


<?php

declare(strict_types=1);

require 'vendor/autoload.php';

use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Validator as v;

$bodyParams = [
    'original_name' => 'Exiles',
    'translations' => [
        [
            'language' => 'en',
            'title' => 'Exiles',
            'overview' => 'From the creators of the award winning...',
            'platforms' => [
                [
                    'id' => 8,
                    'minimum' => 'iPad 2',
                ],
                [
                    'id' => 7,
                    // There is no minimum
                ]
            ]
        ]
    ]
];

try {
    v::create()
        ->key('original_name', v::stringType())
        ->key('translations',
            v::create()
                ->key('language', v::stringType())
                ->key('platforms', v::create()
                    ->key('id', v::intType())
                    ->key('minimum', v::stringType())
                    ->key('recommended', v::optional(
                        v::stringType()
                    ))
                    ->key('release_date', v::allOf(
                        v::stringType(),
                        v::date('Y-m-d')
                    )),
                    true)
            , true)->assert($bodyParams);
} catch (NestedValidationException $exception) {
    print_r($exception->getMessages());
}
alganet commented 2 years ago

You could walk the exception tree by yourself, implementing an Iterator.

Internally, Respect does so to resolve the tree into a flat list: https://github.com/Respect/Validation/blob/master/library/Exceptions/RecursiveExceptionIterator.php. This can be seen in action using $exception->getFullMessage().

You could either re-implement the RecursiveExceptionIterator to allow multidimensional keys or perhaps use the flat result:

<?php

require 'vendor/autoload.php';

use Respect\Validation\Exceptions\NestedValidationException;
use Respect\Validation\Validator as v;

$bodyParams = [
    'original_name' => 'Exiles',
    'translations' => [
        [
            'language' => 'en',
            'title' => 'Exiles',
            'overview' => 'From the creators of the award winning...',
            'platforms' => [
                [
                    'id' => 8,
                    'minimum' => 'iPad 2',
                ],
                [
                    'id' => 7,
                    // There is no minimum
                ]
            ]
        ]
    ]
];

try {
    v::key('original_name', v::stringType())
        ->key('translations', v::arrayType()->each(
            v::key('language', v::stringType())
            ->key('platforms', v::arrayType()
                ->each(
                v::key('id', v::intType())
                    ->key('minimum', v::stringType())
                    ->key('recommended', v::stringType(), false)
                    ->key('release_date', v::allOf(
                        v::stringType(),
                        v::date('Y-m-d')
                    ))->setName('Platform Object')
            ))->setName("Translation Object")
        )
    )->assert($bodyParams);
} catch (NestedValidationException $exception) {
    $messages = [];
    foreach ($exception->getIterator() as $sub) {
        $messages[] = [
            'message' => $sub->getMessage(),
            'identifier' => $sub->getId(),
            'input' => $sub->getParams()['input']
        ];
    }

    print_r($messages);
}

I've added setName to some of the grouped validators, this reflects on the identifier they inherit and it might make individual failures easier to find.

Instead of $exception->getIterator you could:

    $iterator = new RecursiveIteratorIterator(
        new Respect\Validation\Exceptions\RecursiveExceptionIterator($exception),
        RecursiveIteratorIterator::SELF_FIRST
    );

This would allow using $iterator->hasChildren() and $iterator->getChildren() to navigate freely on the structure. Check https://www.php.net/manual/en/class.recursiveiteratoriterator.php for more methods.

The code with the simple $exception->getIterator() should print this:

Array
(
    [0] => Array
        (
            [message] => These rules must pass for Translation Object
            [identifier] => Translation Object
            [input] => Array
                (
                    [language] => en
                    [title] => Exiles
                    [overview] => From the creators of the award winning...
                    [platforms] => Array
                        (
                            [0] => Array
                                (
                                    [id] => 8
                                    [minimum] => iPad 2
                                )

                            [1] => Array
                                (
                                    [id] => 7
                                )

                        )

                )

        )

    [1] => Array
        (
            [message] => Each item in platforms must be valid
            [identifier] => platforms
            [input] => Array
                (
                    [0] => Array
                        (
                            [id] => 8
                            [minimum] => iPad 2
                        )

                    [1] => Array
                        (
                            [id] => 7
                        )

                )

        )

    [2] => Array
        (
            [message] => These rules must pass for Platform Object
            [identifier] => Platform Object
            [input] => Array
                (
                    [id] => 8
                    [minimum] => iPad 2
                )

        )

    [3] => Array
        (
            [message] => release_date must be present
            [identifier] => release_date
            [input] => Array
                (
                    [id] => 8
                    [minimum] => iPad 2
                )

        )

    [4] => Array
        (
            [message] => These rules must pass for Platform Object
            [identifier] => Platform Object
            [input] => Array
                (
                    [id] => 7
                )

        )

    [5] => Array
        (
            [message] => minimum must be present
            [identifier] => minimum
            [input] => Array
                (
                    [id] => 7
                )

        )

    [6] => Array
        (
            [message] => release_date must be present
            [identifier] => release_date
            [input] => Array
                (
                    [id] => 7
                )

        )

)

From there you could find what went wrong with each specific key or maybe even rebuild the hierarchy.

I hope this can help in your scenario.

dimuska139 commented 2 years ago

@alganet thank you!