BehavioralPHPverifiedVerified
Command Pattern in PHP
Encapsulates a request as an object, allowing you to parameterize clients, queue or log requests, and support undoable operations.
How to Implement the Command Pattern in PHP
1Step 1: Define the command interface
interface Command
{
public function execute(): void;
public function undo(): void;
}2Step 2: Implement concrete commands
class AddTextCommand implements Command
{
private string $previousContent = '';
public function __construct(
private Document $document,
private readonly string $text,
) {}
public function execute(): void
{
$this->previousContent = $this->document->getContent();
$this->document->append($this->text);
}
public function undo(): void
{
$this->document->setContent($this->previousContent);
}
}
class Document
{
private string $content = '';
public function append(string $text): void { $this->content .= $text; }
public function getContent(): string { return $this->content; }
public function setContent(string $content): void { $this->content = $content; }
}3Step 3: Implement the invoker with undo history
class Editor
{
/** @var Command[] */
private array $history = [];
public function execute(Command $command): void
{
$command->execute();
$this->history[] = $command;
}
public function undo(): void
{
$command = array_pop($this->history);
$command?->undo();
}
}
// Usage
$doc = new Document();
$editor = new Editor();
$editor->execute(new AddTextCommand($doc, 'Hello '));
$editor->execute(new AddTextCommand($doc, 'World'));
echo $doc->getContent(); // "Hello World"
$editor->undo();
echo $doc->getContent(); // "Hello "<?php
declare(strict_types=1);
// [step] Define command interfaces with metadata
interface CommandInterface
{
public function execute(): CommandResult;
public function undo(): CommandResult;
public function getDescription(): string;
}
final readonly class CommandResult
{
public function __construct(
public bool $success,
public string $description,
public ?string $error = null,
public float $timestamp = 0,
) {}
}
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
}
// [step] Implement concrete commands for a bank account scenario
final class BankAccount
{
private float $balance;
/** @var array<array{type: string, amount: float, timestamp: float}> */
private array $transactions = [];
public function __construct(float $initialBalance = 0)
{
$this->balance = $initialBalance;
}
public function deposit(float $amount): void
{
if ($amount <= 0) {
throw new \InvalidArgumentException('Deposit amount must be positive');
}
$this->balance += $amount;
$this->transactions[] = ['type' => 'deposit', 'amount' => $amount, 'timestamp' => microtime(true)];
}
public function withdraw(float $amount): void
{
if ($amount <= 0) {
throw new \InvalidArgumentException('Withdrawal amount must be positive');
}
if ($amount > $this->balance) {
throw new \OverflowException("Insufficient funds: balance={$this->balance}, requested={$amount}");
}
$this->balance -= $amount;
$this->transactions[] = ['type' => 'withdrawal', 'amount' => $amount, 'timestamp' => microtime(true)];
}
public function getBalance(): float { return $this->balance; }
public function getTransactions(): array { return $this->transactions; }
}
final class DepositCommand implements CommandInterface
{
private bool $executed = false;
public function __construct(
private readonly BankAccount $account,
private readonly float $amount,
) {}
public function execute(): CommandResult
{
try {
$this->account->deposit($this->amount);
$this->executed = true;
return new CommandResult(true, "Deposited {$this->amount}", timestamp: microtime(true));
} catch (\Throwable $e) {
return new CommandResult(false, "Deposit failed", $e->getMessage(), microtime(true));
}
}
public function undo(): CommandResult
{
if (!$this->executed) {
return new CommandResult(false, 'Cannot undo: command not executed');
}
try {
$this->account->withdraw($this->amount);
$this->executed = false;
return new CommandResult(true, "Reversed deposit of {$this->amount}", timestamp: microtime(true));
} catch (\Throwable $e) {
return new CommandResult(false, "Undo failed", $e->getMessage(), microtime(true));
}
}
public function getDescription(): string { return "Deposit {$this->amount}"; }
}
final class WithdrawCommand implements CommandInterface
{
private bool $executed = false;
public function __construct(
private readonly BankAccount $account,
private readonly float $amount,
) {}
public function execute(): CommandResult
{
try {
$this->account->withdraw($this->amount);
$this->executed = true;
return new CommandResult(true, "Withdrew {$this->amount}", timestamp: microtime(true));
} catch (\Throwable $e) {
return new CommandResult(false, "Withdrawal failed", $e->getMessage(), microtime(true));
}
}
public function undo(): CommandResult
{
if (!$this->executed) {
return new CommandResult(false, 'Cannot undo: command not executed');
}
try {
$this->account->deposit($this->amount);
$this->executed = false;
return new CommandResult(true, "Reversed withdrawal of {$this->amount}", timestamp: microtime(true));
} catch (\Throwable $e) {
return new CommandResult(false, "Undo failed", $e->getMessage(), microtime(true));
}
}
public function getDescription(): string { return "Withdraw {$this->amount}"; }
}
// [step] Implement the command history manager with redo support
final class CommandHistory
{
/** @var CommandResult[] */
private array $log = [];
/** @var CommandInterface[] */
private array $undoStack = [];
/** @var CommandInterface[] */
private array $redoStack = [];
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function execute(CommandInterface $command): CommandResult
{
$result = $command->execute();
$this->log[] = $result;
if ($result->success) {
$this->undoStack[] = $command;
$this->redoStack = []; // Clear redo stack on new action
$this->logger->info("Executed: {$command->getDescription()}");
} else {
$this->logger->error("Failed: {$command->getDescription()}", ['error' => $result->error]);
}
return $result;
}
public function undo(): ?CommandResult
{
$command = array_pop($this->undoStack);
if ($command === null) return null;
$result = $command->undo();
if ($result->success) {
$this->redoStack[] = $command;
$this->logger->info("Undone: {$command->getDescription()}");
}
$this->log[] = $result;
return $result;
}
public function redo(): ?CommandResult
{
$command = array_pop($this->redoStack);
if ($command === null) return null;
$result = $command->execute();
if ($result->success) {
$this->undoStack[] = $command;
$this->logger->info("Redone: {$command->getDescription()}");
}
$this->log[] = $result;
return $result;
}
/** @return CommandResult[] */
public function getLog(): array { return $this->log; }
}Command Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Command Pattern in the Real World
“Think of a restaurant order ticket. A waiter (invoker) takes your order and writes it on a slip (command). The slip is handed to the kitchen (receiver) which executes it. The waiter doesn't cook anything—they just carry and deliver orders. Tickets can be queued, cancelled before cooking, or reviewed in an audit log at day's end.”