Laravel 13 命令模式深度解析
命令模式是一种行为型设计模式,它将请求封装为对象,从而允许用不同的请求对客户进行参数化。本文将深入探讨 Laravel 13 中命令模式的高级用法。
命令模式基础
什么是命令模式
命令模式将操作的调用者和接收者解耦,通过命令对象来封装操作请求。
1 2 3 4 5 6 7 8 9 10
| <?php
namespace App\Contracts;
interface CommandInterface { public function execute(): mixed;
public function undo(): void; }
|
基础命令实现
简单命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php
namespace App\Commands;
use App\Contracts\CommandInterface;
class SimpleCommand implements CommandInterface { protected $receiver; protected string $action;
public function __construct($receiver, string $action) { $this->receiver = $receiver; $this->action = $action; }
public function execute(): mixed { return $this->receiver->{$this->action}(); }
public function undo(): void { } }
|
文件操作命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <?php
namespace App\Commands\File;
use App\Contracts\CommandInterface; use Illuminate\Support\Facades\Storage;
class CreateFileCommand implements CommandInterface { protected string $path; protected string $content; protected bool $executed = false;
public function __construct(string $path, string $content) { $this->path = $path; $this->content = $content; }
public function execute(): bool { if ($this->executed) { return false; }
Storage::put($this->path, $this->content); $this->executed = true;
return true; }
public function undo(): void { if ($this->executed) { Storage::delete($this->path); $this->executed = false; } }
public function getPath(): string { return $this->path; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
namespace App\Commands\File;
use App\Contracts\CommandInterface; use Illuminate\Support\Facades\Storage;
class DeleteFileCommand implements CommandInterface { protected string $path; protected ?string $backup = null; protected bool $executed = false;
public function __construct(string $path) { $this->path = $path; }
public function execute(): bool { if ($this->executed) { return false; }
if (Storage::exists($this->path)) { $this->backup = Storage::get($this->path); Storage::delete($this->path); }
$this->executed = true; return true; }
public function undo(): void { if ($this->executed && $this->backup !== null) { Storage::put($this->path, $this->backup); $this->executed = false; } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <?php
namespace App\Commands\File;
use App\Contracts\CommandInterface; use Illuminate\Support\Facades\Storage;
class MoveFileCommand implements CommandInterface { protected string $source; protected string $destination; protected bool $executed = false;
public function __construct(string $source, string $destination) { $this->source = $source; $this->destination = $destination; }
public function execute(): bool { if ($this->executed) { return false; }
if (Storage::exists($this->source)) { Storage::move($this->source, $this->destination); $this->executed = true; return true; }
return false; }
public function undo(): void { if ($this->executed) { Storage::move($this->destination, $this->source); $this->executed = false; } } }
|
数据库操作命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?php
namespace App\Commands\Database;
use App\Contracts\CommandInterface; use Illuminate\Support\Facades\DB;
class InsertRecordCommand implements CommandInterface { protected string $table; protected array $data; protected ?int $insertedId = null; protected bool $executed = false;
public function __construct(string $table, array $data) { $this->table = $table; $this->data = $data; }
public function execute(): int { if ($this->executed) { return 0; }
$this->insertedId = DB::table($this->table)->insertGetId($this->data); $this->executed = true;
return $this->insertedId; }
public function undo(): void { if ($this->executed && $this->insertedId) { DB::table($this->table)->where('id', $this->insertedId)->delete(); $this->executed = false; } }
public function getInsertedId(): ?int { return $this->insertedId; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <?php
namespace App\Commands\Database;
use App\Contracts\CommandInterface; use Illuminate\Support\Facades\DB;
class UpdateRecordCommand implements CommandInterface { protected string $table; protected int $id; protected array $newData; protected ?array $oldData = null; protected bool $executed = false;
public function __construct(string $table, int $id, array $newData) { $this->table = $table; $this->id = $id; $this->newData = $newData; }
public function execute(): bool { if ($this->executed) { return false; }
$this->oldData = (array) DB::table($this->table)->where('id', $this->id)->first();
if ($this->oldData) { DB::table($this->table)->where('id', $this->id)->update($this->newData); $this->executed = true; return true; }
return false; }
public function undo(): void { if ($this->executed && $this->oldData) { DB::table($this->table)->where('id', $this->id)->update($this->oldData); $this->executed = false; } } }
|
命令调用者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| <?php
namespace App\Commands;
use App\Contracts\CommandInterface; use Illuminate\Support\Collection;
class CommandInvoker { protected Collection $history; protected Collection $redoStack; protected int $maxHistory = 100;
public function __construct() { $this->history = collect(); $this->redoStack = collect(); }
public function execute(CommandInterface $command): mixed { $result = $command->execute();
$this->history->push($command); $this->redoStack = collect();
if ($this->history->count() > $this->maxHistory) { $this->history->shift(); }
return $result; }
public function undo(): bool { if ($this->history->isEmpty()) { return false; }
$command = $this->history->pop(); $command->undo();
$this->redoStack->push($command);
return true; }
public function redo(): bool { if ($this->redoStack->isEmpty()) { return false; }
$command = $this->redoStack->pop(); $command->execute();
$this->history->push($command);
return true; }
public function canUndo(): bool { return !$this->history->isEmpty(); }
public function canRedo(): bool { return !$this->redoStack->isEmpty(); }
public function getHistory(): Collection { return $this->history; }
public function clearHistory(): void { $this->history = collect(); $this->redoStack = collect(); } }
|
宏命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| <?php
namespace App\Commands;
use App\Contracts\CommandInterface; use Illuminate\Support\Collection;
class MacroCommand implements CommandInterface { protected Collection $commands;
public function __construct(array $commands = []) { $this->commands = collect($commands); }
public function add(CommandInterface $command): self { $this->commands->push($command); return $this; }
public function execute(): array { $results = [];
foreach ($this->commands as $command) { $results[] = $command->execute(); }
return $results; }
public function undo(): void { foreach ($this->commands->reverse() as $command) { $command->undo(); } }
public function getCommands(): Collection { return $this->commands; } }
|
用户操作命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <?php
namespace App\Commands\User;
use App\Contracts\CommandInterface; use App\Models\User; use Illuminate\Support\Facades\Hash;
class CreateUserCommand implements CommandInterface { protected array $userData; protected ?User $user = null; protected bool $executed = false;
public function __construct(array $userData) { $this->userData = $userData; }
public function execute(): User { if ($this->executed) { return $this->user; }
if (isset($this->userData['password'])) { $this->userData['password'] = Hash::make($this->userData['password']); }
$this->user = User::create($this->userData); $this->executed = true;
return $this->user; }
public function undo(): void { if ($this->executed && $this->user) { $this->user->delete(); $this->executed = false; } }
public function getUser(): ?User { return $this->user; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php
namespace App\Commands\User;
use App\Contracts\CommandInterface; use App\Models\User;
class ChangeUserRoleCommand implements CommandInterface { protected User $user; protected string $newRole; protected string $oldRole; protected bool $executed = false;
public function __construct(User $user, string $newRole) { $this->user = $user; $this->newRole = $newRole; $this->oldRole = $user->role; }
public function execute(): bool { if ($this->executed) { return false; }
$this->user->update(['role' => $this->newRole]); $this->executed = true;
return true; }
public function undo(): void { if ($this->executed) { $this->user->update(['role' => $this->oldRole]); $this->executed = false; } } }
|
订单操作命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| <?php
namespace App\Commands\Order;
use App\Contracts\CommandInterface; use App\Models\Order;
class CancelOrderCommand implements CommandInterface { protected Order $order; protected string $previousStatus; protected bool $executed = false;
public function __construct(Order $order) { $this->order = $order; $this->previousStatus = $order->status; }
public function execute(): bool { if ($this->executed) { return false; }
if (!in_array($this->order->status, ['pending', 'processing'])) { return false; }
$this->order->update([ 'status' => 'cancelled', 'cancelled_at' => now(), ]);
$this->releaseInventory(); $this->executed = true;
return true; }
public function undo(): void { if ($this->executed) { $this->order->update([ 'status' => $this->previousStatus, 'cancelled_at' => null, ]);
$this->reserveInventory(); $this->executed = false; } }
protected function releaseInventory(): void { foreach ($this->order->items as $item) { $item->product->increment('stock', $item->quantity); } }
protected function reserveInventory(): void { foreach ($this->order->items as $item) { $item->product->decrement('stock', $item->quantity); } } }
|
队列命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <?php
namespace App\Commands\Queue;
use App\Contracts\CommandInterface; use Illuminate\Support\Facades\Queue;
class QueueCommand implements CommandInterface { protected string $jobClass; protected array $payload; protected ?string $queue; protected ?string $jobId = null;
public function __construct(string $jobClass, array $payload = [], ?string $queue = null) { $this->jobClass = $jobClass; $this->payload = $payload; $this->queue = $queue; }
public function execute(): string { if ($this->queue) { $this->jobId = Queue::pushOn($this->queue, new $this->jobClass(...$this->payload)); } else { $this->jobId = Queue::push(new $this->jobClass(...$this->payload)); }
return $this->jobId; }
public function undo(): void { if ($this->jobId) { Queue::delete($this->jobId); } }
public function getJobId(): ?string { return $this->jobId; } }
|
命令队列处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <?php
namespace App\Services;
use App\Contracts\CommandInterface; use Illuminate\Support\Facades\Cache;
class CommandQueue { protected string $queueKey; protected int $ttl = 86400;
public function __construct(string $name = 'default') { $this->queueKey = "command_queue:{$name}"; }
public function enqueue(CommandInterface $command): void { $queue = $this->getQueue(); $queue[] = serialize($command); Cache::put($this->queueKey, $queue, $this->ttl); }
public function dequeue(): ?CommandInterface { $queue = $this->getQueue();
if (empty($queue)) { return null; }
$serialized = array_shift($queue); Cache::put($this->queueKey, $queue, $this->ttl);
return unserialize($serialized); }
public function processAll(): array { $results = [];
while ($command = $this->dequeue()) { $results[] = $command->execute(); }
return $results; }
public function count(): int { return count($this->getQueue()); }
public function clear(): void { Cache::forget($this->queueKey); }
protected function getQueue(): array { return Cache::get($this->queueKey, []); } }
|
测试命令模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| <?php
namespace Tests\Unit\Commands;
use Tests\TestCase; use App\Commands\CommandInvoker; use App\Commands\File\CreateFileCommand; use App\Commands\File\DeleteFileCommand; use App\Commands\MacroCommand; use Illuminate\Support\Facades\Storage;
class CommandTest extends TestCase { protected function setUp(): void { parent::setUp(); Storage::fake('local'); }
public function test_create_file_command(): void { $command = new CreateFileCommand('test.txt', 'Hello World');
$result = $command->execute();
$this->assertTrue($result); Storage::disk('local')->assertExists('test.txt'); $this->assertEquals('Hello World', Storage::get('test.txt')); }
public function test_command_undo(): void { $command = new CreateFileCommand('test.txt', 'Hello World'); $command->execute();
$command->undo();
Storage::disk('local')->assertMissing('test.txt'); }
public function test_invoker_undo_redo(): void { $invoker = new CommandInvoker();
$command = new CreateFileCommand('test.txt', 'Hello World'); $invoker->execute($command);
Storage::disk('local')->assertExists('test.txt');
$invoker->undo(); Storage::disk('local')->assertMissing('test.txt');
$invoker->redo(); Storage::disk('local')->assertExists('test.txt'); }
public function test_macro_command(): void { $macro = new MacroCommand(); $macro->add(new CreateFileCommand('file1.txt', 'Content 1')); $macro->add(new CreateFileCommand('file2.txt', 'Content 2'));
$macro->execute();
Storage::disk('local')->assertExists('file1.txt'); Storage::disk('local')->assertExists('file2.txt');
$macro->undo();
Storage::disk('local')->assertMissing('file1.txt'); Storage::disk('local')->assertMissing('file2.txt'); } }
|
最佳实践
1. 命令应该独立
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <?php
class IndependentCommand implements CommandInterface { protected array $context;
public function __construct(array $context) { $this->context = $context; }
public function execute(): mixed { return $this->performAction(); }
protected function performAction(): mixed { return true; } }
|
2. 支持序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <?php
class SerializableCommand implements CommandInterface, \Serializable { public function serialize(): string { return serialize($this->getSerializableData()); }
public function unserialize(string $data): void { $this->setUnserializedData(unserialize($data)); }
protected function getSerializableData(): array { return []; }
protected function setUnserializedData(array $data): void { } }
|
3. 提供命令元数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <?php
abstract class BaseCommand implements CommandInterface { protected \DateTime $createdAt; protected string $description = '';
public function __construct() { $this->createdAt = new \DateTime(); }
public function getDescription(): string { return $this->description; }
public function getCreatedAt(): \DateTime { return $this->createdAt; } }
|
总结
Laravel 13 的命令模式提供了一种优雅的方式来封装操作请求。通过合理使用命令模式,可以实现操作的撤销、重做、队列化和日志记录等功能,提高应用程序的灵活性和可维护性。