BehavioralPHPverifiedVerified
Memento Pattern in PHP
Captures and externalizes an object's internal state without violating encapsulation, allowing the object to be restored to that state later.
How to Implement the Memento Pattern in PHP
1Step 1: Define the memento (snapshot)
class Memento
{
public function __construct(
private readonly string $state,
private readonly \DateTimeImmutable $date,
) {}
public function getState(): string { return $this->state; }
public function getDate(): \DateTimeImmutable { return $this->date; }
}2Step 2: Define the originator that creates and restores from mementos
class TextEditor
{
private string $content = '';
public function type(string $text): void
{
$this->content .= $text;
}
public function getContent(): string { return $this->content; }
public function save(): Memento
{
return new Memento($this->content, new \DateTimeImmutable());
}
public function restore(Memento $memento): void
{
$this->content = $memento->getState();
}
}3Step 3: Implement the caretaker that manages memento history
class History
{
/** @var Memento[] */
private array $snapshots = [];
public function push(Memento $memento): void
{
$this->snapshots[] = $memento;
}
public function pop(): ?Memento
{
return array_pop($this->snapshots);
}
}
// Usage
$editor = new TextEditor();
$history = new History();
$editor->type('Hello ');
$history->push($editor->save());
$editor->type('World');
echo $editor->getContent(); // "Hello World"
$editor->restore($history->pop());
echo $editor->getContent(); // "Hello "<?php
declare(strict_types=1);
// [step] Define a type-safe, immutable memento with metadata
final readonly class EditorMemento
{
/** @param array<string, mixed> $metadata */
public function __construct(
private string $content,
private int $cursorPosition,
private array $metadata,
private \DateTimeImmutable $createdAt,
) {}
public function getContent(): string { return $this->content; }
public function getCursorPosition(): int { return $this->cursorPosition; }
/** @return array<string, mixed> */
public function getMetadata(): array { return $this->metadata; }
public function getCreatedAt(): \DateTimeImmutable { return $this->createdAt; }
}
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
}
// [step] Implement the originator with rich state
final class DocumentEditor
{
private string $content = '';
private int $cursorPosition = 0;
/** @var array<string, mixed> */
private array $formatting = [];
public function type(string $text): void
{
$this->content = substr($this->content, 0, $this->cursorPosition)
. $text
. substr($this->content, $this->cursorPosition);
$this->cursorPosition += strlen($text);
}
public function deleteChars(int $count): void
{
$start = max(0, $this->cursorPosition - $count);
$this->content = substr($this->content, 0, $start)
. substr($this->content, $this->cursorPosition);
$this->cursorPosition = $start;
}
public function moveCursor(int $position): void
{
$this->cursorPosition = max(0, min($position, strlen($this->content)));
}
public function setFormatting(string $key, mixed $value): void
{
$this->formatting[$key] = $value;
}
public function getContent(): string { return $this->content; }
public function getCursorPosition(): int { return $this->cursorPosition; }
public function save(): EditorMemento
{
return new EditorMemento(
content: $this->content,
cursorPosition: $this->cursorPosition,
metadata: ['formatting' => $this->formatting],
createdAt: new \DateTimeImmutable(),
);
}
public function restore(EditorMemento $memento): void
{
$this->content = $memento->getContent();
$this->cursorPosition = $memento->getCursorPosition();
$this->formatting = $memento->getMetadata()['formatting'] ?? [];
}
}
// [step] Implement the caretaker with bounded history and redo stack
final class UndoManager
{
/** @var EditorMemento[] */
private array $undoStack = [];
/** @var EditorMemento[] */
private array $redoStack = [];
public function __construct(
private readonly int $maxHistory = 100,
private readonly LoggerInterface $logger,
) {}
public function save(EditorMemento $memento): void
{
$this->undoStack[] = $memento;
$this->redoStack = []; // New action clears redo history
// Enforce max history
if (count($this->undoStack) > $this->maxHistory) {
array_shift($this->undoStack);
}
$this->logger->info('State saved', [
'historySize' => count($this->undoStack),
]);
}
public function undo(): ?EditorMemento
{
if (empty($this->undoStack)) return null;
$memento = array_pop($this->undoStack);
$this->redoStack[] = $memento;
// Return the previous state (top of remaining undo stack)
return end($this->undoStack) ?: null;
}
public function redo(): ?EditorMemento
{
if (empty($this->redoStack)) return null;
$memento = array_pop($this->redoStack);
$this->undoStack[] = $memento;
return $memento;
}
public function canUndo(): bool { return count($this->undoStack) > 1; }
public function canRedo(): bool { return !empty($this->redoStack); }
public function getHistorySize(): int { return count($this->undoStack); }
}Memento Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Memento Pattern in the Real World
“Think of a video game save point. When you save, the game (originator) packages your character's stats, inventory, and position into a save file (memento). The save system (caretaker) stores these files without understanding their contents. When you die and reload, the save file is handed back to the game, which restores everything exactly as it was.”