sync
@@ -3,17 +3,10 @@ language: php
|
||||
sudo: false
|
||||
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
- hhvm
|
||||
- 7.1
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: hhvm
|
||||
- php: 7.0
|
||||
fast_finish: true
|
||||
|
||||
cache:
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -31,13 +31,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
|
||||
Request.php
|
||||
|
||||
.. literalinclude:: Request.php
|
||||
:language: php
|
||||
:linenos:
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
Handler.php
|
||||
|
||||
|
@@ -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
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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!';
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -2,80 +2,51 @@
|
||||
|
||||
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;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* ChainTest tests the CoR.
|
||||
*/
|
||||
class ChainTest extends \PHPUnit_Framework_TestCase
|
||||
class ChainTest extends 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));
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -2,9 +2,6 @@
|
||||
|
||||
namespace DesignPatterns\Behavioral\Command;
|
||||
|
||||
/**
|
||||
* class CommandInterface.
|
||||
*/
|
||||
interface CommandInterface
|
||||
{
|
||||
/**
|
||||
|
@@ -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');
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -38,7 +38,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
CommandInterface.php
|
||||
|
||||
|
@@ -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()
|
||||
{
|
||||
|
@@ -5,32 +5,17 @@ namespace DesignPatterns\Behavioral\Command\Tests;
|
||||
use DesignPatterns\Behavioral\Command\HelloCommand;
|
||||
use DesignPatterns\Behavioral\Command\Invoker;
|
||||
use DesignPatterns\Behavioral\Command\Receiver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* CommandTest has the role of the Client in the Command Pattern.
|
||||
*/
|
||||
class CommandTest extends \PHPUnit_Framework_TestCase
|
||||
class CommandTest extends 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('Hello World', $receiver->getOutput());
|
||||
}
|
||||
}
|
||||
|
@@ -6,44 +6,28 @@ use DesignPatterns\Behavioral\Command\AddMessageDateCommand;
|
||||
use DesignPatterns\Behavioral\Command\HelloCommand;
|
||||
use DesignPatterns\Behavioral\Command\Invoker;
|
||||
use DesignPatterns\Behavioral\Command\Receiver;
|
||||
use PHPUnit_Framework_TestCase;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* UndoableCommandTest has the role of the Client in the Command Pattern.
|
||||
*/
|
||||
class UndoableCommandTest extends PHPUnit_Framework_TestCase
|
||||
class UndoableCommandTest extends 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('Hello World', $receiver->getOutput());
|
||||
|
||||
$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("Hello World\nHello World [".date('Y-m-d').']', $receiver->getOutput());
|
||||
|
||||
$messageDateCommand->undo();
|
||||
|
||||
$this->invoker->run();
|
||||
$this->assertEquals($this->receiver->getOutput(), "Hello World\nHello World [".date('Y-m-d')."]\nHello World");
|
||||
$invoker->run();
|
||||
$this->assertEquals("Hello World\nHello World [".date('Y-m-d')."]\nHello World", $receiver->getOutput());
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -4,27 +4,33 @@ namespace DesignPatterns\Behavioral\Iterator;
|
||||
|
||||
class Book
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $author;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $title;
|
||||
|
||||
public function __construct($title, $author)
|
||||
public function __construct(string $title, string $author)
|
||||
{
|
||||
$this->author = $author;
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function getAuthor()
|
||||
public function getAuthor(): string
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getAuthorAndTitle()
|
||||
public function getAuthorAndTitle(): string
|
||||
{
|
||||
return $this->getTitle().' by '.$this->getAuthor();
|
||||
}
|
||||
|
@@ -2,18 +2,17 @@
|
||||
|
||||
namespace DesignPatterns\Behavioral\Iterator;
|
||||
|
||||
class BookList implements \Countable
|
||||
class BookList implements \Countable, \Iterator
|
||||
{
|
||||
private $books;
|
||||
/**
|
||||
* @var Book[]
|
||||
*/
|
||||
private $books = [];
|
||||
|
||||
public function getBook($bookNumberToGet)
|
||||
{
|
||||
if (isset($this->books[$bookNumberToGet])) {
|
||||
return $this->books[$bookNumberToGet];
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $currentIndex = 0;
|
||||
|
||||
public function addBook(Book $book)
|
||||
{
|
||||
@@ -23,15 +22,41 @@ class BookList implements \Countable
|
||||
public function removeBook(Book $bookToRemove)
|
||||
{
|
||||
foreach ($this->books as $key => $book) {
|
||||
/** @var Book $book */
|
||||
if ($book->getAuthorAndTitle() === $bookToRemove->getAuthorAndTitle()) {
|
||||
unset($this->books[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->books = array_values($this->books);
|
||||
}
|
||||
|
||||
public function count()
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->books);
|
||||
}
|
||||
|
||||
public function current(): Book
|
||||
{
|
||||
return $this->books[$this->currentIndex];
|
||||
}
|
||||
|
||||
public function key(): int
|
||||
{
|
||||
return $this->currentIndex;
|
||||
}
|
||||
|
||||
public function next()
|
||||
{
|
||||
$this->currentIndex++;
|
||||
}
|
||||
|
||||
public function rewind()
|
||||
{
|
||||
$this->currentIndex = 0;
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return isset($this->books[$this->currentIndex]);
|
||||
}
|
||||
}
|
||||
|
@@ -1,86 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DesignPatterns\Behavioral\Iterator;
|
||||
|
||||
class BookListIterator implements \Iterator
|
||||
{
|
||||
/**
|
||||
* @var BookList
|
||||
*/
|
||||
private $bookList;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $currentBook = 0;
|
||||
|
||||
public function __construct(BookList $bookList)
|
||||
{
|
||||
$this->bookList = $bookList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current book.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return Book Can return any type.
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->bookList->getBook($this->currentBook);
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.0.0)<br/>
|
||||
* Move forward to next element.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void Any returned value is ignored.
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->currentBook++;
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.0.0)<br/>
|
||||
* Return the key of the current element.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return mixed scalar on success, or null on failure.
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->currentBook;
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.0.0)<br/>
|
||||
* Checks if current position is valid.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return bool The return value will be casted to boolean and then evaluated.
|
||||
* Returns true on success or false on failure.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return null !== $this->bookList->getBook($this->currentBook);
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.0.0)<br/>
|
||||
* Rewind the Iterator to the first element.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void Any returned value is ignored.
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->currentBook = 0;
|
||||
}
|
||||
}
|
@@ -1,87 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DesignPatterns\Behavioral\Iterator;
|
||||
|
||||
class BookListReverseIterator implements \Iterator
|
||||
{
|
||||
/**
|
||||
* @var BookList
|
||||
*/
|
||||
private $bookList;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $currentBook = 0;
|
||||
|
||||
public function __construct(BookList $bookList)
|
||||
{
|
||||
$this->bookList = $bookList;
|
||||
$this->currentBook = $this->bookList->count() - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current book.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.current.php
|
||||
*
|
||||
* @return Book Can return any type.
|
||||
*/
|
||||
public function current()
|
||||
{
|
||||
return $this->bookList->getBook($this->currentBook);
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.0.0)<br/>
|
||||
* Move forward to next element.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.next.php
|
||||
*
|
||||
* @return void Any returned value is ignored.
|
||||
*/
|
||||
public function next()
|
||||
{
|
||||
$this->currentBook--;
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.0.0)<br/>
|
||||
* Return the key of the current element.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.key.php
|
||||
*
|
||||
* @return mixed scalar on success, or null on failure.
|
||||
*/
|
||||
public function key()
|
||||
{
|
||||
return $this->currentBook;
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.0.0)<br/>
|
||||
* Checks if current position is valid.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.valid.php
|
||||
*
|
||||
* @return bool The return value will be casted to boolean and then evaluated.
|
||||
* Returns true on success or false on failure.
|
||||
*/
|
||||
public function valid()
|
||||
{
|
||||
return null !== $this->bookList->getBook($this->currentBook);
|
||||
}
|
||||
|
||||
/**
|
||||
* (PHP 5 >= 5.0.0)<br/>
|
||||
* Rewind the Iterator to the first element.
|
||||
*
|
||||
* @link http://php.net/manual/en/iterator.rewind.php
|
||||
*
|
||||
* @return void Any returned value is ignored.
|
||||
*/
|
||||
public function rewind()
|
||||
{
|
||||
$this->currentBook = $this->bookList->count() - 1;
|
||||
}
|
||||
}
|
@@ -4,8 +4,7 @@
|
||||
Purpose
|
||||
-------
|
||||
|
||||
To make an object iterable and to make it appear like a collection of
|
||||
objects.
|
||||
To make an object iterable and to make it appear like a collection of objects.
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -31,7 +30,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
Book.php
|
||||
|
||||
@@ -45,18 +44,6 @@ BookList.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
BookListIterator.php
|
||||
|
||||
.. literalinclude:: BookListIterator.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
BookListReverseIterator.php
|
||||
|
||||
.. literalinclude:: BookListReverseIterator.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
Test
|
||||
----
|
||||
|
||||
@@ -67,4 +54,4 @@ Tests/IteratorTest.php
|
||||
:linenos:
|
||||
|
||||
.. _`GitHub`: https://github.com/domnikl/DesignPatternsPHP/tree/master/Behavioral/Iterator
|
||||
.. __: http://en.wikipedia.org/wiki/Iterator_pattern
|
||||
.. __: http://en.wikipedia.org/wiki/Iterator_pattern
|
||||
|
@@ -6,69 +6,72 @@ use DesignPatterns\Behavioral\Iterator\Book;
|
||||
use DesignPatterns\Behavioral\Iterator\BookList;
|
||||
use DesignPatterns\Behavioral\Iterator\BookListIterator;
|
||||
use DesignPatterns\Behavioral\Iterator\BookListReverseIterator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class IteratorTest extends \PHPUnit_Framework_TestCase
|
||||
class IteratorTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var BookList
|
||||
*/
|
||||
protected $bookList;
|
||||
|
||||
protected function setUp()
|
||||
public function testCanIterateOverBookList()
|
||||
{
|
||||
$this->bookList = new BookList();
|
||||
$this->bookList->addBook(new Book('Learning PHP Design Patterns', 'William Sanders'));
|
||||
$this->bookList->addBook(new Book('Professional Php Design Patterns', 'Aaron Saray'));
|
||||
$this->bookList->addBook(new Book('Clean Code', 'Robert C. Martin'));
|
||||
}
|
||||
$bookList = new BookList();
|
||||
$bookList->addBook(new Book('Learning PHP Design Patterns', 'William Sanders'));
|
||||
$bookList->addBook(new Book('Professional Php Design Patterns', 'Aaron Saray'));
|
||||
$bookList->addBook(new Book('Clean Code', 'Robert C. Martin'));
|
||||
|
||||
public function expectedAuthors()
|
||||
{
|
||||
return array(
|
||||
array(
|
||||
array(
|
||||
'Learning PHP Design Patterns by William Sanders',
|
||||
'Professional Php Design Patterns by Aaron Saray',
|
||||
'Clean Code by Robert C. Martin',
|
||||
),
|
||||
),
|
||||
$books = [];
|
||||
|
||||
foreach ($bookList as $book) {
|
||||
$books[] = $book->getAuthorAndTitle();
|
||||
}
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
'Learning PHP Design Patterns by William Sanders',
|
||||
'Professional Php Design Patterns by Aaron Saray',
|
||||
'Clean Code by Robert C. Martin',
|
||||
],
|
||||
$books
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider expectedAuthors
|
||||
*/
|
||||
public function testUseAIteratorAndValidateAuthors($expected)
|
||||
public function testCanIterateOverBookListAfterRemovingBook()
|
||||
{
|
||||
$iterator = new BookListIterator($this->bookList);
|
||||
$book = new Book('Clean Code', 'Robert C. Martin');
|
||||
$book2 = new Book('Professional Php Design Patterns', 'Aaron Saray');
|
||||
|
||||
while ($iterator->valid()) {
|
||||
$expectedBook = array_shift($expected);
|
||||
$this->assertEquals($expectedBook, $iterator->current()->getAuthorAndTitle());
|
||||
$iterator->next();
|
||||
$bookList = new BookList();
|
||||
$bookList->addBook($book);
|
||||
$bookList->addBook($book2);
|
||||
$bookList->removeBook($book);
|
||||
|
||||
$books = [];
|
||||
foreach ($bookList as $book) {
|
||||
$books[] = $book->getAuthorAndTitle();
|
||||
}
|
||||
|
||||
$this->assertEquals(
|
||||
['Professional Php Design Patterns by Aaron Saray'],
|
||||
$books
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider expectedAuthors
|
||||
*/
|
||||
public function testUseAReverseIteratorAndValidateAuthors($expected)
|
||||
public function testCanAddBookToList()
|
||||
{
|
||||
$iterator = new BookListReverseIterator($this->bookList);
|
||||
$book = new Book('Clean Code', 'Robert C. Martin');
|
||||
|
||||
while ($iterator->valid()) {
|
||||
$expectedBook = array_pop($expected);
|
||||
$this->assertEquals($expectedBook, $iterator->current()->getAuthorAndTitle());
|
||||
$iterator->next();
|
||||
}
|
||||
$bookList = new BookList();
|
||||
$bookList->addBook($book);
|
||||
|
||||
$this->assertCount(1, $bookList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test BookList Remove.
|
||||
*/
|
||||
public function testBookRemove()
|
||||
public function testCanRemoveBookFromList()
|
||||
{
|
||||
$this->bookList->removeBook($this->bookList->getBook(0));
|
||||
$this->assertEquals($this->bookList->count(), 2);
|
||||
$book = new Book('Clean Code', 'Robert C. Martin');
|
||||
|
||||
$bookList = new BookList();
|
||||
$bookList->addBook($book);
|
||||
$bookList->removeBook($book);
|
||||
|
||||
$this->assertCount(0, $bookList);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
MediatorInterface.php
|
||||
|
||||
|
@@ -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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
@@ -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';
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
@@ -6,28 +6,16 @@ use DesignPatterns\Behavioral\Mediator\Mediator;
|
||||
use DesignPatterns\Behavioral\Mediator\Subsystem\Client;
|
||||
use DesignPatterns\Behavioral\Mediator\Subsystem\Database;
|
||||
use DesignPatterns\Behavioral\Mediator\Subsystem\Server;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* MediatorTest tests hello world.
|
||||
*/
|
||||
class MediatorTest extends \PHPUnit_Framework_TestCase
|
||||
class MediatorTest extends 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();
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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()
|
||||
{
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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.
|
||||
@@ -52,7 +52,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
Memento.php
|
||||
|
||||
@@ -60,15 +60,15 @@ Memento.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
Originator.php
|
||||
State.php
|
||||
|
||||
.. literalinclude:: Originator.php
|
||||
.. literalinclude:: State.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
Caretaker.php
|
||||
Ticket.php
|
||||
|
||||
.. literalinclude:: Caretaker.php
|
||||
.. literalinclude:: Ticket.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
@@ -82,4 +82,4 @@ Tests/MementoTest.php
|
||||
:linenos:
|
||||
|
||||
.. _`GitHub`: https://github.com/domnikl/DesignPatternsPHP/tree/master/Behavioral/Memento
|
||||
.. __: http://en.wikipedia.org/wiki/Memento_pattern
|
||||
.. __: http://en.wikipedia.org/wiki/Memento_pattern
|
||||
|
48
Behavioral/Memento/State.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -2,161 +2,31 @@
|
||||
|
||||
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;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* MementoTest tests the memento pattern.
|
||||
*/
|
||||
class MementoTest extends \PHPUnit_Framework_TestCase
|
||||
class MementoTest extends 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);
|
||||
// now 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());
|
||||
}
|
||||
}
|
||||
|
49
Behavioral/Memento/Ticket.php
Normal 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;
|
||||
}
|
||||
}
|
21
Behavioral/Memento/uml/Memento.uml
Normal 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>
|
||||
|
@@ -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>
|
||||
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 164 KiB |
@@ -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);
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -36,7 +36,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
Service.php
|
||||
|
||||
|
@@ -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...
|
||||
}
|
||||
}
|
||||
|
@@ -5,18 +5,14 @@ namespace DesignPatterns\Behavioral\NullObject\Tests;
|
||||
use DesignPatterns\Behavioral\NullObject\NullLogger;
|
||||
use DesignPatterns\Behavioral\NullObject\PrintLogger;
|
||||
use DesignPatterns\Behavioral\NullObject\Service;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* LoggerTest tests for different loggers.
|
||||
*/
|
||||
class LoggerTest extends \PHPUnit_Framework_TestCase
|
||||
class LoggerTest extends 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('');
|
||||
$service->doSomething();
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@ Purpose
|
||||
-------
|
||||
|
||||
To implement a publish/subscribe behaviour to an object, whenever a
|
||||
"Subject" object changes it's state, the attached "Observers" will be
|
||||
"Subject" object changes its state, the attached "Observers" will be
|
||||
notified. It is used to shorten the amount of coupled objects and uses
|
||||
loose coupling instead.
|
||||
|
||||
@@ -31,7 +31,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
User.php
|
||||
|
||||
@@ -55,4 +55,4 @@ Tests/ObserverTest.php
|
||||
:linenos:
|
||||
|
||||
.. _`GitHub`: https://github.com/domnikl/DesignPatternsPHP/tree/master/Behavioral/Observer
|
||||
.. __: http://en.wikipedia.org/wiki/Observer_pattern
|
||||
.. __: http://en.wikipedia.org/wiki/Observer_pattern
|
||||
|
@@ -4,66 +4,18 @@ namespace DesignPatterns\Behavioral\Observer\Tests;
|
||||
|
||||
use DesignPatterns\Behavioral\Observer\User;
|
||||
use DesignPatterns\Behavioral\Observer\UserObserver;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* ObserverTest tests the Observer pattern.
|
||||
*/
|
||||
class ObserverTest extends \PHPUnit_Framework_TestCase
|
||||
class ObserverTest extends 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());
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 108 KiB |
@@ -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);
|
||||
}
|
||||
}
|
33
Behavioral/Specification/AndSpecification.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace DesignPatterns\Behavioral\Specification;
|
||||
|
||||
class AndSpecification implements SpecificationInterface
|
||||
{
|
||||
/**
|
||||
* @var SpecificationInterface[]
|
||||
*/
|
||||
private $specifications;
|
||||
|
||||
/**
|
||||
* @param SpecificationInterface[] ...$specifications
|
||||
*/
|
||||
public function __construct(SpecificationInterface ...$specifications)
|
||||
{
|
||||
$this->specifications = $specifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* if at least one specification is false, return false, else return true.
|
||||
*/
|
||||
public function isSatisfiedBy(Item $item): bool
|
||||
{
|
||||
foreach ($this->specifications as $specification) {
|
||||
if (!$specification->isSatisfiedBy($item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
21
Behavioral/Specification/NotSpecification.php
Normal 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);
|
||||
}
|
||||
}
|
32
Behavioral/Specification/OrSpecification.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace DesignPatterns\Behavioral\Specification;
|
||||
|
||||
class OrSpecification implements SpecificationInterface
|
||||
{
|
||||
/**
|
||||
* @var SpecificationInterface[]
|
||||
*/
|
||||
private $specifications;
|
||||
|
||||
/**
|
||||
* @param SpecificationInterface[] ...$specifications
|
||||
*/
|
||||
public function __construct(SpecificationInterface ...$specifications)
|
||||
{
|
||||
$this->specifications = $specifications;
|
||||
}
|
||||
|
||||
/**
|
||||
* if at least one specification is true, return true, else return false
|
||||
*/
|
||||
public function isSatisfiedBy(Item $item): bool
|
||||
{
|
||||
foreach ($this->specifications as $specification) {
|
||||
if ($specification->isSatisfiedBy($item)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -24,7 +24,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
Item.php
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -3,101 +3,45 @@
|
||||
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;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* SpecificationTest tests the specification pattern.
|
||||
*/
|
||||
class SpecificationTest extends \PHPUnit_Framework_TestCase
|
||||
class SpecificationTest extends 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));
|
||||
$andSpec = 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($andSpec->isSatisfiedBy(new Item(150)));
|
||||
$this->assertFalse($andSpec->isSatisfiedBy(new Item(1)));
|
||||
$this->assertFalse($andSpec->isSatisfiedBy(new Item(51)));
|
||||
$this->assertTrue($andSpec->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);
|
||||
$notSpec = 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($notSpec->isSatisfiedBy(new Item(150)));
|
||||
$this->assertFalse($notSpec->isSatisfiedBy(new Item(50)));
|
||||
}
|
||||
}
|
||||
|
26
Behavioral/State/ContextOrder.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace DesignPatterns\Behavioral\State;
|
||||
|
||||
class ContextOrder extends StateOrder
|
||||
{
|
||||
public function getState():StateOrder
|
||||
{
|
||||
return static::$state;
|
||||
}
|
||||
|
||||
public function setState(StateOrder $state)
|
||||
{
|
||||
static::$state = $state;
|
||||
}
|
||||
|
||||
public function done()
|
||||
{
|
||||
static::$state->done();
|
||||
}
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return static::$state->getStatus();
|
||||
}
|
||||
}
|
@@ -2,49 +2,15 @@
|
||||
|
||||
namespace DesignPatterns\Behavioral\State;
|
||||
|
||||
/**
|
||||
* Class CreateOrder.
|
||||
*/
|
||||
class CreateOrder implements OrderInterface
|
||||
class CreateOrder extends StateOrder
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $order;
|
||||
|
||||
/**
|
||||
* @param array $order
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(array $order)
|
||||
public function __construct()
|
||||
{
|
||||
if (empty($order)) {
|
||||
throw new \Exception('Order can not be empty!');
|
||||
}
|
||||
$this->order = $order;
|
||||
$this->setStatus('created');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function shipOrder()
|
||||
protected function done()
|
||||
{
|
||||
$this->order['status'] = 'shipping';
|
||||
$this->order['updatedTime'] = time();
|
||||
|
||||
// Setting the new order status into database;
|
||||
return $this->updateOrder($this->order);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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!');
|
||||
static::$state = new ShippingOrder();
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace DesignPatterns\Behavioral\State;
|
||||
|
||||
/**
|
||||
* Class OrderInterface.
|
||||
*/
|
||||
interface OrderInterface
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function shipOrder();
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function completeOrder();
|
||||
}
|
@@ -18,23 +18,17 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
OrderController.php
|
||||
ContextOrder.php
|
||||
|
||||
.. literalinclude:: OrderController.php
|
||||
.. literalinclude:: ContextOrder.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
OrderFactory.php
|
||||
StateOrder.php
|
||||
|
||||
.. literalinclude:: OrderFactory.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
OrderInterface.php
|
||||
|
||||
.. literalinclude:: OrderInterface.php
|
||||
.. literalinclude:: StateOrder.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
|
@@ -2,49 +2,15 @@
|
||||
|
||||
namespace DesignPatterns\Behavioral\State;
|
||||
|
||||
/**
|
||||
* Class ShippingOrder.
|
||||
*/
|
||||
class ShippingOrder implements OrderInterface
|
||||
class ShippingOrder extends StateOrder
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $order;
|
||||
|
||||
/**
|
||||
* @param array $order
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __construct(array $order)
|
||||
public function __construct()
|
||||
{
|
||||
if (empty($order)) {
|
||||
throw new \Exception('Order can not be empty!');
|
||||
}
|
||||
$this->order = $order;
|
||||
$this->setStatus('shipping');
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*
|
||||
* @return mixed|void
|
||||
*/
|
||||
public function shipOrder()
|
||||
protected function done()
|
||||
{
|
||||
//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();
|
||||
|
||||
// Setting the new order status into database;
|
||||
return $this->updateOrder($this->order);
|
||||
$this->setStatus('completed');
|
||||
}
|
||||
}
|
||||
|
32
Behavioral/State/StateOrder.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace DesignPatterns\Behavioral\State;
|
||||
|
||||
abstract class StateOrder
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $details;
|
||||
|
||||
/**
|
||||
* @var StateOrder $state
|
||||
*/
|
||||
protected static $state;
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function done();
|
||||
|
||||
protected function setStatus(string $status)
|
||||
{
|
||||
$this->details['status'] = $status;
|
||||
$this->details['updatedTime'] = time();
|
||||
}
|
||||
|
||||
protected function getStatus(): string
|
||||
{
|
||||
return $this->details['status'];
|
||||
}
|
||||
}
|
31
Behavioral/State/Tests/StateTest.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace DesignPatterns\Behavioral\State\Tests;
|
||||
|
||||
use DesignPatterns\Behavioral\State\ContextOrder;
|
||||
use DesignPatterns\Behavioral\State\CreateOrder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class StateTest extends TestCase
|
||||
{
|
||||
public function testCanShipCreatedOrder()
|
||||
{
|
||||
$order = new CreateOrder();
|
||||
$contextOrder = new ContextOrder();
|
||||
$contextOrder->setState($order);
|
||||
$contextOrder->done();
|
||||
|
||||
$this->assertEquals('shipping', $contextOrder->getStatus());
|
||||
}
|
||||
|
||||
public function testCanCompleteShippedOrder()
|
||||
{
|
||||
$order = new CreateOrder();
|
||||
$contextOrder = new ContextOrder();
|
||||
$contextOrder->setState($order);
|
||||
$contextOrder->done();
|
||||
$contextOrder->done();
|
||||
|
||||
$this->assertEquals('completed', $contextOrder->getStatus());
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 124 KiB |
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
} else {
|
||||
return $aDate < $bDate ? -1 : 1;
|
||||
}
|
||||
return $aDate <=> $bDate;
|
||||
}
|
||||
}
|
||||
|
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -32,7 +32,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
ObjectCollection.php
|
||||
|
||||
|
@@ -5,43 +5,43 @@ 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;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests for Strategy pattern.
|
||||
*/
|
||||
class StrategyTest extends \PHPUnit_Framework_TestCase
|
||||
class StrategyTest extends 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 provideDates()
|
||||
{
|
||||
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 +54,10 @@ class StrategyTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getDateCollection
|
||||
* @dataProvider provideDates
|
||||
*
|
||||
* @param array $collection
|
||||
* @param array $expected
|
||||
*/
|
||||
public function testDateComparator($collection, $expected)
|
||||
{
|
||||
|
@@ -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";
|
||||
}
|
||||
}
|
||||
|
@@ -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";
|
||||
}
|
||||
}
|
||||
|
@@ -2,11 +2,13 @@
|
||||
|
||||
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.
|
||||
@@ -15,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 !
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
Journey.php
|
||||
|
||||
|
@@ -3,41 +3,35 @@
|
||||
namespace DesignPatterns\Behavioral\TemplateMethod\Tests;
|
||||
|
||||
use DesignPatterns\Behavioral\TemplateMethod;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* JourneyTest tests all journeys.
|
||||
*/
|
||||
class JourneyTest extends \PHPUnit_Framework_TestCase
|
||||
class JourneyTest extends 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 107 KiB |
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ UML Diagram
|
||||
Code
|
||||
----
|
||||
|
||||
You can also find these code on `GitHub`_
|
||||
You can also find this code on `GitHub`_
|
||||
|
||||
RoleVisitorInterface.php
|
||||
|
||||
@@ -31,9 +31,9 @@ RoleVisitorInterface.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
RolePrintVisitor.php
|
||||
RoleVisitor.php
|
||||
|
||||
.. literalinclude:: RolePrintVisitor.php
|
||||
.. literalinclude:: RoleVisitor.php
|
||||
:language: php
|
||||
:linenos:
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
29
Behavioral/Visitor/RoleVisitor.php
Normal 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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
@@ -3,43 +3,36 @@
|
||||
namespace DesignPatterns\Tests\Visitor\Tests;
|
||||
|
||||
use DesignPatterns\Behavioral\Visitor;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* VisitorTest tests the visitor pattern.
|
||||
*/
|
||||
class VisitorTest extends \PHPUnit_Framework_TestCase
|
||||
class VisitorTest extends 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]);
|
||||
}
|
||||
}
|
||||
|