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,58 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class DebugCommand extends Command
{
/**
* @var array
*/
private $mappings;
public function __construct(array $mappings)
{
parent::__construct();
$this->mappings = $mappings;
}
protected function configure()
{
$this->setName('debug:tactician');
}
public function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->title('Tactician routing');
$headers = ['Command', 'Handler Service'];
foreach ($this->mappings as $busId => $map) {
$io->section('Bus: ' . $busId);
if (count($map) > 0) {
$io->table($headers, $this->mappingToRows($map));
} else {
$io->warning("No registered commands for bus $busId");
}
}
}
private function mappingToRows(array $map)
{
$rows = [];
foreach ($map as $commandName => $handlerService) {
$rows[] = [$commandName, $handlerService];
}
return $rows;
}
}

View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\Compiler\BusBuilder;
use League\Tactician\Bundle\Handler\ContainerBasedHandlerLocator;
use League\Tactician\CommandBus;
use League\Tactician\Container\ContainerLocator;
use League\Tactician\Handler\CommandHandlerMiddleware;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
final class BusBuilder
{
/**
* @var string
*/
private $busId;
/**
* @var string[]
*/
private $middlewareIds = [];
/**
* @var string
*/
private $methodInflectorId;
public function __construct(string $busId, string $methodInflector, array $middlewareIds)
{
$this->busId = $busId;
$this->methodInflectorId = $methodInflector;
$this->middlewareIds = $middlewareIds;
}
public function id(): string
{
return $this->busId;
}
public function serviceId(): string
{
return "tactician.commandbus.$this->busId";
}
public function locatorServiceId()
{
return "tactician.commandbus.{$this->busId}.handler.locator";
}
public function commandHandlerMiddlewareId(): string
{
return "tactician.commandbus.{$this->busId}.middleware.command_handler";
}
public function registerInContainer(ContainerBuilder $container, array $commandsToAccept)
{
$this->registerLocatorService($container, $commandsToAccept);
$container->setDefinition(
$this->commandHandlerMiddlewareId(),
new Definition(
CommandHandlerMiddleware::class,
[
new Reference('tactician.handler.command_name_extractor.class_name'),
new Reference($this->locatorServiceId()),
new Reference($this->methodInflectorId),
]
)
);
$container->setDefinition(
$this->serviceId(),
new Definition(
CommandBus::class,
[
array_map(
function (string $id) { return new Reference($id); },
$this->middlewareIds
)
]
)
)->setPublic(true);
if (method_exists($container, 'registerAliasForArgument')) {
$container->registerAliasForArgument($this->serviceId(), CommandBus::class, "{$this->busId}Bus");
}
}
private function registerLocatorService(ContainerBuilder $container, $commandsToAccept)
{
// Leverage symfony/dependency-injection:^3.3 service locators
if (class_exists(ServiceLocator::class)) {
$definition = new Definition(
ContainerLocator::class,
[new Reference($this->registerHandlerServiceLocator($container, $commandsToAccept)), $commandsToAccept]
);
} else {
$definition = new Definition(
ContainerBasedHandlerLocator::class,
[new Reference('service_container'), $commandsToAccept]
);
}
$container->setDefinition($this->locatorServiceId(), $definition);
}
private function registerHandlerServiceLocator(ContainerBuilder $container, array $commandsToAccept): string
{
$handlers = [];
foreach ($commandsToAccept as $commandName => $handlerId) {
$handlers[$handlerId] = new ServiceClosureArgument(new Reference($handlerId));
}
$handlerServiceLocator = (new Definition(ServiceLocator::class, [$handlers]))
->setPublic(false)
->addTag('container.service_locator');
$container->setDefinition(
$handlerId = "tactician.commandbus.{$this->busId}.handler.service_locator",
$handlerServiceLocator
);
return $handlerId;
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\Compiler\BusBuilder;
use League\Tactician\Bundle\DependencyInjection\DuplicatedCommandBusId;
use League\Tactician\Bundle\DependencyInjection\HandlerMapping\Routing;
use League\Tactician\Bundle\DependencyInjection\InvalidCommandBusId;
use ArrayIterator;
final class BusBuilders implements \IteratorAggregate
{
/**
* @var BusBuilder[]
*/
private $busBuilders = [];
/**
* @var string
*/
private $defaultBusId;
public function __construct(array $busBuilders, string $defaultBusId)
{
foreach ($busBuilders as $builder) {
$this->add($builder);
}
$this->assertValidBusId($defaultBusId);
$this->defaultBusId = $defaultBusId;
}
public function createBlankRouting(): Routing
{
return new Routing(array_keys($this->busBuilders));
}
public function defaultBus(): BusBuilder
{
return $this->get($this->defaultBusId);
}
private function get(string $busId): BusBuilder
{
$this->assertValidBusId($busId);
return $this->busBuilders[$busId];
}
/**
* @return ArrayIterator|BusBuilder[]
*/
public function getIterator()
{
return new ArrayIterator($this->busBuilders);
}
private function assertValidBusId($busId)
{
if (!isset($this->busBuilders[$busId])) {
throw InvalidCommandBusId::ofName($busId, array_keys($this->busBuilders));
}
}
private function add(BusBuilder $builder)
{
$id = $builder->id();
if (isset($this->busBuilders[$id])) {
throw DuplicatedCommandBusId::withId($id);
}
$this->busBuilders[$id] = $builder;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\Compiler\BusBuilder;
final class BusBuildersFromConfig
{
const DEFAULT_METHOD_INFLECTOR = 'tactician.handler.method_name_inflector.handle';
const DEFAULT_BUS_ID = 'default';
public static function convert(array $config): BusBuilders
{
$defaultInflector = $config['method_inflector'] ?? self::DEFAULT_METHOD_INFLECTOR;
$builders = [];
foreach ($config['commandbus'] ?? [] as $busId => $busConfig) {
$builders[] = new BusBuilder(
$busId,
$busConfig['method_inflector'] ?? $defaultInflector,
$busConfig['middleware']
);
}
return new BusBuilders($builders, $config['default_bus'] ?? self::DEFAULT_BUS_ID);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace League\Tactician\Bundle\DependencyInjection\Compiler;
use League\Tactician\Bundle\DependencyInjection\Compiler\BusBuilder\BusBuildersFromConfig;
use League\Tactician\Bundle\DependencyInjection\HandlerMapping\HandlerMapping;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use League\Tactician\CommandBus;
/**
* This compiler pass maps Handler DI tags to specific commands.
*/
class CommandHandlerPass implements CompilerPassInterface
{
/**
* @var HandlerMapping
*/
private $handlerMapping;
public function __construct(HandlerMapping $mappingStrategy)
{
$this->handlerMapping = $mappingStrategy;
}
public function process(ContainerBuilder $container)
{
$builders = BusBuildersFromConfig::convert(
$this->readAndForgetParameter($container, 'tactician.merged_config')
);
$routing = $this->handlerMapping->build($container, $builders->createBlankRouting());
$mappings = [];
// Register the completed builders in our container
foreach ($builders as $builder) {
$commandToServiceMapping = $routing->commandToServiceMapping($builder->id());
$mappings[$builder->id()] = $commandToServiceMapping;
$builder->registerInContainer($container, $commandToServiceMapping);
}
// Setup default aliases
$container->setAlias('tactician.commandbus', $builders->defaultBus()->serviceId());
$container->setAlias(CommandBus::class, 'tactician.commandbus');
$container->setAlias('tactician.handler.locator.symfony', $builders->defaultBus()->locatorServiceId());
$container->setAlias('tactician.middleware.command_handler', $builders->defaultBus()->commandHandlerMiddlewareId());
// Wire debug command
if ($container->hasDefinition('tactician.command.debug')) {
$container->getDefinition('tactician.command.debug')->addArgument($mappings);
}
}
private function readAndForgetParameter(ContainerBuilder $container, $parameter)
{
$value = $container->getParameter($parameter);
$container->getParameterBag()->remove($parameter);
return $value;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace League\Tactician\Bundle\DependencyInjection\Compiler;
use League\Tactician\Doctrine\ORM\RollbackOnlyTransactionMiddleware;
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) ])
);
$container->setDefinition(
sprintf('tactician.middleware.doctrine_rollback_only.%s', $name),
new Definition(RollbackOnlyTransactionMiddleware::class, [ new Reference($serviceId) ])
);
}
$defaultEntityManager = $container->getParameter('doctrine.default_entity_manager');
$container->setAlias('tactician.middleware.doctrine', sprintf('tactician.middleware.doctrine.%s', $defaultEntityManager));
$container->setAlias('tactician.middleware.doctrine_rollback_only', sprintf('tactician.middleware.doctrine_rollback_only.%s', $defaultEntityManager));
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace League\Tactician\Bundle\DependencyInjection\Compiler;
use League\Tactician\Bundle\Middleware\SecurityMiddleware;
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 security middleware if possible
*/
class SecurityMiddlewarePass implements CompilerPassInterface
{
const SERVICE_ID = 'tactician.middleware.security';
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition('security.authorization_checker')) {
return;
}
$container->setDefinition(
static::SERVICE_ID,
new Definition(SecurityMiddleware::class, [ new Reference('security.authorization_checker') ])
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace League\Tactician\Bundle\DependencyInjection\Compiler;
use League\Tactician\Bundle\Middleware\ValidatorMiddleware;
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 validator middleware if possible
*/
class ValidatorMiddlewarePass implements CompilerPassInterface
{
const SERVICE_ID = 'tactician.middleware.validator';
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition('validator')) {
return;
}
$container->setDefinition(
static::SERVICE_ID,
new Definition(ValidatorMiddleware::class, [ new Reference('validator') ])
);
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace League\Tactician\Bundle\DependencyInjection;
use League\Tactician\Bundle\DependencyInjection\Compiler\BusBuilder\BusBuildersFromConfig;
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('tactician');
if (\method_exists($treeBuilder, 'getRootNode')) {
$rootNode = $treeBuilder->getRootNode();
} else {
// BC layer for symfony/config 4.1 and older
$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 the last middleware loaded '.
'when it is used.'
)
->end()
->end()
->scalarNode('method_inflector')->end()
->end()
->end()
->end()
->scalarNode('default_bus')
->defaultValue(BusBuildersFromConfig::DEFAULT_BUS_ID)
->cannotBeEmpty()
->end()
->scalarNode('method_inflector')
->defaultValue(BusBuildersFromConfig::DEFAULT_METHOD_INFLECTOR)
->cannotBeEmpty()
->end()
->arrayNode('security')
->defaultValue([])
->useAttributeAsKey('name')
->prototype('array')
->prototype('scalar')->end()
->end()
->end()
->scalarNode('logger_formatter')
->defaultValue('tactician.logger.class_properties_formatter')
->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 a command bus. Valid option(s): %s',
$config['default_bus'],
implode(', ', $busNames)
)
);
}
return $config;
})
->end()
;
return $treeBuilder;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection;
final class DuplicatedCommandBusId extends \Exception
{
public static function withId(string $id)
{
return new static("There are multiple command buses with the id '$id'. All bus ids must be unique.");
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\HandlerMapping;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
final class ClassNameMapping extends TagBasedMapping
{
protected function isSupported(ContainerBuilder $container, Definition $definition, array $tagAttributes): bool
{
return isset($tagAttributes['command']) && class_exists($container->getParameterBag()->resolveValue($tagAttributes['command']));
}
protected function findCommandsForService(ContainerBuilder $container, Definition $definition, array $tagAttributes): array
{
return [
$container->getParameterBag()->resolveValue($tagAttributes['command'])
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\HandlerMapping;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class CompositeMapping implements HandlerMapping
{
/**
* @var HandlerMapping[]
*/
private $strategies;
public function __construct(HandlerMapping ...$strategies)
{
$this->strategies = $strategies;
}
public function build(ContainerBuilder $container, Routing $routing): Routing
{
foreach ($this->strategies as $strategy) {
$routing = $strategy->build($container, $routing);
}
return $routing;
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\HandlerMapping;
use Symfony\Component\DependencyInjection\ContainerBuilder;
interface HandlerMapping
{
public function build(ContainerBuilder $container, Routing $routing): Routing;
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\HandlerMapping;
use League\Tactician\Bundle\DependencyInjection\InvalidCommandBusId;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
final class Routing
{
/**
* [
* 'busId_1' => [
* 'My\Command\Name1' => 'some.service.id',
* 'My\Other\Command' => 'some.service.id.or.same.one'
* ],
* 'busId_2' => [
* 'Legacy\App\Command1' => 'some.old.handler',
* ...
* ],
* ]
*
* @var array
*/
private $mapping = [];
public function __construct(array $validBusIds)
{
foreach ($validBusIds as $validBusId) {
$this->mapping[$validBusId] = [];
}
}
public function routeToBus($busId, $commandClassName, $serviceId)
{
$this->assertValidBusId($busId);
$this->assertValidCommandFQCN($commandClassName, $serviceId);
$this->mapping[$busId][$commandClassName] = $serviceId;
}
public function routeToAllBuses($commandClassName, $serviceId)
{
$this->assertValidCommandFQCN($commandClassName, $serviceId);
foreach($this->mapping as $busId => $mapping) {
$this->mapping[$busId][$commandClassName] = $serviceId;
}
}
public function commandToServiceMapping(string $busId): array
{
$this->assertValidBusId($busId);
return $this->mapping[$busId];
}
private function assertValidBusId(string $busId)
{
if (!isset($this->mapping[$busId])) {
throw InvalidCommandBusId::ofName($busId, array_keys($this->mapping));
}
}
/**
* @param $commandClassName
* @param $serviceId
*/
protected function assertValidCommandFQCN($commandClassName, $serviceId)
{
if (!class_exists($commandClassName)) {
throw new InvalidArgumentException("Can not route $commandClassName to $serviceId, class $commandClassName does not exist!");
}
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\HandlerMapping;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
abstract class TagBasedMapping implements HandlerMapping
{
const TAG_NAME = 'tactician.handler';
public function build(ContainerBuilder $container, Routing $routing): Routing
{
foreach ($container->findTaggedServiceIds(self::TAG_NAME) as $serviceId => $tags) {
foreach ($tags as $attributes) {
$this->mapServiceByTag($container, $routing, $serviceId, $attributes);
}
}
return $routing;
}
/**
* @param ContainerBuilder $container
* @param Routing $routing
* @param $serviceId
* @param $attributes
*/
private function mapServiceByTag(ContainerBuilder $container, Routing $routing, $serviceId, $attributes)
{
$definition = $container->getDefinition($serviceId);
if (!$this->isSupported($container, $definition, $attributes)) {
return;
}
foreach ($this->findCommandsForService($container, $definition, $attributes) as $commandClassName) {
if (isset($attributes['bus'])) {
$routing->routeToBus($attributes['bus'], $commandClassName, $serviceId);
} else {
$routing->routeToAllBuses($commandClassName, $serviceId);
}
}
}
abstract protected function isSupported(ContainerBuilder $container, Definition $definition, array $tagAttributes): bool;
abstract protected function findCommandsForService(ContainerBuilder $container, Definition $definition, array $tagAttributes): array;
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection\HandlerMapping;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use ReflectionClass;
use function method_exists;
/**
* Routes commands based on typehints in the handler.
*
* If your handler has a public method with a single, non-scalar, no-interface type hinted
* parameter, we'll assume that typehint is a command and route it to this
* service definition as the handler.
*
* So, a class like this:
*
* class MyHandler
* {
* public function handle(RegisterUser $command) {...}
* private function foobar(SomeObject $obj) {...}
* public function checkThings(OtherObject $obj, WhatObject $obj2)
* public function setADependency(ManagerInterface $interface) {...}
* }
*
* would have RegisterUser routed to it, but not SomeObject (because it's
* used in a private method), not OtherObject or WhatObject (because they
* don't appear as the only parameter) and not setADependency (because it
* has an interface type hinted parameter).
*/
final class TypeHintMapping extends TagBasedMapping
{
protected function isSupported(ContainerBuilder $container, Definition $definition, array $tagAttributes): bool
{
return isset($tagAttributes['typehints']) && $tagAttributes['typehints'] === true;
}
protected function findCommandsForService(ContainerBuilder $container, Definition $definition, array $tagAttributes): array
{
$results = [];
$reflClass = new ReflectionClass($container->getParameterBag()->resolveValue($definition->getClass()));
foreach ($reflClass->getMethods() as $method) {
if (!$method->isPublic()
|| $method->isConstructor()
|| $method->isStatic()
|| $method->isAbstract()
|| $method->isVariadic()
|| $method->getNumberOfParameters() !== 1
) {
continue;
}
$parameter = $method->getParameters()[0];
if (!$parameter->hasType()
|| $parameter->getType()->isBuiltin()
|| $parameter->getClass()->isInterface()
) {
continue;
}
$type = $parameter->getType();
if (version_compare(PHP_VERSION, '7.1.0') >= 0) {
$results[] = $type->getName();
} else {
$results[] = (string)$type;
}
}
return $results;
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace League\Tactician\Bundle\DependencyInjection;
final class InvalidCommandBusId extends \Exception
{
public static function ofName(string $expectedId, array $validIds)
{
return new static(
"Could not find a command bus with id '$expectedId'. Valid buses are: " . implode(', ', $validIds)
);
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace League\Tactician\Bundle\DependencyInjection;
use League\Tactician\Bundle\Security\Voter\HandleCommandVoter;
use League\Tactician\Logger\Formatter\ClassNameFormatter;
use League\Tactician\Logger\Formatter\ClassPropertiesFormatter;
use League\Tactician\Logger\LoggerMiddleware;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Loader;
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');
$container->setParameter('tactician.merged_config', $mergedConfig);
$this->configureSecurity($mergedConfig, $container);
$this->configureLogger($mergedConfig, $container);
}
public function getAlias()
{
return 'tactician';
}
/**
* Configure the security voter if the security middleware is loaded.
*
* @param array $mergedConfig
* @param ContainerBuilder $container
*/
private function configureSecurity(array $mergedConfig, ContainerBuilder $container)
{
foreach ($mergedConfig['commandbus'] as $commandBusConfig) {
if (in_array('tactician.middleware.security', $commandBusConfig['middleware'])) {
return $this->configureCommandSecurityVoter($mergedConfig, $container);
}
}
}
/**
* Configure the security voter.
*
* @param array $mergedConfig
* @param ContainerBuilder $container
*/
private function configureCommandSecurityVoter(array $mergedConfig, ContainerBuilder $container)
{
if (!$container->has('tactician.middleware.security_voter')) {
$definition = new Definition(
HandleCommandVoter::class,
[
new Reference('security.access.decision_manager'),
$mergedConfig['security']
]
);
$definition->addTag('security.voter');
$container->setDefinition('tactician.middleware.security_voter', $definition);
}
}
/**
* Configure the logger middleware.
*
* @param array $mergedConfig
* @param ContainerBuilder $container
*/
private function configureLogger(array $mergedConfig, ContainerBuilder $container)
{
$this->configureLoggerFormatters($container);
$loggerMiddleware = new Definition(LoggerMiddleware::class, [
new Reference($mergedConfig['logger_formatter']),
new Reference('logger')
]);
$loggerMiddleware->setPublic(false);
$loggerMiddleware->addTag('monolog.logger', ['channel' => 'command_bus']);
$container->setDefinition('tactician.middleware.logger', $loggerMiddleware);
}
private function configureLoggerFormatters(ContainerBuilder $container)
{
$container->setDefinition(
'tactician.logger.class_properties_formatter',
new Definition(ClassPropertiesFormatter::class)
)->setPublic(false);
$container->setDefinition(
'tactician.logger.class_name_formatter',
new Definition(ClassNameFormatter::class)
)->setPublic(false);
}
}

View File

@@ -0,0 +1,50 @@
<?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 Symfony DI container
*/
class ContainerBasedHandlerLocator implements HandlerLocator
{
/**
* @var ContainerInterface
*/
private $container;
/**
* @var array
*/
private $commandToServiceId = [];
/**
* @param ContainerInterface $container
* @param array $commandToServiceIdMapping
*/
public function __construct(ContainerInterface $container, array $commandToServiceIdMapping)
{
$this->container = $container;
$this->commandToServiceId = $commandToServiceIdMapping;
}
/**
* Retrieves the handler for a specified command
*
* @param string $commandName
* @return object
*
* @throws MissingHandlerException
*/
public function getHandlerForCommand($commandName)
{
if (!isset($this->commandToServiceId[$commandName])) {
throw MissingHandlerException::forCommand($commandName);
}
return $this->container->get($this->commandToServiceId[$commandName]);
}
}

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,41 @@
<?php
namespace League\Tactician\Bundle\Middleware;
use League\Tactician\Middleware;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
class SecurityMiddleware implements Middleware
{
/**
* @var AuthorizationCheckerInterface
*/
private $authorizationChecker;
/**
* @param AuthorizationCheckerInterface $authorizationChecker
*/
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
{
$this->authorizationChecker = $authorizationChecker;
}
/**
* @param object $command
* @param callable $next
*
* @return mixed
*/
public function execute($command, callable $next)
{
if ($this->authorizationChecker->isGranted('handle', $command)) {
return $next($command);
}
throw new AccessDeniedException(
sprintf('The current user is not allowed to handle command of type \'%s\'', get_class($command))
);
}
}

View File

@@ -0,0 +1,42 @@
<?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 $validator
*/
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
/**
* @param object $command
* @param callable $next
*
* @return mixed
*
* @throws InvalidCommandException
*/
public function execute($command, callable $next)
{
$constraintViolations = $this->validator->validate($command);
if (count($constraintViolations) > 0) {
throw InvalidCommandException::onCommand($command, $constraintViolations);
}
return $next($command);
}
}

View File

@@ -0,0 +1,31 @@
services:
tactician.middleware.locking:
class: League\Tactician\Plugins\LockingMiddleware
# The standard Handler method name inflectors
tactician.handler.method_name_inflector.handle:
class: League\Tactician\Handler\MethodNameInflector\HandleInflector
tactician.handler.method_name_inflector.class_name:
class: League\Tactician\Handler\MethodNameInflector\ClassNameInflector
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
tactician.command.debug:
class: League\Tactician\Bundle\Command\DebugCommand
tags:
- { name: console.command }

View File

@@ -0,0 +1,91 @@
<?php
namespace League\Tactician\Bundle\Security\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
/**
* Voter for security checks on handling commands.
*
* @author Ron Rademaker
*/
class HandleCommandVoter extends Voter
{
/**
* The decision manager.
*
* @var AccessDecisionManagerInterface
*/
private $decisionManager;
/**
* Command - Require role mapping
*
* @var array
*/
private $commandRoleMapping = [];
/**
* Create a new HandleCommandVoter.
*
* @param AccessDecisionManagerInterface $decisionManager
* @param array $commandRoleMapping
*/
public function __construct(AccessDecisionManagerInterface $decisionManager, array $commandRoleMapping = [])
{
$this->decisionManager = $decisionManager;
$this->commandRoleMapping = $commandRoleMapping;
}
/**
* The voter supports checking handle commands
*
* @param string $attribute
* @param object $subject
*
* @return bool
*/
protected function supports($attribute, $subject): bool
{
return $attribute === 'handle' && is_object($subject);
}
/**
* Checks if the currently logged on user may handle $subject.
*
* @param string $attribute
* @param mixed $subject
* @param TokenInterface $token
*
* @return bool
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$allowedRoles = $this->getAllowedRoles(get_class($subject));
if (count($allowedRoles) > 0) {
return $this->decisionManager->decide($token, $allowedRoles);
}
// default conclusion is access denied
return false;
}
/**
* Gets the roles allowed to handle a command of $type
*
* @param string $type
*
* @return array
*/
private function getAllowedRoles(string $type)
{
if (array_key_exists($type, $this->commandRoleMapping)) {
return $this->commandRoleMapping[$type];
}
return [];
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace League\Tactician\Bundle;
use League\Tactician\Bundle\DependencyInjection\HandlerMapping\ClassNameMapping;
use League\Tactician\Bundle\DependencyInjection\HandlerMapping\CompositeMapping;
use League\Tactician\Bundle\DependencyInjection\HandlerMapping\HandlerMapping;
use League\Tactician\Bundle\DependencyInjection\HandlerMapping\TypeHintMapping;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use League\Tactician\Bundle\DependencyInjection\Compiler;
use League\Tactician\Bundle\DependencyInjection\TacticianExtension;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class TacticianBundle extends Bundle
{
/**
* @var HandlerMapping
*/
private $handlerMapping;
public function __construct(HandlerMapping $handlerMapping = null)
{
if ($handlerMapping === null) {
$handlerMapping = static::defaultMappingStrategy();
}
$this->handlerMapping = $handlerMapping;
}
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new Compiler\DoctrineMiddlewarePass());
$container->addCompilerPass(new Compiler\ValidatorMiddlewarePass());
$container->addCompilerPass(new Compiler\SecurityMiddlewarePass());
$container->addCompilerPass(new Compiler\CommandHandlerPass($this->handlerMapping));
}
public function getContainerExtension()
{
return new TacticianExtension();
}
public static function defaultMappingStrategy(): HandlerMapping
{
return new CompositeMapping(new TypeHintMapping(), new ClassNameMapping());
}
}