Validating dates
I discovered recently that Zend Framework 1’s Zend_Date has two operating modes when it comes to format specifiers: iso and php, where iso is the default.
When using Zend_Validate_Date in forms, I like to use the php format specifiers as they are what I’m used to and so can easily know what they mean when reviewing code that I wrote months ago.
My code looks something like this:
$subForm->addElement('text', 'start_date', array(
'filters' => array('StringTrim', 'StripTags'),
'required' => true,
'label' => 'Start date',
'validators' => array(
array('Date', true, array('format'=>'j F Y')),
),
));
As you can see, I want the text field to be filled in with a date like “8 November 2010”.
This is easy to achieve using this code in your Bootstrap.php like this:
function _initDateFormat()
{
Zend_Date::setOptions(array('format_type' => 'php'));
}
Note that this is a static method call and so it affects all instances of Zend_Date.
I also discovered that when you are in php formatting mode, then all the Zend_Date formatting constants like Zend_Date::MONTH do not work. This was a problem as I have other code in the project that uses them.
There are a number of choices.
One option is to change the formatting mode when you need to. As it is a static, you need to keep track of what you’re doing, so the code looks like this:
$currentOptions = Zend_Date::setOptions();
$currentFormatType = $currentOptions['format_type'];
Zend_Date::setOptions(array('format_type' => 'iso'));
// You can now use Zend_Date::MONTH, ZEND_DATE::ISO etc
// After use, reset the format type back to what it was originally set to
Zend_Date::setOptions(array('format_type' => $currentFormatType));
Similarly, we can override Zend_Validate_Date with the same logic:
class App_Validate_Date extends Zend_Validate_Date
{
public function isValid ($value)
{
$currentOptions = Zend_Date::setOptions();
$currentFormatType = $currentOptions['format_type'];
Zend_Date::setOptions(array('format_type' => 'php'));
$valid = parent::isValid($value);
Zend_Date::setOptions(array('format_type' => $currentFormatType));
}
}
I also have two other requirements for date validation in this project:
- An empty $value fails validation.
- Regardless of the format that’s been set, it would be helpful to always allow a valid Y-m-d formatted date.
Whilst talking on IRC about date validation issues that I was having, I also found out that a $value of 1-1-1TEST passes validation! This is noted in issue ZF-7583.
Allowing an empty $value is easy to add to my App_Validate_Date. Similarly, allowing a Y-m-d format is also fairly easy by resetting the formatting and then calling parent::isValid() again.
However, I really don’t want ‘1111’ or ‘1-1-1TEST’ or to be considered a valid date! I couldn’t see an easy way to fix this, so I went for the easy way out and wrote my own validator:
class App_Validate_Date extends Zend_Validate_Date
{
public function isValid ($value)
{
$this->_setValue($value);
if (empty($value)) {
return true;
}
$valid = $this->_testDateAgainstFormat($value, $this->getFormat());
if (!$valid) {
// re-test for Y-m-d as this format is always a valid option
$valid = $this->_testDateAgainstFormat($value, 'Y-m-d');
}
if ($valid) {
return true;
}
$this->_error(self::INVALID_DATE);
return false;
}
protected function _testDateAgainstFormat($value, $format)
{
$ts = strtotime($value);
if ($ts !== false) {
$testValue = date($format, $ts);
if ($testValue == $value) {
return true;
}
}
return false;
}
}
Obviously this code is highly unlikely to work if you need to validate localised dates! However, it solves my needs and it’s useful to document here for when I forget how I solved these issues!
function _initDateFormat()
{ Zend_Date::setOptions(array('format_type' => 'php'));
}
be careful: if Zend_date is not used throughout your application your application will be needlessly including not one but many files.
e.g. with ZFv1.10 You'll be including a minimum of 5 files:
require_once 'Zend/Date/DateObject.php';
require_once 'Zend/Locale.php';
require_once 'Zend/Locale/Format.php'; + 'Zend/Locale/Data.php';
require_once 'Zend/Locale/Math.php';
That's a pretty clever way of getting around Zend_Validate_Date's mindless validation. I've used date_parse() to do something similar.