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 ContractLib
ContractLib 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 Project
Assuming you’re using a PSR-0 compatible autoloader, just import the Contract class into your PHP file:
use Phix_Project\ContractLib\Contract;
Adding A Pre-condition Contract To Your Method Or Function
Take a trivial method like this:
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.
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:
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, '$expectedOptions must have some switches defined' ); }); // method's code follows on here ... } }
In this real-life code, we start off by checking for basic errors first (by making sure we’re looking at the right type for each parameter), and then we follow up with more specific tests, that ensure that we have data that we’re happy to work with. We’ve done these tests at the start of the method, so that it isn’t cluttered with error checking, which makes our code much cleaner that it might otherwise be. And, because all the tests are in one really easy to spot block, anyone reading your code can immediately see what they have to do to meet the contract you’ve created.
Because these tests are just plain-old PHP code, and don’t rely on annotations or any other such nonsense, the contracts you create and enforce are limited only by your choices.
But Aren’t All Those Tests Slow?
They are. PHP’s getting better and better at this, but function/method calls have always been painfully slow in PHP. I’m afraid that if you want robust code, you can’t have it for free. (You can in C, but that’s a topic to discuss over a decent whiskey at a conference).
I’ve done key two things with ContractLib to keep the runtime cost down:
- Contract::Preconditions() accepts a lambda function as its parameter. Your contract’s tests go inside this lambda function, and Contract::Preconditions() only calls the lambda function if contracts are enabled.
- By default, ContractLib does not enable contracts. You have to choose to do so by calling Contract::EnforceWrappedContracts().
This keeps the overhead down to just one method call (to Contract::Preconditions()) when contracts are not enabled. It isn’t as good as having no overhead, but it’s cheaper than the developer time lost trying to track down bugs in code that always assumes the caller can be trusted to do the right thing every time.
Any Questions?
I hope these examples have given you an idea on how to get started with ContractLib. If you have any questions or suggestions, please let me know, and I’ll do my best to answer them.