mcpField
L
laravel-actions

laravel-actions

github.com/aaronflorey开发237 次安装22 周前更新
代码仓库

关于 laravel-actions

Laravel Actions 是面向 Laravel 开发者的 PHP 库,可将单一任务封装为类,并作为对象、控制器、作业、监听器或命令运行。它还提供验证、队列行为和测试辅助功能。

主要功能

  1. 01让一个 Action 承担多种 Laravel 角色
  2. 02支持控制器验证与授权
  3. 03配置作业、队列、链式任务和批处理
  4. 04将 Action 注册为监听器或命令
  5. 05模拟 Action 并断言作业派发

适用场景

  1. 01在 HTTP 与队列流程间复用任务逻辑
  2. 02验证并授权控制器请求
  3. 03派发链式或批量后台任务
  4. 04测试 Action 行为与作业派发

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/aaronflorey/agent-skills --skill laravel-actions

安装后在 Claude Code 中输入/laravel-actions确认已加载。

附带文件references/api.md

详细介绍

Laravel Actions

lorisleiva/laravel-actions 让你只需编写一个 PHP 类,就能处理一项特定任务,并根据需要将其作为对象、控制器、作业、监听器或命令运行。

安装:composer require lorisleiva/laravel-actions 创建:php artisan make:action MyAction

核心结构

每个 Action 都是一个普通 PHP 类,使用 AsAction trait 并包含一个 handle 方法:

use Lorisleiva\Actions\Concerns\AsAction;

class PublishNewArticle
{
    use AsAction;

    public function handle(User $author, string $title, string $body): Article
    {
        return $author->articles()->create(compact('title', 'body'));
    }
}
  • 将 Action 放在 app/Actions/ 中,并按主题分组(例如 app/Actions/Articles/)
  • 使用简短、动词开头的名称:SendWelcomeEmail、CreateInvoice、SyncContacts
  • 使用构造函数注入依赖项——Action 始终从容器中解析

作为对象

// Resolve and run
PublishNewArticle::run($author, 'Title', 'Body');

// Resolve only
$action = PublishNewArticle::make();

// Conditional execution
PublishNewArticle::runIf($condition, $author, 'Title', 'Body');
PublishNewArticle::runUnless($condition, $author, 'Title', 'Body');

作为控制器

像注册可调用控制器一样,在路由中注册:

Route::post('/articles', PublishNewArticle::class)->middleware('auth');

实现 asController,将请求数据映射到 handle 参数:

public function asController(Request $request): ArticleResource
{
    $article = $this->handle(
        $request->user(),
        $request->input('title'),
        $request->input('body'),
    );
    return new ArticleResource($article);
}

如果省略 asController,则会直接将 handle 用作可调用方法。

Action 本身的中间件:

public function getControllerMiddleware(): array
{
    return ['auth', 'verified'];
}

针对 JSON 和 HTML 返回不同的响应:

public function jsonResponse(Article $article, Request $request): ArticleResource
{
    return new ArticleResource($article);
}

public function htmlResponse(Article $article, Request $request): RedirectResponse
{
    return redirect()->route('articles.show', $article);
}

内联注册路由(可选):

public static function routes(Router $router): void
{
    $router->post('/articles', static::class);
}

然后在服务提供者中调用 Actions::registerRoutes(['app/Actions'])。

多端点 Action 的显式路由方法:

Route::get('/articles/create', [PublishNewArticle::class, 'showForm']);
Route::post('/articles', PublishNewArticle::class);

验证与授权(作为控制器)

注入 ActionRequest,以触发在 Action 本身定义的验证和授权:

use Lorisleiva\Actions\ActionRequest;

public function asController(ActionRequest $request): ArticleResource
{
    $article = $this->handle(
        $request->user(),
        $request->validated('title'),
        $request->validated('body'),
    );
    return new ArticleResource($article);
}

public function authorize(ActionRequest $request): bool
{
    return $request->user()->can('create', Article::class);
}

public function rules(): array
{
    return [
        'title' => ['required', 'string', 'max:255'],
        'body'  => ['required', 'string'],
    ];
}

其他验证钩子:

public function prepareForValidation(ActionRequest $request): void { /* mutate input */ }
public function withValidator(Validator $validator): void { /* add callbacks */ }
public function afterValidator(Validator $validator): void { /* after hook */ }
public function getValidator(): Validator { /* full control */ }
public function getValidationData(): array { return $this->all(); }
public function getValidationMessages(): array { return []; }
public function getValidationAttributes(): array { return []; }
public function getValidationRedirect(Request $request): string { return url()->previous(); }
public function getValidationErrorBag(): string { return 'default'; }
public function getValidationFailure(): void { throw new ValidationException(...); }
public function getAuthorizationFailure(): void { throw new AuthorizationException(...); }

作为作业

// Async dispatch
PublishNewArticle::dispatch($author, 'Title', 'Body');

// Conditional dispatch
PublishNewArticle::dispatchIf($cond, $author, 'Title', 'Body');
PublishNewArticle::dispatchUnless($cond, $author, 'Title', 'Body');

// Sync dispatch
PublishNewArticle::dispatchSync($author, 'Title', 'Body');

// After response is sent
PublishNewArticle::dispatchAfterResponse($author, 'Title', 'Body');

仅当作业专属行为与 handle 不同时,才实现 asJob:

public function asJob(Team $team): void
{
    $this->handle($team, fullReport: true);
}

配置作业默认值:

public string $queue = 'emails';
public int $tries = 3;
public int $timeout = 60;
public int $maxExceptions = 2;

public function configureJob(JobDecorator $job): void
{
    $job->onQueue('high')->delay(now()->addMinutes(5));
}

public function getJobBackoff(): array { return [10, 30, 60]; }
public function getJobRetryUntil(): DateTime { return now()->addHour(); }
public function getJobMiddleware(): array { return [new WithoutOverlapping($this->team->id)]; }

唯一作业:

use Illuminate\Contracts\Queue\ShouldBeUnique;

class SendTeamReport implements ShouldBeUnique
{
    use AsAction;

    public function getJobUniqueId(Team $team): int { return $team->id; }
    public function getJobUniqueFor(): int { return 3600; }
}

作业链:

SendWelcomeEmail::withChain([
    VerifyEmailAddress::makeJob($user),
    AssignDefaultRole::makeJob($user),
])->dispatch($user);

批处理:

use Illuminate\Support\Facades\Bus;

Bus::batch([
    ProcessInvoice::makeJob($invoiceA),
    ProcessInvoice::makeJob($invoiceB),
])->dispatch();

Horizon 标签和显示名称:

public function getJobTags(Team $team): array { return ["team:{$team->id}"]; }
public function getJobDisplayName(): string { return 'Send Team Report'; }

作为监听器

在 EventServiceProvider 中注册:

protected $listen = [
    UserRegistered::class => [SendWelcomeEmail::class],
];

或使用 Event facade:

Event::listen(UserRegistered::class, SendWelcomeEmail::class);

如需使用队列监听器,在 Action 中添加 implements ShouldQueue。

使用 asListener 将事件数据映射到 handle 参数:

public function asListener(UserRegistered $event): void
{
    $this->handle($event->user);
}

作为命令

在 Kernel::$commands 中注册或自动注册:

Actions::registerCommands(['app/Actions']);
use Illuminate\Console\Command;

class SendTeamReport
{
    use AsAction;

    public string $commandSignature = 'teams:report {team_id}';
    public string $commandDescription = 'Send the weekly report to a team.';

    public function asCommand(Command $command): void
    {
        $team = Team::findOrFail($command->argument('team_id'));
        $this->handle($team);
        $command->info('Report sent!');
    }

    // Dynamic signature/description/help:
    public function getCommandSignature(): string { return '...'; }
    public function getCommandDescription(): string { return '...'; }
    public function getCommandHelp(): string { return '...'; }
    public function isCommandHidden(): bool { return false; }
}

测试与模拟

// Mock — set expectations before running
PublishNewArticle::mock()
    ->shouldReceive('handle')
    ->once()
    ->andReturn($fakeArticle);

// Shorthand
PublishNewArticle::mock()->shouldRun()->once()->andReturn($fakeArticle);
PublishNewArticle::mock()->shouldNotRun();

// Partial mock (only mocked methods get expectations)
PublishNewArticle::partialMock()->shouldReceive('fetch')->andReturn([...]);

// Spy — run first, assert after
PublishNewArticle::spy()->shouldHaveReceived('handle')->once();
PublishNewArticle::spy()->allowToRun();

// Lifecycle helpers
PublishNewArticle::isFake();   // bool — is currently mocked?
PublishNewArticle::clearFake(); // reset to real implementation

断言作业已分发:

Queue::fake();

// ...trigger code...

PublishNewArticle::assertPushed();
PublishNewArticle::assertPushed(2); // dispatched exactly N times
PublishNewArticle::assertPushed(fn ($action, $args) => $args[0]->is($team));
PublishNewArticle::assertNotPushed();
PublishNewArticle::assertPushedOn('high', fn ($action, $args) => true);

WithAttributes(可选,v2.1+)

对于适合使用经过验证的统一属性集的 Action(在迁移 v1 代码,或需要让对象和控制器用法共用相同验证规则时很有用):

use Lorisleiva\Actions\Concerns\AsAction;
use Lorisleiva\Actions\Concerns\WithAttributes;

class PublishNewArticle
{
    use AsAction;
    use WithAttributes;

    public function handle(User $author, array $data = []): Article
    {
        $this->fill($data);
        $this->validateAttributes(); // triggers authorize + rules
        return $author->articles()->create($this->validated());
    }

    public function asController(ActionRequest $request): Article
    {
        $this->fillFromRequest($request);
        return $this->handle($request->user());
    }
}

WithAttributes 方法:fill、set、get、has、all、only、except、fillFromRequest、validateAttributes。

注意:使用 WithAttributes 时,ActionRequest 不会自动验证——如有需要,请手动调用 $request->validate()。

更细粒度的 traits

你可以按需选用以下 traits,而不必使用 AsAction:

  • AsObject — 支持 run、make、runIf、runUnless
  • AsController — 支持控制器装饰器
  • AsJob — 支持作业装饰器
  • AsListener — 支持监听器装饰器
  • AsCommand — 支持命令装饰器
  • AsFake — 支持模拟和间谍

参考文档

如需完整的 API 详情,请参阅:

评论

支持 Markdown