Zend Form Recipes

Published on Friday, 03 September 2010.

When I had to work on a project using Zend Framework 1.10.X, I encountered a few issues/problems/obstacles. Last time I worked with ZF, the version was 1.7.2, so things changed a bit. This post describes my solutions to those questions/issues/etc.

For reference, the application namespace prefix used in the following examples is App.

  1. How to add colons to form labels?
  2. Adding links to a label, or how to disable escaping for some labels?
  3. How to add validation for Password verification?
  4. Validating unique values accross the database for new users
  5. How to add a field for uploading files?
  6. How to add a recaptcha field?

1. How to add colons to the labels?

This is probably the least important topic about forms, however, I like my form labels with colons!

The Zend Form Label decorators provides you with two methods to do this: setRequiredSuffix() and setOptionalSuffix(). But using a custom decorator saves some typing:

<?php

// Set a base decorator path
class MyForm extends Zend_form
{
    // ...

    public function init()
    {
        $this->addElementPrefixPath(
            'App_Form_Decorator',
            'App/Form/Decorator/',
            'decorator'
        );
    }

    // ...
}

// Sample Label decorator
class App_Form_Decorator_ColonLabel extends Zend_Form_Decorator_Label
{
    public function getLabel()
    {
        $label = parent::getLabel();

        return $label . ':';
    }
}

2. Adding links to a label, or how to disable escaping for some labels?

It is very common to have an "Agree to terms" link as a checkbox on Signup forms. Since labels in Zend Form are escaped by default, in order to display arbitrary HTML, you need to set some options for the Label decorator. These are my default decorator for those kind of checkboxes:

<?php
$checkboxDecorators = array(
   'ViewHelper',
    array('Label', array('placement' => 'APPEND', 'escape' => false)),
    array('Errors', array('class' => 'inlineError')),
    array(array('row' => 'HtmlTag'), 
          array('tag' => 'li', 'class' => 'checkbox')
    ),
);

Now, if you are using FormErrors and the field has an error, its label will display escaped even when you already said that you don't want that. The solution is to use a custom FormErrors decorator:

<?php
class App_Form_Decorator_FormErrors extends Zend_Form_Decorator_FormErrors
{
    public function renderLabel(Zend_Form_Element $element, Zend_View_Interface $view)
    {
        $label = $element->getLabel();
        if (empty($label)) {
            $label = $element->getName();
        }
        $options = $element->getDecorator('label')->getOptions();
        $escape = isset($options['escape']) 
                  ? (bool) $options['escape'] : true;

        $labelOutput = $escape ? $view->escape($label) : $label;

        return $this->getMarkupElementLabelStart()
             . $labelOutput
             . $this->getMarkupElementLabelEnd();
    }
}

3. How to add validation for Password verification?

This one is often seen on signup/registration forms. Sometimes not only for passwords, but also for emails. Here's an approach to adding this kind of validation.

First we define a base class called EqualValue and then the validation classes.

<?php

// First let's setup access to custom validation rules:
class Signup_Form_Signup extends Zend_Form
{
    public function init()
    {
        $this->addElementPrefixPath(
            'Signup_Validate',
            APPLICATION_PATH . '/modules/signup/models/validate/',
            'validate'
        );
    }
}


// This belongs to the Signup module
class Signup_Validate_EqualValue extends Zend_Validate_Abstract
{
    const NOT_MATCH = 'notMatch';

    protected $_messageTemplates = array(
        self::NOT_MATCH => 'do not match'
    );

    protected $_verifyKey = '';

    public function isValid($value, $context=null)
    {
        $value = (string) $value;
        $this->_setValue($value);

        if (is_array($context)) {
            if (isset($context[$this->_verifyKey])
                && ($value == $context[$this->_verifyKey])) {
                return true;
            }
        } elseif (is_string($context) && ($value == $context)) {
            return true;
        }

        $this->_error(self::NOT_MATCH);

        return false;
    }
}


// Password Verification

/**
 * Validates that password and the password verification values are equal
 */
require_once 'EqualValue.php';

class Signup_Validate_PasswordVerification extends Signup_Validate_EqualValue
{
    public function isValid($value, $context=null)
    {
        $this->_verifyKey = 'password';
        $this->_messageTemplates[self::NOT_MATCH] = 
            'Passwords ' . $this->_messageTemplates[self::NOT_MATCH];

        return parent::isValid($value, $context);
    }
}


// Likewise for the Email Field

require_once 'EqualValue.php';

class Signup_Validate_EmailVerification extends Signup_Validate_EqualValue
{
    public function isValid($value, $context=null)
    {
        $this->_verifyKey = 'email';
        $this->_messageTemplates[self::NOT_MATCH] = 
            'Emails ' . $this->_messageTemplates[self::NOT_MATCH];

        return parent::isValid($value, $context);
    }
}

4. Validating unique values accross the database

<?php
// The validation rule
class Signup_Validate_UniqueEmail extends Zend_Validate_Abstract
{
    const EMAIL_EXISTS = 'emailExists';

    protected $_messageTemplates = array(
        self::EMAIL_EXISTS => 'Email "%value%" already exists, did you forget your password?'
    );

    public function __construct(User_Service_Datasource $model)
    {
        $this->_model = $model;
    }

    public function isValid($value, $context=null)
    {
        $value = (string) $value;
        $this->_setValue($value);
        
        $user = $this->_model->getUserByEmail($value);
        if (false === $user) {
            return true;
        }

        $this->_error(self::EMAIL_EXISTS);
        return false;
    }
}

// Adding the validator inside your form

        $this->addElement('text', 'email', array(
            'filters'    => array('StringTrim', 'StringToLower'),
            'validators' => array(
                'EmailAddress',
                array('StringLength', false, array(1, 255)),
                array('UniqueEmail', false, array(new User_Service_Datasource)),
            ),
            'required'   => true,
            'label'      => 'signup_email',
            'decorators' => $this->elementDecorators
        ));

5. How to add a field for uploading files

This is how I typically create an Upload field :

<?php

// inside a form definition...

        $config = Zend_Registry::get('config');

        // adding a field for the user photo/picture
        $photoUploadDir = $config['files']['photoUploadDir'];
        $this->addElement('file', 'photo', array(
            'filters'    => array('StringTrim'),
            'validators' => array(
                array('StringLength', false, array(5, 50)),
                array('IsImage'),
                array('Upload'),
            ),
            'required'   => true,
            'label'      => 'Your Photo:',
            'destination' => $photoUploadDir,
            'decorators' => array(
                'File',
                array('Errors', array('class' => 'inlineError')),
                'Label',
                array(array('data' => 'HtmlTag'), array('tag' => 'li', 'class' => 'file'))
            ),
        ));

The main thing to remember is that the destination directory must exist and be writable.

6. How to add a recaptcha field

Instead of using an addElement() method, I create the captcha field manually, and then add it.

<?php
        public $bareDecorators = array(
            array('Errors', array('class' => 'inlineError')),
            array('Label', array('id' => 'captcha-input')),
            array(array('data' => 'HtmlTag'), 
                  array('tag' => 'li', 'class' => 'element')
            ),
            array(array('row' => 'HtmlTag'), array('tag' => 'li')),
        );


        $config = Zend_Registry::get('config');

        $recaptcha = new Zend_Service_ReCaptcha(
            $config['recaptcha']['publickey'], 
            $config['recaptcha']['privatekey']
        );
        
        $captcha = new Zend_Form_Element_Captcha('captcha', array(
            'label' => 'Please confirm you are human:',
            'captcha' => array(
                'captcha' => 'ReCaptcha',
                'service' => $recaptcha,
            ),
            'decorators' => $this->bareDecorators,
        ));

        $this->addElement($captcha);

That's all I could think of at this time. I'm sure there are other topics worth putting in a “recipe” compilation, so feel free to post a comment. ;)