matheus-rosa / php-interactor
为PHP构建的单用途对象库
Requires
- php: >=5.4
Requires (Dev)
README
为PHP构建的单用途对象库。深受Ruby的interactor宝石的启发。
需求
- PHP >= 5.6
什么是Interactor?
Interactor简单来说是一个单用途对象。这意味着一个类具有SOLID原则描述的单个职责。Interactor通常表示一个动作,如SaveUser、BuildAttributes、GetExternalAPIResource等。一个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/ 目录下的所有示例。希望其中的一些示例可以更好地说明用法,并提供实际世界的示例。