bradietilley / laravel-actions
Laravel 的动作类框架
Requires
- illuminate/bus: ^10.0
- illuminate/container: ^10.0
- illuminate/support: ^10.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^8.21
- pestphp/pest: ^2.0
- phpstan/phpstan: ^1.8
- symfony/var-dumper: ^6.1.5
README
一个简单且灵活的 Laravel 动作实现。
介绍
动作是一些封装好的代码块,用于执行特定操作。此包提供了一个一站式平台来定义应用程序的动作。许多开发者认为,动作“不应该”作为方法定义在模型中(例如 $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(), )); });