0

In my Form I have a Fieldset, that contains two elements foo and bar. The business rule for them is, that one has to be set. So, the fieldset is valid, when foo OR bar is set, and invalid, when no-one is set.

I solved this as follows:

public function getInputFilterSpecification()
{
    return [
        'foo' => [
            'required' => empty($this->get('bar')->getValue())
        ],
        'bar' => [
            'required' => empty($this->get('foo')->getValue())
        ],
    ];
}

Working. But there is still an issue with the error messages: If bot fields are empty, the user gets for every field the message "Value is required and can't be empty". The user thinks then, he has to fill in both fields.

How to customize the error message for a required field, in order to show correct messages like "Value for foo is required and can't be empty, if bar is not set." and "Value for bar is required and can't be empty, if foo is not set."?

automatix
  • 14,018
  • 26
  • 105
  • 230

2 Answers2

0

You could wirte a custom ValidatorChain by extending the default Chain

You could then override this method:

/**
 * Returns true if and only if $value passes all validations in the chain
 *
 * Validators are run in the order in which they were added to the chain  (FIFO).
 *
 * @param  mixed $value
 * @param  mixed $context Extra "context" to provide the validator
 * @return bool
 */
 public function isValid($value, $context = null)
 { .. }

By default this method will only return true if ALL validators return true, but it also has access to the context - this means you can get all other field values too.

It's simple to amend the logic to then check if either of the validators are true return true.

You then simply attach all the fields you want to be "one of these required"

Kwido
  • 1,382
  • 8
  • 21
Andrew
  • 12,617
  • 1
  • 34
  • 48
-1

You probaly end up with a custom validator like this:

class MyCustomValidator extends ZendAbstractValidator
{
    const FIELDS_EMPTY = 'fieldsEmpty';

    /**
     * Error messages
     *
     * @var array
     */
    protected $abstractMessageTemplates = [
        self::FIELDS_EMPTY => "Vale for %field1% is required and can't be empty, if %field2% is not set.",
    ];

    /**
     * Variables which can be used in the message templates
     *
     * @var array
     */
    protected $abstractMessageVariables = [
        'field1' => 'field1',
        'field2' => 'field2',
    ];

    /**
     * Value of the field
     * @var mixed
     */
    protected $value;

    /**
     * Name of the first field to check, which the validator is bind to
     * @var mixed
     */
    protected $field1;

    /**
     * Name of the second field to check
     * @var string
     */
    protected $field2;

    /**
     * MyCustomValidator constructor.
     *
     * @param array|null|\Traversable $options
     *
     * @throws \Exception
     */
    public function __construct($options)
    {
        if ($options instanceof Traversable) {
            $options = ArrayUtils::iteratorToArray($options);
        }

        if (!array_key_exists('field1', $options) || !array_key_exists('field2', $options)) {
            throw new \Exception('Options should include both fields to be defined within the form context');
        }

        $this->field1 = $options['field1'];
        $this->field2 = $options['field2'];

        parent::__construct($options);
    }

    /**
     * Returns true if and only if $value meets the validation requirements
     * If $value fails validation, then this method returns false, and
     * getMessages() will return an array of messages that explain why the
     * validation failed.
     *
     * @param  mixed $value
     * @param array  $context
     *
     * @return bool
     */
    public function isValid($value, $context = [])
    {
        $this->setValue($value);

        if (empty($value) && (array_key_exists($this->field2, $context) || empty($context[$this->field2]))) {
            $this->error(self::FIELDS_EMPTY);

            return false;
        }

        return true;
    }
}

So how to use it:

public function getInputFilterSpecification()
{
    return [
        'foo' => [
            'validators' => [
                [
                    'name' => MyCustomValidator::class,
                    'options' => [
                        'field1' => 'foo',
                        'field2' => 'bar',
                    ]
                ]
            ]
        ],
        'bar' => [
            'validators' => [
                [
                    'name' => MyCustomValidator::class,
                    'options' => [
                        'field1' => 'bar',
                        'field2' => 'foo',
                    ]
                ]
            ]
        ],
    ];
}

For those who don't know how to register the MyCustomValidator - in your module.config.php or use the public function getValidatorConfig() within your Module.php. Don't use both, it's one or the other:

How to register in your module.config.php

'validators' => array(
    'factories' => array(
        MyCustomValidator::class => MyCustomValidatorFactory::class,
    ),
 ),

How to register in your module.php:

/**
 * Expected to return \Zend\ServiceManager\Config object or array to
 * seed such an object.
 * @return array|\Zend\ServiceManager\Config
 */
public function getValidatorConfig()
{
    return [
        'aliases' => [
            'myCustomValidator' => MyCustomValidator::class,
            'MyCustomValidator' => MyCustomValidator::class,
            'mycustomvalidator' => MyCustomValidator::class,
        ],
        'factories' => [
            MyCustomValidator::class => MyCustomValidatorFactory::class,
        ],
    ];
}

The Factory class:

class MyCustomValidatorFactory implements FactoryInterface, MutableCreationOptionsInterface
{
    /**
     * Options for the InputFilter
     *
     * @var array
     */
    protected $options;

    /**
     * Create InputFilter
     *
     * @param ServiceLocatorInterface $serviceLocator
     *
     * @return BlockChangeOnSerialsValidator
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        return new MyCustomValidator($this->options);
    }

    /**
     * Set creation options
     *
     * @param  array $options
     *
     * @return void
     */
    public function setCreationOptions(array $options)
    {
        $this->setOptions($options);
    }

    /**
     * Set options
     *
     * @param array $options
     */
    public function setOptions(array $options)
    {
        $this->options = $options;
    }
}

Notice that I kept the validator isValid() method pretty simple as I'm not sure it was covering your case, but this is to help you push in the right direction. But an improvement that can be made is to reuse the NotEmpty validator to check whether the field is empty or not instead.

Notice that Context witin the isValid($value, $context = null) form is the formData when you call $form->setData($this->getRequest()->getPost()).

Kwido
  • 1,382
  • 8
  • 21
  • Thank you for your answer, but it doesn't work in the case with "conditionally required" fields I described in the question. Please read the question. – automatix Nov 14 '16 at 15:55
  • Ahh okay. I see I was a bit to quick with my conclusion probaly due to your highlighting of "How to customize the message of error message". Will try to update my answer, as it would probaly come to a custom validator. – Kwido Nov 15 '16 at 08:56
  • @automatix Did my updated answer helped you any further? As the question is still unanswered, did you found a solution already for it? – Kwido Nov 25 '16 at 13:09