zendframework / zend-inputfilter

InputFilter component from Zend Framework
BSD 3-Clause "New" or "Revised" License
64 stars 50 forks source link

Change validator chain of collection fields #109

Closed FabianKoestring closed 8 years ago

FabianKoestring commented 8 years ago

Hey there,

in some cases i used to change some validators for some fields if the value of them is equals to a given one. Like in this example i change aNotherTestField to required and add an NotEmpty validator if the raw value of testField is equals 4.

public function isValid($context = null)
{
    if ($this->has('testField')) {
        $testFieldRawValue = $this->get('testField')->getRawValue();
        if ($testFieldRawValue == 4) {
            $validatorChain = new Validator\ValidatorChain();
            $notEmptyValidator = new Validator\NotEmpty();
            $notEmptyValidator->setMessage('My Message.', Validator\NotEmpty::IS_EMPTY);
            $validatorChain->attach(
                $notEmptyValidator,
                true
            );
            $this->get('aNotherTestField')->setRequired(true);
            $this->get('aNotherTestField')->setValidatorChain($validatorChain);
        }
    }
}

This works great. But when it come to fieldsets and collections i am not able to change the validator chain of an collections field anymore.

if ($this->has('testCollection')) {
    $testCollection= $this->get('testCollection');
    if ($testCollection->has('testField')) {
        $testFieldRawValue = $testCollection->get('testField')->getRawValue();
        if ($testFieldRawValue == 4) {
            $validatorChain = new Validator\ValidatorChain();
            $notEmptyValidator = new Validator\NotEmpty();
            $notEmptyValidator->setMessage('My Message.', Validator\NotEmpty::IS_EMPTY);
            $validatorChain->attach(
                $notEmptyValidator,
                true
            );
            $testCollection->get('aNotherTestField')->setRequired(true);
            $testCollection->get('aNotherTestField')->setValidatorChain($validatorChain);
        }
    }
}

Is there something wrong with my code or is it a bug?

froschdesign commented 8 years ago
$testCollection = $this->get('individuellesBerufsbild');
if ($testCollection->has('testField')) {
    $testFieldRawValue = $individuellesBerufsbild->get('status')->getRawValue();
}

Is this correct? Or do you mean:

$testCollection->get('status')->getRawValue();

or:

$testCollection->get('testField')->getRawValue();
$individuellesBerufsbild

Schöner Variablenname! 😉

FabianKoestring commented 8 years ago

:smile: :smile: copy/paste.

Maks3w commented 8 years ago

Can you provide a functional example with the input data, the result and the expected result?

FabianKoestring commented 8 years ago

Ok! I used the forms, controller and views from the Form Collections Page. I tested this with zend skeleton application and following versions.

container-interop/container-interop 1.1.0  Promoting the interoperability ...
doctrine/instantiator               1.0.5  A small, lightweight utility to...
myclabs/deep-copy                   1.5.1  Create deep copies (clones) of ...
phpdocumentor/reflection-common     1.0    Common reflection classes used ...
phpdocumentor/reflection-docblock   3.0.2  With this component, a library ...
phpdocumentor/type-resolver         0.1.8  
phpspec/prophecy                    v1.6.1 Highly opinionated mocking fram...
phpunit/php-code-coverage           4.0.0  Library that provides collectio...
phpunit/php-file-iterator           1.4.1  FilterIterator implementation t...
phpunit/php-text-template           1.2.1  Simple template engine.
phpunit/php-timer                   1.0.8  Utility class for timing
phpunit/php-token-stream            1.4.8  Wrapper around PHP's tokenizer ...
phpunit/phpunit                     5.4.2  The PHP Unit Testing framework.
phpunit/phpunit-mock-objects        3.2.1  Mock Object library for PHPUnit
psr/http-message                    1.0    Common interface for HTTP messages
psr/log                             1.0.0  Common interface for logging li...
sebastian/code-unit-reverse-lookup  1.0.0  Looks up which function or meth...
sebastian/comparator                1.2.0  Provides the functionality to c...
sebastian/diff                      1.4.1  Diff implementation
sebastian/environment               1.3.7  Provides functionality to handl...
sebastian/exporter                  1.2.1  Provides the functionality to e...
sebastian/global-state              1.1.1  Snapshotting of global state
sebastian/object-enumerator         1.0.0  Traverses array structures and ...
sebastian/recursion-context         1.0.2  Provides functionality to recur...
sebastian/resource-operations       1.0.0  Provides a list of PHP built-in...
sebastian/version                   1.0.6  Library that helps with managin...
symfony/yaml                        v3.1.0 Symfony Yaml Component
webmozart/assert                    1.0.2  Assertions to validate method i...
zendframework/zend-authentication   2.5.3  provides an API for authenticat...
zendframework/zend-barcode          2.6.0  provides a generic way to gener...
zendframework/zend-cache            2.7.1  provides a generic way to cache...
zendframework/zend-captcha          2.5.4  
zendframework/zend-code             2.6.2  provides facilities to generate...
zendframework/zend-config           2.6.0  provides a nested object proper...
zendframework/zend-console          2.6.0  
zendframework/zend-crypt            2.6.0  
zendframework/zend-db               2.8.1  
zendframework/zend-debug            2.5.1  
zendframework/zend-di               2.6.1  
zendframework/zend-diactoros        1.3.5  PSR HTTP Message implementations
zendframework/zend-dom              2.6.0  provides tools for working with...
zendframework/zend-escaper          2.5.1  
zendframework/zend-eventmanager     2.6.3  
zendframework/zend-feed             2.7.0  provides functionality for cons...
zendframework/zend-file             2.7.0  
zendframework/zend-filter           2.7.1  provides a set of commonly need...
zendframework/zend-form             2.8.3  
zendframework/zend-http             2.5.4  provides an easy interface for ...
zendframework/zend-hydrator         1.1.0  
zendframework/zend-i18n             2.7.2  
zendframework/zend-i18n-resources   2.5.2  Provides validator translations...
zendframework/zend-inputfilter      2.7.1  
zendframework/zend-json             2.6.1  provides convenience methods fo...
zendframework/zend-loader           2.5.1  
zendframework/zend-log              2.8.3  component for general purpose l...
zendframework/zend-mail             2.7.1  provides generalized functional...
zendframework/zend-math             2.7.0  
zendframework/zend-memory           2.5.2  
zendframework/zend-mime             2.6.0  
zendframework/zend-modulemanager    2.7.2  
zendframework/zend-mvc              2.7.8  
zendframework/zend-navigation       2.7.1  provides support for managing t...
zendframework/zend-paginator        2.7.0  
zendframework/zend-permissions-acl  2.6.0  provides a lightweight and flex...
zendframework/zend-permissions-rbac 2.5.1  provides a role-based access co...
zendframework/zend-progressbar      2.5.2  component to create and update ...
zendframework/zend-psr7bridge       0.2.2  PSR-7 <-> Zend\Http bridge
zendframework/zend-serializer       2.7.2  provides an adapter based inter...
zendframework/zend-server           2.6.1  
zendframework/zend-servicemanager   2.7.6  
zendframework/zend-session          2.7.1  manage and preserve session dat...
zendframework/zend-soap             2.6.0  
zendframework/zend-stdlib           2.7.7  
zendframework/zend-tag              2.6.1  a component suite which provide...
zendframework/zend-test             2.6.1  
zendframework/zend-text             2.6.0  
zendframework/zend-uri              2.5.2  a component that aids in manipu...
zendframework/zend-validator        2.8.0  provides a set of commonly need...
zendframework/zend-version          2.5.1  
zendframework/zend-view             2.7.0  provides a system of helpers, o...
zendframework/zend-xmlrpc           2.5.2  
zendframework/zendframework         2.5.3  Zend Framework 2
zendframework/zendxml               1.0.2  Utility library for XML usage, ...

The Controller

<?php
namespace Application\Controller;

use Application\Form\CreateProduct;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $form = new CreateProduct();
        $form->bind(new \stdClass());

        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());

            if ($form->isValid()) {
                var_dump($form->getData());
            }
        }
        $viewModel = new ViewModel();
        $viewModel->setVariable('form', $form);
        return $viewModel;
    }
}

Category Fieldset

<?php
namespace Application\Form;

use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;
use Zend\Validator\StringLength;

class CategoryFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct()
    {
        parent::__construct('category');

        $this
            ->setHydrator(new ClassMethodsHydrator(false))
            ->setObject(new \stdClass());

        $this->setLabel('Category');

        $this->add(
            array(
                'name'       => 'name',
                'options'    => array(
                    'label' => 'Name of the category',
                ),
                'attributes' => array(
                    'required' => 'required',
                ),
            )
        );
    }

    /**
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return array(
            'name' => array(
                'required'   => true,
                'filters'    => [],
                'validators' => [
                    [
                        'name'                   => StringLength::class,
                        'break_chain_on_failure' => true,
                        'options'                => [
                            'min' => 10,
                            'max' => 15
                        ]
                    ],
                ]
            ),
        );
    }
}

Product Fieldset

<?php
namespace Application\Form;

use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;

class ProductFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function __construct()
    {
        parent::__construct('product');

        $this
            ->setHydrator(new ClassMethodsHydrator(false))
            ->setObject(new \stdClass());

        $this->add(
            array(
                'type'    => 'Zend\Form\Element\Collection',
                'name'    => 'categories',
                'options' => array(
                    'label'                  => 'Please choose categories for this product',
                    'count'                  => 5,
                    'should_create_template' => true,
                    'allow_add'              => true,
                    'target_element'         => array(
                        'type' => 'Application\Form\CategoryFieldset',
                    ),
                ),
            )
        );
    }

    /**
     * Should return an array specification compatible with
     * {@link Zend\InputFilter\Factory::createInputFilter()}.
     *
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return array();
    }
}

CreateProduct Form

<?php
namespace Application\Form;

use Zend\Form\Form;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;

class CreateProduct extends Form
{
    public function __construct()
    {
        parent::__construct('create_product');

        $this
            ->setAttribute('method', 'post')
            ->setHydrator(new ClassMethodsHydrator(false))
            ->setInputFilter(new CreateProductFilter());

        $this->add(
            array(
                'type'    => 'Application\Form\ProductFieldset',
                'options' => array(
                    'use_as_base_fieldset' => true,
                ),
            )
        );

        $this->add(
            array(
                'type' => 'Zend\Form\Element\Text',
                'name' => 'single',
                'options' => [
                    'label' => 'Single Field'
                ]
            )
        );

        $this->add(
            array(
                'type' => 'Zend\Form\Element\Csrf',
                'name' => 'csrf',
            )
        );

        $this->add(
            array(
                'name'       => 'submit',
                'attributes' => array(
                    'type'  => 'submit',
                    'value' => 'Send',
                ),
            )
        );
    }
}

Create Product Filter

<?php
namespace Application\Form;

use Zend\InputFilter\InputFilter;
use Zend\Validator;
use Zend\Filter;

class CreateProductFilter extends InputFilter
{
    public function __construct()
    {
        $this->add(
            [
                'name'       => 'single',
                'required'   => true,
                'validators' => [
                    [
                        'name'                   => Validator\StringLength::class,
                        'break_chain_on_failure' => true,
                        'options'                => [
                            'min' => 10,
                            'max' => 15
                        ]
                    ]
                ]
            ]
        );
    }

    /**
     * @inheritDoc
     */
    public function isValid($context = null)
    {
        // Single
        if ($this->has('single')) {
            $single = $this->get('single');

            $stringLengthValidator = new Validator\StringLength();
            $stringLengthValidator->setMax(1000);
            $stringLengthValidator->setMin(100);

            $validatorChain = new Validator\ValidatorChain();
            $validatorChain->attach($stringLengthValidator);

            $single->setValidatorChain($validatorChain);
            $single->setRequired(true);
        }

        // Collection
        if ($this->has('product')) {
            $product = $this->get('product');
            if ($product->has('categories')) {
                $categories = $product->get('categories');
                if ($categories->has('name')) {
                    $name = $categories->get('name');

                    $stringLengthValidator = new Validator\StringLength();
                    $stringLengthValidator->setMax(1000);
                    $stringLengthValidator->setMin(100);

                    $validatorChain = new Validator\ValidatorChain();
                    $validatorChain->attach($stringLengthValidator);

                    $name->setValidatorChain($validatorChain);
                    $name->setRequired(true);
                }
            }
        }
        return parent::isValid($context);
    }
}

View Model

<div class="row">
    <?php
    $form->setAttribute('action', $this->url('home'))
        ->prepare();

    echo $this->form()->openTag($form);

    $product = $form->get('product');
    echo $this->formCollection($product->get('categories'));
    echo $this->formRow($form->get('single'));
    echo $this->formHidden($form->get('csrf'));
    echo $this->formElement($form->get('submit'));

    echo $this->form()->closeTag();
    ?>
</div>

Result

As you can see in the picture below, the validator chain of that single field gets refreshed in isValid of the defined filter. I know that becaus of that error message i get. But the error messages of that collection fields arent refreshed. They are still as they are defined in that CategoryFieldset.

image

I hope this helps to solve my problem. Thanks so far!

Maks3w commented 8 years ago

The issue is categories is a CollectionInputFilter so its not possible to query about if has any field. The correct way is:

                /** @var CollectionInputFilter $categories */
                $categories = $product->get('categories');
                $categoryIF = $categories->getInputFilter();
                if ($categoryIF->has('name')) {
                    $name = $categoryIF->get('name');