Programming by contracts is a modern trend in .NET, but what about libraries/frameworks for code contracts in PHP? What do you think about applicability of this paradigm for PHP?
Googling for "code contracts php" gave nothing to me.
Note: by "code by contract", I mean Design by contract, so it has nothing to do with .NET or PHP interfaces.
Introduction. Laravel's "contracts" are a set of interfaces that define the core services provided by the framework. For example, an Illuminate\Contracts\Queue\Queue contract defines the methods needed for queueing jobs, while the Illuminate\Contracts\Mail\Mailer contract defines the methods needed for sending e-mail.
Code contracts provide a way to specify preconditions, postconditions, and object invariants in . NET Framework code. Preconditions are requirements that must be met when entering a method or property. Postconditions describe expectations at the time the method or property code exits.
It prescribes that software designers should define formal, precise and verifiable interface specifications for software components, which extend the ordinary definition of abstract data types with preconditions, postconditions and invariants.
I was searching for the same thing by curiosity, and found this question, so will try to give an answer.
First, PHP, by design, is not really code-contracty. You cannot even enforce, when need, the core types¹ of parameters inside the methods, so I hardly believe that code contracts will exist in PHP one day.
Let's see what happens if we do a custom, third party library/framework implementation.
The freedom of passing everything we want to a method makes code contracts (or something more or less similar to code contracts) very valuable, at least on preconditions, since protecting methods against bad values in arguments is more difficult to do, comparing to normal programming languages, where types can be enforced through the language itself.
It would be more convenient to write:
public function AddProduct($productId, $name, $price, $isCurrentlyInStock)
{
Contracts::Require(__FILE__, __LINE__, is_int($productId), 'The product ID must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_string($name), 'The product name must be a string.');
Contracts::Require(__FILE__, __LINE__, is_int($price), 'The price must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_bool($isCurrentlyInStock), 'The product availability must be an boolean.');
Contracts::Require(__FILE__, __LINE__, $productId > 0 && $productId <= 5873, 'The product ID is out of range.');
Contracts::Require(__FILE__, __LINE__, $price > 0, 'The product price cannot be negative.');
// Business code goes here.
}
instead of:
public function AddProduct($productId, $name, $price, $isCurrentlyInStock)
{
if (!is_int($productId))
{
throw new ArgumentException(__FILE__, __LINE__, 'The product ID must be an integer.');
}
if (!is_int($name))
{
throw new ArgumentException(__FILE__, __LINE__, 'The product name must be a string.');
}
// Continue with four other checks.
// Business code goes here.
}
What is easy to do with preconditions remains impossible for postconditions. Of course, you can imagine something like:
public function FindLastProduct()
{
$lastProduct = ...
// Business code goes here.
Contracts::Ensure($lastProduct instanceof Product, 'The method was about to return a non-product, when an instance of a Product class was expected.');
return $lastProduct;
}
The only problem is that this approach has nothing to do with code contracts, neither at the implementation level (just like a preconditions example), nor on code level (since postconditions go before actual business code, not between code and method return).
It also means that if there are multiple returns in a method or a throw
, postcondition will never be checked, unless you include the $this->Ensure()
before every return
or throw
(maintenance nightmare!).
With setters, it is possible to emulate some sort of code contracts on properties. But setters are so badly implemented in PHP, that this will cause too many problems, and auto-completion will not work if setters are used instead of fields.
To finish, PHP is not a best candidate for code contracts, and since its design is so poor, it will probably never have code contracts, unless there will be substantial changes in future in the language design.
Currently, pseudo-code contracts² are pretty worthless when it comes to postconditions or invariants. On the other hand, some pseudo-preconditions can be easily written in PHP, making checks on arguments much more elegant and shorter.
Here's a short example of such implementation:
class ArgumentException extends Exception
{
// Code here.
}
class CodeContracts
{
public static function Require($file, $line, $precondition, $failureMessage)
{
Contracts::Require(__FILE__, __LINE__, is_string($file), 'The source file name must be a string.');
Contracts::Require(__FILE__, __LINE__, is_int($line), 'The source file line must be an integer.');
Contracts::Require(__FILE__, __LINE__, is_string($precondition), 'The precondition must evaluate to a boolean.');
Contracts::Require(__FILE__, __LINE__, is_int($failureMessage), 'The failure message must be a string.');
Contracts::Require(__FILE__, __LINE__, $file != '', 'The source file name cannot be an empty string.');
Contracts::Require(__FILE__, __LINE__, $line >= 0, 'The source file line cannot be negative.');
if (!$precondition)
{
throw new ContractException('The code contract was violated in ' . $file . ':' . $line . ': ' . $failureMessage);
}
}
}
Of course, an exception may be replaced by log-and-continue/log-and-stop approach, a error page, etc.
Looking at the implementation of precontracts, the whole idea seems worthless. Why are we bothering with those pseudo-code contracts, which are actually very different from code contracts in normal programming languages? What does it brings to us? Pretty nothing, except the fact that we can write the checks in the same way as if we were using real code contracts. And there is no reason to do this just because we can.
Why code contracts exist in normal languages? For two reasons:
From what I see, in an implementation of pseudo-code contracts in PHP, the first reason is very limited, and the second one does not exist and will probably never exist.
It means that actually, a simple check of arguments is a good alternative, especially since PHP works well with arrays. Here's a copy-paste from an old personal project:
class ArgumentException extends Exception
{
private $argumentName = null;
public function __construct($message = '', $code = 0, $argumentName = '')
{
if (!is_string($message)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. String value expected.', 0, 'message');
if (!is_long($code)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. Integer value expected.', 0, 'code');
if (!is_string($argumentName)) throw new ArgumentException('Wrong parameter for ArgumentException constructor. String value expected.', 0, 'argumentName');
parent::__construct($message, $code);
$this->argumentName = $argumentName;
}
public function __toString()
{
return 'exception \'' . get_class($this) . '\' ' . ((!$this->argumentName) ? '' : 'on argument \'' . $this->argumentName . '\' ') . 'with message \'' . parent::getMessage() . '\' in ' . parent::getFile() . ':' . parent::getLine() . '
Stack trace:
' . parent::getTraceAsString();
}
}
class Component
{
public static function CheckArguments($file, $line, $args)
{
foreach ($args as $argName => $argAttributes)
{
if (isset($argAttributes['type']) && (!VarTypes::MatchType($argAttributes['value'], $argAttributes['type'])))
{
throw new ArgumentException(String::Format('Invalid type for argument \'{0}\' in {1}:{2}. Expected type: {3}.', $argName, $file, $line, $argAttributes['type']), 0, $argName);
}
if (isset($argAttributes['length']))
{
settype($argAttributes['length'], 'integer');
if (is_string($argAttributes['value']))
{
if (strlen($argAttributes['value']) != $argAttributes['length'])
{
throw new ArgumentException(String::Format('Invalid length for argument \'{0}\' in {1}:{2}. Expected length: {3}. Current length: {4}.', $argName, $file, $line, $argAttributes['length'], strlen($argAttributes['value'])), 0, $argName);
}
}
else
{
throw new ArgumentException(String::Format('Invalid attributes for argument \'{0}\' in {1}:{2}. Either remove length attribute or pass a string.', $argName, $file, $line), 0, $argName);
}
}
}
}
}
Usage example:
/// <summary>
/// Determines whether the ending of the string matches the specified string.
/// </summary>
public static function EndsWith($string, $end, $case = true)
{
Component::CheckArguments(__FILE__, __LINE__, array(
'string' => array('value' => $string, 'type' => VTYPE_STRING),
'end' => array('value' => $end, 'type' => VTYPE_STRING),
'case' => array('value' => $case, 'type' => VTYPE_BOOL)
));
$stringLength = strlen($string);
$endLength = strlen($end);
if ($endLength > $stringLength) return false;
if ($endLength == $stringLength && $string != $end) return false;
return (($case) ? substr_compare($string, $end, $stringLength - $endLength) : substr_compare($string, $end, $stringLength - $endLength, $stringLength, true)) == 0;
}
It will not be enough if we want to check preconditions which are not just dependent of arguments (for example checking a value of a property in a precondition). But in most cases, all we need is to check arguments, and pseudo-code contracts in PHP are not the best way to do it.
In other words, if your only purpose is to check the arguments, pseudo-code contracts are an overkill. They may be possible when you need something more, like a precondition which depends on an object property. But in this last case, there are probably more PHPy ways to do things⁴, so the only reason to use code contracts stays: because we can.
¹ We can specify that an argument must be an instance of a class. Curiously, there is no way to specify that an argument must be an integer or a string.
² By pseudo-code contracts, I mean that the implementation presented above is very different from the implementation of code contracts in .NET Framework. The real implementation would be possible only by changing the language itself.
³ If Contract Reference Assembly is built, or, even better, if contracts are specified in an XML file.
⁴ A simple if - throw
can do the trick.
I have created PHP-Contract,
A lightweight and versatile implementation of C# contracts for PHP. These contracts, in many ways, surpass the functionality in C#. Please check out my Github project, grab a copy, and take a look at the wiki.
https://github.com/axiom82/PHP-Contract
Here is a basic example:
class Model {
public function getFoos($barId, $includeBaz = false, $limit = 0, $offset = 0){
$contract = new Contract();
$contract->term('barId')->id()->end()
->term('includeBaz')->boolean()->end()
->term('limit')->natural()->end()
->term('offset')->natural()->end()
->metOrThrow();
/* Continue with peace of mind ... */
}
}
For documentation, please visit the wiki.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With