0

I've been reading a bit about form collections/fieldsets in ZF2 as I want to add some array fields to my form. However as I understand - fieldsets/collections are build with domain entities in mind. In my situation this solution doesn't seem to be anywhere close to good idea.

My form is not related to any entity, it is just meant to pass params to pdf generator. These params are serialized and save in just one column in db.

Form itself is quite big and has a lot of fields like:

Question:
a) smth
b) smth
c) smth
d) other
   a) other 1 
   b) other 2 
   ...
   x) other 22

So, I have multiple elements for extra input from user which have to be added dynamically.

Building a separated class for a fieldset containing ... 1 extra field each would result in atleast 20 extra classes per one form.

I thought I can make it simply by:

$element = new Element\Text('citiesOther[]');
$element->setAttributes(array(
    'id' => 'citiesOther',
    'placeholder' => 'Other city',
));
$this->add($element);

//view:
$this->formElement( $form->get( 'citiesOther[]' )

And then in the frontend there was a button "add another" which was just cloning whole input. It worked quite well, I could receive params from the post.

The thing is... I am unable to filter/validate these fields. If I pass a name "citiesOther[]" to inputFilter, it's not even validating one of these fields, not even mentioning whole array.

1 Answer 1

2

Collections in Zend Framework are a bit tricky to understand. Basically you need a basic fieldset for a collection. This fieldset implements the InputFilterProviderInterface for filtering and validation. With this in mind, you have to define all your repeatable input fields and the validation for this fields in a collection. It is not a good idea not validating this data. There are a few scenarios for injecting really bad data in a pdf. So please validate all data, which is coming from an form. Always!

Here 's a quick example how collection with Zend Framework are working.

namespace Application\Form;

use Zend\Form\Fieldset;
use Zend\Form\Element\Text;
use Zend\InputFilter\InputFilterProviderInterface;

class YourCollection extends Fieldset implements InputFilterProviderInterface
{
    public function init()
    {
        $this->add([
            'name' => 'simple',
            'type' => Text::class,
            'attributes' => [
                ...
            ],
            'options' => [
                ...
            ]
        ]);
    }

    public function getInputFilterSpecification()
    {
        return [
            'simple' => [
                'required' => true,
                'filters' => [
                    ...
                ],
                'validators' => [
                    ...
                ],
            ],
        ];
    }
}

This is the basic fieldset class, which acts as collection of all input fields you want to repeat in your form. This basic fieldset filters and validates itself by implementing the InputFilterProviderInterface. By using the implemented method, you can place filters and validators for your inputs If you have dependent input fields, you have to define them here in this fieldset.

For using it right, you need an entity, which is bound to the basic fieldset later in this post. Next, we create the entity for this fieldset.

namespace Application\Entity;

class YourCollectionEntity
{
    protected $simple;

    public function getSimple()
    {
        return $this->simple;
    }

    public function setSimple($simple)
    {
        $this->simple = $simple;
        return $this;
    }
}

This is a very simple entity. This will act as your data holder and will be bound to the collection later. Next we need a fieldset, that contains the collection and a hidden field for the collection count. Sounds a bit complecated using another fieldset. But in my eyes this is the best way.

namespace Application\Form;

class YourCollectionFieldset extends Fieldset implements InputFilterProviderInterface
{
    public function init()
    {
        $this->add([
            'name' => 'collectionCounter',
            'type' => Hidden::class,
            'attributes' => [
                ...
            ],
        ]);

        // add the collection fieldset here
        $this->add([
            'name' => 'yourCollection',
            'type' => Collection::class,
            'options' => [
                'count' => 1, // initial count
                'should_create_template' => true,
                'template_placeholder' => '__index__',
                'allow_add' => true,
                'allow_remove' => true,
                'target_element' => [
                    'type' => YourCollection::class,
                ],
            ],
        ]);
    }

    public function getInputFilterSpecification()
    {
        return [
            'collectionCounter' => [
                'required' => true,
                'filters' => [
                    [
                        'name' => ToInt::class,
                    ],
                ],
            ],
        ];
    }
}

This fieldset implements your collection and a hidden field, which acts as a counter for your collection. You need both when you want to handle collections. A case could be editing collection contents. Then you have to set the count of the collection.

As you can imagine, you need an entity class for this fieldset, too. So let 's write one.

namespace YourCollectionFieldsetEntity
{
    protected $collectionCounter;

    protected $yourCollection;

    public function getCollectionCounter()
    {
        return $this->collectionCounter;
    }

    public function setCollectionCounter($collectionCounter)
    {
        $this->collectionCounter = $collectionCounter;
        return $this;
    }

    public function getYourCollection()
    {
        return $this->yourCollection;
    }

    public function setYourCollection(YourCollectionEntity $yourCollection)
    {
        $this->yourCollection = $yourCollection;
        return $this;
    }
}

This entity contains the setYourCollection method, which takes an YourCollectionEntity instance as parameter. We will see later, how we will do that little monster.

Let 's wrap it up in a factory.

namespace Application\Form\Service;

YourCollectionFactory
{
    public function __invoke(ContainerInterface $container)
    {
        $entity = new YourCollectionFieldsetEntity();
        $hydrator = (new ClassMethods(false))
            ->addStrategy('yourCollection', new YourCollectionStrategy());

        $fieldset = (new YourCollectionFieldset())
            ->setHydrator($hydrator)
            ->setObject($entity);

        return $fieldset;
    } 
}

This factory adds a hydrator strategy to the hydrator. This will hydrate the entity bound to the repeatable fieldset.

How to use this in a form?

The above shown classes are for the needed collection only. The collection itself is not in a form yet. Let 's imagine we have a simple form, which implements this collection.

namespace Application\Form;

class YourForm extends Form
{
    public function __construct($name = null, array $options = [])
    {
        parent::__construct($name, $options);

        // add a few more input fields here

        // add your collection fieldset
        $this->add([
            'name' => 'fancy_collection',
            'type' => YourCollectionFieldset::class,
        ]);

        // add other simple input fields here
    }
}

Well, this is a simple form class, which uses the collection shown above. As you have written, you need a validator for validating all the data this form contains. No form validator without an entity. So first, we need an entity class for this form.

namespace Application\Entity;

class YourFormEntity
{
    // possible other form members
    ...

    protected $fancyCollection;

    public function getFancyCollection()
    {
        return $this->fancyCollection; 
    }

    public function setFancyCollection(YourCollectionFieldsetEntity $fancyCollection)
    {
         $this->fancyCollection = $fancyCollection;
         return $this;
    }

    // other getters and setters for possible other form fields
}

... and finally the validator class for your form.

namespace Application\InputFilter;

class YourFormInputFilter extends InputFilter
{
    // add other definitions for other form fields here
    ...
}

}

We don 't need to redefine filters and validators here for your collection. Remember, the collection itself implements the InputFilterProviderInterface which is executed automtically, when the validator for the form, which contains the collection, is executed.

Let 's wrap it up in a factory

namespace Application\Form\Service;

class YourFormFactory
{
    public function __invoke(ContainerInterface $container)
    {
         $container = $container->getServiceLocator();

         $entity = new YourFormEntity();
         $filter = new YourFormInputFilter();
         $hydrator = (new ClassMethods(false))
             ->addStrategy('fancy_collection', new FancyCollectionStrategy());

         $form = (new YourForm())
             ->setHydrator($hydrator)
             ->setObject($entity)
             ->setInputFilter($filter);

         return $form;
    }
}

That 's all. This is your form containing your collection.

Hydrator strategies are your friend

Above two hydrator strategies are added to the hydrators you need for your form. Adding hydrator strategies to a hydrator works like a charme, if you have complex post data, which is ment to be pressed in enities.

namespace Application\Hydrator\Strategy;

class YourCollectionStrategy extends DefaultStrategy
{
    public function hydrate($value)
    {
        $entities = [];
        if (is_array($value)) {
            foreach ($value as $key => $data) {
                $entities[] = (new ClassMethods())->hydrate($data, new YourCollectionEntity());
            }
        }

        return $entities;
    }
}

This hydrator strategy will hydrate the entity four the repeated collection fieldset. The next strategy will hydrate the whole collection data into your form entity.

namespace Application\Hydrator\Strategy;

class FancyCollectionStrategy extends DefaultStrategy
{
    public function hydrate($value)
    {
        return (new ClassMethods())
            ->addStrategy('yourCollection', new YourCollectionFieldsetEntity())
            ->hydrate($value);
    }
}

This one will hydrate the collection count and the repeated fieldset. That 's all for the hydration of your data.

How does that look in a controller?

Well, that 's simple. Now, as we have all the classes we need for a complex form with collection, wie can go on with the controller.

namespace Application\Controller;

class YourController extends AbstractActionController
{
    protected $form;

    public function __consturct(Form $form)
    {
        $this->form = $form;
    }

    public function indexAction()
    { 
        $request = $this->getRequest();

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

            if ($this->form->isValid()) {
                 // get all data as entity
                 $data = $this->form->getData();

                 // iterate through all collection data
                 foreach ($data->getFancyCollection()->getYourCollection() as $collection) {
                      // get every simple field from every collection
                      echo $collection->getSimple();
                 }
            }
        }
    }
}

Sure, this is way more complex than just rerieving the raw post data. But as a mentioned before, you should not use the raw data because of security reasons. Always validate and filter data, which was given by a user over a form. Or just to keep it simple: do not trust the user!

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you for your message! I'am aware that validation is must-have, however the moment I saw your message I got a bit ... disappointed? I can't belive that to achive such a simple thing so much work is needed in zf2. It like building a combine-harvester when you simply need to harvest a single carrot. Maybe we can build own inputfilter to recognize input like "name[]"? After all, when it comes to validating it should work exactly same as multicheckbox element - it's the same array in the post after all...
Well, the given example is like an example with all features zend framework comes with, when one have to deal with collections - validators for every single form element and complex hydration. Of course you can write your own input filter. Perhaps there is a ready to use solution with the ArrayInput input filter class. But this is for filtering simple arrays. For solving your issue with complex post data, I 'd suggest using collections. I truely can understand, that this complex feature is disappointing. Multicheckbox elements just use a simple in array validation. This won 't fit.
Actually these additional fields aren't complex at all - all I need to check if they're not empty and make sure there is no special chars. To be honest, I can't find any valuable resource for ArrayInput and how to implement it, as it seems that name with array like "name[]" ale somehow trimed by zf inputfilter base class. Thanks for your post tho.
Perhaps nested input filters are a way for you: stackoverflow.com/questions/34167386/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.