Skip to content

命令总线

介绍

Laravel 命令总线提供了一种方便的方法,将应用程序需要执行的任务封装成简单、易于理解的“命令”。为了帮助我们理解命令的目的,让我们假设我们正在构建一个允许用户购买播客的应用程序。

当用户购买播客时,需要发生各种事情。例如,我们可能需要向用户的信用卡收费,向数据库添加一条代表购买的记录,并发送购买确认电子邮件。也许我们还需要执行某种验证,以确定用户是否被允许购买播客。

我们可以将所有这些逻辑放在控制器方法中;然而,这有几个缺点。第一个缺点是我们的控制器可能处理其他几个传入的 HTTP 操作,并且在每个控制器方法中包含复杂的逻辑会很快使我们的控制器膨胀并使其更难阅读。其次,在控制器上下文之外重用购买播客逻辑很困难。第三,单元测试命令更困难,因为我们还必须生成一个存根 HTTP 请求并对应用程序进行完整请求以测试购买播客逻辑。

与其将此逻辑放在控制器中,我们可以选择将其封装在一个“命令”对象中,例如 PurchasePodcast 命令。

创建命令

Artisan CLI 可以使用 make:command 命令生成新的命令类:

php
php artisan make:command PurchasePodcast

新生成的类将放置在 app/Commands 目录中。默认情况下,命令包含两个方法:构造函数和 handle 方法。当然,构造函数允许您将任何相关对象传递给命令,而 handle 方法执行命令。例如:

php
class PurchasePodcast extends Command implements SelfHandling {

	protected $user, $podcast;

	/**
	 * 创建一个新的命令实例。
	 *
	 * @return void
	 */
	public function __construct(User $user, Podcast $podcast)
	{
		$this->user = $user;
		$this->podcast = $podcast;
	}

	/**
	 * 执行命令。
	 *
	 * @return void
	 */
	public function handle()
	{
		// 处理购买播客的逻辑...

		event(new PodcastWasPurchased($this->user, $this->podcast));
	}

}

handle 方法还可以类型提示依赖项,并且它们将由服务容器自动注入。例如:

php
/**
	 * 执行命令。
	 *
	 * @return void
	 */
	public function handle(BillingGateway $billing)
	{
		// 处理购买播客的逻辑...
	}

调度命令

那么,一旦我们创建了一个命令,如何调度它呢?当然,我们可以直接调用 handle 方法;然而,通过 Laravel 的“命令总线”调度命令有几个优点,我们将在后面讨论。

如果您查看应用程序的基本控制器,您将看到 DispatchesCommands trait。此 trait 允许我们从任何控制器调用 dispatch 方法。例如:

php
public function purchasePodcast($podcastId)
{
	$this->dispatch(
		new PurchasePodcast(Auth::user(), Podcast::findOrFail($podcastId))
	);
}

命令总线将负责执行命令并调用 IoC 容器以将任何需要的依赖项注入 handle 方法。

您可以将 Illuminate\Foundation\Bus\DispatchesCommands trait 添加到您希望的任何类中。如果您希望通过构造函数接收命令总线实例,可以类型提示 Illuminate\Contracts\Bus\Dispatcher 接口。最后,您还可以使用 Bus facade 快速调度命令:

php
Bus::dispatch(
		new PurchasePodcast(Auth::user(), Podcast::findOrFail($podcastId))
	);

从请求映射命令属性

将 HTTP 请求变量映射到命令中是非常常见的。因此,Laravel 提供了一些辅助方法来使这变得轻而易举,而不是强迫您为每个请求手动执行此操作。让我们看看 DispatchesCommands trait 中可用的 dispatchFrom 方法:

php
$this->dispatchFrom('Command\Class\Name', $request);

此方法将检查给定命令类的构造函数,然后从 HTTP 请求(或任何其他 ArrayAccess 对象)中提取变量以填充命令所需的构造函数参数。因此,如果我们的命令类在其构造函数中接受一个 firstName 变量,命令总线将尝试从 HTTP 请求中提取 firstName 参数。

您还可以将数组作为第三个参数传递给 dispatchFrom 方法。此数组将用于填充请求中不可用的任何构造函数参数:

php
$this->dispatchFrom('Command\Class\Name', $request, [
	'firstName' => 'Taylor',
]);

队列命令

命令总线不仅用于在当前请求周期内运行的同步作业,还作为在 Laravel 中构建队列作业的主要方式。那么,我们如何指示命令总线将我们的作业排队以进行后台处理而不是同步运行呢?这很简单。首先,在生成新命令时,只需在命令中添加 --queued 标志:

php
php artisan make:command PurchasePodcast --queued

正如您将看到的,这为命令添加了一些功能,即 Illuminate\Contracts\Queue\ShouldBeQueued 接口和 SerializesModels trait。这些指示命令总线将命令排队,并优雅地序列化和反序列化命令存储为属性的任何 Eloquent 模型。

如果您希望将现有命令转换为队列命令,只需在类上手动实现 Illuminate\Contracts\Queue\ShouldBeQueued 接口即可。它不包含任何方法,仅作为调度程序的“标记接口”。

然后,只需正常编写命令。当您将其调度到总线时,该总线将自动将命令排队以进行后台处理。没有比这更简单的了。

有关与队列命令交互的更多信息,请查看完整的队列文档

命令管道

在命令被调度到处理程序之前,您可以将其通过其他类的“管道”。命令管道的工作方式与 HTTP 中间件相同,只是针对您的命令!例如,命令管道可以将整个命令操作包装在数据库事务中,或者简单地记录其执行。

要将管道添加到总线中,请从 App\Providers\BusServiceProvider::boot 方法中调用调度程序的 pipeThrough 方法:

php
$dispatcher->pipeThrough(['UseDatabaseTransactions', 'LogCommand']);

命令管道是通过一个 handle 方法定义的,就像中间件一样:

php
class UseDatabaseTransactions {

	public function handle($command, $next)
	{
		return DB::transaction(function() use ($command, $next)
		{
			return $next($command);
		});
	}

}

命令管道类是通过IoC 容器解析的,因此请随意在其构造函数中类型提示您需要的任何依赖项。

您甚至可以将 Closure 定义为命令管道:

php
$dispatcher->pipeThrough([function($command, $next)
{
	return DB::transaction(function() use ($command, $next)
	{
		return $next($command);
	});
}]);