A few weeks ago I went to the beach, and finally had some time to read. I picked up a copy of 'The Pragmatic Programmer' just because it was on my list and somehow I still had not read it. Overall it's a very good book that I encourage every programmer to read.
One of the sections it went over was on the idea of 'Design by Contract', or DbC. At it's simpliest, which is about as far as PHP can go out of the box, is the idea that each object has a contract it wants to enforce via it's API. You might have the following object:
class Meal
{
/**
* Serves items to guests
* @param int $numGuests Number of guests to serve
* @param array $foodItems Food items to serve guests
* @return bool
function serve($numGuests, $foodItems) {
// ...
}
}
The contract for the Meal::serve() method is this:
- You will send me the number of guests as an integer
- You will send me the food items as an array
- I will return to you a boolean value
We could go further and use type hinting to further enforce this contract, but PHP doesn't have support for type hinting basic scalar values. You are then left with this sort of 'verbal' contract where the object and the caller shake hands and agree to play nice.
In 'The Pragmatic Programmer', the authors describe something more advanced. This is the idea of actually enforcing the contract to better help with testing and making sure that logic is followed. This enters into the idea of Preconditions, which must be true before the code is called, and Postconditions, which must be true when the method is finished. There is also the idea of a Class Invariant, which always must be true on an object.
This programmatic idea is part of the Eiffel programming langauge. You add some conditions to your function and these must be true for everything to work correctly.
How do this in PHP?
Magic and Traits
PHP 5.4 added the idea of traits, which allows classes to use code as part of itself without having to extend a class. It would look like this:
trait MyTrait {
public function hello() {
echo 'Hello';
}
}
class MyClass extends SomethingElse {
use MyTrait;
}
$class = new MyClass();
$class->hello();
PHP also supports the idea of magic methods, like __get, __set, and __call. These are invoked when the object can't find a public method or member. __call() is used many times to provide access to protected methods/members for automatic creation of getter and setter methods. This got me thinking.
Let's put them together
I started working on Pacts, which wraps method calls with __call() and the Reflection API to look at DocBlock notation to automatically generate contracts. You can do this:
class Meal
{
use Pacts\Pact;
/**
* Serves items to guests
* @param int $numGuests Number of guests to serve
* @param array $foodItems Food items to serve guests
* @return bool
protected function serve($numGuests, $foodItems) {
// ...
}
}
All we did was use Pacts\Pact
as a trait, and changed serve() to be a
protected method. Pacts will automatically make sure that $numGuests is an
integer, that $foodItems is an array, and that the return value is a boolean.
It also supports an @pre
comment, which lists a function and what arguments to pass:
class Meal
{
use Pacts\Pact;
/**
* Serves items to guests
*
* @pre isValidFood 2
*
* @param int $numGuests Number of guests to serve
* @param array $foodItems Food items to serve guests
* @return bool
protected function serve($numGuests, $foodItems) {
// ...
}
protected function isValidFood($foodItems) {
// ...
}
}
In the above case, we'll send the 2nd argument ($foodItems) to a function on the object called isValidFood(). This will check to make sure all of the items in the array are valid, or throw an exception.
The current version of Pacts only does preconditions as well as returns false for failures, where is should throw an exception. It's only about 4 hours worth of coding though and is as much as a proof of concept than anything.
Some Downsides
Brian Fenton took some time to look over the code and had some comments, which I hearly acknowledge or have answers for.
Strong Hinting
He mentioned the use of typehinting using SplTypes. I had not seen this project before and I hope that it becomes a bit more mainstream, but currently Traits are supported while SplTypes is an experimental extension.
Strong Hinting also doesn't support complex logic, like the @pre condition does.
Lots of Magic
There is a lot of magic here, but I didn't see any better way to wrap method calls and have other methods fire before and after without a ton of boilerplate. I would like a non-magical way do this that didn't though.
Use ctype_* or assert()
I think these functions could be integrated, but I think what Brian was getting at was to just use those instead of the automatic stuff like what Pacts is doing. One motivation for Pacts is to reduce the amount of code I have to write. Using ctypes_*, is_*, or assert() directly in each method call is tedious. The basic @param checking in Pacts already calls the is_* functions for me.
Looking for Feedback
Pacts is implemented with my limited knowledge of Design by Contract. I'd love some more feedback. I plan on extending this in my free time for anyone that is interested.