diff --git a/docs/plugins.md b/docs/plugins.md index 4802c06..e051c47 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -51,6 +51,38 @@ $router->route('My.Event.OrderWasPayed')->to("delivery_processor"); $eventBus->utilize($router); ``` +## Prooph\ServiceBus\Router\RegexRouter + +The RegexRouter works with regular expressions to determine handlers for messages. It can be used together with a CommandBus and +an EventBus but behaves different for both. When routing a command the RegexRouter makes sure that only one pattern matches. +If more than one pattern matches it throws a `Prooph\ServiceBus\Exception\RuntimeException`. On the other hand when routing +an event each time a pattern matches the corresponding listener is added to the list of listeners. + +```php +//You can provide the pattern list as an associative array in the constructor ... +$router = new RegexRouter(array('/^My\.Command\.Buy.*/' => new BuyArticleHandler())); + +//... or using the programmatic api +$router->route('/^My\.Command\.Register.*/')->to(new RegisterUserHandler()); + +//Add the router to a CommandBus +$commandBus->utilize($router); + +//When routing an event you can provide a list of listeners for each pattern ... +$router = new RegexRouter(array('/^My\.Event\.Article.*/' => array(new OrderCartUpdater(), new InventoryUpdater()))); + +//... or using the programmatic api +$router->route('/^My\.Event\.Article.*/')->to(new OrderCartUpdater()); + +//The RegexRouter does not provide a andTo method like the EventRouter. +//You need to call route again for the same pattern, +//otherwise the router throws an exception +$router->route('/^My\.Event\.Article.*/')->to(new InventoryUpdater()); + +//Add the router to an EventBus +$eventBus->utilize($router); +``` + # Invoke Strategies An invoke strategy knows how a message handler can be invoked. You can register many invoke strategies at once depending on diff --git a/src/Prooph/ServiceBus/Process/EventDispatch.php b/src/Prooph/ServiceBus/Process/EventDispatch.php index bc246fb..5c11bb2 100644 --- a/src/Prooph/ServiceBus/Process/EventDispatch.php +++ b/src/Prooph/ServiceBus/Process/EventDispatch.php @@ -247,7 +247,7 @@ public function setException(\Exception $exception) } /** - * @return null|Exception + * @return null|\Exception */ public function getException() { diff --git a/src/Prooph/ServiceBus/Router/CommandRouter.php b/src/Prooph/ServiceBus/Router/CommandRouter.php index a375b2d..0d6e676 100644 --- a/src/Prooph/ServiceBus/Router/CommandRouter.php +++ b/src/Prooph/ServiceBus/Router/CommandRouter.php @@ -58,7 +58,7 @@ public function __construct(array $commandMap = null) */ public function attach(EventManagerInterface $events) { - $this->listeners[] = $events->attach(CommandDispatch::ROUTE, array($this, "onRouteEvent")); + $this->listeners[] = $events->attach(CommandDispatch::ROUTE, array($this, "onRouteCommand")); } /** @@ -111,7 +111,7 @@ public function to($commandHandler) /** * @param CommandDispatch $commandDispatch */ - public function onRouteEvent(CommandDispatch $commandDispatch) + public function onRouteCommand(CommandDispatch $commandDispatch) { if (is_null($commandDispatch->getCommandName())) { $commandDispatch->getLogger()->notice( diff --git a/src/Prooph/ServiceBus/Router/RegexRouter.php b/src/Prooph/ServiceBus/Router/RegexRouter.php new file mode 100644 index 0000000..2e448b0 --- /dev/null +++ b/src/Prooph/ServiceBus/Router/RegexRouter.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * Date: 30.10.14 - 22:29 + */ + +namespace Prooph\ServiceBus\Router; + +use Prooph\ServiceBus\Exception\RuntimeException; +use Prooph\ServiceBus\Process\CommandDispatch; +use Prooph\ServiceBus\Process\EventDispatch; +use Zend\EventManager\AbstractListenerAggregate; +use Zend\EventManager\EventManagerInterface; + +/** + * Class RegexRouter + * + * @package Prooph\ServiceBus\Router + * @author Alexander Miertsch + */ +class RegexRouter extends AbstractListenerAggregate +{ + const ALL = '/.*/'; + + /** + * @var array[array[pattern => handler], ...] + */ + protected $patternMap = array(); + + /** + * @var string + */ + protected $tmpPattern; + + /** + * @param null|array[pattern => handler|handler[]] $patternMap + */ + public function __construct(array $patternMap = null) + { + if (! is_null($patternMap)) { + foreach ($patternMap as $pattern => $handler) { + if (is_array($handler)) { + foreach($handler as $singleHandler) $this->route($pattern)->to($singleHandler); + } else { + $this->route($pattern)->to($handler); + } + + } + } + } + + /** + * implementation will pass this to the aggregate. + * + * @param EventManagerInterface $events + * + * @return void + */ + public function attach(EventManagerInterface $events) + { + $identifiers = $events->getIdentifiers(); + + if (in_array('command_bus', $identifiers)) { + $this->listeners[] = $events->attach(CommandDispatch::ROUTE, array($this, 'onRouteCommand'), 100); + } + + if (in_array('event_bus', $identifiers)) { + $this->listeners[] = $events->attach(EventDispatch::ROUTE, array($this, 'onRouteEvent'), 100); + } + } + + /** + * @param string $pattern + * @return $this + * @throws \Prooph\ServiceBus\Exception\RuntimeException + */ + public function route($pattern) + { + \Assert\that($pattern)->notEmpty()->string(); + + if (! is_null($this->tmpPattern)) { + throw new RuntimeException(sprintf("pattern %s is not mapped to a handler.", $this->tmpPattern)); + } + + $this->tmpPattern = $pattern; + + return $this; + } + + /** + * @param string|object|callable $handler + * @return $this + * @throws \Prooph\ServiceBus\Exception\RuntimeException + * @throws \InvalidArgumentException + */ + public function to($handler) + { + if (! is_string($handler) && ! is_object($handler) && ! is_callable($handler)) { + throw new \InvalidArgumentException(sprintf( + "Invalid handler provided. Expected type is string, object or callable but type of %s given.", + gettype($handler) + )); + } + + if (is_null($this->tmpPattern)) { + throw new RuntimeException(sprintf( + "Cannot map handler %s to a pattern. Please use method route before calling method to", + (is_object($handler))? get_class($handler) : (is_string($handler))? $handler : gettype($handler) + )); + } + + $this->patternMap[] = [$this->tmpPattern => $handler]; + + $this->tmpPattern = null; + + return $this; + } + + /** + * @param CommandDispatch $commandDispatch + * @throws \Prooph\ServiceBus\Exception\RuntimeException + */ + public function onRouteCommand(CommandDispatch $commandDispatch) + { + if (is_null($commandDispatch->getCommandName())) { + $commandDispatch->getLogger()->notice( + sprintf("%s: CommandDispatch contains no command name", get_called_class()) + ); + return; + } + + $alreadyMatched = false; + + foreach($this->patternMap as $map) { + list($pattern, $handler) = each($map); + if (preg_match($pattern, $commandDispatch->getCommandName())) { + + if ($alreadyMatched) { + throw new RuntimeException(sprintf( + "Multiple handlers detected for command %s. The patterns %s and %s matches both", + $commandDispatch->getCommandName(), + $alreadyMatched, + $pattern + )); + } else { + $commandDispatch->setCommandHandler($handler); + $alreadyMatched = $pattern; + } + } + } + } + + /** + * @param EventDispatch $eventDispatch + */ + public function onRouteEvent(EventDispatch $eventDispatch) + { + if (is_null($eventDispatch->getEventName())) { + $eventDispatch->getLogger()->notice( + sprintf("%s: EventDispatch contains no event name", get_called_class()) + ); + return; + } + + foreach($this->patternMap as $map) { + list($pattern, $handler) = each($map); + if (preg_match($pattern, $eventDispatch->getEventName())) { + $eventDispatch->addEventListener($handler); + } + } + } +} + \ No newline at end of file diff --git a/tests/Prooph/ServiceBusTest/Router/CommandRouterTest.php b/tests/Prooph/ServiceBusTest/Router/CommandRouterTest.php index b21e469..334f699 100644 --- a/tests/Prooph/ServiceBusTest/Router/CommandRouterTest.php +++ b/tests/Prooph/ServiceBusTest/Router/CommandRouterTest.php @@ -37,7 +37,7 @@ public function it_can_handle_routing_definition_by_chaining_route_to() $commandDispatch->setName(CommandDispatch::ROUTE); - $router->onRouteEvent($commandDispatch); + $router->onRouteCommand($commandDispatch); $this->assertEquals("DoSomethingHandler", $commandDispatch->getCommandHandler()); } @@ -81,7 +81,7 @@ public function it_takes_a_routing_definition_on_instantiation() $commandDispatch->setName(CommandDispatch::ROUTE); - $router->onRouteEvent($commandDispatch); + $router->onRouteCommand($commandDispatch); $this->assertEquals("DoSomethingHandler", $commandDispatch->getCommandHandler()); } diff --git a/tests/Prooph/ServiceBusTest/Router/RegexRouterTest.php b/tests/Prooph/ServiceBusTest/Router/RegexRouterTest.php new file mode 100644 index 0000000..58618da --- /dev/null +++ b/tests/Prooph/ServiceBusTest/Router/RegexRouterTest.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + * + * Date: 30.10.14 - 22:47 + */ + +namespace Prooph\ServiceBusTest\Router; + +use Prooph\ServiceBus\CommandBus; +use Prooph\ServiceBus\EventBus; +use Prooph\ServiceBus\Process\CommandDispatch; +use Prooph\ServiceBus\Process\EventDispatch; +use Prooph\ServiceBus\Router\RegexRouter; +use Prooph\ServiceBusTest\Mock\DoSomething; +use Prooph\ServiceBusTest\Mock\SomethingDone; +use Prooph\ServiceBusTest\TestCase; + +/** + * Class RegexRouterTest + * + * @package Prooph\ServiceBusTest\Router + * @author Alexander Miertsch + */ +class RegexRouterTest extends TestCase +{ + /** + * @test + */ + public function it_matches_pattern_with_command_name_to_detect_appropriate_handler() + { + $regexRouter = new RegexRouter(); + + $regexRouter->route('/^'.preg_quote('Prooph\ServiceBusTest\Mock\Do').'.*/')->to("DoSomethingHandler"); + + $commandDispatch = CommandDispatch::initializeWith(DoSomething::getNew(), new CommandBus()); + + $commandDispatch->setName(CommandDispatch::ROUTE); + + $regexRouter->onRouteCommand($commandDispatch); + + $this->assertEquals("DoSomethingHandler", $commandDispatch->getCommandHandler()); + } + + /** + * @test + */ + public function it_does_not_allow_that_two_pattern_matches_with_same_command_name() + { + $regexRouter = new RegexRouter(); + + $regexRouter->route('/^'.preg_quote('Prooph\ServiceBusTest\Mock\Do').'.*/')->to("DoSomethingHandler"); + $regexRouter->route('/^'.preg_quote('Prooph\ServiceBusTest\Mock\\').'.*/')->to("DoSomethingHandler2"); + + $this->setExpectedException('\Prooph\ServiceBus\Exception\RuntimeException'); + + $commandDispatch = CommandDispatch::initializeWith(DoSomething::getNew(), new CommandBus()); + + $commandDispatch->setName(CommandDispatch::ROUTE); + + $regexRouter->onRouteCommand($commandDispatch); + } + + /** + * @test + */ + public function it_matches_pattern_with_event_name_and_routes_to_multiple_listeners() + { + $regexRouter = new RegexRouter(); + + $regexRouter->route('/^'.preg_quote('Prooph\ServiceBusTest\Mock\\').'.*Done$/')->to("SomethingDoneListener1"); + $regexRouter->route('/^'.preg_quote('Prooph\ServiceBusTest\Mock\\').'.*Done$/')->to("SomethingDoneListener2"); + + $eventDispatch = EventDispatch::initializeWith(SomethingDone::getNew(), new EventBus()); + + $eventDispatch->setName(EventDispatch::ROUTE); + + $regexRouter->onRouteEvent($eventDispatch); + + $this->assertEquals(["SomethingDoneListener1", "SomethingDoneListener2"], $eventDispatch->getEventListeners()->getArrayCopy()); + } + + /** + * @test + */ + public function it_fails_on_routing_a_second_pattern_before_first_definition_is_finished() + { + $router = new RegexRouter(); + + $router->route('Prooph\ServiceBusTest\Mock\DoSomething'); + + $this->setExpectedException('\Prooph\ServiceBus\Exception\RuntimeException'); + + $router->route('/.*/'); + } + + /** + * @test + */ + public function it_fails_on_setting_a_handler_before_a_pattern_is_set() + { + $router = new RegexRouter(); + + $this->setExpectedException('\Prooph\ServiceBus\Exception\RuntimeException'); + + $router->to('DoSomethingHandler'); + } + + /** + * @test + */ + public function it_takes_a_routing_definition_on_instantiation() + { + $router = new RegexRouter(array( + '/^'.preg_quote('Prooph\ServiceBusTest\Mock\Do').'.*/' => 'DoSomethingHandler', + '/^'.preg_quote('Prooph\ServiceBusTest\Mock\\').'.*Done$/' => ["SomethingDoneListener1", "SomethingDoneListener2"] + + )); + + $commandDispatch = CommandDispatch::initializeWith(DoSomething::getNew(), new CommandBus()); + + $commandDispatch->setName(CommandDispatch::ROUTE); + + $router->onRouteCommand($commandDispatch); + + $this->assertEquals("DoSomethingHandler", $commandDispatch->getCommandHandler()); + + $eventDispatch = EventDispatch::initializeWith(SomethingDone::getNew(), new EventBus()); + + $eventDispatch->setName(EventDispatch::ROUTE); + + $router->onRouteEvent($eventDispatch); + + $this->assertEquals(["SomethingDoneListener1", "SomethingDoneListener2"], $eventDispatch->getEventListeners()->getArrayCopy()); + } +} + \ No newline at end of file