1.8. State

1.8.1. Intent

According to the Gang of Four, the State pattern is a way to “allow an object to alter its behavior when its internal state changes. The object will appear to change its class.” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 305).

1.8.2. When to use it?

The State pattern should be used in mainly two different cases:

  • the behavior of an object depends on its state, and the behavior have to change dynamically (according to the state)
  • lots of conditional structures are used to handle what to do depending on the state of the object

1.8.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the State pattern

1.8.4. Implementation

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

namespace Phpatterns\Behavioral\State;

interface BookingStateInterface
{
    /**
     * @param Booking $booking
     * @throws \Exception
     * @return mixed
     */
    public function cancel(Booking $booking);

    /**
     * @param Booking $booking
     * @throws \Exception
     * @return mixed
     */
    public function pay(Booking $booking);

    /**
     * @param Booking $booking
     * @throws \Exception
     * @return mixed
     */
    public function reserve(Booking $booking);
}

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

namespace Phpatterns\Behavioral\State\BookingState;

use Phpatterns\Behavioral\State;

class Cancelled implements State\BookingStateInterface
{
    /**
     * @param State\Booking $booking
     * @throws \Exception
     * @return bool
     */
    public function cancel(State\Booking $booking)
    {
        throw new \Exception('Already cancelled');
    }

    /**
     * @param State\Booking $booking
     * @throws \Exception
     * @return bool
     */
    public function pay(State\Booking $booking)
    {
        throw new \Exception('Order cancelled');
    }

    /**
     * @param State\Booking $booking
     * @throws \Exception
     * @return bool
     */
    public function reserve(State\Booking $booking)
    {
        throw new \Exception('Order cancelled');
    }
}

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

namespace Phpatterns\Behavioral\State\BookingState;

use Phpatterns\Behavioral\State;

class Prepared implements State\BookingStateInterface
{
    /**
     * @param State\Booking $booking
     * @return bool
     */
    public function cancel(State\Booking $booking)
    {
        // ...
        // Cancel the reservation
        // ...

        $booking->setState(new Cancelled());
        return true;
    }

    /**
     * @param State\Booking $booking
     * @return bool
     */
    public function pay(State\Booking $booking)
    {
        // ...
        // Do the necessary to pay
        // ...

        $booking->setState(new Reserved());
        return true;
    }

    /**
     * @param State\Booking $booking
     * @throws \Exception
     * @return bool
     */
    public function reserve(State\Booking $booking)
    {
        throw new \Exception('The order has to be paid before processing reservation');
    }
}

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

namespace Phpatterns\Behavioral\State\BookingState;

use Phpatterns\Behavioral\State;

class Reserved implements State\BookingStateInterface
{
    /**
     * The reservation can always be canceled
     * @param State\Booking $booking
     * @return bool
     */
    public function cancel(State\Booking $booking)
    {
        // ...
        // Cancel the reservation
        // ...

        $booking->setState(new Cancelled());
        return true;
    }

    /**
     * @param State\Booking $booking
     * @throws \Exception
     * @return bool
     */
    public function pay(State\Booking $booking)
    {
        throw new \Exception('Already paid');
    }

    /**
     * @param State\Booking $booking
     * @throws \Exception
     * @return bool
     */
    public function reserve(State\Booking $booking)
    {
        throw new \Exception('Already reserved');
    }
}

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

namespace Phpatterns\Behavioral\State;

class Booking
{
    /** @var BookingStateInterface */
    private $state = null;

    /**
     * @throws \Exception
     * @return bool
     */
    public function cancel()
    {
        return $this->state->cancel($this);
    }

    /**
     * @throws \Exception
     * @return bool
     */
    public function pay()
    {
        return $this->state->pay($this);
    }

    /**
     * @throws \Exception
     * @return bool
     */
    public function reserve()
    {
        return $this->state->reserve($this);
    }

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

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

1.8.5. Tests

StateTest.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
56
57
58
59
60
<?php

namespace Test\Phpatterns\Behavioral\State;

use Phpatterns\Behavioral\State;
use Phpatterns\Behavioral\State\BookingState;

class StateTest extends \PHPUnit_Framework_TestCase
{
    /** @var State\Booking */
    private $booking;

    protected function setUp()
    {
        $this->booking = new State\Booking();
    }

    public function testPreparedStateMechanism()
    {
        $this->booking->setState(new BookingState\Prepared());
        $this->assertTrue($this->booking->cancel());
        $this->assertInstanceOf(BookingState\Cancelled::class, $this->booking->getState());

        $this->booking->setState(new BookingState\Prepared());
        $this->assertTrue($this->booking->pay());
        $this->assertInstanceOf(BookingState\Reserved::class, $this->booking->getState());

        $this->booking->setState(new BookingState\Prepared());
        $this->setExpectedException('Exception', 'The order has to be paid before processing reservation');
        $this->booking->reserve();
    }

    public function testReservedStateMechanism()
    {
        $this->booking->setState(new BookingState\Reserved());
        $this->assertTrue($this->booking->cancel());
        $this->assertInstanceOf(BookingState\Cancelled::class, $this->booking->getState());

        $this->booking->setState(new BookingState\Reserved());
        $this->setExpectedException('Exception', 'Already paid');
        $this->booking->pay();

        $this->setExpectedException('Exception', 'Already reserved');
        $this->booking->reserve();
    }

    public function testCancelledStateMechanism()
    {
        $this->booking->setState(new BookingState\Cancelled());

        $this->setExpectedException('Exception', 'Already cancelled');
        $this->booking->cancel();

        $this->setExpectedException('Exception', 'Order cancelled');
        $this->booking->pay();

        $this->setExpectedException('Exception', 'Order cancelled');
        $this->booking->reserve();
    }
}