matheus-rosa/php-interactor

该软件包最新版本(1.0.0)没有可用的许可证信息。

为PHP构建的单用途对象库

1.0.0 2023-07-13 17:10 UTC

This package is auto-updated.

Last update: 2024-09-13 19:36:38 UTC


README

为PHP构建的单用途对象库。深受Ruby的interactor宝石的启发。

需求

  • PHP >= 5.6

什么是Interactor?

Interactor简单来说是一个单用途对象。这意味着一个类具有SOLID原则描述的单个职责。Interactor通常表示一个动作,如SaveUserBuildAttributesGetExternalAPIResource等。一个SaveUser的Interactor仅意味着在某个存储(例如数据库)中保存用户记录,因此它不会负责做其他任何事情。

好吧,但为什么使用它是个大问题呢?

你可能想知道这个库对你有什么用。当然,你可以继续创建自己的单用途对象实现,尽管我们都知道,在开发周期的初期不建立一个良好的设计模式,可能会导致完全混乱,每个人都随心所欲,没有模式。此外,为什么别人已经做了这项繁重的工作,你还要重新发明轮子呢?

通过尝试这个库,你会发现自己在创建简单、易于维护的服务,同时采用其中的传统模式。我向你保证,你会节省相当多的时间。试试看! :)

安装

composer require matheus-rosa/php-interactor 

创建第一个Interactor

假设我们即将创建一个负责保存User模型记录的组件

<?php

class SaveUser
{
}

要使其成为Interactor,你只需要简单地导入Interactable特质。为此,你只需在你的类中导入它

<?php

use MatheusRosa\PhpInteractor\Interactable;
use MatheusRosa\PhpInteractor\Context;

class SaveUser
{
    use Interactable;
    
    protected function execute(Context $context)
    {
        // When using the Interactable trait, the execute method
        // needs to be implemented. 
    }
}

就这样!你可以在execute方法中放置所有的业务逻辑。可以这样调用SaveUser

<?php

SaveUser::call([
    'name' => 'John Doe',
    'email' => 'john.doe@email.com',
]);

注意我们只是将一个数组作为参数传递给了静态的call方法。你可以传递任何值到你的关联数组,甚至可以留空(完全不需要传递任何东西,例如SaveUser::call())。

注意2:将call方法视为公开API,而execute方法是处理你的业务逻辑的内部方式。每个Interactor都需要实现execute方法。

你可以在SaveUser类中这样检索告知的参数

use MatheusRosa\PhpInteractor\Interactable;
use MatheusRosa\PhpInteractor\Context;

class SaveUser
{
    use Interactable;
    
    protected function execute(Context $context)
    {
        // All values passed to SaveUser::call are accessible here
        // within the current context object.
        var_dump($context->name, $context->email);
        
        // You can even create brand-new values and assign them to the current context
        $context->currentTime = time();
        
        $context->user = new User($context->name, $context->email);
        $context->user->save();
    }
}

检查Interactor的成功

如果一个Interactor没有调用带有错误信息的fail方法,则视为成功场景。

你可以通过调用返回上下文中的success方法来检查它

<?php

$context = SaveUser::call([
    'name' => 'John Doe',
    'email' => 'john.doe@email.com',
]);

$context->success(); // returns either true or false

失败Interactor

Interactors可以被设置为失败,如下所示

use MatheusRosa\PhpInteractor\Interactable;
use MatheusRosa\PhpInteractor\Context;

class SaveUser
{
    use Interactable;
    
    protected function execute(Context $context)
    {
        $context->user = new User($context->name, $context->email);
        
        if (!$context->user->save()) {
            $context->fail('custom error message | model error message');
        }
        
        // some other cool code
        // it will be unreachable if the $context->fail() was invoked
    }
}

一旦调用fail方法,执行流将立即停止。这意味着示例中if条件之后的任何代码都将变得不可达。

默认情况下,fail方法不会抛出任何异常,尽管你可以通过将其第二个参数($strict)设置为true来更改其行为

$context->fail('an error message', true);

这样,从现在开始,将引发ContextFailureException

错误本身可以通过以下方式检索

$context->errors(); // returns ['an error message']

钩子

Interactors包含一组在特定情况下可以运行的钩子

around

想象一下,这是一个中间件,它将在你的 execute 方法定义之前运行。你可以完全阻止一个交互器运行,如果某些特定规则不令人满意。这在需要定义一系列防止代码执行的保护器时非常有用。

<?php

use \MatheusRosa\PhpInteractor\Interactable;
use \MatheusRosa\PhpInteractor\Context;

class SaveUser
{
    use Interactable;
    
    protected function around(Context $context)
    {
        // If the `around` method returns false
        // the `execute` method will not even start
        if (empty($context->user->email)) {
            return false;
        }
        
        // you can do whatever you want from this point forward,
        // like creating new variables to the $context or even adding new guards
    }
    
    protected function execute(Context $context)
    {
        if ($context->user->save()) {
            $context->fail('error message');
        }
    }
}

在...之前

正如其名,before 钩子是在你的 execute 方法定义之前执行的内容。

重要提示:此方法优先级低于 around 方法。

<?php

use \MatheusRosa\PhpInteractor\Interactable;
use \MatheusRosa\PhpInteractor\Context;

class SaveUser
{
    use Interactable;
    
    protected function before(Context $context)
    {
        // The `before` method will execute before the `execute` method.
        // Unlike the `around` method, it can't stop the execution flow of the current Interactor.
        // It comes more handy to initialize new variables.
        $context->currentTime = time();
    }
    
    protected function execute(Context $context)
    {
        if ($context->user->save()) {
            $context->fail('error message');
        }
    }
}

之后

如果你想在你 execute 方法之后运行任何内容,请使用 after 方法。

use \MatheusRosa\PhpInteractor\Interactable;
use \MatheusRosa\PhpInteractor\Context;

class SaveUser
{
    use Interactable;
    
    protected function after(Context $context)
    {
        // this will execute after what's defined in your `execute` method
        $context->endTime = time();
    }
    
    protected function execute(Context $context)
    {
        if ($context->user->save()) {
            $context->fail('error message');
        }
    }

钩子优先级

为了更清楚地说明,执行顺序可以表示如下

around -> before -> execute -> after

包含所有钩子的交互器的完整示例

<?php

use \MatheusRosa\PhpInteractor\Interactable;
use \MatheusRosa\PhpInteractor\Context;

class YourClazz
{
    use Interactable;
    
    protected function around(Context $context)
    {
        $context->number += 1;
        
        echo "around | number: {$context->number}\n";
    }
    
    protected function before(Context $context)
    {
        $context->number += 1;
        
        echo "before | number: {$context->number}\n";
    }
    
    protected function execute(Context $context)
    {
        $context->number += 1;
        
        echo "execute | number: {$context->number}\n";
    }
    
    protected function after(Context $context)
    {
        $context->number += 1;
        
        echo "after | number: {$context->number}\n";
    }
}

YourClass::call(['number' => 0]);

将输出

around | number: 1
before | number: 2
execute | number: 3
after | number: 4

组织者

有时候,一个单一目的的交互器不足以涵盖业务逻辑的所有要求。

比如说,你即将处理一个需要执行许多操作的定制流程。当然,你可以在交互器内部调用其他交互器,尽管组织者存在是为了使这变得更加容易。使用组织者,你可以定义一系列交互器以连续顺序运行。

要创建一个组织者,你只需要像这样使用 Organizable 特性

<?php

use \MatheusRosa\PhpInteractor\Organizable;

class YourClazz
{
    use Organizable;
    
    protected function organize()
    {
        // when using the Organizable trait,
        // the organize method needs to be implemented.
    }
}

好了!然后在 organize 方法中,你可以定义交互器的执行顺序

<?php

use \MatheusRosa\PhpInteractor\Organizable;

class YourOrganizedClazz
{
    use Organizable;
    
    protected function organize()
    {
        return [
            FirstInteractor::class,
            SecondInteractor::class,
            ThirdInteractor::class,
        ];
    }
}

完成!现在你已经定义了你的链,每个交互器将按照定义的顺序执行。你可以像调用单个交互器一样调用你的组织者

$context = YourOrganizedClazz::call(['foo' => 'bar']);

// you can do the same context operations
$context->success(); // returns boolean
$context->failure(); // returns boolean
$context->errors(); // returns an array of errors

如果你愿意,你可以在 Organizer 中使用与 Interactor 中相同的钩子

<?php

use \MatheusRosa\PhpInteractor\Organizable;
use \MatheusRosa\PhpInteractor\Context;

class YourOrganizedClazz
{
    use Organizable;
    
    protected function around(Context $context)
    {
        // implement an around logic.
        // You can stop this organizer pipeline
        // by returning false.
    }
    
    protected function before(Context $context)
    {
        // implement a before logic
    }
    
    protected function after(Context $context)
    {
        // implement an after logic
    }
    
    protected function organize()
    {
        return [
            FirstInteractor::class,
            SecondInteractor::class,
            ThirdInteractor::class,
        ];
    }
}

组织者管道中的失败

默认情况下,如果组织者管道上定义的任何交互器失败,组织者管道流将立即停止。当发生这种情况时,每个已经运行的交互器都有机会 回滚 已应用的变化。这将以相反的顺序发生(从最后一个到第一个交互器)

<?php
use \MatheusRosa\PhpInteractor\Interactable;
use \MatheusRosa\PhpInteractor\Context;

class CreateUser
{
    use Interactable;
    
    public function rollback(Context $context)
    {
        $this->user->destroy();
    }
    
    protected function execute(Context $context)
    {
        if ($context->user->save()) {
            $context->fail('error message');
        }
    }
}

无论交互器失败与否,继续组织者流程

你可以通过覆盖 continueOnFailure 方法来完全替换组织者的默认行为

<?php

use \MatheusRosa\PhpInteractor\Organizable;
use \MatheusRosa\PhpInteractor\Context;

class YourOrganizedClazz
{
    use Organizable;
    
    protected function continueOnFailure()
    {
        return true;
    }
    
    protected function organize()
    {
        return [
            FirstInteractor::class,
            SecondInteractor::class,
            ThirdInteractor::class,
        ];
    }
}

示例

如果你还不确定如何使用它或它如何对你的工程团队能带来价值,请随意查看 examples/ 目录下的所有示例。希望其中的一些示例可以更好地说明用法,并提供实际世界的示例。