Memento — snapshot and restore object state
Concept
Memento pattern captures and externalizes an object's internal state so the object can be restored to that state later — without violating encapsulation. The object saves a snapshot of itself (the Memento), and restores from that snapshot when needed.
Three participants:
- Originator: The object whose state is saved/restored. Creates Mementos. Restores from Mementos. Mementos contain a copy of Originator's private state.
- Memento: Holds the saved state. Opaque to everyone except the Originator.
- Caretaker: Stores Mementos (the history). Tells Originator when to save or restore. Does NOT access Memento contents.
Encapsulation preservation: The Caretaker holds Mementos but can't read their contents (Memento's state is private, only Originator can access it). This is the key difference from just copying an array.
PHP implementation: In PHP 8+, you can use readonly classes for Mementos. The Memento class can be nested inside the Originator class so only the Originator can construct/read it.
Undo/Redo systems: The Caretaker maintains a stack of Mementos. undo(): pop the last Memento, restore. redo(): pop from redo stack, restore.
Memory concern: Each Memento is a full copy of the Originator's state. For large objects, this can be expensive. Optimize with incremental snapshots (store only the diff).
Laravel connection: withoutEvents() saves/restores model state. Database transactions are the runtime equivalent of Memento for database state.
Code Example
<?php
// Originator
class TextDocument
{
private string $content = '';
private string $fontName = 'Arial';
private int $fontSize = 12;
public function setContent(string $content): void { $this->content = $content; }
public function setFont(string $name, int $size): void { $this->fontName = $name; $this->fontSize = $size; }
public function getContent(): string { return $this->content; }
public function display(): void
{
echo "Content: '{$this->content}' | Font: {$this->fontName} {$this->fontSize}px\n";
}
// Creates a Memento (snapshot)
public function save(): TextDocumentMemento
{
return new TextDocumentMemento($this->content, $this->fontName, $this->fontSize);
}
// Restores from a Memento
public function restore(TextDocumentMemento $memento): void
{
$this->content = $memento->getContent();
$this->fontName = $memento->getFontName();
$this->fontSize = $memento->getFontSize();
}
}
// Memento — stores a snapshot of Originator's state
class TextDocumentMemento
{
public function __construct(
private readonly string $content,
private readonly string $fontName,
private readonly int $fontSize,
) {}
// Only the Originator should call these
public function getContent(): string { return $this->content; }
public function getFontName(): string { return $this->fontName; }
public function getFontSize(): int { return $this->fontSize; }
}
// Caretaker — manages the history of Mementos
class UndoHistory
{
private \SplStack $undoStack;
private \SplStack $redoStack;
public function __construct()
{
$this->undoStack = new \SplStack();
$this->redoStack = new \SplStack();
}
public function push(TextDocumentMemento $memento): void
{
$this->undoStack->push($memento);
// Clear redo history when new action is taken
while (!$this->redoStack->isEmpty()) {
$this->redoStack->pop();
}
}
public function undo(): ?TextDocumentMemento
{
if ($this->undoStack->isEmpty()) return null;
$memento = $this->undoStack->pop();
$this->redoStack->push($memento);
return $this->undoStack->isEmpty() ? null : $this->undoStack->top();
}
public function canUndo(): bool { return $this->undoStack->count() > 1; }
}
// Usage
$doc = new TextDocument();
$history = new UndoHistory();
$doc->setContent('Hello');
$history->push($doc->save());
$doc->setContent('Hello World');
$history->push($doc->save());
$doc->setFont('Times New Roman', 14);
$doc->display(); // Content: 'Hello World' | Font: Times New Roman 14px
if ($history->canUndo()) {
$doc->restore($history->undo());
$doc->display(); // Content: 'Hello' | Font: Arial 12px
}