Issues

ZF-3242: Add functionality to set form-level errors

Description

As discussed on fw-mvc, it would be nice to be able to set form-level errors that apply to a form submission as a whole and not to any particular element. This could be useful for, e.g., 'Invalid login credentials' or 'there was an error submitting your form'.

I have outlined a possible solution at http://codecaine.co.za/blog/posts/….

I think that the code should operate at the Zend_Form level and not at the Zend_Form_Element level.

Comments

Scheduling for next minor release.

version 1.6.0RC3

if you set the decorator "Errors" at form level

e.g.. $form->setDecorators(array( 'Description', 'Errors', 'FormElements', 'Form' ));

and then set a form level error $form->addError('Error message') it will produce a form level error

BUT, if you don't set a form level error i.e. $form->addError('Error message') an error occurs on post back:

Warning: htmlspecialchars() expects parameter 1 to be string, array given in \path\zend\1.6.0RC3\library\Zend\View\Abstract.php on line 804

Warning: htmlspecialchars() expects parameter 1 to be string, array given in \path\to\zend1.6.0RC3\library\Zend\View\Abstract.php on line 804

Functionality released with 1.6.0

Changing issues in preparation for the 1.7.0 release.

I still get an error adding the Errors decorator to the form.

This is the code that reproduces the issue using Zend 1.7.8:


<?php
require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

$oEmail = new Zend_Form_Element_Text('email');
$oEmail
    ->addValidator('EmailAddress', true)
    ->clearDecorators()
    ->addDecorator('ViewHelper')
    ->addDecorator('Errors')
    ->setRequired(true);

$oForm = new Zend_Form();
$oForm
    ->addElement($oEmail)
    ->clearDecorators()
    ->addDecorator('FormElements')
    ->addDecorator('Errors', array('placement' => Zend_Form_Decorator_Abstract::PREPEND))
    ->addDecorator('Form');

$oForm->isValid(array('email' => 'foo'));
$oForm->addErrors(array('Sign up failed')); // Without this throws an error

echo $oForm->render(new Zend_View());

I'm particularly keen to see this one resolved, too. Whole of form error messages is just too common a function to not be properly catered for in Zend.

Seems to me that this issue would be solved with a sanitisation filter on the {{$errors}} parameter of the {{formErrors}} function in the {{FormErrors}} view helper.

The function expects the parameter {{$errors}} to be either a string or an array of strings. When the "Errors" decorator is used at the form level and element level errors are created an array of arrays of strings is passed in. These sub arrays may then be passed to {{$this->view->escape}} which causes the above error or are output as empty strings in the html output (undesirable to have empty errors showing up).

Removing any non-string values from the {{$errors}} parameter would solve this. The filter could be akin to:

{quote} foreach ($errors as $key => $error) { if (!is_string($error)) { unset($errors[$key]); } } if (count($errors) == 0) { return ''; } {quote}

Oops. Wiki markup-fail. : (


foreach ($errors as $key => $error) {
    if (!is_string($error)) {
        unset($errors[$key]);
    }
}

if (count($errors) == 0) {
    return '';
} 

Apparent errors in current functionality.

Closing again. The functionality added was the FormErrors decorator. If you wish to report form-level errors, or report all errors on a given form at once, use the FormErrors decorator. The Errors decorator is intended for individual form elements only.

But the FormErrors decorator doesn't support form-level errors:


<? require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

$form = new Zend_Form();
$form
    ->setDecorators(
        array(
            'FormErrors',
            'FormElements',
            'Form'
        )
    )
    ->addError('Form level error');

echo($form->render(new Zend_View())); ?>

I cannot get it working. I watch in the FormErrors decorator , method _recurseForm() , I noticed $errors should be indexed by element name, but when you attach errors with $form->addError("...") error messages are indexed by int value 0,1,2....

You can't get it to work because Zend doesn't natively support form-level errors.

Here's a quick plugin that I use:

<>\Zend\Form\Decorator\FormLevelErrors.php

 
<? require_once 'Zend/Form/Decorator/Errors.php';

class Zend_Form_Decorator_FormLevelErrors extends Zend_Form_Decorator_Errors {
    // Standard Zend_Form_Decorator_Errors render function (Zend v1.9)
    public function render($content) {
        $element = $this->getElement();
        $view   = $element->getView();
        if (null === $view) {
            return $content;
        }

        $errors = $element->getMessages();
        
        // Remove errors deeper than one level (errors associated with an element)
        foreach ($errors as $key => $error) {
            if (!is_string($error)) {
                unset($errors[$key]);
            }
        }
        
        if (empty($errors)) {
            return $content;
        }

        $separator = $this->getSeparator();
        $placement = $this->getPlacement();
        $errors = $view->formErrors($errors, $this->getOptions()); 

        switch ($placement) {
            case self::APPEND:
                return $content . $separator . $errors;
            case self::PREPEND:
                return $errors . $separator . $content;
        }
    }
} ?>

Usage example

 
<? require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();

$form = new Zend_Form();
$form
    ->setDecorators(
        array(
            'FormLevelErrors',
            'FormElements',
            'Form'
        )
    )
    ->addError('Form level error');

echo($form->render(new Zend_View())); ?>

I know this a two year old issue, but it's still not resolved IMO. Only the code from Casey works as expected:

  • The Errors decorator shows only the element-level errors
  • The FormErrors decorator shows all errors, but displays element-level errors twice (at the top + next to each element)

Whereas the behavior of Casey's code is to display: - Form-level errors at the top of the form - Element-level errors next to the elements

And I think this is what most users expect. Could it be possible to have this feature built-in ZF?

Anyone to reopen the issue, or should we create a new one as a feature request?

I'm confused as to what you want, to be honest.

  • FormErrors is used to show all errors from all elements at once, as well as any attached directly to the form.
  • Errors is used to show errors specific to a given element.
  • If your elements all compose the Errors decorator, and you also have the FormErrors decorator attached, you'll end up displaying the errors from each element both at the top as well as with each element.

Do I understand correctly that what you would like to do is have a form-level decorator that displays only errors attached directly to the form? (That appears to be what Casey is suggesting.)

If so, we can re-open. However, this would be a target for 1.12 (next minor release) as opposed to the next 1.11 bugfix.

I'll try to summarize the issue with screenshots. For all examples, unless otherwise specified, this code is applied to the form:


$form->addError('This is a form level error');

1) If no specific decorator is used, the form-level errors are not displayed:

!http://i.imgur.com/prHiv.png!

2) If the FormErrors decorator is used, the form-level errors are shown, but the element-level errors are shown twice:

!http://i.imgur.com/UldKB.png!

3) If the Errors decorator is used, that looks perfect:

!http://i.imgur.com/dusJp.png!

But here is what happens if you remove the call to addError():

!http://i.imgur.com/Jwk0O.png!

So I'm not sure whether Zend_Form needs a new decorator, or if something just needs to be fixed. Casey highlighted a possible fix in [his comment above|#comment-33194], but that doesn't completely solve the problem: if the form is posted with only form-level errors, these are not displayed.

In my opinion, the default behavior for Zend_Form (which shouldn't break backward compatibility) should be to unconditionally display both kind of errors with default decorators, as highlighted again in this screenshot:

!http://i.imgur.com/dusJp.png!

That won't break any code relying on the default decorators, as they'll have no form-level errors set. I don't know what is the best way to achieve this goal, but I can work on a patch on my own if you agree with this behavior.

You can set the onlyCustomFormErrors option to true for the FormErrors decorator so that it will return only custom errors and ignore any error returned by the form elements. The code below (line 433 in FormErrors.php) is the part that retrieves the messages.

 
    protected function _recurseForm(Zend_Form $form, Zend_View_Interface $view)
    {
        $content = '';

        $custom = $form->getCustomMessages();
        if ($this->getShowCustomFormErrors() && count($custom)) {
            $content .= $this->getMarkupListItemStart()
                     .  $view->formErrors($custom, $this->getOptions())
                     .  $this->getMarkupListItemEnd();
        }
        foreach ($form->getElementsAndSubFormsOrdered() as $subitem) {
            if ($subitem instanceof Zend_Form_Element && !$this->getOnlyCustomFormErrors()) {
                $messages = $subitem->getMessages();
                if (count($messages)) {
                    $subitem->setView($view);
                    $content .= $this->getMarkupListItemStart()
                             .  $this->renderLabel($subitem, $view)
                             .  $view->formErrors($messages, $this->getOptions())
                             .  $this->getMarkupListItemEnd();
                }
            } else if ($subitem instanceof Zend_Form && !$this->ignoreSubForms()) {
                $markup = $this->_recurseForm($subitem, $view);

                if (!empty($markup)) {
                    $content .= $this->getMarkupListStart()
                              . $markup
                              . $this->getMarkupListEnd();
                }
            }
        }
        return $content;
    }

Actually, after reading through everything again I think that all this behavior is consistent with what you've just said in your post, Matthew. What's missing is indeed a form-level-errors-only decorator, and Casey's [FormLevelErrors|#comment-36076] fits the bill perfectly. It could even be part of the default Zend_Form decorators.

Thanks for picking up the baton on this one, Strict Coding. ;)

@Giuliano: sorry, I hadn't taken the time to look at your post closely. You're right, this solution, poorly documented, actually works perfectly, no need for modifications in Zend_Form!


$this->addDecorator('FormErrors', array('onlyCustomFormErrors' => true))
     ->addDecorator('FormElements')
     ->addDecorator('HtmlTag', array('tag' => 'dl', 'class' => 'zend_form'))
     ->addDecorator('Form');

It could be helpful to document carefully this feature on the website. Or wouldn't it be interesting, once again, to make this decorator part of the default decorators?