ntwalibas / contracts
合约库,一个基于合约的库。
Requires
- php: >=5.3.0
- jakubledl/dissect: v1.0.0
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"
}
}
概念
您通常编写当需要使用时才会评估的命题。每个命题由谓词组成,这些谓词可以通过逻辑运算符(如 AND、OR、IMPLIES 和 EQUIVALENT)连接起来。尚未实现 NOT 逻辑运算符,而是用否定谓词代替。
用法
用法相当简单:将您的谓词或量化词传递给 AssertThat 函数或 Assert::That 静态方法,如果发生任何失败,它将抛出 AssertionFailedException 异常。
首先让我们了解谓词、量化词和计算的概念。
谓词
谓词是一个简单返回 true 或 false 的函数。在我们的情况下,这些函数将是特定对象上的方法。
在谓词演算中,谓词将具有变量,这些变量在评估时取不同的值。我们这里也是这样,只是有一些语法上的差异。以下是一个示例。
假设我们想要评估给定的数字是否大于 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());
所有谓词
合约提供不同类型的谓词,分为不同的数据类型。有些谓词仅适用于数字,有些适用于数组,依此类推。提供了辅助函数,因此您无需自己实例化实现这些谓词的类。在此,类 Variable 和 Constant 将实例化所有谓词(及更多),以便您开始使用它们。但这不是推荐的做法,因为这会带来您可能不需要的某些开销。如果您只想针对特定的给定情况处理数字(包括整数和浮点数),请使用 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());
计算旨在在您想对操作数进行转换以传递给约束时简化事情,而无需在业务逻辑中添加额外的计算来污染它。
量词
合约目前提供两个量词:ForAll 和 ThereExists。
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来正确评估布尔表达式。
下一步
- 追求全面测试覆盖
- 编写更多谓词和计算
- 用一个小型递归下降解析器替换布尔表达式解析器,以消除对dissect的依赖
- 找到一种方法来减少对
__call的依赖
欢迎贡献力量。如果您有要添加的内容,请发送pull request。测试特别受欢迎!