Merged branch master into translate-template

This commit is contained in:
Axel Pardemann
2016-09-23 11:44:18 -05:00
288 changed files with 9359 additions and 9024 deletions

View File

@@ -2,77 +2,42 @@
namespace DesignPatterns\Behavioral\ChainOfResponsibilities;
/**
* Handler is a generic handler in the chain of responsibilities.
*
* Yes you could have a lighter CoR with a simpler handler but if you want your CoR
* to be extendable and decoupled, it's a better idea to do things like that in real
* situations. Usually, a CoR is meant to be changed everytime and evolves, that's
* why we slice the workflow in little bits of code.
*/
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
abstract class Handler
{
/**
* @var Handler
* @var Handler|null
*/
private $successor = null;
/**
* Append a responsibility to the end of chain.
*
* A prepend method could be done with the same spirit
*
* You could also send the successor in the constructor but in PHP that is a
* bad idea because you have to remove the type-hint of the parameter because
* the last handler has a null successor.
*
* And if you override the constructor, that Handler can no longer have a
* successor. One solution is to provide a NullObject (see pattern).
* It is more preferable to keep the constructor "free" to inject services
* you need with the DiC of symfony2 for example.
*
* @param Handler $handler
*/
final public function append(Handler $handler)
public function __construct(Handler $handler = null)
{
if (is_null($this->successor)) {
$this->successor = $handler;
} else {
$this->successor->append($handler);
}
$this->successor = $handler;
}
/**
* Handle the request.
*
* This approach by using a template method pattern ensures you that
* each subclass will not forget to call the successor. Besides, the returned
* boolean value indicates you if the request have been processed or not.
* each subclass will not forget to call the successor
*
* @param Request $req
* @param RequestInterface $request
*
* @return bool
* @return string|null
*/
final public function handle(Request $req)
final public function handle(RequestInterface $request)
{
$req->forDebugOnly = get_called_class();
$processed = $this->processing($req);
if (!$processed) {
$processed = $this->processing($request);
if ($processed === null) {
// the request has not been processed by this handler => see the next
if (!is_null($this->successor)) {
$processed = $this->successor->handle($req);
if ($this->successor !== null) {
$processed = $this->successor->handle($request);
}
}
return $processed;
}
/**
* Each concrete handler has to implement the processing of the request.
*
* @param Request $req
*
* @return bool true if the request has been processed
*/
abstract protected function processing(Request $req);
abstract protected function processing(RequestInterface $request);
}

View File

@@ -33,12 +33,6 @@ Code
You can also find these code on `GitHub`_
Request.php
.. literalinclude:: Request.php
:language: php
:linenos:
Handler.php
.. literalinclude:: Handler.php

View File

@@ -1,19 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities;
/**
* Request is a request which goes through the chain of responsibilities.
*
* About the request: Sometimes, you don't need an object, just an integer or
* an array. But in this case of a full example, I've made a class to illustrate
* this important idea in the CoR (Chain of Responsibilities). In the real world,
* I recommend to always use a class, even a \stdClass if you want, it proves
* to be more adaptive because a single handler doesn't know much about the
* outside world and it is more difficult if, one day, you want to add some
* criterion in a decision process.
*/
class Request
{
// getter and setter but I don't want to generate too much noise in handlers
}

View File

@@ -1,40 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Request;
/**
* Class FastStorage.
*/
class FastStorage extends Handler
{
/**
* @var array
*/
protected $data = array();
/**
* @param array $data
*/
public function __construct($data = array())
{
$this->data = $data;
}
protected function processing(Request $req)
{
if ('get' === $req->verb) {
if (array_key_exists($req->key, $this->data)) {
// the handler IS responsible and then processes the request
$req->response = $this->data[$req->key];
// instead of returning true, I could return the value but it proves
// to be a bad idea. What if the value IS "false" ?
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use Psr\Http\Message\RequestInterface;
class HttpInMemoryCacheHandler extends Handler
{
/**
* @var array
*/
private $data;
/**
* @param array $data
* @param Handler|null $successor
*/
public function __construct(array $data, Handler $successor = null)
{
parent::__construct($successor);
$this->data = $data;
}
/**
* @param RequestInterface $request
*
* @return string|null
*/
protected function processing(RequestInterface $request)
{
$key = sprintf(
'%s?%s',
$request->getUri()->getPath(),
$request->getUri()->getQuery()
);
if ($request->getMethod() == 'GET' && isset($this->data[$key])) {
return $this->data[$key];
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use Psr\Http\Message\RequestInterface;
class SlowDatabaseHandler extends Handler
{
/**
* @param RequestInterface $request
*
* @return string|null
*/
protected function processing(RequestInterface $request)
{
// this is a mockup, in production code you would ask a slow (compared to in-memory) DB for the results
return 'Hello World!';
}
}

View File

@@ -1,44 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Request;
/**
* This is mostly the same code as FastStorage but in fact, it may greatly differs.
*
* One important fact about CoR: each item in the chain MUST NOT assume its position
* in the chain. A CoR is not responsible if the request is not handled UNLESS
* you make an "ExceptionHandler" which throws exception if the request goes there.
*
* To be really extendable, each handler doesn't know if there is something after it.
*/
class SlowStorage extends Handler
{
/**
* @var array
*/
protected $data = array();
/**
* @param array $data
*/
public function __construct($data = array())
{
$this->data = $data;
}
protected function processing(Request $req)
{
if ('get' === $req->verb) {
if (array_key_exists($req->key, $this->data)) {
$req->response = $this->data[$req->key];
return true;
}
}
return false;
}
}

View File

@@ -2,80 +2,50 @@
namespace DesignPatterns\Behavioral\ChainOfResponsibilities\Tests;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Request;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\FastStorage;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowStorage;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Handler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\HttpInMemoryCacheHandler;
use DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowDatabaseHandler;
/**
* ChainTest tests the CoR.
*/
class ChainTest extends \PHPUnit_Framework_TestCase
{
/**
* @var FastStorage
* @var Handler
*/
protected $chain;
private $chain;
protected function setUp()
{
$this->chain = new FastStorage(array('bar' => 'baz'));
$this->chain->append(new SlowStorage(array('bar' => 'baz', 'foo' => 'bar')));
}
public function makeRequest()
{
$request = new Request();
$request->verb = 'get';
return array(
array($request),
$this->chain = new HttpInMemoryCacheHandler(
['/foo/bar?index=1' => 'Hello In Memory!'],
new SlowDatabaseHandler()
);
}
/**
* @dataProvider makeRequest
*/
public function testFastStorage($request)
public function testCanRequestKeyInFastStorage()
{
$request->key = 'bar';
$ret = $this->chain->handle($request);
$uri = $this->createMock('Psr\Http\Message\UriInterface');
$uri->method('getPath')->willReturn('/foo/bar');
$uri->method('getQuery')->willReturn('index=1');
$this->assertTrue($ret);
$this->assertObjectHasAttribute('response', $request);
$this->assertEquals('baz', $request->response);
// despite both handle owns the 'bar' key, the FastStorage is responding first
$className = 'DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\FastStorage';
$this->assertEquals($className, $request->forDebugOnly);
$request = $this->createMock('Psr\Http\Message\RequestInterface');
$request->method('getMethod')
->willReturn('GET');
$request->method('getUri')->willReturn($uri);
$this->assertEquals('Hello In Memory!', $this->chain->handle($request));
}
/**
* @dataProvider makeRequest
*/
public function testSlowStorage($request)
public function testCanRequestKeyInSlowStorage()
{
$request->key = 'foo';
$ret = $this->chain->handle($request);
$uri = $this->createMock('Psr\Http\Message\UriInterface');
$uri->method('getPath')->willReturn('/foo/baz');
$uri->method('getQuery')->willReturn('');
$this->assertTrue($ret);
$this->assertObjectHasAttribute('response', $request);
$this->assertEquals('bar', $request->response);
// FastStorage has no 'foo' key, the SlowStorage is responding
$className = 'DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowStorage';
$this->assertEquals($className, $request->forDebugOnly);
}
$request = $this->createMock('Psr\Http\Message\RequestInterface');
$request->method('getMethod')
->willReturn('GET');
$request->method('getUri')->willReturn($uri);
/**
* @dataProvider makeRequest
*/
public function testFailure($request)
{
$request->key = 'kurukuku';
$ret = $this->chain->handle($request);
$this->assertFalse($ret);
// the last responsible :
$className = 'DesignPatterns\Behavioral\ChainOfResponsibilities\Responsible\SlowStorage';
$this->assertEquals($className, $request->forDebugOnly);
$this->assertEquals('Hello World!', $this->chain->handle($request));
}
}

View File

@@ -4,14 +4,14 @@ namespace DesignPatterns\Behavioral\Command;
/**
* This concrete command tweaks receiver to add current date to messages
* invoker just knows that it can call "execute".
* invoker just knows that it can call "execute"
*/
class AddMessageDateCommand implements UndoableCommandInterface
{
/**
* @var Receiver
*/
protected $output;
private $output;
/**
* Each concrete command is built with different receivers.

View File

@@ -2,9 +2,6 @@
namespace DesignPatterns\Behavioral\Command;
/**
* class CommandInterface.
*/
interface CommandInterface
{
/**

View File

@@ -4,18 +4,18 @@ namespace DesignPatterns\Behavioral\Command;
/**
* This concrete command calls "print" on the Receiver, but an external
* invoker just knows that it can call "execute".
* invoker just knows that it can call "execute"
*/
class HelloCommand implements CommandInterface
{
/**
* @var Receiver
*/
protected $output;
private $output;
/**
* Each concrete command is built with different receivers.
* There can be one, many or completely no receivers, but there can be other commands in the parameters.
* There can be one, many or completely no receivers, but there can be other commands in the parameters
*
* @param Receiver $console
*/
@@ -29,8 +29,7 @@ class HelloCommand implements CommandInterface
*/
public function execute()
{
// sometimes, there is no receiver and this is the command which
// does all the work
// sometimes, there is no receiver and this is the command which does all the work
$this->output->write('Hello World');
}
}

View File

@@ -11,11 +11,11 @@ class Invoker
/**
* @var CommandInterface
*/
protected $command;
private $command;
/**
* In the invoker we find this kind of method for subscribing the command.
* There can be also a stack, a list, a fixed set...
* in the invoker we find this kind of method for subscribing the command
* There can be also a stack, a list, a fixed set ...
*
* @param CommandInterface $cmd
*/
@@ -25,12 +25,10 @@ class Invoker
}
/**
* executes the command.
* executes the command; the invoker is the same whatever is the command
*/
public function run()
{
// here is a key feature of the invoker
// the invoker is the same whatever is the command
$this->command->execute();
}
}

View File

@@ -7,14 +7,20 @@ namespace DesignPatterns\Behavioral\Command;
*/
class Receiver
{
/**
* @var bool
*/
private $enableDate = false;
private $output = array();
/**
* @var string[]
*/
private $output = [];
/**
* @param string $str
*/
public function write($str)
public function write(string $str)
{
if ($this->enableDate) {
$str .= ' ['.date('Y-m-d').']';
@@ -23,13 +29,13 @@ class Receiver
$this->output[] = $str;
}
public function getOutput()
public function getOutput(): string
{
return implode("\n", $this->output);
return join("\n", $this->output);
}
/**
* Enable receiver to display message date.
* Enable receiver to display message date
*/
public function enableDate()
{
@@ -37,7 +43,7 @@ class Receiver
}
/**
* Disable receiver to display message date.
* Disable receiver to display message date
*/
public function disableDate()
{

View File

@@ -6,31 +6,15 @@ use DesignPatterns\Behavioral\Command\HelloCommand;
use DesignPatterns\Behavioral\Command\Invoker;
use DesignPatterns\Behavioral\Command\Receiver;
/**
* CommandTest has the role of the Client in the Command Pattern.
*/
class CommandTest extends \PHPUnit_Framework_TestCase
{
/**
* @var Invoker
*/
protected $invoker;
/**
* @var Receiver
*/
protected $receiver;
protected function setUp()
{
$this->invoker = new Invoker();
$this->receiver = new Receiver();
}
public function testInvocation()
{
$this->invoker->setCommand(new HelloCommand($this->receiver));
$this->invoker->run();
$this->assertEquals($this->receiver->getOutput(), 'Hello World');
$invoker = new Invoker();
$receiver = new Receiver();
$invoker->setCommand(new HelloCommand($receiver));
$invoker->run();
$this->assertEquals($receiver->getOutput(), 'Hello World');
}
}

View File

@@ -6,44 +6,27 @@ use DesignPatterns\Behavioral\Command\AddMessageDateCommand;
use DesignPatterns\Behavioral\Command\HelloCommand;
use DesignPatterns\Behavioral\Command\Invoker;
use DesignPatterns\Behavioral\Command\Receiver;
use PHPUnit_Framework_TestCase;
/**
* UndoableCommandTest has the role of the Client in the Command Pattern.
*/
class UndoableCommandTest extends PHPUnit_Framework_TestCase
class UndoableCommandTest extends \PHPUnit_Framework_TestCase
{
/**
* @var Invoker
*/
protected $invoker;
/**
* @var Receiver
*/
protected $receiver;
protected function setUp()
{
$this->invoker = new Invoker();
$this->receiver = new Receiver();
}
public function testInvocation()
{
$this->invoker->setCommand(new HelloCommand($this->receiver));
$this->invoker->run();
$this->assertEquals($this->receiver->getOutput(), 'Hello World');
$invoker = new Invoker();
$receiver = new Receiver();
$messageDateCommand = new AddMessageDateCommand($this->receiver);
$invoker->setCommand(new HelloCommand($receiver));
$invoker->run();
$this->assertEquals($receiver->getOutput(), 'Hello World');
$messageDateCommand = new AddMessageDateCommand($receiver);
$messageDateCommand->execute();
$this->invoker->run();
$this->assertEquals($this->receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d').']');
$invoker->run();
$this->assertEquals($receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d').']');
$messageDateCommand->undo();
$this->invoker->run();
$this->assertEquals($this->receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d')."]\nHello World");
$invoker->run();
$this->assertEquals($receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d')."]\nHello World");
}
}

View File

@@ -2,14 +2,10 @@
namespace DesignPatterns\Behavioral\Command;
/**
* Interface UndoableCommandInterface.
*/
interface UndoableCommandInterface extends CommandInterface
{
/**
* This method is used to undo change made by command execution
* The Receiver goes in the constructor.
*/
public function undo();
}

View File

@@ -4,7 +4,7 @@ namespace DesignPatterns\Behavioral\Mediator;
/**
* Colleague is an abstract colleague who works together but he only knows
* the Mediator, not other colleague.
* the Mediator, not other colleagues
*/
abstract class Colleague
{
@@ -13,21 +13,13 @@ abstract class Colleague
*
* @var MediatorInterface
*/
private $mediator;
protected $mediator;
/**
* @param MediatorInterface $medium
* @param MediatorInterface $mediator
*/
public function __construct(MediatorInterface $medium)
public function setMediator(MediatorInterface $mediator)
{
// in this way, we are sure the concrete colleague knows the mediator
$this->mediator = $medium;
}
// for subclasses
protected function getMediator()
{
return $this->mediator;
$this->mediator = $mediator;
}
}

View File

@@ -3,59 +3,54 @@
namespace DesignPatterns\Behavioral\Mediator;
/**
* Mediator is the concrete Mediator for this design pattern.
* In this example, I have made a "Hello World" with the Mediator Pattern.
* Mediator is the concrete Mediator for this design pattern
*
* In this example, I have made a "Hello World" with the Mediator Pattern
*/
class Mediator implements MediatorInterface
{
/**
* @var Subsystem\Server
*/
protected $server;
private $server;
/**
* @var Subsystem\Database
*/
protected $database;
private $database;
/**
* @var Subsystem\Client
*/
protected $client;
private $client;
/**
* @param Subsystem\Database $db
* @param Subsystem\Client $cl
* @param Subsystem\Server $srv
* @param Subsystem\Database $database
* @param Subsystem\Client $client
* @param Subsystem\Server $server
*/
public function setColleague(Subsystem\Database $db, Subsystem\Client $cl, Subsystem\Server $srv)
public function __construct(Subsystem\Database $database, Subsystem\Client $client, Subsystem\Server $server)
{
$this->database = $db;
$this->server = $srv;
$this->client = $cl;
$this->database = $database;
$this->server = $server;
$this->client = $client;
$this->database->setMediator($this);
$this->server->setMediator($this);
$this->client->setMediator($this);
}
/**
* make request.
*/
public function makeRequest()
{
$this->server->process();
}
/**
* query db.
*
* @return mixed
*/
public function queryDb()
public function queryDb(): string
{
return $this->database->getData();
}
/**
* send response.
*
* @param string $content
*/
public function sendResponse($content)

View File

@@ -4,7 +4,7 @@ namespace DesignPatterns\Behavioral\Mediator;
/**
* MediatorInterface is a contract for the Mediator
* This interface is not mandatory but it is better for LSP concerns.
* This interface is not mandatory but it is better for Liskov substitution principle concerns.
*/
interface MediatorInterface
{
@@ -16,12 +16,12 @@ interface MediatorInterface
public function sendResponse($content);
/**
* makes a request.
* makes a request
*/
public function makeRequest();
/**
* queries the DB.
* queries the DB
*/
public function queryDb();
}

View File

@@ -5,24 +5,16 @@ namespace DesignPatterns\Behavioral\Mediator\Subsystem;
use DesignPatterns\Behavioral\Mediator\Colleague;
/**
* Client is a client that make request et get response.
* Client is a client that makes requests and gets the response response.
*/
class Client extends Colleague
{
/**
* request.
*/
public function request()
{
$this->getMediator()->makeRequest();
$this->mediator->makeRequest();
}
/**
* output content.
*
* @param string $content
*/
public function output($content)
public function output(string $content)
{
echo $content;
}

View File

@@ -4,15 +4,9 @@ namespace DesignPatterns\Behavioral\Mediator\Subsystem;
use DesignPatterns\Behavioral\Mediator\Colleague;
/**
* Database is a database service.
*/
class Database extends Colleague
{
/**
* @return string
*/
public function getData()
public function getData(): string
{
return 'World';
}

View File

@@ -4,17 +4,11 @@ namespace DesignPatterns\Behavioral\Mediator\Subsystem;
use DesignPatterns\Behavioral\Mediator\Colleague;
/**
* Server serves responses.
*/
class Server extends Colleague
{
/**
* process on server.
*/
public function process()
{
$data = $this->getMediator()->queryDb();
$this->getMediator()->sendResponse("Hello $data");
$data = $this->mediator->queryDb();
$this->mediator->sendResponse(sprintf("Hello %s", $data));
}
}

View File

@@ -7,27 +7,14 @@ use DesignPatterns\Behavioral\Mediator\Subsystem\Client;
use DesignPatterns\Behavioral\Mediator\Subsystem\Database;
use DesignPatterns\Behavioral\Mediator\Subsystem\Server;
/**
* MediatorTest tests hello world.
*/
class MediatorTest extends \PHPUnit_Framework_TestCase
{
protected $client;
protected function setUp()
{
$media = new Mediator();
$this->client = new Client($media);
$media->setColleague(new Database($media), $this->client, new Server($media));
}
public function testOutputHelloWorld()
{
// testing if Hello World is output :
$client = new Client();
new Mediator(new Database(), $client, new Server());
$this->expectOutputString('Hello World');
// as you see, the 3 components Client, Server and Database are totally decoupled
$this->client->request();
// Anyway, it remains complexity in the Mediator that's why the pattern
// Observer is preferable in mnay situations.
$client->request();
}
}

View File

@@ -1,49 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Memento;
class Caretaker
{
protected $history = array();
/**
* @return Memento
*/
public function getFromHistory($id)
{
return $this->history[$id];
}
/**
* @param Memento $state
*/
public function saveToHistory(Memento $state)
{
$this->history[] = $state;
}
public function runCustomLogic()
{
$originator = new Originator();
//Setting state to State1
$originator->setState('State1');
//Setting state to State2
$originator->setState('State2');
//Saving State2 to Memento
$this->saveToHistory($originator->getStateAsMemento());
//Setting state to State3
$originator->setState('State3');
// We can request multiple mementos, and choose which one to roll back to.
// Saving State3 to Memento
$this->saveToHistory($originator->getStateAsMemento());
//Setting state to State4
$originator->setState('State4');
$originator->restoreFromMemento($this->getFromHistory(1));
//State after restoring from Memento: State3
return $originator->getStateAsMemento()->getState();
}
}

View File

@@ -4,19 +4,21 @@ namespace DesignPatterns\Behavioral\Memento;
class Memento
{
/* @var mixed */
/**
* @var State
*/
private $state;
/**
* @param mixed $stateToSave
* @param State $stateToSave
*/
public function __construct($stateToSave)
public function __construct(State $stateToSave)
{
$this->state = $stateToSave;
}
/**
* @return mixed
* @return State
*/
public function getState()
{

View File

@@ -1,38 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Memento;
class Originator
{
/* @var mixed */
private $state;
// The class could also contain additional data that is not part of the
// state saved in the memento..
/**
* @param mixed $state
*/
public function setState($state)
{
// you must check type of state inside child of this class
// or use type-hinting for full pattern implementation
$this->state = $state;
}
/**
* @return Memento
*/
public function getStateAsMemento()
{
// you must save a separate copy in Memento
$state = is_object($this->state) ? clone $this->state : $this->state;
return new Memento($state);
}
public function restoreFromMemento(Memento $memento)
{
$this->state = $memento->getState();
}
}

View File

@@ -6,8 +6,8 @@ Purpose
It provides the ability to restore an object to it's previous state (undo
via rollback) or to gain access to state of the object, without revealing
it's implementation (i.e., the object is not required to have a functional
for return the current state).
it's implementation (i.e., the object is not required to have a function
to return the current state).
The memento pattern is implemented with three objects: the Originator, a
Caretaker and a Memento.
@@ -66,9 +66,9 @@ Originator.php
:language: php
:linenos:
Caretaker.php
Ticket.php
.. literalinclude:: Caretaker.php
.. literalinclude:: Ticket.php
:language: php
:linenos:

View File

@@ -0,0 +1,48 @@
<?php
namespace DesignPatterns\Behavioral\Memento;
class State
{
const STATE_CREATED = 'created';
const STATE_OPENED = 'opened';
const STATE_ASSIGNED = 'assigned';
const STATE_CLOSED = 'closed';
/**
* @var string
*/
private $state;
/**
* @var string[]
*/
private static $validStates = [
self::STATE_CREATED,
self::STATE_OPENED,
self::STATE_ASSIGNED,
self::STATE_CLOSED,
];
/**
* @param string $state
*/
public function __construct(string $state)
{
self::ensureIsValidState($state);
$this->state = $state;
}
private static function ensureIsValidState(string $state)
{
if (!in_array($state, self::$validStates)) {
throw new \InvalidArgumentException('Invalid state given');
}
}
public function __toString(): string
{
return $this->state;
}
}

View File

@@ -2,161 +2,30 @@
namespace DesignPatterns\Behavioral\Memento\Tests;
use DesignPatterns\Behavioral\Memento\Caretaker;
use DesignPatterns\Behavioral\Memento\Memento;
use DesignPatterns\Behavioral\Memento\Originator;
use DesignPatterns\Behavioral\Memento\State;
use DesignPatterns\Behavioral\Memento\Ticket;
/**
* MementoTest tests the memento pattern.
*/
class MementoTest extends \PHPUnit_Framework_TestCase
{
public function testUsageExample()
public function testOpenTicketAssignAndSetBackToOpen()
{
$originator = new Originator();
$caretaker = new Caretaker();
$ticket = new Ticket();
$character = new \stdClass();
// new object
$character->name = 'Gandalf';
// connect Originator to character object
$originator->setState($character);
// open the ticket
$ticket->open();
$openedState = $ticket->getState();
$this->assertEquals(State::STATE_OPENED, (string) $ticket->getState());
// work on the object
$character->name = 'Gandalf the Grey';
// still change something
$character->race = 'Maia';
// time to save state
$snapshot = $originator->getStateAsMemento();
// put state to log
$caretaker->saveToHistory($snapshot);
$memento = $ticket->saveToMemento();
// change something
$character->name = 'Sauron';
// and again
$character->race = 'Ainur';
// state inside the Originator was equally changed
$this->assertAttributeEquals($character, 'state', $originator);
// assign the ticket
$ticket->assign();
$this->assertEquals(State::STATE_ASSIGNED, (string) $ticket->getState());
// time to save another state
$snapshot = $originator->getStateAsMemento();
// put state to log
$caretaker->saveToHistory($snapshot);
// no restore to the opened state, but verify that the state object has been cloned for the memento
$ticket->restoreFromMemento($memento);
$rollback = $caretaker->getFromHistory(0);
// return to first state
$originator->restoreFromMemento($rollback);
// use character from old state
$character = $rollback->getState();
// yes, that what we need
$this->assertEquals('Gandalf the Grey', $character->name);
// make new changes
$character->name = 'Gandalf the White';
// and Originator linked to actual object again
$this->assertAttributeEquals($character, 'state', $originator);
}
public function testStringState()
{
$originator = new Originator();
$originator->setState('State1');
$this->assertAttributeEquals('State1', 'state', $originator);
$originator->setState('State2');
$this->assertAttributeEquals('State2', 'state', $originator);
$snapshot = $originator->getStateAsMemento();
$this->assertAttributeEquals('State2', 'state', $snapshot);
$originator->setState('State3');
$this->assertAttributeEquals('State3', 'state', $originator);
$originator->restoreFromMemento($snapshot);
$this->assertAttributeEquals('State2', 'state', $originator);
}
public function testSnapshotIsClone()
{
$originator = new Originator();
$object = new \stdClass();
$originator->setState($object);
$snapshot = $originator->getStateAsMemento();
$object->new_property = 1;
$this->assertAttributeEquals($object, 'state', $originator);
$this->assertAttributeNotEquals($object, 'state', $snapshot);
$originator->restoreFromMemento($snapshot);
$this->assertAttributeNotEquals($object, 'state', $originator);
}
public function testCanChangeActualState()
{
$originator = new Originator();
$first_state = new \stdClass();
$originator->setState($first_state);
$snapshot = $originator->getStateAsMemento();
$second_state = $snapshot->getState();
// still actual
$first_state->first_property = 1;
// just history
$second_state->second_property = 2;
$this->assertAttributeEquals($first_state, 'state', $originator);
$this->assertAttributeNotEquals($second_state, 'state', $originator);
$originator->restoreFromMemento($snapshot);
// now it lost state
$first_state->first_property = 11;
// must be actual
$second_state->second_property = 22;
$this->assertAttributeEquals($second_state, 'state', $originator);
$this->assertAttributeNotEquals($first_state, 'state', $originator);
}
public function testStateWithDifferentObjects()
{
$originator = new Originator();
$first = new \stdClass();
$first->data = 'foo';
$originator->setState($first);
$this->assertAttributeEquals($first, 'state', $originator);
$first_snapshot = $originator->getStateAsMemento();
$this->assertAttributeEquals($first, 'state', $first_snapshot);
$second = new \stdClass();
$second->data = 'bar';
$originator->setState($second);
$this->assertAttributeEquals($second, 'state', $originator);
$originator->restoreFromMemento($first_snapshot);
$this->assertAttributeEquals($first, 'state', $originator);
}
public function testCaretaker()
{
$caretaker = new Caretaker();
$memento1 = new Memento('foo');
$memento2 = new Memento('bar');
$caretaker->saveToHistory($memento1);
$caretaker->saveToHistory($memento2);
$this->assertAttributeEquals(array($memento1, $memento2), 'history', $caretaker);
$this->assertEquals($memento1, $caretaker->getFromHistory(0));
$this->assertEquals($memento2, $caretaker->getFromHistory(1));
}
public function testCaretakerCustomLogic()
{
$caretaker = new Caretaker();
$result = $caretaker->runCustomLogic();
$this->assertEquals('State3', $result);
$this->assertEquals(State::STATE_OPENED, (string) $ticket->getState());
$this->assertNotSame($openedState, $ticket->getState());
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace DesignPatterns\Behavioral\Memento;
/**
* Ticket is the "Originator" in this implementation
*/
class Ticket
{
/**
* @var State
*/
private $currentState;
public function __construct()
{
$this->currentState = new State(State::STATE_CREATED);
}
public function open()
{
$this->currentState = new State(State::STATE_OPENED);
}
public function assign()
{
$this->currentState = new State(State::STATE_ASSIGNED);
}
public function close()
{
$this->currentState = new State(State::STATE_CLOSED);
}
public function saveToMemento(): Memento
{
return new Memento(clone $this->currentState);
}
public function restoreFromMemento(Memento $memento)
{
$this->currentState = $memento->getState();
}
public function getState(): State
{
return $this->currentState;
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\Memento\Memento</OriginalElement>
<nodes>
<node x="0.0" y="0.0">\DesignPatterns\Behavioral\Memento\State</node>
<node x="313.0" y="252.0">\DesignPatterns\Behavioral\Memento\Memento</node>
<node x="0.0" y="252.0">\DesignPatterns\Behavioral\Memento\Ticket</node>
</nodes>
<notes />
<edges />
<settings layout="Hierarchic Group" zoom="1.0" x="150.0" y="130.0" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\Memento\Caretaker</OriginalElement>
<nodes>
<node x="182.0" y="153.0">\DesignPatterns\Behavioral\Memento\Caretaker</node>
<node x="0.0" y="0.0">\DesignPatterns\Behavioral\Memento\Originator</node>
<node x="0.0" y="153.0">\DesignPatterns\Behavioral\Memento\Memento</node>
</nodes>
<notes />
<edges />
<settings layout="Hierarchic Group" zoom="1.0" x="131.0" y="121.0" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Constructors</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 164 KiB

View File

@@ -3,16 +3,9 @@
namespace DesignPatterns\Behavioral\NullObject;
/**
* LoggerInterface is a contract for logging something.
*
* Key feature: NullLogger MUST inherit from this interface like any other Loggers
* Key feature: NullLogger must inherit from this interface like any other loggers
*/
interface LoggerInterface
{
/**
* @param string $str
*
* @return mixed
*/
public function log($str);
public function log(string $str);
}

View File

@@ -2,19 +2,9 @@
namespace DesignPatterns\Behavioral\NullObject;
/**
* Performance concerns : ok there is a call for nothing but we spare an "if is_null"
* I didn't run a benchmark but I think it's equivalent.
*
* Key feature : of course this logger MUST implement the same interface (or abstract)
* like the other loggers.
*/
class NullLogger implements LoggerInterface
{
/**
* {@inheritdoc}
*/
public function log($str)
public function log(string $str)
{
// do nothing
}

View File

@@ -2,15 +2,9 @@
namespace DesignPatterns\Behavioral\NullObject;
/**
* PrintLogger is a logger that prints the log entry to standard output.
*/
class PrintLogger implements LoggerInterface
{
/**
* @param string $str
*/
public function log($str)
public function log(string $str)
{
echo $str;
}

View File

@@ -2,24 +2,19 @@
namespace DesignPatterns\Behavioral\NullObject;
/**
* Service is dummy service that uses a logger.
*/
class Service
{
/**
* @var LoggerInterface
*/
protected $logger;
private $logger;
/**
* we inject the logger in ctor and it is mandatory.
*
* @param LoggerInterface $log
* @param LoggerInterface $logger
*/
public function __construct(LoggerInterface $log)
public function __construct(LoggerInterface $logger)
{
$this->logger = $log;
$this->logger = $logger;
}
/**
@@ -27,8 +22,7 @@ class Service
*/
public function doSomething()
{
// no more check "if (!is_null($this->logger))..." with the NullObject pattern
// notice here that you don't have to check if the logger is set with eg. is_null(), instead just use it
$this->logger->log('We are in '.__METHOD__);
// something to do...
}
}

View File

@@ -6,17 +6,12 @@ use DesignPatterns\Behavioral\NullObject\NullLogger;
use DesignPatterns\Behavioral\NullObject\PrintLogger;
use DesignPatterns\Behavioral\NullObject\Service;
/**
* LoggerTest tests for different loggers.
*/
class LoggerTest extends \PHPUnit_Framework_TestCase
{
public function testNullObject()
{
// one can use a singleton for NullObjet : I don't think it's a good idea
// because the purpose behind null object is to "avoid special case".
$service = new Service(new NullLogger());
$this->expectOutputString(null); // no output
$this->expectOutputString(null);
$service->doSomething();
}

View File

@@ -5,65 +5,16 @@ namespace DesignPatterns\Behavioral\Observer\Tests;
use DesignPatterns\Behavioral\Observer\User;
use DesignPatterns\Behavioral\Observer\UserObserver;
/**
* ObserverTest tests the Observer pattern.
*/
class ObserverTest extends \PHPUnit_Framework_TestCase
{
protected $observer;
protected function setUp()
public function testChangeInUserLeadsToUserObserverBeingNotified()
{
$this->observer = new UserObserver();
}
$observer = new UserObserver();
/**
* Tests the notification.
*/
public function testNotify()
{
$this->expectOutputString('DesignPatterns\Behavioral\Observer\User has been updated');
$subject = new User();
$user = new User();
$user->attach($observer);
$subject->attach($this->observer);
$subject->property = 123;
}
/**
* Tests the subscribing.
*/
public function testAttachDetach()
{
$subject = new User();
$reflection = new \ReflectionProperty($subject, 'observers');
$reflection->setAccessible(true);
/** @var \SplObjectStorage $observers */
$observers = $reflection->getValue($subject);
$this->assertInstanceOf('SplObjectStorage', $observers);
$this->assertFalse($observers->contains($this->observer));
$subject->attach($this->observer);
$this->assertTrue($observers->contains($this->observer));
$subject->detach($this->observer);
$this->assertFalse($observers->contains($this->observer));
}
/**
* Tests the update() invocation on a mockup.
*/
public function testUpdateCalling()
{
$subject = new User();
$observer = $this->getMock('SplObserver');
$subject->attach($observer);
$observer->expects($this->once())
->method('update')
->with($subject);
$subject->notify();
$user->changeEmail('foo@bar.com');
$this->assertCount(1, $observer->getChangedUsers());
}
}

View File

@@ -3,60 +3,42 @@
namespace DesignPatterns\Behavioral\Observer;
/**
* Observer pattern : The observed object (the subject).
*
* The subject maintains a list of Observers and sends notifications.
* User implements the observed object (called Subject), it maintains a list of observers and sends notifications to
* them in case changes are made on the User object
*/
class User implements \SplSubject
{
/**
* user data.
*
* @var array
* @var string
*/
protected $data = array();
private $email;
/**
* observers.
*
* @var \SplObjectStorage
*/
protected $observers;
private $observers;
public function __construct()
{
$this->observers = new \SplObjectStorage();
}
/**
* attach a new observer.
*
* @param \SplObserver $observer
*
* @return void
*/
public function attach(\SplObserver $observer)
{
$this->observers->attach($observer);
}
/**
* detach an observer.
*
* @param \SplObserver $observer
*
* @return void
*/
public function detach(\SplObserver $observer)
{
$this->observers->detach($observer);
}
/**
* notify observers.
*
* @return void
*/
public function changeEmail(string $email)
{
$this->email = $email;
$this->notify();
}
public function notify()
{
/** @var \SplObserver $observer */
@@ -64,21 +46,4 @@ class User implements \SplSubject
$observer->update($this);
}
}
/**
* Ideally one would better write setter/getter for all valid attributes and only call notify()
* on attributes that matter when changed.
*
* @param string $name
* @param mixed $value
*
* @return void
*/
public function __set($name, $value)
{
$this->data[$name] = $value;
// notify the observers, that user has been updated
$this->notify();
}
}

View File

@@ -2,19 +2,28 @@
namespace DesignPatterns\Behavioral\Observer;
/**
* class UserObserver.
*/
class UserObserver implements \SplObserver
{
/**
* This is the only method to implement as an observer.
* It is called by the Subject (usually by SplSubject::notify() ).
* @var User[]
*/
private $changedUsers = [];
/**
* It is called by the Subject, usually by SplSubject::notify()
*
* @param \SplSubject $subject
*/
public function update(\SplSubject $subject)
{
echo get_class($subject).' has been updated';
$this->changedUsers[] = clone $subject;
}
/**
* @return User[]
*/
public function getChangedUsers(): array
{
return $this->changedUsers;
}
}

View File

@@ -1,27 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\Observer\User</OriginalElement>
<nodes>
<node x="239.0" y="0.0">\DesignPatterns\Behavioral\Observer\UserObserver</node>
<node x="0.0" y="137.0">\DesignPatterns\Behavioral\Observer\User</node>
<node x="8.0" y="0.0">\SplSubject</node>
</nodes>
<notes />
<edges>
<edge source="\DesignPatterns\Behavioral\Observer\User" target="\SplSubject">
<point x="0.0" y="-74.0" />
<point x="0.0" y="43.5" />
</edge>
</edges>
<settings layout="Hierarchic Group" zoom="1.0" x="186.0" y="142.5" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Constructors</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\Observer\UserObserver</OriginalElement>
<nodes>
<node x="215.0" y="112.0">\DesignPatterns\Behavioral\Observer\UserObserver</node>
<node x="229.5" y="7.0">\SplObserver</node>
<node x="-29.0" y="25.0">\DesignPatterns\Behavioral\Observer\User</node>
</nodes>
<notes />
<edges>
<edge source="\DesignPatterns\Behavioral\Observer\UserObserver" target="\SplObserver">
<point x="0.0" y="-48.0" />
<point x="0.0" y="25.5" />
</edge>
</edges>
<settings layout="Hierarchic Group" zoom="1.0" x="58.0" y="91.0" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1,52 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
/**
* An abstract specification allows the creation of wrapped specifications.
*/
abstract class AbstractSpecification implements SpecificationInterface
{
/**
* Checks if given item meets all criteria.
*
* @param Item $item
*
* @return bool
*/
abstract public function isSatisfiedBy(Item $item);
/**
* Creates a new logical AND specification.
*
* @param SpecificationInterface $spec
*
* @return SpecificationInterface
*/
public function plus(SpecificationInterface $spec)
{
return new Plus($this, $spec);
}
/**
* Creates a new logical OR composite specification.
*
* @param SpecificationInterface $spec
*
* @return SpecificationInterface
*/
public function either(SpecificationInterface $spec)
{
return new Either($this, $spec);
}
/**
* Creates a new logical NOT specification.
*
* @return SpecificationInterface
*/
public function not()
{
return new Not($this);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
class AndSpecification implements SpecificationInterface
{
/**
* @var SpecificationInterface[]
*/
private $specifications;
/**
* @param SpecificationInterface[] ...$specifications
*/
public function __construct(SpecificationInterface ...$specifications)
{
$this->specifications = $specifications;
}
public function isSatisfiedBy(Item $item): bool
{
$satisfied = [];
foreach ($this->specifications as $specification) {
$satisfied[] = $specification->isSatisfiedBy($item);
}
return !in_array(false, $satisfied);
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
/**
* A logical OR specification.
*/
class Either extends AbstractSpecification
{
protected $left;
protected $right;
/**
* A composite wrapper of two specifications.
*
* @param SpecificationInterface $left
* @param SpecificationInterface $right
*/
public function __construct(SpecificationInterface $left, SpecificationInterface $right)
{
$this->left = $left;
$this->right = $right;
}
/**
* Returns the evaluation of both wrapped specifications as a logical OR.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item)
{
return $this->left->isSatisfiedBy($item) || $this->right->isSatisfiedBy($item);
}
}

View File

@@ -2,29 +2,19 @@
namespace DesignPatterns\Behavioral\Specification;
/**
* An trivial item.
*/
class Item
{
protected $price;
/**
* An item must have a price.
*
* @param int $price
* @var float
*/
public function __construct($price)
private $price;
public function __construct(float $price)
{
$this->price = $price;
}
/**
* Get the items price.
*
* @return int
*/
public function getPrice()
public function getPrice(): float
{
return $this->price;
}

View File

@@ -1,33 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
/**
* A logical Not specification.
*/
class Not extends AbstractSpecification
{
protected $spec;
/**
* Creates a new specification wrapping another.
*
* @param SpecificationInterface $spec
*/
public function __construct(SpecificationInterface $spec)
{
$this->spec = $spec;
}
/**
* Returns the negated result of the wrapped specification.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item)
{
return !$this->spec->isSatisfiedBy($item);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
class NotSpecification implements SpecificationInterface
{
/**
* @var SpecificationInterface
*/
private $specification;
public function __construct(SpecificationInterface $specification)
{
$this->specification = $specification;
}
public function isSatisfiedBy(Item $item): bool
{
return !$this->specification->isSatisfiedBy($item);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
class OrSpecification implements SpecificationInterface
{
/**
* @var SpecificationInterface[]
*/
private $specifications;
/**
* @param SpecificationInterface[] ...$specifications
*/
public function __construct(SpecificationInterface ...$specifications)
{
$this->specifications = $specifications;
}
public function isSatisfiedBy(Item $item): bool
{
$satisfied = [];
foreach ($this->specifications as $specification) {
$satisfied[] = $specification->isSatisfiedBy($item);
}
return in_array(true, $satisfied);
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Specification;
/**
* A logical AND specification.
*/
class Plus extends AbstractSpecification
{
protected $left;
protected $right;
/**
* Creation of a logical AND of two specifications.
*
* @param SpecificationInterface $left
* @param SpecificationInterface $right
*/
public function __construct(SpecificationInterface $left, SpecificationInterface $right)
{
$this->left = $left;
$this->right = $right;
}
/**
* Checks if the composite AND of specifications passes.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item)
{
return $this->left->isSatisfiedBy($item) && $this->right->isSatisfiedBy($item);
}
}

View File

@@ -2,47 +2,35 @@
namespace DesignPatterns\Behavioral\Specification;
/**
* A specification to check an Item is priced between min and max.
*/
class PriceSpecification extends AbstractSpecification
class PriceSpecification implements SpecificationInterface
{
protected $maxPrice;
protected $minPrice;
/**
* @var float|null
*/
private $maxPrice;
/**
* Sets the optional maximum price.
*
* @param int $maxPrice
* @var float|null
*/
public function setMaxPrice($maxPrice)
private $minPrice;
/**
* @param float $minPrice
* @param float $maxPrice
*/
public function __construct($minPrice, $maxPrice)
{
$this->minPrice = $minPrice;
$this->maxPrice = $maxPrice;
}
/**
* Sets the optional minimum price.
*
* @param int $minPrice
*/
public function setMinPrice($minPrice)
public function isSatisfiedBy(Item $item): bool
{
$this->minPrice = $minPrice;
}
/**
* Checks if Item price falls between bounds.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item)
{
if (!empty($this->maxPrice) && $item->getPrice() > $this->maxPrice) {
if ($this->maxPrice !== null && $item->getPrice() > $this->maxPrice) {
return false;
}
if (!empty($this->minPrice) && $item->getPrice() < $this->minPrice) {
if ($this->minPrice !== null && $item->getPrice() < $this->minPrice) {
return false;
}

View File

@@ -38,15 +38,9 @@ SpecificationInterface.php
:language: php
:linenos:
AbstractSpecification.php
OrSpecification.php
.. literalinclude:: AbstractSpecification.php
:language: php
:linenos:
Either.php
.. literalinclude:: Either.php
.. literalinclude:: OrSpecification.php
:language: php
:linenos:
@@ -56,15 +50,15 @@ PriceSpecification.php
:language: php
:linenos:
Plus.php
AndSpecification.php
.. literalinclude:: Plus.php
.. literalinclude:: AndSpecification.php
:language: php
:linenos:
Not.php
NotSpecification.php
.. literalinclude:: Not.php
.. literalinclude:: NotSpecification.php
:language: php
:linenos:

View File

@@ -2,36 +2,7 @@
namespace DesignPatterns\Behavioral\Specification;
/**
* An interface for a specification.
*/
interface SpecificationInterface
{
/**
* A boolean evaluation indicating if the object meets the specification.
*
* @param Item $item
*
* @return bool
*/
public function isSatisfiedBy(Item $item);
/**
* Creates a logical AND specification.
*
* @param SpecificationInterface $spec
*/
public function plus(SpecificationInterface $spec);
/**
* Creates a logical OR specification.
*
* @param SpecificationInterface $spec
*/
public function either(SpecificationInterface $spec);
/**
* Creates a logical not specification.
*/
public function not();
public function isSatisfiedBy(Item $item): bool;
}

View File

@@ -3,101 +3,44 @@
namespace DesignPatterns\Behavioral\Specification\Tests;
use DesignPatterns\Behavioral\Specification\Item;
use DesignPatterns\Behavioral\Specification\NotSpecification;
use DesignPatterns\Behavioral\Specification\OrSpecification;
use DesignPatterns\Behavioral\Specification\AndSpecification;
use DesignPatterns\Behavioral\Specification\PriceSpecification;
/**
* SpecificationTest tests the specification pattern.
*/
class SpecificationTest extends \PHPUnit_Framework_TestCase
{
public function testSimpleSpecification()
public function testCanOr()
{
$item = new Item(100);
$spec = new PriceSpecification();
$spec1 = new PriceSpecification(50, 99);
$spec2 = new PriceSpecification(101, 200);
$this->assertTrue($spec->isSatisfiedBy($item));
$orSpec = new OrSpecification($spec1, $spec2);
$spec->setMaxPrice(50);
$this->assertFalse($spec->isSatisfiedBy($item));
$spec->setMaxPrice(150);
$this->assertTrue($spec->isSatisfiedBy($item));
$spec->setMinPrice(101);
$this->assertFalse($spec->isSatisfiedBy($item));
$spec->setMinPrice(100);
$this->assertTrue($spec->isSatisfiedBy($item));
$this->assertFalse($orSpec->isSatisfiedBy(new Item(100)));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(51)));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(150)));
}
public function testNotSpecification()
public function testCanAnd()
{
$item = new Item(100);
$spec = new PriceSpecification();
$not = $spec->not();
$spec1 = new PriceSpecification(50, 100);
$spec2 = new PriceSpecification(80, 200);
$this->assertFalse($not->isSatisfiedBy($item));
$orSpec = new AndSpecification($spec1, $spec2);
$spec->setMaxPrice(50);
$this->assertTrue($not->isSatisfiedBy($item));
$spec->setMaxPrice(150);
$this->assertFalse($not->isSatisfiedBy($item));
$spec->setMinPrice(101);
$this->assertTrue($not->isSatisfiedBy($item));
$spec->setMinPrice(100);
$this->assertFalse($not->isSatisfiedBy($item));
$this->assertFalse($orSpec->isSatisfiedBy(new Item(150)));
$this->assertFalse($orSpec->isSatisfiedBy(new Item(1)));
$this->assertFalse($orSpec->isSatisfiedBy(new Item(51)));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(100)));
}
public function testPlusSpecification()
public function testCanNot()
{
$spec1 = new PriceSpecification();
$spec2 = new PriceSpecification();
$plus = $spec1->plus($spec2);
$spec1 = new PriceSpecification(50, 100);
$orSpec = new NotSpecification($spec1);
$item = new Item(100);
$this->assertTrue($plus->isSatisfiedBy($item));
$spec1->setMaxPrice(150);
$spec2->setMinPrice(50);
$this->assertTrue($plus->isSatisfiedBy($item));
$spec1->setMaxPrice(150);
$spec2->setMinPrice(101);
$this->assertFalse($plus->isSatisfiedBy($item));
$spec1->setMaxPrice(99);
$spec2->setMinPrice(50);
$this->assertFalse($plus->isSatisfiedBy($item));
}
public function testEitherSpecification()
{
$spec1 = new PriceSpecification();
$spec2 = new PriceSpecification();
$either = $spec1->either($spec2);
$item = new Item(100);
$this->assertTrue($either->isSatisfiedBy($item));
$spec1->setMaxPrice(150);
$spec2->setMaxPrice(150);
$this->assertTrue($either->isSatisfiedBy($item));
$spec1->setMaxPrice(150);
$spec2->setMaxPrice(0);
$this->assertTrue($either->isSatisfiedBy($item));
$spec1->setMaxPrice(0);
$spec2->setMaxPrice(150);
$this->assertTrue($either->isSatisfiedBy($item));
$spec1->setMaxPrice(99);
$spec2->setMaxPrice(99);
$this->assertFalse($either->isSatisfiedBy($item));
$this->assertTrue($orSpec->isSatisfiedBy(new Item(150)));
$this->assertFalse($orSpec->isSatisfiedBy(new Item(50)));
}
}

View File

@@ -2,49 +2,34 @@
namespace DesignPatterns\Behavioral\State;
/**
* Class CreateOrder.
*/
class CreateOrder implements OrderInterface
class CreateOrder implements Order
{
/**
* @var array
*/
private $order;
private $details;
/**
* @param array $order
*
* @throws \Exception
* @param array $details
*/
public function __construct(array $order)
public function __construct(array $details)
{
if (empty($order)) {
throw new \Exception('Order can not be empty!');
}
$this->order = $order;
$this->details = $details;
}
/**
* @return mixed
*/
public function shipOrder()
{
$this->order['status'] = 'shipping';
$this->order['updatedTime'] = time();
// Setting the new order status into database;
return $this->updateOrder($this->order);
$this->details['status'] = 'shipping';
$this->details['updatedTime'] = time();
}
/**
* @throws \Exception
*
* @return mixed|void
*/
public function completeOrder()
{
//Can not complete the order which status is created, throw exception;
throw new \Exception('Can not complete the order which status is created!');
throw new \Exception('Can not complete the order which status is created');
}
public function getStatus(): string
{
return $this->details['status'];
}
}

View File

@@ -2,10 +2,7 @@
namespace DesignPatterns\Behavioral\State;
/**
* Class OrderInterface.
*/
interface OrderInterface
interface Order
{
/**
* @return mixed
@@ -16,4 +13,6 @@ interface OrderInterface
* @return mixed
*/
public function completeOrder();
public function getStatus(): string;
}

View File

@@ -1,37 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\State;
/**
* Class OrderController.
*/
class OrderController
{
/**
* @param int $id
*/
public function shipAction($id)
{
$order = OrderFactory::getOrder($id);
try {
$order->shipOrder();
} catch (Exception $e) {
//handle error!
}
// response to browser
}
/**
* @param int $id
*/
public function completeAction($id)
{
$order = OrderFactory::getOrder($id);
try {
$order->completeOrder();
} catch (Exception $e) {
//handle error!
}
// response to browser
}
}

View File

@@ -1,36 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\State;
/**
* Class OrderFactory.
*/
class OrderFactory
{
private function __construct()
{
throw new \Exception('Can not instance the OrderFactory class!');
}
/**
* @param int $id
*
* @throws \Exception
*
* @return CreateOrder|ShippingOrder
*/
public static function getOrder($id)
{
$order = 'Get Order From Database';
switch ($order['status']) {
case 'created':
return new CreateOrder($order);
case 'shipping':
return new ShippingOrder($order);
default:
throw new \Exception('Order status error!');
break;
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace DesignPatterns\Behavioral\State;
class OrderRepository
{
/**
* @var array
*/
private static $orders = [
1 => ['status' => 'created'],
2 => ['status' => 'shipping'],
3 => ['status' => 'completed'],
];
public static function findById(int $id): Order
{
if (!isset(self::$orders[$id])) {
throw new \InvalidArgumentException(sprintf('Order with id %d does not exist', $id));
}
$order = self::$orders[$id];
switch ($order['status']) {
case 'created':
return new CreateOrder($order);
case 'shipping':
return new ShippingOrder($order);
default:
throw new \InvalidArgumentException('Invalid order status given');
break;
}
}
}

View File

@@ -20,21 +20,15 @@ Code
You can also find these code on `GitHub`_
OrderController.php
OrderRepository.php
.. literalinclude:: OrderController.php
.. literalinclude:: OrderRepository.php
:language: php
:linenos:
OrderFactory.php
Order.php
.. literalinclude:: OrderFactory.php
:language: php
:linenos:
OrderInterface.php
.. literalinclude:: OrderInterface.php
.. literalinclude:: Order.php
:language: php
:linenos:

View File

@@ -2,49 +2,34 @@
namespace DesignPatterns\Behavioral\State;
/**
* Class ShippingOrder.
*/
class ShippingOrder implements OrderInterface
class ShippingOrder implements Order
{
/**
* @var array
*/
private $order;
private $details;
/**
* @param array $order
*
* @throws \Exception
* @param array $details
*/
public function __construct(array $order)
public function __construct(array $details)
{
if (empty($order)) {
throw new \Exception('Order can not be empty!');
}
$this->order = $order;
$this->details = $details;
}
/**
* @throws \Exception
*
* @return mixed|void
*/
public function shipOrder()
{
//Can not ship the order which status is shipping, throw exception;
throw new \Exception('Can not ship the order which status is shipping!');
}
/**
* @return mixed
*/
public function completeOrder()
{
$this->order['status'] = 'completed';
$this->order['updatedTime'] = time();
$this->details['status'] = 'completed';
$this->details['updatedTime'] = time();
}
// Setting the new order status into database;
return $this->updateOrder($this->order);
public function getStatus(): string
{
return $this->details['status'];
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace DesignPatterns\Behavioral\State\Tests;
use DesignPatterns\Behavioral\State\OrderRepository;
class StateTest extends \PHPUnit_Framework_TestCase
{
public function testCanShipCreatedOrder()
{
$order = (new OrderRepository())->findById(1);
$order->shipOrder();
$this->assertEquals('shipping', $order->getStatus());
}
public function testCanCompleteShippedOrder()
{
$order = (new OrderRepository())->findById(2);
$order->completeOrder();
$this->assertEquals('completed', $order->getStatus());
}
/**
* @expectedException \Exception
*/
public function testThrowsExceptionWhenTryingToCompleteCreatedOrder()
{
$order = (new OrderRepository())->findById(1);
$order->completeOrder();
}
}

View File

@@ -2,16 +2,13 @@
namespace DesignPatterns\Behavioral\Strategy;
/**
* Class ComparatorInterface.
*/
interface ComparatorInterface
{
/**
* @param mixed $a
* @param mixed $b
*
* @return bool
* @return int
*/
public function compare($a, $b);
public function compare($a, $b): int;
}

View File

@@ -2,23 +2,19 @@
namespace DesignPatterns\Behavioral\Strategy;
/**
* Class DateComparator.
*/
class DateComparator implements ComparatorInterface
{
/**
* {@inheritdoc}
* @param mixed $a
* @param mixed $b
*
* @return int
*/
public function compare($a, $b)
public function compare($a, $b): int
{
$aDate = new \DateTime($a['date']);
$bDate = new \DateTime($b['date']);
if ($aDate == $bDate) {
return 0;
}
return $aDate < $bDate ? -1 : 1;
return $aDate <=> $bDate;
}
}

View File

@@ -2,20 +2,16 @@
namespace DesignPatterns\Behavioral\Strategy;
/**
* Class IdComparator.
*/
class IdComparator implements ComparatorInterface
{
/**
* {@inheritdoc}
* @param mixed $a
* @param mixed $b
*
* @return int
*/
public function compare($a, $b)
public function compare($a, $b): int
{
if ($a['id'] == $b['id']) {
return 0;
} else {
return $a['id'] < $b['id'] ? -1 : 1;
}
return $a['id'] <=> $b['id'];
}
}

View File

@@ -2,9 +2,6 @@
namespace DesignPatterns\Behavioral\Strategy;
/**
* Class ObjectCollection.
*/
class ObjectCollection
{
/**
@@ -20,30 +17,24 @@ class ObjectCollection
/**
* @param array $elements
*/
public function __construct(array $elements = array())
public function __construct(array $elements = [])
{
$this->elements = $elements;
}
/**
* @return array
*/
public function sort()
public function sort(): array
{
if (!$this->comparator) {
throw new \LogicException('Comparator is not set');
}
$callback = array($this->comparator, 'compare');
uasort($this->elements, $callback);
uasort($this->elements, [$this->comparator, 'compare']);
return $this->elements;
}
/**
* @param ComparatorInterface $comparator
*
* @return void
*/
public function setComparator(ComparatorInterface $comparator)
{

View File

@@ -5,43 +5,42 @@ namespace DesignPatterns\Behavioral\Strategy\Tests;
use DesignPatterns\Behavioral\Strategy\DateComparator;
use DesignPatterns\Behavioral\Strategy\IdComparator;
use DesignPatterns\Behavioral\Strategy\ObjectCollection;
use DesignPatterns\Behavioral\Strategy\Strategy;
/**
* Tests for Strategy pattern.
*/
class StrategyTest extends \PHPUnit_Framework_TestCase
{
public function getIdCollection()
public function provideIntegers()
{
return array(
array(
array(array('id' => 2), array('id' => 1), array('id' => 3)),
array('id' => 1),
),
array(
array(array('id' => 3), array('id' => 2), array('id' => 1)),
array('id' => 1),
),
);
return [
[
[['id' => 2], ['id' => 1], ['id' => 3]],
['id' => 1],
],
[
[['id' => 3], ['id' => 2], ['id' => 1]],
['id' => 1],
],
];
}
public function getDateCollection()
public function providateDates()
{
return array(
array(
array(array('date' => '2014-03-03'), array('date' => '2015-03-02'), array('date' => '2013-03-01')),
array('date' => '2013-03-01'),
),
array(
array(array('date' => '2014-02-03'), array('date' => '2013-02-01'), array('date' => '2015-02-02')),
array('date' => '2013-02-01'),
),
);
return [
[
[['date' => '2014-03-03'], ['date' => '2015-03-02'], ['date' => '2013-03-01']],
['date' => '2013-03-01'],
],
[
[['date' => '2014-02-03'], ['date' => '2013-02-01'], ['date' => '2015-02-02']],
['date' => '2013-02-01'],
],
];
}
/**
* @dataProvider getIdCollection
* @dataProvider provideIntegers
*
* @param array $collection
* @param array $expected
*/
public function testIdComparator($collection, $expected)
{
@@ -54,7 +53,10 @@ class StrategyTest extends \PHPUnit_Framework_TestCase
}
/**
* @dataProvider getDateCollection
* @dataProvider providateDates
*
* @param array $collection
* @param array $expected
*/
public function testDateComparator($collection, $expected)
{

View File

@@ -2,16 +2,10 @@
namespace DesignPatterns\Behavioral\TemplateMethod;
/**
* BeachJourney is vacation at the beach.
*/
class BeachJourney extends Journey
{
/**
* prints what to do to enjoy your vacation.
*/
protected function enjoyVacation()
protected function enjoyVacation(): string
{
echo "Swimming and sun-bathing\n";
return "Swimming and sun-bathing";
}
}

View File

@@ -2,16 +2,15 @@
namespace DesignPatterns\Behavioral\TemplateMethod;
/**
* CityJourney is a journey in a city.
*/
class CityJourney extends Journey
{
/**
* prints what to do in your journey to enjoy vacation.
*/
protected function enjoyVacation()
protected function enjoyVacation(): string
{
echo "Eat, drink, take photos and sleep\n";
return "Eat, drink, take photos and sleep";
}
protected function buyGift(): string
{
return "Buy a gift";
}
}

View File

@@ -4,6 +4,11 @@ namespace DesignPatterns\Behavioral\TemplateMethod;
abstract class Journey
{
/**
* @var string[]
*/
private $thingsToDo = [];
/**
* This is the public service provided by this class and its subclasses.
* Notice it is final to "freeze" the global behavior of algorithm.
@@ -12,46 +17,49 @@ abstract class Journey
*/
final public function takeATrip()
{
$this->buyAFlight();
$this->takePlane();
$this->enjoyVacation();
$this->buyGift();
$this->takePlane();
$this->thingsToDo[] = $this->buyAFlight();
$this->thingsToDo[] = $this->takePlane();
$this->thingsToDo[] = $this->enjoyVacation();
$buyGift = $this->buyGift();
if ($buyGift !== null) {
$this->thingsToDo[] = $buyGift;
}
$this->thingsToDo[] = $this->takePlane();
}
/**
* This method must be implemented, this is the key-feature of this pattern.
*/
abstract protected function enjoyVacation();
abstract protected function enjoyVacation(): string;
/**
* This method is also part of the algorithm but it is optional.
* This is an "adapter" (do not confuse with the Adapter pattern, not related)
* You can override it only if you need to.
* You can override it only if you need to
*
* @return null|string
*/
protected function buyGift()
{
return null;
}
private function buyAFlight(): string
{
return 'Buy a flight ticket';
}
private function takePlane(): string
{
return 'Taking the plane';
}
/**
* This method will be unknown by subclasses (better).
* @return string[]
*/
private function buyAFlight()
public function getThingsToDo(): array
{
echo "Buying a flight\n";
return $this->thingsToDo;
}
/**
* Subclasses will get access to this method but cannot override it and
* compromise this algorithm (warning : cause of cyclic dependencies).
*/
final protected function takePlane()
{
echo "Taking the plane\n";
}
// A note regarding the keyword "final" : don't use it when you start coding :
// add it after you narrow and know exactly what change and what remain unchanged
// in this algorithm.
// [abstract] x [3 access] x [final] = 12 combinations, it can be hard !
}

View File

@@ -4,40 +4,33 @@ namespace DesignPatterns\Behavioral\TemplateMethod\Tests;
use DesignPatterns\Behavioral\TemplateMethod;
/**
* JourneyTest tests all journeys.
*/
class JourneyTest extends \PHPUnit_Framework_TestCase
{
public function testBeach()
public function testCanGetOnVacationOnTheBeach()
{
$journey = new TemplateMethod\BeachJourney();
$this->expectOutputRegex('#sun-bathing#');
$journey->takeATrip();
$beachJourney = new TemplateMethod\BeachJourney();
$beachJourney->takeATrip();
$this->assertEquals(
['Buy a flight ticket', 'Taking the plane', 'Swimming and sun-bathing', 'Taking the plane'],
$beachJourney->getThingsToDo()
);
}
public function testCity()
public function testCanGetOnAJourneyToACity()
{
$journey = new TemplateMethod\CityJourney();
$this->expectOutputRegex('#drink#');
$journey->takeATrip();
}
$beachJourney = new TemplateMethod\CityJourney();
$beachJourney->takeATrip();
/**
* How to test an abstract template method with PHPUnit.
*/
public function testLasVegas()
{
$journey = $this->getMockForAbstractClass('DesignPatterns\Behavioral\TemplateMethod\Journey');
$journey->expects($this->once())
->method('enjoyVacation')
->will($this->returnCallback(array($this, 'mockUpVacation')));
$this->expectOutputRegex('#Las Vegas#');
$journey->takeATrip();
}
public function mockUpVacation()
{
echo "Fear and loathing in Las Vegas\n";
$this->assertEquals(
[
'Buy a flight ticket',
'Taking the plane',
'Eat, drink, take photos and sleep',
'Buy a gift',
'Taking the plane'
],
$beachJourney->getThingsToDo()
);
}
}

View File

@@ -1,35 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\TemplateMethod\BeachJourney</OriginalElement>
<nodes>
<node x="152.0" y="177.0">\DesignPatterns\Behavioral\TemplateMethod\CityJourney</node>
<node x="47.0" y="0.0">\DesignPatterns\Behavioral\TemplateMethod\Journey</node>
<node x="0.0" y="177.0">\DesignPatterns\Behavioral\TemplateMethod\BeachJourney</node>
</nodes>
<notes />
<edges>
<edge source="\DesignPatterns\Behavioral\TemplateMethod\CityJourney" target="\DesignPatterns\Behavioral\TemplateMethod\Journey">
<point x="0.0" y="-23.5" />
<point x="218.0" y="152.0" />
<point x="189.5" y="152.0" />
<point x="47.5" y="63.5" />
</edge>
<edge source="\DesignPatterns\Behavioral\TemplateMethod\BeachJourney" target="\DesignPatterns\Behavioral\TemplateMethod\Journey">
<point x="0.0" y="-23.5" />
<point x="66.0" y="152.0" />
<point x="94.5" y="152.0" />
<point x="-47.5" y="63.5" />
</edge>
</edges>
<settings layout="Hierarchic Group" zoom="1.0" x="142.0" y="112.0" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Constructors</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Behavioral\TemplateMethod\BeachJourney</OriginalElement>
<nodes>
<node x="-18.0" y="234.0">\DesignPatterns\Behavioral\TemplateMethod\BeachJourney</node>
<node x="13.0" y="-3.0">\DesignPatterns\Behavioral\TemplateMethod\Journey</node>
<node x="151.0" y="234.0">\DesignPatterns\Behavioral\TemplateMethod\CityJourney</node>
</nodes>
<notes />
<edges>
<edge source="\DesignPatterns\Behavioral\TemplateMethod\BeachJourney" target="\DesignPatterns\Behavioral\TemplateMethod\Journey">
<point x="0.0" y="-23.5" />
<point x="66.0" y="152.0" />
<point x="94.5" y="152.0" />
<point x="-47.5" y="63.5" />
</edge>
<edge source="\DesignPatterns\Behavioral\TemplateMethod\CityJourney" target="\DesignPatterns\Behavioral\TemplateMethod\Journey">
<point x="0.0" y="-23.5" />
<point x="218.0" y="152.0" />
<point x="189.5" y="152.0" />
<point x="47.5" y="63.5" />
</edge>
</edges>
<settings layout="Hierarchic Group" zoom="1.0" x="142.0" y="112.0" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Constructors</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 46 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -2,29 +2,25 @@
namespace DesignPatterns\Behavioral\Visitor;
/**
* An example of a Visitor: Group.
*/
class Group extends Role
class Group implements Role
{
/**
* @var string
*/
protected $name;
private $name;
/**
* @param string $name
*/
public function __construct($name)
public function __construct(string $name)
{
$this->name = (string) $name;
$this->name = $name;
}
/**
* @return string
*/
public function getName()
public function getName(): string
{
return 'Group: '.$this->name;
return sprintf('Group: %s', $this->name);
}
public function accept(RoleVisitorInterface $visitor)
{
$visitor->visitGroup($this);
}
}

View File

@@ -31,9 +31,9 @@ RoleVisitorInterface.php
:language: php
:linenos:
RolePrintVisitor.php
RoleVisitor.php
.. literalinclude:: RolePrintVisitor.php
.. literalinclude:: RoleVisitor.php
:language: php
:linenos:

View File

@@ -2,32 +2,7 @@
namespace DesignPatterns\Behavioral\Visitor;
/**
* class Role.
*/
abstract class Role
interface Role
{
/**
* This method handles a double dispatch based on the short name of the Visitor.
*
* Feel free to override it if your object must call another visiting behavior
*
* @param \DesignPatterns\Behavioral\Visitor\RoleVisitorInterface $visitor
*
* @throws \InvalidArgumentException
*/
public function accept(RoleVisitorInterface $visitor)
{
// this trick to simulate double-dispatch based on type-hinting
$klass = get_called_class();
preg_match('#([^\\\\]+)$#', $klass, $extract);
$visitingMethod = 'visit'.$extract[1];
// this ensures strong typing with visitor interface, not some visitor objects
if (!method_exists(__NAMESPACE__.'\RoleVisitorInterface', $visitingMethod)) {
throw new \InvalidArgumentException("The visitor you provide cannot visit a $klass instance");
}
call_user_func(array($visitor, $visitingMethod), $this);
}
public function accept(RoleVisitorInterface $visitor);
}

View File

@@ -1,27 +0,0 @@
<?php
namespace DesignPatterns\Behavioral\Visitor;
/**
* Visitor Pattern.
*
* An implementation of a concrete Visitor
*/
class RolePrintVisitor implements RoleVisitorInterface
{
/**
* {@inheritdoc}
*/
public function visitGroup(Group $role)
{
echo 'Role: '.$role->getName();
}
/**
* {@inheritdoc}
*/
public function visitUser(User $role)
{
echo 'Role: '.$role->getName();
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace DesignPatterns\Behavioral\Visitor;
class RoleVisitor implements RoleVisitorInterface
{
/**
* @var Role[]
*/
private $visited = [];
public function visitGroup(Group $role)
{
$this->visited[] = $role;
}
public function visitUser(User $role)
{
$this->visited[] = $role;
}
/**
* @return Role[]
*/
public function getVisited(): array
{
return $this->visited;
}
}

View File

@@ -3,29 +3,12 @@
namespace DesignPatterns\Behavioral\Visitor;
/**
* Visitor Pattern.
*
* The contract for the visitor.
*
* Note 1 : in C++ or Java, with method polymorphism based on type-hint, there are many
* methods visit() with different type for the 'role' parameter.
*
* Note 2 : the visitor must not choose itself which method to
* invoke, it is the Visitee that make this decision.
* Note: the visitor must not choose itself which method to
* invoke, it is the Visitee that make this decision
*/
interface RoleVisitorInterface
{
/**
* Visit a User object.
*
* @param \DesignPatterns\Behavioral\Visitor\User $role
*/
public function visitUser(User $role);
/**
* Visit a Group object.
*
* @param \DesignPatterns\Behavioral\Visitor\Group $role
*/
public function visitGroup(Group $role);
}

View File

@@ -4,42 +4,34 @@ namespace DesignPatterns\Tests\Visitor\Tests;
use DesignPatterns\Behavioral\Visitor;
/**
* VisitorTest tests the visitor pattern.
*/
class VisitorTest extends \PHPUnit_Framework_TestCase
{
protected $visitor;
/**
* @var Visitor\RoleVisitor
*/
private $visitor;
protected function setUp()
{
$this->visitor = new Visitor\RolePrintVisitor();
$this->visitor = new Visitor\RoleVisitor();
}
public function getRole()
public function provideRoles()
{
return array(
array(new Visitor\User('Dominik'), 'Role: User Dominik'),
array(new Visitor\Group('Administrators'), 'Role: Group: Administrators'),
);
return [
[new Visitor\User('Dominik')],
[new Visitor\Group('Administrators')],
];
}
/**
* @dataProvider getRole
* @dataProvider provideRoles
*
* @param Visitor\Role $role
*/
public function testVisitSomeRole(Visitor\Role $role, $expect)
public function testVisitSomeRole(Visitor\Role $role)
{
$this->expectOutputString($expect);
$role->accept($this->visitor);
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Mock
*/
public function testUnknownObject()
{
$mock = $this->getMockForAbstractClass('DesignPatterns\Behavioral\Visitor\Role');
$mock->accept($this->visitor);
$this->assertSame($role, $this->visitor->getVisited()[0]);
}
}

View File

@@ -2,31 +2,25 @@
namespace DesignPatterns\Behavioral\Visitor;
/**
* Visitor Pattern.
*
* One example for a visitee. Each visitee has to extends Role
*/
class User extends Role
class User implements Role
{
/**
* @var string
*/
protected $name;
private $name;
/**
* @param string $name
*/
public function __construct($name)
public function __construct(string $name)
{
$this->name = (string) $name;
$this->name = $name;
}
/**
* @return string
*/
public function getName()
public function getName(): string
{
return 'User '.$this->name;
return sprintf('User %s', $this->name);
}
public function accept(RoleVisitorInterface $visitor)
{
$visitor->visitUser($this);
}
}

View File

@@ -3,38 +3,10 @@
namespace DesignPatterns\Creational\AbstractFactory;
/**
* class AbstractFactory.
*
* Sometimes also known as "Kit" in a GUI libraries.
*
* This design pattern implements the Dependency Inversion Principle since
* it is the concrete subclass which creates concrete components.
*
* In this case, the abstract factory is a contract for creating some components
* for the web. There are two components : Text and Picture. There are two ways
* of rendering : HTML or JSON.
*
* Therefore 4 concrete classes, but the client just needs to know this contract
* to build a correct HTTP response (for a HTML page or for an AJAX request).
* for the web. There are two ways of rendering text: HTML and JSON
*/
abstract class AbstractFactory
{
/**
* Creates a text component.
*
* @param string $content
*
* @return Text
*/
abstract public function createText($content);
/**
* Creates a picture component.
*
* @param string $path
* @param string $name
*
* @return Picture
*/
abstract public function createPicture($path, $name = '');
abstract public function createText(string $content): Text;
}

View File

@@ -1,23 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory\Html;
use DesignPatterns\Creational\AbstractFactory\Picture as BasePicture;
/**
* Class Picture.
*
* Picture is a concrete image for HTML rendering
*/
class Picture extends BasePicture
{
/**
* some crude rendering from HTML output.
*
* @return string
*/
public function render()
{
return sprintf('<img src="%s" title="%s"/>', $this->path, $this->name);
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory\Html;
use DesignPatterns\Creational\AbstractFactory\Text as BaseText;
/**
* Class Text.
*
* Text is a concrete text for HTML rendering
*/
class Text extends BaseText
{
/**
* some crude rendering from HTML output.
*
* @return string
*/
public function render()
{
return '<div>'.htmlspecialchars($this->text).'</div>';
}
}

View File

@@ -2,35 +2,10 @@
namespace DesignPatterns\Creational\AbstractFactory;
/**
* Class HtmlFactory.
*
* HtmlFactory is a concrete factory for HTML component
*/
class HtmlFactory extends AbstractFactory
{
/**
* Creates a picture component.
*
* @param string $path
* @param string $name
*
* @return Html\Picture|Picture
*/
public function createPicture($path, $name = '')
public function createText(string $content): Text
{
return new Html\Picture($path, $name);
}
/**
* Creates a text component.
*
* @param string $content
*
* @return Html\Text|Text
*/
public function createText($content)
{
return new Html\Text($content);
return new HtmlText($content);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class HtmlText extends Text
{
// do something here
}

View File

@@ -1,23 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory\Json;
use DesignPatterns\Creational\AbstractFactory\Picture as BasePicture;
/**
* Class Picture.
*
* Picture is a concrete image for JSON rendering
*/
class Picture extends BasePicture
{
/**
* some crude rendering from JSON output.
*
* @return string
*/
public function render()
{
return json_encode(array('title' => $this->name, 'path' => $this->path));
}
}

View File

@@ -1,23 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory\Json;
use DesignPatterns\Creational\AbstractFactory\Text as BaseText;
/**
* Class Text.
*
* Text is a text component with a JSON rendering
*/
class Text extends BaseText
{
/**
* some crude rendering from JSON output.
*
* @return string
*/
public function render()
{
return json_encode(array('content' => $this->text));
}
}

View File

@@ -2,36 +2,10 @@
namespace DesignPatterns\Creational\AbstractFactory;
/**
* Class JsonFactory.
*
* JsonFactory is a factory for creating a family of JSON component
* (example for ajax)
*/
class JsonFactory extends AbstractFactory
{
/**
* Creates a picture component.
*
* @param string $path
* @param string $name
*
* @return Json\Picture|Picture
*/
public function createPicture($path, $name = '')
public function createText(string $content): Text
{
return new Json\Picture($path, $name);
}
/**
* Creates a text component.
*
* @param string $content
*
* @return Json\Text|Text
*/
public function createText($content)
{
return new Json\Text($content);
return new JsonText($content);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory;
class JsonText extends Text
{
// do something here
}

View File

@@ -1,19 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory;
/**
* Interface MediaInterface.
*
* This contract is not part of the pattern, in general case, each component
* are not related
*/
interface MediaInterface
{
/**
* some crude rendering from JSON or html output (depended on concrete class).
*
* @return string
*/
public function render();
}

View File

@@ -1,29 +0,0 @@
<?php
namespace DesignPatterns\Creational\AbstractFactory;
/**
* Class Picture.
*/
abstract class Picture implements MediaInterface
{
/**
* @var string
*/
protected $path;
/**
* @var string
*/
protected $name;
/**
* @param string $path
* @param string $name
*/
public function __construct($path, $name = '')
{
$this->name = (string) $name;
$this->path = (string) $path;
}
}

View File

@@ -39,45 +39,21 @@ HtmlFactory.php
:language: php
:linenos:
MediaInterface.php
.. literalinclude:: MediaInterface.php
:language: php
:linenos:
Picture.php
.. literalinclude:: Picture.php
:language: php
:linenos:
Text.php
.. literalinclude:: Text.php
:language: php
:linenos:
Json/Picture.php
JsonText.php
.. literalinclude:: Json/Picture.php
.. literalinclude:: JsonText.php
:language: php
:linenos:
Json/Text.php
HtmlText.php
.. literalinclude:: Json/Text.php
:language: php
:linenos:
Html/Picture.php
.. literalinclude:: Html/Picture.php
:language: php
:linenos:
Html/Text.php
.. literalinclude:: Html/Text.php
.. literalinclude:: HtmlText.php
:language: php
:linenos:

View File

@@ -2,43 +2,24 @@
namespace DesignPatterns\Creational\AbstractFactory\Tests;
use DesignPatterns\Creational\AbstractFactory\AbstractFactory;
use DesignPatterns\Creational\AbstractFactory\HtmlFactory;
use DesignPatterns\Creational\AbstractFactory\JsonFactory;
/**
* AbstractFactoryTest tests concrete factories.
*/
class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
{
public function getFactories()
public function testCanCreateHtmlText()
{
return array(
array(new JsonFactory()),
array(new HtmlFactory()),
);
$factory = new HtmlFactory();
$text = $factory->createText('foobar');
$this->assertInstanceOf('DesignPatterns\Creational\AbstractFactory\HtmlText', $text);
}
/**
* This is the client of factories. Note that the client does not
* care which factory is given to him, he can create any component he
* wants and render how he wants.
*
* @dataProvider getFactories
*/
public function testComponentCreation(AbstractFactory $factory)
public function testCanCreateJsonText()
{
$article = array(
$factory->createText('Lorem Ipsum'),
$factory->createPicture('/image.jpg', 'caption'),
$factory->createText('footnotes'),
);
$factory = new JsonFactory();
$text = $factory->createText('foobar');
$this->assertContainsOnly('DesignPatterns\Creational\AbstractFactory\MediaInterface', $article);
/* this is the time to look at the Builder pattern. This pattern
* helps you to create complex object like that article above with
* a given Abstract Factory
*/
$this->assertInstanceOf('DesignPatterns\Creational\AbstractFactory\JsonText', $text);
}
}

View File

@@ -2,21 +2,15 @@
namespace DesignPatterns\Creational\AbstractFactory;
/**
* Class Text.
*/
abstract class Text implements MediaInterface
abstract class Text
{
/**
* @var string
*/
protected $text;
private $text;
/**
* @param string $text
*/
public function __construct($text)
public function __construct(string $text)
{
$this->text = (string) $text;
$this->text = $text;
}
}

View File

@@ -1,38 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Creational\AbstractFactory\AbstractFactory</OriginalElement>
<nodes>
<node x="281.0" y="228.0">\DesignPatterns\Creational\AbstractFactory\Html\Text</node>
<node x="150.0" y="229.0">\DesignPatterns\Creational\AbstractFactory\Html\Picture</node>
<node x="223.0" y="117.0">\DesignPatterns\Creational\AbstractFactory\HtmlFactory</node>
<node x="0.0" y="117.0">\DesignPatterns\Creational\AbstractFactory\JsonFactory</node>
<node x="126.0" y="0.0">\DesignPatterns\Creational\AbstractFactory\AbstractFactory</node>
<node x="0.0" y="229.0">\DesignPatterns\Creational\AbstractFactory\MediaInterface</node>
</nodes>
<notes />
<edges>
<edge source="\DesignPatterns\Creational\AbstractFactory\HtmlFactory" target="\DesignPatterns\Creational\AbstractFactory\AbstractFactory">
<point x="0.0" y="-33.5" />
<point x="324.5" y="92.0" />
<point x="256.5" y="92.0" />
<point x="43.5" y="33.5" />
</edge>
<edge source="\DesignPatterns\Creational\AbstractFactory\JsonFactory" target="\DesignPatterns\Creational\AbstractFactory\AbstractFactory">
<point x="0.0" y="-33.5" />
<point x="101.5" y="92.0" />
<point x="169.5" y="92.0" />
<point x="-43.5" y="33.5" />
</edge>
</edges>
<settings layout="Hierarchic Group" zoom="1.0" x="213.0" y="138.0" />
<SelectedNodes />
<Categories>
<Category>Fields</Category>
<Category>Constants</Category>
<Category>Constructors</Category>
<Category>Methods</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>
<?xml version="1.0" encoding="UTF-8"?>
<Diagram>
<ID>PHP</ID>
<OriginalElement>\DesignPatterns\Creational\AbstractFactory\AbstractFactory</OriginalElement>
<nodes>
<node x="214.0" y="101.0">\DesignPatterns\Creational\AbstractFactory\JsonFactory</node>
<node x="107.0" y="0.0">\DesignPatterns\Creational\AbstractFactory\AbstractFactory</node>
<node x="0.0" y="101.0">\DesignPatterns\Creational\AbstractFactory\HtmlFactory</node>
<node x="111.0" y="298.0">\DesignPatterns\Creational\AbstractFactory\JsonText</node>
<node x="0.0" y="298.0">\DesignPatterns\Creational\AbstractFactory\HtmlText</node>
<node x="51.5" y="197.0">\DesignPatterns\Creational\AbstractFactory\Text</node>
</nodes>
<notes />
<edges>
<edge source="\DesignPatterns\Creational\AbstractFactory\HtmlText" target="\DesignPatterns\Creational\AbstractFactory\Text">
<point x="0.0" y="-14.5" />
<point x="45.5" y="273.0" />
<point x="75.75" y="273.0" />
<point x="-24.25" y="25.5" />
</edge>
<edge source="\DesignPatterns\Creational\AbstractFactory\JsonText" target="\DesignPatterns\Creational\AbstractFactory\Text">
<point x="0.0" y="-14.5" />
<point x="154.5" y="273.0" />
<point x="124.25" y="273.0" />
<point x="24.25" y="25.5" />
</edge>
<edge source="\DesignPatterns\Creational\AbstractFactory\JsonFactory" target="\DesignPatterns\Creational\AbstractFactory\AbstractFactory">
<point x="0.0" y="-25.5" />
<point x="311.0" y="76.0" />
<point x="252.5" y="76.0" />
<point x="48.5" y="25.5" />
</edge>
<edge source="\DesignPatterns\Creational\AbstractFactory\HtmlFactory" target="\DesignPatterns\Creational\AbstractFactory\AbstractFactory">
<point x="0.0" y="-25.5" />
<point x="97.0" y="76.0" />
<point x="155.5" y="76.0" />
<point x="-48.5" y="25.5" />
</edge>
</edges>
<settings layout="Hierarchic Group" zoom="1.0" x="117.0" y="130.5" />
<SelectedNodes>
<node>\DesignPatterns\Creational\AbstractFactory\AbstractFactory</node>
</SelectedNodes>
<Categories>
<Category>Methods</Category>
<Category>Constants</Category>
<Category>Fields</Category>
</Categories>
<VISIBILITY>private</VISIBILITY>
</Diagram>

Some files were not shown because too many files have changed in this diff Show More