Skip to content

Commit

Permalink
Merge pull request #10 from thephpleague/feature/middleware
Browse files Browse the repository at this point in the history
Command Middleware
  • Loading branch information
rosstuck committed Feb 4, 2015
2 parents 75be389 + cb236dc commit ebc51e1
Show file tree
Hide file tree
Showing 15 changed files with 248 additions and 131 deletions.
2 changes: 1 addition & 1 deletion examples/1-beginner-standard-usage.php
Expand Up @@ -25,7 +25,7 @@ public function handleRegisterUserCommand(RegisterUserCommand $command)
$locator = new InMemoryLocator();
$locator->addHandler(new RegisterUserHandler(), RegisterUserCommand::class);

$commandBus = new League\Tactician\HandlerCommandBus(
$commandBus = new League\Tactician\HandlerMiddleware(
$locator,
new HandleClassNameInflector()
);
Expand Down
4 changes: 2 additions & 2 deletions examples/3-intermediate-custom-naming-conventions.php
Expand Up @@ -37,9 +37,9 @@ public function handle($command)

// Now let's create our command bus again, but this time using our custom
// method naming strategy
use League\Tactician\HandlerCommandBus;
use League\Tactician\HandlerMiddleware;
$locator->addHandler(new NewRegisterUserHandler(), RegisterUserCommand::class);
$commandBus = new HandlerCommandBus($locator, new MyCustomInflector());
$commandBus = new HandlerMiddleware($locator, new MyCustomInflector());

// Controller Code time!
$command = new RegisterUserCommand();
Expand Down
4 changes: 2 additions & 2 deletions examples/4-advanced-custom-handler-loading.php
Expand Up @@ -37,10 +37,10 @@ public function getHandlerForCommand(Command $command)
->andReturn(new RegisterUserHandler());

// Now, we create our command bus using our container based loader instead
use League\Tactician\HandlerCommandBus;
use League\Tactician\HandlerMiddleware;
use League\Tactician\Handler\MethodNameInflector\HandleClassNameInflector;

$commandBus = new HandlerCommandBus(
$commandBus = new HandlerMiddleware(
new ContainerBasedHandlerLocator($fakeContainer),
new HandleClassNameInflector()
);
Expand Down
2 changes: 1 addition & 1 deletion examples/repeated-sample-code.php
Expand Up @@ -25,7 +25,7 @@ public function handleRegisterUserCommand(RegisterUserCommand $command)
$locator = new InMemoryLocator();
$locator->addHandler(new RegisterUserHandler(), RegisterUserCommand::class);

$commandBus = new League\Tactician\HandlerCommandBus(
$commandBus = new League\Tactician\HandlerMiddleware(
$locator,
new HandleClassNameInflector()
);
7 changes: 5 additions & 2 deletions src/CommandBus.php
@@ -1,9 +1,12 @@
<?php

namespace League\Tactician;

/**
* Receives a command and modifies or dispatches it to a handler in some way
* Receives a command and sends it through a chain of middleware for processing.
*
* This interface is only useful for mocks and legacy decorators to implement,
* you should always use StandardCommandBus and implement Middleware in your
* application, rather than decorate the command bus for additional behavior.
*/
interface CommandBus
{
Expand Down
@@ -1,15 +1,17 @@
<?php

namespace League\Tactician;
namespace League\Tactician\Handler;

use League\Tactician\Middleware;
use League\Tactician\Command;
use League\Tactician\Exception\CanNotInvokeHandlerException;
use League\Tactician\Handler\MethodNameInflector\MethodNameInflector;
use League\Tactician\Handler\Locator\HandlerLocator;

/**
* The "core" CommandBus. Locates the appropriate handler and executes command.
*/
class HandlerCommandBus implements CommandBus
class HandlerMiddleware implements Middleware
{
/**
* @var HandlerLocator
Expand All @@ -36,9 +38,10 @@ public function __construct(HandlerLocator $handlerLoader, MethodNameInflector $
*
* @throws CanNotInvokeHandlerException
* @param Command $command
* @param callable $next
* @return mixed
*/
public function execute(Command $command)
public function execute(Command $command, callable $next)
{
$handler = $this->handlerLocator->getHandlerForCommand($command);
$methodName = $this->methodNameInflector->inflect($command, $handler);
Expand Down
10 changes: 10 additions & 0 deletions src/Middleware.php
@@ -0,0 +1,10 @@
<?php
namespace League\Tactician;

/**
*
*/
interface Middleware
{
public function execute(Command $command, callable $next);
}
31 changes: 11 additions & 20 deletions src/LockingCommandBus.php → src/Plugins/LockingMiddleware.php
@@ -1,54 +1,45 @@
<?php

namespace League\Tactician;
namespace League\Tactician\Plugins;

use League\Tactician\Command;
use League\Tactician\Middleware;

/**
* If another command is already being executed, locks the command bus and
* queues the new incoming commands until the first has completed.
*/
class LockingCommandBus implements CommandBus
class LockingMiddleware implements Middleware
{
/**
* @var CommandBus
*/
private $innerCommandBus;

/**
* @var bool
*/
private $isExecuting;

/**
* @var object[]
* @var callable[]
*/
private $queue = [];

/**
* @param CommandBus $innerCommandBus
*/
public function __construct(CommandBus $innerCommandBus)
{
$this->innerCommandBus = $innerCommandBus;
}

/**
* Queues incoming commands until the first has completed
*
* @param Command $command
* @param callable $next
* @return mixed
*/
public function execute(Command $command)
public function execute(Command $command, callable $next)
{
$this->queue[] = $command;
$this->queue[] = $next;
if ($this->isExecuting) {
return;
}

$this->isExecuting = true;

$returnValues = [];
while ($command = array_shift($this->queue)) {
$returnValues[] = $this->innerCommandBus->execute($command);
while ($pendingNext = array_shift($this->queue)) {
$returnValues[] = $pendingNext($command);
}

$this->isExecuting = false;
Expand Down
11 changes: 7 additions & 4 deletions src/Setup/QuickStart.php
Expand Up @@ -2,10 +2,11 @@
namespace League\Tactician\Setup;

use League\Tactician\CommandBus;
use League\Tactician\StandardCommandBus;
use League\Tactician\Handler\Locator\InMemoryLocator;
use League\Tactician\Handler\MethodNameInflector\HandleInflector;
use League\Tactician\HandlerCommandBus;
use League\Tactician\LockingCommandBus;
use League\Tactician\Handler\HandlerMiddleware;
use League\Tactician\Plugins\LockingMiddleware;

/**
* Builds a working command bus with minimum fuss.
Expand All @@ -29,11 +30,13 @@ class QuickStart
*/
public static function create($commandToHandlerMap)
{
$handlerCommandBus = new HandlerCommandBus(
$handlerMiddleware = new HandlerMiddleware(
new InMemoryLocator($commandToHandlerMap),
new HandleInflector()
);

return new LockingCommandBus($handlerCommandBus);
$lockingMiddleware = new LockingMiddleware();

return new StandardCommandBus([$lockingMiddleware, $handlerMiddleware]);
}
}
55 changes: 55 additions & 0 deletions src/StandardCommandBus.php
@@ -0,0 +1,55 @@
<?php

namespace League\Tactician;

use League\Tactician\Exception\CommandWasNotHandledException;

/**
* Receives a command and modifies or dispatches it to a handler in some way
*/
class StandardCommandBus implements CommandBus
{
/**
* @var Closure
*/
private $middlewareChain;

/**
* @param Middleware[] $middleware
*/
public function __construct(array $middleware)
{
$this->middlewareChain = $this->createExecutionChain($middleware);
}

/**
* Executes the given command and optionally returns a value
*
* @param Command $command
* @return mixed
*/
public function execute(Command $command)
{
$middlewareChain = $this->middlewareChain;
return $middlewareChain($command);
}

/**
* @param Middleware[] $middlewareList
* @return \Closure
*/
protected function createExecutionChain($middlewareList)
{
$lastCallable = function (Command $command) {
// the final callable is a no-op
};

while ($middleware = array_pop($middlewareList)) {
$lastCallable = function (Command $command) use ($middleware, $lastCallable) {
return $middleware->execute($command, $lastCallable);
};
}

return $lastCallable;
}
}
@@ -1,8 +1,8 @@
<?php

namespace League\Tactician\Tests;
namespace League\Tactician\Tests\Handler;

use League\Tactician\HandlerCommandBus;
use League\Tactician\Handler\HandlerMiddleware;
use League\Tactician\Handler\MethodNameInflector\MethodNameInflector;
use League\Tactician\Handler\Locator\HandlerLocator;
use League\Tactician\Tests\Fixtures\Command\CompleteTaskCommand;
Expand All @@ -11,20 +11,20 @@
use stdClass;
use Mockery;

class HandlerCommandBusTest extends \PHPUnit_Framework_TestCase
class HandlerMiddlewareTest extends \PHPUnit_Framework_TestCase
{
/**
* @var HandlerCommandBus
* @var HandlerMiddleware
*/
private $commandBus;
private $middleware;

/**
* @var HandlerLocator|Mockery\MockInterface
*/
private $handlerLocator;

/**
* @var InvokingStrategy|Mockery\MockInterface
* @var MethodNameInflector|Mockery\MockInterface
*/
private $methodNameInflector;

Expand All @@ -33,7 +33,7 @@ protected function setUp()
$this->handlerLocator = Mockery::mock(HandlerLocator::class);
$this->methodNameInflector = Mockery::mock(MethodNameInflector::class);

$this->commandBus = new HandlerCommandBus(
$this->middleware = new HandlerMiddleware(
$this->handlerLocator,
$this->methodNameInflector
);
Expand All @@ -60,7 +60,7 @@ public function testHandlerIsExecuted()
->with($command)
->andReturn($handler);

$this->assertEquals('a-return-value', $this->commandBus->execute($command));
$this->assertEquals('a-return-value', $this->middleware->execute($command, $this->mockNext()));
}

/**
Expand All @@ -78,7 +78,7 @@ public function testMissingMethodOnHandlerObjectIsDetected()
->shouldReceive('getHandlerForCommand')
->andReturn(new stdClass);

$this->assertEquals('a-return-value', $this->commandBus->execute($command));
$this->assertEquals('a-return-value', $this->middleware->execute($command, $this->mockNext()));
}

public function testDynamicMethodNamesAreSupported()
Expand All @@ -94,7 +94,7 @@ public function testDynamicMethodNamesAreSupported()
->shouldReceive('getHandlerForCommand')
->andReturn($handler);

$this->commandBus->execute($command);
$this->middleware->execute($command, $this->mockNext());

$this->assertEquals(
['someHandlerMethod'],
Expand All @@ -118,8 +118,18 @@ public function testClosuresCanBeInvoked()
->shouldReceive('getHandlerForCommand')
->andReturn($handler);

$this->commandBus->execute($command);
$this->middleware->execute($command, $this->mockNext());

$this->assertTrue($closureWasExecuted);
}

/**
* @return callable
*/
protected function mockNext()
{
return function() {
throw new \LogicException('Middleware fell through to next callable, this should not happen in the test.');
};
}
}

0 comments on commit ebc51e1

Please sign in to comment.