Collection Element Validation

I have an application which has a base fieldset (Task) and collections (Roles) within that fieldset. I’ve setup validation to check the role collections are not empty but am struggling to get the system to validate the required field in the role collection.

I’ve tried adding a CollectionInputFilter to the base fieldset which does work and makes the field required in the collection however the validation always fails saying the field cannot be empty.

The CollectionInputFilter looks like this:

                'type' => CollectionInputFilter::class,
                'input_filter' => [
                    'IdRole' => [
                        'required' => true
                    ]
                ]

The collection input filter is defined as this:

  'claimTaskDefinitionResponsibleRoles' => [
                'required' => true,
                'validators' => [
                    [
                        'name' => NotEmpty::class,
                        'options' => array(
                            'messages' => array(
                                NotEmpty::IS_EMPTY => 'You must select at least 1 Role'
                            )
                        )
                    ]
                ],
                'type' => CollectionInputFilter::class,
                'input_filter' => [
                    'IdRole' => [
                        'required' => true
                    ]
                ]
            ],

Hello and welcome to our forums! :smiley:

Unfortunately, I am a little confused, because there are two collection input filters or is this a mistake in your post?

Thanks for the welcome! We’ve moved to Laminas from ZF1 so a steep learning curve.

The 2nd CollectionInputFilter is actually the entire element defined in getInputFilterSpecification function. The first should have been the form element so I am not sure what happened there. It seems I can’t edit my post either to correct it. So to kinda start again in my fieldset init function I’ve defined:

 $this->add([
            'name' => 'claimTaskDefinitionResponsibleRoles',
            'type' => Collection::class,
            'options' => [
                'count' => 0,
                'should_create_template' => true,
                'allow_add' => true,
                'allow_remove' => true,
                'target_element' => [
                    'type' => ClaimTaskDefinitionResponsibleRoleFieldset::class
                ]
            ]
        ]);

In the getInputFilterSpecification function I’ve got

  'claimTaskDefinitionResponsibleRoles' => [
                'required' => true,
                'validators' => [
                    [
                        'name' => NotEmpty::class,
                        'options' => array(
                            'messages' => array(
                                NotEmpty::IS_EMPTY => 'You must select at least 1 Role'
                            )
                        )
                    ]
                ],
                'type' => CollectionInputFilter::class,
                'input_filter' => [
                    'IdRole' => [
                        'required' => true
                    ]
                ]
            ],

When I first try to submit my form without having anything in my collection then the not empty validation kicks in for the collection and it displays my ‘you must select at least 1 role error’. If I add an item to the collection but don’t select a role then the not empty validator for the ‘input_filter’ kicks in and everything is fine. However once I’ve selected a role and try to submit my form then the form always fails validation stating that the field is still empty.

EDIT: I did originally have IdRole validation in the ClaimTaskDefinitionResponsibleRoleFieldset but it was never triggering

Here is an example and I hope I have not overlooked anything:

class ExampleForm extends Laminas\Form\Form
    implements Laminas\InputFilter\InputFilterProviderInterface
{
    public function init(): void
    {
        $this->add(
            [
                'name'    => 'claimTaskDefinitionResponsibleRoles',
                'type'    => Laminas\Form\Element\Collection::class,
                'options' => [
                    'count'                  => 0,
                    'should_create_template' => true,
                    'allow_add'              => true,
                    'allow_remove'           => true,
                    'target_element'         => [
                        'type' => ClaimTaskDefinitionResponsibleRoleFieldset::class,
                    ],
                ],
            ]
        );
    }

    public function getInputFilterSpecification(): array
    {
        return [
            'claimTaskDefinitionResponsibleRoles' => [
                'type'         => Laminas\InputFilter\CollectionInputFilter::class,
                'required'     => true,
                'input_filter' => [
                    'IdRole' => [
                        'required' => true,
                    ],
                ],
            ],
        ];
    }
}

class ClaimTaskDefinitionResponsibleRoleFieldset extends Laminas\Form\Fieldset
{
    public function init(): void
    {
        $this->add(
            [
                'type' => Laminas\Form\Element\Text::class,
                'name' => 'IdRole',
            ]
        );
    }
}

$formElementManager = new Laminas\Form\FormElementManager(
    new Laminas\ServiceManager\ServiceManager()
);

/** @var \Laminas\Form\FormInterface $form */
$form = $formElementManager->get(ExampleForm::class);

Test with an empty array:

$form->setData([]);
var_dump($form->isValid()); // false

Test with null:

$form->setData(
    [
        'claimTaskDefinitionResponsibleRoles' => null,
    ]
);
var_dump($form->isValid()); // false

Test with an empty array:

$form->setData(
    [
        'claimTaskDefinitionResponsibleRoles' => [],
    ]
);

var_dump($form->isValid()); // false

Test with wrong name for collection item:

$form->setData(
    [
        'claimTaskDefinitionResponsibleRoles' => [
            [
                'wrong-name' => 'Foo',
            ],
        ],
    ]
);
var_dump($form->isValid()); // false

$form->setData(
    [
        'claimTaskDefinitionResponsibleRoles' => [
            [
                'IdRole' => 'Foo',
            ],
            [
                'wrong-name' => 'Bar',
            ],
        ],
    ]
);
var_dump($form->isValid()); // false

Test with valid data:

$form->setData(
    [
        'claimTaskDefinitionResponsibleRoles' => [
            [
                'IdRole' => 'Foo',
            ],
        ],
    ]
);
var_dump($form->isValid()); // true

$form->setData(
    [
        'claimTaskDefinitionResponsibleRoles' => [
            [
                'IdRole' => 'Foo',
            ],
            [
                'IdRole' => 'Bar',
            ],
            [
                'IdRole' => 'Baz',
            ],
        ],
    ]
);
var_dump($form->isValid()); // true

Thanks froschdesign so much for your help, following your example it works and shows as valid with the data provided. How your example differs from my code is I have a fieldset which the claimTaskDefinitionReponsibleRoles element is added to.

    public function init()
    {

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

I am wondering if it is this layer which is causing the validation issues.

EDIT: We use fieldsets because we use doctrine. That is why we have the fieldset inside the form.

Unfortunately, no one can know or see this. :wink:

Please provide the entire form definition.

haha true without the code you’ll never know :smiley:

Here is the form:

<?php

namespace ClaimTaskDefinition\Form;

use Doctrine\Laminas\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManager;
use Gen10\Framework\Repository\RoleRepository;
use Laminas\Form\Form;

class ClaimTaskDefinitionForm extends Form
{
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        parent::__construct();
        $this->entityManager = $entityManager;
        $this->setHydrator(new DoctrineObject($this->entityManager));
    }

    public function init()
    {
        $this->roleRepo = new RoleRepository();

        $this->setAttribute('method', 'POST');
        $this->setAttribute('id', 'claimTaskDefinition');
        $this->setAttribute('name', 'claimTaskDefinition');

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

        $this->add(array(
            'name' => 'submit',
            'type' => 'Submit',
            'attributes' => array(
                'value' => 'Submit',
                'class' => 'btn btn-primary'
            ),
        ));

        $this->add(array(
            'name' => 'cancel',
            'type' => 'Button',
            'options' => array(
                'label' => 'Cancel',
                'label_options' => array(
                    'disable_html_escape' => true,
                )
            ),
            'attributes' => array(
                'value' => 'Cancel',
                'class' => 'btn btn-secondary'
            ),
        ));
    }

    public function isValid()
    {
        // If the user has selected Alarm Notification then the expiration, user role and email template fields are required.
         if ($this->data['claimTaskDefinition']['AlarmNotification'] == 1) {

             if ($this->data['claimTaskDefinition']['idEmailTemplate_Alarm'] == '' && $this->data['claimTaskDefinition']['AlarmNotification'] == 1) {
                 $this->getInputFilter()->get('claimTaskDefinition')
                     ->get('idEmailTemplate_Alarm')->setRequired(true);
             }

             error_log(print_r($this->data['claimTaskDefinition'], 1));
             if (!isset($this->data['claimTaskDefinition']['claimTaskDefinitionAlarmRecipientRoles']) || ($this->data['claimTaskDefinition']['claimTaskDefinitionAlarmRecipientRoles'] == '' && $this->data['claimTaskDefinition']['AlarmNotification'] == 1)) {
                 $this->getInputFilter()->get('claimTaskDefinition')
                     ->get('claimTaskDefinitionAlarmRecipientRoles')->setRequired(true);
             }

             if ($this->data['claimTaskDefinition']['TaskExpirationDays'] == '' && $this->data['claimTaskDefinition']['AlarmNotification'] == 1) {
                 $this->getInputFilter()->get('claimTaskDefinition')
                     ->get('TaskExpirationDays')->setRequired(true);
             }
         }

        // If the user has selected Reminder Notification then the interval days and reminder email template fields are required.
        if ($this->data['claimTaskDefinition']['ReminderNotification'] == 1) {
            if ($this->data['claimTaskDefinition']['ReminderIntervalDays'] == '' && $this->data['claimTaskDefinition']['ReminderNotification'] == 1) {
                $this->getInputFilter()->get('claimTaskDefinition')
                    ->get('ReminderIntervalDays')->setRequired(true);
            }

            if ($this->data['claimTaskDefinition']['idEmailTemplate_Reminder'] == '' && $this->data['claimTaskDefinition']['ReminderNotification'] == 1) {
                $this->getInputFilter()->get('claimTaskDefinition')
                    ->get('idEmailTemplate_Reminder')->setRequired(true);
            }
        }

        // If the user has selected Send Notification on completion then the uer role and email template fields are required.
        if ($this->data['claimTaskDefinition']['CompletedNotification'] == 1) {
            if (!isset($this->data['claimTaskDefinition']['claimTaskDefinitionCompletedRecipientRoles']) || ($this->data['claimTaskDefinition']['claimTaskDefinitionCompletedRecipientRoles'] == '' && $this->data['claimTaskDefinition']['CompletedNotification'] == 1)) {
                $this->getInputFilter()->get('claimTaskDefinition')
                    ->get('claimTaskDefinitionCompletedRecipientRoles')->setRequired(true);
            }

            if ($this->data['claimTaskDefinition']['idEmailTemplate_Completed'] == '' && $this->data['claimTaskDefinition']['CompletedNotification'] == 1) {
                $this->getInputFilter()->get('claimTaskDefinition')
                    ->get('idEmailTemplate_Completed')->setRequired(true);
            }

        }

        $result = parent::isValid();


        // If the responsible role collection is invalid then set the is-invalid attribute to is-invalid. This will be
        // used on the collection view to add the is-invalid class so the validation messages are displayed correctly.
        if(!$this->getInputFilter()->get('claimTaskDefinition')->get('claimTaskDefinitionResponsibleRoles')->isValid()){
            $this->get('claimTaskDefinition')->get('claimTaskDefinitionResponsibleRoles')->setAttribute('is-invalid', 'is-invalid');
        }


        if(!isset($this->data['claimTaskDefinition']['claimTaskDefinitionVisibleRoles'])){
            $this->get('claimTaskDefinition')->get('claimTaskDefinitionVisibleRoles')->setAttribute('is-invalid', 'is-invalid');
        }

        if(!isset($this->data['claimTaskDefinition']['claimTaskDefinitionAlarmRecipientRoles'])){
            $this->get('claimTaskDefinition')->get('claimTaskDefinitionAlarmRecipientRoles')->setAttribute('is-invalid', 'is-invalid');
        }

        if(!isset($this->data['claimTaskDefinition']['claimTaskDefinitionCompletedRecipientRoles'])){
            $this->get('claimTaskDefinition')->get('claimTaskDefinitionCompletedRecipientRoles')->setAttribute('is-invalid', 'is-invalid');
        }

        return $result;
    }
}

Good point you can’t see the code:

Here is the form

<?php

namespace ClaimTaskDefinition\Form;

use Doctrine\Laminas\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManager;
use Gen10\Framework\Repository\RoleRepository;
use Laminas\Form\Form;

class ClaimTaskDefinitionForm extends Form
{
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        parent::__construct();
        $this->entityManager = $entityManager;
        $this->setHydrator(new DoctrineObject($this->entityManager));
    }

    public function init()
    {
        $this->roleRepo = new RoleRepository();

        $this->setAttribute('method', 'POST');
        $this->setAttribute('id', 'claimTaskDefinition');
        $this->setAttribute('name', 'claimTaskDefinition');

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

        $this->add(array(
            'name' => 'submit',
            'type' => 'Submit',
            'attributes' => array(
                'value' => 'Submit',
                'class' => 'btn btn-primary'
            ),
        ));

        $this->add(array(
            'name' => 'cancel',
            'type' => 'Button',
            'options' => array(
                'label' => 'Cancel',
                'label_options' => array(
                    'disable_html_escape' => true,
                )
            ),
            'attributes' => array(
                'value' => 'Cancel',
                'class' => 'btn btn-secondary'
            ),
        ));
    }

    public function isValid()
    {
        // If the user has selected Alarm Notification then the expiration, user role and email template fields are required.
         if ($this->data['claimTaskDefinition']['AlarmNotification'] == 1) {

             if ($this->data['claimTaskDefinition']['idEmailTemplate_Alarm'] == '' && $this->data['claimTaskDefinition']['AlarmNotification'] == 1) {
                 $this->getInputFilter()->get('claimTaskDefinition')
                     ->get('idEmailTemplate_Alarm')->setRequired(true);
             }

             error_log(print_r($this->data['claimTaskDefinition'], 1));
             if (!isset($this->data['claimTaskDefinition']['claimTaskDefinitionAlarmRecipientRoles']) || ($this->data['claimTaskDefinition']['claimTaskDefinitionAlarmRecipientRoles'] == '' && $this->data['claimTaskDefinition']['AlarmNotification'] == 1)) {
                 $this->getInputFilter()->get('claimTaskDefinition')
                     ->get('claimTaskDefinitionAlarmRecipientRoles')->setRequired(true);
             }

             if ($this->data['claimTaskDefinition']['TaskExpirationDays'] == '' && $this->data['claimTaskDefinition']['AlarmNotification'] == 1) {
                 $this->getInputFilter()->get('claimTaskDefinition')
                     ->get('TaskExpirationDays')->setRequired(true);
             }
         }

        // If the user has selected Reminder Notification then the interval days and reminder email template fields are required.
        if ($this->data['claimTaskDefinition']['ReminderNotification'] == 1) {
            if ($this->data['claimTaskDefinition']['ReminderIntervalDays'] == '' && $this->data['claimTaskDefinition']['ReminderNotification'] == 1) {
                $this->getInputFilter()->get('claimTaskDefinition')
                    ->get('ReminderIntervalDays')->setRequired(true);
            }

            if ($this->data['claimTaskDefinition']['idEmailTemplate_Reminder'] == '' && $this->data['claimTaskDefinition']['ReminderNotification'] == 1) {
                $this->getInputFilter()->get('claimTaskDefinition')
                    ->get('idEmailTemplate_Reminder')->setRequired(true);
            }
        }

        // If the user has selected Send Notification on completion then the uer role and email template fields are required.
        if ($this->data['claimTaskDefinition']['CompletedNotification'] == 1) {
            if (!isset($this->data['claimTaskDefinition']['claimTaskDefinitionCompletedRecipientRoles']) || ($this->data['claimTaskDefinition']['claimTaskDefinitionCompletedRecipientRoles'] == '' && $this->data['claimTaskDefinition']['CompletedNotification'] == 1)) {
                $this->getInputFilter()->get('claimTaskDefinition')
                    ->get('claimTaskDefinitionCompletedRecipientRoles')->setRequired(true);
            }

            if ($this->data['claimTaskDefinition']['idEmailTemplate_Completed'] == '' && $this->data['claimTaskDefinition']['CompletedNotification'] == 1) {
                $this->getInputFilter()->get('claimTaskDefinition')
                    ->get('idEmailTemplate_Completed')->setRequired(true);
            }

        }

        $result = parent::isValid();


        // If the responsible role collection is invalid then set the is-invalid attribute to is-invalid. This will be
        // used on the collection view to add the is-invalid class so the validation messages are displayed correctly.
        if(!$this->getInputFilter()->get('claimTaskDefinition')->get('claimTaskDefinitionResponsibleRoles')->isValid()){
            $this->get('claimTaskDefinition')->get('claimTaskDefinitionResponsibleRoles')->setAttribute('is-invalid', 'is-invalid');
        }


        if(!isset($this->data['claimTaskDefinition']['claimTaskDefinitionVisibleRoles'])){
            $this->get('claimTaskDefinition')->get('claimTaskDefinitionVisibleRoles')->setAttribute('is-invalid', 'is-invalid');
        }

        if(!isset($this->data['claimTaskDefinition']['claimTaskDefinitionAlarmRecipientRoles'])){
            $this->get('claimTaskDefinition')->get('claimTaskDefinitionAlarmRecipientRoles')->setAttribute('is-invalid', 'is-invalid');
        }

        if(!isset($this->data['claimTaskDefinition']['claimTaskDefinitionCompletedRecipientRoles'])){
            $this->get('claimTaskDefinition')->get('claimTaskDefinitionCompletedRecipientRoles')->setAttribute('is-invalid', 'is-invalid');
        }

        return $result;
    }
}

Good point!

Here is the form code:

<?php

namespace ClaimTaskDefinition\Form;

use Doctrine\Laminas\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManager;
use Laminas\Form\Form;

class ClaimTaskDefinitionForm extends Form
{
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        parent::__construct();
        $this->entityManager = $entityManager;
        $this->setHydrator(new DoctrineObject($this->entityManager));
    }

    public function init()
    {

        $this->setAttribute('method', 'POST');
        $this->setAttribute('id', 'claimTaskDefinition');
        $this->setAttribute('name', 'claimTaskDefinition');

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

        $this->add(array(
            'name' => 'submit',
            'type' => 'Submit',
            'attributes' => array(
                'value' => 'Submit',
                'class' => 'btn btn-primary'
            ),
        ));

        $this->add(array(
            'name' => 'cancel',
            'type' => 'Button',
            'options' => array(
                'label' => 'Cancel',
                'label_options' => array(
                    'disable_html_escape' => true,
                )
            ),
            'attributes' => array(
                'value' => 'Cancel',
                'class' => 'btn btn-secondary'
            ),
        ));
    }
}


And the fieldset ClaimTaskDefinitionFieldset contains the collection element claimTaskDefinitionResponsibleRoles, right?

Correct. That is a big fieldset and I can’t seem to put lots of code into 1 reply

Can be removed, POST is the default.

Discourse sets for new users some restrictions.

did you want me to post the fieldset? well just the start and the relevant bits?

Here is the ClaimTaskDefinitionFieldset class

<?php


namespace ClaimTaskDefinition\Form;

use ClaimTaskDefinition\ValueObject\TaskType;
use Doctrine\Laminas\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManager;
use Gen10\Cowbell\Entity\ClaimTaskDefinition;
use Gen10\Framework\Repository\EmailTemplateRepository;
use Laminas\Form\Element\Checkbox;
use Laminas\Form\Element\Collection;
use Laminas\Form\Element\Hidden;
use Laminas\Form\Element\Select;
use Laminas\Form\Element\Text;
use Laminas\Form\Element\Textarea;
use Laminas\Form\Fieldset;
use Laminas\InputFilter\CollectionInputFilter;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Validator\NotEmpty;
use function Symfony\Component\DependencyInjection\Loader\Configurator\param;

class ClaimTaskDefinitionFieldset extends Fieldset implements InputFilterProviderInterface
{
    protected $emailTemplateRepo;
    protected $roleRepo;

    /**
     * @var EntityManager
     */
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        parent::__construct();
        $this->entityManager = $entityManager;
        $this->setHydrator(new DoctrineObject($this->entityManager));
        $this->setObject(new ClaimTaskDefinition());
    }

    public function init($name = null, $options = [])
    {
        $this->emailTemplateRepo = new EmailTemplateRepository();
        $taskTypeVo = new TaskType();

     .....

        $this->add([
            'name' => 'claimTaskDefinitionResponsibleRoles',
            'type' => Collection::class,
            'options' => [
                'count' => 0,
                'should_create_template' => true,
                'allow_add' => true,
                'allow_remove' => true,
                'target_element' => [
                    'type' => ClaimTaskDefinitionResponsibleRoleFieldset::class
                ]
            ]
        ]);

   .....
    }

    /**
     * @return array
     */
    public function getInputFilterSpecification(): array
    {
        return [
      .....
            'claimTaskDefinitionResponsibleRoles' => [
                'required' => true,
                'validators' => [
                    [
                        'name' => NotEmpty::class,
                        'options' => array(
                            'messages' => array(
                                NotEmpty::IS_EMPTY => 'You must select at least 1 Role'
                            )
                        )
                    ]
                ],
                'type' => CollectionInputFilter::class,
                'input_filter' => [
                    'IdRole' => [
                        'required' => true
                    ]
                ]
            ],
            .....
        ];
    }
}

You should check that the filter is created correctly or as expected and that all inputs are included.

Debugging this form validation and looking at the inputFilter there are 2 inputs instead of 1 and the ones in the filter do not match the input defined for the collection:

So perhaps the input with IdRole isn’t being validated so failing validation whereas the one called 1 is passing

Can you test if the input filter specification works when you add everything to the form and remove other specifications from the fieldsets?

When you say remove other specifictions do you mean remove all of them apart from the field I’m trying to validate or just debug with no validation at all?

For testing, there should be only one getInputFilterSpecification method in the form and not in any other fieldset. Maybe there will be unexpected results when merging the input filter specification.

(I will try to create my own example later.)