In this series of articles I describe simple, elegant functions from my own personal library that have proven useful to me over the course of many projects.
The most frequently used functions in my php library are UtilsValidator::checkArray() and UtilsValidator::checkArrayAndSetDefaults(). I will refer to them as checkArray() and checkArrayAndSetDefaults() for brevity.
Example 1: checkArray()
The checkArray() function validates a php array supplied in the first parameter, according to a specification that is supplied in the second and optional third parameters. Compulsory keys are specified in the second parameter, and optional keys are specified in the optional third parameter. If any of the validation checks fail, the function throws an exception carrying a description of the check that failed.
Utils_validator::checkArray
(
$bookInfo, array
(
'id' => 'positiveInt',
'title' => 'string'
), array
(
'authorNameFirst' => 'string',
'authorNameLast' => 'string',
'isHardcover' => 'bool' ,
'nPages' => 'positiveInt'
)
);
The code in the above example will cause an exception to be thrown if any of the following statements are false:
* $bookInfo is an array containing a minimum of two and a maximum of six elements, * $bookInfo['id' ] is a positive integer, * $bookInfo['title' ] is a string, * $bookInfo['authorNameFirst'] is undefined or a string, * $bookInfo['authorNameLast' ] is undefined or a string, * $bookInfo['isHardcover' ] is undefined or a boolean, * $bookInfo['nPages' ] is undefined or a positive integer
More useful to the programmer reading the above code, is that if the code executes and does not cause an exception to be thrown, the above statements are true. This is a great aid to code readability.
Example 2: checkArrayAndSetDefaults()
The checkArrayAndSetDefaults() function is a variation on checkArray() that performs the same checks, but also may modify the supplied array if necessary by assigning a default value to each optional key that is undefined. To enable this behaviour, the array to be checked is passed call-by-reference.
Utils_validator::checkArrayAndSetDefaults
(
$bookInfo, array
(
'id' => 'positiveInt',
'title' => 'string'
), array
(
'authorNameFirst' => array('string' => 'unknown'),
'authorNameLast' => array('string' => 'unknown')
'isHardcover' => array('bool' => false ),
'nPages' => array('positiveInt' => null ),
)
);
If the above code executes and does not cause an exception to be thrown, the below statements are true.
* $bookInfo is an array containing six elements, * $bookInfo['id' ] is a positive integer, * $bookInfo['title' ] is a string, * $bookInfo['authorNameFirst'] is the supplied string or 'unknown' if nothing was supplied, * $bookInfo['authorNameLast' ] is the supplied string or 'unknown' if nothing was supplied, * $bookInfo['isHardcover' ] is the supplied boolean or false if nothing was supplied, * $bookInfo['nPages' ] is the supplied positive integer or null if nothing was supplied
Uses
I find the functions described above to be particularly useful in two situations.
- When validating a $_GET or $_POST array following a form submission
- Converting a function that accepts many parameters to a more readable and usable form.
The following example illustrates situation number two.
Example 3: Validating Function Parameters
Consider the following example function of many parameters, and the call to that function below the function definition.
function sendEmail($emailTo, $emailFrom, $emailSubject, $emailContent, $emailCc = null, $emailBcc = null)
{
// code...
}
// ...
sendMail
(
'bor.alurin@foundation.com' , // To.
'hari.seldon@trantor.com' , // From.
'Instructions' , // Subject.
'Blah, blah, blah, ...' , // Content.
'salvor.hardin@foundation.com', // CC.
'onum.barr@siwenna.com' // BCC.
);
The problem with functions of many parameters, is that it is very easy for a programmer to make the mistake of supplying parameters in the wrong order.
A partial solution often used is to supply an identifying comment next to each parameter, as in the example code above. Maybe the comments correctly identified the parameters at the time the comments were written, but there is no guarantee that the parameter order remains correct. This problem is the source of many bugs.
Consider the following alternative code, where the parameters are supplied as an array, and validated inside the function. The parameters may now be supplied in any order, since they are identified by descriptive keys. Comments are no longer needed to explain what parameters are passed.
The code below is not as compact as the code above, but is significantly better for long-term maintainability.
function sendEmail($params)
{
Utils_validator::checkArrayAndSetDefaults
(
$params, array
(
'emailTo' => 'string',
'emailFrom' => 'string',
'emailSubject' => 'string',
'emailContent' => 'string'
), array
(
'emailCc' => array('nullOrString', null),
'emailBcc' => array('nullOrString', null)
)
);
extract($params);
// code...
}
sendMail
(
array
(
'emailTo' => 'bor.alurin@foundation.com' ,
'emailFrom' => 'hari.seldon@trantor.com' ,
'emailSubject' => 'Instructions' ,
'emailContent' => 'Blah, blah, blah, ...' ,
'emailCc' => 'salvor.hardin@foundation.com',
'emailBcc' => 'onum.barr@siwenna.com'
}
);
Note also the use of the native php function extract() in the code above. Since the contents of the $params array are clearly identified by the call to the checkArrayAndSetDefaults() function, the extract() function may be used to automatically declare all array values as variables. Once extract() has been called, the variables passed in the array may be used as if they had been passed directly rather than in the array. See the php documentation for more details.
Full Code of UtilsValidator Class
The full code of the UtilsValidator class is included below. It has no dependencies. Note in the function checkType() below, that many types besides those used in the examples above may be checked for. Note also that user-defined types may be checked for by supplying the name of the class as the type string (this is accomplished by the line labelled 'Catch all').
/**************************************************************************************************\
*
* vim: ts=3 sw=3 et wrap co=100 go-=b
*
* Filename: "Utils_validator.php"
*
* Project: Utilities.
*
* Purpose: Utilities concerning validation.
*
* Author: Tom McDonnell 2008-07-01.
*
\**************************************************************************************************/
// Class definition. ///////////////////////////////////////////////////////////////////////////////
/*
* Validation functions. Most will perform some checks, and throw an exception any check fails.
*
* Note: Only use these functions where speed is unimportant.
*/
class Utils_validator
{
// Public functions. -----------------------------------------------------------------------//
/*
*
*/
public function __construct()
{
throw new Exception('This class is not intended to be instantiated.');
}
/*
*
*/
public static function checkArray($array, $typeByRequiredKey, $typeByOptionalKey = array())
{
assert('is_array($array )');
assert('is_array($typeByRequiredKey)');
assert('is_array($typeByOptionalKey)');
$nKeys = count($array );
$nKeysRequired = count($typeByRequiredKey);
$nKeysOptional = count($typeByOptionalKey);
$nKeysMax = $nKeysRequired + $nKeysOptional;
if ($nKeys < $nKeysRequired || $nKeys > $nKeysMax)
{
throw new Exception
(
"Incorrect number of keys in array ($nKeys). " .
"Expected number in range [$nKeysRequired, $nKeysMax]."
);
}
// Check required keys and types.
foreach ($typeByRequiredKey as $key => $type)
{
if (!array_key_exists($key, $array))
{
throw new Exception("Required key '$key' does not exist in array.");
}
try
{
self::checkType($array[$key], $type);
}
catch (Exception $e)
{
throw new Exception("Type check failed for required key '$key'.\n" . $e->getMessage());
}
}
// Check optional keys and types.
$arrayExtra = array_diff_key($array, $typeByRequiredKey);
foreach (array_keys($arrayExtra) as $key)
{
if (!array_key_exists($key, $typeByOptionalKey))
{
throw new Exception("Unexpected key '$key' found.");
}
try
{
self::checkType($array[$key], $typeByOptionalKey[$key]);
}
catch (Exception $e)
{
throw new Exception("Type check failed for optional key '$key'.\n" . $e->getMessage());
}
}
}
/*
*
*/
public static function checkArrayAndSetDefaults
(
&$array, $typeByRequiredKey, $typeAndDefaultByOptionalKey = array()
)
{
$typeByOptionalKey = array();
foreach ($typeAndDefaultByOptionalKey as $key => $typeAndDefault)
{
if (!is_array($typeAndDefault) || count($typeAndDefault) != 2)
{
throw new Exception
(
'Type and default value for optional parameter must be two-element array.'
);
}
$typeByOptionalKey[$key] = $typeAndDefault[0];
}
self::checkArray($array, $typeByRequiredKey, $typeByOptionalKey);
foreach ($typeAndDefaultByOptionalKey as $key => $typeAndDefault)
{
if (!array_key_exists($key, $array))
{
$array[$key] = $typeAndDefault[1];
}
}
}
/*
*
*/
public static function checkType($v, $type)
{
if (!is_string($type))
{
throw new Exception('Received non-string for $type.');
}
switch ($type)
{
// Basic types.
case 'array' : $b = is_array($v) ; break;
case 'bool' : // Fall through.
case 'boolean' : $b = is_bool($v) ; break;
case 'float' : $b = is_float($v) ; break;
case 'int' : $b = is_int($v) ; break;
case 'null' : $b = is_null($v) ; break;
case 'numeric' : $b = is_numeric($v) ; break;
case 'object' : $b = is_object($v) ; break;
case 'resource': $b = is_resource($v); break;
case 'scalar' : $b = is_scalar($v) ; break;
case 'string' : $b = is_string($v) ; break;
// Combinations of basic types.
case 'arrayOrString': $b = (is_array($v) || is_string($v)); break;
// C-type character checks.
case 'ctype_alnum' : $b = ctype_alnum($v) ; break;
case 'ctype_alpha' : $b = ctype_alpha($v) ; break;
case 'ctype_cntrl' : $b = ctype_cntrl($v) ; break;
case 'ctype_digit' : $b = ctype_digit($v) ; break;
case 'ctype_graph' : $b = ctype_graph($v) ; break;
case 'ctype_lower' : $b = ctype_lower($v) ; break;
case 'ctype_print' : $b = ctype_print($v) ; break;
case 'ctype_punct' : $b = ctype_punct($v) ; break;
case 'ctype_space' : $b = ctype_space($v) ; break;
case 'ctype_upper' : $b = ctype_upper($v) ; break;
case 'ctype_xdigit': $b = ctype_xdigit($v); break;
// Basic types with condition.
case 'character' : $b = (is_string($v) && strlen($v) == 1); break;
case 'nonNegativeInt' : $b = (is_int($v) && $v >= 0); break;
case 'negativeInt' : $b = (is_int($v) && $v < 0); break;
case 'positiveInt' : $b = (is_int($v) && $v > 0); break;
case 'nonNegativeFloat': $b = (is_float($v) && $v >= 0); break;
case 'nonPositiveFloat': $b = (is_float($v) && $v <= 0); break;
case 'negativeFloat' : $b = (is_float($v) && $v < 0); break;
case 'positiveFloat' : $b = (is_float($v) && $v > 0); break;
case 'nonEmptyString' : $b = (is_string($v) && strlen($v) > 0); break;
case 'nonEmptyArray' : $b = (is_array($v) && count($v) > 0); break;
// Basic types or null.
case 'nullOrArray' : $b = (is_null($v) || is_array($v) ); break;
case 'nullOrBool' : $b = (is_null($v) || is_bool($v) ); break;
case 'nullOrInt' : $b = (is_null($v) || is_int($v) ); break;
case 'nullOrFloat' : $b = (is_null($v) || is_float($v) ); break;
case 'nullOrNumeric' : $b = (is_null($v) || is_numeric($v) ); break;
case 'nullOrObject' : $b = (is_null($v) || is_object($v) ); break;
case 'nullOrResource': $b = (is_null($v) || is_resource($v)); break;
case 'nullOrScalar' : $b = (is_null($v) || is_scalar($v) ); break;
case 'nullOrString' : $b = (is_null($v) || is_string($v) ); break;
// Basic types with condition or null.
case 'nullOrCharacter' : $b = (is_null($v) || is_string($v) && strlen($v) == 1); break;
case 'nullOrNonEmptyString' : $b = (is_null($v) || is_string($v) && strlen($v) > 0); break;
case 'nullOrNonEmptyArray' : $b = (is_null($v) || is_array($v) && count($v) > 0); break;
case 'nullOrPositiveInt' : $b = (is_null($v) || is_int($v) && $v > 0); break;
case 'nullOrNegativeInt' : $b = (is_null($v) || is_int($v) && $v < 0); break;
case 'nullOrNonPositiveInt' : $b = (is_null($v) || is_int($v) && $v <= 0); break;
case 'nullOrNonNegativeInt' : $b = (is_null($v) || is_int($v) && $v >= 0); break;
case 'nullOrPositiveFloat' : $b = (is_null($v) || is_float($v) && $v > 0); break;
case 'nullOrNegativeFloat' : $b = (is_null($v) || is_float($v) && $v < 0); break;
case 'nullOrNonPositiveFloat': $b = (is_null($v) || is_float($v) && $v <= 0); break;
case 'nullOrNonNegativeFloat': $b = (is_null($v) || is_float($v) && $v >= 0); break;
// Date strings.
case 'date_yyyy-mm-dd': $b = self::checkDateString($v, 'yyyy-mm-dd'); break;
case 'date_yyyy/mm/dd': $b = self::checkDateString($v, 'yyyy/mm/dd'); break;
case 'date_dd-mm-yyyy': $b = self::checkDateString($v, 'dd-mm-yyyy'); break;
case 'date_dd/mm/yyyy': $b = self::checkDateString($v, 'dd/mm/yyyy'); break;
// Date-time strings.
case 'Y-m-d H:i:s': $b = self::checkDatetimeString($v, 'Y-m-d H:i:s'); break;
// Catch all.
default: $b = (gettype($v) == 'object')? (get_class($v) == $type): false;
}
if (!$b)
{
throw new Exception
(
"Variable type check failed. Expected '$type', received " .
(
(gettype($v) == 'object')?
'object of class \'' . get_class($v) . '\'.':
'variable of type \'' . gettype($v) . '\'.'
)
);
}
}
/*
*
*/
public static function checkDateString($dateStr, $format = 'yyyy-mm-dd')
{
switch ($format)
{
case 'yyyy-mm-dd': $regEx = '/^(\d{4})-(\d{2})-(\d{2})$/' ; $y = 1; $m = 2; $d = 3; break;
case 'yyyy/mm/dd': $regEx = '/^(\d{4})\/(\d{2})\/(\d{2})$/'; $y = 1; $m = 2; $d = 3; break;
case 'dd-mm-yyyy': $regEx = '/^(\d{2})-(\d{2})-(\d{4})$/' ; $d = 1; $m = 2; $y = 3; break;
case 'dd/mm/yyyy': $regEx = '/^(\d{2})\/(\d{2})\/(\d{4})$/'; $d = 1; $m = 2; $y = 3; break;
default: throw new Exception("Unknown format string '$format'");
}
return
(
preg_match($regEx, $dateStr, $matches) &&
checkdate($matches[$m], $matches[$d], $matches[$y])
);
}
/*
*
*/
public static function checkDatetimeString($datetimeStr, $format = 'Y-m-d H:i:s')
{
switch ($format)
{
case 'Y-m-d H:i:s':
$regEx = '/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/';
$y = 1; $m = 2; $d = 3; $h = 4; $i = 5; $s = 6;
break;
default:
throw new Exception("Unknown format string '$format'");
}
return
(
(
preg_match($regEx, $datetimeStr, $matches) &&
checkdate($matches[$m], $matches[$d], $matches[$y]) &&
(0 <= $matches[$h] && $matches[$h] <= 23) &&
(0 <= $matches[$i] && $matches[$i] <= 59) &&
(0 <= $matches[$s] && $matches[$s] <= 59)
) ||
(
// A zero date is allowed for debugging purposes
// and for queries designed to include all.
preg_match($regEx, $datetimeStr, $matches) &&
$matches[$y] == 0 && $matches[$m] == 0 && $matches[$d] == 0 &&
$matches[$h] == 0 && $matches[$i] == 0 && $matches[$s] == 0
)
);
}
}
/*******************************************END*OF*FILE********************************************/
Javascript Version
I have written a similar class in Javascript that I have found to be similarly useful. Find it here. Note however that the file linked to has dependencies, and so is not usable by itself.