Initial commit

This commit is contained in:
2020-10-07 10:37:15 +02:00
commit ce5f440392
28157 changed files with 4429172 additions and 0 deletions

View File

@@ -0,0 +1,43 @@
<?php
namespace League\Tactician\Bundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* This compiler pass maps Handler DI tags to specific commands
*/
class CommandHandlerPass implements CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
*
* @param ContainerBuilder $container
*
* @throws \Exception
* @api
*/
public function process(ContainerBuilder $container)
{
if (!$container->has('tactician.handler.locator.symfony')) {
throw new \Exception('Missing tactician.handler.locator.symfony definition');
}
$handlerLocator = $container->findDefinition('tactician.handler.locator.symfony');
$mapping = [];
foreach ($container->findTaggedServiceIds('tactician.handler') as $id => $tags) {
foreach ($tags as $attributes) {
if (!isset($attributes['command'])) {
throw new \Exception('The tactician.handler tag must always have a command attribute');
}
$mapping[$attributes['command']] = $id;
}
}
$handlerLocator->addArgument($mapping);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace League\Tactician\Bundle\DependencyInjection\Compiler;
use League\Tactician\Doctrine\ORM\TransactionMiddleware;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* This compiler pass registers doctrine entity manager middleware
*/
class DoctrineMiddlewarePass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!class_exists(TransactionMiddleware::class) || !$container->hasParameter('doctrine.entity_managers')) {
return;
}
$entityManagers = $container->getParameter('doctrine.entity_managers');
if (empty($entityManagers)) {
return;
}
foreach ($entityManagers as $name => $serviceId) {
$container->setDefinition(
sprintf('tactician.middleware.doctrine.%s', $name),
new Definition(TransactionMiddleware::class, [ new Reference($serviceId) ])
);
}
$defaultEntityManager = $container->getParameter('doctrine.default_entity_manager');
$container->setAlias('tactician.middleware.doctrine', sprintf('tactician.middleware.doctrine.%s', $defaultEntityManager));
}
}

View File

@@ -0,0 +1,91 @@
<?php namespace League\Tactician\Bundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
/**
* This is the class that validates and merges configuration from your app/config files
*
* To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class}
*/
class Configuration implements ConfigurationInterface
{
/**
* Create a rootnode tree for configuration that can be injected into the DI container
*
* @return TreeBuilder
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('tactician');
$rootNode
->children()
->arrayNode('commandbus')
->defaultValue(['default' => ['middleware' => ['tactician.middleware.command_handler']]])
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('array')
->children()
->arrayNode('middleware')
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('scalar')->end()
->validate()
->ifTrue(function ($config) {
$isPresent = in_array('tactician.middleware.command_handler', $config);
$isLast = end($config) == 'tactician.middleware.command_handler';
return ($isPresent && !$isLast);
})
->thenInvalid(
'"tactician.middleware.command_handler" should be last loaded middleware'.
' when it is use.'
)
->end()
->end()
->end()
->end()
->end()
->scalarNode('default_bus')
->defaultValue('default')
->cannotBeEmpty()
->end()
->scalarNode('method_inflector')
->defaultValue('tactician.handler.method_name_inflector.handle')
->cannotBeEmpty()
->end()
->end()
->validate()
->ifTrue(function($config) {
return is_array($config) &&
array_key_exists('default_bus', $config) &&
array_key_exists('commandbus', $config)
;
})
->then(function($config) {
$busNames = [];
foreach ($config['commandbus'] as $busName => $busConfig) {
$busNames[] = $busName;
}
if (!in_array($config['default_bus'], $busNames)) {
throw new InvalidConfigurationException(
sprintf(
'The default_bus "%s" was not defined as command bus. Valid option(s): %s',
$config['default_bus'],
implode(', ', $busNames)
)
);
}
return $config;
})
->end()
;
return $treeBuilder;
}
}

View File

@@ -0,0 +1,74 @@
<?php namespace League\Tactician\Bundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
class TacticianExtension extends ConfigurableExtension
{
/**
* Configures the passed container according to the merged configuration.
*
* @param array $mergedConfig
* @param ContainerBuilder $container
*/
protected function loadInternal(array $mergedConfig, ContainerBuilder $container)
{
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config/services'));
$loader->load('services.yml');
$this->configureCommandBuses($mergedConfig, $container);
$this->injectMethodNameInflector($mergedConfig, $container);
}
public function getAlias()
{
return 'tactician';
}
/**
* @param array $mergedConfig
* @param ContainerBuilder $container
*/
private function configureCommandBuses(array $mergedConfig, ContainerBuilder $container)
{
foreach ($mergedConfig['commandbus'] as $commandBusName => $commandBusConfig) {
$middlewares = array_map(
function ($middlewareServiceId) {
return new Reference($middlewareServiceId);
},
$commandBusConfig['middleware']
);
$serviceName = 'tactician.commandbus.' . $commandBusName;
$definition = new Definition($container->getParameter('tactician.commandbus.class'), [$middlewares]);
$container->setDefinition($serviceName, $definition);
if ($commandBusName === $mergedConfig['default_bus']) {
$container->setAlias('tactician.commandbus', $serviceName);
}
}
}
/**
* Define the default Method Name Inflector.
* This will fail silently if the command_handler service does not exist
*
* @param array $mergedConfig
* @param ContainerBuilder $container
*/
private function injectMethodNameInflector(array $mergedConfig, ContainerBuilder $container)
{
if (! $container->has('tactician.middleware.command_handler')) {
return;
}
$inflectorReference = new Reference($mergedConfig['method_inflector']);
$handlerLocator = $container->findDefinition('tactician.middleware.command_handler');
$handlerLocator->replaceArgument(2, $inflectorReference);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace League\Tactician\Bundle\Handler;
use League\Tactician\Exception\MissingHandlerException;
use League\Tactician\Handler\Locator\HandlerLocator;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Lazily loads Command Handlers from the Symfony2 DI container
*/
class ContainerBasedHandlerLocator implements HandlerLocator
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @var array
*/
private $commandToServiceId = [];
/**
* @param ContainerInterface $container
* @param $commandToServiceIdMapping
*/
public function __construct(ContainerInterface $container, $commandToServiceIdMapping)
{
$this->container = $container;
$this->commandToServiceId = $commandToServiceIdMapping;
}
/**
* Retrieves the handler for a specified command
*
* @param string $commandName
* @return mixed
*/
public function getHandlerForCommand($commandName)
{
if (!isset($this->commandToServiceId[$commandName])) {
throw MissingHandlerException::forCommand($commandName);
}
return $this->container->get($this->commandToServiceId[$commandName]);
}
}

19
vendor/league/tactician-bundle/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) 2014-2015 Ross Tuck
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,54 @@
<?php
namespace League\Tactician\Bundle\Middleware;
use League\Tactician\Exception\Exception;
use Symfony\Component\Validator\ConstraintViolationListInterface;
class InvalidCommandException extends \Exception implements Exception
{
/**
* @var object
*/
protected $command;
/**
* @var ConstraintViolationListInterface
*/
protected $violations;
/**
* @param object $command
* @param ConstraintViolationListInterface $violations
* @return static
*/
public static function onCommand($command, ConstraintViolationListInterface $violations)
{
$exception = new static(
'Validation failed for ' . get_class($command) .
' with ' . $violations->count() . ' violation(s).'
);
$exception->command = $command;
$exception->violations = $violations;
return $exception;
}
/**
* @return object
*/
public function getCommand()
{
return $this->command;
}
/**
* @return ConstraintViolationListInterface
*/
public function getViolations()
{
return $this->violations;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace League\Tactician\Bundle\Middleware;
use League\Tactician\Middleware;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class ValidatorMiddleware implements Middleware
{
/**
* @var ValidatorInterface
*/
protected $validator;
/**
* @param ValidatorInterface | null $validator
*/
public function __construct(ValidatorInterface $validator = null) {
$this->validator = $validator;
}
/**
* @param object $command
* @param callable $next
* @return mixed
* @throws InvalidCommandException
* @throws \Exception
*/
public function execute($command, callable $next)
{
if ($this->validator === null) {
throw new \Exception(
"The Validator Middleware requires the Validator service (@validator) to be present and configured." .
"Please configure it."
);
}
$constraintViolations = $this->validator->validate($command);
if (count($constraintViolations) > 0) {
throw InvalidCommandException::onCommand($command, $constraintViolations);
}
return $next($command);
}
}

View File

@@ -0,0 +1,44 @@
parameters:
tactician.commandbus.class: League\Tactician\CommandBus
services:
tactician.handler.locator.symfony:
class: League\Tactician\Bundle\Handler\ContainerBasedHandlerLocator
arguments:
- "@service_container"
# The standard middleware
tactician.middleware.command_handler:
class: League\Tactician\Handler\CommandHandlerMiddleware
arguments:
- "@tactician.handler.command_name_extractor.class_name"
- "@tactician.handler.locator.symfony"
- "@tactician.handler.method_name_inflector.handle"
tactician.middleware.locking:
class: League\Tactician\Plugins\LockingMiddleware
tactician.middleware.validator:
class: League\Tactician\Bundle\Middleware\ValidatorMiddleware
arguments:
- "@?validator"
# The standard Handler method name inflectors
tactician.handler.method_name_inflector.handle:
class: League\Tactician\Handler\MethodNameInflector\HandleInflector
tactician.handler.method_name_inflector.handle_class_name:
class: League\Tactician\Handler\MethodNameInflector\HandleClassNameInflector
tactician.handler.method_name_inflector.handle_class_name_without_suffix:
class: League\Tactician\Handler\MethodNameInflector\HandleClassNameWithoutSuffixInflector
tactician.handler.method_name_inflector.invoke:
class: League\Tactician\Handler\MethodNameInflector\InvokeInflector
# The CommandNameExtractors in Tactician core
tactician.handler.command_name_extractor.class_name:
class: League\Tactician\Handler\CommandNameExtractor\ClassNameExtractor
tactician.plugins.named_command.extractor:
class: League\Tactician\Plugins\NamedCommand\NamedCommandExtractor

View File

@@ -0,0 +1,22 @@
<?php namespace League\Tactician\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use League\Tactician\Bundle\DependencyInjection\Compiler\CommandHandlerPass;
use League\Tactician\Bundle\DependencyInjection\Compiler\DoctrineMiddlewarePass;
use League\Tactician\Bundle\DependencyInjection\TacticianExtension;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class TacticianBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new DoctrineMiddlewarePass());
$container->addCompilerPass(new CommandHandlerPass());
}
public function getContainerExtension()
{
return new TacticianExtension();
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace League\Tactician;
use League\Tactician\Exception\InvalidCommandException;
use League\Tactician\Exception\InvalidMiddlewareException;
/**
* Receives a command and sends it through a chain of middleware for processing.
*
* @final
*/
class CommandBus
{
/**
* @var callable
*/
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 object $command
*
* @return mixed
*/
public function handle($command)
{
if (!is_object($command)) {
throw InvalidCommandException::forUnknownValue($command);
}
$middlewareChain = $this->middlewareChain;
return $middlewareChain($command);
}
/**
* @param Middleware[] $middlewareList
*
* @return callable
*/
private function createExecutionChain($middlewareList)
{
$lastCallable = function () {
// the final callable is a no-op
};
while ($middleware = array_pop($middlewareList)) {
if (! $middleware instanceof Middleware) {
throw InvalidMiddlewareException::forMiddleware($middleware);
}
$lastCallable = function ($command) use ($middleware, $lastCallable) {
return $middleware->execute($command, $lastCallable);
};
}
return $lastCallable;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace League\Tactician\Exception;
/**
* Thrown when a CommandNameExtractor cannot determine the command's name
*/
class CanNotDetermineCommandNameException extends \RuntimeException implements Exception
{
/**
* @var mixed
*/
private $command;
/**
* @param mixed $command
*
* @return static
*/
public static function forCommand($command)
{
$type = is_object($command) ? get_class($command) : gettype($command);
$exception = new static('Could not determine command name of ' . $type);
$exception->command = $command;
return $exception;
}
/**
* Returns the command that could not be invoked
*
* @return mixed
*/
public function getCommand()
{
return $this->command;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace League\Tactician\Exception;
/**
* Thrown when a specific handler object can not be used on a command object.
*
* The most common reason is the receiving method is missing or incorrectly
* named.
*/
class CanNotInvokeHandlerException extends \BadMethodCallException implements Exception
{
/**
* @var mixed
*/
private $command;
/**
* @param mixed $command
* @param string $reason
*
* @return static
*/
public static function forCommand($command, $reason)
{
$type = is_object($command) ? get_class($command) : gettype($command);
$exception = new static(
'Could not invoke handler for command ' . $type .
' for reason: ' . $reason
);
$exception->command = $command;
return $exception;
}
/**
* Returns the command that could not be invoked
*
* @return mixed
*/
public function getCommand()
{
return $this->command;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace League\Tactician\Exception;
/**
* Marker interface for all Tactician exceptions
*/
interface Exception
{
}

View File

@@ -0,0 +1,37 @@
<?php
namespace League\Tactician\Exception;
/**
* Thrown when the command bus is given an non-object to use as a command.
*/
class InvalidCommandException extends \RuntimeException implements Exception
{
/**
* @var mixed
*/
private $invalidCommand;
/**
* @param mixed $invalidCommand
*
* @return static
*/
public static function forUnknownValue($invalidCommand)
{
$exception = new static(
'Commands must be an object but the value given was of type: ' . gettype($invalidCommand)
);
$exception->invalidCommand = $invalidCommand;
return $exception;
}
/**
* @return mixed
*/
public function getInvalidCommand()
{
return $this->invalidCommand;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace League\Tactician\Exception;
/**
* Thrown when the CommandBus was instantiated with an invalid middleware object
*/
class InvalidMiddlewareException extends \InvalidArgumentException implements Exception
{
public static function forMiddleware($middleware)
{
$name = is_object($middleware) ? get_class($middleware) : gettype($middleware);
$message = sprintf(
'Cannot add "%s" to middleware chain as it does not implement the Middleware interface.',
$name
);
return new static($message);
}
}

View File

@@ -0,0 +1,35 @@
<?php
namespace League\Tactician\Exception;
/**
* No handler could be found for the given command.
*/
class MissingHandlerException extends \OutOfBoundsException implements Exception
{
/**
* @var string
*/
private $commandName;
/**
* @param string $commandName
*
* @return static
*/
public static function forCommand($commandName)
{
$exception = new static('Missing handler for command ' . $commandName);
$exception->commandName = $commandName;
return $exception;
}
/**
* @return string
*/
public function getCommandName()
{
return $this->commandName;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace League\Tactician\Handler;
use League\Tactician\Middleware;
use League\Tactician\Exception\CanNotInvokeHandlerException;
use League\Tactician\Handler\CommandNameExtractor\CommandNameExtractor;
use League\Tactician\Handler\Locator\HandlerLocator;
use League\Tactician\Handler\MethodNameInflector\MethodNameInflector;
/**
* The "core" CommandBus. Locates the appropriate handler and executes command.
*/
class CommandHandlerMiddleware implements Middleware
{
/**
* @var CommandNameExtractor
*/
private $commandNameExtractor;
/**
* @var HandlerLocator
*/
private $handlerLocator;
/**
* @var MethodNameInflector
*/
private $methodNameInflector;
/**
* @param CommandNameExtractor $commandNameExtractor
* @param HandlerLocator $handlerLocator
* @param MethodNameInflector $methodNameInflector
*/
public function __construct(
CommandNameExtractor $commandNameExtractor,
HandlerLocator $handlerLocator,
MethodNameInflector $methodNameInflector
) {
$this->commandNameExtractor = $commandNameExtractor;
$this->handlerLocator = $handlerLocator;
$this->methodNameInflector = $methodNameInflector;
}
/**
* Executes a command and optionally returns a value
*
* @param object $command
* @param callable $next
*
* @return mixed
*
* @throws CanNotInvokeHandlerException
*/
public function execute($command, callable $next)
{
$commandName = $this->commandNameExtractor->extract($command);
$handler = $this->handlerLocator->getHandlerForCommand($commandName);
$methodName = $this->methodNameInflector->inflect($command, $handler);
// is_callable is used here instead of method_exists, as method_exists
// will fail when given a handler that relies on __call.
if (!is_callable([$handler, $methodName])) {
throw CanNotInvokeHandlerException::forCommand(
$command,
"Method '{$methodName}' does not exist on handler"
);
}
return $handler->{$methodName}($command);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace League\Tactician\Handler\CommandNameExtractor;
/**
* Extract the name from the class
*/
class ClassNameExtractor implements CommandNameExtractor
{
/**
* {@inheritdoc}
*/
public function extract($command)
{
return get_class($command);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace League\Tactician\Handler\CommandNameExtractor;
use League\Tactician\Exception\CanNotDetermineCommandNameException;
/**
* Extract the name from a command so that the name can be determined
* by the context better than simply the class name
*/
interface CommandNameExtractor
{
/**
* Extract the name from a command
*
* @param object $command
*
* @return string
*
* @throws CannotDetermineCommandNameException
*/
public function extract($command);
}

View File

@@ -0,0 +1,54 @@
<?php
namespace League\Tactician\Handler\Locator;
use League\Tactician\Exception\MissingHandlerException;
/**
* This locator loads Handlers from a provided callable.
*
* At first glance, this might seem fairly useless but it's actually very
* useful to encapsulate DI containers without having to write a custom adapter
* for each one.
*
* Let's say you have a Symfony container or similar that works via a 'get'
* method. You can pass in an array style callable such as:
*
* $locator = new CallableLocator([$container, 'get'])
*
* This is easy to set up and will now automatically pipe the command name
* straight through to the $container->get() method without having to write
* the custom locator.
*
* Naturally, you can also pass in closures for further behavior tweaks.
*/
class CallableLocator implements HandlerLocator
{
/**
* @var callable
*/
private $callable;
/**
* @param callable $callable
*/
public function __construct(callable $callable)
{
$this->callable = $callable;
}
/**
* {@inheritdoc}
*/
public function getHandlerForCommand($commandName)
{
$callable = $this->callable;
$handler = $callable($commandName);
// Odds are the callable threw an exception but it always pays to check
if ($handler === null) {
throw MissingHandlerException::forCommand($commandName);
}
return $handler;
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace League\Tactician\Handler\Locator;
use League\Tactician\Exception\MissingHandlerException;
/**
* Service locator for handler objects
*
* This interface is often a wrapper around your frameworks dependency
* injection container or just maps command names to handler names on disk somehow.
*/
interface HandlerLocator
{
/**
* Retrieves the handler for a specified command
*
* @param string $commandName
*
* @return object
*
* @throws MissingHandlerException
*/
public function getHandlerForCommand($commandName);
}

View File

@@ -0,0 +1,79 @@
<?php
namespace League\Tactician\Handler\Locator;
use League\Tactician\Exception\MissingHandlerException;
/**
* Fetch handler instances from an in-memory collection.
*
* This locator allows you to bind a handler object to receive commands of a
* certain class name. For example:
*
* // Wire everything together
* $myHandler = new TaskAddedHandler($dependency1, $dependency2);
* $inMemoryLocator->addHandler($myHandler, 'My\TaskAddedCommand');
*
* // Returns $myHandler
* $inMemoryLocator->getHandlerForCommand('My\TaskAddedCommand');
*/
class InMemoryLocator implements HandlerLocator
{
/**
* @var object[]
*/
protected $handlers = [];
/**
* @param array $commandClassToHandlerMap
*/
public function __construct(array $commandClassToHandlerMap = [])
{
$this->addHandlers($commandClassToHandlerMap);
}
/**
* Bind a handler instance to receive all commands with a certain class
*
* @param object $handler Handler to receive class
* @param string $commandClassName Command class e.g. "My\TaskAddedCommand"
*/
public function addHandler($handler, $commandClassName)
{
$this->handlers[$commandClassName] = $handler;
}
/**
* Allows you to add multiple handlers at once.
*
* The map should be an array in the format of:
* [
* AddTaskCommand::class => $someHandlerInstance,
* CompleteTaskCommand::class => $someHandlerInstance,
* ]
*
* @param array $commandClassToHandlerMap
*/
protected function addHandlers(array $commandClassToHandlerMap)
{
foreach ($commandClassToHandlerMap as $commandClass => $handler) {
$this->addHandler($handler, $commandClass);
}
}
/**
* Returns the handler bound to the command's class name.
*
* @param string $commandName
*
* @return object
*/
public function getHandlerForCommand($commandName)
{
if (!isset($this->handlers[$commandName])) {
throw MissingHandlerException::forCommand($commandName);
}
return $this->handlers[$commandName];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace League\Tactician\Handler\MethodNameInflector;
/**
* Assumes the method is only the last portion of the class name.
*
* Examples:
* - \MyGlobalCommand => $handler->myGlobalCommand()
* - \My\App\CreateUser => $handler->createUser()
*/
class ClassNameInflector implements MethodNameInflector
{
/**
* {@inheritdoc}
*/
public function inflect($command, $commandHandler)
{
$commandName = get_class($command);
// If class name has a namespace separator, only take last portion
if (strpos($commandName, '\\') !== false) {
$commandName = substr($commandName, strrpos($commandName, '\\') + 1);
}
return strtolower($commandName[0]) . substr($commandName, 1);
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace League\Tactician\Handler\MethodNameInflector;
/**
* Assumes the method is handle + the last portion of the class name.
*
* Examples:
* - \MyGlobalCommand => $handler->handleMyGlobalCommand()
* - \My\App\TaskCompletedCommand => $handler->handleTaskCompletedCommand()
*/
class HandleClassNameInflector extends ClassNameInflector
{
/**
* {@inheritdoc}
*/
public function inflect($command, $commandHandler)
{
$commandName = parent::inflect($command, $commandHandler);
return 'handle' . ucfirst($commandName);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace League\Tactician\Handler\MethodNameInflector;
/**
* Returns a method name that is handle + the last portion of the class name
* but also without a given suffix, typically "Command". This allows you to
* handle multiple commands on a single object but with slightly less annoying
* method names.
*
* The string removal is case sensitive.
*
* Examples:
* - \CompleteTaskCommand => $handler->handleCompleteTask()
* - \My\App\DoThingCommand => $handler->handleDoThing()
*/
class HandleClassNameWithoutSuffixInflector extends HandleClassNameInflector
{
/**
* @var string
*/
private $suffix;
/**
* @var int
*/
private $suffixLength;
/**
* @param string $suffix The string to remove from end of each class name
*/
public function __construct($suffix = 'Command')
{
$this->suffix = $suffix;
$this->suffixLength = strlen($suffix);
}
/**
* @param object $command
* @param object $commandHandler
* @return string
*/
public function inflect($command, $commandHandler)
{
$methodName = parent::inflect($command, $commandHandler);
if (substr($methodName, $this->suffixLength * -1) !== $this->suffix) {
return $methodName;
}
return substr($methodName, 0, strlen($methodName) - $this->suffixLength);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace League\Tactician\Handler\MethodNameInflector;
/**
* Handle command by calling the "handle" method.
*/
class HandleInflector implements MethodNameInflector
{
/**
* {@inheritdoc}
*/
public function inflect($command, $commandHandler)
{
return 'handle';
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace League\Tactician\Handler\MethodNameInflector;
/**
* Handle command by calling the __invoke magic method. Handy for single
* use classes or closures.
*/
class InvokeInflector implements MethodNameInflector
{
/**
* {@inheritdoc}
*/
public function inflect($command, $commandHandler)
{
return '__invoke';
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace League\Tactician\Handler\MethodNameInflector;
/**
* Deduce the method name to call on the command handler based on the command
* and handler instances.
*/
interface MethodNameInflector
{
/**
* Return the method name to call on the command handler and return it.
*
* @param object $command
* @param object $commandHandler
*
* @return string
*/
public function inflect($command, $commandHandler);
}

View File

@@ -0,0 +1,24 @@
<?php
namespace League\Tactician;
/**
* Middleware are the plugins of Tactician. They receive each command that's
* given to the CommandBus and can take any action they choose. Middleware can
* continue the Command processing by passing the command they receive to the
* $next callable, which is essentially the "next" Middleware in the chain.
*
* Depending on where they invoke the $next callable, Middleware can execute
* their custom logic before or after the Command is handled. They can also
* modify, log, or replace the command they receive. The sky's the limit.
*/
interface Middleware
{
/**
* @param object $command
* @param callable $next
*
* @return mixed
*/
public function execute($command, callable $next);
}

View File

@@ -0,0 +1,71 @@
<?php
namespace League\Tactician\Plugins;
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 LockingMiddleware implements Middleware
{
/**
* @var bool
*/
private $isExecuting;
/**
* @var callable[]
*/
private $queue = [];
/**
* Execute the given command... after other running commands are complete.
*
* @param object $command
* @param callable $next
*
* @throws \Exception
*
* @return mixed|void
*/
public function execute($command, callable $next)
{
$this->queue[] = function () use ($command, $next) {
return $next($command);
};
if ($this->isExecuting) {
return;
}
$this->isExecuting = true;
try {
$returnValue = $this->executeQueuedJobs();
} catch (\Exception $e) {
$this->isExecuting = false;
$this->queue = [];
throw $e;
}
$this->isExecuting = false;
return $returnValue;
}
/**
* Process any pending commands in the queue. If multiple, jobs are in the
* queue, only the first return value is given back.
*
* @return mixed
*/
protected function executeQueuedJobs()
{
$returnValues = [];
while ($resumeCommand = array_shift($this->queue)) {
$returnValues[] = $resumeCommand();
}
return array_shift($returnValues);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace League\Tactician\Plugins\NamedCommand;
/**
* Exposes a name for a command
*/
interface NamedCommand
{
/**
* Returns the name of the command
*
* @return string
*/
public function getCommandName();
}

View File

@@ -0,0 +1,24 @@
<?php
namespace League\Tactician\Plugins\NamedCommand;
use League\Tactician\Exception\CanNotDetermineCommandNameException;
use League\Tactician\Handler\CommandNameExtractor\CommandNameExtractor;
/**
* Extract the name from a NamedCommand
*/
class NamedCommandExtractor implements CommandNameExtractor
{
/**
* {@inheritdoc}
*/
public function extract($command)
{
if ($command instanceof NamedCommand) {
return $command->getCommandName();
}
throw CanNotDetermineCommandNameException::forCommand($command);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace League\Tactician\Setup;
use League\Tactician\CommandBus;
use League\Tactician\Handler\CommandNameExtractor\ClassNameExtractor;
use League\Tactician\Handler\Locator\InMemoryLocator;
use League\Tactician\Handler\MethodNameInflector\HandleInflector;
use League\Tactician\Handler\CommandHandlerMiddleware;
use League\Tactician\Plugins\LockingMiddleware;
/**
* Builds a working command bus with minimum fuss.
*
* Currently, the default setup is:
* - Handlers instances in memory
* - The expected handler method is always "handle"
* - And only one command at a time can be executed.
*
* This factory is a decent place to start trying out Tactician but you're
* better off moving to a custom setup or a framework bundle/module/provider in
* the long run. As you can see, it's not difficult. :)
*/
class QuickStart
{
/**
* Creates a default CommandBus that you can get started with.
*
* @param array $commandToHandlerMap
*
* @return CommandBus
*/
public static function create($commandToHandlerMap)
{
$handlerMiddleware = new CommandHandlerMiddleware(
new ClassNameExtractor(),
new InMemoryLocator($commandToHandlerMap),
new HandleInflector()
);
$lockingMiddleware = new LockingMiddleware();
return new CommandBus([$lockingMiddleware, $handlerMiddleware]);
}
}