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 的命令模式提供了一种优雅的方式来封装操作请求。通过合理使用命令模式,可以实现操作的撤销、重做、队列化和日志记录等功能,提高应用程序的灵活性和可维护性。