PHP does not enforce types at compile-time (if anything, because there isn’t a compile time) and runtime checking only happens at the leaves of your source code tree, when you use a PHP function and that function notices one of its arguments is incorrect.
There are of course ways of introducing additional type safety into PHP code, both through development practices and through hints. For instance, you can hard-code checks into function prologues:
function SetUsername($username, $usr_id)
{
assert (is_string($username));
assert (is_int($usr_id));
// ...
}
And, if using class types, you can also use the type hint mechanism in PHP 5 to get automatic warnings:
function FitToWindow(Image $img, Window $window)
{
// ...
}
There remains the issue of member variables, which are modified and read in many different places. This means a “check the object is in a valid state” function is an useful addition to a class, to be used as a validity check during development to catch any errors as soon as they occur.
I sometimes use the following for my checks:
class Type { public static function Is($value, $type) { if (func_num_args() > 2) { $args = func_get_args(); array_shift($args); return self::Is($value, $args); } if (is_string($type)) return self::Is($value, array_filter(explode(' ', $type))); if (empty($type)) return true; $first = array_shift($type); if ($first == 'null') return $value === null || self::Is($value, $type); if ($first == 'array') { if (!is_array($value)) return false; $next = 0; foreach ($value as $key => $val) { if ($key != $next++) return false; if (!self::Is($val, $type)) return false; } return true; } if ($first == 'time') return is_int($value) && $value >= 0; if ($first == 'hash') { if (!is_array($value)) return false; foreach ($value as $val) if (!is($val, $type)) return false; return true; } if (is_callable($first)) return call_user_func($first, $value) && self::Is($value, $type); if (is_callable('is_' . $first)) return call_user_func('is_' . $first, $value) && self::Is($value, $type); if (class_exists($first)) return($value instanceof $first); return false; } public function checkTypes() { self::check($this); } public static function check($obj) { $class = get_class($obj); foreach (get_class_vars($class) as $var => $value) if ($var{0} != '_') if (!is($obj->$var, $value)) throw new Exception("Type error: `$class::$var` is not of type `$value`"); } }
The typical use is to define a new class, then assign a default value to all type-checked variables: that default value is a type string (or array) that is parsed and verified by the check functions. For instance:
class User
{
var $id = 'int';
var $name = 'null string';
var $media = 'array Media';
var $friends = 'positive int';
var $_hash;
}
This would check that the identifier is an integer, that the name is a string or null, that media is an array of instances of the Media class, and that friends is an integer such that is_positive($obj->friends) returns true (assuming you define that function somewhere). The hash variable is unchecked because it starts with an underscore. This has some advantages:
- Type expressions are shorter than the corresponding assert statements.
- They go deeper as far as checks go (for instance, arrays also check that all members are of a certain type).
- They document the code, by explaining in the class definition what the types of the variables are, as opposed to staying in a function.
- They help with automated testing by allowing the creation of classes with arbitrary values of the chosen type.
This also has disadvantages:
- This prevents setting an actual default value for the variables.
- It introduces an artificial naming convention for variables starting with or without underscores.
- Type-checking arrays or large structures takes time.
- It’s not detected by documentation generators.
- Does not play well with private variables.
Hi. I'm Victor Nicollet,
Recent Comments