ntwalibas/contracts

合约库,一个基于合约的库。

0.1.1 2015-06-19 12:56 UTC

This package is not auto-updated.

Last update: 2024-09-28 18:10:56 UTC


README

状态: 完全测试覆盖,下一个目标是更多谓词和更好的文档。

这个库 有点 可以在生产环境中使用。请勿将其用于敏感数据验证,如与金钱相关的领域。

Contracts 是一个库,可以帮助您编写非常有趣的断言。最初,我试图在 PHP 中实现设计合约,但由于任务的性质,我最终决定只实现断言,但保留了名称。

它非常强大,因为它以直观的方式实现了 一阶谓词演算,这是我在许多断言和验证库中未能找到的特性。嘿,您甚至可以自己实现自己的谓词。
它还处于早期阶段(许多原生谓词尚未实现),但它旨在不断发展!

安装

此库可在 packagist 上找到,并通过 composer 安装。

{
    "require": {
        "ntwalibas/contracts": "0.1.1"
    }
}

概念

您通常编写当需要使用时才会评估的命题。每个命题由谓词组成,这些谓词可以通过逻辑运算符(如 ANDORIMPLIESEQUIVALENT)连接起来。尚未实现 NOT 逻辑运算符,而是用否定谓词代替。

用法

用法相当简单:将您的谓词或量化词传递给 AssertThat 函数或 Assert::That 静态方法,如果发生任何失败,它将抛出 AssertionFailedException 异常。
首先让我们了解谓词、量化词和计算的概念。

谓词

谓词是一个简单返回 truefalse 的函数。在我们的情况下,这些函数将是特定对象上的方法。

在谓词演算中,谓词将具有变量,这些变量在评估时取不同的值。我们这里也是这样,只是有一些语法上的差异。以下是一个示例。

假设我们想要评估给定的数字是否大于 18。那么我们怎么做呢?

<?php
use Contracts\Helpers\Constant;
use function Contracts\Helpers\constx; // As of PHP 5.6+

// PHP 5.5-
// You could as well initialize this object in your class where it will be used
function constx($value)
{
    $constant = new Constant;
    return $constant($value);
}

$age = 17;
$predicate = constx($age)->greaterThan(18);
var_dump($predicate->evaluate());

请注意第一个约定:$age 并非我们通常理解的 PHP 中的常量。但在这里我们将其视为常量,因为一旦其值被设置,在谓词接收到后就不会改变。
您接下来可能会问的是为什么是 const**x** 而不是 const?这是因为 const 是 PHP 的关键字。实际上,每次您准备使用库提供的任何方法或函数,并且它是 PHP 关键字或保留字列表中的一部分时,它后面都会跟着一个 x

现在回到库的 constants 上。这里的一个自然问题是:库将什么视为变量?以下是一个示例。
假设我们有一个将用户姓名映射到其年龄的数组,并且我们想知道他们是否是成年人或未成年人。使用“变量”,我们可以遍历该数组并使用谓词进行检查。

<?php
use Contracts\Helpers\Variable;
use function Contracts\Helpers\varx; // As of PHP 5.6+

// PHP 5.5-
function varx($symbol)
{
    $variable = new Variable;
    return $variable($symbol);
}

$userAges = array(
    "John" => 11, 
    "Marie" => 21, 
    "Paul" => 13, 
    "Dupont" => 42, 
    "Dixit" => 15, 
    "Avinash" => 56, 
    "Bora" => 27
);

$predicate = varx("age")->greaterThan(18);

foreach ($userAges as $name => $age) {
    $predicate->setOperand("age", $age);
    if ($predicate->evaluate() === true) {
        print "$name is a legal adult <br>";
    } else {
        print "$name is a minor <br>";
    }
}

这就是变量的概念:变量是一个可以在执行上下文中取不同值的符号。这里的执行上下文是 foreach 循环。在这里,年龄将在循环执行时保持变化。使用 setOperand(string $symbol, mixed $value) 更新由给定符号表示的变量的值。
注意:我们将稍后看到使用量词检查所有用户是否成年的一种更好的方法。

更多关于变量的内容

I. 如果您想声明一个新变量而不是使用常量(出于某种原因),则提供了一个辅助函数。

<?php
use Contracts\Helpers\Let;
use function Contracts\Helpers\let; // PHP 5.6+

// PHP 5.5-
function let($symbol)
{
    return new Let($symbol)
}

let("age")->be(18);
// Now you can use "age" bellow in predicates and it will have the value of 18

II. 对象和数组允许一个额外的特性:假设您将一个特定的符号绑定到对象或数组。您可以通过键(必须是字符串)访问对象或数组元素上的方法(例如,获取器,无需参数)。

** 对象示例:**

<?php
class User
{
    protected $age = 18;

    public function age()
    {
        return $this->age;
    }
}

$user = new User;
let("user")->be($user);

// The age method will be called on the user object
$predicate = varx("user:age")->greaterThan(18);
var_dump($predicate->evaluate());

** 数组示例:**

<?php
$user = ["age" => 12];
let("user")->be($user);

// The age key will be access on the user array
$predicate = varx("user:age")->greaterThan(18);
var_dump($predicate->evaluate());

所有谓词

合约提供不同类型的谓词,分为不同的数据类型。有些谓词仅适用于数字,有些适用于数组,依此类推。提供了辅助函数,因此您无需自己实例化实现这些谓词的类。在此,类 VariableConstant 将实例化所有谓词(及更多),以便您开始使用它们。但这不是推荐的做法,因为这会带来您可能不需要的某些开销。如果您只想针对特定的给定情况处理数字(包括整数和浮点数),请使用 Number 辅助函数。对于所有其他数据类型也是如此。以下是所有辅助函数的列表。

<?php
// Booleans
use Contracts\Helpers\Boolean;
use function Contracts\Helpers\boolx;

// Numbers: ints and floats
use Contracts\Helpers\Number;
use function Contracts\Helpers\number;

// Strings
use Contracts\Helpers\StringX;
use function Contracts\Helpers\stringx;

// Arrays
use Contracts\Helpers\ArrayX;
use function Contracts\Helpers\arrayx;

// Objects
use Contracts\Helpers\ObjectX;
use function Contracts\Helpers\objectx;

// Resource
use Contracts\Helpers\ResourceX;
use function Contracts\Helpers\resourcex;

// Callables: not included at the moment but sure is coming

有时您可能需要一个库原生不提供的组合。这可以通过以下方式轻松实现

<?php
use Contracts\Operators;
use Contracts\Predicates\NumberPredicates;
use Contracts\Predicates\StringPredicates;

function varx($symbol)
{
    $predicates = new StringPredicates(new NumberPredicates(new Operators));
    $predicates->setSymbol($symbol);

    return $predicates;
}

无论您是否打算使用逻辑运算符,都必须将 Operators 实例作为链的第一个参数传递。因为它执行谓词没有做的另一项工作。其余的可以按任何顺序传递。StringPredicates 在没有问题的前提下可以放在 NumberPredicates 之前。
从那时起,您可以使用 varx 如以前一样。

逻辑运算符

您可以通过使用逻辑运算符如这样组合谓词

<?php
$age = 17;
// We do not believe people older than 120 use our product
$predicate = constx($age)->greaterThan(18)->andx()->constx($age)->lessThan(120);
var_dump($predicate->evaluate());

实际上,如果第二个谓词将重用第一个谓词的运算符(在本例中为 $age),则不需要在它之前有 constx($age)varx($age)。所以下面的做法同样正确且简洁

<?php
$age = 17;
// We do not believe people older than 120 use our product
$predicate = constx($age)->greaterThan(18)->andx()->lessThan(120);
var_dump($predicate->evaluate());

以下逻辑运算符可用

<?php

andx()
orx()
implies()
isEquivalentTo()

计算

有时您可能想在运行谓词之前对传递给谓词的变量(常量)执行计算。这就是计算介入的地方。
假设用户提供了他们的出生年份,并且您想知道他们是否是成年人

<?php
$yob = 1990;
$predicate = constx(2015)->minus($yob)->greaterThan(18);

var_dump($predicate->evaluate());

计算旨在在您想对操作数进行转换以传递给约束时简化事情,而无需在业务逻辑中添加额外的计算来污染它。

量词

合约目前提供两个量词:ForAllThereExists

ForAll

使用 ForAll 确保可遍历中的所有元素都遵守给定的谓词。回到我们之前的例子:假设我们想确保所有用户都是合法的成年人。我们会这样做

<?php
use Contracts\Quantifiers\ForAll;
use function Contracts\Helpers\forAll; // PHP 5.6+

// PHP 5.5-
function forAll($symbol)
{
    return new ForAll($symbol);
}

$userAges = array(   
    [
        "name" => "John",
        "age" => 11
    ],
    [
        "name" => "Marie",
        "age" => 21
    ],
    [
        "name" => "Paul",
        "age" => 13
    ],
    [
        "name" => "Dupont",
        "age" => 42
    ],
    [
        "name" => "Dixit",
        "age" => 15
    ],
    [
        "name" => "Avinash",
        "age" => 56
    ],
    [
        "name" => "Bora",
        "age" => 27
    ]
);

$allAdults =
forAll("user")->in($userAges)->itHoldsThat(
    varx("user:age")->greaterThan(18)
);

var_dump($allAdults->evaluate()); // Return false

ThereExists

其原理与 ForAll 量词相同。

<?php
use Contracts\Quantifiers\ThereExists;
use Contracts\Helpers\thereExists; // PHP 5.6+

// PHP 5.5-
function thereExists($symbol)
{
    return new ThereExists($symbol);
}

$userAges = array(   
    [
        "name" => "John",
        "age" => 11
    ],
    [
        "name" => "Marie",
        "age" => 21
    ],
    [
        "name" => "Paul",
        "age" => 13
    ],
    [
        "name" => "Dupont",
        "age" => 42
    ],
    [
        "name" => "Dixit",
        "age" => 15
    ],
    [
        "name" => "Avinash",
        "age" => 56
    ],
    [
        "name" => "Bora",
        "age" => 27
    ]
);

$oneAdult =
thereExists("user")->in($userAges)->suchThat(
    varx("user:age")->greaterThan(18)
);

var_dump($oneAdult->evaluate()); // Return true

量词组合

您可以像传递谓词一样将一个量词传递给另一个量词。

<?php
$set = array(1, 2, 3, 4, 5, 6, 7);

$test =
forAll('x')->in($set)->itHoldsThat(
    thereExists('y')->in($set)->suchThat(
        varx('x')->dividedBy('y')->equalTo(1)
    )
);

var_dump($test->evaluate()); // Will print true

请注意,在上面的示例中,对于该集合中的所有元素,您始终可以找到另一个相同的元素(即该元素本身),其除法等于一。

断言

要运行断言,只需将您的谓词或量词传递给 AssertThat 函数或 Assert::That 静态方法。需要一个额外的参数来记录断言。
实际上,您还可以将谓词/量词数组作为第一个参数传递,如果您想分组断言,因此请确保提供标识每个断言以在抛出异常时理解消息的键。

<?php
use Contracts\Assertions\Assert;
use Contracts\Assertions\AssertionFailedException;
use function Contracts\Helpers\AssertThat; // PHP 5.6+

// PHP 5.5-
function AssertThat($predicate, $doc)
{
    Assert::That($predicate, $doc);
}

$set = array(1, 2, 3, 4, 5, 6, 7);

try {
    AssertThat(
        forAll('x')->in($set)->itHoldsThat(
            varx('x')->greaterThan(0)->andx()->lessThan("7")
        ),
        "All the numbers must be greater than 0 and less than 7"
    );
} catch (AssertionFailedException $exception) {
    echo $exception->getMessage(); // Will print a message with enough details to know the problem.
    
    // Get the constraints that were provided to the predicates
    $exception->getConstraints($assertionId); // The assertion ID in our case would be "unnamed-assertion" because we did not name the assertion
    
    // Get the operands that we provided to the predicates
    $exception->getOperands($assertionId); 
    
    // Get any possible exceptions throw by either the predicates or quantifiers
    $exception->getExceptions($assertionId);   
}

要命名一个断言,请将具有以下结构的数组["assertionId" => "predicate|quantifier"]作为第一个参数传递给AssertThat。如果您直接传递了谓词或量词而没有断言ID,则将赋予ID unnamed-assertion

结论

该库提供了一个非常直观但仍然要牺牲某些东西的API。在这种情况下,性能将低于大多数其他“轻量级”断言/验证库。但我怀疑这种影响不会很明显,更不用说这个声明是基于__call方法的使用,而这将是性能瓶颈。

作者

Ntwali Bashige - ntwali.bashige@gmail.com - http://twitter.com/nbashige

许可证

Contracts遵循MIT许可证,请参阅LICENSE文件。

致谢

内部,Contracts使用dissect来正确评估布尔表达式。

下一步

  1. 追求全面测试覆盖
  2. 编写更多谓词和计算
  3. 用一个小型递归下降解析器替换布尔表达式解析器,以消除对dissect的依赖
  4. 找到一种方法来减少对__call的依赖

欢迎贡献力量。如果您有要添加的内容,请发送pull request。测试特别受欢迎!