compared with
Version 47 by Bryce Lohr
on Feb 25, 2008 20:23.

Key
This line was removed.
This word was removed. This word was added.
This line was added.

Changes (234)

View Page History

{zone-data:proposer-list}
[Bryce Lohr|mailto:blohr@triad.rr.com] Lohr|mailto:bryce.lohr@gmail.com]
{zone-data}

{zone-data:revision}
0.23
{zone-data}

{zone-data:overview}
I'd like to propose This proposal is simply an alternative to the [Zend_Filter_Input|Zend_Filter_Input redesign - Bill Karwin] proposal. Bill Karwin and others have put a lot of time and energy into that work, and I'd like to take this opportunity to thank them. This work is built upon those, and I'd like to acknowledge that up front.

This proposal is inspired by Christopher Thompson's original [Zend_FilterChain|Zend_FilterChain Zend_Validator - Christopher Thompson] proposal, and provides a similar interface. This is quite a different tact than the array-based structure used by [Zend_Filter_Input|Zend_Filter_Input redesign - Bill Karwin]; some people may find this interface preferable.
This was originally inspired by Christopher Thompson's [Zend_FilterChain|Zend_FilterChain Zend_Validator - Christopher Thompson] proposal, and provides a vaguely similar interface. This takes quite a different tact than the array-driven structure used by [Zend_Filter_Input|Zend_Filter_Input redesign - Bill Karwin]; some people may find the code-driven interface preferable.

I actually propose quite a bit of change here. Here's a quick rundown to get started:
Here's a quick rundown of the major parts:
* Zend_Filter_Builder and Zend_Validate_Builder classes
* Facades providing a fluent interface for both Builder classes
* An Error Manager class to faciliate configuring error messages for many validators at once
* An Error Manager class to facilitate configuring error messages for many validators at once, as well as retrieving raised errors
{zone-data}

* Neither component handles any other aspect of form processing
* No mechanism for statically calling filters or validators is provided (out of scope)
* The solution in this proposal is not designed to make using the existing filters and validators more convenient for quickly checking one value; it's geared toward easily handling a lot of data at once
* Thanks to the pre-1.0 changes to the Zend_Validate_* classes with regards to error messages, this proposal works with the existing validation API as-is.
* This solution is not designed to make it easier to use single filters or validators; rather, it automates using many filters/validators together on whole sets of data
* This is compatible with the existing Zend_(Validate|Filter)_Interface API; i.e., existing Framework filters and validators will work without being modified
{zone-data}

Essentially, Zend_Validate_Builder and Zend_Filter_Builder use the Builder and Composite patterns to construct a recursive tree structure of Zend_Validate_* or Zend_Filter_* instances, which is then applied wholesale to an array of input data. In the case of Zend_Filter_Builder, the result is the original input array after having each of the constituent filters applied to each input array element. In the case of Zend_Validate_Builder, the result is a boolean value indicating whether any single element in the input data was deemed invalid by any of the constituent Zend_Validate_* instances.

In addition to the Builder classes, I there are also propose Zend_Validate_Builder_FluentFacade and Zend_Filter_Builder_FluentFacade. These classes provide a very elegant and easy to use fluent interface to the Builder classes, and their use is totally optional. The Builder classes in no way depend on these.

Error messages are a vital part of data validation. If your code decides that something the user entered is not acceptable, you must be able to tell the user why. To that end, I propose a Zend_Validate_Builder_ErrorManager class. This class provides a way for the Zend_Validate_* classes, Zend_Validate_Builder, and your application code to define and communicate errors. The Zend_Validate_* classes all have built-in error messages which, for a variety of reasons, are often not suitable to display directly to users. Therefore, the error messages for validators can be overridden. However, this can only be done on an individual, validator-by-validator basis. Zend_Validate_Builder_ErrorManager allows you to define all the error messages for any arbitrary set of validators, by data field. This way, you can easily set up each error message for a field, and also easily retreive any error messages by field for display.
Error messages are a vital part of data validation. If your code decides that something the user entered is not acceptable, you must be able to tell the user why. The Zend_Validate_Builder_ErrorManager class addresses this concern. It provides a way for the Zend_Validate_* classes, Zend_Validate_Builder, and your application code to define and communicate error messages. The Zend_Validate_* classes all have built-in error messages which, for a variety of reasons, are often not suitable to display directly to users. Therefore, the error messages for validators can be overridden. However, this must be done manually, on each individual instance with the built-in validators. Zend_Validate_Builder_ErrorManager allows you to define all the error messages for any arbitrary set of validators, by data field. This way, you can easily set up each error message for a field, and also easily retrieve any error messages by field for display.
{zone-data}

* Milestone 1: \[DONE\] Design interface and test feasibility
* Milestone 2: \[DONE\] Write proposal
* Milestone 3: \[IN PROGRESS\] \[DONE\] Gather feedback and revise design as necessary (done, but
more feedback is always welcomed)
* Milestone 4: \[IN PROGRESS\] \[DONE\] Develop full implementation and unit tests
* Milestone 5: Write documentation (this proposal actually _is_ the current documentation)
{zone-data}


{zone-data:use-cases}
For all of these use cases, assume that the code shown is extracted from a valid Action Controller method that is processing a form with the intention of saving the data to the database. Assume that methods referenced after {{$this->}} exist and are valid. In most of these examples, I refer to Zend_Validate_* classes that don't currently exist; for now assume that they do. I plan to add a separate proposal for some new validators that would work well with this proposal, as well as being useful in general.
For all of these use cases, assume that the code shown is extracted from a class that is processing a form, with the intention of saving the data to the database. Assume that methods referenced after {{$this->}} exist and are valid; variables such as {{$post}} pretty obviously refer to the user-submitted data. In most of these examples, I refer to Zend_Validate_* classes that don't exist in the Framework. Many of them are available in the Laboratory, along side ZVB and ZFB; as for the others--just assume they exist for now.

||UC-01||
{code}
$filter = new Zend_Filter_Builder;
$filter->addFilter('phone_no', new Zend_Filter_Digits);
$filter->addFilter('description', new Zend_Filter_StripTags);
$filter->add(new Zend_Filter_Digits, 'phone_no');
$filter->add(new Zend_Filter_StripTags, 'description');
$filteredPost = $filter->filter($unfilteredPost);
{code}
{code}
$validate = new Zend_Validate_Builder;
$validate->addValidator('phone_no', new Zend_Validate_IsNumeric);
$validate->addValidator('phone_no', new Zend_Validate_Between(1000000, 9999999));
$validate->addValidator('description', new Zend_Validate_StringLength(255), Zend_Validate_Buider::OPTIONAL);
$validate->add(new Zend_Validate_IsNumeric, 'phone_no');
$validate->add(new Zend_Validate_Between(1000000, 9999999), 'phone_no');
$validate->add(new Zend_Validate_StringLength(255), 'description', Zend_Validate_Buider::OPTIONAL);
if ($validator->isValid($post)) {
$this->saveData($post);
}
{code}
Here, the {{phone_no}} field is checked to make sure it's numeric, and {{description}} field is checked to make sure it's less than 255 characters long. Since the Zend_Validate_* classes will usually fail an empty value, all fields are required by default. The {{addValidator}} method takes an optional third argument, which are flags for that specific field. combination of field and validator. We're passing the OPTIONAL flag for the {{description}} field, so Zend_Validate_Builder will skip over it when it's empty, and the StringLength validator will never see it unless it has a value.

Note the {{phone_no}} example: the order the validators are added to the field is preserved; that is, the IsNumeric validator will be run before the Between validator. All the validators assigned to a field will always be executed. Therefore, a single field might generate several error messages, depending on how many validators it fails. Since these error messages are built up in the Error Manager object, your application code can decide which errors to display to the user; you need not display all of them.

||UC-02||
This demonstrates a little more advanced usage.

Globbing:
Pattern matching:
{code}
// Decode HTML entities and strip tags from all POST fields
// Zend_Filter_HtmlEntityDecode is in the Laboratory
$filter = new Zend_Filter_Builder;
$filter->addFilter('*', new Zend_Filter_HtmlEntitiesDecode);
$filter->addFilter('*', new Zend_Filter_StripTags);
$filter->add(new Zend_Filter_HtmlEntityDecode, '/./', Zend_Filter_Builder::PATTERN);
$filter->add(new Zend_Filter_StripTags, '/./', Zend_Filter_Builder::PATTERN);
$filteredPost = $filter->filter($post);

// Require all fields on the form to be Alpha-only characters
$validate = new Zend_Validate_Builder;
$validate->addValidator('*', new Zend_Validate_Alpha);
$validate->add(new Zend_Validate_Alpha, '/./', Zend_Validate_Builder::PATTERN);
if ($validate->isValid($filteredPost)) {
$this->saveData($filteredPost);
}

// Say we had a bunch of sequentially-numbered checkboxes starting with 'item_', and we wanted to make sure they all had {{int}} values:
$validate = new Zend_Validate_Builder;
$validate->add(new Zend_Validate_Int, '/^item_\d+$/', Zend_Validate_Builder::PATTERN);
if ($validate->isValid($formData)) {
$this->saveData($formData);
}
{code}
{info:title=Supported Meta-characters}
I currently only have support for the '\*' meta-character for globbing. However, it's fairly straight-forward to expand the support for more complex matching, including using regular expressions to pick which fields to match. In this implementation, using '\*' is semantically equivilent to manually specifying every field in the top level input array.
{info}
Specifying the PATTERN flag causes the Builder classes to interpret the field name argument as a PCRE regex to be matched against input field names. The given validator or filter is applied to each matching field.

Grouping:
Validating each field in a list:
{code}
// Filter out non-digit characters from three fields
$filter = new Zend_Filter_Builder;
$filter->addFilter(array('phone_no', 'ss_no', 'cc_no'), new Zend_Filter_Digits);
$filter->add(new Zend_Filter_Digits, array('phone_no', 'ss_no', 'cc_no'));
$filteredPost = $filter->filter($post);

// Make sure none of the address fields are longer than 100 characters
$validate = new Zend_Validate_Builder;
$validate->addValidator(array('address1', 'address2', 'address3'), new Zend_Validate_StringLength(100));
$validate->add(new Zend_Validate_StringLength(100), array('address1', 'address2', 'address3'));
if ($validate->isValid($filteredPost)) {
$this->saveData($filteredPost);
}
{code}
{info:title=Note}
Processing arrays of patterns (i.e., array in 2nd parm, with PATTERN flag in 3rd) is currently not supported. However, it is possible (and likely) to be added in the future.
{info}

||UC-03||
Password confirmation, catpcha validation, etc. This uses the (currently) fictional Zend_Validate_Equals, which validates that the input value is equal to the value given to the constructor. I propose that we add such a validator to the core distribution.

Password Confirmation:
{code}
$validator = new Zend_Validate_Builder;
// My_Password_Validator is a user-defined (i.e., non-ZF) class the app uses to validate passwords
$validator->addValidator('password1', new My_Password_Validator);
$validator->addValidator('password2', new Zend_Validate_Equals($post['password1']));
if ($validator->isValid($post)) {
$this->_addUser($post);
}
{code}

Catpcha validation:
{code}
$validator = new Zend_Validate_Builder;
$validator->addValidator('captcha', new Zend_Validate_Equals($session['captcha']));
if ($validator->isValid($post)){
$this->_addComment($post);
}
{code}

||UC-O4||
Since this follows the Composite pattern, you can nest validators to do some neat things. In this example, assume we're trying to validate a mailing address. Suppose, for organizational purposes, you structured the form so the mailing address was in a sub-array, something like this:

{code}
$outer = new Zend_Validate_Builder;
$outer->addValidator('name', new Zend_Validate_StringLength(50));
$outer->addValidator('email', new Zend_Validate_EmailAddress);
$outer->add(new Zend_Validate_StringLength(50), 'name');
$outer->add(new Zend_Validate_EmailAddress, 'email');

$string100 = new Zend_Validate_StringLength(100);

$inner = new Zend_Validate_Builder;
$inner->addValidator(array('addr1', 'city'), $string100);
$inner->addValidator(array('addr2', 'addr3'), $string100, Zend_Validate_Builder::OPTIONAL);
$inner->add($string100, array('addr1', 'city'));
$inner->add($string100, array('addr2', 'addr3'), Zend_Validate_Builder::OPTIONAL);
// Assume $states is a valid array of US states
$inner->addValidator('state', new Zend_Validate_InArray($states));
$inner->add(new Zend_Validate_InArray($states), 'state');
// Another custom validator for Zip codes
$inner->addValidator('zip', new My_Zipcode_Valiadator);
$inner->add(new My_Zipcode_Valiadator, 'zip');

// ZVB will pass the entire array in the 'address' field to the inner validator, which will operate just like the outer does normally.
$outer->addValidator('address', $inner);
$outer->add($inner, 'address');
if ($outer->isValid($post)) {
$this->saveData($post);
{code}

If you have a situation where the entire address is optional (therefore if any one field is given, they must all be valid; otherwise it's valid if they're all blank), you might be temped to pass the OPTIONAL flag to the {{address}} field. However, that won't work: when the OPTIONAL flag is set, the Builder checks the value of the current field, and if it's blank, the Builder never calls the validator. In this situation, the current field would be an array with six elements; even though each element could have an empty value, the array itself won't ever be considered an empty value. Therefore, the entire {{address}} field will always be validated against the sub-validator. In this situation, what you actually need is _conditionally_ optional fields, based on whether or not _other_ fields are valid, which requires completely different logic.
If you have a situation where the entire address is optional, you might be temped to pass the OPTIONAL flag to the {{address}} field. However, that won't work: when the OPTIONAL flag is set, the Builder checks the value of the current field, and if it's blank, the Builder never calls the validator. Here, the current field would be a six-element array, which will never be considered empty. The elements might be empty, but the array itself won't be. Therefore, the entire {{address}} field will always be validated against the sub-validator. In this situation, what you actually need is _conditionally_ optional fields, which requires completely different logic.

||UC-O4||
Password confirmation, catpcha validation, etc. This uses the PASS_GROUP flag, and a pair of similar validators called Zend_Validate_Equals and Zend_Validate_AllEqual. Zend_Validate_Equals takes a scalar value and compares it to a standard value passed to the constructor. Zend_Validate_AllEqual takes an array, and returns true if all of its elements equal each other. Both of these can be found in the Laboratory, and both optionally support strict type checking, which is false by default. The PASS_GROUP flag allows you to tell the Builder to pass all the fields named in the second parameter as a single array to the validator. This is handy for validators like Zend_Validate_AllEqual, which examine the contents of several fields in the input to decide if they are valid.

Password Confirmation:
{code}
$validator = new Zend_Validate_Builder;
// My_Password_Validator is a user-defined (i.e., non-ZF) class the app uses to validate passwords
$validator->add(new My_Password_Validator, 'password1');
$validator->add(new Zend_Validate_AllEqual, array('password1', 'password2'), Zend_Validate_Builder::PASS_GROUP);
if ($validator->isValid($post)) {
$this->_addUser($post);
}
{code}

Catpcha validation:
{code}
$validator = new Zend_Validate_Builder;
$validator->add('captcha', new Zend_Validate_Equals($_SESSION['captcha']));
if ($validator->isValid($post)){
$this->_addComment($post);
}
{code}

||UC-05||
In order to handle a situation like the optional address mentioned above, you'd need something like this. This uses another fictional validator called Zend_Validate_AllOrNone. It allows you to do some limited conditional requirement of fields: you pass it a set of fields, and if all of them are empty, or if all of them are filled, then it considers the set valid. If only some are filled, then it considers the set invalid. The example may clarify things a bit. I plan on adding this validator to a separate validators proposal, since it seems useful for a variety of situations.
In order to handle a situation like the optional address mentioned above, you'd need something like this. This uses another new validator called Zend_Validate_AllOrNone. It allows you to make some fields conditionally required. You pass it a set of fields, and will consider them valid under two distinct cases: if either all of them are empty, or if all of them are filled. If only some are filled, then it considers the set invalid. To get the most benefit from this, combine it with other validators. The example may clarify things a bit. This validator can be found in the Laboratory, with the Builder classes.

{code}
// Put all the conditionally required address fields into an array
// Required, in this sense, means that if any one value is given, they all must be given
$fields = array('address1', 'city', 'state', 'zip')
$reqd = array_intersect_key($post, array_flip($fields));
$string100 = new Zend_Validate_StringLength(100);

// In this example, the POST data doesn't have the nested array as in the previous use case it did previously
$validate = new Zend_Validate_Builder;
// This is where the magic happens: based on the set of fields passed to its constructor, AllOrNone will return
// valid if the entire set is either all empty values or all non-empty values. Otherwise, it returns invalid.
$validate->addValidator($fields, new Zend_Validate_AllOrNone($reqd));
$validate->add(new Zend_Validate_AllOrNone($reqd), $fields);
// After the AllOrNone check, other validators are used to check the validity of the specific fields
$validate->addValidator(array('address1', 'address2', 'address3', 'city'), $string100, Zend_Validate_Builder::OPTIONAL);
$validate->add($string100, array('address1', 'address2', 'address3', 'city'), Zend_Validate_Builder::OPTIONAL);
// Assume $states is a valid array of US states
$validate->addValidator('state', new Zend_Validate_InArray($states), Zend_Validate_Builder::OPTIONAL);
$validate->add(new Zend_Validate_InArray($states), 'state', Zend_Validate_Builder::OPTIONAL);
// Another custom validator for Zip codes
$validate->addValidator('zip', new My_Zipcode_Valiadator, Zend_Validate_Builder::OPTIONAL);
$validate->add(new My_Zipcode_Valiadator, 'zip', Zend_Validate_Builder::OPTIONAL);
{code}


||UC-06||
Here are some examples that demonstrate some of the neat things that can be done with custom validators that act as Decorators for other valiators. I may add some of these to a separate proposal for additional validators. Keep in mind that a Zend_Validate_Builder instance counts as a validator that can be decorated; if you're creative, you can probably come up with some really cool things to do with this.
Zend_Validate_Builder and Zend_Filter_Builder really promote composing together validators to do interesting things. Here are some examples that show some of neat things that can be done with custom "Decorator" validators. Keep in mind that a Zend_Validate_Builder instance counts as a validator that can be decorated; if you're creative, you can probably come up with some really cool things to do with this.

*Zend_Validate_Array*
Most validators take a scalar value to {{isValid()}}, and perform checks on that. {{Zend_Validate_Array}} takes another validator passed to the constructor, and an array value to {{isValid()}}. It will iterate over all the elements of the array, performing the given validator on each one. Obviously, you could nest these as much as you needed to, provided that you know the structure of the input array ahead of time.
Most validators only take a scalar value to {{isValid}}. {{Zend_Validate_Array}} turns an exisiting scalar validator into an array validator. You pass a validator to the constructor, and an array value to {{isValid}}. It will then iterate over all the elements of the array, performing the given validator on each one. Obviously, you could nest these as much as you needed to, provided that you know the structure of the input array ahead of time.
{code}
// Suppose the 'selected' field is an array of IDs, which were checkboxes in the UI. The user
// been tampered with.
$validate = new Zend_Validate_Builder;
$validate->addValidator('selected', new Zend_Validate_Array(new Zend_Validate_Int));
$validate->add(new Zend_Validate_Array(new Zend_Validate_Int), 'selected');
if ($validate->isValid($post)) {
$this->deleteSelected($post);
{code}

*Zend_Validate_Switch*
Acts like a data-driven {{switch}} statement. The constructor takes an associative array of validator instances. The field value passed to its {{isValid()}} method is used as the key to look up which validator to run in the array passed to the constructor. This way you can attach different validations to fields based on what the user selects at runtime.
*Zend_Validate_AtLeastN* and *Zend_Validate_AtMostN*
These two are decorating array validators. They expect an array passed to {{isValid}}, a validator instance passed to the constructor, and check that at least (or at most) _n_ elements in the array pass the validator. This works great with the PASS_GROUP flag when your input data doesn't already have an array in it.
{code}
// Say you have a search form with three mutually-exclusive search options (represented by radio
// buttons, for example): search by Item Id, Title, or Publish Date. You only want to validate
// the respective search boxes according to the type of search they selected. This also uses nested
// Zend_Validate_Builder instances.

// Remember, you could add a whole bunch of validators to each field, if needed
$valId = new Zend_Validate_Builder(array('searchId' => $post['searchId']));
$valId->addValidator('searchId', new Zend_Validate_Int);

$valTitle = new Zend_Validate_Builder(array('searchTitle' => $post['searchTitle']));
$valTitle->addValidator('searchTitle', new Zend_Validate_StringLength(255));

$valDate = new Zend_Validate_Builder(array('searchDate' => $post['searchDate']));
$valDate->addValidator('searchDate', new Zend_Validate_Date);

// Set up our array of validators
$valArray = array(
'id' => $valId,
'title' => $valTitle,
'date' => $valDate
);

$valdiate = new Zend_Validate_Builder;
$validate->addValidator('searchType', new Zend_Validate_InArray(array('id', 'title', 'date')));
$validate->addValidator('searchType', new Zend_Validate_Switch($valArray));

if ($validate->isValid($post)) {
$this->doSearch($post);
}
{code}

*Zend_Validate_AtLeastNofM* and *Zend_Validate_AtMostNofM*
These two are complements of each other, and do basically the same thing. Their constructors take three arguments: a threshold number, a total count, and a validator instance. Basically, they determine whether at least, or at most, _n_ fields of the _m_ total pass the given validator. It's easier to understand by seeing it:
{code}
// This example is pretty contrived, but bear with me. Assume there are seven fields. The user is
// required to choose at least any three of them, but they can choose no more than five.

$validate = new Zend_Validate_Builder;
$validate->addValidator($fields, new Zend_Validate_AtLeastNofM(3, 7, $isInt));
$validate->addValidator($fields, new Zend_Validate_AtMostNofM(5, 7, $isInt));
$validate->add(new Zend_Validate_AtLeastN(3, $isInt), $fields, Zend_Validate_Builder::PASS_GROUP);
$validate->add(new Zend_Validate_AtMostN (5, $isInt), $fields, Zend_Validate_Builder::PASS_GROUP);
if ($validate->isValid($post)) {
$this->saveData($post);
}
{code}
Granted, it's hard to come up with a real-world use-case for this, but it's a good example of the creative things you can do. Also remember that you don't have to always use these two together as I have here.
I haven't come up with a good, realistic example for this, but there's a lot of creative potential here. Also remember that you don't have to always use these two together as I have here. All three of these validators are in the Laboratory.

||UC-07||
I used to build forms that mimiced my table structure, with the ability to edit multiple records for multiple tables in one form. In this example, assume we want to validate a CRUD form for a simple contact table, but the user can add multiple contacts on the form (maybe with the help of some Javascript like WForms). in UI). The form fields are laid out in a three-dimensional array, where the first key is the table name, the second key is the row number, and the third key is the field name. It would result in post data that looks something like this:
{code}
$post = array(
);
{code}
Assume that the user actually entered several contacts, so the 'contacts' array would have several sub-arrays, one for each record, just like a DB query result set.

Here's how to set up a filter/validator that would process all of the records:
{code}
// The filter structure for the contact "record"
$filterContacts = new Zend_Filter_Builder;
$filterContacts->addFilter('*', new Zend_Filter_StringTrim);
$filterContacts->addFilter('*', new Zend_Filter_HtmlEntitiesDecode);
$fitlerContacts->addFilter('*', new Zend_Filter_StripTags);
$fitlerContacts->addFilter('phone_no', new Zend_Filter_Digits);
$filterContacts->add(new Zend_Filter_StringTrim, '/./', Zend_Filter_Builder::PATTERN);
$filterContacts->add(new Zend_Filter_HtmlEntityDecode, '/./', Zend_Filter_Builder::PATTERN);
$fitlerContacts->add(new Zend_Filter_StripTags, '/./', Zend_Filter_Builder::PATTERN);
$fitlerContacts->add(new Zend_Filter_Digits, 'phone_no');

// Use the Array filter to validate all the "records" in the set
$filterForm = new Zend_Filter_Builder;
// We don't actually have Zend_Filter_Array yet; it would be needed for this (would work just like Zend_Validate_Array mentioned earlier)
$filterForm->addFilter('contacts', new Zend_Filter_Array($filterContacts));
// Zend_Filter_Array is the Filter analog to Zend_Validate_Array (look in the Laboratory)
$filterForm->add(new Zend_Filter_Array($filterContacts), 'contacts');
$post = $filterForm->filter($post);

// The validator structure for the contact "record"
$valContacts = new Zend_Validate_Builder;
$valContacts->addValidator('name', new Zend_Validate_StringLength(50));
$valContacts->addValidator('phone_no', new Zend_Validate_IsNumeric));
$valContacts->addValidator('email', new Zend_Validate_EmailAddress);
$valContacts->addValidator('notes', new Zend_Validate_StringLength(2000), Zend_Validate_Builder::OPTIONAL);
$valContacts->add(new Zend_Validate_StringLength(50), 'name');
$valContacts->add(new Zend_Validate_IsNumeric), 'phone_no');
$valContacts->add(new Zend_Validate_EmailAddress, 'email');
$valContacts->add(new Zend_Validate_StringLength(2000), 'notes', Zend_Validate_Builder::OPTIONAL);

// Use the Array validator to validate all the "records" in the set
$valForm = new Zend_Validate_Builder;
$valForm->addValidator('contacts', new Zend_Validate_Array($valContacts));
$valForm->add(new Zend_Validate_Array($valContacts), 'contacts');
if ($valForm->isValid($post)) {
$this->saveData($post);

||UC-08||
Up to now, any field that failed a validator's test would result in the validator's built-in error message being reported. This shows how to use the Error Manager to define custom error messages and retrieve raised errors.
So where have the error messages from all these validators been going all this time? To the Error Manager! This shows how to use the Error Manager both to retrieve raised errors, and define custom error messages.

The {{Zend_Validate_Interface}} interface defines the methods {{getMessages}} and {{getErrors}}, which when used with the standard Zend_Validate_* classes, return either all the error messages raised by the validator, or the codes for the raised errors, respectively. Since {{Zend_Validate_Builder}} adheres to this interface, it implements these methods, which you can use to retrieve all the error messages that have been raised during the entire validation run. Internally, it delegates this work to the Error Manager; so for retrieving messages, you don't really ever need to directly touch it. You only need to access the Error Manager directly when you want to use it set up custom error messages, or want more control over which errors are returned.
The {{Zend_Validate_Interface}} interface defines the method {{getMessages}}, which returns an array of all the error messages raised by the validator, indexed by error code. {{Zend_Validate_Builder}} implements this method (albeit, with an incompatible return value), which you can use to retrieve all the error messages that have been raised during the entire validation run. Internally, it delegates this work to the Error Manager; so for retrieving messages, you don't need to touch it directly. You only need to access the Error Manager directly when you want to use it set up custom error messages, or want more control over which errors are returned.

*Retreiving errors*
Normally, calling {{getMessages}} on a validator returns a flat array of error messages. But because the Builder manages a whole collection of fields and validators, it's return value is slightly different. It returns a three-dimensional array, indexed by field name, validator class name, and failure reason code. Say the user entered a value in the {{name}} field that was too short:

{code}
$userInput = array(
'name' => 'bal'
);

$zvb = new Zend_Validate_Builder;
$zvb->add(new Zend_Validate_StringLength(4), 'name');
if (!$zvb->isValid($userInput)) {

// Retrieve the error message
$messages = $zvb->getMessages();

// $messages would look like this:
$messages = array(
'name' => array(
'Zend_Validate_StringLength' => array(
'stringLengthTooShort' => "'bal' is less than 4 characters long"
)
)
);
}
{code}
Of course, if there were other fields and validators that failed, their messages would all be in the returned array, under the appropriate index.

*Custom error messages*
You can grab an instance of the Error Manager object to set any number of custom error messages for any combination of field, validator, and failure reason code.

{code}
$zvb = new Zend_Validate_Builder;

// Get an instance of the Error Manager
$em = new Zend_Validate_Builder_ErrorManager;
// This also equally valid; acts kind of like a singleton
// This also equally valid; it creates a new instance if one isn't already present
// $em = $zvb->getErrorManager();

// The error messages can contain the same substitution parameters allowed by the validators themselves, which is very useful
$em->setMessage('name', $em->setTemplate('name', 'Zend_Validate_StringLength', 'stringLengthTooShort', 'Name must be less than %min% characters long.');
$em->setMessage('phone_no', $em->setTemplate('phone_no', 'Zend_Validate_IsNumeric', 'isNumericNot', 'Phone number can only contain numeric digits.');
$em->setMessage('phone_no', $em->setTemplate('phone_no', 'Zend_Validate_Between', 'notBetween', '"%value%" does not appear to be a valid US phone number.');
// You don't have to override all the possible messages in a validator
// You don't have to override all the possible messages in a validator. It will fall back to the built-in messages when necessary
$em->setMessage('email', $em->setTemplate('email', 'Zend_Validate_EmailAddress', 'emailAddressInvalid', 'Please enter a valid email address');

$zvb->addValidator('name', new Zend_Validate_StringLength(100));
$zvb->addValidator('phone_no', new Zend_Validate_IsNumeric);
$zvb->addValidator('phone_no', new Zend_Validate_Between(1000000, 9999999));
$zvb->addValidator('email', new Zend_Validate_EmailAddress);
$zvb->add(new Zend_Validate_StringLength(100), 'name');
$zvb->add(new Zend_Validate_IsNumeric, 'phone_no');
$zvb->add(new Zend_Validate_Between(1000000, 9999999), 'phone_no');
$zvb->add(new Zend_Validate_EmailAddress, 'email');

if (!$zvb->isValid($post)) {
{code}

Normally, calling {{getMessages}} on a validator returns a flat array of error messages. But because the Builder manages a whole collection of fields and validators, it can't be completely compatible with the return value of this method. Instead, it returns a three-dimensional array, indexed by field name, validator class name, and failure reason code. It would look something like this:
The Error Manager provides a {{hasMessages}} method, in addition to {{getMessages}}. Both of these methods take optional arguments that allow you to filter down to a particular subset of the error messages you might be interested in.

{code}
$messages = array(
'name' => array(
'Zend_Validate_StringLength' => array(
'stringLengthTooShort' => 'Name must be less than 100 characters long.'
),
),
...
);
{code}
One key benefit of all this is that it allows you to completely separate your error messages from the code that does validation. You could put all your messages in a separate file, or use a Zend_Config object to load all the error messages for a form at once. Among other things, this should make I18N much easier.

It should be mentioned that the Error Manager provides the methods {{getErrors}} and {{hasErrors}} for more granular access to the set of raised errors.
Here's one possible way to store errors in an INI file, and set them via Zend_Config. The {{setTemplates}} method makes this possible, since it accepts an entire array structure of error messages.

One key benefit of all this is that it allows you to completely separate your error messages from the code that does validation. You could put all your messages in a separate file, or use a Zend_Config object to load all the error messages for a form at once. Among other things, this should make I18N/L10N for your forms much easier.

Here's one possible way to store errors in an INI file, and set them via Zend_Config. The {{setMessages}} method makes this possible, since it accepts an entire array structure of error messages.

*errors.ini*
{code}
{code}

*someController.php* *SomeClass.php*
{code}
// Inside whatever appropriate method...
$zvb = new Zend_Validate_Builder;
$em = $zvb->getErrorManager();
$filter = new Zend_Filter_Builder;

$filter->addFilter('*', new Zend_Filter_StringTrim); $filter->add(new Zend_Filter_StringTrim, '/./', Zend_Filter_Builder::PATTERN);
$filter->addFilter('*', new Zend_Filter_HtmlSpecialCharsDecode); $filter->add(new Zend_Filter_HtmlSpecialCharsDecode, '/./', Zend_Filter_Builder::PATTERN);
$filter->addFilter('*', new Zend_Filter_StripTags); $filter->add(new Zend_Filter_StripTags, '/./', Zend_Filter_Builder::PATTERN);
// This runs the Digits filter all the fields that end with "_ent" or "_acct"
$filter->add(new Zend_Filter_Digits, '/^\[a-z_]+?_(ent|acct)$/', Zend_Filter_Builder::PATTERN);
$form = $filter->filter($form);

$glFields = array_intersect_key($form, array_flip(array(
'cash_ent', 'revenue_ent', 'ar_ent', 'reserve_ar_ent', 'due_to_ent', 'due_from_ent', 'prepaid_ent',
'cash_acct', 'revenue_acct', 'ar_acct', 'reserve_ar_acct', 'due_to_acct', 'due_from_acct', 'prepaid_acct',
)));

$glFilter = new Zend_Filter_Builder;
$glFilter->addFilter('*', new Zend_Filter_Digits);
$glFields = $glFilter->filter($glFields);

// This would also work, but filter() returns the same set of fields passed
// in, so the GL fields wouldn't be isolated into their own array in this
// case:
/*
$glFilter = new Zend_Filter_Builder;
$glFilter->addFilter(array('cash_ent', 'revenue_ent', 'ar_ent', 'reserve_ar_ent', 'due_to_ent', 'due_from_ent', 'prepaid_ent',
'cash_acct', 'revenue_acct', 'ar_acct', 'reserve_ar_acct', 'due_to_acct', 'due_from_acct', 'prepaid_acct',),
new Zend_Filter_Digits);
$form = $glFilter->filter($form);
*/

// Order validators are added to fields is preserved

// Reuse validators
$notEmpty = new Zend_Validate_NotEmpty;

$validator = new Zend_Validate_Builder;
$validator->onError(array($this, 'handleError'));

$validator->addValidator('id', $isNumeric, $validator->add($isNumeric, 'id', Zend_Validate_Builder::OPTIONAL);
$validator->addValidator('code', $isNumeric); $validator->add($isNumeric, 'code');
$validator->addValidator('title', $string50); $validator->add($string50, 'title');
$validator->addValidator('general_statute', $string10); $validator->add($string10, 'general_statute');

// The user can use either the 'amount' or 'rate_schedule' field, but not both
// At least one or the other, but not both $validator->add(new Zend_Validate_AtLeastN(1, $notEmpty),
array('amount', 'rate_schedule'),
Zend_Validate_Builder::PASS_GROUP);
$validator->addValidator(array('amount', 'rate_schedule'), $validator->add(new Zend_Validate_AtMostN(1, $notEmpty),
new Zend_Validate_AtLeastNofM(1, 2, $notEmpty)); array('amount', 'rate_schedule'),
Zend_Validate_Builder::PASS_GROUP);
$validator->addValidator(array('amount', 'rate_schedule'), $validator->add($isNumeric, 'amount', Zend_Validate_Builder::OPTIONAL);
$validator->add(new PL_Validate_RateSchedule, 'rate_schedule',
new Zend_Validate_AtMostNofM(1, 2, $notEmpty)); Zend_Validate_Builder::OPTIONAL);
$validator->addValidator('amount', $isNumeric, Zend_Validate_Builder::OPTIONAL);
$validator->addValidator('rate_schedule', new PL_Validate_RateSchedule, Zend_Validate_Builder::OPTIONAL);

// Cash Ent/Account and Revenue Ent/Account are required
$validator->addValidator(array('cash_ent', 'revenue_ent'), $validator->add($isNumeric, array('cash_ent', 'revenue_ent'));
$isNumeric);
$validator->addValidator('cash_acct', $validator->add(new GL_Validate_AccountNo($form['cash_ent']), 'cash_acct');
new GL_Validate_AccountNo($form['cash_ent']));
$validator->addValidator('revenue_acct', $validator->add(new GL_Validate_AccountNo($form['revenue_ent']), 'revenue_acct');
new GL_Validate_AccountNo($form['revenue_ent']));;

// The other account fields are optional
$validator->addValidator(array('ar_ent', $validator->add($isNumeric, array('ar_ent', 'reserve_ar_ent', 'prepaid_ent'),
$isNumeric, Zend_Validate_Builder::OPTIONAL);
$validator->addValidator('ar_acct', $validator->add(new GL_Validate_AccountNo($form['ar_ent']), 'ar_acct',
new GL_Validate_AccountNo($form['ar_ent']), Zend_Validate_Builder::OPTIONAL);
$validator->add(new GL_Validate_AccountNo($form['reserve_ar_ent']), 'reserve_ar_acct',
Zend_Validate_Builder::OPTIONAL);
$validator->addValidator('reserve_ar_acct', $validator->add(new GL_Validate_AccountNo($form['prepaid_ent']), 'prepaid_acct',
new GL_Validate_AccountNo($form['reserve_ar_ent']), Zend_Validate_Builder::OPTIONAL);
Zend_Validate_Builder::OPTIONAL);
$validator->addValidator('prepaid_acct',
new GL_Validate_AccountNo($form['prepaid_ent']),
Zend_Validate_Builder::OPTIONAL);

$validator->add(new Zend_Validate_Equals($form['cash_ent']), 'due_to_ent',
Zend_Validate_Builder::OPTIONAL);
$validator->addValidator('due_to_ent', $validator->add(new Zend_Validate_Equals($form['revenue_ent']),
new Zend_Validate_Equals($form['cash_ent']), array('ar_ent', 'reserve_ar_ent', 'due_from_ent', 'prepaid_ent'),
Zend_Validate_Builder::OPTIONAL);
$validator->addValidator(array('ar_ent', 'reserve_ar_ent', 'due_from_ent', 'prepaid_ent'),
new Zend_Validate_Equals($form['revenue_ent']),
Zend_Validate_Builder::OPTIONAL);

// If we have an AR Account, and the Cash Ent is different from the AR Ent,
$form['cash_ent'] != $form['revenue_ent']) {

$validator->addValidator(array('due_to_ent', 'due_from_ent'), $validator->add($isNumeric, array('due_to_ent', 'due_from_ent'));
$isNumeric);
$validator->addValidator('due_to_acct', $validator->add(new GL_Validate_AccountNo($form['due_to_ent']), 'due_to_acct');
new GL_Validate_AccountNo($form['due_to_ent']));
$validator->addValidator('due_from_acct', $validator->add(new GL_Validate_AccountNo($form['due_from_ent']), 'due_from_acct');
new GL_Validate_AccountNo($form['due_from_ent']));
}

if (!$validator->isValid($form)) {
$this->_forward('editPage');
return;

{code}
// The filter structure for the contact "record"
$zfb1 = new Zend_Filter_Builder;
$filterContacts = new Zend_Filter_Builder_FluentFacade($zfb1);
$filterContacts->glob('*')->stringTrim()->htmlEntitiesDecode()->stripTags(); $filterContacts->glob('/./')->stringTrim()->htmlEntityDecode()->stripTags();
$fitlerContacts->phone_no->digits();

// Use the Array filter to validate all the "records" in the set
$zfb2 = new Zend_Filter_Builder;
$filterForm = new Zend_Filter_Builder_FluentFacade($zfb2);
// We don't actually have Zend_Filter_Array yet; it would be needed for this (would work just like Zend_Validate_Array mentioned earlier)
// Zend_Filter_Array is the Filter analog to Zend_Validate_Array (look in the Laboratory)
$filterForm->contacts->array($filterContacts);
$post = $filterForm->filter($post);

// The validator structure for the contact "record"
$zvb1 = new Zend_Validate_Builder;
$valContacts = new Zend_Validate_Builder_FluentFacade($zvb1);
$valContacts->notes->stringLength(2000)->optional();

// Use the Array validator to validate all the "records" in the set
$zvb2 = new Zend_Validate_Builder;
$valForm = new Zend_Validate_Builder_FluentFacade($zvb2);
$valForm->addValidator('contacts', new Zend_Validate_Array($valContacts));
$valForm->contacts->array($valContacts);
if ($valForm->isValid($post)) {
$this->saveData($post);
$filter = new Zend_Filter_Builder_FluentFacade($zfb);

$filter->glob('*')->stringTrim()->htmlSpecialCharsDecode()->stripTags(); $filter->glob('/./')->stringTrim()->htmlSpecialCharsDecode()->stripTags();
$filter->glob('/^\[a-z_]+?_(ent|acct)$/')->digits();
$form = $filter->filter($form);

$glFields = array_intersect_key($form, array_flip(array(
// The 'namespaces' option is passed to the Validator Factory, so it can find
'cash_ent', 'revenue_ent', 'ar_ent', 'reserve_ar_ent', 'due_to_ent', 'due_from_ent', 'prepaid_ent', // validators other than the default Zend_Validate_* ones.
'cash_acct', 'revenue_acct', 'ar_acct', 'reserve_ar_acct', 'due_to_acct', 'due_from_acct', 'prepaid_acct',
)));

$zfb2 = new Zend_Filter_Builder;
$glFilter = new Zend_Filter_Builder_FluentFacade($zfb2);
$glFilter->glob('*')->digits();
$glFields = $glFilter->filter($glFields);

// This would also work, but filter() returns the same set of fields passed
// in, so the GL fields wouldn't be isolated into their own array in this
// case:
/*
$zfb2 = new Zend_Filter_Builder;
$glFilter = new Zend_Filter_Builder_FluentFacade($zfb2);
$glFilter->group(array('cash_ent', 'revenue_ent', 'ar_ent', 'reserve_ar_ent', 'due_to_ent', 'due_from_ent', 'prepaid_ent',
'cash_acct', 'revenue_acct', 'ar_acct', 'reserve_ar_acct', 'due_to_acct', 'due_from_acct', 'prepaid_acct'))->digits;
$form = $glFilter->filter($form);
*/

// Order validators are added to fields is preserved
// Future optimization: make fluent Facade internally reuse validators

$options['namespaces'] = array('GL_Validate', 'PL_Validate');

$validate->general_statute->stringLength(10);

// Note the shortcut used to create instances of Zend_Validate_NotEmpty
// At least one or the other, but not both $validate->each(array('amount', 'rate_schedule'))
$validate->group(array('amount', 'rate_schedule'))
->atLeastNofM(1, 2, ->atLeastN(1, $validate->notEmpty())
->atMostNofM(1, 2, ->atMostN (1, $validate->notEmpty());
$validate->amount->isNumeric()->optional();
$validate->rate_schedule->rateSchedule()->optional();

// Cash Ent/Account and Revenue Ent/Account are required
$validate->group(array('cash_ent', $validate->each(array('cash_ent', 'revenue_ent'))->isNumeric();
$validate->cash_acct->accountNo($form['cash_ent']);
$validate->revenue_acct->accountNo($form['revenue_ent']);

// The other account fields are optional
$validate->group(array('ar_ent', $validate->each(array('ar_ent', 'reserve_ar_ent', 'prepaid_ent'))->isNumeric()->optional();
$validate->ar_acct->accountNo($form['ar_ent'])->optional();
$validate->reserve_ar_acct->accountNo($form['reserve_ar_ent'])->optional();

$validate->due_to_ent->equals($form['cash_ent'])->optional();
$validate->group(array('ar_ent', $validate->each(array('ar_ent', 'reserve_ar_ent', 'due_from_ent', 'prepaid_ent'))
->equals($form['revenue_ent'])->optional();

$form['cash_ent'] != $form['revenue_ent']) {

$validate->group(array('due_to_ent', $validate->each(array('due_to_ent', 'due_from_ent'))->isNumeric();
$validate->due_to_acct->accountNo($form['due_to_ent']);
$validate->due_from_acct->accountNo($form['due_from_ent']);
class Zend_Validate_Builder implements Zend_Validate_Interface
{
const OPTIONAL = 1;
const PATTERN = 2;
const PASS_GROUP = 4;

protected $_validators; $_validatorTable;
protected $_patterns;
protected $_flags;
protected $_errorManager;
public $dataSet;
public function getErrorManager() { }
public function setErrorManager(Zend_Validate_Builder_ErrorManager_Interface $em) { }
public function setFlags($id, $flags = 0) { }
public function addFlags($id, $flags = 0) { }
public function setOptional($field, $value = true) getFlags($id) { }
public function addValidator($field, Zend_Validate_Interface add(Zend_Validate_Interface $validator, $fieldFlags $fieldSpec, $flags = 0) { }
public function isPattern($test) { }
protected function _expandMeta(array $dataSet) { }
public function isValid($dataSet) { }
public function getMessages() { }
public function setOptions(array $options) { }
public function glob($pattern) { }
public function each() { }
public function group() { }
public function __get($field) { }
public function __call($name, $args) { }
protected function _getAdder($fieldSpec, $flags = 0) { }
public function isValid($data) { }
public function getMessages() { }
protected $_builder;
protected $_factory;
protected $_fieldSpec;
protected $_flags;
protected $_rowIds;

public function __construct(Zend_Validate_Builder $builder, Zend_Validate_Builder_ValidatorFactory_Interface $factory, $field) { }
Zend_Validate_Builder_ValidatorFactory_Interface $factory,
$fieldSpec,
$flags = 0) { }
public function optional($value = true) { }
public function __call($name, $args) { }

public function __construct() { }
public function setMessage($field, setTemplate($field, $valClass, $reason, $message) { }
public function setMessages(array setTemplates(array $errors) { }
public function getMessages($field getTemplates($field = null, $valClass = null, $reason = null) { }
public function raise($field, Zend_Validate_Interface $val) { }
public function getErrors($field getMessages($field = null, $valClass = null, $reason = null) { }
public function hasErrors($field hasMessages($field = null, $valClass = null, $reason = null) { }
public function clear($field = null, $valClass = null, $reason = null) { }
protected function _createMessage($message, Zend_Validate_Interface $val) { }
{
public function raise($field, Zend_Validate_Interface $val);
public function getErrors($field getMessages($field = null, $valClass = null, $reason = null);
public function clear($field = null, $valClass = null, $reason = null);
}
class Zend_Filter_Builder implements Zend_Filter_Interface
{
const PATTERN = 1;

protected $_filters; $_filterTable;
protected $_patterns;
public $dataSet;

public function __construct(array $dataSet = null) { }
public function clear() { }
public function setFlags($id, $flags = 0) { }
public function addFlags($id, $flags = 0) { }
public function addFilter($field, Zend_Filter_Interface $filter) getFlags($id) { }
public function isPattern($test) { }
protected function _expandMeta(array $dataSet) { }
public function add(Zend_Filter_Interface $filter, $fieldSpec, $flags = 0) { }
public function filter($dataSet) { }
}
public function setOptions(array $options) { }
public function glob($pattern) { }
public function group() each() { }
public function __get($field) { }
public function __call($name, $args) { }
protected function _getAdder($fieldSpec, $flags = 0) { }
public function filter($data) { }
}
protected $_builder;
protected $_factory;
protected $_fieldSpec;
protected $_flags;
protected $_rowIds;

public function __construct(Zend_Filter_Builder $builder, Zend_Filter_Builder_FilterFactory_Interface $factory, $field) { }
Zend_Filter_Builder_FilterFactory_Interface $factory,
$fieldSpec,
$flags = 0) { }
public function __call($name, $args) { }
}
protected $_options = array();

public function __construct(array $options = array()) { }
public function getOptions() { }
public function setOptions(array $options) { }
public function create($name, $args) { }
public function namespaceLoad($basename, array $namespaces = null, $dirs = null) { }
}
{code}