通过PHP合同编程 - php

契约式编程是.NET中的一种现代趋势,但是PHP中的代码契约库/框架又如何呢?您如何看待这种范例对PHP的适用性?

谷歌搜索“代码合同的PHP”没有给我。

注意:“按合同编码”是指Design by contract,因此它与.NET或PHP接口无关。

参考方案

我是出于好奇而寻找同一件事,发现了这个问题,因此将尝试给出答案。

首先,从设计上来说,PHP并不是真正的代码契约。您甚至无法在需要时强制使用方法内部参数的核心类型¹,因此我几乎不相信有一天PHP中会存在代码契约。

让我们看看如果我们执行定制的第三方库/框架实现会发生什么。

1.前提条件

可以自由地将我们想要的所有内容传递给方法,这使得代码契约(或类似或多或少与代码契约相似)至少在先决条件下非常有价值,因为与普通编程相比,保护方法免受参数中的不良值的影响更加困难语言,其中可以通过语言本身强制执行类型。

编写起来会更方便:

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

代替:

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.后置条件:大问题

对于先决条件,用先决条件容易做到的事情仍然是不可能的。当然,您可以想像如下:

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

唯一的问题是,这种方法与代码契约无关,无论是在实现级别(就像前提条件示例),还是在代码级别(因为后置条件在实际的业务代码之前,而不是在代码和方法返回之间)。

这也意味着,如果方法或throw中有多个返回,则除非在每个$this->Ensure()return之前都包含throw(维护噩梦!),否则永远不会检查后置条件。

3.不变量:可能吗?

使用setter,可以在属性上模拟某种代码协定。但是setter在PHP中的实现如此糟糕,以至于会引起太多问题,并且如果使用setter代替字段,则自动补全将无法正常工作。

4.实施

最后,PHP并不是代码合同的最佳选择,并且由于其设计太差了,除非将来的语言设计发生重大变化,否则它可能永远不会有代码合同。

当前,关于后置条件或不变式,伪代码契约²毫无价值。另一方面,可以用PHP轻松编写一些伪先决条件,从而使对参数的检查更加优雅和简短。

这是此类实现的简短示例:

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);
        }
    }
}

当然,可以用日志继续/日志停止方法,错误页面等替换异常。

5.结论

观察预合同的执行情况,整个想法似乎一文不值。为什么我们要为那些伪造的代码契约而烦恼,这些契约实际上与普通编程语言中的代码契约有很大不同?它给我们带来了什么?几乎没有什么,除了事实是,我们可以像使用真实代码协定一样来编写支票。而且没有理由仅仅因为我们可以这样做。

为什么代码契约以普通语言存在?有两个原因:

因为它们提供了一种简单的方式来强制执行条件,而条件必须在代码块的开始或结束时匹配,
因为当我使用一个使用代码协定的.NET Framework库时,无需访问源代码³,就可以在IDE中轻松地知道该方法需要什么,该方法需要什么以及这些。

从我的角度来看,在PHP中实现伪代码契约时,第一个原因非常有限,第二个原因不存在,可能永远也不会存在。

这意味着实际上,简单地检查参数是一个不错的选择,特别是因为PHP与数组配合良好。这是一个旧的个人项目的复制粘贴:

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);
                }
            }
        }
    }
}

用法示例:

/// <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;
}

如果我们要检查不仅仅依赖于参数的前提条件(例如,检查前提条件中的属性值),这还不够。但是在大多数情况下,我们所需要做的只是检查参数,而PHP中的伪代码契约并不是做到这一点的最佳方法。

换句话说,如果您的唯一目的是检查自变量,那么伪代码合同就太过分了。当您需要更多东西时,它们可能是可能的,例如取决于对象属性的前提条件。但是在最后一种情况下,可能有更多的PHPy处理方法⁴,因此使用代码协定的唯一原因仍然存在:因为我们可以。

¹我们可以指定参数必须是类的实例。奇怪的是,没有办法指定参数必须是整数还是字符串。

²通过伪代码协定,我的意思是上面介绍的实现与.NET Framework中的代码协定的实现有很大不同。仅通过更改语言本身,才可能实现真正的实现。

³如果构建了合同参考程序集,或者甚至更好,如果在XML文件中指定了合同。

⁴一个简单的if - throw就能解决问题。

将大字符串分成多个小字符串-PHP - php

我从数据库中获取了一个长字符串,我需要对其进行解析,以使其不包含一个大字符串,而是多个,其中每个字符串都有2个字符。让我们以示例为例:我连接到表,获取此字符串:B1C1F4G6H4I7J1J8L5O6P2Q1R6T5U8V1Z5,之后,我必须对字符串进行解析,因此:B1 C1 F4 G6 H4 I7 J1 J8 L5 O6 P2 Q1 R6 T5 U8 V1…

PHP:不推荐使用password_hash的'salt'选项 - php

我正在使用密码哈希进行注册。我需要手动创建Salt,以下是我使用的代码:$options = [ 'cost' => 11, 'salt' => mcrypt_create_iv(22, MCRYPT_DEV_URANDOM) ]; $password = password_hash( $this->…

PHP-全局变量的性能和内存问题 - php

假设情况:我在php中运行一个复杂的站点,并且我使用了很多全局变量。我可以将变量存储在现有的全局范围内,例如$_REQUEST['userInfo'],$_REQUEST['foo']和$_REQUEST['bar']等,然后将许多不同的内容放入请求范围内(这将是适当的用法,因为这些数据指的是要求自…

PHP strtotime困境 - php

有人可以解释为什么这在我的服务器上输出为true吗?date_default_timezone_set('Europe/Bucharest'); var_dump( strtotime('29.03.2015 03:00', time()) === strtotime('29.03.2015 04:00�…

php-casperjs获取内部文本 - php

我正在为casperjs使用php包装器-https://github.com/alwex/php-casperjs我正在网上自动化一些重复的工作,我需要访问一个项目的innerText,但是我尚不清楚如何从casperjs浏览器访问dom。我认为在js中我会var arr = document.querySelector('label.input…