1.6. Memento

1.6.1. Intent

According to the Gang of Four, the Memento pattern is defined like that: “Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 283).

1.6.2. When to use it?

Memento pattern should be used to implement undo / redo mechanisms (you save a snapshot of an object’s state and you restore or not that state later).

1.6.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the Memento pattern

1.6.4. Implementation

Order.php

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

namespace Phpatterns\Behavioral\Memento;

/**
 * Class Order
 * Used as the Originator (part of the Memento pattern).
 * It's the state of that object that we want to save and restore.
 */
class Order
{
    /** @var string */
    private $customerName;

    /** @var OrderState */
    private $state;

    public function __construct($customerName, OrderState $state)
    {
        $this->customerName = $customerName;
        $this->state = $state;
    }

    /**
     * @return Memento
     */
    public function exportState()
    {
        return new Memento(clone $this->state);
    }

    /**
     * @param Memento $memento
     */
    public function restoreState(Memento $memento)
    {
        $this->state = $memento->getState();
    }

    /**
     * @return OrderState
     */
    public function getState()
    {
        return $this->state;
    }

    /**
     * @param OrderState $state
     */
    public function setState(OrderState $state)
    {
        $this->state = $state;
    }
}

OrderState.php

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

namespace Phpatterns\Behavioral\Memento;

class OrderState
{
    const ORDER_STATUS_PENDING = 1; //default
    const ORDER_STATUS_PAID = 2;
    const ORDER_STATUS_PREPARED = 3;
    const ORDER_STATUS_SHIPPED = 4;
    const ORDER_STATUS_DELIVERED = 5;
    const ORDER_STATUS_CANCELED = 6;

    /** @var int */
    private $status;

    /** @var \DateTime */
    private $created;

    public function __construct($status = self::ORDER_STATUS_PENDING)
    {
        $this->status = $status;
        $this->created = new \DateTime('now');
    }
}

Memento.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace Phpatterns\Behavioral\Memento;

class Memento
{
    /** @var OrderState */
    private $state;

    public function __construct(OrderState $state)
    {
        $this->state = $state;
    }

    /**
     * @return OrderState
     */
    public function getState()
    {
        return $this->state;
    }
}

History.php

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

namespace Phpatterns\Behavioral\Memento;

/**
 * Class History
 * Used as the Caretaker (part of the Memento pattern)
 */
class History
{
    /** @var Memento[] */
    private $mementos;

    public function __construct()
    {
        $this->mementos = [];
    }

    /**
     * Add a Memento to the history stack
     * @param Memento $memento
     */
    public function addMemento(Memento $memento)
    {
        $this->mementos[] = $memento;
    }

    /**
     * Retrieve the last Memento inserted
     * @return Memento
     */
    public function popMemento()
    {
        return array_pop($this->mementos);
    }
}

1.6.5. Tests

MementoTest.php

 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 Test\Phpatterns\Behavioral\Memento;

use Phpatterns\Behavioral\Memento;

class MementoTest extends \PHPUnit_Framework_TestCase
{
    /** @var Memento\Order */
    private $order;

    protected function setUp()
    {
        //the Originator
        $this->order = new Memento\Order(
            'Mike Tyson',
            new Memento\OrderState(Memento\OrderState::ORDER_STATUS_PENDING)
        );
    }

    public function testUndoMechanism()
    {
        //the Caretaker
        $history = new Memento\History();
        $history->addMemento($this->order->exportState());

        //storing current state and changing Order's state
        $previousState = $this->order->getState();
        $this->order->setState(new Memento\OrderState(Memento\OrderState::ORDER_STATUS_PAID));
        $this->assertNotEquals($previousState, $this->order->getState());

        //restoring previous state
        $this->order->restoreState($history->popMemento());
        $this->assertEquals($previousState, $this->order->getState());
    }

    public function testMementoStateInstanceIsDifferentFromOrderState()
    {
        //the Caretaker
        $history = new Memento\History();
        $history->addMemento($this->order->exportState());

        $this->assertNotSame(
            $history->popMemento()->getState(),
            $this->order->getState()
        );
    }
}