mixteer/reshi

Reshi 库,一个断言库。

0.1.0 2015-01-24 09:54 UTC

This package is not auto-updated.

Last update: 2024-09-28 16:31:08 UTC


README

"如果它不可能发生,请使用断言来确保它不会" -- A. Hunt 和 D. Thomas 在《实用程序员》一书中。

Reshi 是一个断言库,旨在检查你对函数输入参数施加的约束。它的理念来自于《实用程序员》第四十二章第三节中的断言编程。

你使用 Reshi 来确保你认为永远不会发生的事件在真的发生时不会造成损害。请参阅同一章节中的“死程序不会说谎”。

安装

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

{
    "require": {
        "mixteer/reshi": "0.1.*"
    }
}

概念

关于这个库的为何和如何的三个主要概念来自于第四章的《实用程序员》。

  1. 断言编程 - 使用断言来确保那些不应该发生的事情,以防万一它们真的发生了。

  2. 死程序不会说谎 - 如果断言失败,库会触发一个 E_USER_ERROR 错误,立即终止程序。你应该有一个错误处理器来记录这个错误,因为它将包含断言失败的详细信息,例如文件名和行号。

  3. 对异常情况使用异常 - 库在断言失败时不抛出异常,以避免有人捕获异常并对其不采取任何行动,从而使程序继续运行。是的,错误可以被转换为异常,但这假设你有很好的理由这么做。请注意,虽然断言有助于防止罕见的坏事件,但这些事件不是你的业务逻辑的一部分,所以在这里使用异常不是一个好主意。但当库方法被提供错误的参数时,它们会抛出异常,因为这属于它们的业务逻辑。

用法

使用这个库相当简单

<?php
$assertThat = new Assertion;

$title = "The Pragmatic Programmer";

# Fails if the title is not a string
# Using an instance of Assertion
$assertThat($title)->isString();

# Using a static method
Assertion::assertIsString($title);

# Fails if the title is null
# Using an instance of Assertion
$assertThat($title)->isNotNull();

# Using a static method
Assertion::assertIsNotNull($title);

你很可能会在一个类中使用这个方法,并且 $this->assertThat 会暂停问题,因为 PHP 会在这个当前类中查找 assertThat 方法。
有几种方法可以解决这个问题

  • 辅助方法
    创建一个辅助方法,它返回一个 Assertion 实例并使用它。这种方法的问题是它创建了太多的 Assertion 对象。所以通常不推荐这种方法。
<?php
function assertThat($param)
{
    $assertThat = new Assertion;
    return $assertThat($param);
}

class User
{
    protected $name;
    
    public function changeName($name)
    {
        // Make sure the name is a string and is not empty
        assertThat($name)->isString();
        $assertThat($name)->isNotEmpty(); // !Don't this to check if a string is empty in production code
    }
}
  • 具有缓存的辅助方法
    我们还可以缓存前一个函数调用的结果,并重用先前构建的对象。这要好得多,因为我们将重用相同的断言对象在多个调用之间。请注意,我们注意更改断言正在处理的参数,否则它会重用旧的一个。
<?php
function assertThat($param)
{
    static $assertion = [];
    
    if (count($assertion) > 0) {
        $assertThat = $assertion[0];
        $assertThat->changeParameter($param);
    } else {
        $assertThat = new Assertion;
        $assertion[] = $assertThat($param);
    }    

    return $assertThat;
}

class User
{
    protected $name;
    
    public function changeName($name)
    {
        // Make sure the name is a string and is not empty
        assertThat($name)->isString();
        $assertThat($name)->isNotEmpty(); // !Don't this to check if a string is empty in production code
    }
}
  • **实现 __call**: **
    在这个方法中,我们实现了魔法方法 __call 来拦截“缺失的方法”,执行它并返回我们想要的结果。
<?php
class User
{
    protected $name = "";
    protected $assertThat = null;
    
    public function __construct()
    {
        $this->assertThat = new Assertion;
    }
    
    public function changeName($name)
    {
        $this->assertThat($name)->isString();
        
        $this->name = $name;
    }
    
    public function getName()
    {
        return $this->name;
    }
    
    // Without this method, the code would fail with a message such as the method User::assertThat() could not be found.
    public function __call($method, $args)
    {
        if (is_callable(array($this, $method))) {
            return call_user_func_array($this->{$method}, $args);
        }
    }       
}

对于重度使用领域模型的开发者来说,理想情况下__call方法应该放在层超类型上,以避免重复。

  • 创建一个命名空间函数
    假设您正在使用层超类型。您不需要实现__call方法,而是在层超类型类中创建一个函数,并导入它(自PHP 5.6+)。
<?php
namespace Domain\Model;

use Reshi\Assertion;

function assertThat($param)
{
    static $assertion = [];
    
    if (count($assertion) > 0) {
        $assertThat = $assertion[0];
        $assertThat->changeParameter($param);
    } else {
        $assertThat = new Assertion;
        $assertion[] = $assertThat($param);
    }    

    return $assertThat;
}

class LayerSupertype
{
    protected $id;
    
    // Method common to all domain models go here
}

用法如下

<?php
namespace Domain\Model\Users;

use function Domain\Model\assertThat;

class User
{
    protected $name = "";
    
    public function changeName($name)
    {
        assertThat($name)->isString();
        
        $this->name = $name;
    }
    
    public function getName()
    {
        return $this->name;
    }  
}

断言

这还不是断言的完整列表,但我们将在文档和代码中尽快添加更多内容。

  • assertIsTrue($param):如果$param为假,则停止程序执行。
    方法:isTrue()
    静态示例:Assertion::assertIsTrue(true);实例示例:$assertThat(true)->isTrue();

  • assertIsFalse($param):如果$param为真,则停止程序执行。
    方法:isFalse()
    静态示例:Assertion::assertIsFalse(false);实例示例:$assertThat(true)->isFalse();

  • assertIsInstanceOf($object, string $klass):如果$object不是$klass的实例,则停止程序执行。
    方法:isInstanceOf(string $klass)
    静态示例:Assertion::assertIsInstanceOf($user, 'User');实例示例:$assertThat($user)->isInstanceOf('User');

  • assertIsNotInstanceOf($object, string $klass):如果$object$klass的实例,则停止程序执行。
    方法:isNotInstanceOf(string $klass)
    静态示例:Assertion::assertIsNotInstanceOf($user, 'User');实例示例:$assertThat($user)->isNotInstanceOf('User');

目前,查看Assertion.php文件将显示所有断言。以下是未记录的断言列表,但我们将逐步记录它们。

  • assertIsType($param, string $type)
  • assertIsBool($param)
  • assertIsInt($param)
  • assertIsFloat($param)
  • assertIsString($param)
  • assertIsArray($param)
  • assertIsObject($param)
  • assertIsResource($param)
  • assertIsCallable($param)
  • assertIsNull($param)
  • assertIsEmpty($param)
  • assertHasCount($array)
  • assertEquals($operandOne, $operandTwo)
  • assertIsGreaterThan($operandOne, $operandTwo)
  • assertIsGreaterThanOrEqualTo($operandOne, $operandTwo)
  • assertIsLessThan($operandOne, $operandTwo)
  • assertIsLessThanOrEqualTo($operandOne, $operandTwo)
  • assertSame($operandOne, $operandTwo)
  • assertFileExists(string $file)
  • assertHasSameContent(string $fileOne, string $fileTwo)
  • assertEqualsFile(string $string, string $file)
  • assertStartsWith(string $needle, string $string)
  • assertEndsWith(string $needle, string $string)
  • assertArrayContains($needle, array $haystack)
  • assertArrayContainsOnly(string $type, array $haystack)
  • assertArrayContainsOnlyInstancesOf(string $klass, array $haystack)
  • assertArrayHasKey($key, array $haystack)
  • assertObjectHasAttribute(string $attribute, object $object)

这些静态方法都有实例对应的方法(可能没有相同的“签名”),以及否定对应的方法。

添加新断言

Reshi允许您创建自己的断言。
要添加新断言,您只需实现ReshiConstraint接口。以下是实现的示例。

  • 实现ReshiConstraint接口
    此接口需要两个方法:evaluate(),将由静态方法assertThat调用,以及getName(),必须返回一个字符串,其内容是约束的名称。此名称包含在失败消息中,以便开发人员可以识别哪个断言失败。
<?php
namespace MyConstraints;

use Reshi\ReshiConstraint;

class HasBeenSet implements ReshiConstraint
{
    const NAME = "HAS_BEEN_SET";

    public function __construct()
    {
    }

    public function evaluate($param)
    {
        if (isset($param)) {
            return true;
        }

        return false;
    }

    public function getName()
    {
        return self::NAME;
    }
}
  • ** 扩展Assertion类**:
    您可能想扩展Assertion类,以便您可以使用具有所有现有方法的统一断言类。
<?php
namespace MyAssertions;

use Reshi\Assertion;
use MyConstraints\HasBeenSet;

class MyAssertion extends Assertion
{
    public static function assertHasBeenSert($param)
    {
        self::$callFromInside += 1; // This is used for backstracing - must always be there
        return self::assertThat($param, new HasBeenSet);
    }

    public function hasBeenSet()
    {
        self::$callFromInside += 1;
        return self::assertHasBeen($this->param);
    }
    
    public static function assertHasNotBeenSert($param)
    {
        self::$callFromInside += 1; // This is used for backstracing - must always be there
        return self::assertThat($param, new HasBeenSet, false); // Pass false to get the constraint to fial if its evaluation returns true
    }

    public function hasNotBeenSet()
    {
        self::$callFromInside += 1;
        return self::assertHasNotBeen($this->param);
    }
}

现在,您可以使用MyAssertion代替Assertion,并访问您自己的断言和现有断言。

关于

要求

Reshi已在PHP 5.5上进行了测试,但所有功能应从PHP 5.3开始工作——请参阅composer.json。欢迎在其他平台上进行测试。请向以下作者之一发送电子邮件。

错误和功能请求

所有错误和功能请求都在 GitHub 上跟踪。

贡献

请向我们发送一个 pull request。

测试

要运行测试

$ phpunit

作者

Ntwali Bashige - ntwali.bashige@gmail.com - http://twitter.com/nbashige
Armando Sudi - https://github.com/ArmandoSudi

许可证

Reshi 在 MIT 许可下发布,请参阅 LICENSE 文件。

致谢

断言灵感来自 PHPUnit