PHP 程序结构设计:使用 Action 重构你的控制器
在 Spatie 的最新项目中,我们已经开始使用称为动作(Action) 的概念。它使我们的控制器和模型保持苗条。这是一个简单的实践。我将在这篇文章中解释。
来自控制器和模型中的逻辑
考虑你又一个用 Laravel 开发的博客,您想在上面发布帖子。帖子发布后,应用会将它的标题和链接关联到推特。
执行该操作的控制器可能如下所示:
class PostsController
{
public function create()
{
// ...
}
public function store()
{
// ...
}
public function edit()
{
// ...
}
public function update()
{
// ...
}
public function delete()
{
// ...
}
public function publish(Post $post, TwitterApi $twitterApi)
{
$post->markAsPublished();
$twitterApi->tweet($post->title . PHP_EOL . $post->url);
flash()->success('Your post has been published!');
return back();
}
}
如果你想知道为什么该控制器不扩展默认控制器,请移步这里。
对我而言,非 CRUD 行为存在控制器中显得有点脏乱。让我们遵循 Adam’s advice 的建议,将 publish
方法放到它自己的控制器中。
class PublishPostController
{
public function __invoke(Post $post, TwitterApi $twitter)
{
$post->markAsPublished();
$twitter->tweet($post->title . PHP_EOL . $post->url);
flash()->success('Your post has been published!');
return back();
}
}
这已经比刚才好一点了,但是我们可以做的更好。加入你想要创建一个发布博文的命名。目前而言是不可能的,因为相关逻辑放置于控制器中。
为了相关逻辑可以从命令行(或应用的任何地方)调用,该逻辑不应该在控制器中。理想状态下,控制器唯一要做的就是处理 HTTP 层
你可能想将 publish
方法的的相关逻辑移动到 Post
模型中,对于小型项目而言很好。但是想象一下,与文章有关的动作很多,比如归档、复制等。所有的这些动作都将让你的模型变得巨大。
在动作中保存逻辑
与将逻辑写在控制器或者模型相比,让我们将其移到专用的类中。在 Spatie 公司,我们将其称之为动作。
动作是非常简单的类。只有一个公共的方法 execute
,你也可以根据自己的喜好命名。
namespace App\Actions;
use App\Services\TwitterApi;
class PublishPostAction
{
/** @var \App\Services\TwitterApi */
private $twitter;
public function __construct(TwitterApi $twitter)
{
$this->twitter = $twitter;
}
public function execute(Post $post)
{
$post->markAsPublished();
$this->tweet($post->title . PHP_EOL . $post->url);
}
private function tweet(string $text)
{
$this->twitter->tweet($text);
}
}
markAsPublished
仍然可以通过 $post
实例调用,但是我们现在已经有了专门保存发布文章逻辑的地方,所以应当将代码逻辑移动到 PublishPostAction
中,让 Post
模型变得轻量化一些。
// in PublishPostAction
public function execute(Post $post)
{
$this->markAsPublished($post);
$this->tweet($post->title . PHP_EOL . $post->url);
}
private function markAsPublished(Post $post)
{
$post->published_at = now();
$post->save();
}
private function tweet(string $text)
{
$this->twitter->tweet($text);
}
在控制器中,你可以像这样调用 action:
namespace App\Http\Controllers;
use App\Actions\PublishPostAction;
class PublishPostController
{
public function __invoke(Post $post, PublishPostAction $publishPostAction)
{
$publishPostAction->execute($post);
flash()->success('Hurray, your post has been published!');
return back();
}
}
我们使用方法注入来解决 PublishPostAction
,所以 Laravel 的 容器将自动将 TwitterApi
实例自身注入 PublishPostAction
中。
命令现在也可以使用 action 了。
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Actions\PublishPostAction;
use App\Models\Post;
class PublishPostCommand extends Command
{
protected $signature = 'blog:publish-post {postId}';
protected $description = 'Publish a post';
public function handle(PublishPostAction $publishPostAction)
{
$post = Post::findOrFail($this->argument('postId'));
$publishPostAction->execute($post);
$this->comment('The post has been published!');
}
}
我们将其提取到 action 中还有另一个好处是,由于不再与 HTTP 层绑定,该代码更具有可测试性。
class PublishPostActionTest extends TestCase
{
public function setUp(): void
{
parent::setUp();
Carbon::setTestNow(Carbon::createFromFormat('Y-m-d H:i:s', '2019-01-01 01:23:45'));
TwitterApi::fake();
}
/** @test */
public function it_can_publish_a_post()
{
$post = factory(Post::class)->state('unpublished')->create();
(new PublishPostAction())->execute($post);
$this->assertEquals('2019-01-01 01:23:45', $post->published_at->format('Y-m-d H:i:s'));
TweetterApi::assertTweetSent();
}
}
可队列的 action
想象一下,你有个需要花费一些时间执行的 action。一个简单的方案就是为它创建一个队列并从 action 中调度工作。
让我们在 PublishPostAction
中使用队列去发送推文。
// in PublishPostAction
public function execute(Post $post)
{
$this->markAsPublished($post);
$this->tweet($post->title . PHP_EOL . $post->url);
}
private function markAsPublished(Post $post)
{
$post->published_at = now();
$post->save();
}
private function tweet(string $text)
{
dispatch(new SendTweetJob($text));
}
现在,如果你想在应用程序的其他地方发送推文,确保你可以像这样使用一个工作:
namespace App\Http\Controllers
class SendTweetController
{
public function __invoke(SendTweetRequest $request)
{
dispatch(new TweetJob($request->text);
flash()->success('The tweet has been sent');
return back();
}
}
这会完美地工作。但是,如果我们可以对任何事物使用 action,包括异步工作,那岂不是很好吗?
进入我们的 laravel-queueable-action 包。这个包使你可以轻松地操作队列。你可以通过提供的 QueueableAction
应用到 action,使其可排队。该特性增加了一个 onQueue
方法。
use Spatie\QueueableAction\QueueableAction;
namespace App\Actions;
class SendTweetAction
{
use QueueableAction;
/** @var \App\Services\TwitterApi */
private $twitter;
public function __construct(TwitterApi $twitter)
{
$this->twitter = $twitter;
}
public function execute(string $text)
{
$this->twitter->tweet($text);
}
}
现在我们调用 action,它将在队列中执行它的工作。
class SendTweetController
{
public function __invoke(SendTweetRequest $request, SendTweetAction $sendTweetAction)
{
$sendTweetAction->onQueue()->execute($request->text);
flash()->success('The tweet will be sent very shortly!');
return back();
}
}
你也可以通过传递其名称给onQueue
来指定应该执行工作的队列。
$sendTweetAction->onQueue('tweets')->execute($request->text);
如果想要了解更多关于可队列的 action,请务必查看由我的同事和包创建者Brent发布的内容丰富的博客。
结束语
将逻辑提取到动作中使得在应用程序的多个地方都可以进行调用。也让代码测试变得更加简单。如果动作变大,可以将其分解成多个动作。
在 Spatie 公司,我们将这种操作命名为 execute
方法。你可以根据需要来进行调用。该实践并不是我们发明的,很多开发者早已经使用该方法。如果你来自于 DDD 的架构世界,你可能会注意到,动作只是将命令和处理程序包装在一起。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。