Decoupled validation with Zend_Form, Zend_Validate and Zend MVC

by jon on June 4, 2008

Lately I've been experimenting a lot with trying to serialize a form and send it over the wire as a means of doing asynchronous postback of partially validated forms.

I've come up with a little prototype that's a PHP version of an ASP.NET implementation I did a couple months ago for a project I was working on.

The PHP version was surprisingly easier to implement, due to the simplicity of Zend_Form when compared to the bulldozer we-control-everything-and-rewrite-your-user-control-id's approach of ASP.NET forms.

Essentially, I've created a class with a simple form (1 lookup list (one-to-many for the data modelers out there) and a couple of simple textboxes).

While I think that it's crucial to decouple business rules from the actual form implementation, I think that Zend_Validate validators, as well as custom validators, are an excellent way of creating testable, declaritive validation rules that can be applied to form elements as you see fit. The other decoupling you'll find in this example is from the postback model, essentially allowing you to drop the same form into any part of a Zend Framework application and posting to an Ajax Controller service for validation purposes. What's nice about this is that no business rules are left on the client. I've used prototype for a little styling.

The finishing touch in all of this is that I can leverage the server-side validation routine for both the asynchronous and synchronous calls, so that postback and validation run through the same validation code, essentially providing a second interface with to the same form object.

I'm going to look into extending this example to provide not only one-to-one mapping and one-to-many mapping (lookup lists), but also many-to-one mapping (generating sub-forms dynamically and performing partial validation). Not sure exactly how to do that, so comments would be greatly appreciated!

Here's a quick demonstration.

and now for the code. The webservice bit is all contained in the IndexController for the sake of brevity. In a full blown application, you would likely have a Controller (or several controllers) that would offer different asynchronous entry points into your MVC framework.

class IndexController extends Zend_Controller_Action
{
	function indexAction()
	{
 
		$this->view->f = new RegistrationForm();
		$this->view->setMode = "setMode($('wsTab').childNodes[0]);";
 
		if ($this->view->f->isValid($_POST))
			return $this->render("FormSuccess");
 
		else if($this->_request->isPost())
		{
			$this->view->errorElements = $this->view->f->getMessages();
			$this->view->setMode = "setMode($('pbTab').childNodes[0]);";
		}
 
	}
 
	/**
	 * Ajax JSON call is made through this controller
	 *
	 */
	function asyncAction()
	{
		//prepare a JSON response
		$this->_helper->viewRenderer->setNoRender();
		$this->_helper->getHelper('layout')->disableLayout();
 
     	//Map the form from the client-side call
     	$myFormData = Zend_Json::decode($this->getRequest()->getParam("fs") , Zend_Json::TYPE_ARRAY);
     	$form = new RegistrationForm();
     	$form->isValid($myFormData);
 
     	//return the result
        $this->getResponse()->setHeader('Content-Type', 'application/json')
							   ->setBody(Zend_Json::encode($form->getMessages()));
 
	}
 
}
 

You'll notice that I've pulled out the form into its own class (RegistrationForm):

class RegistrationForm extends Zend_Form
{
 
    public function __construct()
    {
    	$this->setAttrib("id" , "frm");
 
    	$onetomany = new Zend_Form_Element_Select('sad');
    	$onetomany->setLabel('One to many:')
    			  ->setMultiOptions($this->getOneToMany())
    			  ->addValidator(new Onetomany());
 
        $usernameRequiredValidator = new Zend_Validate_NotEmpty();
        $usernameRequiredValidator->setMessage("Please enter a username.");
 
        $usernameStringLengthValidator = new Zend_Validate_StringLength(8,20);
        $usernameStringLengthValidator->setMessage("Username is too short! (8 character minimum)", Zend_Validate_StringLength::TOO_SHORT);
        $usernameStringLengthValidator->setMessage("Username is too long! (less than 20 characters)", Zend_Validate_StringLength::TOO_LONG);
 
    	$username = new Zend_Form_Element_Text('username');
        $username->setLabel('Username:')
        		 ->addValidator($usernameRequiredValidator)
        		 ->addValidator($usernameStringLengthValidator)
        		 ->setRequired(true);
 
    	$name = new Zend_Form_Element_Text('name');
        $name ->setLabel('Name:')
        		 ->addValidator('StringLength', true, array(4, 20))
        		 ->setRequired(true);    	
 
        $password = new Zend_Form_Element_Password('password');
        $password->setLabel('password:');
 
        $this->addElements(array(
        	$onetomany,
            $username,
            $name,
            $password
 
        ));
 
        $this->setDecorators(array(
        	'FormElements',
            'Fieldset',
            'Form'
 
        ));
 
        foreach ($this->getElements() as $element)
        {
        	$element->setDecorators(array(
                     array('ViewHelper', array('class' => 'formText')),
                     array('Label', array('class' => 'label')),
            		 array('HtmlTag', array('tag' => 'dd'))
                 ));
 
			$element->class = 'formText';
 
        }
    }
 
    private function getOneToMany()
	{
		$ar[" "] = "please select";
		$ar["car"] = "car";
		$ar["rollerBlade"] = "roller blades";
		return $ar;
 
	}
 
}
 

In my post about enterprise validation, I described a scenario where you would create custom validators that could be applied declaratively in C# using the Enterprise Library. The same thinking was applied here with the One To Many by creating a custom validator. This allows you to test the validation (and your business rules) without thinking about the form, the UI or even session:

class Onetomany extends Zend_Validate_Abstract
{
    const NOT_VALID  = 'stringLengthTooLong';
 
    /**
     * @var array
     */
    protected $_messageTemplates = array(
        self::NOT_VALID => "one to many was not valid choice",
 
    );
 
    /**
     * @var array
     */
    protected $_messageVariables = array(
        'not_valid' => '_not_valid',
 
    );
 
    protected $_not_valid;
 
    public function __construct()
    {
 
    }
 
    /**
     * Returns the min option
     *
     * @return integer
     */
    public function getNotValid()
    {
        return $this->_not_valid;
    }
 
    /**
     * Defined by Zend_Validate_Interface
     * @param  string $value
     * @return boolean
     */
    public function isValid($value)
    {
 
		// more business rules would go somewhere here...
        if ($value == " ")
            $this->_error(self::NOT_VALID);
 
        if (count($this->_messages)) {
            return false;
        } else {
            return true;
        }
    }
 
}
 

The last bit of magic is the Javascript. My weapon of choice (as mentioned above) is prototype, however I'm sure you could do the same thing with any other javascript framework:

Event.observe(window , "load" , function()
{
		<?= $this->setMode ?>
});
function attachWebServiceObserver()
{
		var url = "/index/async/";
		var f = JSON.stringify($('frm').serialize(true));
		new Ajax.Request(url , {
			method: 'post',
			parameters: 'fs=' + f,
			onComplete : doAjaxCall.bindAsEventListener(this)
			});
}
function attachPostBackObserver()
{
	$('frm').submit(); //this ID comes from the RegistrationForm class
}
 
function setMode(e)
{
	$$('.tab').each(function(emt) { emt.setStyle( {background : "" }) });
	e.parentNode.setStyle({background :"#FFFF99"});
 
	if (!FIRST_LOAD)
		clearErrors();
 
	if (e.parentNode.id =="wsTab")
	{
		Event.observe('bt', 'click', attachWebServiceObserver);
		Event.stopObserving('bt', 'click', attachPostBackObserver);
		$('bt').value = "Submit Asynchronously";
	}
 
	if (e.parentNode.id =="pbTab" )
	{
		Event.stopObserving('bt', 'click', attachWebServiceObserver);
		Event.observe('bt','click', attachPostBackObserver);
		$('bt').value = "Submit With Postback";
	}
	FIRST_LOAD = false;
 
}
function doAjaxCall(tr)
{
	clearErrors();
 
	if (formIsValid(tr.responseJSON))
		$('frm').submit();
 
	displayErrorMessages(tr.responseJSON);	
 
}
function clearErrors()
{
	$('dvPostbackErrors').update();
	$('dvPostbackErrors').hide();
 
	$$(".errormessage").each(function(e)
	{
		Element.remove(e);
	});
 
	$$("input.formText ").each(function(e)
	{
		e.setStyle({border : "1px solid #CCC"});
	});
	$$("select.formText ").each(function(e)
	{
		e.setStyle({border : "1px solid #CCC"});
	});
 
}
function formIsValid(errorJson)
{
 
	if(isNaN(errorJson))
		return false;
 
	return true;
}
function displayErrorMessages(errorJson)
{
 
	for (formElement in errorJson)
	{
 
		var elementObj = eval("errorJson."+formElement);
 
		for (error in elementObj)
			displayError(formElement , eval("errorJson."+formElement+"."+error));
 
	}
}
function displayError(emt , msg)
{
	var tpl = "<span class='errormessage' >"+msg+"</span>";
 
	markInvalidTextbox(emt);
 
	Element.insert(emt , {"after" : tpl });
}
function markInvalidTextbox(emt)
{
	$(emt).setStyle({border: "2px solid red"});
}
 

In the version that you can download, I include the JSON.stringify class (courtesy of Crawford) to serialize the form and send it the AsyncAction in my IndexController.

This architectural approach allows you to keep all the validation rules server-side, while allowing you to pick and choose when and how you postback. Because this example works with both postback and asynchronous calls, it's a little verbose.

Regardless, you can download the whole project and try it out for yourself.

{ 8 comments… read them below or add one }

Willem September 6, 2008 at 5:11 am

Hi Jon,

The example is nice but how about crypting the password content? I think you dont want it as clear text on the line.

jon September 6, 2008 at 1:39 pm

Hi Willem,

good point! the cheapest way (but not the most secure) would probably be to pass the password field into a hidden form element after being encrypted using one-way md5 encryption with a salt. then the server would have to pick up the value and compare it / insert it into the database as is.

I’ll see about doing a follow-up post about encrypting strings in javascript.

Another way, and a better way would be to just use SSL encryption for all communication between the client and the server. However, even SSL certificates can be spoofed by curious governments with proxy servers.

Willem September 8, 2008 at 3:26 pm

Hi Jon,

I finally got this example workingdue to spare free time. Nice tutorial. Can you tell me about the autocompleter.js, where it comes from? It is not included in the zip.

jon September 8, 2008 at 3:35 pm

Hi Willem,

I think autocompleter is here: http://jon.lebensold.ca/creative-stuff/demo-ajax-autocompleter-with-scriptaculous-and-a-zend-json-web-service .

it seems that the original posting on the script.aculo.us wiki has disappeared.

Willem September 9, 2008 at 3:36 pm

Hi Jon,

Do you know a list of all default validator plugins?

jon September 9, 2008 at 6:25 pm

Hi Willem,

check out http://framework.zend.com/manual/en/zend.validate.set.html

however, using incorporating the Zend Framework into PDT / Zend Studio and just letting intellisense sort out everything in the Zend_Validate_* namespace. Since they all implement the same abstract class, they work very similarly.

Joost February 4, 2010 at 8:14 am

Hi Jon,

thank you very much for this fantastic tutorial. It was a helpful reference for connecting Zend_Validate with dojo!

Joost

David March 14, 2010 at 3:03 pm

Hi Jon

I have been a follower of your Zendcasts and I came across this site in my searches.
Its a great reference.

If you dont mind I would gladly appreciate if you could post the JQuery version of the javascript example.

Lots of thanks

Leave a Comment

Previous post:

Next post: