deefour/interactor

2.0.0 2017-01-26 15:29 UTC

README

Build Status Total Downloads Latest Stable Version License

简单的PHP服务对象。受collectiveidea/interactor启发。

入门指南

运行以下命令将Interactor添加到您的项目中的composer.json文件。有关特定版本,请参阅Packagist

composer require deefour/interactor

需要>=PHP5.6.0

什么是Interactor

Interactor是一个简单、单功能的对象。

Interactors用于封装应用程序的业务逻辑。每个interactor代表应用程序执行的一个操作。

Interactor

  1. 扩展Deefour\Interactor\Interactor
  2. 实现一个call()方法

以下是一个简单的示例,该interactor创建一个新的Car对象。

use Deefour\Interactor\Interactor;

class CreateCar extends Interactor
{
    public function call()
    {
        $c = $this->context();

        $c->car = new Car([ 'make' => $c->make, 'model' => $c->model ]);

        if ( ! $c->car->save()) {
            $this->fail('Creating the car failed!');
        }
    }
}

上下文

Interactor基于给定的上下文运行。上下文包含interactor执行工作所需的信息。interactor可能会影响其传递的上下文,将数据从interactor返回给调用者。

所有上下文都扩展自deefour/transformer包中的Deefour\Transformer\MutableTransformer。MutableTransformer提供了对底层数据的便捷访问和修改,包括但不限于实现ArrayAccessJsonSerializable

访问上下文

可以通过context()方法访问interactor的上下文。

$this->context();
$this->context()->make; //=> 'Honda'

修改上下文

interactor可以添加或修改上下文。

$this->context()->car = new Car;

这非常有用,可以提供数据回调用者。

允许的属性

由于MutableTransformer的only()方法,执行安全的大规模赋值变得非常容易。

$car       = new Car;
$permitted = $this->context()->only($this->car->getFillable());

$car->fill($permitted);

$car->save();

特定上下文需求

默认的上下文构造函数期望一个包含属性键值对的数组。

public function __construct(array $attributes = [])
{
    $this->attributes = $attributes;
}

interactor通常需要从提供上下文中获取特定的数据。例如,一个CreateCar interactor可能期望将所有者分配给创建的Car。应为此Interactor专门创建一个上下文类,在实例化时提供用户。

use Deefour\Interactor\Context;

class CarContext extends Context
{
    /**
     * The owner of the vehicle.
     *
     * @var User
     */
    public $user;

    /**
     * Constructor.
     *
     * @param User  $user
     * @param array $attributes
     */
    public function __construct(User $user, array $attributes = [])
    {
        $this->user = $user;

        parent::__construct($attributes);
    }
}

CreateCar interactor应期望在实例化时接收一个新的CarContext实例

public function __construct(CarContext $context)
{
    parent::__construct($context);
}

上下文工厂

虽然手动实例化上下文是常规做法,但在某些情况下,ContextFactory是一个有用的替代方案。将要实例化的上下文的完全限定名称以及要传递给create方法的属性/参数集合传递给工厂。

use App\User;
use Deefour\Interactor\ContextFactory;

$user       = User::find(34);
$attributes = [ 'make' => 'Honda', 'model' => 'Accord' ];

$context = ContextFactory::create(CarContext::class, compact('user', 'attributes'));

$context->user->id; //=> 34
$context->make;     //=> Honda

不需要显式指定'attributes'参数。任何与构造函数上的参数名称不匹配的源数据数组中的键都将推入一个$attributes参数。如果您在提供额外数据的同时手动提供'attributes'参数,则额外数据将合并到$attributes数组中。

注意:利用此功能需要上下文类的构造函数中有一个可用的$attributes参数。

use Deefour\Interactor\ContextFactory;

$user = User::find(34);
$data = [ 'make' => 'Honda', 'model' => 'Accord' ];

$context = ContextFactory::create(CarContext::class, array_merge(compact('user'), $data));

$context->make;  //=> Toyota
$context->model; //=> Accord
$context->foo;   //=> bar

状态

交互器的状态被认为是通过或失败。上下文通过一个$status属性来持有当前状态。这个库提供了SuccessError状态。上下文默认情况下被赋予Success状态。

使上下文失败

当交互器执行过程中发生错误时,交互器可以被视为失败。这是通过标记上下文为已失败来完成的。

$this->context()->fail();

可以提供一个消息来解释失败的原因。

$this->context()->fail('Some explicit error message here');

使上下文失败将抛出Deefour\Interactor\Exception\Failure异常。

如果提供了异常,它将在将消息复制到上下文上的Error状态后抛出。

try {
    $this->context()->fail(new CarCreationException('Invalid make/model combination'));
} catch (CarCreationException $e) {
    (string)$this->context()->status(); //=> Invalid make/model combination
}

检查状态

你可以询问当前状态是否成功/通过。

$c = $this->context();

$c->ok();     //=> true
$c->status(); //=> Deefour\Interactor\Status\Success

try {
    $c->fail('Oops!');
} catch (\Deefour\Interactor\Exception\Failure $e) {
    $c->ok();             //=> false
    $c->status();         //=> Deefour\Interactor\Status\Error
    (string)$c->status(); //=> Oops!
}

用法

在一个控制器中,通过CreateCar交互器实现车辆创建可能看起来像这样。

public function create(CreateRequest $request)
{
    $context = new CarContext($request->user(), $request->only('make', 'model'));

    (new CreateCar($context))->call();

    if ($context->ok()) {
        echo 'Wow! Nice new ' . $context->car->make;
    } else {
        echo 'ERROR: ' . $context->status()->error();
    }
}

分发交互器

可以在任何类中包含Deefour\Interactor\DispatchesInteractors特质,以将交互器的创建和执行简化为单个方法调用。在下面的示例中,这个特质将

  1. 使用提供的User'make''model'解析一个新的CarContext实例
  2. 使用新创建的CarContext实例化一个新的CreateCar实例
  3. 在交互器上执行call()方法
  4. 返回上下文
namespace App\Controllers;

use App\Interactors\CreateCar;
use App\Contexts\CarContext;
use Deefour\Interactor\DispatchesInteractors;

class CarController extends BaseController
{
    use DispatchesInteractors;

    /**
     * Create a new car.
     *
     * @param  Request $request
     * @return string
     */
    public function store(Request $request)
    {
        $context = $this->dispatchInteractor(
            CreateCar::class,
            CarContext::class,
            array_merge([ 'user' => $request->user() ], $request->only('make', 'model'))
        );

        if ($context->ok()) {
            return 'Wow! Nice new ' . $context->car->make;
        } else {
            return 'ERROR: ' . $context->status()->error();
        }
    }
}

组织者

复杂的场景可能需要按顺序使用多个交互器。如果一个注册表单要求用户提供电子邮件、密码和车辆的VIN,提交可能会注册新的用户帐户并根据VIN为用户创建新的车辆。这两个动作最好分别使用CreateUserCreateVehicle交互器来处理。可以使用组织者来管理这些交互器的执行。

通过组织者组合交互器

要创建一个组织者,扩展Deefour\Interactor\Organizer并实现一个将交互器推入队列的organize()方法。组织者的call()方法由库实现。像标准交互器一样,组织者可以通过构造函数的类型提示要求特定的上下文。

use Deefour\Interactor\Organizer;

class RegisterUser extends Organizer
{
    public function __construct(RegisterUserContext $context)
    {
        parent::__construct($context);
    }

    public function organize()
    {
        $this->enqueue(function ($context) {
            return new CreateUser(
                new CreateUserContext($context->user['first_name'], $context->user['last_name'])
            );
        });
        
        $this->enqueue(function ($context, $previous) {
            return new CreateVehicle(
                new CreateVehicleContext($previous->user, $context->vin)
            );
        });
    }
}

enqueue()方法接受一个callable,该可调用应该返回一个交互器实例。可调用以先入先出的方式执行。每个可调用都接收到组织者的上下文以及先前执行的交互器的上下文。

延迟实例化允许在创建当前交互器时使用在先前交互器执行后才能获得的信息。

执行组织者

组织者的执行方式与其他交互器一样。实例化后,调用call()方法开始执行。

$params = [
    'user' => [
        'first_name' => 'Jason',
        'last_name'  => 'Daly',
    ],
    'vin' => 'VINNUMBERHERE',
];

$context = new RegisterUserContext($params['user'], $params['vin']);

(new RegisterUser($context))->call();

组织者失败和回滚

如果在组织者的执行过程中发生失败,将在失败之前成功运行的每个交互器上调用rollback(),顺序相反。覆盖Deefour\Interactor\Interactor上的空rollback()方法以利用这一点。

注意:当交互器独立执行时,不会调用rollback()方法,尽管可以通过测试上下文上的失败来手动调用它。

与Laravel 5集成

Laravel 5中的作业可以被视为交互器或组织者。通过Laravel的作业调度器调用的作业上的handle()方法支持通过服务容器进行依赖注入。利用依赖注入和Laravel作业调度器的CreateCar交互器实现可能看起来像这样

namespace App\Jobs;

use App\Car;
use App\Contexts\CreateCarContext as CarContext;
use Deefour\Interactor\Interactor;
use Illuminate\Contracts\Redis\Database as Redis;

class CreateCar extends Interactor
{
    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct(CarContext $context)
    {
        parent::__construct($context);
    }

    /**
     * Execute the command.
     *
     * @return void
     */
    public function handle(Redis $redis)
    {
        $c      = $this->context();
        $c->car = Car::create($c->only('make', 'model'));

        $redis->publish('A new' . (string)$c->car . ' was just added to the lot!');

        return $this->context();
    }
}

Laravel需要被告知使用它自己的dispatch()方法,而不是这个库提供的那个。这允许分发交互器和纯Laravel作业。

namespace App\Http\Controllers;

use Illuminate\Foundation\Bus\DispatchesJobs;
use Deefour\Interactor\DispatchesInteractors;

class Controller
{
    use DispatchesJobs, DispatchesInteractors {
      DispatchesJobs::dispatch insteadof DispatchesInteractors;
    }
}

注意:交互器甚至可以实现Laravel的Illuminate\Contracts\Queue\ShouldQueue接口,以便延迟执行!

贡献

变更日志

2.0.0 - 2017年1月26日

  • 为组织者重写了API。

1.2.0 - 2016年10月16日

  • 现在可以将异常传递给上下文的fail()方法。传递的异常将代替新的Deefour\Interactor\Exception\Failure抛出。感谢@gpassarelli#6中的贡献。

1.1.0 - 2015年10月19日

  • call()不再抽象;它被留空以供覆盖。这样,当使用其他方法进行业务逻辑时(例如,当使用handle()与Laravel的命令总线一起工作时),就不需要定义call()
  • 重大变更 DispatchesInteractors特质不再通过服务容器解析交互器。
    • 现在可以在Laravel之外使用此特质。已实现一个基本的dispatch()方法,用于在交互器上执行call()方法。
    • 在Laravel的上下文中,所有依赖注入都应在handle()方法上完成,就像任何其他Laravel作业一样。不再支持构造函数注入。
    • 当同时使用Laravel的作业调度器和此库的交互器调度器时,必须告诉Laravel使用Illuminate\Foundation\Bus\DispatchesJobs::dispatch()方法。有关更多信息,请参阅上面的与Laravel集成

1.0.0 - 2015年10月7日

  • 发布1.0.0版本。

0.7.0 - 2015年6月21日

  • 新增OrganizerCompositeContext以分组交互器。

0.6.2 - 2015年6月5日

  • 现在遵循PSR-2。

0.6.0 - 2015年5月30日

  • 新增ContextFactory以创建上下文对象。
  • 现在以deefour/transformer为依赖项。
  • Context现在扩展了MutableTransformer。此类不再直接实现ArrayAccess
  • Context上的attributes()方法已被删除。请使用all()raw()(用于非转换的属性版本)代替。
  • Interactor已简化,仅使用类型提示来强制执行交互器的正确上下文。

0.5.0 - 2015年5月25日

  • 现在建议将deefour/transformer作为必需项。如果可用,上下文将包装在MutableTransformer中,透明地在上下文对象上提供deefour/transformer的所有功能。
  • 新增__isset()实现和更好的空上下文值支持。
  • 改进代码格式。

0.4.4 - 2015年2月20日

0.4.0 - 2015年2月1日

  • 将大部分API从交互器移到上下文。
  • perform()已更改为call()
  • 添加了带有dispatchInteractor()方法的新特质。

0.3.0 - 2015年1月3日

  • 重构,移除了对Illuminate组件的依赖和支持。
  • 兼容性更改,以便与Laravel 5的新命令总线处理程序和事件处理程序轻松协作。
  • 反转解析查找;现在上下文解析交互器,而不是相反。

0.2.0 - 2014年10月7日

  • 从实例化的交互器自动解析上下文。

0.1.0 - 2014年10月2日

  • 初始发布

许可证

版权所有(c)2017 Jason Daly (deefour)。在MIT许可证下发布。