开发者

Programming by contracts in PHP

开发者 https://www.devze.com 2023-01-22 03:53 出处:网络
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?

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 interface开发者_JAVA技巧s.


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.

1. Preconditions

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.
}

2. Postconditions: big problems

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!).

3. Invariants: possible?

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.

4. Implementation

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.

5. Conclusion

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:

  • Because they provide a simple way to enforce conditions which must be matched when a block of code starts or finishes,
  • Because when I use a .NET Framework library which uses code contracts, I can easily know within the IDE what is required by the method, and what is expected from the method, and this, without having an access to the source code³.

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.


An interface is not a contact(in fact, Laravel definition is wrong), Design By Contract (DbC) is a software correctness methodology. It uses preconditions and postconditions to document (or programmatically assert) the change in state caused by a piece of a program. I find a good php approach here


I am guessing that WikiPedia mentions Component Oriented Software Methodologies. In such methodologies methods are referred as Public Interfaces or Contracts of the component.

A contract is a 'kind-of-agreement' between the provider of the service and the client. In a component environment where systems are composed by components by various creators/vendors the 'construction' of your contracts is of critical importance.

In such environments, think of your component as a black-box that MUST be able to efficiently co-exist and collaborate with other components that other people have created, thus forming a larger system or sub-system of a larger system, etc.

For more details i can suggest you google for the 'Component Software - Beyond Component Oriented Programming' book, for all things related to component oriented programming.

0

精彩评论

暂无评论...
验证码 换一张
取 消