Download project

This commit is contained in:
Roman Pyrih
2024-11-20 09:09:44 +01:00
parent 547a138d6a
commit 5ff041757f
40737 changed files with 7766183 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
{
"name": "league/tactician",
"description": "A small, flexible command bus. Handy for building service layers.",
"keywords": ["command", "command bus", "service layer"],
"license": "MIT",
"authors": [
{
"name": "Ross Tuck",
"homepage": "http://tactician.thephpleague.com"
}
],
"require": {
"php": ">=5.5"
},
"require-dev": {
"mockery/mockery": "~0.9",
"phpunit/phpunit": "^4.8.35",
"squizlabs/php_codesniffer": "~2.3"
},
"autoload": {
"psr-4": {
"League\\Tactician\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"League\\Tactician\\Tests\\": "tests/"
},
"files": [
"tests/Fixtures/Command/CommandWithoutNamespace.php"
]
},
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
}
}

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]);
}
}