lisachenko/php-deal

此包已被废弃,不再维护。作者建议使用php-deal/framework包。

设计契约框架用于PHP

1.0.0 2019-01-27 09:12 UTC

README

设计契约框架用于PHP

什么是设计契约?

类或接口的规范是非私有项目的集合,这些项目作为服务提供给调用者,包括使用说明,如phpDoc中所述。《设计契约》是一种创建规范的有效技术。

设计契约的基本思想是将类或接口提供的服务视为类(或接口)与其调用者之间的契约。在这里,“契约”一词旨在传达双方之间的一种正式、明确的协议。

  • 类对调用者提出的要求
  • 类对调用者做出的承诺

如果调用者满足要求,则类承诺提供一些明确定义的服务。对规范/契约的一些修改会破坏调用者,而另一些则不会。为了确定修改是否会破坏调用者,C++ FAQ使用了一个容易记住的短语“要求不多,承诺不少”:如果新的规范对调用者的要求不比以前多,并且它不承诺提供比以前更少的,则新的规范与旧规范兼容,不会破坏调用者。

Build Status GitHub release Code Coverage Scrutinizer Code Quality Minimum PHP Version License

安装

可以使用composer安装PhpDeal框架。安装很简单,只需运行以下命令让composer下载框架及其依赖项:

$ composer require php-deal/framework

配置

将以下代码放在应用程序入口点的开头或从外部文件中包含它。

$instance = ContractApplication::getInstance();
$instance->init(array(
    'debug'    => true,
    'appDir'   => __DIR__,
    'excludePaths' => [
        __DIR__ . '/vendor'
    ],
    'includePaths' => [

    ],
    'cacheDir' => __DIR__.'/cache/',
));

Symfony配置

将以下代码放入app_dev.php中,并根据您的文件夹结构进行调整。appDir必须指向包含src文件的文件夹,而不是根目录文件夹!

$instance = ContractApplication::getInstance();
$instance->init(array(
    'debug'    => true,
    'appDir'   => __DIR__ . '/../src',
    'excludePaths' => [

    ],
    'includePaths' => [

    ],
    'cacheDir' => __DIR__.'/var/cache/',
));

前置和后置契约

前置契约指定在执行语句之前必须满足的先决条件(要求)。最典型的用途是在验证函数参数时。后置契约(承诺)验证语句的结果。最典型的用途是在验证方法返回值及其任何副作用时。语法是

<?php
namespace Vendor\Namespace;

use PhpDeal\Annotation as Contract; //import DbC annotations

/**
 * Some account class
 */
class Account
{

    /**
     * Current balance
     *
     * @var float
     */
    protected $balance = 0.0;

    /**
     * Deposits fixed amount of money to the account
     *
     * @param float $amount
     *
     * @Contract\Verify("$amount>0 && is_numeric($amount)")
     * @Contract\Ensure("$this->balance == $__old->balance+$amount")
     */
    public function deposit($amount)
    {
        $this->balance += $amount;
    }
}

根据定义,如果前置契约失败,则接收到的参数是错误的。抛出ContractViolation异常。如果后置契约失败,则表示主体存在错误。抛出ContractViolation异常。

不变量

不变量用于指定类必须始终为真的特征(除非在执行受保护的或私有成员函数时)。

不变量是一个契约,说明断言必须为真。在类构造函数完成后以及在类公共方法结束时检查不变量。

<?php
namespace Vendor\Namespace;

use PhpDeal\Annotation as Contract; //import DbC annotations

/**
 * Some account class
 *
 * @Contract\Invariant("$this->balance > 0")
 */
class Account
{

    /**
     * Current balance
     *
     * @var float
     */
    protected $balance = 0.0;

    /**
     * Deposits fixed amount of money to the account
     *
     * @param float $amount
     */
    public function deposit($amount)
    {
        $this->balance += $amount;
    }
}

不变量包含断言表达式,因此当它们失败时,会抛出ContractViolation异常。

注意:不变量中的代码不能直接或间接调用类的任何公共非静态成员。这样做会导致栈溢出,因为不变量将以无限递归的方式被调用。

合同传播

合同在继承中存在一些差异

  1. 确保
  • 如果提供了Ensure,将自动从父类或接口继承所有合同
  1. 验证
  • 如果提供了Verify,将不会从父类或接口继承合同
  • 要继承合同,您需要提供@inheritdocInherit合同
  1. 不变量
  • 如果提供了Invariant,将自动从父类或接口继承所有合同
  1. Inherit
  • 如果提供了Inherit,将自动从给定级别(类、方法)继承所有合同,无需在当前类或方法上提供合同

注意:

  • 只有当您提供该包中的任何给定注释时,才会解析合同。如果没有它,您的合同将无法工作!
  • 注释不得有花括号({}),否则注释读取器无法找到它们。
class Foo extends FooParent
{
    /**
     * @param int $amount
     * @Contract\Verify("$amount != 1")
     */
    public function bar($amount)
    {
        ...
    }
}
    
class FooParent
{
    /**
     * @param int $amount
     * @Contract\Verify("$amount != 2")
     */
    public function bar($amount)
    {
        ...
    }
}
    

Foo::bar接受2个字面量参数,不接受1

使用@inheritdoc

class Foo extends FooParent
{
    /**
     * @param int $amount
     * @Contract\Verify("$amount != 1")
     * {@inheritdoc}
     */
    public function bar($amount)
    {
        ...
    }
}
    
class FooParent
{
    /**
     * @param int $amount
     * @Contract\Verify("$amount != 2")
     */
    public function bar($amount)
    {
        ...
    }
}
    

Foo::bar不接受12个字面量参数。

对于后置条件(Ensure和Invariant合同),子类继承合同,它们不需要@inheritdoc。示例

    
/**
 * @Contract\Invariant("$this->amount != 1")
 */
class Foo extends FooParent
{
    
}

/**
 * @Contract\Invariant("$this->amount != 2")
 */
class FooParent
{
    /**
     * @var int
     */
    protected $amount;
    
    /**
     * @param int $amount
     */
    protected function setBar($amount)
    {
        $this->amount = $amount;
    }
}
    

Foo::setBar不接受12个字面量参数。

如果您不希望在当前方法/类上提供合同,可以使用Inherit注释

class Foo extends FooParent
{
    /**
     * @param int $amount
     * @Contract\Inherit
     */
    public function bar($amount)
    {
        ...
    }
}
    
class FooParent
{
    /**
     * @param int $amount
     * @Contract\Verify("$amount != 2")
     */
    public function bar($amount)
    {
        ...
    }
}

Foo:bar()接受除2之外的所有内容

与断言库集成

为了增强合同的功能,可以使用断言库

    /**
     * Deposits fixed amount of money to the account
     *
     * @param float $amount
     *
     * @Contract\Ensure("Assert\Assertion::integer($this->balance)")
     */
    public function deposit($amount)
    {
        $this->balance += $amount;
    }

更多断言

IDE集成

为了提高您的PhpStorm生产力,您应该安装一个Go! AOP Framework插件(>=1.0.1),以实现定义合同和导航到AOP建议的PHP语法高亮显示。PhpStorm示例

常见问题

致命错误:未捕获的Error: Class 'Go\ParserReflection\Instrument\PathResolver'
Fatal error: Uncaught Error: Class 'Go\ParserReflection\Instrument\PathResolver' 
not found in .../vendor/goaop/parser-reflection/src/ReflectionEngine.php on line XXX

如果您的appDir配置指向与您的vendor目录相同的级别,则会出现这种情况。要解决此问题,请尝试将您的vendor文件夹添加到excludePaths配置中。

ContractApplication::getInstance()->init(array(
    'debug'    => true,
    'appDir'   => __DIR__,,
    'excludePaths' => [
        __DIR__ . '/vendor'
    ],
    'cacheDir' => __DIR__.'/cache/',
));