Page 1 of 2

Zend_Validate and Zend_Filter done right

Posted: Tue Sep 11, 2007 9:38 pm
by Luke
OK, so I'd like some help with a small addition to the Zend Framework that will do Filter / Validation correctly. I mean, I don't plan on inclusion in the framework or anything, but their validation / filtering sucks, so I'm fixing it for my own purposes. If you like it, feel free to use it. I'm trying to come up with some common use cases. Here is what I have so far. Any suggestions?

Within a Zend Controller:

Code: Select all

<?php
$request = $this->getRequest();

$data = new MC2_Input($request->getPost());

$filter = new MC2_Input_Filter_Set();
$filter->addFilter(new Zend_Filter_StringTrim()); // applies to all
$filter->addFilter(new Zend_Filter_Digits(), array('age', 'bees_eaten', 'some_element'));

$data->addFilter(new Zend_Filter_StringTrim());
$data->addFilter($filter);

$ruleset1 = new MC2_Input_Validate_Set();
$data->addValidator(new Zend_Validate_StringLength(1, 3), 'username', 'This field must be 1 to 3 characters in length');
$data->addValidator(new Zend_Validate_Alnum(), array('username', 'amount_of_monkeys'), 'This field must be alphanumeric');

$ruleset2 = new MC2_Input_Validate_Set();
$data->addValidator(new MC2_Input_Validator_conditionalTermsConditions());

$data->addValidator($ruleset1);
$data->addValidator($ruleset2);
$data->addValidator(new Zend_Validate_StringLength(6, 35), 'password', 'Password must be between 6 and 35 characters');

$data->filter();

if ($data->isValid())
{
    echo "Good job!";
}
else
{
    $errors = $data->getErrors();
    $username_errors = $data->getErrors('username');
    $age_and_chinese_people_errors = $data->getErrors(array('age', 'amount_of_monkeys'));
}

Posted: Tue Sep 11, 2007 10:12 pm
by wei
a suggestion, break up responsibility of chaining filters and validators from the processor class. e.g.

Code: Select all

//one validator only
$processor->setValidator(new Validator1());

//custom validation grouping, conditional validation, etc.
$valGroup = new MyCustomValidationGroup();
$valGroup->Dependencies[] = new Validator1();
$valGroup->Dependencies[] = new Validator2();

$processor->setValidator($valGroup);

if(!$processor->isValid())
{
     var_dump($processor->getErrorMessage());
}
the main point is that it permits custom conditional validation, groupings, group within groups, etc. i.e. the Composite pattern.

you may want to do it similarly for the filters for consistency (a little over kill may be).

Wei.

Posted: Tue Sep 11, 2007 11:29 pm
by Luke
That looks very interesting. Can you provide me with a situation where this would come in handy so that I may better understand your reasoning?

Posted: Wed Sep 12, 2007 12:10 am
by wei
e.g. conditional validation base on some value

Code: Select all

class ConditionalTermsConditions extends BaseValidator
{
    protected function evaluateIsValid()
    {
         if($this->Request['checkbox2'] === 'something')
              $val = new RequiredFieldValidator('checkbox1');
         else
              $val = new SomeOtherValidator();
         return $val->isValid();
    } 
}
chained validators (an example of Composite pattern), for simplicity, just use an array

Code: Select all

class ChainedValidator extends BaseValidator
{
     public $Dependencies=array();
    
     protected function evaluateIsValid()
     {
          $valid = true;
          foreach($this->Dependencies as $val)
                 $valid = $valid && $val->isValid();
          return $valid;
     }
}
a base class is not necessary, use an interface if you like, or just duck type (e.g. assume isValid() is defined)

what you get is a succinct validator class that does exact 1 thing. From there you can build some commonly used validators.

Posted: Wed Sep 12, 2007 12:33 am
by Christopher
The basic design is to have polymorphic Filters and Rules -- and then implement Validator and Filter chains to run those Filters and Rules on a container (which is really the Request). The goal of the standard interfaces is to make the the system easily infinitely extensible.

Traditionally the Rules have ab isValid() method as does the Validator (so it in turn can be used as a Rule). Filters have a doFilter() (or run() or execute()) methods.

The goals of the Rules and Filters are different. Filters modify the container passed through them. Rules provide both a pass/fail status (isValid()) and an error message that usually is displayed for the user.

I don't think FilterChain/Filters and Validator/Rules should be mixed. This is one weird god class thing about Zend_Filter_Input. They are separate systems and they are not usually needed in the same place in the code.

Posted: Wed Sep 12, 2007 1:20 am
by Luke
wei, forgive my ignorance. It is late and I am not at my best. I think I am seeing some of the benefits in what you are suggesting, but I could use a bit more elaboration. Please use as realistic of an example as you can think of (if you dont mind). Thank you very much.

arborint,
The basic design is to have polymorphic Filters and Rules -- and then implement Validator and Filter chains to run those Filters and Rules on a container (which is really the Request). The goal of the standard interfaces is to make the the system easily infinitely extensible.
I was planning on relying on the Zend_Validate_Abstract class to provide my interface for validators since this code is meant to be used with the Zend Framework and its built-in validators. The only part I planned on really replacing was Zend_Filter_Input
The goals of the Rules and Filters are different. Filters modify the container passed through them. Rules provide both a pass/fail status (isValid()) and an error message that usually is displayed for the user.

I don't think FilterChain/Filters and Validator/Rules should be mixed. This is one weird god class thing about Zend_Filter_Input. They are separate systems and they are not usually needed in the same place in the code.
A very good point. I have updated my use cases.

Code: Select all

$request = $this->getRequest();

$filter = new MC2_Input_Filter($request->getPost());

$filter->addFilter('__ALL__', new Zend_Filter_StringTrim());
$filter->addFilter(array('age', 'bees_eaten', 'some_element'), new Zend_Filter_Digits());

$filtered_post = $filter->filter();

$validate = new MC2_Input_Validate($filtered_post);

$validate->addValidator('username', new Zend_Validate_StringLength(1, 3), 'This field must be 1 to 3 characters in length');
$validate->addValidator(array('username', 'amount_of_monkeys'), new Zend_Validate_Alnum(), 'This field must be alphanumeric');

if ($validate->isValid())
{
    echo "Good job!";
}
else
{
    $errors = $processor->getErrors();
    $username_errors = $processor->getErrors('username');
    $age_and_chinese_people_errors = $processor->getErrors(array('age', 'amount_of_monkeys'));
}

Posted: Wed Sep 12, 2007 1:31 am
by Luke
wei: is this kind of what you're getting at?

Code: Select all

$request = $this->getRequest();

$data = new MC2_Input($request->getPost());

$filter = new MC2_Input_Filter();
$filter->addFilter(new Zend_Filter_StringTrim()); // applies to all
$filter->addFilter(new Zend_Filter_Digits(), array('age', 'bees_eaten', 'some_element'));

$data->addFilter($filter);

$validator1 = new MC2_Input_Valitate();
$validator1->addValidator(new Zend_Validate_StringLength(1, 3), 'username', 'This field must be 1 to 3 characters in length');
$validator1->addValidator(new Zend_Validate_Alnum(), array('username', 'amount_of_monkeys'), 'This field must be alphanumeric');

$validator2 = new MC2_Input_Validate();
$validator2->addValidator(new MC2_Input_Validator_conditionalTermsConditions());

$data->addValidator($validator1);
$data->addValidator($validator2);

$data->filter();

if ($data->isValid())
{
    echo "Good job!";
}
else
{
    $errors = $data->getErrors();
    $username_errors = $data->getErrors('username');
    $age_and_chinese_people_errors = $data->getErrors(array('age', 'amount_of_monkeys'));
}

Posted: Wed Sep 12, 2007 1:49 am
by wei
my apologies for the brief explainations. here is a working snippet.

Code: Select all

<?php

class ValidationManager
{
	var $Request;
	var $Validator;

	function IsValid()
	{
		if($this->Validator!==null)
		{
			$this->Validator->Request=$this->Request;
			return $this->Validator->IsValid();
		}
		return true;
	}
}

abstract class BaseValidator
{
	var $Request;

	function IsValid()
	{
		return $this->evaluateIsValid();
	}

	abstract protected function evaluateIsValid();
}

class RequiredFieldValidator extends BaseValidator
{
	private $_field;

	function __construct($fieldID)
	{
		$this->_field=$fieldID;
	}

	protected function evaluateIsValid()
	{
		if(isset($this->Request[$this->_field]))
			return trim($this->Request[$this->_field]) !== "";
		return false;
	}
}

class ChainedValidator extends BaseValidator
{
	var $Dependencies=array();

	protected function evaluateIsValid()
	{
		$valid=true;
		foreach($this->Dependencies as $val)
		{
			$val->Request=$this->Request;
			$valid = $valid && $val->IsValid();
		}
		return $valid;
	}
}

class MyConditionalValidator extends BaseValidator
{
	protected function evaluateIsValid()
	{
		$checked = isset($this->Request['checkbox'])
				&& intval($this->Request['checkbox']) === 1;
		if($checked)
			$val = new RequiredFieldValidator('textbox1');
		else
			$val = new RequiredFieldValidator('textbox2');
		$val->Request = $this->Request;
		return $val->IsValid();
	}
}


//builder function
function validate($Request)
{
	$validation = new ValidationManager();

	$validation->Request = $Request;

	$group = new ChainedValidator;
	$group->Dependencies[] = new RequiredFieldValidator('textbox3');
	$group->Dependencies[] = new MyConditionalValidator;

	$validation->Validator = $group;

	return $validation->IsValid();
}

$_POST['checkbox'] = "1";
$_POST['textbox1'] = "";
$_POST['textbox2'] = "";
$_POST['textbox3'] = "";

var_dump(validate($_POST)); //false;

$_POST['checkbox'] = "1";
$_POST['textbox1'] = "hello";
$_POST['textbox2'] = "";
$_POST['textbox3'] = "world";

var_dump(validate($_POST)); //true	;

?>
The "MyConditionalValidator " class can contain funky complicated business logic. the validate() function just builds the validator.

Posted: Wed Sep 12, 2007 11:05 am
by Luke
Take a look at the following lines in my code above:

Code: Select all

$validator2 = new MC2_Input_Validate();
$validator2->addValidator(new MC2_Input_Validator_conditionalTermsConditions());
Wouldn't that do essentially what you're asking about?

(I have edited my original post to include the most updated use case. Please refer to the initial post for use case changes from this post forward)

Posted: Wed Sep 12, 2007 11:29 am
by John Cartwright
I prefer to have a generic input class to which you can apply the filters and rules using a common method, and having it decide how to apply filters/rules to the objects. I.e. filters are only run after the validation has occured unless otherwise specified. I've included a snipplet above which demonstrates how to handle the ordering of how you want to apply your filter and rules, however I've left a couple things out as I don't have my codebase infront of me and simply cannot remember how I handled errors and such. Hope this helps in some way..

Code: Select all

$data = new Zodiac_Input($this->_request->getPost()); 
$data->add('name', new Zodiac_Input_Filter_Trim(), Zodiac_Input::PreValidation); //before validation
$data->add('age', new Zodiac_Input_Filter_Digits()); //after validation (default behavior)

$data->add('name', new Zodiac_Input_Rule_Length(1, 3), 'This field must be 1 to 3 characters in length');
$data->add('age', new Zodiac_Input_Rule_Alnum(), 'This field must be alphanumeric');
$data->add('password', new Zodiac_Input_Rule_Equals('password2'), 'Passwords do not match..');

Code: Select all

public function add($name, $object, $option) 
{
   /** 
    * Organize the input/filter objects so we can run them in the correct order
   */
   if ($object instanceof Zodiac_Input_Filter) {
      if ($option === true) {
         $this->_dispatch['pre']['filter'][$name] = $object;
      }
      else {
         $this->_dispatch['post']['filter'][$name] = $object;
      }
   } 
   elseif ($object instanceof Zodiac_Input_Rule) {
      $this->_dispatch['pre']['rule'][$name] = $object;
   }
}
You should search for some of my posts on chain validation, as Maugrim or Arbortint really had some good advise for me.. especially creating rule/filter sets using YAML which would then automatically be parsed into this validation library.

Posted: Wed Sep 12, 2007 11:44 am
by Luke
Thanks jcart! I am not sure I like the common method for adding filters and validators though. I think I would rather keep them seperate. What is the benefit here?

Also, does anybody else see the benefit of the composite structure wei is suggesting? I am starting to like the idea (look at my new use case on the first post). Let me know what you think of the new use case, jcart! Thanks.

Posted: Wed Sep 12, 2007 12:02 pm
by John Cartwright
The Ninja Space Goat wrote:Thanks jcart! I am not sure I like the common method for adding filters and validators though. I think I would rather keep them seperate. What is the benefit here?
The benefit is you do not have to specify whether it is a validator or a filter, and why should you really? .. thus avoiding

Code: Select all

$filter = new Input_Filter();
$filter->add(.. );

$validator = new Input_Validator();
$validator->add(.. );

$input = new Input();
$input->add($filter, $validator);
You still have to pass your filter and validator objects to your input object so the benefit here is having things handled internally. By nature a chain library validator/filter involves a lot of keystroke, so I personally like to keep the interface as small as possible.
Also, does anybody else see the benefit of the composite structure wei is suggesting?


Minus the function he used it looks good, but arn't we already using the composite pattern? :wink:
Let me know what you think of the new use case, jcart! Thanks.


Look at what? Sorry

Edit | Added some tidbits..

Posted: Wed Sep 12, 2007 12:57 pm
by Luke
Take a look at my first post and let me know what you think of the interface I have there. Too complicated. I am now considering one add method... but I'm not 100% convinced. I'm going to play around with it. Thanks for your input jcart... I really appreciate it.

Posted: Wed Sep 12, 2007 2:40 pm
by Luke
I need some help coming up with a term. I need a word that would describe both a Filter and a Validator. What would a term be that collectively describes filter and validator? Here is the context:

Code: Select all

abstract class MC2_Input_(term)_Abstract
{
    
}

class MC2_Input_Filter_Trim extends MC2_Input_(term)_Abstract implements MC2_Input_Filter_Interface
{
    
}

class MC2_Input_Validate_Alnum extends MC2_Input_(term)_Abstract implements MC2_Input_Validate_Interface
{
}

Posted: Wed Sep 12, 2007 3:25 pm
by John Cartwright
I would keep it simple as

Code: Select all

abstract class MC2_Input
{
}

class MC2_Input_Filter_Trim extends MC2_Input_Filter_Abstract implements MC2_Input_Filter_Interface
{
}

class MC2_Input_Validate_Alnum extends MC2_Input_Validate_Abstract implements MC2_Input_Validate_Interface
{
}