Custom Validator Suite – Zend Framework

In Zend Framework, there are already tons of validators under Zend_Validate. In Zend_Form, you can attach validators, as a result, you can have a validator suite ready. However, I don’t use Zend_Form and I just don’t like how validations are used in Zend Framework. That’s why I’m creating my own custom validator suite that fits my style.

Mode of operation

This is how I usually process a submitted data via form POST.

  1. User submits form
  2. Controller extracts the posted data
  3. Validate each data that has validation rules (using simple if/else statement)
  4. Returns either boolean true or an array of error message and the name of the field to focus (first invalid field perhaps)
  5. Display the error messages
  6. Focus cursor on the first invalid field

If we are using Zend_Form, we can easily implement this, however, I don’t use Zend_Form, because I simply don’t like it. I’d like to put validations on the model than messing with the controller. Moreover, Zend_Form is too heavy and mostly not suited for our projects.

I’m really annoyed with the default error messages the Zend_Validate library has. Perhaps, my ultimate goal is to customize the error messages. Not translating it.

I created this validator suite with these goals in mind:

  • Validators are customizable and can be extended. I want my own error messages
  • Validators can validate one or more value (for example a pair of x and y coordinates)
  • Validators can refer another field (for example a password and a confirm password field)
  • A validator suite that can determine which fields have errors

The Codes

For me to bring this validation suite to other projects, I will put it into a single package which will be placed inside the library directory on Zend Framework directory structure. We can also put them somewhere else, as long as it is accessible via include path.

We will put all the validators and the validator suite under Dc/Validate. First, we will create a basic string validator. We will extend this validator for other string related validation rules. We will extend Zend_Validate_Abstract for our validation class Dc_Validate_String.

Dc_Validate_String (String.php)
Dc_Validate_String validates string using the minimum and maximum length rules. It has also support for setting encodings. It is based on Zend_Validate_StringLength. Some unnecessary parts are removed.

<?php

/**
 * Validation for regular strings
 * 
 * @author lysender
 */
class Dc_Validate_String extends Zend_Validate_Abstract
{
    const INVALID   = 'stringInvalid';
    const TOO_SHORT = 'stringTooShort';
    const TOO_LONG  = 'stringTooLong';

    protected $_invalid = self::INVALID;
    protected $_tooShort = self::TOO_SHORT;
    protected $_tooLong = self::TOO_LONG;
    
    /**
     * @var string
     */
    protected $_key = 'string';
    
    /**
     * Message displayed when the key is missing from the class
     * $_key value
     * 
     * @var string
     */
    protected $_missingKeyMessage = 'Validating string failed because of missing string key';
    
    /**
     * @var array
     */
    protected $_messageTemplates = array(
        self::INVALID   => "Value must be a string",
        self::TOO_SHORT => "Value must be greater than %min% character(s) long",
        self::TOO_LONG  => "Value must be less than %max% character(s) long"
    );

    /**
     * @var array
     */
    protected $_messageVariables = array(
        'min' => '_min',
        'max' => '_max'
    );

    /**
     * Minimum length
     *
     * @var integer
     */
    protected $_min = 0;

    /**
     * Maximum length
     *
     * If null, there is no maximum length
     *
     * @var integer|null
     */
    protected $_max;

    /**
     * Encoding to use
     *
     * @var string|null
     */
    protected $_encoding;

    /**
     * Sets validator options
     * $options is an array the either contains the following keys
     * min: minimum length
     * max: maximum length
     * encoding: encoding to use
     * key: the key of the value to ve validated
     *
     * @param  array $options
     * @return void
     */
    public function __construct(array $options = array())
    {
		foreach ($options as $opt => $val)
		{
			$key = '_' . $opt;
			if (isset($this->$key))
			{
				$func = 'set' . ucfirst($opt);
				$this->$func($val);
			}
		}
    }

    /**
     * Returns the key for the value
     * 
     * @return string
     */
    public function getKey()
    {
    	return $this->_key;
    }
    
    /**
     * Sets the key for the value
     * 
     * @param $key
     * @return this
     */
    public function setKey($key)
    {
    	if (!empty($key) && is_string($key))
    	{
    		$this->_key = $key;
    	}
    	return $this;
    }
    
    /**
     * Returns the min option
     *
     * @return integer
     */
    public function getMin()
    {
        return $this->_min;
    }

    /**
     * Sets the min option
     *
     * @param  integer $min
     * @throws Zend_Validate_Exception
     * @return this
     */
    public function setMin($min)
    {
        if (null !== $this->_max && $min > $this->_max)
        {
            throw new Zend_Validate_Exception("The minimum must be less than or equal to the maximum length, but $min >"
                                            . " $this->_max");
        }
        $this->_min = max(0, (integer) $min);
        return $this;
    }

    /**
     * Returns the max option
     *
     * @return integer|null
     */
    public function getMax()
    {
        return $this->_max;
    }

    /**
     * Sets the max option
     *
     * @param  integer|null $max
     * @throws Zend_Validate_Exception
     * @return this
     */
    public function setMax($max)
    {
        if (null === $max)
        {
            $this->_max = null;
        }
        else if ($max < $this->_min)
        {
            throw new Zend_Validate_Exception("The maximum must be greater than or equal to the minimum length, but "
                                            . "$max < $this->_min");
        }
        else
        {
            $this->_max = (integer) $max;
        }

        return $this;
    }

    /**
     * Returns the actual encoding
     *
     * @return string
     */
    public function getEncoding()
    {
        return $this->_encoding;
    }

    /**
     * Sets a new encoding to use
     *
     * @param string $encoding
     * @return this
     */
    public function setEncoding($encoding = null)
    {
        if ($encoding !== null)
        {
            $orig   = iconv_get_encoding('internal_encoding');
            $result = iconv_set_encoding('internal_encoding', $encoding);
            if (!$result)
            {
                throw new Zend_Validate_Exception('Given encoding not supported on this OS!');
            }

            iconv_set_encoding('internal_encoding', $orig);
        }

        $this->_encoding = $encoding;
        return $this;
    }

    /**
     * Defined by Zend_Validate_Interface
     *
     * Returns true if and only if the string length of $value is at least the min option and
     * no greater than the max option (when the max option is not null).
     *
     * @param  string $value
     * @return boolean
     */
    public function isValid($value)
    {
    	if (!isset($value[$this->_key]))
    	{
    		throw new Zend_Validate_Exception($this->_missingKeyMessage);
    	}
    	
        if (!is_string($value[$this->_key]))
        {
            $this->_error($this->_invalid);
            return false;
        }

        $this->_setValue($value[$this->_key]);
        if ($this->_encoding !== null)
        {
            $length = iconv_strlen($value[$this->_key], $this->_encoding);
        }
        else
        {
            $length = iconv_strlen($value[$this->_key]);
        }

        if ($length < $this->_min)
        {
            $this->_error($this->_tooShort);
        }

        if (null !== $this->_max && $this->_max < $length)
        {
            $this->_error($this->_tooLong);
        }

        if (count($this->_messages))
        {
            return false;
        }
        else
        {
            return true;
        }
    }
}

Each validator accept an array as parameter for the isValid() method. The array is an associative array, keys are the field name. This enables the validator to refer another field. To use the validator above, we will write:

$v = new Dc_Validate_string();
$data = ANY_ARRAY_SOURCE; // could be a filtered post
$check = $v->isValid($data);
if (!$check)
{
    var_dump($v->getMessages());
}

Note that getMessages() method is defined by the Zend_Validate_Abstract. Now, it is time to create a real validator. For example we have a field called name. Name must be between 1-60 characters for example. We will extend Dc_Validate_String to implement the rules. This time, our class will become shorter.

<?php 

/**
 * Validator for given name
 * 
 * @author lysender
 */

class Dc_Validate_Name extends Dc_Validate_String
{
    const INVALID   = 'nameInvalid';
    const TOO_SHORT = 'nameTooShort';
    const TOO_LONG  = 'nameTooLong';
    
	protected $_invalid = self::INVALID;
	protected $_tooShort = self::TOO_SHORT;
	protected $_tooLong = self::TOO_LONG;
	
    /**
     * @var string
     */
    protected $_key = 'name';
    
    /**
     * Message displayed when the key is missing from the class
     * $_key value
     * 
     * @var string
     */
    protected $_missingKeyMessage = 'Validating name failed because of missing name key';
    
    /**
     * @var array
     */
    protected $_messageTemplates = array(
        self::INVALID   => "Name must be a string",
        self::TOO_SHORT => "Name must be between %min% to %max% character(s) long",
        self::TOO_LONG  => "Name must be between %min% to %max% character(s) long"
    );

    /**
     * @var array
     */
    protected $_messageVariables = array(
        'min' => '_min',
        'max' => '_max'
    );

    /**
     * Minimum length
     *
     * @var integer
     */
    protected $_min = 1;

    /**
     * Maximum length
     *
     * @var integer
     */
    protected $_max = 60;
}

As you’ve noticed, only the error messages and the min/max rules are changed. That’s just the way it is. If we have several forms that needs to validate a name, we will use this validator. To use this:

$v = new Dc_Validate_Name;
$data = ANY_ARRAY_SOURCE; //could be POST
$check = $v->isValid($data);
if (!$check)
{
    var_dump($v->getMessages();
}

Now look, what if we want to validate a username for registration. A usename must be between 6-16 characters long and must be alpha numeric and underscores only. We can achieve this by extending Dc_Validate_Name and override isValid method so that it will validate the value as name first, then as username. For example as Dc_Validate_String_Username

public function isValid(array $value)
{
    if (!parent::isValid($value))
    {
        return false;
    }

    // you alpha numeric underscore validations here
}

If you want to validate username against a database, for example a username must be unique, then we will just create a new validator, extend Dc_Validate_String_Username and apply another rule. For example as Dc_Validate_Model_UserExists

public function isValid(array $value)
{
    if (!parent::isValid($value))
    {
        return false;
    }

    // your rule that checks if the username already exists
}

Beautiful isn’t it? At a cost of having a lot of files to load thus a performance issue especially if there are tons of fields to validate.

Next we will add another basic validator to validate integers.

<?php

/**
 * Number validator
 *
 * @author lysender
 */
 
class Dc_Validate_Num extends Zend_Validate_Abstract
{
    const NOT_NUMERIC  	= 'numNotNumeric';
    const TOO_SMALL		= 'numTooSmall';
    const TOO_LARGE 	= 'numTooLarge';
    
    protected $_notNumeric = self::NOT_NUMERIC;
    protected $_tooSmall = self::TOO_SMALL;
    protected $_tooLarge = self::TOO_LARGE;
    
    /**
     * Array key for the num value
     * @var string
     */
    protected $_key = 'num';
    
    /**
     * @var string
     */
    protected $_missingKeyMessage = 'Validating num failed because of missing num key';
    
    /**
     * @var int
     */
    protected $_min = 1;
    
    /**
     * @var int
     */
    protected $_max = 99999;
    
    /**
     * Validation failure message template definitions
     *
     * @var array
     */
    protected $_messageTemplates = array(
        self::NOT_NUMERIC   => "Must be a number",
        self::TOO_SMALL		=> "Number must be between %min% and %max%",
        self::TOO_LARGE		=> "Number must be between %min% and %max%"
    );

    /**
     * Additional variables available for validation failure messages
     *
     * @var array
     */
    protected $_messageVariables = array(
        'min' => '_min',
        'max' => '_max'
    );
    
    /**
     * Sets default option values for this instance
     *
     * @param array $options
     * 
     * @return void
     */
    public function __construct(array $options = array())
    {
		foreach ($options as $opt => $val)
		{
			$key = '_' . $opt;
			if (isset($this->$key))
			{
				$this->$key = $val;
			}
		}
		
		if ($this->_min > $this->_max)
		{
			throw new Zend_Validate_Exception('Minimum must not be greater than maximum.');
		}
    }

    /**
     * Defined by Zend_Validate_Interface
     *
     * Returns true if and only if $value's num value is a valid number
     * $value must be an associative array containing num key
     *
     * @param  $value
     * @return boolean
     */
    public function isValid($value)
    {
    	if (empty($this->_key))
    	{
    		throw new Exception($this->_missingKeyMessage);
    	}
    
    	if (!is_array($value) || !isset($value[$this->_key]))
    	{
    		$this->_error($this->_missing);
    		return false;
    	}
    	
    	$this->_setValue($value[$this->_key]);

    	if (!is_numeric($value[$this->_key]))
    	{
    		$this->_error($this->_notNumeric);
    		return false;
    	}
    	
    	if ($value[$this->_key] < $this->_min)
    	{
    		$this->_error($this->_tooSmall);
    		return false;
    	}
    	
    	if ($value[$this->_key] > $this->_max)
    	{
    		$this->_error($this->_tooLarge);
    		return false;
    	}

        return true;
    }
}

The purpose of Dc_Validate_Num is to validate if a value is a number and is between a range. Next, we will extend it to create a validator for order count. Order count must be a number between 1-126.

<?php

/**
 * Number validator
 *
 * @author lysender
 */
 
class Dc_Validate_OrderCount extends Dc_Validate_Num
{
    const NOT_NUMERIC  	= 'orderCountNotNumeric';
    const TOO_SMALL		= 'orderCountTooSmall';
    const TOO_LARGE 	= 'orderCountTooLarge';
    
    protected $_notNumeric = self::NOT_NUMERIC;
    protected $_tooSmall = self::TOO_SMALL;
    protected $_tooLarge = self::TOO_LARGE;
	
    /**
     * Array key for the num value
     * @var string
     */
    protected $_key = 'order_count';
    
    /**
     * @var string
     */
    protected $_missingKeyMessage = 'Validating order count failed because of missing order count key';
    
    /**
     * @var int
     */
    protected $_min = 0;
    
    /**
     * @var int
     */
    protected $_max = 126;
    
    /**
     * Validation failure message template definitions
     *
     * @var array
     */
    protected $_messageTemplates = array(
        self::NOT_NUMERIC   => "Order count must be a valid number",
        self::TOO_SMALL		=> "Order count must be between %min% and %max%",
        self::TOO_LARGE		=> "Order count must be between %min% and %max%"
    );

    /**
     * Additional variables available for validation failure messages
     *
     * @var array
     */
    protected $_messageVariables = array(
        'min' => '_min',
        'max' => '_max'
    );
}

They behaves just like our Dc_Validate_String example. Now, the most important part is the validator suite. We will call it Dc_Validate. It’s purpose is to create a validator suite that will allow the developer to add a validator against a field. Run all validations at once and extract error messages pointing to the field that has the errors.

<?php 

/**
 * Validator suite for forms and other validation
 * concerns
 * 
 * @author lysender
 */
class Dc_Validate
{
   /**
     * Validator chain, each validator contains the instance,
     * the key for the field to be valdiated and the 
     * breakChainOnFailure flag
     *
     * @var array
     */
    protected $_validators = array();

    /**
     * Array of validation failure messages
     * Each message contains the key of the field validated
     * and the list of messages
     *
     * @var array
     */
    protected $_messages = array();

    /**
     * Adds a validator to the end of the chain
     *
     * If $breakChainOnFailure is true, then if the validator fails, the next validator in the chain,
     * if one exists, will not be executed.
     *
     * @param  string                  $field
     * @param  Zend_Validate_Interface $validator
     * @param  boolean                 $breakChainOnFailure
     * @return Dc_Validate Provides a fluent interface
     */
    public function addValidator($field, Zend_Validate_Interface $validator, $breakChainOnFailure = false)
    {
        $this->_validators[] = array(
        	'field' => $field,
            'instance' => $validator,
            'breakChainOnFailure' => (boolean) $breakChainOnFailure
            );
        return $this;
    }

    /**
     * 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
     * @return boolean
     */
    public function isValid($value)
    {
        $this->_messages = array();
        $result = true;
        foreach ($this->_validators as $element)
        {
            $validator = $element['instance'];
            if ($validator->isValid($value))
			{
                continue;
            }
            $result = false;
            $messages = array('field' => $element['field'], 'messages' => $validator->getMessages());
            $this->_messages[] = $messages;
            if ($element['breakChainOnFailure'])
			{
                break;
            }
        }
        return $result;
    }

    /**
     * Returns array of validation failure messages
     *
     * @return array
     */
    public function getMessages()
    {
        return $this->_messages;
    }
    
    /**
     * Returns true if and only if there are messages
     * on the validator
     * 
     * @return boolean
     */
    public function hasMessages()
    {
    	return !empty($this->_messages);
    }
    
    /**
     * Merges messages into a single dimensional array or
     * messages. The messages must be compatible with 
     * Dc_Validate for it to work
     * 
     * @param array $messages
     * @return array
     */
    public static function mergeMessages(array $messages)
    {
    	if (!empty($messages))
    	{
    		$result = array();
    		foreach ($messages as $msg)
    		{
    			foreach ($msg['messages'] as $fieldMessage)
    			{
    				$result[] = $fieldMessage;
    			}
    		}
    		return $result;
    	}
    	return false;
    }
    
    /**
     * Returns an array containing the first message and the
     * field name where the message came from
     * 
     * @param array $messages
     * @return array
     */
    public static function getFirstMessage(array $messages)
    {
    	if (!empty($messages))
    	{
    		$result = array();
    		$firstMessage = reset($messages);
    		
    		if (!isset($firstMessage['field']) || !isset($firstMessage['messages']))
    		{
    			return false;
    		}
    		$result['field'] = $firstMessage['field'];
    		
    		$innerMessage = reset($firstMessage['messages']);
    		if (empty($innerMessage))
    		{
    			return false;
    		}
    		
    		$result['message'] = $innerMessage;
    		
    		return $result;
    	}
    	return false;
    }
}

It has two static methods, mergeMessages and getFirstMessage which deals with error messages. This is the class that we will use in the controller or model or wherever we want to put it.

For example we will put it on a controller just for testing and we will provide a sample post data. We write:

        $validator = new Dc_Validate;
        $validator->addValidator('name', new Dc_Validate_Name);
        // above will add a validator for the field "name"

        $validator->addValidator('username', new Dc_Validate_Name(
            array(
                'key' => 'username',
                'min' => 4,
                'max' => 16,
                'encoding' => 'utf-8'
            )
        ));

        // above provides a parameter to the name validator to customize its rules, instead of extending and creating a new class.

        $validator->addValidator('order_count', new Dc_Validate_OrderCount);
        $validator->addValidator('order_count_other', new Dc_Validate_OrderCount(
            array(
                'key' => 'order_count_other',
                'min' => 1,
                'max' => 10
            )
        ));
        // this time, number validators

        $data = array(
                    'name' => '',
                    'username' => 'dc',
                    'order_count' => 'abc',
                    'order_count_other' => 100
                );
        // sample data to be validated, imagine this is a $_POST variable

        if ($validator->isValid($data))
        {
            echo '<h2>Valid!';
        }
        else
        {
            $msg = $validator->getMessages();
            var_dump(Dc_Validate::mergeMessages($msg));
            
            echo '<hr />';
            echo '<h2>First message</h2>';
            var_dump(Dc_Validate::getFirstMessage($msg));
        }

        // now we used the validator suite.

So if we need more validators, we will just create more and make sure it is compatible with Dc_Validate.

Below is the sample output with xdebug enabled.

Get the full code on github: http://github.com/lysender/zf-custom-validator.

This entry was posted in Programming, Zend Framework and tagged , , , , . Bookmark the permalink.

Related Posts

2 Responses to Custom Validator Suite – Zend Framework

  1. Nice information, I really appreciate the way you presented.Thanks for sharing..

  2. Zielons says:

    Good work, that realy helps me

Leave a Reply

Your email address will not be published. Required fields are marked *