The Validation::postal() method that comes with CakePHP 1.2 is good in that it can handle a number of different country formats, but the problem is you can only validate your data against one country. What if you want to accept, say, either Canadian or US postal/zip code formats? I ran into this problem earlier today, and decided to write my own postal() function that can take either a string as the country, just like Validation::postal(), or an array of countries.
Here’s the function, which also resides in its Github repository:
<?php
class AppModel extends Model {
/**
* Modified version of Validation::postal - allows for multiple
* countries to be specified as an array.
*/
function postal($check, $regex = null, $country = null) {
// List of regular expressions to use, if a custom one isn't specified.
$countryRegs = array(
'uk' => '/\\A\\b[A-Z]{1,2}[0-9][A-Z0-9]? [0-9][ABD-HJLNP-UW-Z]{2}\\b\\z/i',
'ca' => '/\\A\\b[ABCEGHJKLMNPRSTVXY][0-9][A-Z][ ]?[0-9][A-Z][0-9]\\b\\z/i',
'it' => '/^[0-9]{5}$/i',
'de' => '/^[0-9]{5}$/i',
'be' => '/^[1-9]{1}[0-9]{3}$/i',
'us' => '/\\A\\b[0-9]{5}(?:-[0-9]{4})?\\b\\z/i',
'default' => '/\\A\\b[0-9]{5}(?:-[0-9]{4})?\\b\\z/i' // Same as US.
);
$value = array_values($check);
$value = $value[0];
if ($regex) {
return preg_match($regex, $value);
} else if (!is_array($country)) {
return preg_match($countryRegs[$country], $value);
}
foreach ($country as $check) {
if (!isset($countryRegs[$check]) && preg_match($countryRegs['default'], $value)) {
return true;
} else if (preg_match($countryRegs[$check], $value)) {
return true;
}
}
return false;
}
}
?>
I put the function in my AppModel, but you can put it in an individual model if you don’t want it to apply to every model in your application. Usage is pretty simple. For example, to validate data that can be either US or Canadian format:
<?php
class MyModel extends AppModel {
var $validate = array(
'postal_zip' => array(
'rule' => array('postal', null, array('us', 'ca')),
'message' => 'Please enter a valid postal or zip code.',
'required' => true
)
);
}
?>

Thanks for this, I had solved the same problem ( my company has clients in Canada and the United States and validating either type was a pain) in much the same way. Our code even looks 90% similar, which as I based mine on the original function I guess it should be expected.
Consider opening a ticket in the new cakephp lighthouse to allow for the validation of more than one localized piece of data. Would work for phone, postal codes, etc. Might be a worthwhile addition.
Would it not make more sense, and save you time implementing, debugging and updating your own solution, by utilising PEAR’s Validate class ( http://pear.php.net/package/Validate) and other associated classes such as Validate_UK, Validate_US etc?
Also, you’ve a bug in your code – it allows for US zipcodes that the United Stated Postal Service tool at http://zip4.usps.com/zip4/citytown_zip.jsp does not recognise, such as ’00000′ – unless this weak level of validation is intentional?
Hi Ken – I posted almost the same response on your blog, but I wanted to repeat much of what I said for the benefit of the readers here.
As my post implies, I was addressing a deficiency with the CakePHP framework’s postal validation code. So, my solution is based on the existing Validation::postal() method that comes with CakePHP. While PEAR is a good solution, it was beyond the scope of my article to suggest scrapping Cake’s entire Validation class in favour of another library. I used the existing regular expressions (from the Cake method), which is why 000000 validates as a US postal code. I might suggest it’s beyond the scope of a data format validator to decide whether a postal code is ‘real’ anyway.
Abba – good call on the lighthouse ticket. I just might do that today (if you haven’t already).
check out the new cake1.3 branches
there localized validation is already supported by using different classes
they can be stored in app/libs/localized/XY_validation.php
whereas XY would be your country code
Thanks for the heads up, Mark – I admit I haven’t had much time to check out 1.3 yet. I was planning to wait until it came out of beta to upgrade, at least on the high traffic production servers we have at work. But I guess that means I don’t have to do a lighthouse ticket now.
I found your blog on technorati and read a couple of of your previous posts. Keep up the good work. I just added up your RSS feed to my Yahoo News Reader. Looking forward to reading more from you down the road!
I ran into this problem and then realized that what I wanted wasn’t to check the ZIP/postal code against multiple countries, but rather I wanted to alter the country against which the ZIP/postal code was validated in real-time.
The simple solution is to alter the model’s $validate var like this:
$this->{ModelName}->validate[{FieldName}][{RuleName}]['rule'] = array(‘postal’, null, {CountryCode});
So for example, imagine the common case where a country and zip are both being provided in a posted form. If you wanted to change the validation for a field called ‘zip’ in your ‘Address’ model and your validation rule was called ‘zipcheck’ and the country was being supplied in the ‘country_code’ field of a posted form, you could do this in your controller just before you validate/or save the data:
$this->Address->validate['zip']['zipcheck']['rule'] = array(‘postal’, null, $this->data['Address']['country_code']);
Now when you validate the data, the ‘zip’ field will be validated against the postal code rules for the country code sent in the form. You just have to be sure to have your form submit a country code (‘ca’, ‘us’, etc.) and not a country name. This is easy enough to do using a with the country codes as the option values.
(Looks like the comment form stripped out my SELECT tag…
The last sentence should read:
“This is easy enough to do using a select element with the country codes as the option values.”