bradietilley/laravel-actions

Laravel 的动作类框架

v0.1.0 2024-02-17 00:13 UTC

This package is auto-updated.

Last update: 2024-09-17 01:37:26 UTC


README

一个简单且灵活的 Laravel 动作实现。

Static Analysis Tests

介绍

动作是一些封装好的代码块,用于执行特定操作。此包提供了一个一站式平台来定义应用程序的动作。许多开发者认为,动作“不应该”作为方法定义在模型中(例如 $user->assignDefaultRole()),也不应该独立存在于作业类中(例如 App\Jobs\Users\AssignDefaultRole)。这就是此包发挥作用的地方。

在此包中,动作的构建类似于(同步派发的)作业。例如,有一个调度器可以同步派发动作,就像作业一样,还有一个 Facade 可以启用动作的模拟,就像作业一样。

Bus 中分离出来对于大型项目来说至关重要,在这些项目中,您有大量的作业和动作。此外,这也是合乎逻辑的。就像您不希望 Event::fake() 模拟 Bus 类(作业)一样,您也不希望 Bus::fake() 模拟您的 Action 类。或者也许您确实需要。由您决定。无论如何...

安装

composer require bradietilley/laravel-actions

文档

首先,熟悉一下 Bus 知识(即作业)。因为这将基本上是一个独立副本,展示了(同步)派发作业如何与 Bus::fake()Bus::assert*() 方法一起操作。

理解动作

首先,作业的等价物是一个实现了 BradieTilley\Actions\Contracts\Actionable 接口的类。一个 Actionable 类具有一个 handle 方法(就像作业一样)并且可以被派发(就像作业一样)。Actionable 接口不提供任何方法签名,以允许完全自定义和依赖注入(为了解决 PHP 的一个限制)。

创建一个 Actionable 类很容易。最简单的方法是扩展 BradieTilley\Actions\Action 抽象类,它包含您需要的样板代码。或者,实现 Actionable 接口并添加 BradieTilley\Actions\Dispatchable 特性。

以下是一个动作的简单示例,使用了上述两种方法

/**
 * Using the Action class
 */
class AssignDefaultRole extends \BradieTilley\Actions\Action
{
    public function __construct(public readonly User $user)
    {}

    public function handle(): User
    {
        if ($this->user->role) {
            return $this->user->role;
        }

        $this->user->update([
            'role' => $default => Role::DEFAULT,
        ]);

        return $default;
    }
}

/**
 * Using the Actionable interface and Dispatchable trait.
 */
class AssignDefaultRole implements \BradieTilley\Actions\Contracts\Actionable
{
    use \BradieTilley\Actions\Dispatchable;

    public function __construct(public readonly User $user)
    {}

    public function handle(): User
    {
        if ($this->user->role) {
            return $this->user->role;
        }

        $this->user->update([
            'role' => $default => Role::DEFAULT,
        ]);

        return $default;
    }
}

理解 Facade

使用 BradieTilley\Actions\Facades\Action 类提供了一个 Facade。

您可以使用 Facade 派发动作,例如

$role = Action::dispatch(new AssignDefaultRole($user));

但是,您也可以完全避免使用 Facade,而是使用派发方法(可能更受欢迎)。

$role = AssignDefaultRole::dispatch($user);

测试和模拟动作

Action Facade 包装了底层的 Dispatcher,可以将其替换为 FakeDispatcher,该调度器跟踪所有已派发的动作,就像 Bus 调度器跟踪作业一样。

以下是一个示例

use BradieTilley\Actions\Facades\Action;

// your test
Action::fake();

// your app
AssignDefaultRole::dispatch($user);

// your test
Action::assertDispatched(AssignDefaultRole::class); // pass
Action::assertNotDispatched(AssignAdminRole::class); // pass

支持以下方法

  • Action::assertDispatched()
  • Action::assertDispatchedTimes()
  • Action::assertNotDispatched()
  • Action::assertNothingDispatched()

这些方法与 Bus 对应方法完全相同,因此您可以参考 Laravel 的 Bus 模拟文档来了解如何使用这 4 个方法。

测试和模拟特定动作

use BradieTilley\Actions\Facades\Action;

// your test
Action::fake([
    RecordAuditLog::class,
]);

// your app
AssignDefaultRole::dispatch($user); // still runs
RecordAuditLog::dispatch($user); // doesn't run

// your test
Action::assertDispatched(RecordAuditLog::class); // pass

测试和模拟所有动作除外

use BradieTilley\Actions\Facades\Action;

// your test
Action::fake()->except([
    AssignDefaultRole::class,
]);

// your app
AssignDefaultRole::dispatch($user); // still runs
RecordAuditLog::dispatch($user); // doesn't run

// your test
Action::assertDispatched(RecordAuditLog::class); // pass

测试但仍然执行动作

Bus(据我所知)没有提供的功能是允许对已派发的作业进行断言,但仍然运行这些作业。对于动作,只需使用以下语法简单地允许执行

use BradieTilley\Actions\Facades\Action;

// your test
Action::fake()->allowExecution();

// your app
AssignDefaultRole::dispatch($user); // still runs
RecordAuditLog::dispatch($user); // still runs

// your test
Action::assertDispatched(RecordAuditLog::class); // pass

然后您可以在测试过程中随时关闭它

use BradieTilley\Actions\Facades\Action;

// your test
Action::fake()->disallowExecution();

// your app
AssignDefaultRole::dispatch($user); // doesn't run
RecordAuditLog::dispatch($user); // doesn't run

// your test
Action::assertDispatched(RecordAuditLog::class); // pass

事件

在动作派发之前,它将触发一个事件:BradieTilley\Actions\Events\ActionDispatching

在事件的 action 属性下提供了 BradieTilley\Actions\Contracts\Actionable 类。

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;

Event::listen(function (ActionDispatching $event) {
    Log::channel('actions')->debug(sprintf(
        'Running action %s',
        $event->action::class,
    ));
});

在执行操作后立即,将触发一个事件:BradieTilley\Actions\Events\ActionDispatched

在事件的 action 属性下提供了 BradieTilley\Actions\Contracts\Actionable 类。

在事件中提供了执行操作所花费的时间的摘要(SebastianBergmann\Timer\Duration 类),位于 duration 属性下。

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;

Event::listen(function (ActionDispatched $event) {
    Log::channel('actions')->debug(sprintf(
        'Successfuly ran action %s in %s milliseconds',
        $event->action::class,
        $event->duration->asMilliseconds(),
    ));
});

当操作抛出 Throwable 错误/异常时,将触发一个事件:BradieTilley\Actions\Events\ActionDispatchErrored

在事件的 action 属性下提供了 BradieTilley\Actions\Contracts\Actionable 类。

异常(Throwable 类的实例)在事件中通过 error 属性提供。

use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;

Event::listen(function (ActionDispatchErrored $event) {
    Log::channel('actions')->debug(sprintf(
        'Failed to run action %s with error %s (see sentry)',
        $event->action::class,
        $event->error->getMessage(),
    ));
});

作者