larapie/actions

Laravel 组件,负责一项特定任务

2.10 2021-02-18 14:34 UTC

README

Build Status Scrutinizer Code Quality Code Coverage StyleCI Latest Stable Version Total Downloads

Larapie Actions

⚡️ Laravel 组件,负责一项特定任务

本软件包通过专注于应用程序提供的行为,引入了一种新的组织 Laravel 应用逻辑的方式。

类似于 VueJS 组件将 HTML、JavaScript 和 CSS 组合在一起,Laravel Actions 将授权、验证和任务的执行组合在一个类中,这个类可以用作可调用的控制器、普通对象、可调度作业和事件监听器。

Cover picture

安装

composer require larapie/actions

目录

基本用法

使用 php artisan make:action PublishANewArticle 创建您的第一个行为,并填写授权逻辑、验证规则和处理方法。请注意,authorizerules 方法是可选的,分别默认为 true[]

// app/Actions/PublishANewArticle.php
class PublishANewArticle extends Action
{
    public function authorize()
    {
        return $this->user()->hasRole('author');
    }
    
    public function rules()
    {
        return [
            'title' => 'required',
            'body' => 'required|min:10',
        ];
    }
    
    public function handle()
    {
        return Article::create($this->validated());
    }
}

您现在可以以多种方式开始使用该行为

作为一个普通对象。

$action = new PublishANewArticle([
    'title' => 'My blog post',
    'body' => 'Lorem ipsum.',
]);

$article = $action->run();

作为一个可调度的作业。

PublishANewArticle::dispatch([
    'title' => 'My blog post',
    'body' => 'Lorem ipsum.',
]);

作为一个事件监听器。

class ProductCreated
{
    public $title;
    public $body;
    
    public function __construct($title, $body)
    {
        $this->title = $title;
        $this->body = $body;
    }
}

Event::listen(ProductCreated::class, PublishANewArticle::class);

event(new ProductCreated('My new SaaS application', 'Lorem Ipsum.'));

作为一个可调用的控制器。

// routes/web.php
Route::post('articles', '\App\Actions\PublishANewArticle');

如果您需要在将行为用作控制器时指定显式的 HTTP 响应,您可以定义 response 方法,该方法将 handle 方法的输出作为第一个参数提供。

class PublishANewArticle extends Action
{
    // ...
    
    public function response($article)
    {
        return redirect()->route('article.show', $article);
    }
}

行为属性

为了统一行为可以采取的各种形式,行为的数据被实现为一系列属性(类似于模型)。

这意味着当与 Action 的实例交互时,您可以使用以下方法操纵其属性

$action = new Action(['key' => 'value']);   // Initialise an action with the provided attribute.
$action->fill(['key' => 'value']);          // Merge the new attributes with the existing attributes.
$action->all();                             // Retrieve all attributes of an action as an array.
$action->only('title', 'body');             // Retrieve only the attributes provided.
$action->except('body');                    // Retrieve all attributes excepts the one provided.
$action->has('title');                      // Whether the action has the provided attribute.
$action->get('title');                      // Get an attribute.
$action->get('title', 'Untitled');          // Get an attribute with default value.
$action->set('title', 'My blog post');      // Set an attribute.
$action->title;                             // Get an attribute.
$action->title = 'My blog post';            // Set an attribute.

根据行为是如何运行的,其属性将填充相关可用信息。例如,作为控制器,行为的属性将包含请求的所有输入。有关更多信息,请参阅

依赖注入

handle 方法支持依赖注入。这意味着,无论您在 handle 方法中输入什么参数,Laravel Actions 都会尝试从容器中解决它们,同时也从其自己的属性中解决。让我们看看一些例子。

// Resolved from the IoC container.
public function handle(Request $request) {/* ... */}
public function handle(MyService $service) {/* ... */}

// Resolved from the attributes.
// -- $title and $body are equivalent to $action->title and $action->body
// -- When attributes are missing, null will be returned unless a default value is provided.
public function handle($title, $body) {/* ... */}
public function handle($title, $body = 'default') {/* ... */}

// Resolved from the attributes using route model binding.
// -- If $action->comment is already an instance of Comment, it provides it.
// -- If $action->comment is an id, it will provide the right instance of Comment from the database or fail.
// -- This will also update $action->comment to be that instance.
public function handle(Comment $comment) {/* ... */}

// They can all be combined.
public function handle($title, Comment $comment, MyService $service) {/* ... */}

如你所见,行为的属性和 IoC 容器都用于解决依赖注入。当类型提示匹配属性时,库会尽力从属性的值中提供该类的实例。

授权

authorize 方法

行为可以使用 authorize 方法定义其授权逻辑。当此方法返回 false 时,它将抛出 AuthorizationException

public function authorize()
{
    // Your authorisation logic here...
}

值得注意的是,与 handle 方法一样,authorize 方法也支持依赖注入。

useractingAs 方法

如果您想从行为中访问已验证的用户,您可以使用简单的 user 方法。

public function authorize()
{
    return $this->user()->isAdmin();
}

当作为控制器运行时,用户是从传入的请求中检索的,否则 $this->user() 等同于 Auth::user()

如果您想代表另一个用户运行行为,您可以使用 actingAs 方法。在这种情况下,user 方法将始终返回提供的用户。

$action->actingAs($admin)->run();

can 方法

如果您仍然想使用Gates和Policies来外部化您的授权逻辑,您可以使用can方法来验证用户是否可以执行提供的权限。

public function authorize()
{
    return $this->can('create', Article::class);
}

验证

就像在请求类中一样,您可以使用ruleswithValidator方法来定义验证逻辑。

rules方法允许您列出动作属性的验证规则。

public function rules()
{
    return [
        'title' => 'required',
        'body' => 'required|min:10',
    ];
}

withValidator方法提供了一个方便的方式来添加自定义验证逻辑。

public function withValidator($validator)
{
    $validator->after(function ($validator) {
        if ($this->somethingElseIsInvalid()) {
            $validator->errors()->add('field', 'Something is wrong with this field!');
        }
    });
}

如果您只想添加一个验证后的钩子,您可以使用afterValidator方法而不是withValidator方法。以下示例与上面的示例等价。

public function afterValidator($validator)
{
    if ($this->somethingElseIsInvalid()) {
        $validator->errors()->add('field', 'Something is wrong with this field!');
    };
}

值得注意的是,与handle方法一样,withValidatorafterValidator方法也支持依赖注入

最后,如果您想在handle方法中直接验证一些数据,您可以使用validate方法。

public function handle()
{
    $this->validate([
        'comment' => 'required|min:10|spamfree',
    ]);
}

这将验证提供的规则与动作属性。

行为作为对象

属性是如何填充的?

当作为普通的PHP对象运行动作时,它们的属性必须通过上述各种辅助方法手动填充。例如

$action = new PublishANewArticle;
$action->title = 'My blog post';
$action->set('body', 'Lorem ipsum.');
$action->run();

注意,run方法也接受要合并的额外属性。

(new PublishANewArticle)->run([
    'title' => 'My blog post',
    'body' => 'Lorem ipsum.',
]);

行为作为作业

属性是如何填充的?

与作为对象的动作类似,当您调度动作时,属性也会手动填充。

PublishANewArticle::dispatch([
    'title' => 'My blog post',
    'body' => 'Lorem ipsum.',
]);

可排队动作

与作业一样,通过实现ShouldQueue接口,动作可以被排队。

use Illuminate\Contracts\Queue\ShouldQueue;

class PublishANewArticle extends Action implements ShouldQueue
{
    // ...
}

请注意,您也可以使用dispatchNow方法强制立即执行可排队动作。

行为作为监听器

属性是如何填充的?

默认情况下,所有事件的所有公共属性都将用作属性。

class ProductCreated
{
    public $title;
    public $body;
    
    // ...
}

您可以通过定义getAttributesFromEvent来覆盖这种行为。

// Event
class ProductCreated
{
    public $product;
}

// Listener
class PublishANewArticle extends Action
{
    public function getAttributesFromEvent($event)
    {
        return [
            'title' => '[New product] ' . $event->product->title,
            'body' => $event->product->description,
        ];
    }
}

这也可以与定义为字符串的事件一起工作。

// Event
Event::listen('product_created', PublishANewArticle::class);

// Dispatch
event('product_created', ['My SaaS app', 'Lorem ipsum']);

// Listener
class PublishANewArticle extends Action
{
    public function getAttributesFromEvent($title, $description)
    {
        return [
            'title' => "[New product] $title",
            'body' => $description,
        ];
    }

    // ...
}

行为作为控制器

属性是如何填充的?

默认情况下,所有来自请求和路由参数的输入数据都将用于填充动作的属性。

您可以通过覆盖getAttributesFromRequest方法来更改这种行为。这是它的默认实现

public function getAttributesFromRequest(Request $request)
{
    return array_merge(
        $this->getAttributesFromRoute($request),
        $request->all()
    );
}

请注意,由于我们正在合并两组数据,当变量在两组中定义时,可能会发生冲突。如您所见,默认情况下,请求的数据比路由参数具有优先权。然而,在解析handle方法参数的依赖关系时,路由参数将比请求的数据具有优先权。

这意味着在冲突的情况下,您可以作为方法参数访问路由参数,作为属性访问请求的数据。例如

// Route endpoint: PATCH /comments/{comment}
// Request input: ['comment' => 'My updated comment']
public function handle(Comment $comment)
{
    $comment;        // <- Comment instance matching the given id.
    $this->comment;  // <- 'My updated comment'
}

使用动作定义路由

因为您的动作默认位于\App\Action命名空间,而不是\App\Http\Controller命名空间,所以如果您想在routes/web.phproutes/api.php文件中定义它们,您必须提供动作的完全限定名。

// routes/web.php
Route::post('articles', '\App\Actions\PublishANewArticle');

请注意,这里的初始\很重要,以确保命名空间不会成为\App\Http\Controller\App\Actions\PublishANewArticle

或者,您可以将它们放在一个重新定义命名空间的组中。

// routes/web.php
Route::namespace('\App\Actions')->group(function () {
    Route::post('articles', 'PublishANewArticle');
});

Laravel Actions提供了一个Route宏,它可以做到这一点

// routes/web.php
Route::actions(function () {
    Route::post('articles', 'PublishANewArticle');
});

另一种解决方案是创建一个新的路由文件routes/action.php,并在您的RouteServiceProvider中注册它。

// app/Providers/RouteServiceProvider.php
Route::middleware('web')
     ->namespace('App\Actions')
     ->group(base_path('routes/action.php'));

// routes/action.php
Route::post('articles', 'PublishANewArticle');

注册中间件

您可以使用register方法来注册中间件。

public function register()
{
    $this->middleware('auth');
}

请注意,这基本上与使用__construct方法等价,只是您不必担心将属性作为参数给出并调用parent::__construct

返回HTTP响应

对于操作返回对您的领域有意义的值,这是一个好习惯。例如,刚刚创建的文章或我们正在搜索的主题过滤列表。

然而,当操作作为一个控制器运行时,您可能希望将那个值包裹在一个合适的HTTP响应中。您可以为此使用response方法。它提供了handle方法的结果作为第一个参数,以及HTTP请求作为第二个参数。

public function response($result, $request)
{
    return view('articles.index', [
        'articles' => $result,
    ])
}

如果您想为需要HTML和需要JSON的客户端返回不同的响应,您可以使用分别的htmlResponsejsonResponse方法。

public function htmlResponse($result, $request)
{
    return view('articles.index', [
        'articles' => $result,
    ]);
}

public function jsonResponse($result, $request)
{
    return ArticleResource::collection($result);
}

跟踪行为执行情况

运行时的方法

在某些罕见的情况下,您可能想知道操作是如何运行的。您可以使用runningAs方法访问此信息。

public function handle()
{
    $this->runningAs('object');
    $this->runningAs('job');
    $this->runningAs('listener');
    $this->runningAs('controller');

    // Returns true of any of them is true.
    $this->runningAs('object', 'job');
}

前置钩子

如果您只想在操作以特定类型运行时执行某些代码,您可以使用前置钩子asObjectasJobasListenerasController

public function asController(Request $request)
{
    $this->token = $request->cookie('token');
}

值得注意的是,与handle方法一样,前置钩子也支持依赖注入。

此外,请注意,这些前置钩子将在handle方法执行之前被调用,而不是在操作创建时。这意味着您不能使用asController方法来注册中间件。您需要使用register方法。

在行为中使用行为

使用Laravel Actions,您可以轻松地在操作中调用其他操作。

如以下示例所示,我们使用另一个操作作为一个对象来访问其结果。

class CreateNewRestaurant extends Action
{
    public function handle()
    {
        $coordinates = (new FetchGoogleMapsCoordinates)->run([
            'address' => $this->address,
        ]);

        return Restaurant::create([
            'name' => $this->name,
            'longitude' => $coordinates->longitude,
            'latitude' => $coordinates->latitude,
        ]);
    }
}

然而,有时您可能想完全委托给另一个操作。这意味着我们委托的操作应该具有相同的属性,并以与父操作相同的方式运行。您可以使用delegateTo方法实现这一点。

例如,假设您有三个操作UpdateProfilePictureUpdatePasswordUpdateProfileDetails,您想在单个端点中使用这些操作。

class UpdateProfile extends Action
{
    public function handle()
    {
        if ($this->has('avatar')) {
            return $this->delegateTo(UpdateProfilePicture::class);
        }

        if ($this->has('password')) {
            return $this->delegateTo(UpdatePassword::class);
        }

        return $this->delegateTo(UpdateProfileDetails::class);
    }
}

在上面的示例中,如果我们以控制器的方式运行UpdateProfile操作,则子操作也将以控制器的方式运行。

值得注意的是,delegateTo方法是使用createFromrunAs方法实现的。

// These two lines are equivalent.
$this->delegateTo(UpdatePassword::class);
UpdatePassword::createFrom($this)->runAs($this);