PHP 程序结构设计:使用 Action 重构你的控制器

PHP

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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://freek.dev/1371-refactoring-to-ac...

译文地址:https://learnku.com/php/t/40553

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!