0

Memento — snapshot and restore object state

Intermediate5 min read·eng-04-010
compare

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
<?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
}