Possible Bug validating non-text input

Hey,

Just a little bit of context:
I’m using OptionalInputFilter into my forms because I have multiple inputs depending of others. For example: if you have to complete a payment form between Bank payment or Credit card payment and all the inputs you can imagine.

Before OptionalInputFilter, I read in blogs that people recommend to set your ValidationGroup array for each case, but when I was refactoring my code I found its not a good idea set this in Controller and I prefer to do all the stuff of Form inside itself (at least for me :man_shrugging:).
For that reason I began to use OptionalInputFilter.

Here my form class:

<?php
namespace Application\Form;

use Laminas\Form\Form;
use Laminas\InputFilter\OptionalInputFilter;

class BillingForm extends Form
{
    public function __construct()
    {
        parent::__construct('profileForm');
        $this->addElements();
        $this->addInputFilter();
    }

    protected function addElements()
    {
        $address = new Fieldset\Address($this->em);
        $this->add($address->get('address'));
        $this->add($address->get('address_height'));

        $this->add([
            'name' => 'submit',
            'type' => 'submit',
            'attributes' => [
                'class' => 'btn btn-sm btn-primary',
                'value' => 'Guardar cambios',
            ]
        ]);
    }

    protected function addInputFilter()
    {
        $inputFilter = $this->getInputFilter();
        $addressFilter = new OptionalInputFilter();
        $addressFilter->add([
            'name' => 'address',
            'required' => true,
            'filters' => [
                ['name' => 'StripTags'],
                ['name' => 'StringTrim'],
            ],
            'validators' => [
                [
                    'name' => 'StringLength',
                    'options' => [
                        'min' => 2,
                        'max' => 50
                    ],
                ],
            ],
        ]);

        $addressFilter->add([
            'name' => 'address_height',
            'required' => true,
            'filters' => [
                ['name' => 'StringTrim'],
            ],
            'validators' => [
                ['name' => 'Digits'],
                [
                    'name' => 'StringLength',
                    'options' => [
                        'max' => 10
                    ],
                ],
            ],
        ]);
        $inputFilter->add($addressFilter, 'address_inputs');
    }
}

And my Fieldset:


$this->add([
            'name' => 'address',
            'attributes' => [
                'required' => true,
                'class' => 'form-control required',
                'maxlength' => 50,
                'placeholder' => 'Calle'
            ],
            'options' => [
                'label' => 'Calle<sup class="red"><small>*</small></sup>',
                'label_options' => [
                    'disable_html_escape' => true,
                ],
            ],
        ]);

        $this->add([
            'name' => 'address_height',
            'type' => 'number',
            'attributes' => [
                'required' => true,
                'class' => 'form-control required',
                'maxlength' => 10,
                'placeholder' => 'Altura'
            ],
            'options' => [
                'label' => 'Altura<sup class="red"><small>*</small></sup>',
                'label_options' => [
                    'disable_html_escape' => true,
                ],
            ],
        ]);

When I submit the form, (using $form->setData($data) and check $form->getMessages() after $form->isValid() clause), I have this array:

array(1) { ["address_height"]=> array(1) { ["isEmpty"]=> string(50) "A value is required and it can not be empty" }

But … if I change type attribute from number to text, this input is valid.

What can it be?
This occurs with OptionalInputFilter and InputFilter, I just named it because there is not enough information about how to use it and I like to record this issue with this usage for other people that may google-it and find there are people using this :laughing:.

Thanks.
Julian

If address_height is required, it can not be empty.

This “may” be causing your issue:

The various elements come with different predefined input filter specifications that include validators and filters.

For the number element this means:

And required will be handled in an input of laminas-inputfilter:

However, the text element defines nothing:

So the hint to the NotEmpty validator is correct here.

Thank you @Tyrsson @froschdesign !!
Very interesting to know that.

If I add allow_empty and continue_is_empty fields in my validation, it continues be required. I think is my ValidationGroup that is verifying this first, is it correct?
I mean I am trying to understand setValidationGroup is applied before inputFilter

If you call setValidationGroup and do not pass that field, it will not be returned when you call the data from the form via $form->getData() post validation.

The validation group is a part of the input filter:

It is also possible to specify a “validation group”, a subset of the data to be validated; this may be done using the setValidationGroup() method. You can specify the list of the input names as an array or as individual parameters.

There is something I don’t understand with ValidationGroup. Let me tell you and I’m sorry if I’m not precise.
For example, if I have these 4 inputs:

  1. billing_type (non-required select box)
  2. tax_number (required if billing_type was filled)
  3. address (required if billing_type was filled)

Do I need to handle and set ValidationGroup? How can I do it inside of form? Do I need to know the form data?

This can be handled with a validation group. As an example, overwrite the setData method of your form.

Here is an incomplete example:

final class ExampleForm extends Laminas\Form\Form
    implements Laminas\InputFilter\InputFilterProviderInterface
{
    public function init(): void
    {
        $this->add(
            [
                'name'    => 'billing_type',
                'type'    => Laminas\Form\Element\Select::class,
                'options' => [
                    'value_options' => [
                        'Credit Card',
                        'Prepayment',
                    ],
                ],
            ]
        );

        $this->add(
            [
                'name' => 'tax_number',
                'type' => Laminas\Form\Element\Number::class,
            ]
        );

        $this->add(
            [
                'name' => 'address',
                'type' => Laminas\Form\Element\Text::class,
            ]
        );
    }

    public function getInputFilterSpecification(): array
    {
        // "billing_type" and "tax_number" are already required

        return [
            [
                'name'     => 'address',
                'required' => true,
            ],
        ];
    }

    public function setData(iterable $data): self
    {
        if ($data instanceof Traversable) {
            $data = ArrayUtils::iteratorToArray($data);
        }
        
        $group = ['billing_type'];
        if (array_key_exists('billing_type', $data)) {
            $group[] = 'tax_number';
            $group[] = 'address';
        }

        $this->setValidationGroup($group);

        return parent::setData($data);
    }
}

Test 1:

$form = new ExampleForm();
$form->init();

$form->setData([]);
var_dump($form->isValid()); // false
var_dump($form->getMessages()); // billing_type => isEmpty

Test 2:

$form = new ExampleForm();
$form->init();

$form->setData(['billing_type' => '1']);
var_dump($form->isValid()); // false
var_dump($form->getMessages()); // tax_number => isEmpty, address => isEmpty
1 Like

Thanks, @froschdesign

Could you show me the same example but tax_number input using OptionalInputFilter? (for example:

$this->add([
   'name' => 'tax_name',
   'attributes' => [
       'required' => true
   ]
]);

$taxNumber = new OptionalInputFilter();
$taxNumber->add([
   'name' => 'tax_number',
   'required' => true,
   'validators' => ['name' => 'IsInt'],
]);

$this->getInputFilter()->add($taxNumber, 'tax_filter');

Because I can’t make it work.
I understand that if tax_number has value, input filter will run, but I enter “text” on it, my form is still valid. Do I need to add tax_filter to validationGroup?

Thank you so much :pray:

You create a nested structure with your OptionalInputFilter:

$taxNumber = new Laminas\InputFilter\OptionalInputFilter();
$taxNumber->add(
    [
        'name'       => 'tax_number',
        'required'   => true,
        'validators' => [
            ['name' => Laminas\I18n\Validator\IsInt::class],
        ],
    ]
);
$inputFilter = new Laminas\InputFilter\InputFilter();
$inputFilter->add($taxNumber, 'tax_filter');

$inputFilter->setData([]);
var_dump($inputFilter->isValid()); // true

$inputFilter->setData(['tax_filter' => '']);
var_dump($inputFilter->isValid()); // true

$inputFilter->setData(['tax_filter' => []]);
var_dump($inputFilter->isValid()); // true

$inputFilter->setData(['tax_filter' => ['tax_number' => '']]);
var_dump($inputFilter->isValid()); // false
var_dump($inputFilter->getMessages()); // tax_filter => tax_number => isEmpty

$inputFilter->setData(['tax_filter' => ['tax_number' => 'foo']]);
var_dump($inputFilter->isValid()); // false
var_dump($inputFilter->getMessages()); // tax_filter => tax_number => notInt

$inputFilter->setData(['tax_filter' => ['tax_number' => '100']]);
var_dump($inputFilter->isValid()); // true

This nested structure does not match your form elements.

1 Like

Hi @froschdesign, I really appreciate your time.
So, to be consistent with OptionalInputFilter usage, do I need to create a nested form input structure to use it? only?

I deleted ‘tax_filter’ $name parameter of InputFilterInterface::add(), and it is still “valid”

This produces an unusable result because the name is an empty string:

var_dump(array_keys($inputFilter->getInputs()));
/*
array(1) {
  [0]=>
  string(0) ""
}
*/

Thank you a lot,
Finally I decided to overwrite setData method to set my validation group instead of use OptionalInputFilter.

Good decision, because the elements are not always optional according to your requirements. Once a payment method is selected, the elements should be filled. This means that these are no longer optional. But with OptionalInputFilter they are always optional.

2 Likes