3.5. Memento

3.5.1. Purpose

It provides the ability to restore an object to it’s previous state (undovia rollback) or to gain access to state of the object, without revealingit’s implementation (i.e., the object is not required to have a functionto return the current state).

The memento pattern is implemented with three objects: the Originator, aCaretaker and a Memento.

Memento – an object that contains a concrete unique snapshot of state ofany object or resource: string, number, array, an instance of class and so on.The uniqueness in this case does not imply the prohibition existence of similarstates in different snapshots. That means the state can be extracted asthe independent clone. Any object stored in the Memento should bea full copy of the original object rather than a reference to the originalobject. The Memento object is a “opaque object” (the object that no one canor should change).

Originator – it is an object that contains the actual state of an externalobject is strictly specified type. Originator is able to create a uniquecopy of this state and return it wrapped in a Memento. The Originator doesnot know the history of changes. You can set a concrete state to Originatorfrom the outside, which will be considered as actual. The Originator mustmake sure that given state corresponds the allowed type of object. Originatormay (but not should) have any methods, but they they can’t make changes tothe saved object state.

Caretaker controls the states history. He may make changes to an object;take a decision to save the state of an external object in the Originator;ask from the Originator snapshot of the current state; or set the Originatorstate to equivalence with some snapshot from history.

3.5.2. Examples

  • The seed of a pseudorandom number generator
  • The state in a finite state machine
  • Control for intermediate states of ORM Model before saving

3.5.3. UML Diagram

Alt Momento UML Diagram

3.5.4. Code

You can also find this code on GitHub

Memento.php

  1. <?php
  2.  
  3. namespace DesignPatterns\Behavioral\Memento;
  4.  
  5. class Memento
  6. {
  7. /**
  8. * @var State
  9. */
  10. private $state;
  11.  
  12. /**
  13. * @param State $stateToSave
  14. */
  15. public function __construct(State $stateToSave)
  16. {
  17. $this->state = $stateToSave;
  18. }
  19.  
  20. /**
  21. * @return State
  22. */
  23. public function getState()
  24. {
  25. return $this->state;
  26. }
  27. }

State.php

  1. <?php
  2.  
  3. namespace DesignPatterns\Behavioral\Memento;
  4.  
  5. class State
  6. {
  7. const STATE_CREATED = 'created';
  8. const STATE_OPENED = 'opened';
  9. const STATE_ASSIGNED = 'assigned';
  10. const STATE_CLOSED = 'closed';
  11.  
  12. /**
  13. * @var string
  14. */
  15. private $state;
  16.  
  17. /**
  18. * @var string[]
  19. */
  20. private static $validStates = [
  21. self::STATE_CREATED,
  22. self::STATE_OPENED,
  23. self::STATE_ASSIGNED,
  24. self::STATE_CLOSED,
  25. ];
  26.  
  27. /**
  28. * @param string $state
  29. */
  30. public function __construct(string $state)
  31. {
  32. self::ensureIsValidState($state);
  33.  
  34. $this->state = $state;
  35. }
  36.  
  37. private static function ensureIsValidState(string $state)
  38. {
  39. if (!in_array($state, self::$validStates)) {
  40. throw new \InvalidArgumentException('Invalid state given');
  41. }
  42. }
  43.  
  44. public function __toString(): string
  45. {
  46. return $this->state;
  47. }
  48. }

Ticket.php

  1. <?php
  2.  
  3. namespace DesignPatterns\Behavioral\Memento;
  4.  
  5. /**
  6. * Ticket is the "Originator" in this implementation
  7. */
  8. class Ticket
  9. {
  10. /**
  11. * @var State
  12. */
  13. private $currentState;
  14.  
  15. public function __construct()
  16. {
  17. $this->currentState = new State(State::STATE_CREATED);
  18. }
  19.  
  20. public function open()
  21. {
  22. $this->currentState = new State(State::STATE_OPENED);
  23. }
  24.  
  25. public function assign()
  26. {
  27. $this->currentState = new State(State::STATE_ASSIGNED);
  28. }
  29.  
  30. public function close()
  31. {
  32. $this->currentState = new State(State::STATE_CLOSED);
  33. }
  34.  
  35. public function saveToMemento(): Memento
  36. {
  37. return new Memento(clone $this->currentState);
  38. }
  39.  
  40. public function restoreFromMemento(Memento $memento)
  41. {
  42. $this->currentState = $memento->getState();
  43. }
  44.  
  45. public function getState(): State
  46. {
  47. return $this->currentState;
  48. }
  49. }

3.5.5. Test

Tests/MementoTest.php

  1. <?php
  2.  
  3. namespace DesignPatterns\Behavioral\Memento\Tests;
  4.  
  5. use DesignPatterns\Behavioral\Memento\State;
  6. use DesignPatterns\Behavioral\Memento\Ticket;
  7. use PHPUnit\Framework\TestCase;
  8.  
  9. class MementoTest extends TestCase
  10. {
  11. public function testOpenTicketAssignAndSetBackToOpen()
  12. {
  13. $ticket = new Ticket();
  14.  
  15. // open the ticket
  16. $ticket->open();
  17. $openedState = $ticket->getState();
  18. $this->assertEquals(State::STATE_OPENED, (string) $ticket->getState());
  19.  
  20. $memento = $ticket->saveToMemento();
  21.  
  22. // assign the ticket
  23. $ticket->assign();
  24. $this->assertEquals(State::STATE_ASSIGNED, (string) $ticket->getState());
  25.  
  26. // now restore to the opened state, but verify that the state object has been cloned for the memento
  27. $ticket->restoreFromMemento($memento);
  28.  
  29. $this->assertEquals(State::STATE_OPENED, (string) $ticket->getState());
  30. $this->assertNotSame($openedState, $ticket->getState());
  31. }
  32. }