0

I had a form that had two fields. An InputFilter with validators was applied to it. It was working fine. Then I moved the fields to a fieldset and added the fieldset to the form. Now the assignment validators to the fields is not present. The validator objects isValid method is not triggered at all. So how to apply the InputFilter validators to fields in a fieldset? Here you are the classes:

Text class Validator

namespace Application\Validator;

use Zend\Validator\StringLength;
use Zend\Validator\ValidatorInterface;

class Text implements ValidatorInterface
{
    protected $stringLength;
    protected $messages = [];

    public function __construct()
    {
        $this->stringLengthValidator = new StringLength();
    }

    public function isValid($value, $context = null)
    {       
        if (empty($context['url'])) {
            if (empty($value)) return false;
            $this->stringLengthValidator->setMin(3);
            $this->stringLengthValidator->setMax(5000);

            if ($this->stringLengthValidator->isValid($value)) {
                return true;
            }
            $this->messages = $this->stringLengthValidator->getMessages();

            return false;
        }
        if (!empty($value)) return false;
        return true;
    }

    public function getMessages()
    {
        return $this->messages;
    }
}

Test class InputFilter

namespace Application\Filter;

use Application\Fieldset\Test as Fieldset;
use Application\Validator\Text;
use Application\Validator\Url;
use Zend\InputFilter\InputFilter;

class Test extends InputFilter
{
    public function init()
    {
        $this->add([
            'name' => Fieldset::TEXT,
            'required' => false,
            'allow_empty' => true,
            'continue_if_empty' => true,
            'validators' => [
                ['name' => Text::class],
            ],
        ]);
        $this->add([
            'name' => Fieldset::URL,
            'required' => false,
            'allow_empty' => true,
            'continue_if_empty' => true,
            'validators' => [
                ['name' => Url::class],
            ],
        ]);
    }
}

Test class Fieldset

namespace Application\Fieldset;

use Zend\Form\Fieldset;

class Test extends Fieldset
{
    const TEXT = 'text';
    const URL = 'url';
    public function init()
    {
        $this->add([
            'name' => self::TEXT,
            'type' => 'textarea',
            'attributes' => [
                'id' => 'text',
                'class' => 'form-control',
                'placeholder' => 'Type text here',
                'rows' => '6',
            ],
            'options' => [
                'label' => self::TEXT,

            ],
        ]);
        $this->add([
            'name' => self::URL,
            'type' => 'text',
            'attributes' => [
                'id' => 'url',
                'class' => 'form-control',
                'placeholder' => 'Type url here',
            ],
            'options' => [
                'label' => self::URL,

            ],
        ]);
    }
}

Test class Form

namespace Application\Form;

use Application\Fieldset\Test as TestFieldset;
use Zend\Form\Form;

class Test extends Form
{
    public function init()
    {
        $this->add([
            'name' => 'test',
            'type' => TestFieldset::class,
            'options' => [
                'use_as_base_fieldset' => true,
            ],
        ]);
        $this->add([
            'name' => 'submit',
            'attributes' => [
                'type' => 'submit',
                'value' => 'Send',
            ],
        ]);
    }
}

TestController class

namespace Application\Controller;

use Application\Form\Test as Form;
use Zend\Debug\Debug;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class TestController extends AbstractActionController
{
    private $form;

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

    public function indexAction()
    {
        if ($this->getRequest()->isPost()) {
            $this->form->setData($this->getRequest()->getPost());
            Debug::dump($this->getRequest()->getPost());
            if ($this->form->isValid()) {
                Debug::dump($this->form->getData());
                die();
            }
        }
        return new ViewModel(['form' => $this->form]);
    }
}

TestControllerFactory class

namespace Application\Factory;

use Application\Controller\TestController;
use Application\Form\Test;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;

class TestControllerFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $form = $container->get('FormElementManager')->get(Test::class);

        return new TestController($form);
    }
}

Test class

namespace Application\Factory;

use Application\Filter\Test as Filter;
use Application\Entity\Form as Entity;
use Application\Form\Test as Form;
use Interop\Container\ContainerInterface;
use Zend\Hydrator\ClassMethods;
use Zend\ServiceManager\Factory\FactoryInterface;

class Test implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        return (new Form())
            ->setHydrator($container
                ->get('HydratorManager')
                ->get(ClassMethods::class))
            ->setObject(new Entity())
            ->setInputFilter($container->get('InputFilterManager')->get(Filter::class));
    }
}

Test Fieldset

namespace Application\Factory;

use Application\Entity\Fieldset as Entity;
use Application\Fieldset\Test as Fieldset;
use Interop\Container\ContainerInterface;
use Zend\Hydrator\ClassMethods;
use Zend\ServiceManager\Factory\FactoryInterface;

class TestFieldset implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        return (new Fieldset())
            ->setHydrator($container->get('HydratorManager')->get(ClassMethods::class))
            ->setObject(new Entity());
    }
}

UPDATE

I updated the fieldset class accordingly to @Nukeface advise by adding setInputFilter(). But it did not worked. It even had not executed InpuFilter class init method. Perhaps I did in wrong:

<?php

namespace Application\Fieldset;

use Application\Filter\Test as Filter;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterAwareTrait;

class Test extends Fieldset
{
    use InputFilterAwareTrait;

    const TEXT = 'text';
    const URL = 'url';

    public function init()
    {
        $this->add([
            'name' => self::TEXT,
            'type' => 'textarea',
            'attributes' => [
                'id' => 'text',
                'class' => 'form-control',
                'placeholder' => 'Type text here',
                'rows' => '6',
            ],
            'options' => [
                'label' => self::TEXT,

            ],
        ]);
        $this->add([
            'name' => self::URL,
            'type' => 'text',
            'attributes' => [
                'id' => 'url',
                'class' => 'form-control',
                'placeholder' => 'Type url here',
            ],
            'options' => [
                'label' => self::URL,

            ],
        ]);
        $this->setInputFilter(new Filter());
    }
}
rkeet
  • 3,406
  • 2
  • 23
  • 49
trzczy
  • 1,325
  • 2
  • 18
  • 43
  • Please have a look at [the answer](https://stackoverflow.com/a/40744201/1155833) to my question a while back. It's all about Separation of Concerns and re-usability with Forms, Fieldset and InputFilters. – rkeet Mar 21 '18 at 07:35
  • @Nukeface thanks for the advise, but it did not worked. See the update of my question. It seems not to see the InputFilter object at all. – trzczy Mar 23 '18 at 19:09
  • Working on it. ` by adding setInputFilter()` is incorrect by the way. You should be using the `InputFilterAwareInterface` on the Form and `InputFilterAwareTrait` on the Fieldset. Also keep both structures (FIeldsets and InputFilters) identical to each other within the Form. – rkeet Mar 23 '18 at 20:23
  • Right, initial setup of repository created [here](https://github.com/rkeet/zf-doctrine-form). Allows re-usable Fieldsets with InputFilters within a Form. Still testing, but should work for both normal objects using ZF Reflection hydrator class and DoctrineObject hydrator. Works the same as lain out in [ZF3 Form tutorial](https://docs.zendframework.com/tutorials/in-depth-guide/zend-form-zend-form-fieldset/), minor differences for Doctrine Entities. Should be updated again within the next 24 hours with documentation on usage and usage examples. – rkeet Mar 25 '18 at 15:01
  • I'm wondering if the start of [my repo](https://github.com/rkeet/zf-doctrine-form) helped you out? I'm also creating an example repo [here](https://github.com/rkeet/zf-doctrine-form-examples), for illustration of how to use. (When it's done, should be soon, I'll come back and write an answer here) – rkeet Apr 04 '18 at 06:15
  • @Nukeface hi this is what I have been trying to manage for a few weeks. Good job – trzczy Apr 04 '18 at 16:24
  • Very welcome and happy to help. Been using that code for passed few years, should've made a repo sooner to help others out. This question just triggered to do it now instead of later, so thank you. Heads up though, still a small bug somewhere when using Collection's of Fieldsets: somehow the base_fieldet's "id" field does not get filtered with `ToInt` filter, as such fails as a string on int property. Something I'm planning to look at the coming weekend. – rkeet Apr 04 '18 at 18:45
  • Been a while. Repo fully works, actually worked at the last comment as well, the bug was in the examples module. Should I write out an answer? – rkeet Apr 19 '18 at 09:37
  • @rkeet sure I would appreciate reading it. Thanks – trzczy Apr 19 '18 at 21:00
  • If your question has been answered, feel free to accept answer that helped you out – rkeet May 12 '18 at 09:51

2 Answers2

1

Tried an answer before and ran out of chars (30k limit), so created a repo instead. The repo contains abstraction of the answer below, which is a working example.

Your question shows you having the right idea, just not yet the implementation. It also contains a few mistakes, such as setting a FQCN for a Fieldset name. Hopefully the below can have you up and running.

As a use case, we'll have a basic Address form. Relationships for Country, Timezones and other things I'll leave out of the scope. For more in depth and nesting of Fieldsets (also with Collections) I'll refer you to my repo.


General setup

First create the basic setup. Create the Entity and configuration.

Basic Entity

namespace Demo\Entity;

class Address
{
    protected $id;     // int - primary key - unique - auto increment
    protected $street; // string - max length 255 - not null
    protected $number; // int - max length 11 - not null
    protected $city;   // string - max length 255 - null

    // getters/setters/annotation/et cetera
}

To handle this in a generic and re-usable way, we're going to need:

  • AddressForm (general container)
  • AddressFormFieldset (form needs to be validated)
  • AddressFieldset (contains the entity inputs)
  • AddressFieldsetInputFilter (must validate the data entered)
  • AddressController (to handle CRUD actions)
  • Factory classes for all of the above
  • a form partial

Configuration

To tie these together in Zend Framework, these need to be registered in the config. With clear naming, you can already add these. If you're using something like PhpStorm as your IDE, you might want to leave this till last, as the use statements can be generated for you.

As this is an explanation, I'm showing you now. Add this to your module's config:

// use statements here
return [
    'controllers' => [
        'factories' => [
            AddressController::class => AddressControllerFactory::class,
        ],
    ],
    'form_elements' => [ // <-- note: both Form and Fieldset classes count as Form elements
        'factories' => [
            AddressForm::class => AddressFormFactory::class,
            AddressFieldset::class => AddressFieldsetFactory::class,
        ],
    ],
    'input_filters' => [ // <-- note: input filter classes only!
        'factories' => [
            AddressFormInputFilter::class => AddressFormInputFilterFactory::class,
            AddressFieldsetInputFilter::class => AddressFieldsetInputFilterFactory::class,
        ],
    ],
    'view_manager' => [
        'template_map' => [
            'addressFormPartial'   => __DIR__ . '/../view/partials/address-form.phtml',
    ],
];

Fieldset

First we create the Fieldset (and Factory) class. This is because this contains the actual object we're going to handle.

AddressFieldset

// other use statements for Elements
use Zend\Form\Fieldset;

class AddressFieldset extends Fieldset
{
    public function init()
    {
        parent::init(); // called due to inheritance

        $this->add([
            'name' => 'id',
            'type' => Hidden::class,
        ]);

        $this->add([
            'name' => 'street',
            'required' => true,
            'type' => Text::class,
            'options' => [
                'label' => 'Name',
            ],
            'attributes' => [
                'minlength' => 1,
                'maxlength' => 255,
            ],
        ]);

        $this->add([
            'name' => 'number',
            'required' => true,
            'type' => Number::class,
            'options' => [
                'label' => 'Number',
            ],
            'attributes' => [
                'step' => 1,
                'min' => 0,
            ],
        ]);

        $this->add([
            'name' => 'city',
            'required' => false,
            'type' => Text::class,
            'options' => [
                'label' => 'Name',
            ],
            'attributes' => [
                'minlength' => 1,
                'maxlength' => 255,
            ],
        ]);
    }
}

AddressFieldsetFactory

// other use statements
use Zend\ServiceManager\Factory\FactoryInterface;

class AddressFieldsetFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $this->setEntityManager($container->get(EntityManager::class));

        /** @var AddressFieldset $fieldset */
        $fieldset = new AddressFieldset($this->getEntityManager(), 'address');
        $fieldset->setHydrator(
            new DoctrineObject($this->getEntityManager())
        );
        $fieldset->setObject(new Address());

        return $fieldset;
    }
}

InputFilter

Above we created the Fieldset. That allows for the generation of the Fieldset for in a Form. At the same time, Zend Framework also has defaults already set per type of input (e.g. 'type' => Text::class). However, if we want to validate it to our own, more strict, standards, we need to override the defaults. For this we need an InputFilter class.

AddressFieldsetInputFilter

// other use statements
use Zend\InputFilter\InputFilter;

class AddressFieldsetInputFilter extends InputFilter
{
    public function init()
    {
        parent::init(); // called due to inheritance

        $this->add([
            'name' => 'id',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],
            ],
            'validators' => [
                ['name' => IsInt::class],
            ],
        ]);

        $this->add([
            'name' => 'street',
            'required' => true,
            'filters' => [
                ['name' => StringTrim::class], // remove whitespace before & after string
                ['name' => StripTags::class],  // remove unwanted tags 
                [                              // if received is empty string, set to 'null'
                    'name' => ToNull::class,
                    'options' => [
                        'type' => ToNull::TYPE_STRING, // also supports other types
                    ],
                ],
            ],
            'validators' => [
                [
                    'name' => StringLength::class, // set min/max string length
                    'options' => [
                        'min' => 1,
                        'max' => 255,
                    ],
                ],
            ],
        ]);

        $this->add([
            'name' => 'number',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],    // received from HTML form always string, have it cast to integer
                [
                    'name' => ToNull::class, // if received is empty string, set to 'null'
                    'options' => [
                        'type' => ToNull::TYPE_INTEGER,
                    ],
                ],
            ],
            'validators' => [
                ['name' => IsInt::class], // check if actually integer
            ],
        ]);

        $this->add([
            'name' => 'city',
            'required' => false, // <-- not required
            'filters' => [
                ['name' => StringTrim::class], // remove whitespace before & after string
                ['name' => StripTags::class],  // remove unwanted tags 
                [                              // if received is empty string, set to 'null'
                    'name' => ToNull::class,
                    'options' => [
                        'type' => ToNull::TYPE_STRING, // also supports other types
                    ],
                ],
            ],
            'validators' => [
                [
                    'name' => StringLength::class, // set min/max string length
                    'options' => [
                        'min' => 1,
                        'max' => 255,
                    ],
                ],
            ],
        ]);
    }
}

AddressFieldsetInputFilterFactory

// other use statements
use Zend\ServiceManager\Factory\FactoryInterface;

class AddressFieldsetInputFilterFactory implements FactoryInterface
{
   public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        // Nothing else required in this example. So it's as plain as can be.
        return new AddressFieldsetInputFilter();
    }
}

Form & Validation

So. Above we created the Fieldset, it's InputFilter and 2 required Factory classes. This already allows us to do a great deal, such as:

  • use the InputFilter in stand-alone setting to dynamically validate an object
  • re-use Fieldset + InputFilter combination in other Fieldset and InputFilter classes for nesting

Form

use Zend\Form\Form;
use Zend\InputFilter\InputFilterAwareInterface;
// other use statements

class AddressForm extends Form implements InputFilterAwareInterface
{
    public function init()
    {
        //Call parent initializer. Check in parent what it does.
        parent::init();

        $this->add([
            'type'    => Csrf::class,
            'name'    => 'csrf',
            'options' => [
                'csrf_options' => [
                    'timeout' => 86400, // day
                ],
            ],
        ]);

        $this->add([
            'name' => 'address',
            'type' => AddressFieldset::class,
            'options' => [
                'use_as_base_fieldset' => true,
            ],
        ]);

        $this->add([
            'name'       => 'submit',
            'type'       => Submit::class,
            'attributes' => [
                'value' => 'Save',
            ],
        ]);
    }
}

Form Factory

use Zend\ServiceManager\Factory\FactoryInterface;
// other use statements

class AddressFormFactory implements FactoryInterface
{
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        /** @var AbstractForm $form */
        $form = new AddressForm('address', $this->options);
        $form->setInputFilter(
            $container->get('InputFilterManager')->get(ContactFormInputFilter::class);
        );

        return $form;
    }
}

Making it all come together

I'll show just the AddressController#addAction

AddressController

use Zend\Mvc\Controller\AbstractActionController;
// other use statements

class AddressController extends AbstractActionController
{
    protected $addressForm;   // + getter/setter
    protected $entityManager; // + getter/setter

    public function __construct(
        EntityManager $entityManager, 
        AddressForm $form
    ) {
        $this->entityManager = $entityManager;
        $this->addressForm = $form;
    }

    // Add your own: index, view, edit and delete functions

    public function addAction () {
        /** @var AddressForm $form */
        $form = $this->getAddressForm();

        /** @var Request $request */
        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());

            if ($form->isValid()) {
                $entity = $form->getObject();
                $this->getEntityManager()->persist($entity);

                try {
                    $this->getEntityManager()->flush();
                } catch (\Exception $e) {
                    $this->flashMessenger()->addErrorMessage($message);

                    return [
                        'form' => $form,
                        'validationMessages' => $form->getMessages() ?: '',
                    ];
                }

                $this->flashMessenger()->addSuccessMessage(
                    'Successfully created object.'
                );

                return $this->redirect()->route($route, ['param' => 'routeParamValue']);
            }

            $this->flashMessenger()->addWarningMessage(
                'Your form contains errors. Please correct them and try again.'
            );
        }

        return [
            'form' => $form,
            'validationMessages' => $form->getMessages() ?: '',
        ];
    }
}

AddressControllerFactory

class AddressControllerFactory implements FactoryInterface
{

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        /** @var AddressController $controller */
        $controller = new AddressController(
            $container->get(EntityManager::class), 
            $container->get('FormElementManager')->get(AddressForm::class);
        );

        return $controller;
    }
}

Display in addressFormPartial

$this->headTitle('Add address');

$form->prepare();
echo $this->form()->openTag($form);
echo $this->formRow($form->get('csrf'));

echo $this->formRow($form->get('address')->get('id'));
echo $this->formRow($form->get('address')->get('street'));
echo $this->formRow($form->get('address')->get('number'));
echo $this->formRow($form->get('address')->get('city'));

echo $this->formRow($form->get('submit'));
echo $this->form()->closeTag($form);

To use this partial, say in a add.phtml view, use:

<?= $this->partial('addressFormPartial', ['form' => $form]) ?>

This bit of code will work with the demonstrated addAction in the Controller code above.


Hope you found this helpful ;-) If you have any questions left, don't hesitate to ask.

rkeet
  • 3,406
  • 2
  • 23
  • 49
  • Thanks alot. It helped me a lot. In my case, I had to set my `Form` i.e. `$fieldset->setObject(new EnquiryForm());` as I'm not having any DB interaction so don't need `EntityManager`. – GoharSahi Feb 24 '22 at 08:05
0

Just use the InputFilterProviderInterface class to your fieldset. This implements the getInputFilterSpecification method to your fieldset, which executes the input filters mentioned in this method.

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

    public function getInputFilterSpecification()
    {
        return [
            'textfield' => [
                'required' => true,
                'filters' => [
                    ...
                ],
                'validators' => [
                    [
                        'name' => YourTextValidator::class,
                        'options' => [
                            ...
                        ],
                    ],
                ],
            ],
        ];
    }
}

AS long as you add this fieldset in your form the bound filters and validators will be executed on the isValid method call of your form.

Marcel
  • 4,854
  • 1
  • 14
  • 24
  • thanks for the helpful answer. But I still seek an answer how to use my InpuFilter class. – trzczy Mar 23 '18 at 19:11
  • It 's all in the answer. Just add the `InputFilterProviderInterface` to your fieldset and use the `getInputFilterSpecification` method to apply your filters. Add your fieldset in your form class and that 's it. – Marcel Mar 24 '18 at 09:18