larapie / actions
Laravel 组件,负责一项特定任务
Requires
- php: >=7.4
- illuminate/support: >=7.0
Requires (Dev)
- mockery/mockery: ^1.0
- orchestra/testbench: ~3.8.0|~4.0|^5.0
- phpunit/phpunit: ^8.0|^9.0
README
Larapie Actions
⚡️ Laravel 组件,负责一项特定任务
本软件包通过专注于应用程序提供的行为,引入了一种新的组织 Laravel 应用逻辑的方式。
类似于 VueJS 组件将 HTML、JavaScript 和 CSS 组合在一起,Laravel Actions 将授权、验证和任务的执行组合在一个类中,这个类可以用作可调用的控制器、普通对象、可调度作业和事件监听器。
安装
composer require larapie/actions
目录
基本用法
使用 php artisan make:action PublishANewArticle
创建您的第一个行为,并填写授权逻辑、验证规则和处理方法。请注意,authorize
和 rules
方法是可选的,分别默认为 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
方法也支持依赖注入。
user
和 actingAs
方法
如果您想从行为中访问已验证的用户,您可以使用简单的 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); }
验证
就像在请求类中一样,您可以使用rules
和withValidator
方法来定义验证逻辑。
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
方法一样,withValidator
和afterValidator
方法也支持依赖注入。
最后,如果您想在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.php
或routes/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的客户端返回不同的响应,您可以使用分别的htmlResponse
和jsonResponse
方法。
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'); }
前置钩子
如果您只想在操作以特定类型运行时执行某些代码,您可以使用前置钩子asObject
、asJob
、asListener
和asController
。
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
方法实现这一点。
例如,假设您有三个操作UpdateProfilePicture
、UpdatePassword
和UpdateProfileDetails
,您想在单个端点中使用这些操作。
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
方法是使用createFrom
和runAs
方法实现的。
// These two lines are equivalent. $this->delegateTo(UpdatePassword::class); UpdatePassword::createFrom($this)->runAs($this);