In my last blog post, I introduced ContractLib, a simple programming by contract library that I’ve created for PHP 5.3 onwards. And I promised some examples :)
Installing ContractLibContractLib is available from the Phix project’s PEAR channel. Installing it is as easy as:
$ pear channel-discover pear.phix-project.org $ pear install -a phix/ContractLib
At the time of writing, this will install ContractLib-2.1.0. We use semantic versioning, so these examples will continue to work with all future releases of ContractLib-2.x.
Adding ContractLib To Your ProjectAssuming you’re using a PSR-0 compatible autoloader, just import the Contract class into your PHP file:
<?php use Phix_Project\ContractLib\Contract;Adding A Pre-condition Contract To Your Method Or Function
Take a trivial method like this:
<?php
class ActionToApply
{
public function appendNow($params)
{
$params[] = time();
}
}
This method works fine … until someone passes a non-array as the parameter. At that point, your code stops working – not because your code is wrong, but because someone used it in the wrong way. This is a classic cause of buggy PHP apps. Thankfully, it’s very easy to address using ContractLib.
If we were certain that the $params parameter was always an array, then we can keep the method itself extremely simple and clean. We can ensure that by adding a pre-condition using ContractLib.
<?php
use Phix_Project\ContractLib\Contract;
class ActionToApply
{
public function appendNow($params)
{
Contract::Preconditions(function() use ($params)
{
Contract::RequiresValue(
$params,
is_array($params),
'$params must be an array'
);
});
// original method code continues here
$params[] = time();
}
}
Now, if someone passes in a non-array, the caller will automatically get an E5xx_ContractFailedException, which makes it clear that the fault is in the caller’s code … not your’s.
PHP 5.4′s upcoming support for better type-hinting is another way to catch this kind of error, but not only does ContractLib work today with PHP 5.3 (which means you don’t have to wait to migrate to PHP 5.4), but also that you can write tests for anything, not just the checking that’s built into PHP.
This means you can make your code much more robust, by tightening up on the quality of the parameter passed into your code by other programmers. To extend our example, we might decide that an empty array is also unacceptable:
<?php
use Phix_Project\ContractLib\Contract;
class ActionToApply
{
public function appendNow($params)
{
Contract::Preconditions(function() use ($params)
{
Contract::RequiresValue(
$params,
is_array($params),
'$params must be an array'
);
Contract::RequiresValue(
$params,
count($params) > 0,
'$params cannot be an empty array'
);
});
// original method code continues here
$params[] = time();
}
}
The point here is that we can go way beyond type-hinting checks (important as they are) and look inside parameters to make sure they are suitable.
Here’s a real example from Phix’s CommandLineLib:
use Phix_Project\ContractLib\Contract;
class CommandLineParser
{
// ...
public function parseSwitches($args, $argIndex, DefinedSwitches $expectedOptions)
{
// catch programming errors
Contract::Preconditions(function() use ($args, $argIndex, $expectedOptions)
{
Contract::RequiresValue(
$args,
is_array($args),
'$args must be array'
);
Contract::RequiresValue(
$args,
count($args) > 0,
'$args cannot be an empty array'
);
Contract::RequiresValue(
$argIndex,
is_integer($argIndex),
'$argIndex must be an integer'
);
Contract::RequiresValue(
$argIndex,
count($args) >= $argIndex,
'$argIndex cannot be more than +1 beyond the end of $args'
);
Contract::RequiresValue(
$expectedOptions,
count($expectedOptions->getSwitches()) > 0,
'$expectTruncated by Planet PHP, read more at the original (another 2318 bytes)