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.
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();
}
}
|