From 1ef3577995424d54fbcfaf6d2ff0bf01ac1dc12b Mon Sep 17 00:00:00 2001 From: Chernosov Denis Date: Tue, 27 Feb 2018 15:11:47 +0400 Subject: [PATCH 01/79] fix doctrine mapping error in prod mode (#94) --- .../Infra/DependencyInjection/Compiler/ResolveDomainPass.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php index 030231a7..07142f88 100644 --- a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php +++ b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php @@ -36,7 +36,7 @@ public function process(ContainerBuilder $container): void self::register($container, DoctrineInfra\MappingCacheWarmer::class) ->setArgument('$dirname', '%msgphp.doctrine.mapping_cache_dirname%') ->setArgument('$mappingFiles', $mappingFiles) - ->addTag('kernel.cache_warmer'); + ->addTag('kernel.cache_warmer', ['priority' => 100]); } } From 6c5afe0ef0ccffc2c981b9114ea5257379471ab1 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Tue, 27 Feb 2018 15:27:02 +0100 Subject: [PATCH 02/79] update readme --- README.md | 1 + src/Domain/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 2917e0fb..36c50a0f 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Domain | Bundle ## Blog posts +- [Domain-driven-design: Writing domain layers. The fast way.](https://medium.com/@ro0NL/domain-driven-design-writing-domain-layers-the-fast-way-60ef87399374) - [Initializing objects with CLI and the power of Symfony Console](https://medium.com/@ro0NL/initializing-objects-with-cli-and-the-power-of-symfony-console-2a008d5611f) - [Commanding a decoupled User entity](https://medium.com/@ro0NL/commanding-a-decoupled-user-entity-aee8723c43e5) - [Decoupling the User entity with a new Symfony User Bundle](https://medium.com/@ro0NL/decoupling-the-user-entity-with-a-new-symfony-user-bundle-7d2d5d85bdf9) diff --git a/src/Domain/README.md b/src/Domain/README.md index d65b215e..7f810bb7 100644 --- a/src/Domain/README.md +++ b/src/Domain/README.md @@ -22,6 +22,7 @@ composer require msgphp/domain ## Blog posts +- [Domain-driven-design: Writing domain layers. The fast way.](https://medium.com/@ro0NL/domain-driven-design-writing-domain-layers-the-fast-way-60ef87399374) - [Initializing objects with CLI and the power of Symfony Console](https://medium.com/@ro0NL/initializing-objects-with-cli-and-the-power-of-symfony-console-2a008d5611f) ## Documentation From 63d8e0ae115c44c4229e592f1a7ededc440fdfc5 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 28 Feb 2018 15:41:50 +0100 Subject: [PATCH 03/79] [0.2] revise bundle config (#91) --- UPGRADE-0.2.md | 11 ++ phpstan.neon | 12 +- src/Domain/Infra/Config/ClassMappingNode.php | 42 +++++ .../Config/ClassMappingNodeDefinition.php | 171 ++++++++++++++++++ src/Domain/Infra/Config/NodeBuilder.php | 28 +++ src/Domain/Infra/Config/TreeBuilder.php | 23 +++ .../DependencyInjection/BundleHelper.php | 4 +- .../DependencyInjection/ConfigHelper.php | 136 +++----------- .../DependencyInjection/ContainerHelper.php | 39 ++-- .../DependencyInjection/Configuration.php | 65 ++++--- .../DependencyInjection/Extension.php | 10 +- src/User/Entity/Credential/Anonymous.php | 2 +- .../DependencyInjection/Configuration.php | 169 +++++++++-------- .../DependencyInjection/Extension.php | 10 +- 14 files changed, 451 insertions(+), 271 deletions(-) create mode 100644 UPGRADE-0.2.md create mode 100644 src/Domain/Infra/Config/ClassMappingNode.php create mode 100644 src/Domain/Infra/Config/ClassMappingNodeDefinition.php create mode 100644 src/Domain/Infra/Config/NodeBuilder.php create mode 100644 src/Domain/Infra/Config/TreeBuilder.php diff --git a/UPGRADE-0.2.md b/UPGRADE-0.2.md new file mode 100644 index 00000000..6819264e --- /dev/null +++ b/UPGRADE-0.2.md @@ -0,0 +1,11 @@ +# UPGRADE FROM 0.1 to 0.2 + +## EavBundle + +- Removed `%msgphp.default_data_type%` DI parameter, use `default_id_type` bundle configuration instead +- Removed `data_type_mapping` bundle configuration, use `id_type_mapping` instead + +## UserBundle + +- Removed `%msgphp.default_data_type%` DI parameter, use `default_id_type` bundle configuration instead +- Removed `data_type_mapping` bundle configuration, use `id_type_mapping` instead diff --git a/phpstan.neon b/phpstan.neon index 33fefef5..7a6e23ab 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,8 +10,16 @@ parameters: - '#Static property MsgPhp\\Domain\\Tests\\Infra\\InMemory\\DomainEntityRepositoryTraitTest::\$memory \(MsgPhp\\Domain\\Infra\\InMemory\\GlobalObjectMemory\) does not accept null\.#' - '#Class MsgPhp\\Domain\\Tests\\Factory\\WrongCase referenced with incorrect case: MsgPhp\\Domain\\Tests\\Factory\\wrongcase\.#' - # fluent mixed interface usage in src/*Bundle/DependencyInjection/Configuration.php - - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition::append\(\)\.#' + # see https://github.com/symfony/symfony/pull/26297 + - '#Parameter \#1 \$name of method Symfony\\Component\\Config\\Definition\\Builder\\NodeBuilder::node\(\) expects string, null given\.#' + + # forward compatibility + - '#Class MsgPhp\\Domain\\Infra\\Config\\ClassMappingNode constructor invoked with 3 parameters, 1\-2 required\.#' + - '#Access to an undefined property MsgPhp\\Domain\\Infra\\Config\\ClassMappingNodeDefinition::\$pathSeparator#' + + # symfony fluent config + - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::classMappingNode\(\)\.#' + - '#Calling method classMappingNode\(\) on possibly null value of type Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface\|null\.#' excludes_analyse: - '*/vendor/*' diff --git a/src/Domain/Infra/Config/ClassMappingNode.php b/src/Domain/Infra/Config/ClassMappingNode.php new file mode 100644 index 00000000..9c2dbd25 --- /dev/null +++ b/src/Domain/Infra/Config/ClassMappingNode.php @@ -0,0 +1,42 @@ + + */ +final class ClassMappingNode extends PrototypedArrayNode +{ + public function setAllowEmptyValue($allowEmptyValue): void + { + $this->setMinNumberOfElements($allowEmptyValue ? 0 : 1); + } + + protected function validateType($value): void + { + parent::validateType($value); + + if (!is_array($value)) { + throw new \UnexpectedValueException(sprintf('Expected configuration value to be type array, got "%s".', gettype($value))); + } + + foreach ($value as $k => $v) { + if (class_exists($k) || interface_exists($k)) { + continue; + } + + $e = new InvalidConfigurationException(sprintf('A class or interface named "%s" does not exists at path "%s".', $k, $this->getPath())); + $e->setPath($this->getPath()); + if ($hint = $this->getInfo()) { + $e->addHint($hint); + } + + throw $e; + } + } +} diff --git a/src/Domain/Infra/Config/ClassMappingNodeDefinition.php b/src/Domain/Infra/Config/ClassMappingNodeDefinition.php new file mode 100644 index 00000000..56614fc4 --- /dev/null +++ b/src/Domain/Infra/Config/ClassMappingNodeDefinition.php @@ -0,0 +1,171 @@ + + */ +final class ClassMappingNodeDefinition extends VariableNodeDefinition implements ParentNodeDefinitionInterface +{ + public const NAME = 'class_mapping'; + + /** @var BaseNodeBuilder|null */ + private $builder; + private $prototype; + private $type = 'scalar'; + + public function requireClasses(array $classes): self + { + foreach ($classes as $class) { + $this->validate() + ->ifTrue(function (array $value) use ($class): bool { + return !isset($value[$class]); + }) + ->thenInvalid(sprintf('Class mapping for "%s" must be configured.', $class)); + } + + if ($classes) { + $this->isRequired(); + } + + return $this; + } + + public function disallowClasses(array $classes): self + { + foreach ($classes as $class) { + $this->validate() + ->ifTrue(function (array $value) use ($class): bool { + return isset($value[$class]); + }) + ->thenInvalid(sprintf('Class mapping for "%s" is not applicable.', $class)); + } + + return $this; + } + + public function typeOfValues(string $type): self + { + $this->type = $type; + + return $this; + } + + public function subClassValues(): self + { + $this->validate()->always(function (array $value): array { + foreach ($value as $class => $mappedClass) { + if (!is_string($mappedClass)) { + throw new \LogicException(sprintf('Mapped value for class "%s" must be a string, got "%s".', $class, gettype($mappedClass))); + } + if (!is_subclass_of($mappedClass, $class)) { + throw new \LogicException(sprintf('Mapped class "%s" must be a sub class of "%s".', $mappedClass, $class)); + } + } + + return $value; + }); + + return $this; + } + + public function subClassKeys(array $classes): self + { + $this->validate()->always(function (array $value) use ($classes): array { + foreach ($value as $class => $classValue) { + foreach ($classes as $subClass) { + if (!is_subclass_of($class, $subClass)) { + throw new \LogicException(sprintf('Class "%s" must be a sub class of "%s".', $class, $subClass)); + } + } + } + + return $value; + }); + + return $this; + } + + public function defaultMapping(array $mapping): self + { + $this->defaultValue($mapping); + $this->validate()->always(function (array $value) use ($mapping): array { + return $value + $mapping; + }); + + return $this; + } + + public function children(): BaseNodeBuilder + { + throw new \BadMethodCallException(sprintf('Method "%s" is not applicable.', __METHOD__)); + } + + public function append(NodeDefinition $node): self + { + throw new \BadMethodCallException(sprintf('Method "%s" is not applicable.', __METHOD__)); + } + + public function getChildNodeDefinitions(): array + { + throw new \BadMethodCallException(sprintf('Method "%s" is not applicable.', __METHOD__)); + } + + public function setBuilder(BaseNodeBuilder $builder): void + { + $this->builder = $builder; + } + + /** + * @return NodeParentInterface|BaseNodeBuilder|NodeDefinition|ArrayNodeDefinition|VariableNodeDefinition|NodeBuilder|self|null + */ + public function end() + { + return $this->parent; + } + + protected function instantiateNode(): ClassMappingNode + { + return new ClassMappingNode($this->name, $this->parent, $this->pathSeparator ?? '.'); + } + + protected function createNode(): NodeInterface + { + /** @var ClassMappingNode $node */ + $node = parent::createNode(); + $node->setKeyAttribute('class'); + + $prototype = $this->getPrototype(); + $prototype->parent = $node; + $prototypedNode = $prototype->getNode(); + + if (!$prototypedNode instanceof PrototypeNodeInterface) { + throw new \LogicException(sprintf('Prototyped node must be an instance of "%s", got "%s".', PrototypeNodeInterface::class, get_class($prototypedNode))); + } + + $node->setPrototype($prototypedNode); + + return $node; + } + + private function getPrototype(): NodeDefinition + { + if (null === $this->prototype) { + $this->prototype = ($this->builder ?? new NodeBuilder())->node(null, $this->type); + $this->prototype->setParent($this); + } + + return $this->prototype; + } +} diff --git a/src/Domain/Infra/Config/NodeBuilder.php b/src/Domain/Infra/Config/NodeBuilder.php new file mode 100644 index 00000000..d86c28d8 --- /dev/null +++ b/src/Domain/Infra/Config/NodeBuilder.php @@ -0,0 +1,28 @@ + + */ +final class NodeBuilder extends BaseNodeBuilder +{ + public function __construct() + { + parent::__construct(); + + $this->setNodeClass(ClassMappingNodeDefinition::NAME, ClassMappingNodeDefinition::class); + } + + public function classMappingNode(string $name): ClassMappingNodeDefinition + { + /** @var ClassMappingNodeDefinition $node */ + $node = $this->node($name, ClassMappingNodeDefinition::NAME); + + return $node; + } +} diff --git a/src/Domain/Infra/Config/TreeBuilder.php b/src/Domain/Infra/Config/TreeBuilder.php new file mode 100644 index 00000000..887b1e44 --- /dev/null +++ b/src/Domain/Infra/Config/TreeBuilder.php @@ -0,0 +1,23 @@ + + */ +final class TreeBuilder extends BaseTreeBuilder +{ + public function rootArray(string $name, BaseNodeBuilder $builder = null): ArrayNodeDefinition + { + /** @var ArrayNodeDefinition $node */ + $node = $this->root($name, 'array', $builder ?? new NodeBuilder()); + + return $node; + } +} diff --git a/src/Domain/Infra/DependencyInjection/BundleHelper.php b/src/Domain/Infra/DependencyInjection/BundleHelper.php index e3c26731..7fea7273 100644 --- a/src/Domain/Infra/DependencyInjection/BundleHelper.php +++ b/src/Domain/Infra/DependencyInjection/BundleHelper.php @@ -72,8 +72,8 @@ public static function initDoctrineTypes(Container $container): void if ($container->hasParameter($param = 'msgphp.doctrine.type_config')) { foreach ($container->getParameter($param) as $config) { - $config['type']::setClass($config['class']); - $config['type']::setDataType($config['data_type']); + $config['type_class']::setClass($config['class']); + $config['type_class']::setDataType($config['type']); } } diff --git a/src/Domain/Infra/DependencyInjection/ConfigHelper.php b/src/Domain/Infra/DependencyInjection/ConfigHelper.php index e52bfb6a..ff198db1 100644 --- a/src/Domain/Infra/DependencyInjection/ConfigHelper.php +++ b/src/Domain/Infra/DependencyInjection/ConfigHelper.php @@ -7,10 +7,6 @@ use MsgPhp\Domain\DomainId; use MsgPhp\Domain\Event\DomainEventHandlerInterface; use MsgPhp\Domain\Infra\Uuid as UuidInfra; -use Ramsey\Uuid\UuidInterface; -use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; -use Symfony\Component\Config\Definition\Builder\NodeBuilder; -use Symfony\Component\DependencyInjection\ContainerBuilder; /** * @author Roland Franssen @@ -19,125 +15,51 @@ */ final class ConfigHelper { - public const NATIVE_DATA_TYPES = ['string', 'integer', 'bigint']; - public const UUID_DATA_TYPES = ['uuid', 'uuid_binary', 'uuid_binary_ordered_time']; + public const DEFAULT_ID_TYPE = 'integer'; + public const UUID_TYPES = ['uuid', 'uuid_binary', 'uuid_binary_ordered_time']; - // @todo consider custom node type instead - public static function createClassMappingNode(string $name, array $required = [], array $abstracts = [], bool $valueIsClass = false, \Closure $normalizer = null, $defaultValue = null, string $prototype = 'scalar', \Closure $prototypeCallback = null, NodeBuilder $builder = null): ArrayNodeDefinition + public static function defaultBundleConfig(array $defaultIdClassMapping, array $idClassMappingPerType): \Closure { - $node = ($builder ?? new NodeBuilder())->arrayNode($name); - $node->useAttributeAsKey('class'); + return function (array $value) use ($defaultIdClassMapping, $idClassMappingPerType): array { + $defaultType = $value['default_id_type'] ?? ConfigHelper::DEFAULT_ID_TYPE; + unset($value['default_id_type']); - if ($required) { - $node->isRequired(); + if (isset($value['id_type_mapping'])) { + foreach ($value['id_type_mapping'] as $class => $type) { + if (isset($value['class_mapping'][$class])) { + continue; + } - foreach ($required as $class) { - $node->validate()->ifTrue(function (array $value) use ($class) { - return !isset($value[$class]); - })->thenInvalid(sprintf('Class "%s" must be configured.', $class)); - } - } - - if (null !== $normalizer) { - $node->beforeNormalization()->always($normalizer); - } - - $prototype = $node->prototype($prototype); - $prototype->defaultValue($defaultValue); - - if (null !== $prototypeCallback) { - $prototypeCallback($prototype); - } - - $node->validate()->always(function (array $value) use ($abstracts, $valueIsClass): array { - foreach ($value as $class => $mappedClass) { - if (!($isClass = class_exists($class)) && !interface_exists($class)) { - throw new \LogicException(sprintf('A class or interface named "%s" does not exists.', $class)); - } - - if (!$valueIsClass || null === $mappedClass || ($isClass && $mappedClass === $class)) { - continue; - } + if (null === $mappedClass = $idClassMapping[$type][$class] ?? $defaultIdClassMapping[$class] ?? null) { + $mappedClass = in_array($type, self::UUID_TYPES, true) ? UuidInfra\DomainId::class : DomainId::class; + } - if (!class_exists($mappedClass)) { - throw new \LogicException(sprintf('A class named "%s" does not exists.', $mappedClass)); - } - - if ((!$isClass || in_array($class, $abstracts, true) || (new \ReflectionClass($class))->isAbstract()) && !is_subclass_of($mappedClass, $class)) { - throw new \LogicException(sprintf('The class "%s" must be a sub class of "%s".', $mappedClass, $class)); + $value['class_mapping'][$class] = $mappedClass; } } - return $value; - }); - - return $node; - } - - public static function resolveResolveDataTypeMapping(ContainerBuilder $container, array &$config): void - { - if (!$container->hasParameter('msgphp.default_data_type')) { - $container->setParameter('msgphp.default_data_type', 'integer'); - } - - foreach ($config as &$value) { - $value = $container->getParameterBag()->resolveValue($value ?? '%msgphp.default_data_type%'); - } - - unset($value, $config); - } - - public static function resolveClassMapping(array $dataTypeMap, array $dataTypeMapping, array &$config): void - { - foreach ($config as $key => &$value) { - if (null !== $value) { - continue; - } - - if (!isset($dataTypeMapping[$key])) { - $value = $key; - continue; - } - - $value = DomainId::class; - if (in_array($dataType = $dataTypeMapping[$key], self::UUID_DATA_TYPES, true)) { - if (!interface_exists(UuidInterface::class)) { - throw new \LogicException(sprintf('Data type "%s" for identifier "%s" requires "ramsey/uuid".', $dataType, $key)); - } - - $value = UuidInfra\DomainId::class; + if (isset($idClassMappingPerType[$defaultType])) { + $value['class_mapping'] += $idClassMappingPerType[$defaultType]; + $value['id_type_mapping'] += array_fill_keys(array_keys($idClassMappingPerType[$defaultType]), $defaultType); } - if (!isset($dataTypeMap[$key])) { - continue; - } - - foreach ($dataTypeMap[$key] as $class => $dataTypes) { - if (in_array($dataType, $dataTypes, true)) { - $value = $class; - break; - } - } - } + $value['class_mapping'] += $defaultIdClassMapping; + $value['id_type_mapping'] += array_fill_keys(array_keys($defaultIdClassMapping), $defaultType); - unset($value, $config); + return $value; + }; } - public static function resolveCommandMapping(array $classMapping, array $mapping, array &$config): void + public static function resolveCommandMappingConfig(array $commandMapping, array $classMapping, array &$config): void { - foreach ($mapping as $class => $traits) { - $mappedClass = $classMapping[$class] ?? $class; - if ($class !== $mappedClass && !is_subclass_of($mappedClass, $class)) { - continue; - } - + foreach ($commandMapping as $commandClass => $features) { + $mappedClass = $classMapping[$commandClass] ?? $commandClass; $isEventHandler = is_subclass_of($mappedClass, DomainEventHandlerInterface::class); - foreach ($traits as $trait => $traitConfig) { - if (!self::uses($mappedClass, $trait)) { - continue; - } - $config += array_fill_keys($traitConfig, $isEventHandler); + foreach ($features as $feature => $featureCommands) { + if (self::uses($mappedClass, $feature)) { + $config += array_fill_keys($featureCommands, $isEventHandler); + } } } } diff --git a/src/Domain/Infra/DependencyInjection/ContainerHelper.php b/src/Domain/Infra/DependencyInjection/ContainerHelper.php index 857a50be..3c378605 100644 --- a/src/Domain/Infra/DependencyInjection/ContainerHelper.php +++ b/src/Domain/Infra/DependencyInjection/ContainerHelper.php @@ -117,48 +117,43 @@ public static function configureEntityFactory(ContainerBuilder $container, array $container->setParameter($param, $values); } - public static function configureDoctrineTypes(ContainerBuilder $container, array $dataTypeMapping, array $classMapping, array $typeMapping): void + public static function configureDoctrineTypes(ContainerBuilder $container, array $classMapping, array $idTypeMapping, array $typeClassMapping): void { if (!class_exists(DoctrineType::class)) { return; } - $types = $mappingTypes = $typeConfig = []; + $dbalTypes = $mappingTypes = $typeConfig = []; $uuidMapping = [ 'uuid' => DoctrineUuid\UuidType::class, 'uuid_binary' => DoctrineUuid\UuidBinaryType::class, 'uuid_binary_ordered_time' => DoctrineUuid\UuidBinaryOrderedTimeType::class, ]; - foreach ($typeMapping as $class => $type) { - $dataType = $dataTypeMapping[$class] ?? DoctrineType::INTEGER; + foreach ($typeClassMapping as $idClass => $typeClass) { + $type = $idTypeMapping[$idClass] ?? DoctrineType::INTEGER; - if (isset($uuidMapping[$dataType])) { - if (!class_exists($uuidClass = $uuidMapping[$dataType])) { - throw new \LogicException(sprintf('Data type "%s" for identifier "%s" requires "ramsey/uuid-doctrine".', $dataType, $class)); + if (isset($uuidMapping[$type])) { + if (!class_exists($uuidClass = $uuidMapping[$type])) { + throw new \LogicException(sprintf('Type "%s" for identifier "%s" requires "ramsey/uuid-doctrine".', $type, $idClass)); } - $types[$uuidClass::NAME] = $uuidClass; + $dbalTypes[$uuidClass::NAME] = $uuidClass; - if ('uuid_binary' === $dataType || 'uuid_binary_ordered_time' === $dataType) { - $mappingTypes[$dataType] = 'binary'; + if ('uuid_binary' === $type || 'uuid_binary_ordered_time' === $type) { + $mappingTypes[$type] = 'binary'; } } - if (!defined($type.'::NAME')) { - throw new \LogicException(sprintf('Type class "%s" for identifier "%s" requires a "NAME" constant.', $type, $class)); + if (!defined($typeClass.'::NAME')) { + throw new \LogicException(sprintf('Type class "%s" for identifier "%s" requires a "NAME" constant.', $typeClass, $idClass)); } - $types[$type::NAME] = $type; - $typeConfig[$type::NAME] = ['class' => $classMapping[$class] ?? $class, 'type' => $type, 'data_type' => $dataType]; + $dbalTypes[$typeClass::NAME] = $typeClass; + $typeConfig[$typeClass::NAME] = ['class' => $classMapping[$idClass] ?? $idClass, 'type' => $type, 'type_class' => $typeClass]; } - $config = $types ? ['types' => $types] : []; - if ($mappingTypes) { - $config['mapping_types'] = $mappingTypes; - } - - if ($config) { + if ($dbalTypes || $mappingTypes) { if ($container->hasParameter($param = 'msgphp.doctrine.type_config')) { $typeConfig += $container->getParameter($param); } @@ -166,9 +161,7 @@ public static function configureDoctrineTypes(ContainerBuilder $container, array $container->setParameter($param, $typeConfig); if (self::hasBundle($container, DoctrineBundle::class)) { - $container->prependExtensionConfig('doctrine', [ - 'dbal' => $config, - ]); + $container->prependExtensionConfig('doctrine', ['dbal' => ['types' => $dbalTypes, 'mapping_types' => $mappingTypes]]); } } } diff --git a/src/EavBundle/DependencyInjection/Configuration.php b/src/EavBundle/DependencyInjection/Configuration.php index 18899dee..787355ef 100644 --- a/src/EavBundle/DependencyInjection/Configuration.php +++ b/src/EavBundle/DependencyInjection/Configuration.php @@ -4,10 +4,11 @@ namespace MsgPhp\EavBundle\DependencyInjection; +use MsgPhp\Domain\DomainIdInterface; +use MsgPhp\Domain\Infra\Config\{NodeBuilder, TreeBuilder}; use MsgPhp\Domain\Infra\DependencyInjection\ConfigHelper; use MsgPhp\Eav\{AttributeId, AttributeIdInterface, AttributeValueId, AttributeValueIdInterface, Entity}; use MsgPhp\Eav\Infra\Uuid as UuidInfra; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; /** @@ -22,44 +23,42 @@ final class Configuration implements ConfigurationInterface public const OPTIONAL_AGGREGATE_ROOTS = []; public const AGGREGATE_ROOTS = self::REQUIRED_AGGREGATE_ROOTS + self::OPTIONAL_AGGREGATE_ROOTS; public const IDENTITY_MAPPING = [ - Entity\Attribute::class => 'id', - Entity\AttributeValue::class => 'id', + Entity\Attribute::class => ['id'], + Entity\AttributeValue::class => ['id'], ]; - public const DATA_TYPE_MAPPING = [ - AttributeIdInterface::class => [ - AttributeId::class => ConfigHelper::NATIVE_DATA_TYPES, - UuidInfra\AttributeId::class => ConfigHelper::UUID_DATA_TYPES, - ], - AttributeValueIdInterface::class => [ - AttributeValueId::class => ConfigHelper::NATIVE_DATA_TYPES, - UuidInfra\AttributeValueId::class => ConfigHelper::UUID_DATA_TYPES, - ], + public const DEFAULT_ID_CLASS_MAPPING = [ + AttributeIdInterface::class => AttributeId::class, + AttributeValueIdInterface::class => AttributeValueId::class, + ]; + public const UUID_CLASS_MAPPING = [ + AttributeIdInterface::class => UuidInfra\AttributeId::class, + AttributeValueIdInterface::class => UuidInfra\AttributeValueId::class, ]; public function getConfigTreeBuilder(): TreeBuilder { - $treeBuilder = new TreeBuilder(); - $ids = array_values(self::AGGREGATE_ROOTS); - $entities = array_keys(self::IDENTITY_MAPPING); - $requiredEntities = array_keys(self::REQUIRED_AGGREGATE_ROOTS); - - $treeBuilder->root(Extension::ALIAS) - ->append( - ConfigHelper::createClassMappingNode('class_mapping', $requiredEntities, $entities, true, function (array $value) use ($ids): array { - return $value + array_fill_keys($ids, null); - }) - ) - ->append( - ConfigHelper::createClassMappingNode('data_type_mapping', [], [], false, function ($value) use ($ids): array { - if (!is_array($value)) { - $value = array_fill_keys($ids, $value); - } else { - $value += array_fill_keys($ids, null); - } + /** @var NodeBuilder $children */ + $children = ($treeBuilder = new TreeBuilder())->rootArray(Extension::ALIAS)->children(); - return $value; - })->addDefaultChildrenIfNoneSet($ids) - ); + $children + ->classMappingNode('class_mapping') + ->requireClasses(array_keys(self::REQUIRED_AGGREGATE_ROOTS)) + ->subClassValues() + ->end() + ->classMappingNode('id_type_mapping') + ->subClassKeys([DomainIdInterface::class]) + ->end() + ->scalarNode('default_id_type') + ->defaultValue(ConfigHelper::DEFAULT_ID_TYPE) + ->cannotBeEmpty() + ->end() + ->end() + ->validate() + ->always(ConfigHelper::defaultBundleConfig( + self::DEFAULT_ID_CLASS_MAPPING, + array_fill_keys(ConfigHelper::UUID_TYPES, self::UUID_CLASS_MAPPING) + )) + ->end(); return $treeBuilder; } diff --git a/src/EavBundle/DependencyInjection/Extension.php b/src/EavBundle/DependencyInjection/Extension.php index e9c2c8de..49c767af 100644 --- a/src/EavBundle/DependencyInjection/Extension.php +++ b/src/EavBundle/DependencyInjection/Extension.php @@ -6,7 +6,7 @@ use Doctrine\Bundle\DoctrineBundle\DoctrineBundle; use Doctrine\ORM\Version as DoctrineOrmVersion; -use MsgPhp\Domain\Infra\DependencyInjection\{ConfigHelper, ContainerHelper}; +use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\Eav\{AttributeIdInterface, AttributeValueIdInterface, Entity}; use MsgPhp\Eav\Infra\Doctrine as DoctrineInfra; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -39,9 +39,6 @@ public function load(array $configs, ContainerBuilder $container): void $loader = new PhpFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config')); $config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs); - ConfigHelper::resolveResolveDataTypeMapping($container, $config['data_type_mapping']); - ConfigHelper::resolveClassMapping(Configuration::DATA_TYPE_MAPPING, $config['data_type_mapping'], $config['class_mapping']); - ContainerHelper::configureIdentityMapping($container, $config['class_mapping'], Configuration::IDENTITY_MAPPING); ContainerHelper::configureEntityFactory($container, $config['class_mapping'], Configuration::AGGREGATE_ROOTS); ContainerHelper::configureDoctrineOrmMapping($container, self::getDoctrineMappingFiles($config, $container), [DoctrineInfra\EntityFieldsMapping::class]); @@ -56,10 +53,7 @@ public function prepend(ContainerBuilder $container): void { $config = $this->processConfiguration($this->getConfiguration($configs = $container->getExtensionConfig($this->getAlias()), $container), $configs); - ConfigHelper::resolveResolveDataTypeMapping($container, $config['data_type_mapping']); - ConfigHelper::resolveClassMapping(Configuration::DATA_TYPE_MAPPING, $config['data_type_mapping'], $config['class_mapping']); - - ContainerHelper::configureDoctrineTypes($container, $config['data_type_mapping'], $config['class_mapping'], [ + ContainerHelper::configureDoctrineTypes($container, $config['class_mapping'], $config['id_type_mapping'], [ AttributeIdInterface::class => DoctrineInfra\Type\AttributeIdType::class, AttributeValueIdInterface::class => DoctrineInfra\Type\AttributeValueIdType::class, ]); diff --git a/src/User/Entity/Credential/Anonymous.php b/src/User/Entity/Credential/Anonymous.php index a242a836..de67e387 100644 --- a/src/User/Entity/Credential/Anonymous.php +++ b/src/User/Entity/Credential/Anonymous.php @@ -13,7 +13,7 @@ final class Anonymous implements CredentialInterface { public static function getUsernameField(): string { - return 'username'; + throw new \LogicException('An anonymous credential has no username field.'); } public function getUsername(): string diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index 18f90ddf..0b75c602 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -4,11 +4,12 @@ namespace MsgPhp\UserBundle\DependencyInjection; +use MsgPhp\Domain\DomainIdInterface; use MsgPhp\Domain\Entity\Features; +use MsgPhp\Domain\Infra\Config\{NodeBuilder, TreeBuilder}; use MsgPhp\Domain\Infra\DependencyInjection\ConfigHelper; use MsgPhp\User\{Command, CredentialInterface, Entity, UserId, UserIdInterface}; use MsgPhp\User\Infra\Uuid as UuidInfra; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; /** @@ -23,16 +24,16 @@ final class Configuration implements ConfigurationInterface public const AGGREGATE_ROOTS = self::REQUIRED_AGGREGATE_ROOTS + self::OPTIONAL_AGGREGATE_ROOTS; public const IDENTITY_MAPPING = [ Entity\UserAttributeValue::class => ['user', 'attributeValue'], - Entity\User::class => 'id', + Entity\User::class => ['id'], Entity\Username::class => ['user', 'username'], Entity\UserRole::class => ['user', 'role'], Entity\UserSecondaryEmail::class => ['user', 'email'], ]; - public const DATA_TYPE_MAPPING = [ - UserIdInterface::class => [ - UserId::class => ConfigHelper::NATIVE_DATA_TYPES, - UuidInfra\UserId::class => ConfigHelper::UUID_DATA_TYPES, - ], + public const DEFAULT_ID_CLASS_MAPPING = [ + UserIdInterface::class => UserId::class, + ]; + public const UUID_CLASS_MAPPING = [ + UserIdInterface::class => UuidInfra\UserId::class, ]; private const COMMAND_MAPPING = [ Entity\User::class => [ @@ -51,101 +52,95 @@ final class Configuration implements ConfigurationInterface public function getConfigTreeBuilder(): TreeBuilder { - $treeBuilder = new TreeBuilder(); - $ids = array_values(self::AGGREGATE_ROOTS); - $entities = array_keys(self::IDENTITY_MAPPING); - $requiredEntities = array_keys(self::REQUIRED_AGGREGATE_ROOTS); - - $treeBuilder->root(Extension::ALIAS) - ->append( - ConfigHelper::createClassMappingNode('class_mapping', $requiredEntities, $entities, true, function (array $value) use ($ids): array { - return $value + array_fill_keys($ids, null); - }) - ) - ->append( - ConfigHelper::createClassMappingNode('data_type_mapping', [], [], false, function ($value) use ($ids): array { - if (!is_array($value)) { - $value = array_fill_keys($ids, $value); - } else { - $value += array_fill_keys($ids, null); - } - - return $value; - })->addDefaultChildrenIfNoneSet($ids) - ) - ->append( - ConfigHelper::createClassMappingNode('commands', [], [], false, null, true, 'boolean') - ) - ->children() - ->arrayNode('username_lookup') - ->arrayPrototype() - ->children() - ->scalarNode('target')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('field')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('mapped_by')->defaultValue('user')->cannotBeEmpty()->end() + /** @var NodeBuilder $children */ + $children = ($treeBuilder = new TreeBuilder())->rootArray(Extension::ALIAS)->children(); + + $children + ->classMappingNode('class_mapping') + ->requireClasses(array_keys(self::REQUIRED_AGGREGATE_ROOTS)) + ->disallowClasses([CredentialInterface::class, Entity\Username::class]) + ->subClassValues() + ->end() + ->classMappingNode('id_type_mapping') + ->subClassKeys([DomainIdInterface::class]) + ->end() + ->classMappingNode('commands') + ->typeOfValues('boolean') + ->defaultMapping([ + Command\CreateUserCommand::class => true, + Command\DeleteUserCommand::class => true, + ]) + ->end() + ->scalarNode('default_id_type') + ->defaultValue(ConfigHelper::DEFAULT_ID_TYPE) + ->cannotBeEmpty() + ->end() + ->arrayNode('username_lookup') + ->arrayPrototype() + ->children() + ->scalarNode('target') + ->isRequired() + ->cannotBeEmpty() + ->validate() + ->ifTrue(function ($value): bool { + return Entity\Username::class === $value; + }) + ->thenInvalid('Target %s is not applicable.') + ->end() ->end() + ->scalarNode('field')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('mapped_by')->defaultValue('user')->cannotBeEmpty()->end() ->end() ->end() ->end() - ->validate() - ->always(function (array $config): array { - if (isset($config['class_mapping'][Entity\Username::class])) { - throw new \LogicException(sprintf('Class mapping for "%s" is not applicable.', Entity\Username::class)); + ->end() + ->validate() + ->always(ConfigHelper::defaultBundleConfig( + self::DEFAULT_ID_CLASS_MAPPING, + array_fill_keys(ConfigHelper::UUID_TYPES, self::UUID_CLASS_MAPPING) + )) + ->end() + ->validate() + ->always(function (array $config): array { + $usernameLookup = []; + foreach ($config['username_lookup'] as &$value) { + if (isset($config['class_mapping'][$value['target']])) { + $value['target'] = $config['class_mapping'][$value['target']]; } - $usernameLookup = []; - foreach ($config['username_lookup'] as &$value) { - if (isset($config['class_mapping'][$value['target']])) { - $value['target'] = $config['class_mapping'][$value['target']]; - } - - if (isset($usernameLookup[$value['target']]) && in_array($value, $usernameLookup[$value['target']], true)) { - throw new \LogicException(sprintf('Duplicate username lookup mapping for "%s".', $value['target'])); - } - - $usernameLookup[$value['target']][] = $value; + if (isset($usernameLookup[$value['target']]) && in_array($value, $usernameLookup[$value['target']], true)) { + throw new \LogicException(sprintf('Duplicate username lookup mapping for "%s".', $value['target'])); } - unset($value); - - $userCredential = self::getUserCredential($userClass = $config['class_mapping'][Entity\User::class]); - if ($usernameLookup) { - if (isset($usernameLookup[$userClass])) { - throw new \LogicException(sprintf('Username lookup mapping for "%s" cannot be overwritten.', $userClass)); - } + $usernameLookup[$value['target']][] = $value; + } + unset($value); - if (null !== $userCredential['username_field']) { - $usernameLookup[$userClass][] = ['target' => $userClass, 'field' => $userCredential['username_field']]; - } + $userCredential = self::getUserCredential($userClass = $config['class_mapping'][Entity\User::class]); - if (isset($usernameLookup[Entity\Username::class])) { - throw new \LogicException(sprintf('Username lookup mapping for "%s" is not applicable.', Entity\Username::class)); - } - - $config['class_mapping'][Entity\Username::class] = Entity\Username::class; + if ($usernameLookup) { + if (isset($usernameLookup[$userClass])) { + throw new \LogicException(sprintf('Username lookup mapping for "%s" cannot be overwritten.', $userClass)); } - $config['username_field'] = $userCredential['username_field']; - $config['username_lookup'] = $usernameLookup; - $config['commands'] += [ - Command\CreateUserCommand::class => true, - Command\DeleteUserCommand::class => true, - ]; + if (null !== $userCredential['username_field']) { + $usernameLookup[$userClass][] = ['target' => $userClass, 'field' => $userCredential['username_field']]; + } + } - if (null !== $userCredential['class']) { - if (isset($config['class_mapping'][CredentialInterface::class])) { - throw new \LogicException(sprintf('Class mapping for "%s" cannot be overwritten.', CredentialInterface::class)); - } + $config['class_mapping'][CredentialInterface::class] = $userCredential['class']; + $config['username_field'] = $userCredential['username_field']; + $config['username_lookup'] = $usernameLookup; - $config['class_mapping'][CredentialInterface::class] = $userCredential['class']; - $config['commands'][Command\ChangeUserCredentialCommand::class] = true; - } + if (null !== $userCredential['username_field'] && !isset($config['commands'][Command\ChangeUserCredentialCommand::class])) { + $config['commands'][Command\ChangeUserCredentialCommand::class] = true; + } - ConfigHelper::resolveCommandMapping($config['class_mapping'], self::COMMAND_MAPPING, $config['commands']); + ConfigHelper::resolveCommandMappingConfig(self::COMMAND_MAPPING, $config['class_mapping'], $config['commands']); - return $config; - }) - ->end(); + return $config; + }) + ->end(); return $treeBuilder; } @@ -153,7 +148,7 @@ public function getConfigTreeBuilder(): TreeBuilder private static function getUserCredential(string $userClass): array { if (null === $credential = (new \ReflectionMethod($userClass, 'getCredential'))->getReturnType()) { - return ['class' => null, 'username_field' => null]; + return ['class' => Entity\Credential\Anonymous::class, 'username_field' => null]; } if ($credential->isBuiltin() || !is_subclass_of($class = $credential->getName(), CredentialInterface::class)) { diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index 2f48515c..a478c95d 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -8,7 +8,7 @@ use Doctrine\ORM\Version as DoctrineOrmVersion; use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; use MsgPhp\Domain\Infra\Console as BaseConsoleInfra; -use MsgPhp\Domain\Infra\DependencyInjection\{ConfigHelper, ContainerHelper}; +use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\EavBundle\MsgPhpEavBundle; use MsgPhp\User\{Command, CredentialInterface, Entity, Repository, UserIdInterface}; use MsgPhp\User\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, Security as SecurityInfra, Validator as ValidatorInfra}; @@ -49,9 +49,6 @@ public function load(array $configs, ContainerBuilder $container): void $loader = new PhpFileLoader($container, new FileLocator(dirname(__DIR__).'/Resources/config')); $config = $this->processConfiguration($this->getConfiguration($configs, $container), $configs); - ConfigHelper::resolveResolveDataTypeMapping($container, $config['data_type_mapping']); - ConfigHelper::resolveClassMapping(Configuration::DATA_TYPE_MAPPING, $config['data_type_mapping'], $config['class_mapping']); - $loader->load('services.php'); ContainerHelper::configureIdentityMapping($container, $config['class_mapping'], Configuration::IDENTITY_MAPPING); @@ -153,10 +150,7 @@ public function prepend(ContainerBuilder $container): void { $config = $this->processConfiguration($this->getConfiguration($configs = $container->getExtensionConfig($this->getAlias()), $container), $configs); - ConfigHelper::resolveResolveDataTypeMapping($container, $config['data_type_mapping']); - ConfigHelper::resolveClassMapping(Configuration::DATA_TYPE_MAPPING, $config['data_type_mapping'], $config['class_mapping']); - - ContainerHelper::configureDoctrineTypes($container, $config['data_type_mapping'], $config['class_mapping'], [ + ContainerHelper::configureDoctrineTypes($container, $config['class_mapping'], $config['id_type_mapping'], [ UserIdInterface::class => DoctrineInfra\Type\UserIdType::class, ]); ContainerHelper::configureDoctrineOrmTargetEntities($container, $config['class_mapping']); From 962325d7a9934f746accb5d48c31da26b1382fb9 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 28 Feb 2018 15:57:11 +0100 Subject: [PATCH 04/79] fix username repository definition --- src/UserBundle/DependencyInjection/Extension.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index a478c95d..a27a6eef 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -181,6 +181,8 @@ private function prepareDoctrineOrm(array $config, LoaderInterface $loader, Cont $container->getDefinition(DoctrineInfra\Repository\UsernameRepository::class) ->setArgument('$targetMapping', $config['username_lookup']); + + $config['class_mapping'][Entity\Username::class] = Entity\Username::class; } else { $container->removeDefinition(DoctrineInfra\Event\UsernameListener::class); } From e7668028dc945bf46fe3e4075d911f110ede8c04 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Thu, 1 Mar 2018 13:04:09 +0100 Subject: [PATCH 05/79] version badges --- src/Domain/README.md | 2 ++ src/Eav/README.md | 2 ++ src/EavBundle/README.md | 2 ++ src/User/README.md | 2 ++ src/UserBundle/README.md | 2 ++ 5 files changed, 10 insertions(+) diff --git a/src/Domain/README.md b/src/Domain/README.md index 7f810bb7..b606f2ca 100644 --- a/src/Domain/README.md +++ b/src/Domain/README.md @@ -2,6 +2,8 @@ A base domain layer to rapidly built other domain layers. +[![Latest Stable Version](https://poser.pugx.org/msgphp/domain/v/stable)](https://packagist.org/packages/msgphp/domain) + This package is part of the _Message driven PHP_ project. > [MsgPHP](https://msgphp.github.io/) is a project that aims to provide (common) message based domain layers for your application. It has a low development time overhead and avoids being overly opinionated. diff --git a/src/Eav/README.md b/src/Eav/README.md index fb734a81..926a6f53 100644 --- a/src/Eav/README.md +++ b/src/Eav/README.md @@ -3,6 +3,8 @@ A domain layer providing basic [EAV](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model) management. +[![Latest Stable Version](https://poser.pugx.org/msgphp/eav/v/stable)](https://packagist.org/packages/msgphp/eav) + This package is part of the _Message driven PHP_ project. > [MsgPHP](https://msgphp.github.io/) is a project that aims to provide (common) message based domain layers for your application. It has a low development time overhead and avoids being overly opinionated. diff --git a/src/EavBundle/README.md b/src/EavBundle/README.md index cd879547..de52b4fa 100644 --- a/src/EavBundle/README.md +++ b/src/EavBundle/README.md @@ -2,6 +2,8 @@ A Symfony bundle for basic [EAV](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model) management. +[![Latest Stable Version](https://poser.pugx.org/msgphp/eav-bundle/v/stable)](https://packagist.org/packages/msgphp/eav-bundle) + This package is part of the _Message driven PHP_ project. > [MsgPHP](https://msgphp.github.io/) is a project that aims to provide (common) message based domain layers for your application. It has a low development time overhead and avoids being overly opinionated. diff --git a/src/User/README.md b/src/User/README.md index c4bce588..b5dec05a 100644 --- a/src/User/README.md +++ b/src/User/README.md @@ -2,6 +2,8 @@ A domain layer providing basic user management. +[![Latest Stable Version](https://poser.pugx.org/msgphp/user/v/stable)](https://packagist.org/packages/msgphp/user) + This package is part of the _Message driven PHP_ project. > [MsgPHP](https://msgphp.github.io/) is a project that aims to provide (common) message based domain layers for your application. It has a low development time overhead and avoids being overly opinionated. diff --git a/src/UserBundle/README.md b/src/UserBundle/README.md index 627cfda3..8ee23770 100644 --- a/src/UserBundle/README.md +++ b/src/UserBundle/README.md @@ -2,6 +2,8 @@ A new Symfony bundle for basic user management. +[![Latest Stable Version](https://poser.pugx.org/msgphp/user-bundle/v/stable)](https://packagist.org/packages/msgphp/user-bundle) + This package is part of the _Message driven PHP_ project. > [MsgPHP](https://msgphp.github.io/) is a project that aims to provide (common) message based domain layers for your application. It has a low development time overhead and avoids being overly opinionated. From 4b857eb96f73ac0fde79b7510be05dd31fee1314 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Thu, 1 Mar 2018 16:06:05 +0100 Subject: [PATCH 06/79] fix Undefined index: data_type --- src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php b/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php index 936df832..2d948bab 100644 --- a/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php +++ b/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php @@ -53,7 +53,7 @@ private function processClassIdentifiers(ClassMetadataInfo $metadata): void } foreach ($metadata->getIdentifierFieldNames() as $field) { - if (!isset($this->typeConfig[$type = $metadata->getTypeOfField($field)]) || !in_array($this->typeConfig[$type]['data_type'], [Type::INTEGER, Type::BIGINT], true)) { + if (!isset($this->typeConfig[$type = $metadata->getTypeOfField($field)]) || !in_array($this->typeConfig[$type]['type'], [Type::INTEGER, Type::BIGINT], true)) { continue; } From cdd7ae05d313e4c40a4eb09db5309bd5b61adb91 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Thu, 1 Mar 2018 16:53:18 +0100 Subject: [PATCH 07/79] tweak class mapping usage (#97) --- .../Compiler/ResolveDomainPass.php | 56 +++++++++---------- .../DependencyInjection/ContainerHelper.php | 4 +- .../Infra/Doctrine/DomainIdentityMapping.php | 6 +- .../Doctrine/DomainIdentityMappingTest.php | 8 ++- 4 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php index 07142f88..f1a3b064 100644 --- a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php +++ b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php @@ -8,7 +8,6 @@ use MsgPhp\Domain\{DomainIdentityHelper, DomainIdentityMappingInterface, Factory, Message}; use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\Domain\Infra\{Doctrine as DoctrineInfra, InMemory as InMemoryInfra, SimpleBus as SimpleBusInfra}; -use SimpleBus\Message\Bus\MessageBus as SimpleMessageBus; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -29,15 +28,20 @@ public function process(ContainerBuilder $container): void $this->registerEntityFactory($container); $this->registerMessageBus($container); - if (interface_exists(CacheWarmerInterface::class) && $container->hasParameter($param = 'msgphp.doctrine.mapping_files')) { - $mappingFiles = array_merge(...$container->getParameter($param)); - $container->getParameterBag()->remove($param); + if (interface_exists(CacheWarmerInterface::class) && $container->hasParameter('msgphp.doctrine.mapping_files')) { + $mappingFiles = array_merge(...$container->getParameter('msgphp.doctrine.mapping_files')); self::register($container, DoctrineInfra\MappingCacheWarmer::class) ->setArgument('$dirname', '%msgphp.doctrine.mapping_cache_dirname%') ->setArgument('$mappingFiles', $mappingFiles) ->addTag('kernel.cache_warmer', ['priority' => 100]); } + + $params = $container->getParameterBag(); + $params->remove('msgphp.doctrine.mapping_files'); + $params->remove('msgphp.doctrine.identity_mapping'); + $params->remove('msgphp.doctrine.class_mapping'); + $params->remove('msgphp.doctrine.id_class_mapping'); } private static function register(ContainerBuilder $container, string $class, string $id = null): Definition @@ -52,21 +56,21 @@ private static function alias(ContainerBuilder $container, string $alias, string private function registerIdentityMapping(ContainerBuilder $container): void { - $identityMapping = array_merge(...$container->getParameter($param = 'msgphp.domain.identity_mapping')); - $container->getParameterBag()->remove($param); + $identityMapping = array_merge(...$container->getParameter('msgphp.domain.identity_mapping')); + $classMapping = array_merge(...$container->getParameter('msgphp.domain.class_mapping')); if ($container->has(DoctrineEntityManager::class)) { - self::register($container, $alias = DoctrineInfra\DomainIdentityMapping::class) - ->setAutowired(true); + self::register($container, $aliasId = DoctrineInfra\DomainIdentityMapping::class) + ->setAutowired(true) + ->setArgument('$classMapping', $classMapping); } else { - self::register($container, InMemoryInfra\ObjectFieldAccessor::class); - - self::register($container, $alias = InMemoryInfra\DomainIdentityMapping::class) + self::register($container, $aliasId = InMemoryInfra\DomainIdentityMapping::class) ->setArgument('$mapping', $identityMapping) - ->setArgument('$accessor', new Reference(InMemoryInfra\ObjectFieldAccessor::class)); + ->setArgument('$accessor', self::register($container, InMemoryInfra\ObjectFieldAccessor::class) + ->setAutowired(true)); } - self::alias($container, DomainIdentityMappingInterface::class, $alias); + self::alias($container, DomainIdentityMappingInterface::class, $aliasId); self::register($container, DomainIdentityHelper::class) ->setAutowired(true); @@ -74,17 +78,14 @@ private function registerIdentityMapping(ContainerBuilder $container): void private function registerEntityFactory(ContainerBuilder $container): void { - $classMapping = array_merge(...$container->getParameter($param = 'msgphp.domain.class_mapping')); - $container->getParameterBag()->remove($param); - - $idClassMapping = array_merge(...$container->getParameter($param = 'msgphp.domain.id_class_mapping')); - $container->getParameterBag()->remove($param); + $classMapping = array_merge(...$container->getParameter('msgphp.domain.class_mapping')); + $idClassMapping = array_merge(...$container->getParameter('msgphp.domain.id_class_mapping')); - self::register($container, Factory\DomainObjectFactory::class) + self::register($container, $aliasId = Factory\DomainObjectFactory::class) ->addMethodCall('setNestedFactory', [new Reference(Factory\DomainObjectFactoryInterface::class)]); self::register($container, Factory\ClassMappingObjectFactory::class) - ->setDecoratedService(Factory\DomainObjectFactory::class) + ->setDecoratedService($aliasId) ->setArgument('$factory', new Reference(Factory\ClassMappingObjectFactory::class.'.inner')) ->setArgument('$mapping', $classMapping); @@ -98,24 +99,19 @@ private function registerEntityFactory(ContainerBuilder $container): void ->setArgument('$classMapping', $classMapping)); } - self::alias($container, Factory\DomainObjectFactoryInterface::class, Factory\DomainObjectFactory::class); + self::alias($container, Factory\DomainObjectFactoryInterface::class, $aliasId); self::alias($container, Factory\EntityAwareFactoryInterface::class, Factory\EntityAwareFactory::class); } private function registerMessageBus(ContainerBuilder $container): void { - if (!($autowire = $container->has(SimpleMessageBus::class)) && !$container->has('simple_bus.command_bus')) { + if (!$container->has('simple_bus.command_bus')) { return; } - $definition = self::register($container, SimpleBusInfra\DomainMessageBus::class); - - if ($autowire) { - $definition->setAutowired(true); - } else { - $definition->setArgument('$bus', new Reference('simple_bus.command_bus')); - } + self::register($container, $aliasId = SimpleBusInfra\DomainMessageBus::class) + ->setArgument('$bus', new Reference('simple_bus.command_bus')); - self::alias($container, Message\DomainMessageBusInterface::class, SimpleBusInfra\DomainMessageBus::class); + self::alias($container, Message\DomainMessageBusInterface::class, $aliasId); } } diff --git a/src/Domain/Infra/DependencyInjection/ContainerHelper.php b/src/Domain/Infra/DependencyInjection/ContainerHelper.php index 3c378605..62a39474 100644 --- a/src/Domain/Infra/DependencyInjection/ContainerHelper.php +++ b/src/Domain/Infra/DependencyInjection/ContainerHelper.php @@ -87,7 +87,7 @@ public static function registerAnonymous(ContainerBuilder $container, string $cl public static function configureIdentityMapping(ContainerBuilder $container, array $classMapping, array $identityMapping): void { foreach ($identityMapping as $class => $mapping) { - if (isset($classMapping[$class])) { + if (isset($classMapping[$class]) && !isset($identityMapping[$classMapping[$class]])) { $identityMapping[$classMapping[$class]] = $mapping; } } @@ -101,7 +101,7 @@ public static function configureIdentityMapping(ContainerBuilder $container, arr public static function configureEntityFactory(ContainerBuilder $container, array $classMapping, array $idClassMapping): void { foreach ($idClassMapping as $class => $idClass) { - if (isset($classMapping[$class])) { + if (isset($classMapping[$class]) && !isset($idClassMapping[$classMapping[$class]])) { $idClassMapping[$classMapping[$class]] = $idClass; } } diff --git a/src/Domain/Infra/Doctrine/DomainIdentityMapping.php b/src/Domain/Infra/Doctrine/DomainIdentityMapping.php index 6f34429c..fefd9583 100644 --- a/src/Domain/Infra/Doctrine/DomainIdentityMapping.php +++ b/src/Domain/Infra/Doctrine/DomainIdentityMapping.php @@ -15,10 +15,12 @@ final class DomainIdentityMapping implements DomainIdentityMappingInterface { private $em; + private $classMapping; - public function __construct(EntityManagerInterface $em) + public function __construct(EntityManagerInterface $em, array $classMapping = []) { $this->em = $em; + $this->classMapping = $classMapping; } public function getIdentifierFieldNames(string $class): array @@ -43,6 +45,8 @@ public function getIdentity($object): array private function getMetadata(string $class): ClassMetadata { + $class = $this->classMapping[$class] ?? $class; + if (!class_exists($class) || $this->em->getMetadataFactory()->isTransient($class)) { throw InvalidClassException::create($class); } diff --git a/src/Domain/Tests/Infra/Doctrine/DomainIdentityMappingTest.php b/src/Domain/Tests/Infra/Doctrine/DomainIdentityMappingTest.php index 14819df8..c80cd6fd 100644 --- a/src/Domain/Tests/Infra/Doctrine/DomainIdentityMappingTest.php +++ b/src/Domain/Tests/Infra/Doctrine/DomainIdentityMappingTest.php @@ -4,9 +4,10 @@ namespace MsgPhp\Domain\Tests\Infra\Doctrine; -use MsgPhp\Domain\DomainIdentityMappingInterface; +use MsgPhp\Domain\{DomainIdentityMappingInterface, DomainIdInterface}; use MsgPhp\Domain\Infra\Doctrine\DomainIdentityMapping; use MsgPhp\Domain\Tests\AbstractDomainIdentityMappingTest; +use MsgPhp\Domain\Tests\Fixtures\Entities; final class DomainIdentityMappingTest extends AbstractDomainIdentityMappingTest { @@ -14,6 +15,11 @@ final class DomainIdentityMappingTest extends AbstractDomainIdentityMappingTest private $createSchema = false; + public function testClassMapping(): void + { + $this->assertSame(['id'], (new DomainIdentityMapping(self::$em, ['foo' => Entities\TestEntity::class]))->getIdentifierFieldNames('foo')); + } + protected static function createMapping(): DomainIdentityMappingInterface { return new DomainIdentityMapping(self::$em); From c9efbe5b279c71b2faaeb09906907bd47739975a Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 13:27:28 +0100 Subject: [PATCH 08/79] handle doctrine discriminator mapping (#90) --- docs/ddd/collections.md | 2 +- docs/ddd/factory/entity-aware.md | 46 ++--- docs/ddd/factory/object.md | 2 +- docs/ddd/identifiers.md | 4 +- docs/ddd/identity-mapping.md | 8 +- docs/ddd/repositories.md | 2 +- docs/infrastructure/doctrine-orm.md | 104 ++++++----- src/Domain/Factory/EntityAwareFactory.php | 18 +- .../Compiler/ResolveDomainPass.php | 13 +- .../Infra/Doctrine/EntityAwareFactory.php | 91 ++++++++++ .../Infra/Doctrine/EntityReferenceLoader.php | 43 ----- .../Infra/InMemory/DomainIdentityMapping.php | 6 +- ...bstractDomainEntityRepositoryTraitTest.php | 4 +- .../Tests/Factory/EntityAwareFactoryTest.php | 38 +---- .../Fixtures/Entities/TestChildEntity.php | 24 +++ .../Fixtures/Entities/TestParentEntity.php | 39 +++++ .../Infra/Doctrine/EntityAwareFactoryTest.php | 161 ++++++++++++++++++ .../Doctrine/EntityReferenceLoaderTest.php | 74 -------- .../InMemory/DomainIdentityMappingTest.php | 11 +- src/Eav/README.md | 2 +- src/EavBundle/README.md | 2 +- .../Infra/Doctrine/Event/UsernameListener.php | 47 ++--- src/User/README.md | 2 +- src/UserBundle/README.md | 2 +- 24 files changed, 490 insertions(+), 255 deletions(-) create mode 100644 src/Domain/Infra/Doctrine/EntityAwareFactory.php delete mode 100644 src/Domain/Infra/Doctrine/EntityReferenceLoader.php create mode 100644 src/Domain/Tests/Fixtures/Entities/TestChildEntity.php create mode 100644 src/Domain/Tests/Fixtures/Entities/TestParentEntity.php create mode 100644 src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php delete mode 100644 src/Domain/Tests/Infra/Doctrine/EntityReferenceLoaderTest.php diff --git a/docs/ddd/collections.md b/docs/ddd/collections.md index b210344a..9abaa5c5 100644 --- a/docs/ddd/collections.md +++ b/docs/ddd/collections.md @@ -124,7 +124,7 @@ $firstTwoIntsPlussed = $firstTwoInts->map(function (int $value): int { ### `MsgPhp\Domain\Infra\Doctrine\DomainCollection` -Domain collection based on _Doctrine Collections_. +A Doctrine tailored domain collection. - [Read more](../infrastructure/doctrine-collections.md#domain-collection) diff --git a/docs/ddd/factory/entity-aware.md b/docs/ddd/factory/entity-aware.md index 323ac65d..821c1b96 100644 --- a/docs/ddd/factory/entity-aware.md +++ b/docs/ddd/factory/entity-aware.md @@ -33,50 +33,54 @@ might be considered empty if it's not capable to calculate one upfront. ### `MsgPhp\Domain\Factory\EntityAwareFactory` -A generic entity factory. It decorates any object factory and additionally must be provided with an entity to identifier -class mapping. +A generic entity factory. It decorates any object factory and is based on a known [identity mapping](../identity-mapping.md) +as well as the entity to identifier class mapping. -- `__construct(DomainObjectFactoryInterface $factory, array $identifierMapping, callable $referenceLoader = null)` +- `__construct(DomainObjectFactoryInterface $factory, DomainIdentityMappingInterface $identityMapping, array $identifierMapping = [])` - `$factory`: The decorated object factory - - `$identifierMapping`: The identifier class mapping (`['EntityClass' => 'IdClass']`) - - `$referenceLoader`: An optional reference loader. If `null` using `reference()` is not supported. The callable - receives the same arguments as given to `reference()`. It should return an instance of the received class name. + - `$identityMapping`: The identity mapping + - `$identifierMapping`: The identifier class mapping (`['EntityType' => 'IdType']`) #### Basic example ```php DomainUuid::class, -], function (string $class, $id) { - $object = new $class(); - $object->id = $id; + public function __construct($id) + { + $this->id = $id; + } +} - return $object; -}); +$factory = new EntityAwareFactory(new DomainObjectFactory(), new DomainIdentityMapping([ + MyEntity::class => 'id', +]), [ + MyEntity::class => DomainId::class, +]); // --- USAGE --- /** @var MyEntity $entity */ -$ref = $factory->reference(MyEntity::class, new DomainUuid()); +$ref = $factory->reference(MyEntity::class, new DomainId('1')); -/** @var DomainUuid $id */ -$id = $factory->identify(MyEntity::class, 'cf3d2f85-6c86-44d1-8634-af51c91a9a74'); +/** @var DomainId $id */ +$id = $factory->identify(MyEntity::class, 1); -/** @var DomainUuid $id */ +/** @var DomainId $id */ $id = $factory->nextIdentifier(MyEntity::class); ``` +### `MsgPhp\Domain\Infra\Doctrine\EntityAwareFactory` + +A Doctrine tailored entity aware factory. -When working with Doctrine one can leverage its tailored [entity reference loader](../../infrastructure/doctrine-orm.md#entity-reference-loader) -and provide it as the callable reference loader to be used. +- [Read more](../../infrastructure/doctrine-orm.md#entity-aware-factory) diff --git a/docs/ddd/factory/object.md b/docs/ddd/factory/object.md index 52c2bd9d..485b624c 100644 --- a/docs/ddd/factory/object.md +++ b/docs/ddd/factory/object.md @@ -98,7 +98,7 @@ mapping. In case the class is not mapped it will be used as is. - `__construct(DomainObjectFactoryInterface $factory, array $mapping)` - `$factory`: The decorated object factory - - `$mapping`: The class mapping (`['SourceClass' => 'TargetClass']`) + - `$mapping`: The class mapping (`['SourceType' => 'TargetType']`) #### Basic example diff --git a/docs/ddd/identifiers.md b/docs/ddd/identifiers.md index efd41694..1d02ec88 100644 --- a/docs/ddd/identifiers.md +++ b/docs/ddd/identifiers.md @@ -80,9 +80,9 @@ $emptyId->isEmpty() ? null : $emptyId->toString(); // null ### `MsgPhp\Domain\Infra\Uuid\DomainId` -Domain identifier based on UUID values. +A UUID tailored domain identifier. -- [Read more](../infrastructure/uuid.md) +- [Read more](../infrastructure/uuid.md#domain-identifier) [string-able]: https://secure.php.net/manual/en/language.oop5.magic.php#object.tostring [serializable]: https://secure.php.net/serializable diff --git a/docs/ddd/identity-mapping.md b/docs/ddd/identity-mapping.md index d1689a87..10c164ab 100644 --- a/docs/ddd/identity-mapping.md +++ b/docs/ddd/identity-mapping.md @@ -22,6 +22,10 @@ identifier field name. Identity mapping based on a known in-memory mapping. +- `__construct(array $mapping, ObjectFieldAccessor $accessor = null)` + - `$mapping`: The identity mapping to use + - `$accessor`: Custom object field accessor. See also [`ObjectFieldAccessor`][api-objectfieldaccessor]. + #### Basic example ```php @@ -65,6 +69,8 @@ $mapping->getIdentity($compositeEntity); // ['name' => ..., 'year' => ...] ### `MsgPhp\Domain\Infra\Doctrine\DomainIdentityMapping` -Identity mapping based on Doctrine's identity map. +A Doctrine tailored identity mapping. - [Read more](../infrastructure/doctrine-orm.md#domain-identity-mapping) + +[api-objectfieldaccessor]: https://msgphp.github.io/api/MsgPhp/Domain/Infra/InMemory/ObjectFieldAccessor.html diff --git a/docs/ddd/repositories.md b/docs/ddd/repositories.md index e57246a5..8a3b4c0f 100644 --- a/docs/ddd/repositories.md +++ b/docs/ddd/repositories.md @@ -110,7 +110,7 @@ if ($repository->exists(1)) { ### `MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait` -Repository trait based on _Doctrine ORM_ persistence. +A Doctrine tailored repository trait. - [Read more](../infrastructure/doctrine-orm.md#domain-repository) diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index 39c3defe..40968da4 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -7,11 +7,12 @@ An overview of available infrastructural code when using Doctrine's [Object Rela ## Domain identity mapping A Doctrine tailored [domain identity mapping](../ddd/identity-mapping.md) is provided by -`MsgPhp\Domain\Infra\Doctrine\DomainIdentityMapping`. It uses Doctrine's [`EntityManagerInterface`][api-em] as -underlying mapping. +`MsgPhp\Domain\Infra\Doctrine\DomainIdentityMapping`. It uses Doctrine's [`EntityManagerInterface`][api-em] to provide +the identity mapping from its class metadata. -- `__construct(EntityManagerInterface $em)` +- `__construct(EntityManagerInterface $em, array $classMapping = [])` - `$em`: The entity manager to use + - `$classMapping`: The class mapping (`['SourceType' => 'TargetType']`) ### Basic example @@ -99,97 +100,122 @@ if ($repository->exists($id = ['name' => ..., 'year' => ...])) { } ``` -## Hydration +## Entity aware factory -When working with [identifiers](../ddd/identifiers.md) and the corresponding [type](doctrine-dbal.md#domain-identifier-type) -a problem might occur when hydrating scalar values, e.g. using [`Query::getScalarResult()`][api-query-getscalarresult]; -it would return instances of `MsgPhp\Domain\DomainIdInterface` that can only be casted to string as its (true) scalar -value (due to `__toString()`). In case the underlying data type is e.g. `integer` we'll lose it. +A Doctrine tailored [entity aware factory](../ddd/factory/entity-aware.md) is provided by +`MsgPhp\Domain\Infra\Doctrine\EntityAwareFactory`. It decorates any entity aware factory and uses Doctrine's +[`EntityManagerInterface`][api-em]. Its purpose is to create lazy-loading references when using `reference()` (see +[`EntityManagerInterface::getReference()`][api-em-getreference]) and handle an entity its discriminator map when working +with [inheritance][orm-inheritance]. -To overcome, two hydration modes are available in order to hydrate the primitive identifier value instead. +- `__construct(EntityAwareFactoryInterface $factory, EntityManagerInterface $em, array $classMapping = [])` + - `$factory`: The decorated factory + - `$em`: The entity manager to use + - `$classMapping`: The class mapping (`['SourceType' => 'TargetType']`) ### Basic example ```php getConfiguration()->addCustomHydrationMode(ScalarHydrator::NAME, ScalarHydrator::class); -$em->getConfiguration()->addCustomHydrationMode(SingleScalarHydrator::NAME, SingleScalarHydrator::class); +$factory = new EntityAwareFactory( + new BaseEntityAwareFactory(new DomainObjectFactory(), new DomainIdentityMapping($em)), + $em +); // --- USAGE --- -$query = $em->createQuery('SELECT entity.id FORM MyEntity entity'); +/** @var MyEntity $ref */ +$ref = $factory->reference(MyEntity::class, 1); // no database hit -$query->getScalarResult()[0]['id']; // "1" -$query->getResult(ScalarHydrator::NAME)[0]['id']; // int(1) +/** @var MyOtherEntity $ref */ +$otherRef = $factory->reference(MyEntity::class, ['id' => 1, 'discriminator' => MyEntity::TYPE_OTHER]); -$query->getSingleScalarResult(); // "1" -$query->getSingleResult(SingleScalarHydrator::NAME); // int(1) +/** @var MyOtherEntity $otherEntity */ +$otherEntity = $factory->create(MyEntity::class, [ + 'discriminator' => MyEntity::TYPE_OTHER, +]); ``` -## Entity reference loader +## Hydration -A Doctrine tailored entity reference loader in the form of an invokable object is provided by -`MsgPhp\Domain\Infra\Doctrine\EntityReferenceLoader`. Its main purpose is to be used as a callable _reference loader_ -when working with the generic [entity aware factory](../ddd/factory/entity-aware.md#msgphpdomainfactoryentityawarefactory) -in effort to get a lazy-loading reference object, managed by Doctrine. See also [`EntityManagerInterface::getReference()`][api-em-getreference]. +When working with [identifiers](../ddd/identifiers.md) and the corresponding [type](doctrine-dbal.md#domain-identifier-type) +a problem might occur when hydrating scalar values, e.g. using [`Query::getScalarResult()`][api-query-getscalarresult]; +it would return instances of `MsgPhp\Domain\DomainIdInterface` that can only be casted to string as its (true) scalar +value (due to `__toString()`). In case the underlying data type is e.g. `integer` we'll lose it. -- `__construct(EntityManagerInterface $em, array $classMapping = [], DomainIdentityHelper $identityHelper = null)` - - `$em`: The entity manager to use - - `$classMapping`: An optional class mapping to use (`['SourceClass' => 'TargetClass']`) - - `$identityHelper`: Custom domain identity helper. By default it's resolved from the given entity manager. - [Read more](../ddd/identities.md). -- `__invoke(string $class, $id): ?object` +To overcome, two hydration modes are available in order to hydrate the primitive identifier value instead. ### Basic example ```php getConfiguration()->addCustomHydrationMode(ScalarHydrator::NAME, ScalarHydrator::class); +$em->getConfiguration()->addCustomHydrationMode(SingleScalarHydrator::NAME, SingleScalarHydrator::class); // --- USAGE --- -/** @var MyEntity|null $ref */ -$ref = $loader(MyEntity::class, 1); // no database hit +$query = $em->createQuery('SELECT entity.id FORM MyEntity entity'); + +$query->getScalarResult()[0]['id']; // "1" +$query->getResult(ScalarHydrator::NAME)[0]['id']; // int(1) + +$query->getSingleScalarResult(); // "1" +$query->getSingleResult(SingleScalarHydrator::NAME); // int(1) ``` [orm-project]: http://www.doctrine-project.org/projects/orm.html +[orm-inheritance]: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html [doctrine/orm]: https://packagist.org/packages/doctrine/orm [api-em]: http://www.doctrine-project.org/api/orm/2.5/class-Doctrine.ORM.EntityManagerInterface.html [api-em-getreference]: http://www.doctrine-project.org/api/orm/2.5/class-Doctrine.ORM.EntityManagerInterface.html#_getReference diff --git a/src/Domain/Factory/EntityAwareFactory.php b/src/Domain/Factory/EntityAwareFactory.php index 0a830b22..09189208 100644 --- a/src/Domain/Factory/EntityAwareFactory.php +++ b/src/Domain/Factory/EntityAwareFactory.php @@ -4,7 +4,7 @@ namespace MsgPhp\Domain\Factory; -use MsgPhp\Domain\DomainIdInterface; +use MsgPhp\Domain\{DomainIdentityMappingInterface, DomainIdInterface}; use MsgPhp\Domain\Exception\InvalidClassException; /** @@ -13,14 +13,14 @@ final class EntityAwareFactory implements EntityAwareFactoryInterface { private $factory; + private $identityMapping; private $identifierMapping; - private $referenceLoader; - public function __construct(DomainObjectFactoryInterface $factory, array $identifierMapping, callable $referenceLoader = null) + public function __construct(DomainObjectFactoryInterface $factory, DomainIdentityMappingInterface $identityMapping, array $identifierMapping = []) { $this->factory = $factory; + $this->identityMapping = $identityMapping; $this->identifierMapping = $identifierMapping; - $this->referenceLoader = $referenceLoader; } public function create(string $class, array $context = []) @@ -30,15 +30,13 @@ public function create(string $class, array $context = []) public function reference(string $class, $id) { - if (null === $this->referenceLoader) { - throw new \LogicException('No reference loader set.'); - } + $idFields = $this->identityMapping->getIdentifierFieldNames($class); - if (is_object($object = ($this->referenceLoader)($class, $id))) { - return $object; + if (!is_array($id)) { + $id = [array_shift($idFields) => $id]; } - throw new \RuntimeException(sprintf('Unable to create a reference object for "%s".', $class)); + return $this->factory->create($class, $id + array_fill_keys($idFields, null)); } public function identify(string $class, $value): DomainIdInterface diff --git a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php index f1a3b064..05d7d7e3 100644 --- a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php +++ b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php @@ -6,7 +6,6 @@ use Doctrine\ORM\EntityManagerInterface as DoctrineEntityManager; use MsgPhp\Domain\{DomainIdentityHelper, DomainIdentityMappingInterface, Factory, Message}; -use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\Domain\Infra\{Doctrine as DoctrineInfra, InMemory as InMemoryInfra, SimpleBus as SimpleBusInfra}; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -89,18 +88,20 @@ private function registerEntityFactory(ContainerBuilder $container): void ->setArgument('$factory', new Reference(Factory\ClassMappingObjectFactory::class.'.inner')) ->setArgument('$mapping', $classMapping); - $entityFactory = self::register($container, Factory\EntityAwareFactory::class) - ->setArgument('$factory', new Reference(Factory\DomainObjectFactory::class)) + self::register($container, $entityAliasId = Factory\EntityAwareFactory::class) + ->setAutowired(true) ->setArgument('$identifierMapping', $idClassMapping); if ($container->has(DoctrineEntityManager::class)) { - $entityFactory->setArgument('$referenceLoader', ContainerHelper::registerAnonymous($container, DoctrineInfra\EntityReferenceLoader::class) + self::register($container, DoctrineInfra\EntityAwareFactory::class) ->setAutowired(true) - ->setArgument('$classMapping', $classMapping)); + ->setDecoratedService($entityAliasId) + ->setArgument('$factory', new Reference(DoctrineInfra\EntityAwareFactory::class.'.inner')) + ->setArgument('$classMapping', $classMapping); } self::alias($container, Factory\DomainObjectFactoryInterface::class, $aliasId); - self::alias($container, Factory\EntityAwareFactoryInterface::class, Factory\EntityAwareFactory::class); + self::alias($container, Factory\EntityAwareFactoryInterface::class, $entityAliasId); } private function registerMessageBus(ContainerBuilder $container): void diff --git a/src/Domain/Infra/Doctrine/EntityAwareFactory.php b/src/Domain/Infra/Doctrine/EntityAwareFactory.php new file mode 100644 index 00000000..fd9206e3 --- /dev/null +++ b/src/Domain/Infra/Doctrine/EntityAwareFactory.php @@ -0,0 +1,91 @@ + + */ +final class EntityAwareFactory implements EntityAwareFactoryInterface +{ + private $factory; + private $em; + private $classMapping; + + public function __construct(EntityAwareFactoryInterface $factory, EntityManagerInterface $em, array $classMapping = []) + { + $this->factory = $factory; + $this->em = $em; + $this->classMapping = $classMapping; + } + + public function create(string $class, array $context = []) + { + if (!$this->isManaged($class = $this->classMapping[$class] ?? $class)) { + throw InvalidClassException::create($class); + } + + return $this->factory->create($this->getDiscriminatorClass($class, $context), $context); + } + + public function reference(string $class, $id) + { + if (!$this->isManaged($class = $this->classMapping[$class] ?? $class)) { + throw InvalidClassException::create($class); + } + if (is_array($id)) { + $class = $this->getDiscriminatorClass($class, $id, true); + } + if (null === $ref = $this->em->getReference($class, $id)) { + throw InvalidClassException::create($class); + } + + return $ref; + } + + public function identify(string $class, $value): DomainIdInterface + { + if (!$this->isManaged($class = $this->classMapping[$class] ?? $class)) { + throw InvalidClassException::create($class); + } + + return $this->factory->identify($class, $value); + } + + public function nextIdentifier(string $class): DomainIdInterface + { + if (!$this->isManaged($class = $this->classMapping[$class] ?? $class)) { + throw InvalidClassException::create($class); + } + + return $this->factory->nextIdentifier($class); + } + + private function isManaged(string $class): bool + { + return class_exists($class) && !$this->em->getMetadataFactory()->isTransient($class); + } + + private function getDiscriminatorClass(string $class, array &$context, bool $clear = false): string + { + $metadata = $this->em->getClassMetadata($class); + + if (isset($metadata->discriminatorColumn['fieldName'], $context[$metadata->discriminatorColumn['fieldName']])) { + $class = $metadata->discriminatorMap[$context[$metadata->discriminatorColumn['fieldName']]] ?? $class; + + if ($clear) { + unset($context[$metadata->discriminatorColumn['fieldName']]); + } + } + + unset($context); + + return $class; + } +} diff --git a/src/Domain/Infra/Doctrine/EntityReferenceLoader.php b/src/Domain/Infra/Doctrine/EntityReferenceLoader.php deleted file mode 100644 index 7039f363..00000000 --- a/src/Domain/Infra/Doctrine/EntityReferenceLoader.php +++ /dev/null @@ -1,43 +0,0 @@ - - */ -final class EntityReferenceLoader -{ - private $em; - private $classMapping; - private $identityHelper; - - public function __construct(EntityManagerInterface $em, array $classMapping = [], DomainIdentityHelper $identityHelper = null) - { - $this->em = $em; - $this->classMapping = $classMapping; - $this->identityHelper = $identityHelper ?? new DomainIdentityHelper(new DomainIdentityMapping($em)); - } - - /** - * @return null|object - */ - public function __invoke(string $class, $id) - { - $class = $this->classMapping[$class] ?? $class; - - if ($this->em->getMetadataFactory()->isTransient($class)) { - return null; - } - - if (!$this->identityHelper->isIdentity($class, $id)) { - return null; - } - - return $this->em->getReference($class, $this->identityHelper->toIdentity($class, $id)); - } -} diff --git a/src/Domain/Infra/InMemory/DomainIdentityMapping.php b/src/Domain/Infra/InMemory/DomainIdentityMapping.php index 0c2065df..a39f7f67 100644 --- a/src/Domain/Infra/InMemory/DomainIdentityMapping.php +++ b/src/Domain/Infra/InMemory/DomainIdentityMapping.php @@ -27,7 +27,11 @@ public function getIdentifierFieldNames(string $class): array throw InvalidClassException::create($class); } - return (array) $this->mapping[$class]; + if (!$fields = (array) $this->mapping[$class]) { + throw new \LogicException(sprintf('No identifier fields available for class "%s".', $class)); + } + + return $fields; } public function getIdentity($object): array diff --git a/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php b/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php index 3f3b26fa..e8f3d84a 100644 --- a/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php +++ b/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php @@ -17,6 +17,8 @@ abstract class AbstractDomainEntityRepositoryTraitTest extends TestCase Entities\TestCompositeEntity::class, Entities\TestDerivedEntity::class, Entities\TestDerivedCompositeEntity::class, + Entities\TestParentEntity::class, + Entities\TestChildEntity::class, ]; protected static $supportsAutoGeneratedIds = true; @@ -374,7 +376,7 @@ final protected function assertEntityCollectionEquals(array $expected, $actual): final protected function assertEntityEquals($expected, $actual): void { - $this->assertInstanceOf(get_class($expected), $actual); + $this->assertSame(get_class($expected), get_class($actual)); if (!$this->equalsEntity($expected, $actual)) { $this->fail(); diff --git a/src/Domain/Tests/Factory/EntityAwareFactoryTest.php b/src/Domain/Tests/Factory/EntityAwareFactoryTest.php index f4e77d8d..8d8364b5 100644 --- a/src/Domain/Tests/Factory/EntityAwareFactoryTest.php +++ b/src/Domain/Tests/Factory/EntityAwareFactoryTest.php @@ -4,6 +4,7 @@ namespace MsgPhp\Domain\Tests\Factory; +use MsgPhp\Domain\DomainIdentityMappingInterface; use MsgPhp\Domain\DomainIdInterface; use MsgPhp\Domain\Exception\InvalidClassException; use MsgPhp\Domain\Factory\{DomainObjectFactoryInterface, EntityAwareFactory}; @@ -36,13 +37,12 @@ protected function setUp(): void return $o; }); - $this->factory = new EntityAwareFactory($innerFactory, ['alias_id' => 'id'], function ($class, $id) { - $o = new \stdClass(); - $o->class = $class; - $o->id = $id; + $identityMapping = $this->createMock(DomainIdentityMappingInterface::class); + $identityMapping->expects($this->any()) + ->method('getIdentifierFieldNames') + ->willReturn(['id_field', 'id_field2']); - return $o; - }); + $this->factory = new EntityAwareFactory($innerFactory, $identityMapping, ['alias_id' => 'id']); } public function testCreate(): void @@ -56,29 +56,9 @@ public function testCreate(): void public function testReference(): void { $this->assertInstanceOf(\stdClass::class, $object = $this->factory->reference('foo', 1)); - $this->assertSame(['class' => 'foo', 'id' => 1], (array) $object); - $this->assertInstanceOf(\stdClass::class, $object = $this->factory->reference('bar', ['id' => 1, 'foo' => '2'])); - $this->assertSame(['class' => 'bar', 'id' => ['id' => 1, 'foo' => '2']], (array) $object); - } - - public function testReferenceWithoutLoader(): void - { - $factory = new EntityAwareFactory($this->createMock(DomainObjectFactoryInterface::class), []); - - $this->expectException(\LogicException::class); - - $factory->reference('foo', 1); - } - - public function testReferenceWithoutResult(): void - { - $factory = new EntityAwareFactory($this->createMock(DomainObjectFactoryInterface::class), [], function ($class, $id) { - return null; - }); - - $this->expectException(\RuntimeException::class); - - $factory->reference('foo', 1); + $this->assertSame(['class' => 'foo', 'context' => ['id_field' => 1, 'id_field2' => null]], (array) $object); + $this->assertInstanceOf(\stdClass::class, $object = $this->factory->reference('foo', ['id_field2' => 2, 'foo' => 'bar'])); + $this->assertSame(['class' => 'foo', 'context' => ['id_field2' => 2, 'foo' => 'bar', 'id_field' => null]], (array) $object); } public function testIdentify(): void diff --git a/src/Domain/Tests/Fixtures/Entities/TestChildEntity.php b/src/Domain/Tests/Fixtures/Entities/TestChildEntity.php new file mode 100644 index 00000000..74398cbf --- /dev/null +++ b/src/Domain/Tests/Fixtures/Entities/TestChildEntity.php @@ -0,0 +1,24 @@ + [null, 'bar'], + ]; + } +} diff --git a/src/Domain/Tests/Fixtures/Entities/TestParentEntity.php b/src/Domain/Tests/Fixtures/Entities/TestParentEntity.php new file mode 100644 index 00000000..a7d92bfc --- /dev/null +++ b/src/Domain/Tests/Fixtures/Entities/TestParentEntity.php @@ -0,0 +1,39 @@ + ['foo'], + 'parentField' => [null, 'foo'], + ]; + } +} diff --git a/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php b/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php new file mode 100644 index 00000000..498cef85 --- /dev/null +++ b/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php @@ -0,0 +1,161 @@ +createMock(EntityAwareFactoryInterface::class); + $innerFactory->expects($this->once()) + ->method('create') + ->with(Entities\TestEntity::class, ['foo' => 'bar']) + ->willReturn($obj = new \stdClass()); + $factory = new EntityAwareFactory($innerFactory, self::$em, ['alias' => Entities\TestEntity::class]); + + $this->assertSame($obj, $factory->create('alias', ['foo' => 'bar'])); + } + + public function testCreateWithDiscriminator(): void + { + $innerFactory = $this->createMock(EntityAwareFactoryInterface::class); + $innerFactory->expects($this->once()) + ->method('create') + ->with(Entities\TestChildEntity::class, ['foo' => 'bar', 'discriminator' => 'child']) + ->willReturn($obj = new \stdClass()); + $factory = new EntityAwareFactory($innerFactory, self::$em); + + $this->assertSame($obj, $factory->create(Entities\TestParentEntity::class, ['foo' => 'bar', 'discriminator' => 'child'])); + } + + public function testCreateWithUnknownClass(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->expectException(InvalidClassException::class); + + $factory->create('foo'); + } + + public function testCreateWithUnknownEntity(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->expectException(InvalidClassException::class); + + $factory->create(\stdClass::class); + } + + public function testReference(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em, ['alias' => Entities\TestEntity::class]); + + $this->assertInstanceOf(Proxy::class, $ref = $factory->reference(Entities\TestEntity::class, $id = $this->createMock(DomainIdInterface::class))); + $this->assertInstanceOf(Entities\TestEntity::class, $ref); + $this->assertSame($id, $ref->getId()); + $this->assertInstanceOf(Proxy::class, $ref = $factory->reference('alias', $id)); + $this->assertInstanceOf(Entities\TestEntity::class, $ref); + $this->assertSame($id, $ref->getId()); + } + + public function testReferenceWithDiscriminator(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->assertInstanceOf(Proxy::class, $ref = $factory->reference(Entities\TestParentEntity::class, ['id' => 'foo', 'discriminator' => 'child'])); + $this->assertInstanceOf(Entities\TestChildEntity::class, $ref); + $this->assertSame('foo', $ref->id); + } + + public function testReferenceWithUnknownClass(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->expectException(InvalidClassException::class); + + $factory->reference('foo', 1); + } + + public function testReferenceWithUnknownEntity(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->expectException(InvalidClassException::class); + + $factory->reference(\stdClass::class, 1); + } + + public function testIdentify(): void + { + $innerFactory = $this->createMock(EntityAwareFactoryInterface::class); + $innerFactory->expects($this->once()) + ->method('identify') + ->with(Entities\TestEntity::class, 1) + ->willReturn($obj = $this->createMock(DomainIdInterface::class)); + $factory = new EntityAwareFactory($innerFactory, self::$em, ['alias' => Entities\TestEntity::class]); + + $this->assertSame($obj, $factory->identify('alias', 1)); + } + + public function testIdentifyWithUnknownClass(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->expectException(InvalidClassException::class); + + $factory->identify('foo', 1); + } + + public function testIdentifyWithUnknownEntity(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->expectException(InvalidClassException::class); + + $factory->identify(\stdClass::class, 1); + } + + public function testNextIdentifier(): void + { + $innerFactory = $this->createMock(EntityAwareFactoryInterface::class); + $innerFactory->expects($this->once()) + ->method('nextIdentifier') + ->with(Entities\TestEntity::class) + ->willReturn($obj = $this->createMock(DomainIdInterface::class)); + $factory = new EntityAwareFactory($innerFactory, self::$em, ['alias' => Entities\TestEntity::class]); + + $this->assertSame($obj, $factory->nextIdentifier('alias')); + } + + public function testNextIdentifierWithUnknownClass(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->expectException(InvalidClassException::class); + + $factory->nextIdentifier('foo'); + } + + public function testNextIdentifierUnknownEntity(): void + { + $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + + $this->expectException(InvalidClassException::class); + + $factory->nextIdentifier(\stdClass::class); + } +} diff --git a/src/Domain/Tests/Infra/Doctrine/EntityReferenceLoaderTest.php b/src/Domain/Tests/Infra/Doctrine/EntityReferenceLoaderTest.php deleted file mode 100644 index 6f4faa1e..00000000 --- a/src/Domain/Tests/Infra/Doctrine/EntityReferenceLoaderTest.php +++ /dev/null @@ -1,74 +0,0 @@ - Entities\TestEntity::class]); - $id = $this->createMock(DomainIdInterface::class); - $id->expects($this->any()) - ->method('isEmpty') - ->willReturn(false); - - $this->assertInstanceOf(Entities\TestEntity::class, $entity = $loader('alias', $id)); - $this->assertSame($id, $entity->getId()); - $this->assertInstanceOf(Entities\TestCompositeEntity::class, $entity = $loader(Entities\TestCompositeEntity::class, ['idA' => $id, 'idB' => 'b'])); - $this->assertSame($id, $entity->idA); - $this->assertSame('b', $entity->idB); - $this->assertInstanceOf(Entities\TestPrimitiveEntity::class, $entity = $loader(Entities\TestPrimitiveEntity::class, ['id' => $id])); - $this->assertSame($id, $entity->id); - } - - public function testInvokeWithEmptyIdentifier(): void - { - $loader = new EntityReferenceLoader(self::$em); - $emptyId = $this->createMock(DomainIdInterface::class); - $emptyId->expects($this->any()) - ->method('isEmpty') - ->willReturn(true); - $id = $this->createMock(DomainIdInterface::class); - $id->expects($this->any()) - ->method('isEmpty') - ->willReturn(false); - - $this->assertNull($loader(\stdClass::class, null)); - $this->assertNull($loader(\stdClass::class, [])); - $this->assertNull($loader(Entities\TestEntity::class, $emptyId)); - $this->assertNull($loader(Entities\TestPrimitiveEntity::class, ['id' => $emptyId])); - $this->assertNull($loader(Entities\TestCompositeEntity::class, ['idA' => $id, 'idB' => $emptyId])); - $this->assertNull($loader(Entities\TestDerivedCompositeEntity::class, ['entity' => Entities\TestPrimitiveEntity::create(['id' => $emptyId]), 'id' => 0])); - } - - public function testInvokeWithInvalidIdentifier(): void - { - $loader = new EntityReferenceLoader(self::$em); - $id = $this->createMock(DomainIdInterface::class); - $id->expects($this->any()) - ->method('isEmpty') - ->willReturn(false); - - $this->assertNull($loader(Entities\TestPrimitiveEntity::class, ['id' => $id, 'foo' => 'bar'])); - } - - public function testInvokeWithInvalidClass(): void - { - $loader = new EntityReferenceLoader(self::$em); - - $this->expectException(\ReflectionException::class); - - $loader('foo', 1); - } -} diff --git a/src/Domain/Tests/Infra/InMemory/DomainIdentityMappingTest.php b/src/Domain/Tests/Infra/InMemory/DomainIdentityMappingTest.php index 5fd86709..eb71c895 100644 --- a/src/Domain/Tests/Infra/InMemory/DomainIdentityMappingTest.php +++ b/src/Domain/Tests/Infra/InMemory/DomainIdentityMappingTest.php @@ -11,7 +11,7 @@ final class DomainIdentityMappingTest extends AbstractDomainIdentityMappingTest { - public function testCreateCastsMapping(): void + public function testGetIdentifierFieldNamesCastsMapping(): void { $mapping = new DomainIdentityMapping(['foo' => 'a', 'bar' => ['b']]); @@ -19,6 +19,15 @@ public function testCreateCastsMapping(): void $this->assertSame(['b'], $mapping->getIdentifierFieldNames('bar')); } + public function testGetIdentifierFieldNamesWithEmptyMapping(): void + { + $mapping = new DomainIdentityMapping(['foo' => []]); + + $this->expectException(\LogicException::class); + + $mapping->getIdentifierFieldNames('foo'); + } + protected static function createMapping(): DomainIdentityMappingInterface { return new DomainIdentityMapping([ diff --git a/src/Eav/README.md b/src/Eav/README.md index 926a6f53..eb1423ff 100644 --- a/src/Eav/README.md +++ b/src/Eav/README.md @@ -17,7 +17,7 @@ composer require msgphp/eav ## Features -- Doctrine persistence +- Doctrine persistence (with built-in discriminator support) - Standard supported attribute value types: `bool`, `int`, `float`, `string`, `\DateTimeInterface` and `null` ## Documentation diff --git a/src/EavBundle/README.md b/src/EavBundle/README.md index de52b4fa..0db9a8f1 100644 --- a/src/EavBundle/README.md +++ b/src/EavBundle/README.md @@ -17,7 +17,7 @@ composer require msgphp/eav-bundle ## Features - Symfony 3.4 / 4.0 ready -- Doctrine persistence +- Doctrine persistence (with built-in discriminator support) - Standard supported attribute value types: `bool`, `int`, `float`, `string`, `\DateTimeInterface` and `null` ## Configuration diff --git a/src/User/Infra/Doctrine/Event/UsernameListener.php b/src/User/Infra/Doctrine/Event/UsernameListener.php index 0ce6adc1..153b5030 100644 --- a/src/User/Infra/Doctrine/Event/UsernameListener.php +++ b/src/User/Infra/Doctrine/Event/UsernameListener.php @@ -4,11 +4,12 @@ namespace MsgPhp\User\Infra\Doctrine\Event; +use Doctrine\Common\Util\ClassUtils; +use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; use Doctrine\ORM\Event\PostFlushEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; -use Doctrine\ORM\Mapping\ClassMetadataInfo; use MsgPhp\User\Entity\Username; /** @@ -42,7 +43,7 @@ public function add($entity, LifecycleEventArgs $event): void { $em = $event->getEntityManager(); - foreach ($this->getUsernames($entity, $event->getEntityManager()->getClassMetadata(get_class($entity))) as $username) { + foreach ($this->getUsernames($entity, $em) as $username) { $em->persist($username); } } @@ -52,11 +53,7 @@ public function add($entity, LifecycleEventArgs $event): void */ public function update($entity, PreUpdateEventArgs $event): void { - if (!isset($this->mapping[$class = get_class($entity)])) { - throw new \LogicException(sprintf('No username mapping available for entity "%s".', $class)); - } - - foreach ($this->mapping[$class] as $mapping) { + foreach ($this->getMapping($entity, $event->getEntityManager()) as $mapping) { if (!$event->hasChangedField($mapping['field'])) { continue; } @@ -70,13 +67,10 @@ public function update($entity, PreUpdateEventArgs $event): void */ public function remove($entity, LifecycleEventArgs $event): void { - if (!isset($this->mapping[$class = get_class($entity)])) { - throw new \LogicException(sprintf('No username mapping available for entity "%s".', $class)); - } - - $metadata = $event->getEntityManager()->getClassMetadata($class); + $em = $event->getEntityManager(); + $metadata = $em->getClassMetadata(get_class($entity)); - foreach ($this->mapping[$class] as $mapping) { + foreach ($this->getMapping($entity, $em) as $mapping) { if (!isset($mapping['mapped_by'])) { continue; } @@ -104,9 +98,9 @@ public function postFlush(PostFlushEventArgs $event): void } } - $this->updateUsernames = []; - $em->flush(); + + $this->updateUsernames = []; } /** @@ -114,16 +108,29 @@ public function postFlush(PostFlushEventArgs $event): void * * @return Username[] */ - private function getUsernames($entity, ClassMetadataInfo $metadata): iterable + private function getUsernames($entity, EntityManagerInterface $em): iterable { - if (!isset($this->mapping[$class = get_class($entity)])) { - throw new \LogicException(sprintf('No username mapping available for entity "%s".', $class)); - } + $metadata = $em->getClassMetadata(get_class($entity)); - foreach ($this->mapping[$class] as $mapping) { + foreach ($this->getMapping($entity, $em) as $mapping) { $user = isset($mapping['mapped_by']) ? $metadata->getFieldValue($entity, $mapping['mapped_by']) : $entity; yield new Username($user, $metadata->getFieldValue($entity, $mapping['field'])); } } + + private function getMapping($entity, EntityManagerInterface $em): array + { + if (isset($this->mapping[$class = ClassUtils::getClass($entity)])) { + return $this->mapping[$class]; + } + + foreach ($em->getClassMetadata($class)->parentClasses as $parent) { + if (isset($this->mapping[$parent])) { + return $this->mapping[$parent]; + } + } + + throw new \LogicException(sprintf('No username mapping available for entity "%s".', $class)); + } } diff --git a/src/User/README.md b/src/User/README.md index b5dec05a..49bbf383 100644 --- a/src/User/README.md +++ b/src/User/README.md @@ -16,7 +16,7 @@ composer require msgphp/user ## Features -- Doctrine persistence +- Doctrine persistence (with built-in discriminator support) - Symfony console commands - Symfony security infrastructure - Symfony validators diff --git a/src/UserBundle/README.md b/src/UserBundle/README.md index 8ee23770..f14a87d2 100644 --- a/src/UserBundle/README.md +++ b/src/UserBundle/README.md @@ -17,7 +17,7 @@ composer require msgphp/user-bundle ## Features - Symfony 3.4 / 4.0 ready -- Doctrine persistence +- Doctrine persistence (with built-in discriminator support) - Symfony console commands - Symfony security infrastructure - Symfony validators From b933def19c9b1ca104346c0c50ab81160d4e3633 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 14:41:42 +0100 Subject: [PATCH 09/79] tweak docs --- docs/ddd/factory/entity-aware.md | 14 +++++++++----- docs/infrastructure/doctrine-orm.md | 11 ++++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/ddd/factory/entity-aware.md b/docs/ddd/factory/entity-aware.md index 821c1b96..e36ca12d 100644 --- a/docs/ddd/factory/entity-aware.md +++ b/docs/ddd/factory/entity-aware.md @@ -62,11 +62,15 @@ class MyEntity } } -$factory = new EntityAwareFactory(new DomainObjectFactory(), new DomainIdentityMapping([ - MyEntity::class => 'id', -]), [ - MyEntity::class => DomainId::class, -]); +$factory = new EntityAwareFactory( + new DomainObjectFactory(), + new DomainIdentityMapping([ + MyEntity::class => 'id', + ]), + [ + MyEntity::class => DomainId::class, + ] +); // --- USAGE --- diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index 40968da4..6a4b2988 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -158,7 +158,10 @@ $factory = new EntityAwareFactory( $ref = $factory->reference(MyEntity::class, 1); // no database hit /** @var MyOtherEntity $ref */ -$otherRef = $factory->reference(MyEntity::class, ['id' => 1, 'discriminator' => MyEntity::TYPE_OTHER]); +$otherRef = $factory->reference(MyEntity::class, [ + 'id' => 1, + 'discriminator' => MyEntity::TYPE_OTHER +]); /** @var MyOtherEntity $otherEntity */ $otherEntity = $factory->create(MyEntity::class, [ @@ -200,8 +203,10 @@ Type::addType(DomainIdType::NAME, DomainIdType::class); /** @var EntityManagerInterface $em */ $em = ...; -$em->getConfiguration()->addCustomHydrationMode(ScalarHydrator::NAME, ScalarHydrator::class); -$em->getConfiguration()->addCustomHydrationMode(SingleScalarHydrator::NAME, SingleScalarHydrator::class); +$config = $em->getConfiguration(); + +$config->addCustomHydrationMode(ScalarHydrator::NAME, ScalarHydrator::class); +$config->addCustomHydrationMode(SingleScalarHydrator::NAME, SingleScalarHydrator::class); // --- USAGE --- From ae433e6752115cad96dadee1bd51da9fecaf4361 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 14:44:39 +0100 Subject: [PATCH 10/79] doc fixes --- docs/ddd/factory/entity-aware.md | 2 +- docs/infrastructure/doctrine-orm.md | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/ddd/factory/entity-aware.md b/docs/ddd/factory/entity-aware.md index e36ca12d..4f7f980f 100644 --- a/docs/ddd/factory/entity-aware.md +++ b/docs/ddd/factory/entity-aware.md @@ -74,7 +74,7 @@ $factory = new EntityAwareFactory( // --- USAGE --- -/** @var MyEntity $entity */ +/** @var MyEntity $ref */ $ref = $factory->reference(MyEntity::class, new DomainId('1')); /** @var DomainId $id */ diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index 6a4b2988..f02c172e 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -148,7 +148,10 @@ class MyOtherEntity extends MyEntity /** @var EntityManagerInterface $em */ $em = ...; $factory = new EntityAwareFactory( - new BaseEntityAwareFactory(new DomainObjectFactory(), new DomainIdentityMapping($em)), + new BaseEntityAwareFactory( + new DomainObjectFactory(), + new DomainIdentityMapping($em) + ), $em ); @@ -157,7 +160,7 @@ $factory = new EntityAwareFactory( /** @var MyEntity $ref */ $ref = $factory->reference(MyEntity::class, 1); // no database hit -/** @var MyOtherEntity $ref */ +/** @var MyOtherEntity $otherRef */ $otherRef = $factory->reference(MyEntity::class, [ 'id' => 1, 'discriminator' => MyEntity::TYPE_OTHER From 856371820721d1f2db2139e37ef6538b4368af18 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 14:57:28 +0100 Subject: [PATCH 11/79] tweak docs --- docs/infrastructure/doctrine-dbal.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/infrastructure/doctrine-dbal.md b/docs/infrastructure/doctrine-dbal.md index eb856251..fe903d86 100644 --- a/docs/infrastructure/doctrine-dbal.md +++ b/docs/infrastructure/doctrine-dbal.md @@ -10,16 +10,16 @@ A translation between the database type and a [identifier](../ddd/identifiers.md `MsgPhp\Domain\Infra\Doctrine\DomainIdType`. Its purpose is to abstract the underlying data type of the identifier value. -The design is based on [late static bindings], due the design of the Doctrine type system itself. Besides extending from -the default [`Type`][api-type] implementation it can be used as a base type for custom identifiers (which in turn require custom -types). +The design is based on [late static bindings], due the design of the Doctrine type system itself. It extends from the +default [`Type`][api-type] implementation and can either be used generic or as a base class for custom identifier +(which in turn require custom types). - `static setClass(string $class): void` - `$class`: A sub class of `DomainIdInterface` to use as PHP value. If not set the [default identifier](../ddd/identifiers.md#msgphpdomaindomainid) - object is used. + is used. - `static getClass(): string` - `static setDataType(string $type): void` - - `$type`: A doctrine type name to use as underlying data type. If not set it defaults to `Type::INTEGER`. + - `$type`: A doctrine type name to use as underlying data type. If not set `Type::INTEGER` is used. - `static getDataType(): string` ### Basic example From 3d028177251a7e80583f6f7997840cdf4fd3d8e7 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 14:58:19 +0100 Subject: [PATCH 12/79] fix docs --- docs/infrastructure/doctrine-dbal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/infrastructure/doctrine-dbal.md b/docs/infrastructure/doctrine-dbal.md index fe903d86..0eccb642 100644 --- a/docs/infrastructure/doctrine-dbal.md +++ b/docs/infrastructure/doctrine-dbal.md @@ -11,7 +11,7 @@ A translation between the database type and a [identifier](../ddd/identifiers.md value. The design is based on [late static bindings], due the design of the Doctrine type system itself. It extends from the -default [`Type`][api-type] implementation and can either be used generic or as a base class for custom identifier +default [`Type`][api-type] implementation and can be used either generic or as a base class for custom identifiers (which in turn require custom types). - `static setClass(string $class): void` From 09e22c8802b08d0c698b48739bf9dbec7925a2cd Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 16:03:42 +0100 Subject: [PATCH 13/79] fix doc --- docs/infrastructure/doctrine-orm.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index f02c172e..9429219d 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -64,7 +64,7 @@ use MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait; // --- SETUP --- /** @ORM\Entity */ -class MyCompositeEntity +class MyEntity { /** @ORM\Id @ORM\Column(type="string") */ public $name; @@ -74,18 +74,20 @@ class MyCompositeEntity } -class MyCompositeEntityRepository +class MyEntityRepository { use DomainEntityRepositoryTrait { doFind as public find; doExists as public exists; doSave as public save; } + + private $alias = 'my_entity'; } /** @var EntityManagerInterface $em */ $em = ...; -$repository = new MyCompositeEntityRepository(MyCompositeEntity::class, $em); +$repository = new MyEntityRepository(MyEntity::class, $em); // --- USAGE --- From 5e9f1cc228eb895157320e9b03cb208704e51f2a Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 16:04:30 +0100 Subject: [PATCH 14/79] fix doc --- docs/infrastructure/doctrine-orm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index 9429219d..107f425f 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -94,7 +94,7 @@ $repository = new MyEntityRepository(MyEntity::class, $em); if ($repository->exists($id = ['name' => ..., 'year' => ...])) { $entity = $repository->find($id); } else { - $entity = new MyCompositeEntity(); + $entity = new MyEntity(); $entity->name = ...; $entity->year = ...; From dc2ff7a6daf72ead5b27eb53977d2b5e2a458f2c Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 16:09:42 +0100 Subject: [PATCH 15/79] fix doc --- docs/ddd/collections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ddd/collections.md b/docs/ddd/collections.md index 9abaa5c5..31434b8c 100644 --- a/docs/ddd/collections.md +++ b/docs/ddd/collections.md @@ -69,7 +69,7 @@ Keys are preserved. ### `map(callable $mapper): array` -Returns a map with each collection element as returned by `$mapper`.s +Returns a map with each collection element as returned by `$mapper`. ## Implementations From 8b1a979ada09f1170cf1cdc33a7ac96b19da5ae5 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 4 Mar 2018 17:25:47 +0100 Subject: [PATCH 16/79] fix doc --- docs/infrastructure/doctrine-orm.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index 107f425f..cc3ecfb7 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -20,21 +20,10 @@ the identity mapping from its class metadata. Date: Wed, 7 Mar 2018 10:46:23 +0100 Subject: [PATCH 17/79] [0.2] name user param converter (#99) --- UPGRADE-0.2.md | 13 +++++++++++++ src/User/Infra/Security/UserParamConverter.php | 6 +++--- src/UserBundle/Resources/config/security.php | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/UPGRADE-0.2.md b/UPGRADE-0.2.md index 6819264e..f17a8644 100644 --- a/UPGRADE-0.2.md +++ b/UPGRADE-0.2.md @@ -9,3 +9,16 @@ - Removed `%msgphp.default_data_type%` DI parameter, use `default_id_type` bundle configuration instead - Removed `data_type_mapping` bundle configuration, use `id_type_mapping` instead +- Configured param converter by name + + Before: + + ``` + @ParamConverter("argument", options={"current": true}) + ``` + + After: + + ``` + @ParamConverter("argument", converter="msgphp.current_user") + ``` diff --git a/src/User/Infra/Security/UserParamConverter.php b/src/User/Infra/Security/UserParamConverter.php index dafda890..b5451a43 100644 --- a/src/User/Infra/Security/UserParamConverter.php +++ b/src/User/Infra/Security/UserParamConverter.php @@ -14,6 +14,8 @@ */ final class UserParamConverter implements ParamConverterInterface { + public const NAME = 'msgphp.current_user'; + use TokenStorageAwareTrait; public function apply(Request $request, ParamConverter $configuration): bool @@ -28,9 +30,7 @@ public function apply(Request $request, ParamConverter $configuration): bool public function supports(ParamConverter $configuration): bool { if (User::class == ($class = $configuration->getClass()) || is_subclass_of($class, User::class)) { - $options = $configuration->getOptions(); - - return !empty($options['current']) && ($configuration->isOptional() || $this->isUser()); + return $configuration->isOptional() || $this->isUser(); } return false; diff --git a/src/UserBundle/Resources/config/security.php b/src/UserBundle/Resources/config/security.php index 51196795..8cb478b0 100644 --- a/src/UserBundle/Resources/config/security.php +++ b/src/UserBundle/Resources/config/security.php @@ -32,6 +32,6 @@ if (interface_exists(ParamConverterInterface::class)) { $services->set(Security\UserParamConverter::class) - ->tag('request.param_converter', ['priority' => 100]); + ->tag('request.param_converter', ['converter' => Security\UserParamConverter::NAME, 'priority' => 100]); } }; From 12075e3e440d33ca6864d00690c44d52c843748d Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 7 Mar 2018 11:40:53 +0100 Subject: [PATCH 18/79] prepare 0.2 release (#88) --- UPGRADE-0.2.md | 8 ++++ .../DependencyInjection/BundleHelper.php | 1 - .../Doctrine/DomainEntityRepositoryTrait.php | 10 ++++- .../Event/ObjectFieldMappingListener.php | 31 ++------------ .../InMemory/DomainEntityRepositoryTrait.php | 10 ++++- .../Infra/InMemory/GlobalObjectMemory.php | 2 +- ...bstractDomainEntityRepositoryTraitTest.php | 20 ++++++++- .../Infra/InMemory/GlobalObjectMemoryTest.php | 13 +++--- src/Eav/Entity/Attribute.php | 14 +------ src/Eav/Entity/AttributeValue.php | 11 ++--- .../dist-mapping/Eav.Entity.Attribute.orm.xml | 4 +- .../Eav.Entity.AttributeValue.orm.xml | 2 - src/Eav/Tests/Entity/AttributeTest.php | 19 --------- src/Eav/Tests/Entity/AttributeValueTest.php | 28 ++++++++++--- src/Eav/composer.json | 2 +- src/EavBundle/composer.json | 2 +- src/User/Entity/User.php | 14 +------ src/User/Entity/UserAttributeValue.php | 2 +- src/User/Entity/UserRole.php | 2 +- src/User/Entity/UserSecondaryEmail.php | 2 +- src/User/Entity/Username.php | 2 + .../dist-mapping/User.Entity.User.orm.xml | 4 +- .../Tests/Entity/UserAttributeValueTest.php | 8 +++- src/User/Tests/Entity/UserRoleTest.php | 8 +++- .../Tests/Entity/UserSecondaryEmailTest.php | 41 ++++++++++++------- src/User/Tests/Entity/UserTest.php | 22 ++++++++-- src/User/composer.json | 2 +- .../DependencyInjection/Extension.php | 2 +- src/UserBundle/composer.json | 2 +- 29 files changed, 156 insertions(+), 132 deletions(-) delete mode 100644 src/Eav/Tests/Entity/AttributeTest.php diff --git a/UPGRADE-0.2.md b/UPGRADE-0.2.md index f17a8644..ebde7b73 100644 --- a/UPGRADE-0.2.md +++ b/UPGRADE-0.2.md @@ -1,10 +1,18 @@ # UPGRADE FROM 0.1 to 0.2 +## Eav + +- Decoupled entity identifiers using abstract entities + ## EavBundle - Removed `%msgphp.default_data_type%` DI parameter, use `default_id_type` bundle configuration instead - Removed `data_type_mapping` bundle configuration, use `id_type_mapping` instead +## User + +- Decoupled entity identifiers using abstract entities + ## UserBundle - Removed `%msgphp.default_data_type%` DI parameter, use `default_id_type` bundle configuration instead diff --git a/src/Domain/Infra/DependencyInjection/BundleHelper.php b/src/Domain/Infra/DependencyInjection/BundleHelper.php index 7fea7273..be58aeeb 100644 --- a/src/Domain/Infra/DependencyInjection/BundleHelper.php +++ b/src/Domain/Infra/DependencyInjection/BundleHelper.php @@ -38,7 +38,6 @@ public static function initDomain(ContainerBuilder $container): void $container->register(DoctrineInfra\Event\ObjectFieldMappingListener::class) ->setPublic(false) - ->setArgument('$typeConfig', '%msgphp.doctrine.type_config%') ->addTag('doctrine.event_listener', ['event' => DoctrineOrmEvents::loadClassMetadata]); if (ContainerHelper::hasBundle($container, DoctrineBundle::class)) { diff --git a/src/Domain/Infra/Doctrine/DomainEntityRepositoryTrait.php b/src/Domain/Infra/Doctrine/DomainEntityRepositoryTrait.php index 270dd071..7162c687 100644 --- a/src/Domain/Infra/Doctrine/DomainEntityRepositoryTrait.php +++ b/src/Domain/Infra/Doctrine/DomainEntityRepositoryTrait.php @@ -10,7 +10,7 @@ use Doctrine\ORM\QueryBuilder; use MsgPhp\Domain\{DomainCollectionInterface, DomainIdentityHelper}; use MsgPhp\Domain\Factory\DomainCollectionFactory; -use MsgPhp\Domain\Exception\{DuplicateEntityException, EntityNotFoundException}; +use MsgPhp\Domain\Exception\{DuplicateEntityException, EntityNotFoundException, InvalidClassException}; /** * @author Roland Franssen @@ -108,6 +108,10 @@ private function doExistsByFields(array $fields): bool */ private function doSave($entity): void { + if (!$entity instanceof $this->class) { + throw InvalidClassException::create(get_class($entity)); + } + $this->em->persist($entity); try { @@ -122,6 +126,10 @@ private function doSave($entity): void */ private function doDelete($entity): void { + if (!$entity instanceof $this->class) { + throw InvalidClassException::create(get_class($entity)); + } + $this->em->remove($entity); $this->em->flush(); } diff --git a/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php b/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php index 2d948bab..bd6f39bd 100644 --- a/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php +++ b/src/Domain/Infra/Doctrine/Event/ObjectFieldMappingListener.php @@ -4,7 +4,6 @@ namespace MsgPhp\Domain\Infra\Doctrine\Event; -use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Event\LoadClassMetadataEventArgs; use Doctrine\ORM\Mapping\ClassMetadataFactory; use Doctrine\ORM\Mapping\ClassMetadataInfo; @@ -14,53 +13,29 @@ */ final class ObjectFieldMappingListener { - private $typeConfig; private $mapping; /** @var ClassMetadataFactory|null */ private $metadataFactory; - public function __construct(array $typeConfig, array $mapping) + public function __construct(array $mapping) { - $this->typeConfig = $typeConfig; $this->mapping = $mapping; } public function loadClassMetadata(LoadClassMetadataEventArgs $event): void { - if (!$this->mapping && !$this->typeConfig) { + if (!$this->mapping) { return; } $this->metadataFactory = $event->getEntityManager()->getMetadataFactory(); - $metadata = $event->getClassMetadata(); - if ($this->typeConfig) { - $this->processClassIdentifiers($metadata); - } - - if ($this->mapping) { - $this->processClassFields($metadata); - } + $this->processClassFields($event->getClassMetadata()); $this->metadataFactory = null; } - private function processClassIdentifiers(ClassMetadataInfo $metadata): void - { - if ($metadata->usesIdGenerator()) { - return; - } - - foreach ($metadata->getIdentifierFieldNames() as $field) { - if (!isset($this->typeConfig[$type = $metadata->getTypeOfField($field)]) || !in_array($this->typeConfig[$type]['type'], [Type::INTEGER, Type::BIGINT], true)) { - continue; - } - - $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); - } - } - private function processClassFields(ClassMetadataInfo $metadata, \ReflectionClass $class = null): void { $class = $class ?? $metadata->getReflectionClass(); diff --git a/src/Domain/Infra/InMemory/DomainEntityRepositoryTrait.php b/src/Domain/Infra/InMemory/DomainEntityRepositoryTrait.php index 12507e88..b6f5d10b 100644 --- a/src/Domain/Infra/InMemory/DomainEntityRepositoryTrait.php +++ b/src/Domain/Infra/InMemory/DomainEntityRepositoryTrait.php @@ -6,7 +6,7 @@ use MsgPhp\Domain\{DomainCollectionInterface, DomainIdentityHelper}; use MsgPhp\Domain\Factory\DomainCollectionFactory; -use MsgPhp\Domain\Exception\{DuplicateEntityException, EntityNotFoundException}; +use MsgPhp\Domain\Exception\{DuplicateEntityException, EntityNotFoundException, InvalidClassException}; /** * @author Roland Franssen @@ -107,6 +107,10 @@ private function doExistsByFields(array $fields): bool */ private function doSave($entity): void { + if (!$entity instanceof $this->class) { + throw InvalidClassException::create(get_class($entity)); + } + if ($this->memory->contains($entity)) { return; } @@ -123,6 +127,10 @@ private function doSave($entity): void */ private function doDelete($entity): void { + if (!$entity instanceof $this->class) { + throw InvalidClassException::create(get_class($entity)); + } + $this->memory->remove($entity); } diff --git a/src/Domain/Infra/InMemory/GlobalObjectMemory.php b/src/Domain/Infra/InMemory/GlobalObjectMemory.php index 6586e885..99d119a1 100644 --- a/src/Domain/Infra/InMemory/GlobalObjectMemory.php +++ b/src/Domain/Infra/InMemory/GlobalObjectMemory.php @@ -23,7 +23,7 @@ public function __construct() public function all(string $class): iterable { foreach ($this->storage as $k => $object) { - if ($class === $this->storage->getInfo()) { + if (($stored = $this->storage->getInfo()) === $class || is_subclass_of($stored, $class)) { yield $object; } } diff --git a/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php b/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php index e8f3d84a..30b62360 100644 --- a/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php +++ b/src/Domain/Tests/AbstractDomainEntityRepositoryTraitTest.php @@ -5,7 +5,7 @@ namespace MsgPhp\Domain\Tests; use MsgPhp\Domain\{DomainCollectionInterface, DomainId, DomainIdInterface}; -use MsgPhp\Domain\Exception\{DuplicateEntityException, EntityNotFoundException}; +use MsgPhp\Domain\Exception\{DuplicateEntityException, EntityNotFoundException, InvalidClassException}; use MsgPhp\Domain\Tests\Fixtures\{DomainEntityRepositoryTraitInterface, Entities}; use PHPUnit\Framework\TestCase; @@ -308,6 +308,15 @@ public function testSaveThrowsOnDuplicate(): void $repository->doSave(Entities\TestPrimitiveEntity::create(['id' => new DomainId('999')])); } + public function testSaveWithInvalidClass(): void + { + $repository = static::createRepository(Entities\TestPrimitiveEntity::class); + + $this->expectException(InvalidClassException::class); + + $repository->doSave(Entities\TestEntity::create()); + } + /** * @dataProvider provideEntities */ @@ -324,6 +333,15 @@ public function testDelete(string $class, Entities\BaseTestEntity $entity): void $this->assertFalse($repository->doExists($ids)); } + public function testDeleteWithInvalidClass(): void + { + $repository = static::createRepository(Entities\TestPrimitiveEntity::class); + + $this->expectException(InvalidClassException::class); + + $repository->doDelete(Entities\TestEntity::create()); + } + public function provideEntityTypes(): iterable { foreach (static::$entityTypes as $class) { diff --git a/src/Domain/Tests/Infra/InMemory/GlobalObjectMemoryTest.php b/src/Domain/Tests/Infra/InMemory/GlobalObjectMemoryTest.php index d3d6c604..19cedf31 100644 --- a/src/Domain/Tests/Infra/InMemory/GlobalObjectMemoryTest.php +++ b/src/Domain/Tests/Infra/InMemory/GlobalObjectMemoryTest.php @@ -21,12 +21,11 @@ public function testAll(): void $anonymous = new class() { }; - foreach ([$std1 = new \stdClass(), $std2 = new \stdClass(), $anonymous, $anonymous2 = clone $anonymous] as $object) { + foreach ([$std1 = new \stdClass(), $std2 = new \stdClass(), $anonymous] as $object) { $memory->persist($object); $objects[get_class($object)] = []; } $memory->persist($std1); - $memory->persist($anonymous2); foreach (array_keys($objects) as $class) { foreach ($memory->all($class) as $i => $object) { @@ -35,16 +34,18 @@ public function testAll(): void } $this->assertSame([$std1, $std2], $objects[\stdClass::class]); - $this->assertSame([$anonymous, $anonymous2], $objects[get_class($anonymous)]); + $this->assertSame([$anonymous], $objects[get_class($anonymous)]); $memory->remove($anonymous); - $objects[$class = get_class($anonymous)] = []; - foreach ($memory->all($class = get_class($anonymous2)) as $i => $object) { + $class = get_class($anonymous); + $objects[$class] = []; + + foreach ($memory->all($class) as $i => $object) { $objects[$class][$i] = $object; } - $this->assertSame([$anonymous2], $objects[$class]); + $this->assertSame([], $objects[$class]); } public function testContains(): void diff --git a/src/Eav/Entity/Attribute.php b/src/Eav/Entity/Attribute.php index cfda6243..d440b52c 100644 --- a/src/Eav/Entity/Attribute.php +++ b/src/Eav/Entity/Attribute.php @@ -9,17 +9,7 @@ /** * @author Roland Franssen */ -class Attribute +abstract class Attribute { - private $id; - - public function __construct(AttributeIdInterface $id) - { - $this->id = $id; - } - - public function getId(): AttributeIdInterface - { - return $this->id; - } + abstract public function getId(): AttributeIdInterface; } diff --git a/src/Eav/Entity/AttributeValue.php b/src/Eav/Entity/AttributeValue.php index 2032a6e6..cf6b9c25 100644 --- a/src/Eav/Entity/AttributeValue.php +++ b/src/Eav/Entity/AttributeValue.php @@ -9,9 +9,8 @@ /** * @author Roland Franssen */ -class AttributeValue +abstract class AttributeValue { - private $id; private $attribute; private $boolValue; private $intValue; @@ -22,18 +21,14 @@ class AttributeValue private $value; private $isNull; - public function __construct(AttributeValueIdInterface $id, Attribute $attribute, $value) + public function __construct(Attribute $attribute, $value) { - $this->id = $id; $this->attribute = $attribute; $this->changeValue($value); } - public function getId(): AttributeValueIdInterface - { - return $this->id; - } + abstract public function getId(): AttributeValueIdInterface; public function getAttribute(): Attribute { diff --git a/src/Eav/Infra/Doctrine/Resources/dist-mapping/Eav.Entity.Attribute.orm.xml b/src/Eav/Infra/Doctrine/Resources/dist-mapping/Eav.Entity.Attribute.orm.xml index c0c8abcf..273b4094 100644 --- a/src/Eav/Infra/Doctrine/Resources/dist-mapping/Eav.Entity.Attribute.orm.xml +++ b/src/Eav/Infra/Doctrine/Resources/dist-mapping/Eav.Entity.Attribute.orm.xml @@ -3,8 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> - - - + diff --git a/src/Eav/Infra/Doctrine/Resources/dist-mapping/Eav.Entity.AttributeValue.orm.xml b/src/Eav/Infra/Doctrine/Resources/dist-mapping/Eav.Entity.AttributeValue.orm.xml index bad7d6ce..486fb8b0 100644 --- a/src/Eav/Infra/Doctrine/Resources/dist-mapping/Eav.Entity.AttributeValue.orm.xml +++ b/src/Eav/Infra/Doctrine/Resources/dist-mapping/Eav.Entity.AttributeValue.orm.xml @@ -4,8 +4,6 @@ xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> - - diff --git a/src/Eav/Tests/Entity/AttributeTest.php b/src/Eav/Tests/Entity/AttributeTest.php deleted file mode 100644 index bb94035a..00000000 --- a/src/Eav/Tests/Entity/AttributeTest.php +++ /dev/null @@ -1,19 +0,0 @@ -createMock(AttributeIdInterface::class)); - - $this->assertSame($id, $attribute->getId()); - } -} diff --git a/src/Eav/Tests/Entity/AttributeValueTest.php b/src/Eav/Tests/Entity/AttributeValueTest.php index 04015c32..2ac75772 100644 --- a/src/Eav/Tests/Entity/AttributeValueTest.php +++ b/src/Eav/Tests/Entity/AttributeValueTest.php @@ -16,9 +16,8 @@ public function testCreate(): void $attribute->expects($this->any()) ->method('getId') ->willReturn($this->createMock(AttributeIdInterface::class)); - $attributeValue = new AttributeValue($id = $this->createMock(AttributeValueIdInterface::class), $attribute, 'value'); + $attributeValue = $this->createEntity($this->createMock(AttributeValueIdInterface::class), $attribute, 'value'); - $this->assertSame($id, $attributeValue->getId()); $this->assertSame($attribute, $attributeValue->getAttribute()); $this->assertSame($attribute->getId(), $attributeValue->getAttributeId()); $this->assertSame('value', $attributeValue->getValue()); @@ -30,7 +29,7 @@ public function testCreate(): void */ public function testChangeValue($initialValue, $newValue): void { - $attributeValue = new AttributeValue($this->createMock(AttributeValueIdInterface::class), $this->createMock(Attribute::class), $initialValue); + $attributeValue = $this->createEntity($this->createMock(AttributeValueIdInterface::class), $this->createMock(Attribute::class), $initialValue); $checksum = $attributeValue->getChecksum(); $this->assertSame($initialValue, $attributeValue->getValue()); @@ -98,9 +97,9 @@ public function testUnknownTypes($value, bool $initial): void if ($initial) { $this->expectException(\LogicException::class); - new AttributeValue($this->createMock(AttributeValueIdInterface::class), $this->createMock(Attribute::class), $value); + $this->createEntity($this->createMock(AttributeValueIdInterface::class), $this->createMock(Attribute::class), $value); } else { - $attributeValue = new AttributeValue($this->createMock(AttributeValueIdInterface::class), $this->createMock(Attribute::class), null); + $attributeValue = $this->createEntity($this->createMock(AttributeValueIdInterface::class), $this->createMock(Attribute::class), null); $this->expectException(\LogicException::class); @@ -117,4 +116,23 @@ public function provideUnknownTypeValues(): iterable yield [function (): void {}, true]; yield [function (): string {}, false]; } + + private function createEntity($id, $attribute, $value): AttributeValue + { + return new class($id, $attribute, $value) extends AttributeValue { + private $id; + + public function __construct($id, $attribute, $value) + { + parent::__construct($attribute, $value); + + $this->id = $id; + } + + public function getId(): AttributeValueIdInterface + { + return $this->id; + } + }; + } } diff --git a/src/Eav/composer.json b/src/Eav/composer.json index 32da8d77..4f2267e3 100644 --- a/src/Eav/composer.json +++ b/src/Eav/composer.json @@ -18,7 +18,7 @@ "exclude-from-classmap": ["/Tests/"] }, "require": { - "msgphp/domain": "^0.1|^1.0" + "msgphp/domain": "^0.2|^1.0" }, "require-dev": { "doctrine/orm": "^2.6", diff --git a/src/EavBundle/composer.json b/src/EavBundle/composer.json index a6c25ed4..ecc997a4 100644 --- a/src/EavBundle/composer.json +++ b/src/EavBundle/composer.json @@ -18,7 +18,7 @@ "exclude-from-classmap": ["/Tests/"] }, "require": { - "msgphp/eav": "^0.1|^1.0", + "msgphp/eav": "^0.2|^1.0", "symfony/config": "^3.4|^4.0", "symfony/dependency-injection": "^3.4|^4.0", "symfony/http-kernel": "^3.4|^4.0" diff --git a/src/User/Entity/User.php b/src/User/Entity/User.php index 431792a3..dc2beba7 100644 --- a/src/User/Entity/User.php +++ b/src/User/Entity/User.php @@ -10,19 +10,9 @@ /** * @author Roland Franssen */ -class User +abstract class User { - private $id; - - public function __construct(UserIdInterface $id) - { - $this->id = $id; - } - - public function getId(): UserIdInterface - { - return $this->id; - } + abstract public function getId(): UserIdInterface; /** * @return CredentialInterface diff --git a/src/User/Entity/UserAttributeValue.php b/src/User/Entity/UserAttributeValue.php index 9c040c31..6de0dc5e 100644 --- a/src/User/Entity/UserAttributeValue.php +++ b/src/User/Entity/UserAttributeValue.php @@ -11,7 +11,7 @@ /** * @author Roland Franssen */ -class UserAttributeValue +abstract class UserAttributeValue { use UserField; use AttributeValueField; diff --git a/src/User/Entity/UserRole.php b/src/User/Entity/UserRole.php index 9d5811bd..2efbc467 100644 --- a/src/User/Entity/UserRole.php +++ b/src/User/Entity/UserRole.php @@ -9,7 +9,7 @@ /** * @author Roland Franssen */ -class UserRole +abstract class UserRole { use UserField; diff --git a/src/User/Entity/UserSecondaryEmail.php b/src/User/Entity/UserSecondaryEmail.php index 5ec9f1c9..28cc96e4 100644 --- a/src/User/Entity/UserSecondaryEmail.php +++ b/src/User/Entity/UserSecondaryEmail.php @@ -10,7 +10,7 @@ /** * @author Roland Franssen */ -class UserSecondaryEmail +abstract class UserSecondaryEmail { use UserField; use CanBeConfirmed; diff --git a/src/User/Entity/Username.php b/src/User/Entity/Username.php index 55eca661..4006567f 100644 --- a/src/User/Entity/Username.php +++ b/src/User/Entity/Username.php @@ -8,6 +8,8 @@ /** * @author Roland Franssen + * + * @final */ class Username { diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.User.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.User.orm.xml index f6928f11..28fd6013 100644 --- a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.User.orm.xml +++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.User.orm.xml @@ -3,8 +3,6 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> - - - + diff --git a/src/User/Tests/Entity/UserAttributeValueTest.php b/src/User/Tests/Entity/UserAttributeValueTest.php index f6f674df..b9819263 100644 --- a/src/User/Tests/Entity/UserAttributeValueTest.php +++ b/src/User/Tests/Entity/UserAttributeValueTest.php @@ -12,9 +12,15 @@ final class UserAttributeValueTest extends TestCase { public function testCreate(): void { - $userAttributeValue = new UserAttributeValue($user = $this->createMock(User::class), $attributeValue = $this->createMock(AttributeValue::class)); + $userAttributeValue = $this->createEntity($user = $this->createMock(User::class), $attributeValue = $this->createMock(AttributeValue::class)); $this->assertSame($user, $userAttributeValue->getUser()); $this->assertSame($attributeValue, $userAttributeValue->getAttributeValue()); } + + private function createEntity($user, $attributeValue): UserAttributeValue + { + return new class($user, $attributeValue) extends UserAttributeValue { + }; + } } diff --git a/src/User/Tests/Entity/UserRoleTest.php b/src/User/Tests/Entity/UserRoleTest.php index 55d2ee36..9323b018 100644 --- a/src/User/Tests/Entity/UserRoleTest.php +++ b/src/User/Tests/Entity/UserRoleTest.php @@ -11,9 +11,15 @@ final class UserRoleTest extends TestCase { public function testCreate(): void { - $userRole = new UserRole($user = $this->createMock(User::class), 'ROLE_USER'); + $userRole = $this->createEntity($user = $this->createMock(User::class), 'ROLE_USER'); $this->assertSame($user, $userRole->getUser()); $this->assertSame('ROLE_USER', $userRole->getRole()); } + + private function createEntity($user, $role): UserRole + { + return new class($user, $role) extends UserRole { + }; + } } diff --git a/src/User/Tests/Entity/UserSecondaryEmailTest.php b/src/User/Tests/Entity/UserSecondaryEmailTest.php index 885e8a0b..7757e486 100644 --- a/src/User/Tests/Entity/UserSecondaryEmailTest.php +++ b/src/User/Tests/Entity/UserSecondaryEmailTest.php @@ -11,31 +11,42 @@ final class UserSecondaryEmailTest extends TestCase { public function testCreate(): void { - $userEmail = new UserSecondaryEmail($user = $this->createMock(User::class), 'foo@bar.baz'); - - $this->assertSame($user, $userEmail->getUser()); - $this->assertSame('foo@bar.baz', $userEmail->getEmail()); - $this->assertNotNull($userEmail->getConfirmationToken()); - $this->assertNotSame((new UserSecondaryEmail($this->createMock(User::class), 'foo@bar.baz'))->getConfirmationToken(), $userEmail->getConfirmationToken()); - $this->assertFalse($userEmail->isPendingPrimary()); - $this->assertNull($userEmail->getConfirmedAt()); + $userSecondaryEmail = $this->createEntity($user = $this->createMock(User::class), 'foo@bar.baz', null); + + $this->assertSame($user, $userSecondaryEmail->getUser()); + $this->assertSame('foo@bar.baz', $userSecondaryEmail->getEmail()); + $this->assertNotNull($userSecondaryEmail->getConfirmationToken()); + $this->assertNotSame($userSecondaryEmail->getConfirmationToken(), $this->createEntity($user, 'foo@bar.baz', null)->getConfirmationToken()); + $this->assertFalse($userSecondaryEmail->isPendingPrimary()); + $this->assertNull($userSecondaryEmail->getConfirmedAt()); + } + + public function testCreateWithToken(): void + { + $this->assertSame('token', $this->createEntity($this->createMock(User::class), 'foo@bar.baz', 'token')->getConfirmationToken()); } public function testMarkPendingPrimary(): void { - $userEmail = new UserSecondaryEmail($this->createMock(User::class), 'foo@bar.baz'); - $userEmail->markPendingPrimary(); + $userSecondaryEmail = $this->createEntity($this->createMock(User::class), 'foo@bar.baz', null); + $userSecondaryEmail->markPendingPrimary(); - $this->assertTrue($userEmail->isPendingPrimary()); + $this->assertTrue($userSecondaryEmail->isPendingPrimary()); - $userEmail->markPendingPrimary(false); + $userSecondaryEmail->markPendingPrimary(false); - $this->assertFalse($userEmail->isPendingPrimary()); + $this->assertFalse($userSecondaryEmail->isPendingPrimary()); - $userEmail->confirm(); + $userSecondaryEmail->confirm(); $this->expectException(\LogicException::class); - $userEmail->markPendingPrimary(); + $userSecondaryEmail->markPendingPrimary(); + } + + private function createEntity($user, $email, $token): UserSecondaryEmail + { + return new class($user, $email, $token) extends UserSecondaryEmail { + }; } } diff --git a/src/User/Tests/Entity/UserTest.php b/src/User/Tests/Entity/UserTest.php index 79d9f696..7910bc3d 100644 --- a/src/User/Tests/Entity/UserTest.php +++ b/src/User/Tests/Entity/UserTest.php @@ -10,11 +10,25 @@ final class UserTest extends TestCase { - public function testCreate(): void + public function testGetCredential(): void { - $user = new User($id = $this->createMock(UserIdInterface::class)); + $this->assertInstanceOf(Credential\Anonymous::class, $this->createEntity($this->createMock(UserIdInterface::class))->getCredential()); + } + + private function createEntity($id): User + { + return new class($id) extends User { + private $id; + + public function __construct($id) + { + $this->id = $id; + } - $this->assertSame($id, $user->getId()); - $this->assertInstanceOf(Credential\Anonymous::class, $user->getCredential()); + public function getId(): UserIdInterface + { + return $this->id; + } + }; } } diff --git a/src/User/composer.json b/src/User/composer.json index fbfcccc5..3d097548 100644 --- a/src/User/composer.json +++ b/src/User/composer.json @@ -18,7 +18,7 @@ "exclude-from-classmap": ["/Tests/"] }, "require": { - "msgphp/domain": "^0.1|^1.0" + "msgphp/domain": "^0.2|^1.0" }, "require-dev": { "doctrine/orm": "^2.6", diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index a27a6eef..a4f3fd0c 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -187,7 +187,7 @@ private function prepareDoctrineOrm(array $config, LoaderInterface $loader, Cont $container->removeDefinition(DoctrineInfra\Event\UsernameListener::class); } - ContainerHelper::configureDoctrineOrmRepositories($container, $config['class_mapping'], [ + ContainerHelper::configureDoctrineOrmRepositories($container, [Entity\Username::class => Entity\Username::class] + $config['class_mapping'], [ DoctrineInfra\Repository\UserRepository::class => Entity\User::class, DoctrineInfra\Repository\UsernameRepository::class => Entity\Username::class, DoctrineInfra\Repository\UserAttributeValueRepository::class => Entity\UserAttributeValue::class, diff --git a/src/UserBundle/composer.json b/src/UserBundle/composer.json index fd56c13e..4a8f757a 100644 --- a/src/UserBundle/composer.json +++ b/src/UserBundle/composer.json @@ -18,7 +18,7 @@ "exclude-from-classmap": ["/Tests/"] }, "require": { - "msgphp/user": "^0.1|^1.0", + "msgphp/user": "^0.2|^1.0", "symfony/config": "^3.4|^4.0", "symfony/dependency-injection": "^3.4|^4.0", "symfony/http-kernel": "^3.4|^4.0" From ad3dfdcae9270f797153e1fd77daddd8ca0eba43 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 7 Mar 2018 11:44:12 +0100 Subject: [PATCH 19/79] update readme --- src/Eav/README.md | 2 +- src/EavBundle/README.md | 2 +- src/User/README.md | 2 +- src/UserBundle/README.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Eav/README.md b/src/Eav/README.md index eb1423ff..926a6f53 100644 --- a/src/Eav/README.md +++ b/src/Eav/README.md @@ -17,7 +17,7 @@ composer require msgphp/eav ## Features -- Doctrine persistence (with built-in discriminator support) +- Doctrine persistence - Standard supported attribute value types: `bool`, `int`, `float`, `string`, `\DateTimeInterface` and `null` ## Documentation diff --git a/src/EavBundle/README.md b/src/EavBundle/README.md index 0db9a8f1..de52b4fa 100644 --- a/src/EavBundle/README.md +++ b/src/EavBundle/README.md @@ -17,7 +17,7 @@ composer require msgphp/eav-bundle ## Features - Symfony 3.4 / 4.0 ready -- Doctrine persistence (with built-in discriminator support) +- Doctrine persistence - Standard supported attribute value types: `bool`, `int`, `float`, `string`, `\DateTimeInterface` and `null` ## Configuration diff --git a/src/User/README.md b/src/User/README.md index 49bbf383..b5dec05a 100644 --- a/src/User/README.md +++ b/src/User/README.md @@ -16,7 +16,7 @@ composer require msgphp/user ## Features -- Doctrine persistence (with built-in discriminator support) +- Doctrine persistence - Symfony console commands - Symfony security infrastructure - Symfony validators diff --git a/src/UserBundle/README.md b/src/UserBundle/README.md index f14a87d2..8ee23770 100644 --- a/src/UserBundle/README.md +++ b/src/UserBundle/README.md @@ -17,7 +17,7 @@ composer require msgphp/user-bundle ## Features - Symfony 3.4 / 4.0 ready -- Doctrine persistence (with built-in discriminator support) +- Doctrine persistence - Symfony console commands - Symfony security infrastructure - Symfony validators From c741e0e71afba7c7cf351f41fd0c739d0799cd77 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 7 Mar 2018 12:28:19 +0100 Subject: [PATCH 20/79] fix doctrine entity factory (#100) --- .../Infra/Doctrine/EntityAwareFactory.php | 8 ++++--- .../Infra/Doctrine/EntityAwareFactoryTest.php | 23 ++++++++----------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/Domain/Infra/Doctrine/EntityAwareFactory.php b/src/Domain/Infra/Doctrine/EntityAwareFactory.php index fd9206e3..afdeea8d 100644 --- a/src/Domain/Infra/Doctrine/EntityAwareFactory.php +++ b/src/Domain/Infra/Doctrine/EntityAwareFactory.php @@ -27,11 +27,13 @@ public function __construct(EntityAwareFactoryInterface $factory, EntityManagerI public function create(string $class, array $context = []) { - if (!$this->isManaged($class = $this->classMapping[$class] ?? $class)) { - throw InvalidClassException::create($class); + $class = $this->classMapping[$class] ?? $class; + + if ($this->isManaged($class)) { + $class = $this->getDiscriminatorClass($class, $context); } - return $this->factory->create($this->getDiscriminatorClass($class, $context), $context); + return $this->factory->create($class, $context); } public function reference(string $class, $id) diff --git a/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php b/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php index 498cef85..6ad5c50a 100644 --- a/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php +++ b/src/Domain/Tests/Infra/Doctrine/EntityAwareFactoryTest.php @@ -16,7 +16,7 @@ final class EntityAwareFactoryTest extends TestCase { use EntityManagerTrait; - private $createSchema = false; + private $createSchema = true; public function testCreate(): void { @@ -42,22 +42,17 @@ public function testCreateWithDiscriminator(): void $this->assertSame($obj, $factory->create(Entities\TestParentEntity::class, ['foo' => 'bar', 'discriminator' => 'child'])); } - public function testCreateWithUnknownClass(): void + public function testCreateWithObject(): void { - $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); - - $this->expectException(InvalidClassException::class); - - $factory->create('foo'); - } - - public function testCreateWithUnknownEntity(): void - { - $factory = new EntityAwareFactory($this->createMock(EntityAwareFactoryInterface::class), self::$em); + $innerFactory = $this->createMock(EntityAwareFactoryInterface::class); + $innerFactory->expects($this->once()) + ->method('create') + ->with(\stdClass::class) + ->willReturn($obj = new \stdClass()); - $this->expectException(InvalidClassException::class); + $factory = new EntityAwareFactory($innerFactory, self::$em); - $factory->create(\stdClass::class); + $this->assertSame($obj, $factory->create(\stdClass::class)); } public function testReference(): void From a81c83fbd5e5b8733c4b0155913c848b025169b0 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 7 Mar 2018 16:24:03 +0100 Subject: [PATCH 21/79] build api docs from scratch --- bin/build-docs | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/build-docs b/bin/build-docs index d1c82d40..5f177b91 100755 --- a/bin/build-docs +++ b/bin/build-docs @@ -9,6 +9,7 @@ REV=$(git rev-parse --abbrev-ref --short --verify HEAD) DIR="var/docs" mkdir -p "${DIR}" +rm -rf var/cache/api bin/sami update .sami.dist [[ $? -ne 0 ]] && exit 1 From 4c0af53be7ad6d79cb579729925b578f40dacf9d Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 7 Mar 2018 21:15:51 +0100 Subject: [PATCH 22/79] fix deps --- src/User/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/User/composer.json b/src/User/composer.json index 3d097548..eb36084f 100644 --- a/src/User/composer.json +++ b/src/User/composer.json @@ -22,7 +22,7 @@ }, "require-dev": { "doctrine/orm": "^2.6", - "msgphp/eav": "^0.1", + "msgphp/eav": "^0.2", "sensio/framework-extra-bundle": "^5.1", "symfony/console": "^3.4|^4.0", "symfony/form": "^3.4|^4.0", From 72785aa1e1ab77dba8d40570b57fd4c16656ecad Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Thu, 8 Mar 2018 21:20:01 +0100 Subject: [PATCH 23/79] remove redundunt param converter priority --- src/UserBundle/Resources/config/security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UserBundle/Resources/config/security.php b/src/UserBundle/Resources/config/security.php index 8cb478b0..70d1b843 100644 --- a/src/UserBundle/Resources/config/security.php +++ b/src/UserBundle/Resources/config/security.php @@ -32,6 +32,6 @@ if (interface_exists(ParamConverterInterface::class)) { $services->set(Security\UserParamConverter::class) - ->tag('request.param_converter', ['converter' => Security\UserParamConverter::NAME, 'priority' => 100]); + ->tag('request.param_converter', ['converter' => Security\UserParamConverter::NAME]); } }; From f32651b9ed17082b4f563eea2e388df8687026df Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 13:23:00 +0100 Subject: [PATCH 24/79] add twig infrastructure (#103) --- src/User/Infra/Twig/GlobalVariables.php | 88 +++++++++++++++++++ src/User/composer.json | 3 +- .../DependencyInjection/Extension.php | 16 +++- src/UserBundle/Resources/config/twig.php | 19 ++++ src/UserBundle/composer.json | 1 + 5 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 src/User/Infra/Twig/GlobalVariables.php create mode 100644 src/UserBundle/Resources/config/twig.php diff --git a/src/User/Infra/Twig/GlobalVariables.php b/src/User/Infra/Twig/GlobalVariables.php new file mode 100644 index 00000000..b1a1ec43 --- /dev/null +++ b/src/User/Infra/Twig/GlobalVariables.php @@ -0,0 +1,88 @@ + + * + * @internal + */ +final class GlobalVariables implements ServiceSubscriberInterface +{ + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + public function getUser(): ?User + { + $id = $this->getUserId(); + + return null === $id ? null : $this->getRepository()->find($id); + } + + public function getUserId(): ?UserIdInterface + { + $token = $this->getTokenStorage()->getToken(); + + if (null === $token) { + return null; + } + + $user = $token->getUser(); + + if (!$user instanceof SecurityUser) { + return null; + } + + /** @var UserIdInterface $id */ + $id = $this->getDomainFactory()->identify(User::class, $user->getUsername()); + + return $id; + } + + public static function getSubscribedServices(): array + { + return [ + EntityAwareFactoryInterface::class, + '?'.TokenStorageInterface::class, + '?'.UserRepositoryInterface::class, + ]; + } + + private function getDomainFactory(): EntityAwareFactoryInterface + { + return $this->container->get(EntityAwareFactoryInterface::class); + } + + private function getTokenStorage(): TokenStorageInterface + { + if (!$this->container->has(TokenStorageInterface::class)) { + throw new \LogicException('No token storage available.'); + } + + return $this->container->get(TokenStorageInterface::class); + } + + private function getRepository(): UserRepositoryInterface + { + if (!$this->container->has(UserRepositoryInterface::class)) { + throw new \LogicException('No repository available.'); + } + + return $this->container->get(UserRepositoryInterface::class); + } +} diff --git a/src/User/composer.json b/src/User/composer.json index eb36084f..40ba2bfc 100644 --- a/src/User/composer.json +++ b/src/User/composer.json @@ -30,7 +30,8 @@ "symfony/phpunit-bridge": "^3.4|^4.0", "symfony/security-core": "^3.4|^4.0", "symfony/validator": "^3.4|^4.0", - "symfony/var-dumper": "^3.4|^4.0" + "symfony/var-dumper": "^3.4|^4.0", + "twig/twig": "^2.4" }, "config": { "preferred-install": { diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index a4f3fd0c..8a4db4a8 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -11,9 +11,10 @@ use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\EavBundle\MsgPhpEavBundle; use MsgPhp\User\{Command, CredentialInterface, Entity, Repository, UserIdInterface}; -use MsgPhp\User\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, Security as SecurityInfra, Validator as ValidatorInfra}; +use MsgPhp\User\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, Security as SecurityInfra, Twig as TwigInfra, Validator as ValidatorInfra}; use SimpleBus\SymfonyBridge\SimpleBusCommandBusBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; @@ -26,6 +27,7 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Form\Form; use Symfony\Component\Validator\Validation; +use Twig\Environment as Twig; /** * @author Roland Franssen @@ -103,6 +105,10 @@ public function load(array $configs, ContainerBuilder $container): void ]); } + if (class_exists(Twig::class)) { + $loader->load('twig.php'); + } + if (class_exists(ConsoleEvents::class)) { $loader->load('console.php'); @@ -154,6 +160,14 @@ public function prepend(ContainerBuilder $container): void UserIdInterface::class => DoctrineInfra\Type\UserIdType::class, ]); ContainerHelper::configureDoctrineOrmTargetEntities($container, $config['class_mapping']); + + if (ContainerHelper::hasBundle($container, TwigBundle::class)) { + $container->prependExtensionConfig('twig', [ + 'globals' => [ + 'msgphp_user' => '@'.TwigInfra\GlobalVariables::class, + ], + ]); + } } public function process(ContainerBuilder $container): void diff --git a/src/UserBundle/Resources/config/twig.php b/src/UserBundle/Resources/config/twig.php new file mode 100644 index 00000000..2bae27f3 --- /dev/null +++ b/src/UserBundle/Resources/config/twig.php @@ -0,0 +1,19 @@ +services() + ->defaults() + ->autowire() + ->autoconfigure() + ->private() + + ->set(Twig\GlobalVariables::class) + ; +}; diff --git a/src/UserBundle/composer.json b/src/UserBundle/composer.json index 4a8f757a..ee321169 100644 --- a/src/UserBundle/composer.json +++ b/src/UserBundle/composer.json @@ -31,6 +31,7 @@ "symfony/console": "^3.4|^4.0", "symfony/form": "^3.4|^4.0", "symfony/security-bundle": "^3.4|^4.0", + "symfony/twig-bundle": "^3.4|^4.0", "symfony/validator": "^3.4|^4.0", "symfony/var-dumper": "^3.4|^4.0" }, From 01a80d805821aad682bad8291d93d5732f956d57 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 13:44:45 +0100 Subject: [PATCH 25/79] fix tests --- src/Eav/Tests/Entity/AttributeValueTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Eav/Tests/Entity/AttributeValueTest.php b/src/Eav/Tests/Entity/AttributeValueTest.php index 2ac75772..1e3dfac5 100644 --- a/src/Eav/Tests/Entity/AttributeValueTest.php +++ b/src/Eav/Tests/Entity/AttributeValueTest.php @@ -60,7 +60,10 @@ public function provideAttributeValues(): iterable */ public function testLazyGetValue($value, string $type): void { - $attributeValue = (new \ReflectionClass(AttributeValue::class))->newInstanceWithoutConstructor(); + $attributeValue = $this->getMockBuilder(AttributeValue::class) + ->disableOriginalConstructor() + ->enableProxyingToOriginalMethods() + ->getMockForAbstractClass(); $this->assertNull($attributeValue->getValue()); From 2d3fb7cd719e4185de07b76e72d5ce0ed4aee2e9 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 14:15:21 +0100 Subject: [PATCH 26/79] finalize event construtors --- src/User/Event/Domain/ChangeCredentialEvent.php | 2 +- src/User/Event/Domain/RequestPasswordEvent.php | 2 +- src/User/Event/UserConfirmedEvent.php | 2 +- src/User/Event/UserCreatedEvent.php | 2 +- src/User/Event/UserCredentialChangedEvent.php | 2 +- src/User/Event/UserDeletedEvent.php | 2 +- src/User/Event/UserDisabledEvent.php | 2 +- src/User/Event/UserEnabledEvent.php | 2 +- src/User/Event/UserPasswordRequestedEvent.php | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/User/Event/Domain/ChangeCredentialEvent.php b/src/User/Event/Domain/ChangeCredentialEvent.php index fe59f51f..caab8d57 100644 --- a/src/User/Event/Domain/ChangeCredentialEvent.php +++ b/src/User/Event/Domain/ChangeCredentialEvent.php @@ -13,7 +13,7 @@ class ChangeCredentialEvent implements DomainEventInterface { public $fields; - public function __construct(array $fields) + final public function __construct(array $fields) { $this->fields = $fields; } diff --git a/src/User/Event/Domain/RequestPasswordEvent.php b/src/User/Event/Domain/RequestPasswordEvent.php index bbf82808..a78a5a84 100644 --- a/src/User/Event/Domain/RequestPasswordEvent.php +++ b/src/User/Event/Domain/RequestPasswordEvent.php @@ -13,7 +13,7 @@ class RequestPasswordEvent implements DomainEventInterface { public $token; - public function __construct(string $token = null) + final public function __construct(string $token = null) { $this->token = $token; } diff --git a/src/User/Event/UserConfirmedEvent.php b/src/User/Event/UserConfirmedEvent.php index d9f7e165..110e06cd 100644 --- a/src/User/Event/UserConfirmedEvent.php +++ b/src/User/Event/UserConfirmedEvent.php @@ -13,7 +13,7 @@ class UserConfirmedEvent { public $user; - public function __construct(User $user) + final public function __construct(User $user) { $this->user = $user; } diff --git a/src/User/Event/UserCreatedEvent.php b/src/User/Event/UserCreatedEvent.php index 46c1d5ca..1a52d211 100644 --- a/src/User/Event/UserCreatedEvent.php +++ b/src/User/Event/UserCreatedEvent.php @@ -13,7 +13,7 @@ class UserCreatedEvent { public $user; - public function __construct(User $user) + final public function __construct(User $user) { $this->user = $user; } diff --git a/src/User/Event/UserCredentialChangedEvent.php b/src/User/Event/UserCredentialChangedEvent.php index 5c19f5a5..e993bf67 100644 --- a/src/User/Event/UserCredentialChangedEvent.php +++ b/src/User/Event/UserCredentialChangedEvent.php @@ -16,7 +16,7 @@ class UserCredentialChangedEvent public $oldCredential; public $newCredential; - public function __construct(User $user, CredentialInterface $oldCredential, CredentialInterface $newCredential) + final public function __construct(User $user, CredentialInterface $oldCredential, CredentialInterface $newCredential) { $this->user = $user; $this->oldCredential = $oldCredential; diff --git a/src/User/Event/UserDeletedEvent.php b/src/User/Event/UserDeletedEvent.php index 7adb8a6f..795f38fc 100644 --- a/src/User/Event/UserDeletedEvent.php +++ b/src/User/Event/UserDeletedEvent.php @@ -13,7 +13,7 @@ class UserDeletedEvent { public $user; - public function __construct(User $user) + final public function __construct(User $user) { $this->user = $user; } diff --git a/src/User/Event/UserDisabledEvent.php b/src/User/Event/UserDisabledEvent.php index 9df4594c..0cc66baf 100644 --- a/src/User/Event/UserDisabledEvent.php +++ b/src/User/Event/UserDisabledEvent.php @@ -13,7 +13,7 @@ class UserDisabledEvent { public $user; - public function __construct(User $user) + final public function __construct(User $user) { $this->user = $user; } diff --git a/src/User/Event/UserEnabledEvent.php b/src/User/Event/UserEnabledEvent.php index c51e9575..a2d2e941 100644 --- a/src/User/Event/UserEnabledEvent.php +++ b/src/User/Event/UserEnabledEvent.php @@ -13,7 +13,7 @@ class UserEnabledEvent { public $user; - public function __construct(User $user) + final public function __construct(User $user) { $this->user = $user; } diff --git a/src/User/Event/UserPasswordRequestedEvent.php b/src/User/Event/UserPasswordRequestedEvent.php index d5404065..002a8f2e 100644 --- a/src/User/Event/UserPasswordRequestedEvent.php +++ b/src/User/Event/UserPasswordRequestedEvent.php @@ -13,7 +13,7 @@ class UserPasswordRequestedEvent { public $user; - public function __construct(User $user) + final public function __construct(User $user) { $this->user = $user; } From 203aaa9559ab042cb6f961dfd73f0730f27288c0 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 14:17:39 +0100 Subject: [PATCH 27/79] finalize command constructors --- src/User/Command/ChangeUserCredentialCommand.php | 2 +- src/User/Command/ConfirmUserCommand.php | 2 +- src/User/Command/CreateUserCommand.php | 2 +- src/User/Command/DeleteUserCommand.php | 2 +- src/User/Command/DisableUserCommand.php | 2 +- src/User/Command/EnableUserCommand.php | 2 +- src/User/Command/RequestUserPasswordCommand.php | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/User/Command/ChangeUserCredentialCommand.php b/src/User/Command/ChangeUserCredentialCommand.php index 705bc9c6..12b1f78d 100644 --- a/src/User/Command/ChangeUserCredentialCommand.php +++ b/src/User/Command/ChangeUserCredentialCommand.php @@ -12,7 +12,7 @@ class ChangeUserCredentialCommand public $userId; public $context; - public function __construct($userId, array $context) + final public function __construct($userId, array $context) { $this->userId = $userId; $this->context = $context; diff --git a/src/User/Command/ConfirmUserCommand.php b/src/User/Command/ConfirmUserCommand.php index 97925cdb..4d5f56d9 100644 --- a/src/User/Command/ConfirmUserCommand.php +++ b/src/User/Command/ConfirmUserCommand.php @@ -11,7 +11,7 @@ class ConfirmUserCommand { public $userId; - public function __construct($userId) + final public function __construct($userId) { $this->userId = $userId; } diff --git a/src/User/Command/CreateUserCommand.php b/src/User/Command/CreateUserCommand.php index 49c4dc9e..12586663 100644 --- a/src/User/Command/CreateUserCommand.php +++ b/src/User/Command/CreateUserCommand.php @@ -11,7 +11,7 @@ class CreateUserCommand { public $context; - public function __construct(array $context) + final public function __construct(array $context) { $this->context = $context; } diff --git a/src/User/Command/DeleteUserCommand.php b/src/User/Command/DeleteUserCommand.php index 9abd625d..895285e6 100644 --- a/src/User/Command/DeleteUserCommand.php +++ b/src/User/Command/DeleteUserCommand.php @@ -11,7 +11,7 @@ class DeleteUserCommand { public $userId; - public function __construct($userId) + final public function __construct($userId) { $this->userId = $userId; } diff --git a/src/User/Command/DisableUserCommand.php b/src/User/Command/DisableUserCommand.php index 0b450883..0ef49474 100644 --- a/src/User/Command/DisableUserCommand.php +++ b/src/User/Command/DisableUserCommand.php @@ -11,7 +11,7 @@ class DisableUserCommand { public $userId; - public function __construct($userId) + final public function __construct($userId) { $this->userId = $userId; } diff --git a/src/User/Command/EnableUserCommand.php b/src/User/Command/EnableUserCommand.php index f30f5c41..092e603c 100644 --- a/src/User/Command/EnableUserCommand.php +++ b/src/User/Command/EnableUserCommand.php @@ -11,7 +11,7 @@ class EnableUserCommand { public $userId; - public function __construct($userId) + final public function __construct($userId) { $this->userId = $userId; } diff --git a/src/User/Command/RequestUserPasswordCommand.php b/src/User/Command/RequestUserPasswordCommand.php index 38df2cc0..af2e3015 100644 --- a/src/User/Command/RequestUserPasswordCommand.php +++ b/src/User/Command/RequestUserPasswordCommand.php @@ -12,7 +12,7 @@ class RequestUserPasswordCommand public $userId; public $token; - public function __construct($userId, string $token = null) + final public function __construct($userId, string $token = null) { $this->userId = $userId; $this->token = $token; From d43a75209bb4946af89f15e1977432237d043a76 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 14:37:21 +0100 Subject: [PATCH 28/79] update user email terminology (#104) --- src/User/Command/CreateUserEmailCommand.php | 22 ++++++++ src/User/Command/DeleteUserEmailCommand.php | 20 +++++++ .../Handler/CreateUserEmailHandler.php | 41 +++++++++++++++ .../Handler/DeleteUserEmailHandler.php | 42 +++++++++++++++ src/User/Entity/Fields/EmailsField.php | 23 ++++++++ .../{UserSecondaryEmail.php => UserEmail.php} | 17 +----- src/User/Event/UserEmailCreatedEvent.php | 20 +++++++ src/User/Event/UserEmailDeletedEvent.php | 20 +++++++ .../Infra/Doctrine/EntityFieldsMapping.php | 9 +++- ...Repository.php => UserEmailRepository.php} | 29 +++++------ ....orm.xml => User.Entity.UserEmail.orm.xml} | 3 +- src/User/README.md | 2 +- ...e.php => UserEmailRepositoryInterface.php} | 18 +++---- .../Tests/Entity/Fields/EmailsFieldTest.php | 36 +++++++++++++ src/User/Tests/Entity/UserEmailTest.php | 33 ++++++++++++ .../Tests/Entity/UserSecondaryEmailTest.php | 52 ------------------- .../DependencyInjection/Configuration.php | 9 +++- .../DependencyInjection/Extension.php | 6 ++- src/UserBundle/README.md | 2 +- 19 files changed, 302 insertions(+), 102 deletions(-) create mode 100644 src/User/Command/CreateUserEmailCommand.php create mode 100644 src/User/Command/DeleteUserEmailCommand.php create mode 100644 src/User/Command/Handler/CreateUserEmailHandler.php create mode 100644 src/User/Command/Handler/DeleteUserEmailHandler.php create mode 100644 src/User/Entity/Fields/EmailsField.php rename src/User/Entity/{UserSecondaryEmail.php => UserEmail.php} (56%) create mode 100644 src/User/Event/UserEmailCreatedEvent.php create mode 100644 src/User/Event/UserEmailDeletedEvent.php rename src/User/Infra/Doctrine/Repository/{UserSecondaryEmailRepository.php => UserEmailRepository.php} (57%) rename src/User/Infra/Doctrine/Resources/dist-mapping/{User.Entity.UserSecondaryEmail.orm.xml => User.Entity.UserEmail.orm.xml} (87%) rename src/User/Repository/{UserSecondaryEmailRepositoryInterface.php => UserEmailRepositoryInterface.php} (53%) create mode 100644 src/User/Tests/Entity/Fields/EmailsFieldTest.php create mode 100644 src/User/Tests/Entity/UserEmailTest.php delete mode 100644 src/User/Tests/Entity/UserSecondaryEmailTest.php diff --git a/src/User/Command/CreateUserEmailCommand.php b/src/User/Command/CreateUserEmailCommand.php new file mode 100644 index 00000000..68af0d0c --- /dev/null +++ b/src/User/Command/CreateUserEmailCommand.php @@ -0,0 +1,22 @@ + + */ +class CreateUserEmailCommand +{ + public $userId; + public $email; + public $context; + + final public function __construct($userId, string $email, array $context = []) + { + $this->userId = $userId; + $this->email = $email; + $this->context = $context; + } +} diff --git a/src/User/Command/DeleteUserEmailCommand.php b/src/User/Command/DeleteUserEmailCommand.php new file mode 100644 index 00000000..ae5faeca --- /dev/null +++ b/src/User/Command/DeleteUserEmailCommand.php @@ -0,0 +1,20 @@ + + */ +class DeleteUserEmailCommand +{ + public $userId; + public $email; + + final public function __construct($userId, string $email) + { + $this->userId = $userId; + $this->email = $email; + } +} diff --git a/src/User/Command/Handler/CreateUserEmailHandler.php b/src/User/Command/Handler/CreateUserEmailHandler.php new file mode 100644 index 00000000..a01243d9 --- /dev/null +++ b/src/User/Command/Handler/CreateUserEmailHandler.php @@ -0,0 +1,41 @@ + + */ +final class CreateUserEmailHandler +{ + use MessageDispatchingTrait; + + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserEmailRepositoryInterface $repository) + { + $this->factory = $factory; + $this->bus = $bus; + $this->repository = $repository; + } + + public function __invoke(CreateUserEmailCommand $command): void + { + $userId = $this->factory->identify(User::class, $command->userId); + $userEmail = $this->factory->create(UserEmail::class, [ + 'user' => $this->factory->reference(User::class, $userId), + 'email' => $command->email, + ] + $command->context); + + $this->repository->save($userEmail); + $this->dispatch(UserEmailCreatedEvent::class, [$userEmail]); + } +} diff --git a/src/User/Command/Handler/DeleteUserEmailHandler.php b/src/User/Command/Handler/DeleteUserEmailHandler.php new file mode 100644 index 00000000..843056b2 --- /dev/null +++ b/src/User/Command/Handler/DeleteUserEmailHandler.php @@ -0,0 +1,42 @@ + + */ +final class DeleteUserEmailHandler +{ + use MessageDispatchingTrait; + + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserEmailRepositoryInterface $repository) + { + $this->factory = $factory; + $this->bus = $bus; + $this->repository = $repository; + } + + public function __invoke(DeleteUserEmailCommand $command): void + { + try { + $userEmail = $this->repository->find($this->factory->identify(User::class, $command->userId), $command->email); + } catch (EntityNotFoundException $e) { + return; + } + + $this->repository->delete($userEmail); + $this->dispatch(UserEmailDeletedEvent::class, [$userEmail]); + } +} diff --git a/src/User/Entity/Fields/EmailsField.php b/src/User/Entity/Fields/EmailsField.php new file mode 100644 index 00000000..fc1d0983 --- /dev/null +++ b/src/User/Entity/Fields/EmailsField.php @@ -0,0 +1,23 @@ + + */ +trait EmailsField +{ + /** @var UserEmail[] */ + private $emails = []; + + public function getEmails(): DomainCollectionInterface + { + return $this->emails instanceof DomainCollectionInterface ? $this->emails : DomainCollectionFactory::create($this->emails); + } +} diff --git a/src/User/Entity/UserSecondaryEmail.php b/src/User/Entity/UserEmail.php similarity index 56% rename from src/User/Entity/UserSecondaryEmail.php rename to src/User/Entity/UserEmail.php index 28cc96e4..3f37f3ac 100644 --- a/src/User/Entity/UserSecondaryEmail.php +++ b/src/User/Entity/UserEmail.php @@ -10,13 +10,12 @@ /** * @author Roland Franssen */ -abstract class UserSecondaryEmail +abstract class UserEmail { use UserField; use CanBeConfirmed; private $email; - private $pendingPrimary = false; public function __construct(User $user, string $email, string $token = null) { @@ -29,18 +28,4 @@ public function getEmail(): string { return $this->email; } - - public function isPendingPrimary(): bool - { - return $this->pendingPrimary; - } - - public function markPendingPrimary(bool $flag = true): void - { - if ($flag && $this->confirmedAt) { - throw new \LogicException('Cannot mark user secondary e-mail a pending primary as it\'s already confirmed.'); - } - - $this->pendingPrimary = $flag; - } } diff --git a/src/User/Event/UserEmailCreatedEvent.php b/src/User/Event/UserEmailCreatedEvent.php new file mode 100644 index 00000000..5b18dd6a --- /dev/null +++ b/src/User/Event/UserEmailCreatedEvent.php @@ -0,0 +1,20 @@ + + */ +class UserEmailCreatedEvent +{ + public $userEmail; + + final public function __construct(UserEmail $userEmail) + { + $this->userEmail = $userEmail; + } +} diff --git a/src/User/Event/UserEmailDeletedEvent.php b/src/User/Event/UserEmailDeletedEvent.php new file mode 100644 index 00000000..293b3460 --- /dev/null +++ b/src/User/Event/UserEmailDeletedEvent.php @@ -0,0 +1,20 @@ + + */ +class UserEmailDeletedEvent +{ + public $userEmail; + + final public function __construct(UserEmail $userEmail) + { + $this->userEmail = $userEmail; + } +} diff --git a/src/User/Infra/Doctrine/EntityFieldsMapping.php b/src/User/Infra/Doctrine/EntityFieldsMapping.php index 1ece94fc..f53a52e7 100644 --- a/src/User/Infra/Doctrine/EntityFieldsMapping.php +++ b/src/User/Infra/Doctrine/EntityFieldsMapping.php @@ -5,7 +5,7 @@ namespace MsgPhp\User\Infra\Doctrine; use MsgPhp\Domain\Infra\Doctrine\ObjectFieldMappingProviderInterface; -use MsgPhp\User\Entity\{Credential, Features, Fields, User}; +use MsgPhp\User\Entity\{Credential, Features, Fields, User, UserEmail}; /** * @author Roland Franssen @@ -48,6 +48,13 @@ public static function getObjectFieldMapping(): array 'nullable' => true, ], ], + Fields\EmailsField::class => [ + 'emails' => [ + 'type' => self::TYPE_ONE_TO_MANY, + 'targetEntity' => UserEmail::class, + 'mappedBy' => 'user', + ], + ], Fields\UserField::class => [ 'user' => [ 'type' => self::TYPE_MANY_TO_ONE, diff --git a/src/User/Infra/Doctrine/Repository/UserSecondaryEmailRepository.php b/src/User/Infra/Doctrine/Repository/UserEmailRepository.php similarity index 57% rename from src/User/Infra/Doctrine/Repository/UserSecondaryEmailRepository.php rename to src/User/Infra/Doctrine/Repository/UserEmailRepository.php index cbd6e0bf..1de5c795 100644 --- a/src/User/Infra/Doctrine/Repository/UserSecondaryEmailRepository.php +++ b/src/User/Infra/Doctrine/Repository/UserEmailRepository.php @@ -6,43 +6,38 @@ use MsgPhp\Domain\DomainCollectionInterface; use MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait; -use MsgPhp\User\Entity\UserSecondaryEmail; -use MsgPhp\User\Repository\UserSecondaryEmailRepositoryInterface; +use MsgPhp\User\Entity\UserEmail; +use MsgPhp\User\Repository\UserEmailRepositoryInterface; use MsgPhp\User\UserIdInterface; /** * @author Roland Franssen */ -final class UserSecondaryEmailRepository implements UserSecondaryEmailRepositoryInterface +final class UserEmailRepository implements UserEmailRepositoryInterface { use DomainEntityRepositoryTrait; - private $alias = 'user_secondary_email'; + private $alias = 'user_email'; /** - * @return DomainCollectionInterface|UserSecondaryEmail[] + * @return DomainCollectionInterface|UserEmail[] */ public function findAllByUserId(UserIdInterface $userId, int $offset = 0, int $limit = 0): DomainCollectionInterface { return $this->doFindAllByFields(['user' => $userId], $offset, $limit); } - public function find(UserIdInterface $userId, string $email): UserSecondaryEmail + public function find(UserIdInterface $userId, string $email): UserEmail { return $this->doFind(['user' => $userId, 'email' => $email]); } - public function findPendingPrimary(UserIdInterface $userId): UserSecondaryEmail - { - return $this->doFindByFields(['user' => $userId, 'pendingPrimary' => true]); - } - - public function findByEmail(string $email): UserSecondaryEmail + public function findByEmail(string $email): UserEmail { return $this->doFindByFields(['email' => $email]); } - public function findByConfirmationToken(string $token): UserSecondaryEmail + public function findByConfirmationToken(string $token): UserEmail { return $this->doFindByFields(['confirmationToken' => $token]); } @@ -52,13 +47,13 @@ public function exists(UserIdInterface $userId, string $email): bool return $this->doExists(['user' => $userId, 'email' => $email]); } - public function save(UserSecondaryEmail $userSecondaryEmail): void + public function save(UserEmail $userEmail): void { - $this->doSave($userSecondaryEmail); + $this->doSave($userEmail); } - public function delete(UserSecondaryEmail $userSecondaryEmail): void + public function delete(UserEmail $userEmail): void { - $this->doDelete($userSecondaryEmail); + $this->doDelete($userEmail); } } diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserSecondaryEmail.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserEmail.orm.xml similarity index 87% rename from src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserSecondaryEmail.orm.xml rename to src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserEmail.orm.xml index e24efcb1..bb640f98 100644 --- a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserSecondaryEmail.orm.xml +++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserEmail.orm.xml @@ -3,7 +3,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> - + @@ -11,7 +11,6 @@ - diff --git a/src/User/README.md b/src/User/README.md index b5dec05a..50ffba29 100644 --- a/src/User/README.md +++ b/src/User/README.md @@ -22,10 +22,10 @@ composer require msgphp/user - Symfony validators - Credential independent (supports e-mail, nickname, etc.) - Multiple username / credential support +- Primary and secondary user e-mails - Disabled / enabled users - User roles - User attribute values -- User secondary e-mails ## Blog posts diff --git a/src/User/Repository/UserSecondaryEmailRepositoryInterface.php b/src/User/Repository/UserEmailRepositoryInterface.php similarity index 53% rename from src/User/Repository/UserSecondaryEmailRepositoryInterface.php rename to src/User/Repository/UserEmailRepositoryInterface.php index 89c650cd..5d4383bc 100644 --- a/src/User/Repository/UserSecondaryEmailRepositoryInterface.php +++ b/src/User/Repository/UserEmailRepositoryInterface.php @@ -5,30 +5,28 @@ namespace MsgPhp\User\Repository; use MsgPhp\Domain\DomainCollectionInterface; -use MsgPhp\User\Entity\UserSecondaryEmail; +use MsgPhp\User\Entity\UserEmail; use MsgPhp\User\UserIdInterface; /** * @author Roland Franssen */ -interface UserSecondaryEmailRepositoryInterface +interface UserEmailRepositoryInterface { /** - * @return DomainCollectionInterface|UserSecondaryEmail[] + * @return DomainCollectionInterface|UserEmail[] */ public function findAllByUserId(UserIdInterface $userId, int $offset = 0, int $limit = 0): DomainCollectionInterface; - public function find(UserIdInterface $userId, string $email): UserSecondaryEmail; + public function find(UserIdInterface $userId, string $email): UserEmail; - public function findPendingPrimary(UserIdInterface $userId): UserSecondaryEmail; + public function findByEmail(string $email): UserEmail; - public function findByEmail(string $email): UserSecondaryEmail; - - public function findByConfirmationToken(string $token): UserSecondaryEmail; + public function findByConfirmationToken(string $token): UserEmail; public function exists(UserIdInterface $userId, string $email): bool; - public function save(UserSecondaryEmail $userSecondaryEmail): void; + public function save(UserEmail $userEmail): void; - public function delete(UserSecondaryEmail $userSecondaryEmail): void; + public function delete(UserEmail $userEmail): void; } diff --git a/src/User/Tests/Entity/Fields/EmailsFieldTest.php b/src/User/Tests/Entity/Fields/EmailsFieldTest.php new file mode 100644 index 00000000..463b6417 --- /dev/null +++ b/src/User/Tests/Entity/Fields/EmailsFieldTest.php @@ -0,0 +1,36 @@ +getObject($emails = [$this->createMock(UserEmail::class)]); + + $this->assertSame($emails, iterator_to_array($object->getEmails())); + + $object = $this->getObject($emails = $this->createMock(DomainCollectionInterface::class)); + + $this->assertSame($emails, $object->getEmails()); + } + + private function getObject($value) + { + return new class($value) { + use EmailsField; + + public function __construct($value) + { + $this->emails = $value; + } + }; + } +} diff --git a/src/User/Tests/Entity/UserEmailTest.php b/src/User/Tests/Entity/UserEmailTest.php new file mode 100644 index 00000000..f578a36e --- /dev/null +++ b/src/User/Tests/Entity/UserEmailTest.php @@ -0,0 +1,33 @@ +createEntity($user = $this->createMock(User::class), 'foo@bar.baz', null); + + $this->assertSame($user, $userEmail->getUser()); + $this->assertSame('foo@bar.baz', $userEmail->getEmail()); + $this->assertNotNull($userEmail->getConfirmationToken()); + $this->assertNotSame($userEmail->getConfirmationToken(), $this->createEntity($user, 'foo@bar.baz', null)->getConfirmationToken()); + $this->assertNull($userEmail->getConfirmedAt()); + } + + public function testCreateWithToken(): void + { + $this->assertSame('token', $this->createEntity($this->createMock(User::class), 'foo@bar.baz', 'token')->getConfirmationToken()); + } + + private function createEntity($user, $email, $token): UserEmail + { + return new class($user, $email, $token) extends UserEmail { + }; + } +} diff --git a/src/User/Tests/Entity/UserSecondaryEmailTest.php b/src/User/Tests/Entity/UserSecondaryEmailTest.php deleted file mode 100644 index 7757e486..00000000 --- a/src/User/Tests/Entity/UserSecondaryEmailTest.php +++ /dev/null @@ -1,52 +0,0 @@ -createEntity($user = $this->createMock(User::class), 'foo@bar.baz', null); - - $this->assertSame($user, $userSecondaryEmail->getUser()); - $this->assertSame('foo@bar.baz', $userSecondaryEmail->getEmail()); - $this->assertNotNull($userSecondaryEmail->getConfirmationToken()); - $this->assertNotSame($userSecondaryEmail->getConfirmationToken(), $this->createEntity($user, 'foo@bar.baz', null)->getConfirmationToken()); - $this->assertFalse($userSecondaryEmail->isPendingPrimary()); - $this->assertNull($userSecondaryEmail->getConfirmedAt()); - } - - public function testCreateWithToken(): void - { - $this->assertSame('token', $this->createEntity($this->createMock(User::class), 'foo@bar.baz', 'token')->getConfirmationToken()); - } - - public function testMarkPendingPrimary(): void - { - $userSecondaryEmail = $this->createEntity($this->createMock(User::class), 'foo@bar.baz', null); - $userSecondaryEmail->markPendingPrimary(); - - $this->assertTrue($userSecondaryEmail->isPendingPrimary()); - - $userSecondaryEmail->markPendingPrimary(false); - - $this->assertFalse($userSecondaryEmail->isPendingPrimary()); - - $userSecondaryEmail->confirm(); - - $this->expectException(\LogicException::class); - - $userSecondaryEmail->markPendingPrimary(); - } - - private function createEntity($user, $email, $token): UserSecondaryEmail - { - return new class($user, $email, $token) extends UserSecondaryEmail { - }; - } -} diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index 0b75c602..f9b7cdcd 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -27,7 +27,7 @@ final class Configuration implements ConfigurationInterface Entity\User::class => ['id'], Entity\Username::class => ['user', 'username'], Entity\UserRole::class => ['user', 'role'], - Entity\UserSecondaryEmail::class => ['user', 'email'], + Entity\UserEmail::class => ['user', 'email'], ]; public const DEFAULT_ID_CLASS_MAPPING = [ UserIdInterface::class => UserId::class, @@ -136,6 +136,13 @@ public function getConfigTreeBuilder(): TreeBuilder $config['commands'][Command\ChangeUserCredentialCommand::class] = true; } + if (isset($config['class_mapping'][Entity\UserEmail::class])) { + $config['commands'] += [ + Command\CreateUserEmailCommand::class => true, + Command\DeleteUserEmailCommand::class => true, + ]; + } + ConfigHelper::resolveCommandMappingConfig(self::COMMAND_MAPPING, $config['class_mapping'], $config['commands']); return $config; diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index 8a4db4a8..f81b3ae6 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -75,6 +75,10 @@ public function load(array $configs, ContainerBuilder $container): void Command\Handler\EnableUserHandler::class, Command\Handler\RequestUserPasswordHandler::class, ]); + ContainerHelper::removeIf($container, !$container->has(Repository\UserEmailRepositoryInterface::class), [ + Command\Handler\CreateUserEmailHandler::class, + Command\Handler\DeleteUserEmailHandler::class, + ]); ContainerHelper::configureCommandMessages($container, $config['class_mapping'], $config['commands']); ContainerHelper::configureEventMessages($container, $config['class_mapping'], array_map(function (string $file): string { return 'MsgPhp\\User\\Event\\'.basename($file, '.php'); @@ -206,7 +210,7 @@ private function prepareDoctrineOrm(array $config, LoaderInterface $loader, Cont DoctrineInfra\Repository\UsernameRepository::class => Entity\Username::class, DoctrineInfra\Repository\UserAttributeValueRepository::class => Entity\UserAttributeValue::class, DoctrineInfra\Repository\UserRoleRepository::class => Entity\UserRole::class, - DoctrineInfra\Repository\UserSecondaryEmailRepository::class => Entity\UserSecondaryEmail::class, + DoctrineInfra\Repository\UserEmailRepository::class => Entity\UserEmail::class, ]); } diff --git a/src/UserBundle/README.md b/src/UserBundle/README.md index 8ee23770..975d12f4 100644 --- a/src/UserBundle/README.md +++ b/src/UserBundle/README.md @@ -23,10 +23,10 @@ composer require msgphp/user-bundle - Symfony validators - Credential independent (supports e-mail, nickname, etc.) - Multiple username / credential support +- Primary and secondary user e-mails - Disabled / enabled users - User roles - User attribute values -- User secondary e-mails ## Blog posts From cab7b75ff069e0c1cd9fbf4c8f6c7c354ffbac99 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 15:04:58 +0100 Subject: [PATCH 29/79] fix hashed pass form type --- src/User/Infra/Form/Type/HashedPasswordType.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/User/Infra/Form/Type/HashedPasswordType.php b/src/User/Infra/Form/Type/HashedPasswordType.php index 8fa0ac2b..ecfdf3f2 100644 --- a/src/User/Infra/Form/Type/HashedPasswordType.php +++ b/src/User/Infra/Form/Type/HashedPasswordType.php @@ -9,6 +9,7 @@ use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -64,7 +65,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void : (is_callable($algorithm) ? $algorithm() : $algorithm); if (!$this->passwordHashing->isValid($user->getPassword(), $plainPassword, $algorithm)) { - $context->addViolation($passwordOptions['invalid_message'], $passwordOptions['invalid_message_parameters']); + /** @var FormInterface $form */ + $form = $context->getObject(); + $form->addError(new FormError($passwordOptions['invalid_message'], $passwordOptions['invalid_message'], $passwordOptions['invalid_message_parameters'], null, $this)); } })); } @@ -97,7 +100,9 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } if (!$this->passwordHashing->isValid($password, $value, is_callable($algorithm) ? $algorithm() : $algorithm)) { - $context->addViolation($passwordConfirmOptions['invalid_message'], $passwordConfirmOptions['invalid_message_parameters']); + /** @var FormInterface $form */ + $form = $context->getObject(); + $form->addError(new FormError($passwordConfirmOptions['invalid_message'], $passwordConfirmOptions['invalid_message'], $passwordConfirmOptions['invalid_message_parameters'], null, $this)); } })); From bfab1cca9711b845729432ee0392ae0b93232d29 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 15:59:40 +0100 Subject: [PATCH 30/79] resolve class mapping in doctrine field definitions --- .../DependencyInjection/BundleHelper.php | 5 ++- .../Compiler/ResolveDomainPass.php | 42 +++++++++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/Domain/Infra/DependencyInjection/BundleHelper.php b/src/Domain/Infra/DependencyInjection/BundleHelper.php index be58aeeb..fc715607 100644 --- a/src/Domain/Infra/DependencyInjection/BundleHelper.php +++ b/src/Domain/Infra/DependencyInjection/BundleHelper.php @@ -26,8 +26,6 @@ public static function initDomain(ContainerBuilder $container): void return; } - $container->addCompilerPass(new Compiler\ResolveDomainPass()); - $container->registerForAutoconfiguration(ConsoleInfra\ContextBuilder\ContextElementProviderInterface::class) ->addTag('msgphp.console.context_element_provider'); @@ -38,6 +36,7 @@ public static function initDomain(ContainerBuilder $container): void $container->register(DoctrineInfra\Event\ObjectFieldMappingListener::class) ->setPublic(false) + ->setArgument('$mapping', []) ->addTag('doctrine.event_listener', ['event' => DoctrineOrmEvents::loadClassMetadata]); if (ContainerHelper::hasBundle($container, DoctrineBundle::class)) { @@ -60,6 +59,8 @@ public static function initDomain(ContainerBuilder $container): void } } + $container->addCompilerPass(new Compiler\ResolveDomainPass()); + $initialized = true; } diff --git a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php index 05d7d7e3..e2cf3851 100644 --- a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php +++ b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php @@ -23,8 +23,12 @@ final class ResolveDomainPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { - $this->registerIdentityMapping($container); - $this->registerEntityFactory($container); + $classMapping = array_merge(...$container->getParameter('msgphp.domain.class_mapping')); + $idClassMapping = array_merge(...$container->getParameter('msgphp.domain.id_class_mapping')); + $identityMapping = array_merge(...$container->getParameter('msgphp.domain.identity_mapping')); + + $this->registerIdentityMapping($container, $classMapping, $identityMapping); + $this->registerEntityFactory($container, $classMapping, $idClassMapping); $this->registerMessageBus($container); if (interface_exists(CacheWarmerInterface::class) && $container->hasParameter('msgphp.doctrine.mapping_files')) { @@ -36,11 +40,16 @@ public function process(ContainerBuilder $container): void ->addTag('kernel.cache_warmer', ['priority' => 100]); } + if ($container->hasDefinition(DoctrineInfra\Event\ObjectFieldMappingListener::class)) { + ($definition = $container->getDefinition(DoctrineInfra\Event\ObjectFieldMappingListener::class)) + ->setArgument('$mapping', self::processClassMapping($definition->getArgument('$mapping'), $classMapping)); + } + $params = $container->getParameterBag(); + $params->remove('msgphp.domain.class_mapping'); + $params->remove('msgphp.domain.id_class_mapping'); + $params->remove('msgphp.domain.identity_mapping'); $params->remove('msgphp.doctrine.mapping_files'); - $params->remove('msgphp.doctrine.identity_mapping'); - $params->remove('msgphp.doctrine.class_mapping'); - $params->remove('msgphp.doctrine.id_class_mapping'); } private static function register(ContainerBuilder $container, string $class, string $id = null): Definition @@ -53,11 +62,23 @@ private static function alias(ContainerBuilder $container, string $alias, string $container->setAlias($alias, new Alias($id, false)); } - private function registerIdentityMapping(ContainerBuilder $container): void + private static function processClassMapping($value, array $classMapping) { - $identityMapping = array_merge(...$container->getParameter('msgphp.domain.identity_mapping')); - $classMapping = array_merge(...$container->getParameter('msgphp.domain.class_mapping')); + if (is_string($value) && isset($classMapping[$value])) { + return $classMapping[$value]; + } + if (is_array($value)) { + array_walk_recursive($value, function (&$value) use ($classMapping): void { + $value = self::processClassMapping($value, $classMapping); + }); + } + + return $value; + } + + private function registerIdentityMapping(ContainerBuilder $container, array $classMapping, array $identityMapping): void + { if ($container->has(DoctrineEntityManager::class)) { self::register($container, $aliasId = DoctrineInfra\DomainIdentityMapping::class) ->setAutowired(true) @@ -75,11 +96,8 @@ private function registerIdentityMapping(ContainerBuilder $container): void ->setAutowired(true); } - private function registerEntityFactory(ContainerBuilder $container): void + private function registerEntityFactory(ContainerBuilder $container, array $classMapping, array $idClassMapping): void { - $classMapping = array_merge(...$container->getParameter('msgphp.domain.class_mapping')); - $idClassMapping = array_merge(...$container->getParameter('msgphp.domain.id_class_mapping')); - self::register($container, $aliasId = Factory\DomainObjectFactory::class) ->addMethodCall('setNestedFactory', [new Reference(Factory\DomainObjectFactoryInterface::class)]); From c0166d2b2ec7bc6a29951a68ee6e3717cd2d6288 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 17:06:15 +0100 Subject: [PATCH 31/79] various fixes --- .../Handler/DeleteUserEmailHandler.php | 2 +- .../Repository/UsernameRepository.php | 20 ++++++++++++---- .../Infra/Form/Type/HashedPasswordType.php | 23 ++++++++----------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/User/Command/Handler/DeleteUserEmailHandler.php b/src/User/Command/Handler/DeleteUserEmailHandler.php index 843056b2..55631a1c 100644 --- a/src/User/Command/Handler/DeleteUserEmailHandler.php +++ b/src/User/Command/Handler/DeleteUserEmailHandler.php @@ -8,7 +8,7 @@ use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; use MsgPhp\Domain\Message\{DomainMessageBusInterface, MessageDispatchingTrait}; use MsgPhp\User\Command\DeleteUserEmailCommand; -use MsgPhp\User\Entity\{User, UserEmail}; +use MsgPhp\User\Entity\User; use MsgPhp\User\Event\UserEmailDeletedEvent; use MsgPhp\User\Repository\UserEmailRepositoryInterface; diff --git a/src/User/Infra/Doctrine/Repository/UsernameRepository.php b/src/User/Infra/Doctrine/Repository/UsernameRepository.php index 41b83ee5..8397a779 100644 --- a/src/User/Infra/Doctrine/Repository/UsernameRepository.php +++ b/src/User/Infra/Doctrine/Repository/UsernameRepository.php @@ -9,7 +9,7 @@ use MsgPhp\Domain\{DomainCollectionInterface, DomainIdentityHelper}; use MsgPhp\Domain\Factory\DomainCollectionFactory; use MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait; -use MsgPhp\User\Entity\Username; +use MsgPhp\User\Entity\{User, Username}; use MsgPhp\User\Repository\UsernameRepositoryInterface; use MsgPhp\User\UserIdInterface; @@ -72,7 +72,7 @@ public function findAllFromTargets(int $offset = 0, int $limit = 0): DomainColle $userField = $mapping['mapped_by']; } else { - $userField = reset($idFields); + $userField = null; } $qb->addSelect(sprintf('partial %s.{%s}', $alias, implode(', ', array_keys($fields)))); @@ -87,8 +87,20 @@ public function findAllFromTargets(int $offset = 0, int $limit = 0): DomainColle $metadata = $this->em->getClassMetadata($class = ClassUtils::getRealClass(get_class($targetEntity))); foreach ($targetInfo[$class] as $info) { - if (null === $user = $metadata->getFieldValue($targetEntity, $info['user_field'])) { - continue; + if ($targetEntity instanceof User) { + $user = $targetEntity; + } elseif (isset($info['user_field'])) { + $user = $metadata->getFieldValue($targetEntity, $info['user_field']); + + if (null === $user) { + continue; + } + + if ($user instanceof User) { + throw new \LogicException(sprintf('Field "%s.%s" must return an instance of "%s" or null, got "%s".', $class, $info['user_field'], User::class, is_object($user) ? get_class($user) : gettype($user))); + } + } else { + throw new \LogicException(sprintf('No user field mapped for entity "%s".', $class)); } if (null === $username = $metadata->getFieldValue($targetEntity, $info['username_field'])) { diff --git a/src/User/Infra/Form/Type/HashedPasswordType.php b/src/User/Infra/Form/Type/HashedPasswordType.php index ecfdf3f2..f1511538 100644 --- a/src/User/Infra/Form/Type/HashedPasswordType.php +++ b/src/User/Infra/Form/Type/HashedPasswordType.php @@ -50,21 +50,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } $passwordOptions = self::withConstraint($passwordOptions, new Callback(function (?string $value, ExecutionContextInterface $context) use ($algorithm, $passwordOptions, &$plainPassword): void { - if (null === $value || '' === $value) { - return; + $valid = true; + if (null === $value || '' === $value || null === $this->tokenStorage || null === ($token = $this->tokenStorage->getToken()) || !($user = $token->getUser()) instanceof UserInterface) { + $valid = false; + } else { + $algorithm = null === $algorithm && null !== ($salt = $user->getSalt()) + ? PasswordAlgorithm::createLegacySalted(new PasswordSalt($salt)) + : (is_callable($algorithm) ? $algorithm() : $algorithm); + + $valid = $this->passwordHashing->isValid($user->getPassword(), $plainPassword, $algorithm); } - if (null === $this->tokenStorage || null === ($token = $this->tokenStorage->getToken()) || !($user = $token->getUser()) instanceof UserInterface) { - $context->addViolation($passwordOptions['invalid_message'], $passwordOptions['invalid_message_parameters']); - - return; - } - - $algorithm = null === $algorithm && null !== ($salt = $user->getSalt()) - ? PasswordAlgorithm::createLegacySalted(new PasswordSalt($salt)) - : (is_callable($algorithm) ? $algorithm() : $algorithm); - - if (!$this->passwordHashing->isValid($user->getPassword(), $plainPassword, $algorithm)) { + if (!$valid) { /** @var FormInterface $form */ $form = $context->getObject(); $form->addError(new FormError($passwordOptions['invalid_message'], $passwordOptions['invalid_message'], $passwordOptions['invalid_message_parameters'], null, $this)); From d75ed0a7ff2048bc0449f5f4f0b8b723e9c08aee Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 17:09:40 +0100 Subject: [PATCH 32/79] various fixes --- src/User/Infra/Doctrine/Event/UsernameListener.php | 4 ++-- src/User/Infra/Doctrine/Repository/UsernameRepository.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/User/Infra/Doctrine/Event/UsernameListener.php b/src/User/Infra/Doctrine/Event/UsernameListener.php index 153b5030..827b3b6d 100644 --- a/src/User/Infra/Doctrine/Event/UsernameListener.php +++ b/src/User/Infra/Doctrine/Event/UsernameListener.php @@ -98,9 +98,9 @@ public function postFlush(PostFlushEventArgs $event): void } } - $em->flush(); - $this->updateUsernames = []; + + $em->flush(); } /** diff --git a/src/User/Infra/Doctrine/Repository/UsernameRepository.php b/src/User/Infra/Doctrine/Repository/UsernameRepository.php index 8397a779..e0ad34b4 100644 --- a/src/User/Infra/Doctrine/Repository/UsernameRepository.php +++ b/src/User/Infra/Doctrine/Repository/UsernameRepository.php @@ -96,7 +96,7 @@ public function findAllFromTargets(int $offset = 0, int $limit = 0): DomainColle continue; } - if ($user instanceof User) { + if (!$user instanceof User) { throw new \LogicException(sprintf('Field "%s.%s" must return an instance of "%s" or null, got "%s".', $class, $info['user_field'], User::class, is_object($user) ? get_class($user) : gettype($user))); } } else { From 27a90a5cc63e4a1f0976df9b80d3c52915e6ac8b Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 21:45:19 +0100 Subject: [PATCH 33/79] cleanup user email leftovers (#106) cleanup user email leftovers --- .../DependencyInjection/ConfigHelper.php | 8 +-- src/User/Command/ConfirmUserEmailCommand.php | 18 +++++++ src/User/Command/DeleteUserEmailCommand.php | 4 +- .../Handler/ConfirmUserEmailHandler.php | 50 +++++++++++++++++++ .../Handler/DeleteUserEmailHandler.php | 3 +- src/User/Entity/UserEmail.php | 5 +- src/User/Event/UserEmailConfirmedEvent.php | 20 ++++++++ .../Infra/Doctrine/EntityFieldsMapping.php | 1 + .../Repository/UserEmailRepository.php | 18 ++----- .../User.Entity.UserEmail.orm.xml | 10 +--- .../UserEmailRepositoryInterface.php | 8 +-- src/User/Tests/Entity/UserEmailTest.php | 14 ++---- .../DependencyInjection/Configuration.php | 22 ++++---- 13 files changed, 118 insertions(+), 63 deletions(-) create mode 100644 src/User/Command/ConfirmUserEmailCommand.php create mode 100644 src/User/Command/Handler/ConfirmUserEmailHandler.php create mode 100644 src/User/Event/UserEmailConfirmedEvent.php diff --git a/src/Domain/Infra/DependencyInjection/ConfigHelper.php b/src/Domain/Infra/DependencyInjection/ConfigHelper.php index ff198db1..62e9eaf8 100644 --- a/src/Domain/Infra/DependencyInjection/ConfigHelper.php +++ b/src/Domain/Infra/DependencyInjection/ConfigHelper.php @@ -56,9 +56,11 @@ public static function resolveCommandMappingConfig(array $commandMapping, array $mappedClass = $classMapping[$commandClass] ?? $commandClass; $isEventHandler = is_subclass_of($mappedClass, DomainEventHandlerInterface::class); - foreach ($features as $feature => $featureCommands) { - if (self::uses($mappedClass, $feature)) { - $config += array_fill_keys($featureCommands, $isEventHandler); + foreach ($features as $feature => $featureInfo) { + if (!trait_exists($feature)) { + $config[$feature] = $featureInfo; + } elseif (self::uses($mappedClass, $feature)) { + $config += array_fill_keys($featureInfo, $isEventHandler); } } } diff --git a/src/User/Command/ConfirmUserEmailCommand.php b/src/User/Command/ConfirmUserEmailCommand.php new file mode 100644 index 00000000..ff254a6c --- /dev/null +++ b/src/User/Command/ConfirmUserEmailCommand.php @@ -0,0 +1,18 @@ + + */ +class ConfirmUserEmailCommand +{ + public $email; + + final public function __construct(string $email) + { + $this->email = $email; + } +} diff --git a/src/User/Command/DeleteUserEmailCommand.php b/src/User/Command/DeleteUserEmailCommand.php index ae5faeca..4cdf250e 100644 --- a/src/User/Command/DeleteUserEmailCommand.php +++ b/src/User/Command/DeleteUserEmailCommand.php @@ -9,12 +9,10 @@ */ class DeleteUserEmailCommand { - public $userId; public $email; - final public function __construct($userId, string $email) + final public function __construct(string $email) { - $this->userId = $userId; $this->email = $email; } } diff --git a/src/User/Command/Handler/ConfirmUserEmailHandler.php b/src/User/Command/Handler/ConfirmUserEmailHandler.php new file mode 100644 index 00000000..9f31bcdf --- /dev/null +++ b/src/User/Command/Handler/ConfirmUserEmailHandler.php @@ -0,0 +1,50 @@ + + */ +final class ConfirmUserEmailHandler +{ + use EventSourcingCommandHandlerTrait; + use MessageDispatchingTrait; + + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserEmailRepositoryInterface $repository) + { + $this->factory = $factory; + $this->bus = $bus; + $this->repository = $repository; + } + + public function __invoke(ConfirmUserEmailCommand $command): void + { + $this->handle($command, function (UserEmail $userEmail): void { + $this->repository->save($userEmail); + $this->dispatch(UserEmailConfirmedEvent::class, [$userEmail]); + }); + } + + protected function getDomainEvent(ConfirmUserEmailCommand $command): ConfirmEvent + { + return $this->factory->create(ConfirmEvent::class); + } + + protected function getDomainEventHandler(ConfirmUserEmailCommand $command): UserEmail + { + return $this->repository->find($command->email); + } +} diff --git a/src/User/Command/Handler/DeleteUserEmailHandler.php b/src/User/Command/Handler/DeleteUserEmailHandler.php index 55631a1c..f81a15ce 100644 --- a/src/User/Command/Handler/DeleteUserEmailHandler.php +++ b/src/User/Command/Handler/DeleteUserEmailHandler.php @@ -8,7 +8,6 @@ use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; use MsgPhp\Domain\Message\{DomainMessageBusInterface, MessageDispatchingTrait}; use MsgPhp\User\Command\DeleteUserEmailCommand; -use MsgPhp\User\Entity\User; use MsgPhp\User\Event\UserEmailDeletedEvent; use MsgPhp\User\Repository\UserEmailRepositoryInterface; @@ -31,7 +30,7 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB public function __invoke(DeleteUserEmailCommand $command): void { try { - $userEmail = $this->repository->find($this->factory->identify(User::class, $command->userId), $command->email); + $userEmail = $this->repository->find($command->email); } catch (EntityNotFoundException $e) { return; } diff --git a/src/User/Entity/UserEmail.php b/src/User/Entity/UserEmail.php index 3f37f3ac..78715e11 100644 --- a/src/User/Entity/UserEmail.php +++ b/src/User/Entity/UserEmail.php @@ -4,7 +4,6 @@ namespace MsgPhp\User\Entity; -use MsgPhp\Domain\Entity\Features\CanBeConfirmed; use MsgPhp\User\Entity\Fields\UserField; /** @@ -13,15 +12,13 @@ abstract class UserEmail { use UserField; - use CanBeConfirmed; private $email; - public function __construct(User $user, string $email, string $token = null) + public function __construct(User $user, string $email) { $this->user = $user; $this->email = $email; - $this->confirmationToken = $token ?? bin2hex(random_bytes(32)); } public function getEmail(): string diff --git a/src/User/Event/UserEmailConfirmedEvent.php b/src/User/Event/UserEmailConfirmedEvent.php new file mode 100644 index 00000000..1450af74 --- /dev/null +++ b/src/User/Event/UserEmailConfirmedEvent.php @@ -0,0 +1,20 @@ + + */ +class UserEmailConfirmedEvent +{ + public $userEmail; + + final public function __construct(UserEmail $userEmail) + { + $this->userEmail = $userEmail; + } +} diff --git a/src/User/Infra/Doctrine/EntityFieldsMapping.php b/src/User/Infra/Doctrine/EntityFieldsMapping.php index f53a52e7..bfeefca7 100644 --- a/src/User/Infra/Doctrine/EntityFieldsMapping.php +++ b/src/User/Infra/Doctrine/EntityFieldsMapping.php @@ -53,6 +53,7 @@ public static function getObjectFieldMapping(): array 'type' => self::TYPE_ONE_TO_MANY, 'targetEntity' => UserEmail::class, 'mappedBy' => 'user', + 'indexBy' => 'email', ], ], Fields\UserField::class => [ diff --git a/src/User/Infra/Doctrine/Repository/UserEmailRepository.php b/src/User/Infra/Doctrine/Repository/UserEmailRepository.php index 1de5c795..05d9a4bf 100644 --- a/src/User/Infra/Doctrine/Repository/UserEmailRepository.php +++ b/src/User/Infra/Doctrine/Repository/UserEmailRepository.php @@ -27,24 +27,14 @@ public function findAllByUserId(UserIdInterface $userId, int $offset = 0, int $l return $this->doFindAllByFields(['user' => $userId], $offset, $limit); } - public function find(UserIdInterface $userId, string $email): UserEmail + public function find(string $email): UserEmail { - return $this->doFind(['user' => $userId, 'email' => $email]); + return $this->doFind($email); } - public function findByEmail(string $email): UserEmail + public function exists(string $email): bool { - return $this->doFindByFields(['email' => $email]); - } - - public function findByConfirmationToken(string $token): UserEmail - { - return $this->doFindByFields(['confirmationToken' => $token]); - } - - public function exists(UserIdInterface $userId, string $email): bool - { - return $this->doExists(['user' => $userId, 'email' => $email]); + return $this->doExists($email); } public function save(UserEmail $userEmail): void diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserEmail.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserEmail.orm.xml index bb640f98..716ad807 100644 --- a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserEmail.orm.xml +++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserEmail.orm.xml @@ -4,18 +4,10 @@ xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> - - - - - - - - - + diff --git a/src/User/Repository/UserEmailRepositoryInterface.php b/src/User/Repository/UserEmailRepositoryInterface.php index 5d4383bc..9ca5afc1 100644 --- a/src/User/Repository/UserEmailRepositoryInterface.php +++ b/src/User/Repository/UserEmailRepositoryInterface.php @@ -18,13 +18,9 @@ interface UserEmailRepositoryInterface */ public function findAllByUserId(UserIdInterface $userId, int $offset = 0, int $limit = 0): DomainCollectionInterface; - public function find(UserIdInterface $userId, string $email): UserEmail; + public function find(string $email): UserEmail; - public function findByEmail(string $email): UserEmail; - - public function findByConfirmationToken(string $token): UserEmail; - - public function exists(UserIdInterface $userId, string $email): bool; + public function exists(string $email): bool; public function save(UserEmail $userEmail): void; diff --git a/src/User/Tests/Entity/UserEmailTest.php b/src/User/Tests/Entity/UserEmailTest.php index f578a36e..897b084f 100644 --- a/src/User/Tests/Entity/UserEmailTest.php +++ b/src/User/Tests/Entity/UserEmailTest.php @@ -11,23 +11,15 @@ final class UserEmailTest extends TestCase { public function testCreate(): void { - $userEmail = $this->createEntity($user = $this->createMock(User::class), 'foo@bar.baz', null); + $userEmail = $this->createEntity($user = $this->createMock(User::class), 'foo@bar.baz'); $this->assertSame($user, $userEmail->getUser()); $this->assertSame('foo@bar.baz', $userEmail->getEmail()); - $this->assertNotNull($userEmail->getConfirmationToken()); - $this->assertNotSame($userEmail->getConfirmationToken(), $this->createEntity($user, 'foo@bar.baz', null)->getConfirmationToken()); - $this->assertNull($userEmail->getConfirmedAt()); } - public function testCreateWithToken(): void + private function createEntity($user, $email): UserEmail { - $this->assertSame('token', $this->createEntity($this->createMock(User::class), 'foo@bar.baz', 'token')->getConfirmationToken()); - } - - private function createEntity($user, $email, $token): UserEmail - { - return new class($user, $email, $token) extends UserEmail { + return new class($user, $email) extends UserEmail { }; } } diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index f9b7cdcd..6505cc57 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -37,6 +37,9 @@ final class Configuration implements ConfigurationInterface ]; private const COMMAND_MAPPING = [ Entity\User::class => [ + Command\CreateUserCommand::class => true, + Command\DeleteUserCommand::class => true, + Features\CanBeConfirmed::class => [ Command\ConfirmUserCommand::class, ], @@ -48,6 +51,14 @@ final class Configuration implements ConfigurationInterface Command\RequestUserPasswordCommand::class, ], ], + Entity\UserEmail::class => [ + Command\CreateUserEmailCommand::class => true, + Command\DeleteUserEmailCommand::class => true, + + Features\CanBeConfirmed::class => [ + Command\ConfirmUserEmailCommand::class, + ], + ], ]; public function getConfigTreeBuilder(): TreeBuilder @@ -66,10 +77,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->classMappingNode('commands') ->typeOfValues('boolean') - ->defaultMapping([ - Command\CreateUserCommand::class => true, - Command\DeleteUserCommand::class => true, - ]) ->end() ->scalarNode('default_id_type') ->defaultValue(ConfigHelper::DEFAULT_ID_TYPE) @@ -136,13 +143,6 @@ public function getConfigTreeBuilder(): TreeBuilder $config['commands'][Command\ChangeUserCredentialCommand::class] = true; } - if (isset($config['class_mapping'][Entity\UserEmail::class])) { - $config['commands'] += [ - Command\CreateUserEmailCommand::class => true, - Command\DeleteUserEmailCommand::class => true, - ]; - } - ConfigHelper::resolveCommandMappingConfig(self::COMMAND_MAPPING, $config['class_mapping'], $config['commands']); return $config; From b983a25c95a11ff9ae7264d7afd1816aef333c77 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 10 Mar 2018 23:08:53 +0100 Subject: [PATCH 34/79] revise twig infra --- src/EavBundle/DependencyInjection/Configuration.php | 2 ++ src/EavBundle/DependencyInjection/Extension.php | 2 ++ src/EavBundle/Resources/config/doctrine.php | 2 -- src/User/composer.json | 3 +-- src/UserBundle/DependencyInjection/Configuration.php | 2 ++ src/UserBundle/DependencyInjection/Extension.php | 7 +++++-- src/UserBundle/Resources/config/console.php | 2 -- src/UserBundle/Resources/config/doctrine.php | 2 -- src/UserBundle/Resources/config/form.php | 2 -- src/UserBundle/Resources/config/message.php | 2 -- src/UserBundle/Resources/config/security.php | 2 -- src/UserBundle/Resources/config/services.php | 2 -- src/UserBundle/Resources/config/twig.php | 4 +--- src/UserBundle/Resources/config/validator.php | 2 -- src/{User/Infra => UserBundle}/Twig/GlobalVariables.php | 2 +- 15 files changed, 14 insertions(+), 24 deletions(-) rename src/{User/Infra => UserBundle}/Twig/GlobalVariables.php (98%) diff --git a/src/EavBundle/DependencyInjection/Configuration.php b/src/EavBundle/DependencyInjection/Configuration.php index 787355ef..c114eff1 100644 --- a/src/EavBundle/DependencyInjection/Configuration.php +++ b/src/EavBundle/DependencyInjection/Configuration.php @@ -13,6 +13,8 @@ /** * @author Roland Franssen + * + * @internal */ final class Configuration implements ConfigurationInterface { diff --git a/src/EavBundle/DependencyInjection/Extension.php b/src/EavBundle/DependencyInjection/Extension.php index 49c767af..19f0ddef 100644 --- a/src/EavBundle/DependencyInjection/Extension.php +++ b/src/EavBundle/DependencyInjection/Extension.php @@ -19,6 +19,8 @@ /** * @author Roland Franssen + * + * @internal */ final class Extension extends BaseExtension implements PrependExtensionInterface { diff --git a/src/EavBundle/Resources/config/doctrine.php b/src/EavBundle/Resources/config/doctrine.php index 750e2cd1..e3c15ddf 100644 --- a/src/EavBundle/Resources/config/doctrine.php +++ b/src/EavBundle/Resources/config/doctrine.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace MsgPhp; - use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\Eav\AttributeIdInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; diff --git a/src/User/composer.json b/src/User/composer.json index 40ba2bfc..eb36084f 100644 --- a/src/User/composer.json +++ b/src/User/composer.json @@ -30,8 +30,7 @@ "symfony/phpunit-bridge": "^3.4|^4.0", "symfony/security-core": "^3.4|^4.0", "symfony/validator": "^3.4|^4.0", - "symfony/var-dumper": "^3.4|^4.0", - "twig/twig": "^2.4" + "symfony/var-dumper": "^3.4|^4.0" }, "config": { "preferred-install": { diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index 6505cc57..74174206 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -14,6 +14,8 @@ /** * @author Roland Franssen + * + * @internal */ final class Configuration implements ConfigurationInterface { diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index f81b3ae6..df49fc3e 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -11,7 +11,8 @@ use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\EavBundle\MsgPhpEavBundle; use MsgPhp\User\{Command, CredentialInterface, Entity, Repository, UserIdInterface}; -use MsgPhp\User\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, Security as SecurityInfra, Twig as TwigInfra, Validator as ValidatorInfra}; +use MsgPhp\User\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, Security as SecurityInfra, Validator as ValidatorInfra}; +use MsgPhp\UserBundle\Twig\GlobalVariables; use SimpleBus\SymfonyBridge\SimpleBusCommandBusBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\TwigBundle\TwigBundle; @@ -31,6 +32,8 @@ /** * @author Roland Franssen + * + * @internal */ final class Extension extends BaseExtension implements PrependExtensionInterface, CompilerPassInterface { @@ -168,7 +171,7 @@ public function prepend(ContainerBuilder $container): void if (ContainerHelper::hasBundle($container, TwigBundle::class)) { $container->prependExtensionConfig('twig', [ 'globals' => [ - 'msgphp_user' => '@'.TwigInfra\GlobalVariables::class, + 'msgphp_user' => '@'.GlobalVariables::class, ], ]); } diff --git a/src/UserBundle/Resources/config/console.php b/src/UserBundle/Resources/config/console.php index d5e35230..99992e50 100644 --- a/src/UserBundle/Resources/config/console.php +++ b/src/UserBundle/Resources/config/console.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace MsgPhp; - use MsgPhp\Domain\Infra\Console; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged; diff --git a/src/UserBundle/Resources/config/doctrine.php b/src/UserBundle/Resources/config/doctrine.php index a589340d..3b155082 100644 --- a/src/UserBundle/Resources/config/doctrine.php +++ b/src/UserBundle/Resources/config/doctrine.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace MsgPhp; - use Doctrine\ORM\Events as DoctrineOrmEvents; use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\User\Infra\Doctrine; diff --git a/src/UserBundle/Resources/config/form.php b/src/UserBundle/Resources/config/form.php index 03f1dc01..1631eb2d 100644 --- a/src/UserBundle/Resources/config/form.php +++ b/src/UserBundle/Resources/config/form.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace MsgPhp; - use MsgPhp\User\Infra\Form; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; diff --git a/src/UserBundle/Resources/config/message.php b/src/UserBundle/Resources/config/message.php index 29240610..1d67c949 100644 --- a/src/UserBundle/Resources/config/message.php +++ b/src/UserBundle/Resources/config/message.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace MsgPhp; - use MsgPhp\Domain\Infra\DependencyInjection\ContainerHelper; use MsgPhp\User\UserIdInterface; use SimpleBus\SymfonyBridge\SimpleBusCommandBusBundle; diff --git a/src/UserBundle/Resources/config/security.php b/src/UserBundle/Resources/config/security.php index 70d1b843..5a5d891c 100644 --- a/src/UserBundle/Resources/config/security.php +++ b/src/UserBundle/Resources/config/security.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace MsgPhp; - use MsgPhp\User\Password\PasswordHashingInterface; use MsgPhp\User\Infra\Security; use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface; diff --git a/src/UserBundle/Resources/config/services.php b/src/UserBundle/Resources/config/services.php index 119d60bc..0b3ac615 100644 --- a/src/UserBundle/Resources/config/services.php +++ b/src/UserBundle/Resources/config/services.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace MsgPhp; - use MsgPhp\User\Password\{PasswordHashing, PasswordHashingInterface}; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; diff --git a/src/UserBundle/Resources/config/twig.php b/src/UserBundle/Resources/config/twig.php index 2bae27f3..ee75228a 100644 --- a/src/UserBundle/Resources/config/twig.php +++ b/src/UserBundle/Resources/config/twig.php @@ -2,9 +2,7 @@ declare(strict_types=1); -namespace MsgPhp; - -use MsgPhp\User\Infra\Twig; +use MsgPhp\UserBundle\Twig; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function (ContainerConfigurator $container): void { diff --git a/src/UserBundle/Resources/config/validator.php b/src/UserBundle/Resources/config/validator.php index 871d913f..71cc4a07 100644 --- a/src/UserBundle/Resources/config/validator.php +++ b/src/UserBundle/Resources/config/validator.php @@ -2,8 +2,6 @@ declare(strict_types=1); -namespace MsgPhp; - use MsgPhp\User\Infra\Validator; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; diff --git a/src/User/Infra/Twig/GlobalVariables.php b/src/UserBundle/Twig/GlobalVariables.php similarity index 98% rename from src/User/Infra/Twig/GlobalVariables.php rename to src/UserBundle/Twig/GlobalVariables.php index b1a1ec43..55e0d850 100644 --- a/src/User/Infra/Twig/GlobalVariables.php +++ b/src/UserBundle/Twig/GlobalVariables.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MsgPhp\User\Infra\Twig; +namespace MsgPhp\UserBundle\Twig; use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; use MsgPhp\User\Entity\User; From bdf1b3a07fc70f5e92436296a313034e2fbf0b8d Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 11 Mar 2018 12:25:41 +0100 Subject: [PATCH 35/79] add first class Role entity (#108) --- .../Config/ClassMappingNodeDefinition.php | 23 +++++++-- src/User/Entity/Fields/RoleField.php | 26 ++++++++++ src/User/Entity/Role.php | 13 +++++ src/User/Entity/UserRole.php | 12 ++--- .../Infra/Doctrine/EntityFieldsMapping.php | 11 ++++- .../Doctrine/Repository/RoleRepository.php | 48 +++++++++++++++++++ .../dist-mapping/User.Entity.Role.orm.xml | 8 ++++ .../dist-mapping/User.Entity.UserRole.orm.xml | 5 +- .../Repository/RoleRepositoryInterface.php | 27 +++++++++++ .../Tests/Entity/Fields/RoleFieldTest.php | 37 ++++++++++++++ src/User/Tests/Entity/UserRoleTest.php | 6 +-- .../DependencyInjection/Configuration.php | 2 + .../DependencyInjection/Extension.php | 1 + 13 files changed, 201 insertions(+), 18 deletions(-) create mode 100644 src/User/Entity/Fields/RoleField.php create mode 100644 src/User/Entity/Role.php create mode 100644 src/User/Infra/Doctrine/Repository/RoleRepository.php create mode 100644 src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Role.orm.xml create mode 100644 src/User/Repository/RoleRepositoryInterface.php create mode 100644 src/User/Tests/Entity/Fields/RoleFieldTest.php diff --git a/src/Domain/Infra/Config/ClassMappingNodeDefinition.php b/src/Domain/Infra/Config/ClassMappingNodeDefinition.php index 56614fc4..4cb3f6e1 100644 --- a/src/Domain/Infra/Config/ClassMappingNodeDefinition.php +++ b/src/Domain/Infra/Config/ClassMappingNodeDefinition.php @@ -32,7 +32,7 @@ public function requireClasses(array $classes): self ->ifTrue(function (array $value) use ($class): bool { return !isset($value[$class]); }) - ->thenInvalid(sprintf('Class mapping for "%s" must be configured.', $class)); + ->thenInvalid(sprintf('Class "%s" must be configured.', $class)); } if ($classes) { @@ -49,12 +49,27 @@ public function disallowClasses(array $classes): self ->ifTrue(function (array $value) use ($class): bool { return isset($value[$class]); }) - ->thenInvalid(sprintf('Class mapping for "%s" is not applicable.', $class)); + ->thenInvalid(sprintf('Class "%s" is not applicable to be configured.', $class)); } return $this; } + public function groupClasses(array $classes): self + { + $this->validate()->always(function (array $value) use ($classes): array { + if ($classes !== ($missing = array_diff($classes, array_keys($value))) && $missing) { + foreach (array_diff($classes, $missing) as $known) { + throw new \LogicException(sprintf('Class "%s" requires "%s" to be configured.', $known, implode('", "', $missing))); + } + } + + return $value; + }); + + return $this; + } + public function typeOfValues(string $type): self { $this->type = $type; @@ -67,10 +82,10 @@ public function subClassValues(): self $this->validate()->always(function (array $value): array { foreach ($value as $class => $mappedClass) { if (!is_string($mappedClass)) { - throw new \LogicException(sprintf('Mapped value for class "%s" must be a string, got "%s".', $class, gettype($mappedClass))); + throw new \LogicException(sprintf('Class "%s" must be configured to a mapped sub class value, got type "%s".', $class, gettype($mappedClass))); } if (!is_subclass_of($mappedClass, $class)) { - throw new \LogicException(sprintf('Mapped class "%s" must be a sub class of "%s".', $mappedClass, $class)); + throw new \LogicException(sprintf('Class "%s" must be configured to a mapped sub class value, got "%s".', $class, $mappedClass)); } } diff --git a/src/User/Entity/Fields/RoleField.php b/src/User/Entity/Fields/RoleField.php new file mode 100644 index 00000000..61f202ca --- /dev/null +++ b/src/User/Entity/Fields/RoleField.php @@ -0,0 +1,26 @@ + + */ +trait RoleField +{ + /** @var Role */ + private $role; + + public function getRole(): Role + { + return $this->role; + } + + public function getRoleName(): string + { + return $this->role->getName(); + } +} diff --git a/src/User/Entity/Role.php b/src/User/Entity/Role.php new file mode 100644 index 00000000..74955a2a --- /dev/null +++ b/src/User/Entity/Role.php @@ -0,0 +1,13 @@ + + */ +abstract class Role +{ + abstract public function getName(): string; +} diff --git a/src/User/Entity/UserRole.php b/src/User/Entity/UserRole.php index 2efbc467..59904ec9 100644 --- a/src/User/Entity/UserRole.php +++ b/src/User/Entity/UserRole.php @@ -4,7 +4,7 @@ namespace MsgPhp\User\Entity; -use MsgPhp\User\Entity\Fields\UserField; +use MsgPhp\User\Entity\Fields\{RoleField, UserField}; /** * @author Roland Franssen @@ -12,17 +12,11 @@ abstract class UserRole { use UserField; + use RoleField; - private $role; - - public function __construct(User $user, string $role) + public function __construct(User $user, Role $role) { $this->user = $user; $this->role = $role; } - - public function getRole(): string - { - return $this->role; - } } diff --git a/src/User/Infra/Doctrine/EntityFieldsMapping.php b/src/User/Infra/Doctrine/EntityFieldsMapping.php index bfeefca7..2659d3ae 100644 --- a/src/User/Infra/Doctrine/EntityFieldsMapping.php +++ b/src/User/Infra/Doctrine/EntityFieldsMapping.php @@ -5,7 +5,7 @@ namespace MsgPhp\User\Infra\Doctrine; use MsgPhp\Domain\Infra\Doctrine\ObjectFieldMappingProviderInterface; -use MsgPhp\User\Entity\{Credential, Features, Fields, User, UserEmail}; +use MsgPhp\User\Entity\{Credential, Features, Fields, Role, User, UserEmail}; /** * @author Roland Franssen @@ -56,6 +56,15 @@ public static function getObjectFieldMapping(): array 'indexBy' => 'email', ], ], + Fields\RoleField::class => [ + 'role' => [ + 'type' => self::TYPE_MANY_TO_ONE, + 'targetEntity' => Role::class, + 'joinColumns' => [ + ['referencedColumnName' => 'name', 'nullable' => false], + ], + ], + ], Fields\UserField::class => [ 'user' => [ 'type' => self::TYPE_MANY_TO_ONE, diff --git a/src/User/Infra/Doctrine/Repository/RoleRepository.php b/src/User/Infra/Doctrine/Repository/RoleRepository.php new file mode 100644 index 00000000..8dd9db67 --- /dev/null +++ b/src/User/Infra/Doctrine/Repository/RoleRepository.php @@ -0,0 +1,48 @@ + + */ +final class RoleRepository implements RoleRepositoryInterface +{ + use DomainEntityRepositoryTrait; + + private $alias = 'role'; + + /** + * @return DomainCollectionInterface|Role[] + */ + public function findAll(int $offset = 0, int $limit = 0): DomainCollectionInterface + { + return $this->doFindAll($offset, $limit); + } + + public function find(string $name): Role + { + return $this->doFind($name); + } + + public function exists(string $name): bool + { + return $this->doExists($name); + } + + public function save(Role $role): void + { + $this->doSave($role); + } + + public function delete(Role $role): void + { + $this->doDelete($role); + } +} diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Role.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Role.orm.xml new file mode 100644 index 00000000..ed0dfd55 --- /dev/null +++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Role.orm.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserRole.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserRole.orm.xml index f3fb281f..8d93bc90 100644 --- a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserRole.orm.xml +++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserRole.orm.xml @@ -5,11 +5,14 @@ - + + + + diff --git a/src/User/Repository/RoleRepositoryInterface.php b/src/User/Repository/RoleRepositoryInterface.php new file mode 100644 index 00000000..e8a75bd1 --- /dev/null +++ b/src/User/Repository/RoleRepositoryInterface.php @@ -0,0 +1,27 @@ + + */ +interface RoleRepositoryInterface +{ + /** + * @return DomainCollectionInterface|Role[] + */ + public function findAll(int $offset = 0, int $limit = 0): DomainCollectionInterface; + + public function find(string $name): Role; + + public function exists(string $name): bool; + + public function save(Role $role): void; + + public function delete(Role $role): void; +} diff --git a/src/User/Tests/Entity/Fields/RoleFieldTest.php b/src/User/Tests/Entity/Fields/RoleFieldTest.php new file mode 100644 index 00000000..357dbc65 --- /dev/null +++ b/src/User/Tests/Entity/Fields/RoleFieldTest.php @@ -0,0 +1,37 @@ +createMock(Role::class); + $value->expects($this->any()) + ->method('getName') + ->willReturn('ROLE_FOO'); + + $object = $this->getObject($value); + + $this->assertSame($value, $object->getRole()); + $this->assertSame('ROLE_FOO', $object->getRoleName()); + } + + private function getObject($value) + { + return new class($value) { + use RoleField; + + public function __construct($value) + { + $this->role = $value; + } + }; + } +} diff --git a/src/User/Tests/Entity/UserRoleTest.php b/src/User/Tests/Entity/UserRoleTest.php index 9323b018..8f76b02e 100644 --- a/src/User/Tests/Entity/UserRoleTest.php +++ b/src/User/Tests/Entity/UserRoleTest.php @@ -4,17 +4,17 @@ namespace MsgPhp\User\Tests\Entity; -use MsgPhp\User\Entity\{User, UserRole}; +use MsgPhp\User\Entity\{Role, User, UserRole}; use PHPUnit\Framework\TestCase; final class UserRoleTest extends TestCase { public function testCreate(): void { - $userRole = $this->createEntity($user = $this->createMock(User::class), 'ROLE_USER'); + $userRole = $this->createEntity($user = $this->createMock(User::class), $role = $this->createMock(Role::class)); $this->assertSame($user, $userRole->getUser()); - $this->assertSame('ROLE_USER', $userRole->getRole()); + $this->assertSame($role, $userRole->getRole()); } private function createEntity($user, $role): UserRole diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index 74174206..c4b12dea 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -25,6 +25,7 @@ final class Configuration implements ConfigurationInterface public const OPTIONAL_AGGREGATE_ROOTS = []; public const AGGREGATE_ROOTS = self::REQUIRED_AGGREGATE_ROOTS + self::OPTIONAL_AGGREGATE_ROOTS; public const IDENTITY_MAPPING = [ + Entity\Role::class => ['name'], Entity\UserAttributeValue::class => ['user', 'attributeValue'], Entity\User::class => ['id'], Entity\Username::class => ['user', 'username'], @@ -72,6 +73,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->classMappingNode('class_mapping') ->requireClasses(array_keys(self::REQUIRED_AGGREGATE_ROOTS)) ->disallowClasses([CredentialInterface::class, Entity\Username::class]) + ->groupClasses([Entity\Role::class, Entity\UserRole::class]) ->subClassValues() ->end() ->classMappingNode('id_type_mapping') diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index df49fc3e..fc1bab5c 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -209,6 +209,7 @@ private function prepareDoctrineOrm(array $config, LoaderInterface $loader, Cont } ContainerHelper::configureDoctrineOrmRepositories($container, [Entity\Username::class => Entity\Username::class] + $config['class_mapping'], [ + DoctrineInfra\Repository\RoleRepository::class => Entity\Role::class, DoctrineInfra\Repository\UserRepository::class => Entity\User::class, DoctrineInfra\Repository\UsernameRepository::class => Entity\Username::class, DoctrineInfra\Repository\UserAttributeValueRepository::class => Entity\UserAttributeValue::class, From 60d4e1960d19d360f3800b537168f241b6dc1162 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 11 Mar 2018 13:24:20 +0100 Subject: [PATCH 36/79] update emails terminology (#109) --- ...ateUserEmailCommand.php => AddUserEmailCommand.php} | 2 +- ...ateUserEmailHandler.php => AddUserEmailHandler.php} | 10 +++++----- src/User/Entity/Fields/EmailsField.php | 3 +++ ...erEmailCreatedEvent.php => UserEmailAddedEvent.php} | 2 +- src/UserBundle/DependencyInjection/Configuration.php | 2 +- src/UserBundle/DependencyInjection/Extension.php | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) rename src/User/Command/{CreateUserEmailCommand.php => AddUserEmailCommand.php} (93%) rename src/User/Command/Handler/{CreateUserEmailHandler.php => AddUserEmailHandler.php} (79%) rename src/User/Event/{UserEmailCreatedEvent.php => UserEmailAddedEvent.php} (91%) diff --git a/src/User/Command/CreateUserEmailCommand.php b/src/User/Command/AddUserEmailCommand.php similarity index 93% rename from src/User/Command/CreateUserEmailCommand.php rename to src/User/Command/AddUserEmailCommand.php index 68af0d0c..15c0ce48 100644 --- a/src/User/Command/CreateUserEmailCommand.php +++ b/src/User/Command/AddUserEmailCommand.php @@ -7,7 +7,7 @@ /** * @author Roland Franssen */ -class CreateUserEmailCommand +class AddUserEmailCommand { public $userId; public $email; diff --git a/src/User/Command/Handler/CreateUserEmailHandler.php b/src/User/Command/Handler/AddUserEmailHandler.php similarity index 79% rename from src/User/Command/Handler/CreateUserEmailHandler.php rename to src/User/Command/Handler/AddUserEmailHandler.php index a01243d9..82198244 100644 --- a/src/User/Command/Handler/CreateUserEmailHandler.php +++ b/src/User/Command/Handler/AddUserEmailHandler.php @@ -6,15 +6,15 @@ use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; use MsgPhp\Domain\Message\{DomainMessageBusInterface, MessageDispatchingTrait}; -use MsgPhp\User\Command\CreateUserEmailCommand; +use MsgPhp\User\Command\AddUserEmailCommand; use MsgPhp\User\Entity\{User, UserEmail}; -use MsgPhp\User\Event\UserEmailCreatedEvent; +use MsgPhp\User\Event\UserEmailAddedEvent; use MsgPhp\User\Repository\UserEmailRepositoryInterface; /** * @author Roland Franssen */ -final class CreateUserEmailHandler +final class AddUserEmailHandler { use MessageDispatchingTrait; @@ -27,7 +27,7 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB $this->repository = $repository; } - public function __invoke(CreateUserEmailCommand $command): void + public function __invoke(AddUserEmailCommand $command): void { $userId = $this->factory->identify(User::class, $command->userId); $userEmail = $this->factory->create(UserEmail::class, [ @@ -36,6 +36,6 @@ public function __invoke(CreateUserEmailCommand $command): void ] + $command->context); $this->repository->save($userEmail); - $this->dispatch(UserEmailCreatedEvent::class, [$userEmail]); + $this->dispatch(UserEmailAddedEvent::class, [$userEmail]); } } diff --git a/src/User/Entity/Fields/EmailsField.php b/src/User/Entity/Fields/EmailsField.php index fc1d0983..187f5e81 100644 --- a/src/User/Entity/Fields/EmailsField.php +++ b/src/User/Entity/Fields/EmailsField.php @@ -16,6 +16,9 @@ trait EmailsField /** @var UserEmail[] */ private $emails = []; + /** + * @return DomainCollectionInterface|UserEmail[] + */ public function getEmails(): DomainCollectionInterface { return $this->emails instanceof DomainCollectionInterface ? $this->emails : DomainCollectionFactory::create($this->emails); diff --git a/src/User/Event/UserEmailCreatedEvent.php b/src/User/Event/UserEmailAddedEvent.php similarity index 91% rename from src/User/Event/UserEmailCreatedEvent.php rename to src/User/Event/UserEmailAddedEvent.php index 5b18dd6a..f750de1b 100644 --- a/src/User/Event/UserEmailCreatedEvent.php +++ b/src/User/Event/UserEmailAddedEvent.php @@ -9,7 +9,7 @@ /** * @author Roland Franssen */ -class UserEmailCreatedEvent +class UserEmailAddedEvent { public $userEmail; diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index c4b12dea..b33966c2 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -55,7 +55,7 @@ final class Configuration implements ConfigurationInterface ], ], Entity\UserEmail::class => [ - Command\CreateUserEmailCommand::class => true, + Command\AddUserEmailCommand::class => true, Command\DeleteUserEmailCommand::class => true, Features\CanBeConfirmed::class => [ diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index fc1bab5c..4028e3d7 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -79,7 +79,7 @@ public function load(array $configs, ContainerBuilder $container): void Command\Handler\RequestUserPasswordHandler::class, ]); ContainerHelper::removeIf($container, !$container->has(Repository\UserEmailRepositoryInterface::class), [ - Command\Handler\CreateUserEmailHandler::class, + Command\Handler\AddUserEmailHandler::class, Command\Handler\DeleteUserEmailHandler::class, ]); ContainerHelper::configureCommandMessages($container, $config['class_mapping'], $config['commands']); From d34eac2adc2c9d4e374f73672d8230b0be672842 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 11 Mar 2018 15:52:27 +0100 Subject: [PATCH 37/79] fix typo --- docs/infrastructure/uuid.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/infrastructure/uuid.md b/docs/infrastructure/uuid.md index e010b26a..b6f0752f 100644 --- a/docs/infrastructure/uuid.md +++ b/docs/infrastructure/uuid.md @@ -1,6 +1,6 @@ # Universally Unique Identifier -An overview of available infrastructural code when working with [UUID's][uuid]. +An overview of available infrastructural code when working with [UUIDs][uuid]. - Requires [ramsey/uuid] From e49694bddd9d513a2d1e2ab45c566d3169fc2037 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 11 Mar 2018 15:57:33 +0100 Subject: [PATCH 38/79] added user:role:add CLI command (#70) --- .../ContextBuilder/ClassContextBuilder.php | 74 +++++++++++++------ .../ContextBuilderInterface.php | 2 +- .../DependencyInjection/BundleHelper.php | 10 +++ .../Compiler/ResolveDomainPass.php | 7 +- .../DependencyInjection/Configuration.php | 6 +- src/User/Command/AddUserRoleCommand.php | 22 ++++++ src/User/Command/DeleteUserRoleCommand.php | 20 +++++ .../Command/Handler/AddUserRoleHandler.php | 40 ++++++++++ .../Command/Handler/DeleteUserRoleHandler.php | 42 +++++++++++ src/User/Entity/Fields/RolesField.php | 26 +++++++ src/User/Event/UserRoleAddedEvent.php | 20 +++++ src/User/Event/UserRoleDeletedEvent.php | 20 +++++ .../Console/Command/AddUserRoleCommand.php | 62 ++++++++++++++++ .../Command/ChangeUserCredentialCommand.php | 4 +- .../Console/Command/DeleteUserRoleCommand.php | 48 ++++++++++++ .../Infra/Console/Command/UserCommand.php | 18 ++--- .../Infra/Console/Command/UserRoleCommand.php | 50 +++++++++++++ .../Infra/Doctrine/EntityFieldsMapping.php | 10 ++- .../Repository/UserRoleRepository.php | 8 +- src/User/Infra/Validator/ExistingUsername.php | 2 +- src/User/Infra/Validator/UniqueUsername.php | 2 +- .../UserRoleRepositoryInterface.php | 4 +- .../DependencyInjection/Configuration.php | 10 ++- .../DependencyInjection/Extension.php | 40 ++++++---- src/UserBundle/Resources/config/console.php | 6 -- 25 files changed, 478 insertions(+), 75 deletions(-) create mode 100644 src/User/Command/AddUserRoleCommand.php create mode 100644 src/User/Command/DeleteUserRoleCommand.php create mode 100644 src/User/Command/Handler/AddUserRoleHandler.php create mode 100644 src/User/Command/Handler/DeleteUserRoleHandler.php create mode 100644 src/User/Entity/Fields/RolesField.php create mode 100644 src/User/Event/UserRoleAddedEvent.php create mode 100644 src/User/Event/UserRoleDeletedEvent.php create mode 100644 src/User/Infra/Console/Command/AddUserRoleCommand.php create mode 100644 src/User/Infra/Console/Command/DeleteUserRoleCommand.php create mode 100644 src/User/Infra/Console/Command/UserRoleCommand.php diff --git a/src/Domain/Infra/Console/ContextBuilder/ClassContextBuilder.php b/src/Domain/Infra/Console/ContextBuilder/ClassContextBuilder.php index d2ad80b5..ad6a0d72 100644 --- a/src/Domain/Infra/Console/ContextBuilder/ClassContextBuilder.php +++ b/src/Domain/Infra/Console/ContextBuilder/ClassContextBuilder.php @@ -19,6 +19,7 @@ final class ClassContextBuilder implements ContextBuilderInterface { public const ALWAYS_OPTIONAL = 1; public const NO_DEFAULTS = 2; + public const REUSE_DEFINITION = 4; private $class; private $method; @@ -43,10 +44,15 @@ public function __construct(string $class, string $method, iterable $elementProv public function configure(InputDefinition $definition): void { - $known = array_flip(array_keys($definition->getOptions() + $definition->getArguments())); + if ($this->flags & self::REUSE_DEFINITION) { + $origOptions = $definition->getOptions(); + $origArgs = $definition->getArguments(); + } else { + $origOptions = $origArgs = []; + } foreach ($this->resolve() as $argument) { - $required = false; + $isOption = true; if ('bool' === $argument['type']) { $mode = InputOption::VALUE_NONE; } elseif (self::isComplex($argument['type'])) { @@ -55,26 +61,27 @@ public function configure(InputDefinition $definition): void $mode = InputOption::VALUE_OPTIONAL; } else { $mode = InputArgument::OPTIONAL; - $required = true; + $isOption = false; } - $i = 1; - $field = $key = str_replace('_', '-', $argument['key']); - while (isset($known[$field])) { - $field = $key.++$i; - } + $field = $isOption ? str_replace('_', '-', $argument['key']) : $argument['key']; + if (!isset($origOptions[$field]) && !isset($origArgs[$field])) { + $field = self::getUniqueFieldName($definition, $field, $isOption); - if ($required) { - $definition->addArgument(new InputArgument($field, $mode, $argument['element']->description)); + if ($isOption) { + $definition->addOption(new InputOption($field, null, $mode, $argument['element']->description)); + } else { + $definition->addArgument(new InputArgument($field, $mode, $argument['element']->description)); + } } else { - $definition->addOption(new InputOption($field, null, $mode, $argument['element']->description)); + $isOption = isset($origOptions[$field]); } - $this->fieldMapping[$argument['name']] = [$field, $required]; + $this->fieldMapping[$argument['name']] = [$field, $isOption]; } } - public function getContext(InputInterface $input, StyleInterface $io, iterable $resolved = null): array + public function getContext(InputInterface $input, StyleInterface $io, array $values = [], iterable $resolved = null): array { $context = $normalizers = []; $interactive = $input->isInteractive(); @@ -82,26 +89,35 @@ public function getContext(InputInterface $input, StyleInterface $io, iterable $ foreach ($resolved ?? $this->resolve() as $argument) { $key = $argument['name']; if (null === $resolved) { - [$field, $isArg] = $this->fieldMapping[$key]; - $value = $isArg ? $input->getArgument($field) : $input->getOption($field); + [$field, $isOption] = $this->fieldMapping[$key]; + $value = $isOption ? $input->getOption($field) : $input->getArgument($field); } else { $field = $key; $value = $argument['value'] ?? null; } + if (array_key_exists($field, $values)) { + $context[$key] = $values[$field]; + continue; + } + + $isEmpty = null === $value || false === $value || [] === $value; + $given = !$isEmpty || $input->hasParameterOption('--'.$field); + /** @var ContextElement $element */ $element = $argument['element']; if (null !== $element->normalizer) { $normalizers[$key] = $element->normalizer; } - $given = $value || $input->hasParameterOption('--'.$field); $required = $argument['required'] && !($this->flags & self::ALWAYS_OPTIONAL); - if (self::isObject($type = $argument['type']) && ($required || $given)) { + if (is_array($value) && self::isObject($type = $argument['type']) && ($required || $given)) { $method = is_subclass_of($type, DomainCollectionInterface::class) || is_subclass_of($type, DomainIdInterface::class) ? 'fromValue' : '__construct'; - $context[$key] = $this->getContext($input, $io, array_map(function (array $argument, int $i) use ($type, $method, $value, $element): array { - if (array_key_exists($i, $value)) { + $context[$key] = $this->getContext($input, $io, $values[$field] ?? [], array_map(function (array $argument, int $i) use ($type, $method, $value, $element): array { + if (array_key_exists($argument['name'], $value)) { + $argument['value'] = $value[$argument['name']]; + } elseif (array_key_exists($i, $value)) { $argument['value'] = $value[$i]; } elseif ('bool' === $argument['type']) { $argument['value'] = false; @@ -117,7 +133,7 @@ public function getContext(InputInterface $input, StyleInterface $io, iterable $ continue; } - if (null !== $value && false !== $value && [] !== $value) { + if (!$isEmpty) { $context[$key] = $value; continue; } @@ -171,6 +187,18 @@ private static function isObject(?string $type): bool return null !== $type && (class_exists($type) || interface_exists($type)); } + private static function getUniqueFieldName(InputDefinition $definition, string $field, bool $isOption = true): string + { + $known = $isOption ? $definition->getOptions() : $definition->getArguments(); + $base = $field; + $i = 1; + while (isset($known[$field])) { + $field = $base.++$i; + } + + return $field; + } + private function resolve(): iterable { if (null !== $this->resolved) { @@ -179,9 +207,9 @@ private function resolve(): iterable $this->resolved = []; - foreach (ClassMethodResolver::resolve($this->classMapping[$this->class] ?? $this->class, $this->method) as $argument) { + foreach (ClassMethodResolver::resolve($class = $this->classMapping[$this->class] ?? $this->class, $this->method) as $argument) { $this->resolved[] = [ - 'element' => $this->getElement($this->class, $this->method, $argument['name']), + 'element' => $this->getElement($class, $this->method, $argument['name']), 'type' => isset($argument['type']) ? ($this->classMapping[$argument['type']] ?? $argument['type']) : null, ] + $argument; } @@ -197,7 +225,7 @@ private function getElement(string $class, string $method, string $argument): Co } } - return new ContextElement(ucfirst(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1 \\2', '\\1 \\2'), $argument))); + return new ContextElement(ucfirst(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1 \\2', '\\1 \\2'], $argument))); } private function askRequiredValue(StyleInterface $io, ContextElement $element, $default) diff --git a/src/Domain/Infra/Console/ContextBuilder/ContextBuilderInterface.php b/src/Domain/Infra/Console/ContextBuilder/ContextBuilderInterface.php index 3476a9f9..628be86f 100644 --- a/src/Domain/Infra/Console/ContextBuilder/ContextBuilderInterface.php +++ b/src/Domain/Infra/Console/ContextBuilder/ContextBuilderInterface.php @@ -15,5 +15,5 @@ interface ContextBuilderInterface { public function configure(InputDefinition $definition): void; - public function getContext(InputInterface $input, StyleInterface $io): array; + public function getContext(InputInterface $input, StyleInterface $io, array $values = []): array; } diff --git a/src/Domain/Infra/DependencyInjection/BundleHelper.php b/src/Domain/Infra/DependencyInjection/BundleHelper.php index fc715607..d59d9a2a 100644 --- a/src/Domain/Infra/DependencyInjection/BundleHelper.php +++ b/src/Domain/Infra/DependencyInjection/BundleHelper.php @@ -8,6 +8,8 @@ use Doctrine\ORM\Events as DoctrineOrmEvents; use Doctrine\ORM\Version as DoctrineOrmVersion; use MsgPhp\Domain\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra}; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -29,6 +31,14 @@ public static function initDomain(ContainerBuilder $container): void $container->registerForAutoconfiguration(ConsoleInfra\ContextBuilder\ContextElementProviderInterface::class) ->addTag('msgphp.console.context_element_provider'); + if (class_exists(ConsoleEvents::class)) { + $container->register(ConsoleInfra\ContextBuilder\ClassContextBuilder::class) + ->setPublic(false) + ->setAbstract(true) + ->setArgument('$method', '__construct') + ->setArgument('$elementProviders', new TaggedIteratorArgument('msgphp.console.context_element_provider')); + } + if (class_exists(DoctrineOrmVersion::class)) { $container->addCompilerPass(new Compiler\DoctrineObjectFieldMappingPass()); diff --git a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php index e2cf3851..eee3fb66 100644 --- a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php +++ b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php @@ -6,7 +6,7 @@ use Doctrine\ORM\EntityManagerInterface as DoctrineEntityManager; use MsgPhp\Domain\{DomainIdentityHelper, DomainIdentityMappingInterface, Factory, Message}; -use MsgPhp\Domain\Infra\{Doctrine as DoctrineInfra, InMemory as InMemoryInfra, SimpleBus as SimpleBusInfra}; +use MsgPhp\Domain\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra, InMemory as InMemoryInfra, SimpleBus as SimpleBusInfra}; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -31,6 +31,11 @@ public function process(ContainerBuilder $container): void $this->registerEntityFactory($container, $classMapping, $idClassMapping); $this->registerMessageBus($container); + if ($container->hasDefinition(ConsoleInfra\ContextBuilder\ClassContextBuilder::class)) { + $container->getDefinition(ConsoleInfra\ContextBuilder\ClassContextBuilder::class) + ->setArgument('$classMapping', $classMapping); + } + if (interface_exists(CacheWarmerInterface::class) && $container->hasParameter('msgphp.doctrine.mapping_files')) { $mappingFiles = array_merge(...$container->getParameter('msgphp.doctrine.mapping_files')); diff --git a/src/EavBundle/DependencyInjection/Configuration.php b/src/EavBundle/DependencyInjection/Configuration.php index c114eff1..5028aea8 100644 --- a/src/EavBundle/DependencyInjection/Configuration.php +++ b/src/EavBundle/DependencyInjection/Configuration.php @@ -18,12 +18,10 @@ */ final class Configuration implements ConfigurationInterface { - public const REQUIRED_AGGREGATE_ROOTS = [ + public const AGGREGATE_ROOTS = [ Entity\Attribute::class => AttributeIdInterface::class, Entity\AttributeValue::class => AttributeValueIdInterface::class, ]; - public const OPTIONAL_AGGREGATE_ROOTS = []; - public const AGGREGATE_ROOTS = self::REQUIRED_AGGREGATE_ROOTS + self::OPTIONAL_AGGREGATE_ROOTS; public const IDENTITY_MAPPING = [ Entity\Attribute::class => ['id'], Entity\AttributeValue::class => ['id'], @@ -44,7 +42,7 @@ public function getConfigTreeBuilder(): TreeBuilder $children ->classMappingNode('class_mapping') - ->requireClasses(array_keys(self::REQUIRED_AGGREGATE_ROOTS)) + ->requireClasses([Entity\Attribute::class, Entity\AttributeValue::class]) ->subClassValues() ->end() ->classMappingNode('id_type_mapping') diff --git a/src/User/Command/AddUserRoleCommand.php b/src/User/Command/AddUserRoleCommand.php new file mode 100644 index 00000000..4f9daeaa --- /dev/null +++ b/src/User/Command/AddUserRoleCommand.php @@ -0,0 +1,22 @@ + + */ +class AddUserRoleCommand +{ + public $userId; + public $roleName; + public $context; + + final public function __construct($userId, string $roleName, array $context = []) + { + $this->userId = $userId; + $this->roleName = $roleName; + $this->context = $context; + } +} diff --git a/src/User/Command/DeleteUserRoleCommand.php b/src/User/Command/DeleteUserRoleCommand.php new file mode 100644 index 00000000..084c92aa --- /dev/null +++ b/src/User/Command/DeleteUserRoleCommand.php @@ -0,0 +1,20 @@ + + */ +class DeleteUserRoleCommand +{ + public $userId; + public $roleName; + + final public function __construct($userId, string $roleName) + { + $this->userId = $userId; + $this->roleName = $roleName; + } +} diff --git a/src/User/Command/Handler/AddUserRoleHandler.php b/src/User/Command/Handler/AddUserRoleHandler.php new file mode 100644 index 00000000..cd3736c9 --- /dev/null +++ b/src/User/Command/Handler/AddUserRoleHandler.php @@ -0,0 +1,40 @@ + + */ +final class AddUserRoleHandler +{ + use MessageDispatchingTrait; + + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRoleRepositoryInterface $repository) + { + $this->factory = $factory; + $this->bus = $bus; + $this->repository = $repository; + } + + public function __invoke(AddUserRoleCommand $command): void + { + $userRole = $this->factory->create(UserRole::class, [ + 'user' => $this->factory->reference(User::class, $this->factory->identify(User::class, $command->userId)), + 'role' => $this->factory->reference(Role::class, $command->roleName), + ] + $command->context); + + $this->repository->save($userRole); + $this->dispatch(UserRoleAddedEvent::class, [$userRole]); + } +} diff --git a/src/User/Command/Handler/DeleteUserRoleHandler.php b/src/User/Command/Handler/DeleteUserRoleHandler.php new file mode 100644 index 00000000..a1e29850 --- /dev/null +++ b/src/User/Command/Handler/DeleteUserRoleHandler.php @@ -0,0 +1,42 @@ + + */ +final class DeleteUserRoleHandler +{ + use MessageDispatchingTrait; + + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRoleRepositoryInterface $repository) + { + $this->factory = $factory; + $this->bus = $bus; + $this->repository = $repository; + } + + public function __invoke(DeleteUserRoleCommand $command): void + { + try { + $userRole = $this->repository->find($this->factory->identify(User::class, $command->userId), $command->roleName); + } catch (EntityNotFoundException $e) { + return; + } + + $this->repository->delete($userRole); + $this->dispatch(UserRoleDeletedEvent::class, [$userRole]); + } +} diff --git a/src/User/Entity/Fields/RolesField.php b/src/User/Entity/Fields/RolesField.php new file mode 100644 index 00000000..855d85ff --- /dev/null +++ b/src/User/Entity/Fields/RolesField.php @@ -0,0 +1,26 @@ + + */ +trait RolesField +{ + /** @var UserRole[] */ + private $roles; + + /** + * @return DomainCollectionInterface|UserRole[] + */ + public function getRoles(): DomainCollectionInterface + { + return $this->roles instanceof DomainCollectionInterface ? $this->roles : DomainCollectionFactory::create($this->roles); + } +} diff --git a/src/User/Event/UserRoleAddedEvent.php b/src/User/Event/UserRoleAddedEvent.php new file mode 100644 index 00000000..487a06b8 --- /dev/null +++ b/src/User/Event/UserRoleAddedEvent.php @@ -0,0 +1,20 @@ + + */ +class UserRoleAddedEvent +{ + public $userRole; + + final public function __construct(UserRole $userRole) + { + $this->userRole = $userRole; + } +} diff --git a/src/User/Event/UserRoleDeletedEvent.php b/src/User/Event/UserRoleDeletedEvent.php new file mode 100644 index 00000000..b3f1dee1 --- /dev/null +++ b/src/User/Event/UserRoleDeletedEvent.php @@ -0,0 +1,20 @@ + + */ +class UserRoleDeletedEvent +{ + public $userRole; + + final public function __construct(UserRole $userRole) + { + $this->userRole = $userRole; + } +} diff --git a/src/User/Infra/Console/Command/AddUserRoleCommand.php b/src/User/Infra/Console/Command/AddUserRoleCommand.php new file mode 100644 index 00000000..338c7c37 --- /dev/null +++ b/src/User/Infra/Console/Command/AddUserRoleCommand.php @@ -0,0 +1,62 @@ + + */ +final class AddUserRoleCommand extends UserRoleCommand +{ + protected static $defaultName = 'user:role:add'; + + /** @var StyleInterface */ + private $io; + private $contextBuilder; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRepositoryInterface $userRepository, RoleRepositoryInterface $roleRepository, ContextBuilderInterface $contextBuilder) + { + $this->contextBuilder = $contextBuilder; + + parent::__construct($factory, $bus, $userRepository, $roleRepository); + } + + public function onMessageReceived($message): void + { + if ($message instanceof UserRoleAddedEvent) { + $this->io->success(sprintf('Added role "%s" to user %s', $message->userRole->getRoleName(), $message->userRole->getUser()->getCredential()->getUsername())); + } + } + + protected function configure(): void + { + parent::configure(); + + $this->setDescription('Add a user role'); + $this->contextBuilder->configure($this->getDefinition()); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io = new SymfonyStyle($input, $output); + $user = $this->getUser($input, $this->io); + $role = $this->getRole($input, $this->io); + $context = $this->contextBuilder->getContext($input, $this->io, ['user' => $user, 'role' => $role]); + + $this->dispatch(AddUserRoleDomainCommand::class, [$user->getId(), $role->getName(), $context]); + + return 0; + } +} diff --git a/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php b/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php index 29a197a5..c664aa7e 100644 --- a/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php +++ b/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php @@ -72,8 +72,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int return $this->run(new ArrayInput([ '--'.$field => null, - '--id' => true, - 'username' => $user->getId()->toString(), + '--by-id' => true, + 'user' => $user->getId()->toString(), ]), $output); } diff --git a/src/User/Infra/Console/Command/DeleteUserRoleCommand.php b/src/User/Infra/Console/Command/DeleteUserRoleCommand.php new file mode 100644 index 00000000..6a0d6bac --- /dev/null +++ b/src/User/Infra/Console/Command/DeleteUserRoleCommand.php @@ -0,0 +1,48 @@ + + */ +final class DeleteUserRoleCommand extends UserRoleCommand +{ + protected static $defaultName = 'user:role:delete'; + + /** @var StyleInterface */ + private $io; + + public function onMessageReceived($message): void + { + if ($message instanceof UserRoleDeletedEvent) { + $this->io->success(sprintf('Deleted role "%s" from user %s', $message->userRole->getRoleName(), $message->userRole->getUser()->getCredential()->getUsername())); + } + } + + protected function configure(): void + { + parent::configure(); + + $this->setDescription('Delete a user role'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io = new SymfonyStyle($input, $output); + $user = $this->getUser($input, $this->io); + $role = $this->getRole($input, $this->io); + + $this->dispatch(DeleteUserRoleDomainCommand::class, [$user->getId(), $role->getName()]); + + return 0; + } +} diff --git a/src/User/Infra/Console/Command/UserCommand.php b/src/User/Infra/Console/Command/UserCommand.php index 70dbf1fb..964d6527 100644 --- a/src/User/Infra/Console/Command/UserCommand.php +++ b/src/User/Infra/Console/Command/UserCommand.php @@ -44,26 +44,26 @@ public function onMessageReceived($message): void protected function configure(): void { $this - ->addOption('id', null, InputOption::VALUE_NONE, 'Find user by identifier') - ->addArgument('username', InputArgument::OPTIONAL, 'The username or identifier value'); + ->addOption('by-id', null, InputOption::VALUE_NONE, 'Find user by identifier') + ->addArgument('user', InputArgument::OPTIONAL, 'The username or user ID'); } protected function getUser(InputInterface $input, StyleInterface $io): User { - $byId = $input->getOption('id'); + $byId = $input->getOption('by-id'); - if (null === $username = $input->getArgument('username')) { + if (null === $value = $input->getArgument('user')) { if (!$input->isInteractive()) { - throw new \LogicException('No value provided for "username".'); + throw new \LogicException('No value provided for "user".'); } do { - $username = $io->ask($byId ? 'Identifier' : 'Username'); - } while (null === $username); + $value = $io->ask($byId ? 'Identifier' : 'Username'); + } while (null === $value); } return $byId - ? $this->repository->find($this->factory->identify(User::class, $username)) - : $this->repository->findByUsername($username); + ? $this->repository->find($this->factory->identify(User::class, $value)) + : $this->repository->findByUsername($value); } } diff --git a/src/User/Infra/Console/Command/UserRoleCommand.php b/src/User/Infra/Console/Command/UserRoleCommand.php new file mode 100644 index 00000000..d71017a5 --- /dev/null +++ b/src/User/Infra/Console/Command/UserRoleCommand.php @@ -0,0 +1,50 @@ + + */ +abstract class UserRoleCommand extends UserCommand +{ + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRepositoryInterface $userRepository, RoleRepositoryInterface $roleRepository) + { + parent::__construct($factory, $bus, $userRepository); + + $this->repository = $roleRepository; + } + + protected function configure(): void + { + parent::configure(); + + $this->addArgument('role', InputArgument::OPTIONAL, 'The role name'); + } + + protected function getRole(InputInterface $input, StyleInterface $io): Role + { + if (null === $value = $input->getArgument('role')) { + if (!$input->isInteractive()) { + throw new \LogicException('No value provided for "role".'); + } + + do { + $value = $io->ask('Role name'); + } while (null === $value); + } + + return $this->repository->find($value); + } +} diff --git a/src/User/Infra/Doctrine/EntityFieldsMapping.php b/src/User/Infra/Doctrine/EntityFieldsMapping.php index 2659d3ae..030f5078 100644 --- a/src/User/Infra/Doctrine/EntityFieldsMapping.php +++ b/src/User/Infra/Doctrine/EntityFieldsMapping.php @@ -5,7 +5,7 @@ namespace MsgPhp\User\Infra\Doctrine; use MsgPhp\Domain\Infra\Doctrine\ObjectFieldMappingProviderInterface; -use MsgPhp\User\Entity\{Credential, Features, Fields, Role, User, UserEmail}; +use MsgPhp\User\Entity\{Credential, Features, Fields, Role, User, UserEmail, UserRole}; /** * @author Roland Franssen @@ -65,6 +65,14 @@ public static function getObjectFieldMapping(): array ], ], ], + Fields\RolesField::class => [ + 'roles' => [ + 'type' => self::TYPE_ONE_TO_MANY, + 'targetEntity' => UserRole::class, + 'mappedBy' => 'user', + 'indexBy' => 'name', + ], + ], Fields\UserField::class => [ 'user' => [ 'type' => self::TYPE_MANY_TO_ONE, diff --git a/src/User/Infra/Doctrine/Repository/UserRoleRepository.php b/src/User/Infra/Doctrine/Repository/UserRoleRepository.php index 6e7e0333..9eebbd63 100644 --- a/src/User/Infra/Doctrine/Repository/UserRoleRepository.php +++ b/src/User/Infra/Doctrine/Repository/UserRoleRepository.php @@ -27,14 +27,14 @@ public function findAllByUserId(UserIdInterface $userId, int $offset = 0, int $l return $this->doFindAllByFields(['user' => $userId], $offset, $limit); } - public function find(UserIdInterface $userId, string $role): UserRole + public function find(UserIdInterface $userId, string $roleName): UserRole { - return $this->doFind(['user' => $userId, 'role' => $role]); + return $this->doFind(['user' => $userId, 'role' => $roleName]); } - public function exists(UserIdInterface $userId, string $role): bool + public function exists(UserIdInterface $userId, string $roleName): bool { - return $this->doExists(['user' => $userId, 'role' => $role]); + return $this->doExists(['user' => $userId, 'role' => $roleName]); } public function save(UserRole $userRole): void diff --git a/src/User/Infra/Validator/ExistingUsername.php b/src/User/Infra/Validator/ExistingUsername.php index 5804a319..6abf0e06 100644 --- a/src/User/Infra/Validator/ExistingUsername.php +++ b/src/User/Infra/Validator/ExistingUsername.php @@ -13,5 +13,5 @@ final class ExistingUsername extends Constraint { public const DOES_NOT_EXIST_ERROR = '4a8b28f7-a2b5-4435-9dd8-3be5188d23f0'; - public $message = 'This value is not a valid username.'; + public $message = 'This value is not valid.'; } diff --git a/src/User/Infra/Validator/UniqueUsername.php b/src/User/Infra/Validator/UniqueUsername.php index d70e276e..0f8fc5a6 100644 --- a/src/User/Infra/Validator/UniqueUsername.php +++ b/src/User/Infra/Validator/UniqueUsername.php @@ -13,5 +13,5 @@ final class UniqueUsername extends Constraint { public const IS_NOT_UNIQUE_ERROR = '37c4ba30-07ae-48e5-9767-19764e027346'; - public $message = 'This value is not a valid username.'; + public $message = 'This value is not valid.'; } diff --git a/src/User/Repository/UserRoleRepositoryInterface.php b/src/User/Repository/UserRoleRepositoryInterface.php index 46df86eb..4703b54c 100644 --- a/src/User/Repository/UserRoleRepositoryInterface.php +++ b/src/User/Repository/UserRoleRepositoryInterface.php @@ -18,9 +18,9 @@ interface UserRoleRepositoryInterface */ public function findAllByUserId(UserIdInterface $userId, int $offset = 0, int $limit = 0): DomainCollectionInterface; - public function find(UserIdInterface $userId, string $role): UserRole; + public function find(UserIdInterface $userId, string $roleName): UserRole; - public function exists(UserIdInterface $userId, string $role): bool; + public function exists(UserIdInterface $userId, string $roleName): bool; public function save(UserRole $userRole): void; diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index b33966c2..a7e043a3 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -19,11 +19,9 @@ */ final class Configuration implements ConfigurationInterface { - public const REQUIRED_AGGREGATE_ROOTS = [ + public const AGGREGATE_ROOTS = [ Entity\User::class => UserIdInterface::class, ]; - public const OPTIONAL_AGGREGATE_ROOTS = []; - public const AGGREGATE_ROOTS = self::REQUIRED_AGGREGATE_ROOTS + self::OPTIONAL_AGGREGATE_ROOTS; public const IDENTITY_MAPPING = [ Entity\Role::class => ['name'], Entity\UserAttributeValue::class => ['user', 'attributeValue'], @@ -62,6 +60,10 @@ final class Configuration implements ConfigurationInterface Command\ConfirmUserEmailCommand::class, ], ], + Entity\UserRole::class => [ + Command\AddUserRoleCommand::class => true, + Command\DeleteUserRoleCommand::class => true, + ], ]; public function getConfigTreeBuilder(): TreeBuilder @@ -71,7 +73,7 @@ public function getConfigTreeBuilder(): TreeBuilder $children ->classMappingNode('class_mapping') - ->requireClasses(array_keys(self::REQUIRED_AGGREGATE_ROOTS)) + ->requireClasses([Entity\User::class]) ->disallowClasses([CredentialInterface::class, Entity\Username::class]) ->groupClasses([Entity\Role::class, Entity\UserRole::class]) ->subClassValues() diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index 4028e3d7..7289f81c 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -82,6 +82,10 @@ public function load(array $configs, ContainerBuilder $container): void Command\Handler\AddUserEmailHandler::class, Command\Handler\DeleteUserEmailHandler::class, ]); + ContainerHelper::removeIf($container, !$container->has(Repository\UserRoleRepositoryInterface::class), [ + Command\Handler\AddUserRoleHandler::class, + Command\Handler\DeleteUserRoleHandler::class, + ]); ContainerHelper::configureCommandMessages($container, $config['class_mapping'], $config['commands']); ContainerHelper::configureEventMessages($container, $config['class_mapping'], array_map(function (string $file): string { return 'MsgPhp\\User\\Event\\'.basename($file, '.php'); @@ -119,6 +123,23 @@ public function load(array $configs, ContainerBuilder $container): void if (class_exists(ConsoleEvents::class)) { $loader->load('console.php'); + $container->getDefinition(ConsoleInfra\Command\AddUserRoleCommand::class) + ->setArgument('$contextBuilder', ContainerHelper::registerAnonymous($container, BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class, true) + ->setArgument('$class', Entity\UserRole::class) + ->setArgument('$flags', BaseConsoleInfra\ContextBuilder\ClassContextBuilder::REUSE_DEFINITION)); + + $container->getDefinition(ConsoleInfra\Command\CreateUserCommand::class) + ->setArgument('$contextBuilder', ContainerHelper::registerAnonymous($container, BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class, true) + ->setArgument('$class', Entity\User::class)); + + $container->getDefinition(ConsoleInfra\Command\ChangeUserCredentialCommand::class) + ->setArgument('$contextBuilder', ContainerHelper::registerAnonymous($container, BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class, true) + ->setArgument('$class', CredentialInterface::class) + ->setArgument('$flags', BaseConsoleInfra\ContextBuilder\ClassContextBuilder::ALWAYS_OPTIONAL | BaseConsoleInfra\ContextBuilder\ClassContextBuilder::NO_DEFAULTS)); + + ContainerHelper::removeIf($container, !$container->has(Command\Handler\AddUserRoleHandler::class), [ + ConsoleInfra\Command\AddUserRoleCommand::class, + ]); ContainerHelper::removeIf($container, !$container->has(Command\Handler\ChangeUserCredentialHandler::class), [ ConsoleInfra\Command\ChangeUserCredentialCommand::class, ]); @@ -131,6 +152,9 @@ public function load(array $configs, ContainerBuilder $container): void ContainerHelper::removeIf($container, !$container->has(Command\Handler\DeleteUserHandler::class), [ ConsoleInfra\Command\DeleteUserCommand::class, ]); + ContainerHelper::removeIf($container, !$container->has(Command\Handler\DeleteUserRoleHandler::class), [ + ConsoleInfra\Command\DeleteUserRoleCommand::class, + ]); ContainerHelper::removeIf($container, !$container->has(Command\Handler\DisableUserHandler::class), [ ConsoleInfra\Command\DisableUserCommand::class, ]); @@ -140,22 +164,6 @@ public function load(array $configs, ContainerBuilder $container): void ContainerHelper::removeIf($container, !$container->has(Repository\UsernameRepositoryInterface::class), [ ConsoleInfra\Command\SynchronizeUsernamesCommand::class, ]); - - $container->getDefinition(BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class) - ->setArgument('$classMapping', $config['class_mapping']); - - if ($container->hasDefinition(ConsoleInfra\Command\CreateUserCommand::class)) { - $container->getDefinition(ConsoleInfra\Command\CreateUserCommand::class) - ->setArgument('$contextBuilder', ContainerHelper::registerAnonymous($container, BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class, true) - ->setArgument('$class', Entity\User::class)); - } - - if ($container->hasDefinition(ConsoleInfra\Command\ChangeUserCredentialCommand::class)) { - $container->getDefinition(ConsoleInfra\Command\ChangeUserCredentialCommand::class) - ->setArgument('$contextBuilder', ContainerHelper::registerAnonymous($container, BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class, true) - ->setArgument('$class', CredentialInterface::class) - ->setArgument('$flags', BaseConsoleInfra\ContextBuilder\ClassContextBuilder::ALWAYS_OPTIONAL | BaseConsoleInfra\ContextBuilder\ClassContextBuilder::NO_DEFAULTS)); - } } } diff --git a/src/UserBundle/Resources/config/console.php b/src/UserBundle/Resources/config/console.php index 99992e50..05617681 100644 --- a/src/UserBundle/Resources/config/console.php +++ b/src/UserBundle/Resources/config/console.php @@ -4,7 +4,6 @@ use MsgPhp\Domain\Infra\Console; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; -use function Symfony\Component\DependencyInjection\Loader\Configurator\tagged; return function (ContainerConfigurator $container): void { $container->services() @@ -14,10 +13,5 @@ ->private() ->load('MsgPhp\\User\\Infra\\Console\\Command\\', '%kernel.project_dir%/vendor/msgphp/user/Infra/Console/Command') - - ->set(Console\ContextBuilder\ClassContextBuilder::class) - ->abstract() - ->arg('$method', '__construct') - ->arg('$elementProviders', tagged('msgphp.console.context_element_provider')) ; }; From 0114dea27c9709e64f5b56946d6d6024713d6419 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Tue, 13 Mar 2018 18:37:55 +0100 Subject: [PATCH 39/79] tweak docs --- docs/ddd/factory/entity-aware.md | 2 +- docs/ddd/factory/object.md | 2 +- docs/infrastructure/doctrine-orm.md | 4 ++-- docs/infrastructure/symfony-console.md | 25 +++++++++++-------------- mkdocs.yml | 10 ++++------ 5 files changed, 19 insertions(+), 24 deletions(-) diff --git a/docs/ddd/factory/entity-aware.md b/docs/ddd/factory/entity-aware.md index 4f7f980f..aed9feab 100644 --- a/docs/ddd/factory/entity-aware.md +++ b/docs/ddd/factory/entity-aware.md @@ -34,7 +34,7 @@ might be considered empty if it's not capable to calculate one upfront. ### `MsgPhp\Domain\Factory\EntityAwareFactory` A generic entity factory. It decorates any object factory and is based on a known [identity mapping](../identity-mapping.md) -as well as the entity to identifier class mapping. +as well as a known entity to identifier class mapping. - `__construct(DomainObjectFactoryInterface $factory, DomainIdentityMappingInterface $identityMapping, array $identifierMapping = [])` - `$factory`: The decorated object factory diff --git a/docs/ddd/factory/object.md b/docs/ddd/factory/object.md index 485b624c..8f7a6aeb 100644 --- a/docs/ddd/factory/object.md +++ b/docs/ddd/factory/object.md @@ -94,7 +94,7 @@ $factory = new ChainObjectFactory([new MyFactory(), new DomainObjectFactory()]); ### `MsgPhp\Domain\Factory\ClassMappingObjectFactory` A class mapping object factory. It decorates any object factory and resolves the actual class name from a provided -mapping. In case the class is not mapped it will be used as is. +mapping. It's usually used to map abstracts to concretes. In case the class is not mapped it will be used as is. - `__construct(DomainObjectFactoryInterface $factory, array $mapping)` - `$factory`: The decorated object factory diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index cc3ecfb7..fb1d5a2f 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -12,7 +12,7 @@ the identity mapping from its class metadata. - `__construct(EntityManagerInterface $em, array $classMapping = [])` - `$em`: The entity manager to use - - `$classMapping`: The class mapping (`['SourceType' => 'TargetType']`) + - `$classMapping`: Global class mapping. Usually used to map abstracts to concretes. ### Basic example @@ -102,7 +102,7 @@ with [inheritance][orm-inheritance]. - `__construct(EntityAwareFactoryInterface $factory, EntityManagerInterface $em, array $classMapping = [])` - `$factory`: The decorated factory - `$em`: The entity manager to use - - `$classMapping`: The class mapping (`['SourceType' => 'TargetType']`) + - `$classMapping`: Global class mapping. Usually used to map abstracts to concretes. ### Basic example diff --git a/docs/infrastructure/symfony-console.md b/docs/infrastructure/symfony-console.md index 436aaa00..e3d07900 100644 --- a/docs/infrastructure/symfony-console.md +++ b/docs/infrastructure/symfony-console.md @@ -7,10 +7,7 @@ An overview of available infrastructural code when using [Symfony Console][conso ## Context builder A context builder is bound to `MsgPhp\Domain\Infra\Console\ContextBuilder\ContextBuilderInterface`. Its purpose is to -(interactively) built an arbitrary array value, i.e. the context, from a CLI command. Its value can be used as e.g. a -context provided to an [object factory](../ddd/factory/object.md). - -- Blog post: [Initializing objects with CLI and the power of Symfony Console](https://medium.com/@ro0NL/initializing-objects-with-cli-and-the-power-of-symfony-console-2a008d5611f) +(interactively) built an arbitrary array value (the context) from a CLI command. ### API @@ -28,19 +25,22 @@ Resolve the actual context from the console IO. See also [`InputInterface`][api- #### `MsgPhp\Domain\Infra\Console\ContextBuilder\ClassContextBuilder` -Build a context value from any class method signature. It configures the CLI signature by mapping required class method +Builds a context based on any class method signature. It configures the CLI signature by mapping required class method arguments to command arguments, whereas optional ones are mapped to command options. -By default any command argument / option will be optional. If the actual class method argument is required and no value -is given it will be asked interactively. If interaction is not possible an exception will be thrown instead. +```bash +bin/console command --optional-argument [--] required-argument +``` + +In both cases a value is optional, if the actual class method argument is required and no value is given it will be +asked interactively. If interaction is not possible an exception will be thrown instead. - `__construct(string $class, string $method, iterable $elementProviders = [], array $classMapping = [], int $flags = 0)` - `$class / $method`: The class method to resolve - `$elementProviders`: Available context element providers (see [Providing context elements](#providing-context-elements)) - - `$classMapping`: Global class mapping which resolves `$class` or any nested class name from type info. Usually used - to map interfaces to concretes. + - `$classMapping`: Global class mapping. Usually used to map abstracts to concretes. - `$flags`: A bit mask value to toggle various flags - - `ClassContextBuilder::ALWAYS_OPTIONAL`: Always map class method argument to command options + - `ClassContextBuilder::ALWAYS_OPTIONAL`: Always map class method arguments to command options - `ClassContextBuilder::NO_DEFAULTS`: Leave out default values when calling `getContext()` ##### Providing context elements @@ -93,15 +93,12 @@ class MyCommand extends Command { $this->setName('my-command'); $this->contextBuilder->configure($this->getDefinition()); - - // The CLI usage is now: - // bin/console my-command [argument] } protected function execute(InputInterface $input,OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $context = $this->contextBuilder->getContext($input, $io); // ['argument' => 'VALUE'] + $context = $this->contextBuilder->getContext($input, $io); $object = new MyClass(...array_values($context)); // do something diff --git a/mkdocs.yml b/mkdocs.yml index ad15b9fe..3ed57c07 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,12 +35,10 @@ pages: - CQRS: message-driven/cqrs.md - Infrastructure: - UUID: infrastructure/uuid.md - - Doctrine: - - Collections: infrastructure/doctrine-collections.md - - DBAL: infrastructure/doctrine-dbal.md - - ORM: infrastructure/doctrine-orm.md - - Symfony: - - Console: infrastructure/symfony-console.md + - Doctrine Collections: infrastructure/doctrine-collections.md + - Doctrine DBAL: infrastructure/doctrine-dbal.md + - Doctrine ORM: infrastructure/doctrine-orm.md + - Symfony Console: infrastructure/symfony-console.md - Domain layers: - Architecture: domain/architecture.md - User domain: domain/user.md From bb2c1c8e475da519f10e8567dca2d2bc1f43c420 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 14 Mar 2018 18:14:50 +0100 Subject: [PATCH 40/79] cs: fix short array syntax (#112) --- .php_cs.dist | 1 + src/Domain/Factory/ClassMethodResolver.php | 2 +- src/Domain/Tests/Fixtures/Entities/BaseTestEntity.php | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.php_cs.dist b/.php_cs.dist index ba9615bd..cd622ee6 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -13,6 +13,7 @@ $rules = [ '@PHP71Migration:risky' => true, '@PHPUnit60Migration:risky' => true, 'single_import_per_statement' => false, + 'array_syntax' => ['syntax' => 'short'], ]; if (!is_dir($cacheDir)) { diff --git a/src/Domain/Factory/ClassMethodResolver.php b/src/Domain/Factory/ClassMethodResolver.php index 10c3f55e..fbedb0c1 100644 --- a/src/Domain/Factory/ClassMethodResolver.php +++ b/src/Domain/Factory/ClassMethodResolver.php @@ -62,7 +62,7 @@ public static function resolve(string $class, string $method): array return [ 'name' => $name = $param->getName(), - 'key' => strtolower(preg_replace(array('/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'), array('\\1_\\2', '\\1_\\2'), $name)), + 'key' => strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)), 'required' => $required, 'default' => $default, 'type' => $type, diff --git a/src/Domain/Tests/Fixtures/Entities/BaseTestEntity.php b/src/Domain/Tests/Fixtures/Entities/BaseTestEntity.php index cd496c55..bbad0cbc 100644 --- a/src/Domain/Tests/Fixtures/Entities/BaseTestEntity.php +++ b/src/Domain/Tests/Fixtures/Entities/BaseTestEntity.php @@ -65,7 +65,7 @@ final public static function getFields(): iterable $subset = array_shift($set); $cartesianSubset = $cartesian($set); - $result = array(); + $result = []; foreach ($subset as $value) { foreach ($cartesianSubset as $p) { array_unshift($p, $value); From a5824ad14578cacbc5f8fe5ac8a360d8332c023f Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 14 Mar 2018 21:58:45 +0100 Subject: [PATCH 41/79] handle doctrine discriminator in console (#105) --- docs/infrastructure/symfony-console.md | 72 ++++---- .../Context/ClassContextElementFactory.php | 16 ++ .../ClassContextElementFactoryInterface.php} | 6 +- .../ClassContextFactory.php} | 158 +++++------------- .../Infra/Console/Context/ContextElement.php | 93 +++++++++++ .../ContextFactoryInterface.php} | 4 +- .../Context/DoctrineEntityContextFactory.php | 84 ++++++++++ .../Console/ContextBuilder/ContextElement.php | 26 --- .../DependencyInjection/BundleHelper.php | 16 +- .../Compiler/ResolveDomainPass.php | 4 +- .../DependencyInjection/ContainerHelper.php | 18 +- .../Console/Command/AddUserRoleCommand.php | 12 +- .../Command/ChangeUserCredentialCommand.php | 12 +- .../Console/Command/CreateUserCommand.php | 12 +- .../DependencyInjection/Extension.php | 22 ++- 15 files changed, 344 insertions(+), 211 deletions(-) create mode 100644 src/Domain/Infra/Console/Context/ClassContextElementFactory.php rename src/Domain/Infra/Console/{ContextBuilder/ContextElementProviderInterface.php => Context/ClassContextElementFactoryInterface.php} (55%) rename src/Domain/Infra/Console/{ContextBuilder/ClassContextBuilder.php => Context/ClassContextFactory.php} (61%) create mode 100644 src/Domain/Infra/Console/Context/ContextElement.php rename src/Domain/Infra/Console/{ContextBuilder/ContextBuilderInterface.php => Context/ContextFactoryInterface.php} (82%) create mode 100644 src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php delete mode 100644 src/Domain/Infra/Console/ContextBuilder/ContextElement.php diff --git a/docs/infrastructure/symfony-console.md b/docs/infrastructure/symfony-console.md index e3d07900..32cd3bd9 100644 --- a/docs/infrastructure/symfony-console.md +++ b/docs/infrastructure/symfony-console.md @@ -4,29 +4,31 @@ An overview of available infrastructural code when using [Symfony Console][conso - Requires [symfony/console] -## Context builder +## Context factory -A context builder is bound to `MsgPhp\Domain\Infra\Console\ContextBuilder\ContextBuilderInterface`. Its purpose is to -(interactively) built an arbitrary array value (the context) from a CLI command. +A context factory is bound to `MsgPhp\Domain\Infra\Console\Context\ContextFactoryInterface`. Its purpose is to leverage +a CLI command in an effort to interactively built an arbitrary array value (the context). ### API #### `configure(InputDefinition $definition): void` -Configure a command input definition. See also [`InputDefinition`][api-inputdefinition]. +Configure a command input definition. See also [`InputDefinition`][api-inputdefinition]. Should be called before using +`getContext()`. --- -#### `getContext(InputInterface $input, StyleInterface $io): array` +#### `getContext(InputInterface $input, StyleInterface $io, array $values = []): array` Resolve the actual context from the console IO. See also [`InputInterface`][api-inputinterface] and [`StyleInterface`][api-styleinterface]. +Any element value provided by `$values` takes precedence and should be used as-is. ### Implementations -#### `MsgPhp\Domain\Infra\Console\ContextBuilder\ClassContextBuilder` +#### `MsgPhp\Domain\Infra\Console\Context\ClassContextFactory` -Builds a context based on any class method signature. It configures the CLI signature by mapping required class method -arguments to command arguments, whereas optional ones are mapped to command options. +Factorizes a context based on any class method signature. It configures the CLI signature by mapping required class +method arguments to command arguments, whereas optional ones are mapped to command options. ```bash bin/console command --optional-argument [--] required-argument @@ -35,27 +37,32 @@ bin/console command --optional-argument [--] required-argument In both cases a value is optional, if the actual class method argument is required and no value is given it will be asked interactively. If interaction is not possible an exception will be thrown instead. -- `__construct(string $class, string $method, iterable $elementProviders = [], array $classMapping = [], int $flags = 0)` +- `__construct(string $class, string $method, array $classMapping = [], int $flags = 0, ClassContextElementFactoryInterface $elementFactory = null)` - `$class / $method`: The class method to resolve - - `$elementProviders`: Available context element providers (see [Providing context elements](#providing-context-elements)) - `$classMapping`: Global class mapping. Usually used to map abstracts to concretes. - `$flags`: A bit mask value to toggle various flags - `ClassContextBuilder::ALWAYS_OPTIONAL`: Always map class method arguments to command options - `ClassContextBuilder::NO_DEFAULTS`: Leave out default values when calling `getContext()` + - `ClassContextBuilder::REUSE_DEFINITION`: Reuse the original input definition for matching class method + arguments + - `$elementFactory`: A custom element factory to use. See also [Customizing context elements](#customizing-context-elements). -##### Providing context elements +##### Customizing context elements -Per-element configuration can be provided by implementing a `MsgPhp\Domain\Infra\Console\ContextBuilder\ContextElementProviderInterface`. +Per-element configuration can be provided by implementing a `MsgPhp\Domain\Infra\Console\Context\ClassContextElementFactoryInterface`. -- `getElement(string $class, string $method, string $argument): ?ContextElement` - - Resolve a [`ContextElement`][api-contextelement] from a class/method/argument combination +- `getElement(string $class, string $method, string $argument): ContextElement` + - Get a custom [`ContextElement`][api-contextelement] to apply to a specific class/method/argument pair + +A default implementation is provided by `MsgPhp\Domain\Infra\Console\Context\ClassContextElementFactory` which simply +transforms argument names to human readable values so that `$argumentName` becomes `Argument Name`. ##### Basic example ```php contextBuilder = new ClassContextBuilder(MyClass::class, '__construct', [new MyContextElementProvider()]); + $this->contextFactory = new ClassContextFactory(MyObject::class, '__construct'); parent::__construct(); } @@ -92,14 +91,14 @@ class MyCommand extends Command protected function configure(): void { $this->setName('my-command'); - $this->contextBuilder->configure($this->getDefinition()); + $this->contextFactory->configure($this->getDefinition()); } protected function execute(InputInterface $input,OutputInterface $output): int { $io = new SymfonyStyle($input, $output); - $context = $this->contextBuilder->getContext($input, $io); - $object = new MyClass(...array_values($context)); + $context = $this->contextFactory->getContext($input, $io); + $object = new MyObject(...array_values($context)); // do something @@ -109,12 +108,23 @@ class MyCommand extends Command // --- USAGE --- -// $ bin/console my-command +// $ bin/console my-command [--option=OPTION] [--] [] ``` +#### `MsgPhp\Domain\Infra\Console\Context\DoctrineEntityContextFactory` + +A [Doctrine](doctrine-orm.md) entity aware context factory. It decorates any context factory. Its purpose is to +provide a discriminator value into the resulting context when working with [inheritance][orm-inheritance]. + +- `__construct(ContextFactoryInterface $factory, EntityManagerInterface $em, string $class)` + - `$factory`: The decorated context factory + - `$em`: The entity manager to use + - `$class`: The entity class to use + [console-project]: https://symfony.com/doc/current/components/console.html [symfony/console]: https://packagist.org/packages/symfony/console [api-inputdefinition]: https://api.symfony.com/master/Symfony/Component/Console/Input/InputDefinition.html [api-inputinterface]: https://api.symfony.com/master/Symfony/Component/Console/Input/InputInterface.html [api-styleinterface]: https://api.symfony.com/master/Symfony/Component/Console/Style/StyleInterface.html [api-contextelement]: https://msgphp.github.io/api/MsgPhp/Domain/Infra/Console/ContextBuilder/ContextElement.html +[orm-inheritance]: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html diff --git a/src/Domain/Infra/Console/Context/ClassContextElementFactory.php b/src/Domain/Infra/Console/Context/ClassContextElementFactory.php new file mode 100644 index 00000000..949e12dc --- /dev/null +++ b/src/Domain/Infra/Console/Context/ClassContextElementFactory.php @@ -0,0 +1,16 @@ + + */ +final class ClassContextElementFactory implements ClassContextElementFactoryInterface +{ + public function getElement(string $class, string $method, string $argument): ContextElement + { + return new ContextElement(ucfirst(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1 \\2', '\\1 \\2'], $argument))); + } +} diff --git a/src/Domain/Infra/Console/ContextBuilder/ContextElementProviderInterface.php b/src/Domain/Infra/Console/Context/ClassContextElementFactoryInterface.php similarity index 55% rename from src/Domain/Infra/Console/ContextBuilder/ContextElementProviderInterface.php rename to src/Domain/Infra/Console/Context/ClassContextElementFactoryInterface.php index 6f697994..24b05981 100644 --- a/src/Domain/Infra/Console/ContextBuilder/ContextElementProviderInterface.php +++ b/src/Domain/Infra/Console/Context/ClassContextElementFactoryInterface.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace MsgPhp\Domain\Infra\Console\ContextBuilder; +namespace MsgPhp\Domain\Infra\Console\Context; /** * @author Roland Franssen */ -interface ContextElementProviderInterface +interface ClassContextElementFactoryInterface { - public function getElement(string $class, string $method, string $argument): ?ContextElement; + public function getElement(string $class, string $method, string $argument): ContextElement; } diff --git a/src/Domain/Infra/Console/ContextBuilder/ClassContextBuilder.php b/src/Domain/Infra/Console/Context/ClassContextFactory.php similarity index 61% rename from src/Domain/Infra/Console/ContextBuilder/ClassContextBuilder.php rename to src/Domain/Infra/Console/Context/ClassContextFactory.php index ad6a0d72..2cb8b13b 100644 --- a/src/Domain/Infra/Console/ContextBuilder/ClassContextBuilder.php +++ b/src/Domain/Infra/Console/Context/ClassContextFactory.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MsgPhp\Domain\Infra\Console\ContextBuilder; +namespace MsgPhp\Domain\Infra\Console\Context; use MsgPhp\Domain\{DomainCollectionInterface, DomainIdInterface}; use MsgPhp\Domain\Factory\ClassMethodResolver; @@ -15,7 +15,7 @@ /** * @author Roland Franssen */ -final class ClassContextBuilder implements ContextBuilderInterface +final class ClassContextFactory implements ContextFactoryInterface { public const ALWAYS_OPTIONAL = 1; public const NO_DEFAULTS = 2; @@ -23,27 +23,38 @@ final class ClassContextBuilder implements ContextBuilderInterface private $class; private $method; - private $elementProviders; private $classMapping; private $flags = 0; + private $elementFactory; private $resolved; private $fieldMapping = []; private $generatedValues = []; - /** - * @param ContextElementProviderInterface[] $elementProviders - */ - public function __construct(string $class, string $method, iterable $elementProviders = [], array $classMapping = [], int $flags = 0) + public static function getUniqueFieldName(InputDefinition $definition, string $field, bool $isOption = true): string + { + $known = $isOption ? $definition->getOptions() : $definition->getArguments(); + $base = $field; + $i = 1; + while (isset($known[$field])) { + $field = $base.++$i; + } + + return $field; + } + + public function __construct(string $class, string $method, array $classMapping = [], int $flags = 0, ClassContextElementFactoryInterface $elementFactory = null) { $this->class = $class; $this->method = $method; - $this->elementProviders = $elementProviders; $this->classMapping = $classMapping; $this->flags = $flags; + $this->elementFactory = $elementFactory ?? new ClassContextElementFactory(); } public function configure(InputDefinition $definition): void { + $this->fieldMapping = []; + if ($this->flags & self::REUSE_DEFINITION) { $origOptions = $definition->getOptions(); $origArgs = $definition->getArguments(); @@ -96,8 +107,8 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val $value = $argument['value'] ?? null; } - if (array_key_exists($field, $values)) { - $context[$key] = $values[$field]; + if (array_key_exists($key, $values)) { + $context[$key] = $values[$key]; continue; } @@ -106,15 +117,11 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val /** @var ContextElement $element */ $element = $argument['element']; - if (null !== $element->normalizer) { - $normalizers[$key] = $element->normalizer; - } - $required = $argument['required'] && !($this->flags & self::ALWAYS_OPTIONAL); if (is_array($value) && self::isObject($type = $argument['type']) && ($required || $given)) { $method = is_subclass_of($type, DomainCollectionInterface::class) || is_subclass_of($type, DomainIdInterface::class) ? 'fromValue' : '__construct'; - $context[$key] = $this->getContext($input, $io, $values[$field] ?? [], array_map(function (array $argument, int $i) use ($type, $method, $value, $element): array { + $context[$key] = $this->getContext($input, $io, [], array_map(function (array $argument, int $i) use ($type, $method, $value, $element): array { if (array_key_exists($argument['name'], $value)) { $argument['value'] = $value[$argument['name']]; } elseif (array_key_exists($i, $value)) { @@ -125,7 +132,7 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val $argument['value'] = []; } - $child = $this->getElement($type, $method, $argument['name']); + $child = $this->elementFactory->getElement($type, $method, $argument['name']); $child->label = $element->label.' > '.$child->label; return ['element' => $child] + $argument; @@ -134,11 +141,17 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val } if (!$isEmpty) { - $context[$key] = $value; + $context[$key] = $element->normalize($value); continue; } - if ($required || $given) { + if ($element->generate($io, $generated)) { + $this->generatedValues[] = [$element->label, json_encode($generated)]; + $context[$key] = $element->normalize($generated); + continue; + } + + if ($required) { if (!$interactive) { throw new \LogicException(sprintf('No value provided for "%s".', $field)); } @@ -148,30 +161,16 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val } if ($this->flags & self::NO_DEFAULTS) { - unset($normalizers[$key]); continue; } - if ($this->generatedValue($element, $generated)) { - $context[$key] = $generated; - continue; - } - - $context[$key] = $argument['default']; - } - - foreach ($normalizers as $key => $normalizer) { - $context[$key] = $normalizer($context[$key], $context); + $context[$key] = $element->normalize($argument['default']); } - $generatedValues = []; - while (null !== $generatedValue = array_shift($this->generatedValues)) { - [$label, $value] = $generatedValue; - $generatedValues[] = [$label, json_encode($value)]; - } - if ($generatedValues) { + if ($this->generatedValues) { $io->note('Generated values'); - $io->table([], $generatedValues); + $io->table([], $this->generatedValues); + $this->generatedValues = []; } return $context; @@ -187,18 +186,6 @@ private static function isObject(?string $type): bool return null !== $type && (class_exists($type) || interface_exists($type)); } - private static function getUniqueFieldName(InputDefinition $definition, string $field, bool $isOption = true): string - { - $known = $isOption ? $definition->getOptions() : $definition->getArguments(); - $base = $field; - $i = 1; - while (isset($known[$field])) { - $field = $base.++$i; - } - - return $field; - } - private function resolve(): iterable { if (null !== $this->resolved) { @@ -207,9 +194,9 @@ private function resolve(): iterable $this->resolved = []; - foreach (ClassMethodResolver::resolve($class = $this->classMapping[$this->class] ?? $this->class, $this->method) as $argument) { + foreach (ClassMethodResolver::resolve($this->class, $this->method) as $argument) { $this->resolved[] = [ - 'element' => $this->getElement($class, $this->method, $argument['name']), + 'element' => $this->elementFactory->getElement($this->class, $this->method, $argument['name']), 'type' => isset($argument['type']) ? ($this->classMapping[$argument['type']] ?? $argument['type']) : null, ] + $argument; } @@ -217,75 +204,20 @@ private function resolve(): iterable return $this->resolved; } - private function getElement(string $class, string $method, string $argument): ContextElement - { - foreach ($this->elementProviders as $provider) { - if (null !== $element = $provider->getElement($class, $method, $argument)) { - return $element; - } - } - - return new ContextElement(ucfirst(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1 \\2', '\\1 \\2'], $argument))); - } - - private function askRequiredValue(StyleInterface $io, ContextElement $element, $default) + private function askRequiredValue(StyleInterface $io, ContextElement $element, $emptyValue) { - $label = $element->label; - $generated = null !== $element->generator; - - if (null === $default) { - if ($generated) { - $label .= ' (leave blank to generate a value)'; - } - - do { - if (null === $value = $element->hidden ? $io->askHidden($label) : $io->ask($label)) { - $this->generatedValue($element, $value); - } - } while (!$generated && null === $value); - - return $value; - } - - if ($generated && $io->confirm(sprintf('Generate value for "%s"?', $label))) { - $this->generatedValue($element, $value); - - return $value; - } - - if (false === $default) { - return $io->confirm($label, false); + if (null === $emptyValue) { + return $element->askString($io); } - if ([] === $default) { - $i = 0; - $value = []; - do { - $offsetLabel = $label.' ['.$i.']'; - $value[] = $element->hidden ? $io->askHidden($offsetLabel) : $io->ask($offsetLabel); - ++$i; - } while ($io->confirm('Add another value?', false)); - - return $value; + if (false === $emptyValue) { + return $element->askBool($io); } - return $default; - } - - private function generatedValue(ContextElement $element, &$generated): bool - { - if (null === $element->generator) { - $generated = null; - $result = false; - } else { - $generated = ($element->generator)(); - $result = true; - - $this->generatedValues[] = [$element->label, $generated]; + if ([] === $emptyValue) { + return $element->askIterable($io); } - unset($generated); - - return $result; + return $emptyValue; } } diff --git a/src/Domain/Infra/Console/Context/ContextElement.php b/src/Domain/Infra/Console/Context/ContextElement.php new file mode 100644 index 00000000..1ea421c3 --- /dev/null +++ b/src/Domain/Infra/Console/Context/ContextElement.php @@ -0,0 +1,93 @@ + + */ +final class ContextElement +{ + public $label; + public $description; + private $hide = false; + private $generator; + private $normalizer; + + public function __construct(string $label, string $description = '') + { + $this->label = $label; + $this->description = $description; + } + + public function hide(bool $flag = true): self + { + $this->hide = $flag; + + return $this; + } + + public function generator(callable $generator): self + { + $this->generator = $generator; + + return $this; + } + + public function normalizer(callable $normalizer): self + { + $this->normalizer = $normalizer; + + return $this; + } + + public function normalize($value) + { + return null === $this->normalizer ? $value : ($this->normalizer)($value); + } + + public function generate(StyleInterface $io, &$generated): bool + { + $generated = null; + $result = false; + + if (null !== $this->generator && $result = $io->confirm(sprintf('Generate a value for "%s"?', $this->label))) { + $generated = ($this->generator)(); + } + + unset($generated); + + return $result; + } + + public function askString(StyleInterface $io): string + { + do { + $value = $this->hide ? $io->askHidden($this->label) : $io->ask($this->label); + } while (null === $value); + + return $this->normalize($value); + } + + public function askBool(StyleInterface $io): bool + { + return $this->normalize($io->confirm($this->label, false)); + } + + public function askIterable(StyleInterface $io): bool + { + $i = 0; + $value = []; + + do { + $label = $this->label.' ['.$i.']'; + $value[] = $this->hide ? $io->askHidden($label) : $io->ask($label); + ++$i; + } while ($io->confirm('Add another value?', false)); + + return $this->normalize($value); + } +} diff --git a/src/Domain/Infra/Console/ContextBuilder/ContextBuilderInterface.php b/src/Domain/Infra/Console/Context/ContextFactoryInterface.php similarity index 82% rename from src/Domain/Infra/Console/ContextBuilder/ContextBuilderInterface.php rename to src/Domain/Infra/Console/Context/ContextFactoryInterface.php index 628be86f..e5a9de62 100644 --- a/src/Domain/Infra/Console/ContextBuilder/ContextBuilderInterface.php +++ b/src/Domain/Infra/Console/Context/ContextFactoryInterface.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace MsgPhp\Domain\Infra\Console\ContextBuilder; +namespace MsgPhp\Domain\Infra\Console\Context; use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; @@ -11,7 +11,7 @@ /** * @author Roland Franssen */ -interface ContextBuilderInterface +interface ContextFactoryInterface { public function configure(InputDefinition $definition): void; diff --git a/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php b/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php new file mode 100644 index 00000000..387be350 --- /dev/null +++ b/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php @@ -0,0 +1,84 @@ + + */ +final class DoctrineEntityContextFactory implements ContextFactoryInterface +{ + private $factory; + private $em; + private $class; + private $discriminatorField; + + public function __construct(ContextFactoryInterface $factory, EntityManagerInterface $em, string $class) + { + $this->factory = $factory; + $this->em = $em; + $this->class = $class; + } + + public function configure(InputDefinition $definition): void + { + $this->discriminatorField = null; + $metadata = $this->getMetadata(); + + if (isset($metadata->discriminatorColumn['fieldName'])) { + $definition->addOption(new InputOption( + $this->discriminatorField = ClassContextFactory::getUniqueFieldName($definition, $metadata->discriminatorColumn['fieldName']), + null, + InputOption::VALUE_OPTIONAL, + 'The entity discriminator value' + )); + } + + $this->factory->configure($definition); + } + + public function getContext(InputInterface $input, StyleInterface $io, array $values = []): array + { + $context = []; + + if (null !== $this->discriminatorField) { + $metadata = $this->getMetadata(); + $key = $metadata->discriminatorColumn['fieldName']; + + if (isset($values[$key])) { + $context[$key] = $values[$key]; + unset($values[$key]); + } elseif (null === $value = $input->getOption($this->discriminatorField)) { + $context[$key] = $io->choice('Select entity discriminator', array_keys($metadata->discriminatorMap), $metadata->discriminatorValue); + } elseif (isset($metadata->discriminatorMap[$value])) { + $context[$key] = $value; + } elseif (false !== $found = array_search($value, $metadata->discriminatorMap, true)) { + $context[$key] = $found; + } else { + throw new \LogicException(sprintf('Invalid entity discriminator "%s" provided.', $value)); + } + + // @todo add feature to ask additional context values, required by the provided discriminator class + } + + return $context + $this->factory->getContext($input, $io, $values); + } + + private function getMetadata(): ClassMetadata + { + if (!class_exists($this->class) || $this->em->getMetadataFactory()->isTransient($this->class)) { + throw InvalidClassException::create($this->class); + } + + return $this->em->getClassMetadata($this->class); + } +} diff --git a/src/Domain/Infra/Console/ContextBuilder/ContextElement.php b/src/Domain/Infra/Console/ContextBuilder/ContextElement.php deleted file mode 100644 index 456f0450..00000000 --- a/src/Domain/Infra/Console/ContextBuilder/ContextElement.php +++ /dev/null @@ -1,26 +0,0 @@ - - */ -final class ContextElement -{ - public $label; - public $description; - public $normalizer; - public $generator; - public $hidden; - - public function __construct(string $label, string $description = '', callable $normalizer = null, callable $generator = null, bool $hidden = false) - { - $this->label = $label; - $this->description = $description; - $this->normalizer = $normalizer; - $this->generator = $generator; - $this->hidden = $hidden; - } -} diff --git a/src/Domain/Infra/DependencyInjection/BundleHelper.php b/src/Domain/Infra/DependencyInjection/BundleHelper.php index d59d9a2a..3629d5ce 100644 --- a/src/Domain/Infra/DependencyInjection/BundleHelper.php +++ b/src/Domain/Infra/DependencyInjection/BundleHelper.php @@ -9,7 +9,7 @@ use Doctrine\ORM\Version as DoctrineOrmVersion; use MsgPhp\Domain\Infra\{Console as ConsoleInfra, Doctrine as DoctrineInfra}; use Symfony\Component\Console\ConsoleEvents; -use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; +use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -28,15 +28,17 @@ public static function initDomain(ContainerBuilder $container): void return; } - $container->registerForAutoconfiguration(ConsoleInfra\ContextBuilder\ContextElementProviderInterface::class) - ->addTag('msgphp.console.context_element_provider'); - if (class_exists(ConsoleEvents::class)) { - $container->register(ConsoleInfra\ContextBuilder\ClassContextBuilder::class) + $container->register(ConsoleInfra\Context\ClassContextFactory::class) ->setPublic(false) ->setAbstract(true) - ->setArgument('$method', '__construct') - ->setArgument('$elementProviders', new TaggedIteratorArgument('msgphp.console.context_element_provider')); + ->setAutowired(true) + ->setArgument('$method', '__construct'); + + $container->register(ConsoleInfra\Context\ClassContextElementFactory::class) + ->setPublic(false); + + $container->setAlias(ConsoleInfra\Context\ClassContextElementFactoryInterface::class, new Alias(ConsoleInfra\Context\ClassContextElementFactory::class, false)); } if (class_exists(DoctrineOrmVersion::class)) { diff --git a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php index eee3fb66..c4638978 100644 --- a/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php +++ b/src/Domain/Infra/DependencyInjection/Compiler/ResolveDomainPass.php @@ -31,8 +31,8 @@ public function process(ContainerBuilder $container): void $this->registerEntityFactory($container, $classMapping, $idClassMapping); $this->registerMessageBus($container); - if ($container->hasDefinition(ConsoleInfra\ContextBuilder\ClassContextBuilder::class)) { - $container->getDefinition(ConsoleInfra\ContextBuilder\ClassContextBuilder::class) + if ($container->hasDefinition(ConsoleInfra\Context\ClassContextFactory::class)) { + $container->getDefinition(ConsoleInfra\Context\ClassContextFactory::class) ->setArgument('$classMapping', $classMapping); } diff --git a/src/Domain/Infra/DependencyInjection/ContainerHelper.php b/src/Domain/Infra/DependencyInjection/ContainerHelper.php index 62a39474..7698307d 100644 --- a/src/Domain/Infra/DependencyInjection/ContainerHelper.php +++ b/src/Domain/Infra/DependencyInjection/ContainerHelper.php @@ -9,7 +9,7 @@ use Doctrine\ORM\Version as DoctrineOrmVersion; use Ramsey\Uuid\Doctrine as DoctrineUuid; use SimpleBus\SymfonyBridge\SimpleBusCommandBusBundle; -use MsgPhp\Domain\Infra\SimpleBus as SimpleBusInfra; +use MsgPhp\Domain\Infra\{Console as ConsoleInfra, SimpleBus as SimpleBusInfra}; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Container; @@ -84,6 +84,22 @@ public static function registerAnonymous(ContainerBuilder $container, string $cl return $container->setDefinition($class.'.'.ContainerBuilder::hash(__METHOD__.++self::$counter), $definition); } + public static function registerConsoleClassContextFactory(ContainerBuilder $container, string $class, int $flags = 0): Definition + { + $definition = self::registerAnonymous($container, ConsoleInfra\Context\ClassContextFactory::class, true) + ->setArgument('$class', $class) + ->setArgument('$flags', $flags); + + if (class_exists(DoctrineOrmVersion::class) && self::hasBundle($container, DoctrineBundle::class)) { + $definition = self::registerAnonymous($container, ConsoleInfra\Context\DoctrineEntityContextFactory::class) + ->setAutowired(true) + ->setArgument('$factory', $definition) + ->setArgument('$class', $class); + } + + return $definition; + } + public static function configureIdentityMapping(ContainerBuilder $container, array $classMapping, array $identityMapping): void { foreach ($identityMapping as $class => $mapping) { diff --git a/src/User/Infra/Console/Command/AddUserRoleCommand.php b/src/User/Infra/Console/Command/AddUserRoleCommand.php index 338c7c37..0bc5cb7e 100644 --- a/src/User/Infra/Console/Command/AddUserRoleCommand.php +++ b/src/User/Infra/Console/Command/AddUserRoleCommand.php @@ -5,7 +5,7 @@ namespace MsgPhp\User\Infra\Console\Command; use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; -use MsgPhp\Domain\Infra\Console\ContextBuilder\ContextBuilderInterface; +use MsgPhp\Domain\Infra\Console\Context\ContextFactoryInterface; use MsgPhp\Domain\Message\DomainMessageBusInterface; use MsgPhp\User\Command\AddUserRoleCommand as AddUserRoleDomainCommand; use MsgPhp\User\Event\UserRoleAddedEvent; @@ -24,11 +24,11 @@ final class AddUserRoleCommand extends UserRoleCommand /** @var StyleInterface */ private $io; - private $contextBuilder; + private $contextFactory; - public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRepositoryInterface $userRepository, RoleRepositoryInterface $roleRepository, ContextBuilderInterface $contextBuilder) + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRepositoryInterface $userRepository, RoleRepositoryInterface $roleRepository, ContextFactoryInterface $contextFactory) { - $this->contextBuilder = $contextBuilder; + $this->contextFactory = $contextFactory; parent::__construct($factory, $bus, $userRepository, $roleRepository); } @@ -45,7 +45,7 @@ protected function configure(): void parent::configure(); $this->setDescription('Add a user role'); - $this->contextBuilder->configure($this->getDefinition()); + $this->contextFactory->configure($this->getDefinition()); } protected function execute(InputInterface $input, OutputInterface $output): int @@ -53,7 +53,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->io = new SymfonyStyle($input, $output); $user = $this->getUser($input, $this->io); $role = $this->getRole($input, $this->io); - $context = $this->contextBuilder->getContext($input, $this->io, ['user' => $user, 'role' => $role]); + $context = $this->contextFactory->getContext($input, $this->io, ['user' => $user, 'role' => $role]); $this->dispatch(AddUserRoleDomainCommand::class, [$user->getId(), $role->getName(), $context]); diff --git a/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php b/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php index c664aa7e..d52fa574 100644 --- a/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php +++ b/src/User/Infra/Console/Command/ChangeUserCredentialCommand.php @@ -5,7 +5,7 @@ namespace MsgPhp\User\Infra\Console\Command; use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; -use MsgPhp\Domain\Infra\Console\ContextBuilder\ContextBuilderInterface; +use MsgPhp\Domain\Infra\Console\Context\ContextFactoryInterface; use MsgPhp\Domain\Message\DomainMessageBusInterface; use MsgPhp\User\Command\ChangeUserCredentialCommand as ChangeUserCredentialDomainCommand; use MsgPhp\User\Event\UserCredentialChangedEvent; @@ -25,12 +25,12 @@ final class ChangeUserCredentialCommand extends UserCommand /** @var StyleInterface */ private $io; - private $contextBuilder; + private $contextFactory; private $fields = []; - public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRepositoryInterface $repository, ContextBuilderInterface $contextBuilder) + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserRepositoryInterface $repository, ContextFactoryInterface $contextFactory) { - $this->contextBuilder = $contextBuilder; + $this->contextFactory = $contextFactory; parent::__construct($factory, $bus, $repository); } @@ -57,7 +57,7 @@ protected function configure(): void $definition = $this->getDefinition(); $currentFields = array_keys($definition->getOptions() + $definition->getArguments()); - $this->contextBuilder->configure($this->getDefinition()); + $this->contextFactory->configure($this->getDefinition()); $this->fields = array_values(array_diff(array_keys($definition->getOptions() + $definition->getArguments()), $currentFields)); } @@ -65,7 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $this->io = new SymfonyStyle($input, $output); $user = $this->getUser($input, $this->io); - $context = $this->contextBuilder->getContext($input, $this->io); + $context = $this->contextFactory->getContext($input, $this->io); if (!$context) { $field = $this->io->choice('Select a field to change', $this->fields); diff --git a/src/User/Infra/Console/Command/CreateUserCommand.php b/src/User/Infra/Console/Command/CreateUserCommand.php index 4e62ebb3..6b6fe100 100644 --- a/src/User/Infra/Console/Command/CreateUserCommand.php +++ b/src/User/Infra/Console/Command/CreateUserCommand.php @@ -5,7 +5,7 @@ namespace MsgPhp\User\Infra\Console\Command; use MsgPhp\Domain\Factory\DomainObjectFactoryInterface; -use MsgPhp\Domain\Infra\Console\ContextBuilder\ContextBuilderInterface; +use MsgPhp\Domain\Infra\Console\Context\ContextFactoryInterface; use MsgPhp\Domain\Message\{DomainMessageBusInterface, MessageDispatchingTrait, MessageReceivingInterface}; use MsgPhp\User\Command\CreateUserCommand as CreateUserDomainCommand; use MsgPhp\User\Event\UserCreatedEvent; @@ -24,16 +24,16 @@ final class CreateUserCommand extends Command implements MessageReceivingInterfa protected static $defaultName = 'user:create'; - private $contextBuilder; + private $contextFactory; /** @var StyleInterface */ private $io; - public function __construct(DomainObjectFactoryInterface $factory, DomainMessageBusInterface $bus, ContextBuilderInterface $contextBuilder) + public function __construct(DomainObjectFactoryInterface $factory, DomainMessageBusInterface $bus, ContextFactoryInterface $contextFactory) { $this->factory = $factory; $this->bus = $bus; - $this->contextBuilder = $contextBuilder; + $this->contextFactory = $contextFactory; parent::__construct(); } @@ -53,13 +53,13 @@ protected function configure(): void parent::configure(); $this->setDescription('Create a user'); - $this->contextBuilder->configure($this->getDefinition()); + $this->contextFactory->configure($this->getDefinition()); } protected function execute(InputInterface $input, OutputInterface $output): int { $this->io = new SymfonyStyle($input, $output); - $context = $this->contextBuilder->getContext($input, $this->io); + $context = $this->contextFactory->getContext($input, $this->io); $this->dispatch(CreateUserDomainCommand::class, [$context]); diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index 7289f81c..b42f4113 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -124,18 +124,24 @@ public function load(array $configs, ContainerBuilder $container): void $loader->load('console.php'); $container->getDefinition(ConsoleInfra\Command\AddUserRoleCommand::class) - ->setArgument('$contextBuilder', ContainerHelper::registerAnonymous($container, BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class, true) - ->setArgument('$class', Entity\UserRole::class) - ->setArgument('$flags', BaseConsoleInfra\ContextBuilder\ClassContextBuilder::REUSE_DEFINITION)); + ->setArgument('$contextFactory', ContainerHelper::registerConsoleClassContextFactory( + $container, + $config['class_mapping'][Entity\UserRole::class] ?? Entity\UserRole::class, + BaseConsoleInfra\Context\ClassContextFactory::REUSE_DEFINITION + )); $container->getDefinition(ConsoleInfra\Command\CreateUserCommand::class) - ->setArgument('$contextBuilder', ContainerHelper::registerAnonymous($container, BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class, true) - ->setArgument('$class', Entity\User::class)); + ->setArgument('$contextFactory', ContainerHelper::registerConsoleClassContextFactory( + $container, + $config['class_mapping'][Entity\User::class] + )); $container->getDefinition(ConsoleInfra\Command\ChangeUserCredentialCommand::class) - ->setArgument('$contextBuilder', ContainerHelper::registerAnonymous($container, BaseConsoleInfra\ContextBuilder\ClassContextBuilder::class, true) - ->setArgument('$class', CredentialInterface::class) - ->setArgument('$flags', BaseConsoleInfra\ContextBuilder\ClassContextBuilder::ALWAYS_OPTIONAL | BaseConsoleInfra\ContextBuilder\ClassContextBuilder::NO_DEFAULTS)); + ->setArgument('$contextFactory', ContainerHelper::registerConsoleClassContextFactory( + $container, + $config['class_mapping'][CredentialInterface::class], + BaseConsoleInfra\Context\ClassContextFactory::ALWAYS_OPTIONAL | BaseConsoleInfra\Context\ClassContextFactory::NO_DEFAULTS + )); ContainerHelper::removeIf($container, !$container->has(Command\Handler\AddUserRoleHandler::class), [ ConsoleInfra\Command\AddUserRoleCommand::class, From 7279c60ed8d136f2613402c0b07fa809ca002c33 Mon Sep 17 00:00:00 2001 From: DevMDamien Date: Thu, 15 Mar 2018 19:09:49 +0100 Subject: [PATCH 42/79] added JWT specific security user provider (#113) --- .../Security/Jwt/SecurityUserProvider.php | 66 +++++++++++++++++++ src/User/composer.json | 1 + .../DependencyInjection/Extension.php | 1 + src/UserBundle/Resources/config/security.php | 5 ++ 4 files changed, 73 insertions(+) create mode 100644 src/User/Infra/Security/Jwt/SecurityUserProvider.php diff --git a/src/User/Infra/Security/Jwt/SecurityUserProvider.php b/src/User/Infra/Security/Jwt/SecurityUserProvider.php new file mode 100644 index 00000000..19c95f6b --- /dev/null +++ b/src/User/Infra/Security/Jwt/SecurityUserProvider.php @@ -0,0 +1,66 @@ + + */ +final class SecurityUserProvider implements UserProviderWithPayloadSupportsInterface +{ + private $provider; + private $repository; + private $factory; + private $roleProvider; + + public function __construct(BaseSecurityUserProvider $provider, UserRepositoryInterface $repository, EntityAwareFactoryInterface $factory, UserRolesProviderInterface $roleProvider = null) + { + $this->provider = $provider; + $this->repository = $repository; + $this->factory = $factory; + $this->roleProvider = $roleProvider; + } + + public function loadUserByUsernameAndPayload($username, array $payload): UserInterface + { + try { + $user = $this->repository->find($this->factory->identify(User::class, $username)); + } catch (EntityNotFoundException $e) { + throw new UsernameNotFoundException($e->getMessage()); + } + + return $this->fromUser($user); + } + + public function loadUserByUsername($username): UserInterface + { + return $this->provider->loadUserByUsername($username); + } + + public function refreshUser(UserInterface $user): UserInterface + { + return $this->provider->refreshUser($user); + } + + public function supportsClass($class): bool + { + return $this->provider->supportsClass($class); + } + + private function fromUser(User $user): SecurityUser + { + return new SecurityUser($user, $this->roleProvider ? $this->roleProvider->getRoles($user) : []); + } +} diff --git a/src/User/composer.json b/src/User/composer.json index eb36084f..ca853001 100644 --- a/src/User/composer.json +++ b/src/User/composer.json @@ -22,6 +22,7 @@ }, "require-dev": { "doctrine/orm": "^2.6", + "lexik/jwt-authentication-bundle": "dev-master", "msgphp/eav": "^0.2", "sensio/framework-extra-bundle": "^5.1", "symfony/console": "^3.4|^4.0", diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index b42f4113..ec8161f2 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -97,6 +97,7 @@ public function load(array $configs, ContainerBuilder $container): void $loader->load('security.php'); ContainerHelper::removeIf($container, !$container->has(Repository\UserRepositoryInterface::class), [ + SecurityInfra\Jwt\SecurityUserProvider::class, SecurityInfra\SecurityUserProvider::class, SecurityInfra\UserParamConverter::class, SecurityInfra\UserValueResolver::class, diff --git a/src/UserBundle/Resources/config/security.php b/src/UserBundle/Resources/config/security.php index 5a5d891c..ea956df6 100644 --- a/src/UserBundle/Resources/config/security.php +++ b/src/UserBundle/Resources/config/security.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use Lexik\Bundle\JWTAuthenticationBundle\Security\User\UserProviderWithPayloadSupportsInterface; use MsgPhp\User\Password\PasswordHashingInterface; use MsgPhp\User\Infra\Security; use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface; @@ -32,4 +33,8 @@ $services->set(Security\UserParamConverter::class) ->tag('request.param_converter', ['converter' => Security\UserParamConverter::NAME]); } + + if (interface_exists(UserProviderWithPayloadSupportsInterface::class)) { + $services->set(Security\Jwt\SecurityUserProvider::class); + } }; From 3932d40144751bff87a7867e229ec14100572d4f Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Thu, 15 Mar 2018 21:47:23 +0100 Subject: [PATCH 43/79] minor tweaks --- src/User/Infra/Doctrine/EntityFieldsMapping.php | 1 - src/UserBundle/DependencyInjection/Configuration.php | 5 ++++- src/UserBundle/DependencyInjection/Extension.php | 6 ++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/User/Infra/Doctrine/EntityFieldsMapping.php b/src/User/Infra/Doctrine/EntityFieldsMapping.php index 030f5078..f27f2277 100644 --- a/src/User/Infra/Doctrine/EntityFieldsMapping.php +++ b/src/User/Infra/Doctrine/EntityFieldsMapping.php @@ -70,7 +70,6 @@ public static function getObjectFieldMapping(): array 'type' => self::TYPE_ONE_TO_MANY, 'targetEntity' => UserRole::class, 'mappedBy' => 'user', - 'indexBy' => 'name', ], ], Fields\UserField::class => [ diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index a7e043a3..6d133480 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -141,7 +141,10 @@ public function getConfigTreeBuilder(): TreeBuilder } } - $config['class_mapping'][CredentialInterface::class] = $userCredential['class']; + $config['class_mapping'] += [ + CredentialInterface::class => $userCredential['class'], + Entity\Username::class => $usernameLookup ? Entity\Username::class : null, + ]; $config['username_field'] = $userCredential['username_field']; $config['username_lookup'] = $usernameLookup; diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index ec8161f2..5e446d60 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -217,13 +217,11 @@ private function prepareDoctrineOrm(array $config, LoaderInterface $loader, Cont $container->getDefinition(DoctrineInfra\Repository\UsernameRepository::class) ->setArgument('$targetMapping', $config['username_lookup']); - - $config['class_mapping'][Entity\Username::class] = Entity\Username::class; } else { $container->removeDefinition(DoctrineInfra\Event\UsernameListener::class); } - ContainerHelper::configureDoctrineOrmRepositories($container, [Entity\Username::class => Entity\Username::class] + $config['class_mapping'], [ + ContainerHelper::configureDoctrineOrmRepositories($container, $config['class_mapping'], [ DoctrineInfra\Repository\RoleRepository::class => Entity\Role::class, DoctrineInfra\Repository\UserRepository::class => Entity\User::class, DoctrineInfra\Repository\UsernameRepository::class => Entity\Username::class, @@ -242,7 +240,7 @@ private static function getDoctrineMappingFiles(array $config, ContainerBuilder unset($files[$baseDir.'/User.Entity.UserAttributeValue.orm.xml']); } - if (!$config['username_lookup']) { + if (null === $config['class_mapping'][Entity\Username::class]) { unset($files[$baseDir.'/User.Entity.Username.orm.xml']); } From 72b1d254fa9f7886afc27630f902ebaa10f57dcf Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 13:17:44 +0100 Subject: [PATCH 44/79] added user attributes domain infra (#118) --- .../Command/AddUserAttributeValueCommand.php | 24 ++++++++++ .../ChangeUserAttributeValueCommand.php | 20 +++++++++ .../DeleteUserAttributeValueCommand.php | 18 ++++++++ .../Handler/AddUserAttributeValueHandler.php | 44 +++++++++++++++++++ .../ChangeUserAttributeValueHandler.php | 43 ++++++++++++++++++ .../DeleteUserAttributeValueHandler.php | 41 +++++++++++++++++ .../Event/UserAttributeValueAddedEvent.php | 20 +++++++++ .../Event/UserAttributeValueChangedEvent.php | 24 ++++++++++ .../Event/UserAttributeValueDeletedEvent.php | 20 +++++++++ .../UserAttributeValueRepository.php | 8 ++-- .../User.Entity.UserAttributeValue.orm.xml | 7 ++- .../UserAttributeValueRepositoryInterface.php | 4 +- 12 files changed, 263 insertions(+), 10 deletions(-) create mode 100644 src/User/Command/AddUserAttributeValueCommand.php create mode 100644 src/User/Command/ChangeUserAttributeValueCommand.php create mode 100644 src/User/Command/DeleteUserAttributeValueCommand.php create mode 100644 src/User/Command/Handler/AddUserAttributeValueHandler.php create mode 100644 src/User/Command/Handler/ChangeUserAttributeValueHandler.php create mode 100644 src/User/Command/Handler/DeleteUserAttributeValueHandler.php create mode 100644 src/User/Event/UserAttributeValueAddedEvent.php create mode 100644 src/User/Event/UserAttributeValueChangedEvent.php create mode 100644 src/User/Event/UserAttributeValueDeletedEvent.php diff --git a/src/User/Command/AddUserAttributeValueCommand.php b/src/User/Command/AddUserAttributeValueCommand.php new file mode 100644 index 00000000..3d54d121 --- /dev/null +++ b/src/User/Command/AddUserAttributeValueCommand.php @@ -0,0 +1,24 @@ + + */ +class AddUserAttributeValueCommand +{ + public $userId; + public $attributeId; + public $value; + public $context; + + final public function __construct($userId, $attributeId, $value, array $context = []) + { + $this->userId = $userId; + $this->attributeId = $attributeId; + $this->value = $value; + $this->context = $context; + } +} diff --git a/src/User/Command/ChangeUserAttributeValueCommand.php b/src/User/Command/ChangeUserAttributeValueCommand.php new file mode 100644 index 00000000..44d59002 --- /dev/null +++ b/src/User/Command/ChangeUserAttributeValueCommand.php @@ -0,0 +1,20 @@ + + */ +class ChangeUserAttributeValueCommand +{ + public $attributeValueId; + public $value; + + final public function __construct($attributeValueId, $value) + { + $this->attributeValueId = $attributeValueId; + $this->value = $value; + } +} diff --git a/src/User/Command/DeleteUserAttributeValueCommand.php b/src/User/Command/DeleteUserAttributeValueCommand.php new file mode 100644 index 00000000..93539aa7 --- /dev/null +++ b/src/User/Command/DeleteUserAttributeValueCommand.php @@ -0,0 +1,18 @@ + + */ +class DeleteUserAttributeValueCommand +{ + public $attributeValueId; + + final public function __construct($attributeValueId) + { + $this->attributeValueId = $attributeValueId; + } +} diff --git a/src/User/Command/Handler/AddUserAttributeValueHandler.php b/src/User/Command/Handler/AddUserAttributeValueHandler.php new file mode 100644 index 00000000..19c7b14c --- /dev/null +++ b/src/User/Command/Handler/AddUserAttributeValueHandler.php @@ -0,0 +1,44 @@ + + */ +final class AddUserAttributeValueHandler +{ + use MessageDispatchingTrait; + + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserAttributeValueRepositoryInterface $repository) + { + $this->factory = $factory; + $this->bus = $bus; + $this->repository = $repository; + } + + public function __invoke(AddUserAttributeValueCommand $command): void + { + $userAttributeValue = $this->factory->create(UserAttributeValue::class, [ + 'user' => $this->factory->reference(User::class, $this->factory->identify(User::class, $command->userId)), + 'attributeValue' => [ + 'attribute' => $this->factory->reference(Attribute::class, $command->attributeId), + 'value' => $command->value, + ], + ] + $command->context); + + $this->repository->save($userAttributeValue); + $this->dispatch(UserAttributeValueAddedEvent::class, [$userAttributeValue]); + } +} diff --git a/src/User/Command/Handler/ChangeUserAttributeValueHandler.php b/src/User/Command/Handler/ChangeUserAttributeValueHandler.php new file mode 100644 index 00000000..88296899 --- /dev/null +++ b/src/User/Command/Handler/ChangeUserAttributeValueHandler.php @@ -0,0 +1,43 @@ + + */ +final class ChangeUserAttributeValueHandler +{ + use MessageDispatchingTrait; + + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserAttributeValueRepositoryInterface $repository) + { + $this->factory = $factory; + $this->bus = $bus; + $this->repository = $repository; + } + + public function __invoke(ChangeUserAttributeValueCommand $command): void + { + $userAttributeValue = $this->repository->find($this->factory->identify(AttributeValue::class, $command->attributeValueId)); + + if ($command->value === $oldValue = $userAttributeValue->getValue()) { + return; + } + + $userAttributeValue->getAttributeValue()->changeValue($command->value); + + $this->repository->save($userAttributeValue); + $this->dispatch(UserAttributeValueChangedEvent::class, [$userAttributeValue, $oldValue, $command->value]); + } +} diff --git a/src/User/Command/Handler/DeleteUserAttributeValueHandler.php b/src/User/Command/Handler/DeleteUserAttributeValueHandler.php new file mode 100644 index 00000000..987295a4 --- /dev/null +++ b/src/User/Command/Handler/DeleteUserAttributeValueHandler.php @@ -0,0 +1,41 @@ + + */ +final class DeleteUserAttributeValueHandler +{ + use MessageDispatchingTrait; + + private $repository; + + public function __construct(EntityAwareFactoryInterface $factory, DomainMessageBusInterface $bus, UserAttributeValueRepositoryInterface $repository) + { + $this->factory = $factory; + $this->bus = $bus; + $this->repository = $repository; + } + + public function __invoke(DeleteUserAttributeValueCommand $command): void + { + try { + $userAttributeValue = $this->repository->find($command->attributeValueId); + } catch (EntityNotFoundException $e) { + return; + } + + $this->repository->delete($userAttributeValue); + $this->dispatch(UserAttributeValueDeletedEvent::class, [$userAttributeValue]); + } +} diff --git a/src/User/Event/UserAttributeValueAddedEvent.php b/src/User/Event/UserAttributeValueAddedEvent.php new file mode 100644 index 00000000..a044e277 --- /dev/null +++ b/src/User/Event/UserAttributeValueAddedEvent.php @@ -0,0 +1,20 @@ + + */ +class UserAttributeValueAddedEvent +{ + public $userAttributeValue; + + final public function __construct(UserAttributeValue $userAttributeValue) + { + $this->userAttributeValue = $userAttributeValue; + } +} diff --git a/src/User/Event/UserAttributeValueChangedEvent.php b/src/User/Event/UserAttributeValueChangedEvent.php new file mode 100644 index 00000000..82d14550 --- /dev/null +++ b/src/User/Event/UserAttributeValueChangedEvent.php @@ -0,0 +1,24 @@ + + */ +class UserAttributeValueChangedEvent +{ + public $userAttributeValue; + public $oldValue; + public $newValue; + + final public function __construct(UserAttributeValue $userAttributeValue, $oldValue, $newValue) + { + $this->userAttributeValue = $userAttributeValue; + $this->oldValue = $oldValue; + $this->newValue = $newValue; + } +} diff --git a/src/User/Event/UserAttributeValueDeletedEvent.php b/src/User/Event/UserAttributeValueDeletedEvent.php new file mode 100644 index 00000000..989b1e94 --- /dev/null +++ b/src/User/Event/UserAttributeValueDeletedEvent.php @@ -0,0 +1,20 @@ + + */ +class UserAttributeValueDeletedEvent +{ + public $userAttributeValue; + + final public function __construct(UserAttributeValue $userAttributeValue) + { + $this->userAttributeValue = $userAttributeValue; + } +} diff --git a/src/User/Infra/Doctrine/Repository/UserAttributeValueRepository.php b/src/User/Infra/Doctrine/Repository/UserAttributeValueRepository.php index 6b2b8db7..d80b773e 100644 --- a/src/User/Infra/Doctrine/Repository/UserAttributeValueRepository.php +++ b/src/User/Infra/Doctrine/Repository/UserAttributeValueRepository.php @@ -64,14 +64,14 @@ public function findAllByUserIdAndAttributeId(UserIdInterface $userId, Attribute return $this->createResultSet($qb->getQuery(), $offset, $limit); } - public function find(UserIdInterface $userId, AttributeValueIdInterface $attributeValueId): UserAttributeValue + public function find(AttributeValueIdInterface $attributeValueId): UserAttributeValue { - return $this->doFind(['user' => $userId, 'attributeValue' => $attributeValueId]); + return $this->doFind($attributeValueId); } - public function exists(UserIdInterface $userId, AttributeValueIdInterface $attributeValueId): bool + public function exists(AttributeValueIdInterface $attributeValueId): bool { - return $this->doExists(['user' => $userId, 'attributeValue' => $attributeValueId]); + return $this->doExists($attributeValueId); } public function save(UserAttributeValue $userAttributeValue): void diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserAttributeValue.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserAttributeValue.orm.xml index 23c2e67d..2c028361 100644 --- a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserAttributeValue.orm.xml +++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.UserAttributeValue.orm.xml @@ -4,16 +4,15 @@ xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> - - - + + - + diff --git a/src/User/Repository/UserAttributeValueRepositoryInterface.php b/src/User/Repository/UserAttributeValueRepositoryInterface.php index cd0a7ac9..58080531 100644 --- a/src/User/Repository/UserAttributeValueRepositoryInterface.php +++ b/src/User/Repository/UserAttributeValueRepositoryInterface.php @@ -37,9 +37,9 @@ public function findAllByUserId(UserIdInterface $userId, int $offset = 0, int $l */ public function findAllByUserIdAndAttributeId(UserIdInterface $userId, AttributeIdInterface $attributeId, int $offset = 0, int $limit = 0): DomainCollectionInterface; - public function find(UserIdInterface $userId, AttributeValueIdInterface $attributeValueId): UserAttributeValue; + public function find(AttributeValueIdInterface $attributeValueId): UserAttributeValue; - public function exists(UserIdInterface $userId, AttributeValueIdInterface $attributeValueId): bool; + public function exists(AttributeValueIdInterface $attributeValueId): bool; public function save(UserAttributeValue $userAttributeValue): void; From 2a8e1c7a91e5753a712191938ec7b01b1b785325 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 13:20:22 +0100 Subject: [PATCH 45/79] update identity mapping --- .../Doctrine/Repository/UsernameRepository.php | 17 ++++++++++++----- .../dist-mapping/User.Entity.Username.orm.xml | 7 +------ .../Repository/UsernameRepositoryInterface.php | 5 ++--- .../DependencyInjection/Configuration.php | 6 +++--- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/User/Infra/Doctrine/Repository/UsernameRepository.php b/src/User/Infra/Doctrine/Repository/UsernameRepository.php index e0ad34b4..04099f30 100644 --- a/src/User/Infra/Doctrine/Repository/UsernameRepository.php +++ b/src/User/Infra/Doctrine/Repository/UsernameRepository.php @@ -11,7 +11,6 @@ use MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait; use MsgPhp\User\Entity\{User, Username}; use MsgPhp\User\Repository\UsernameRepositoryInterface; -use MsgPhp\User\UserIdInterface; /** * @author Roland Franssen @@ -79,6 +78,14 @@ public function findAllFromTargets(int $offset = 0, int $limit = 0): DomainColle $qb->from($mapping['target'], $alias); $targetInfo[$class][] = ['user_field' => $userField, 'username_field' => $mapping['field']]; + + foreach ((array) $metadata->discriminatorMap as $discriminatorClass) { + if (isset($targetInfo[$discriminatorClass]) || isset($this->targetMapping[$discriminatorClass])) { + continue; + } + + $targetInfo[$discriminatorClass] = $targetInfo[$class]; + } } } @@ -116,14 +123,14 @@ public function findAllFromTargets(int $offset = 0, int $limit = 0): DomainColle return $offset || $limit ? $result->slice($offset, $limit) : $result; } - public function find(UserIdInterface $userId, string $username): Username + public function find(string $username): Username { - return $this->doFind(['user' => $userId, 'username' => $username]); + return $this->doFind($username); } - public function exists(UserIdInterface $userId, string $username): bool + public function exists(string $username): bool { - return $this->doExists(['user' => $userId, 'username' => $username]); + return $this->doExists($username); } public function save(Username $user): void diff --git a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Username.orm.xml b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Username.orm.xml index 6fc9ff17..cc3ef8be 100644 --- a/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Username.orm.xml +++ b/src/User/Infra/Doctrine/Resources/dist-mapping/User.Entity.Username.orm.xml @@ -4,15 +4,10 @@ xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> - - - - - - + diff --git a/src/User/Repository/UsernameRepositoryInterface.php b/src/User/Repository/UsernameRepositoryInterface.php index f58a171c..35635adc 100644 --- a/src/User/Repository/UsernameRepositoryInterface.php +++ b/src/User/Repository/UsernameRepositoryInterface.php @@ -6,7 +6,6 @@ use MsgPhp\Domain\DomainCollectionInterface; use MsgPhp\User\Entity\Username; -use MsgPhp\User\UserIdInterface; /** * @author Roland Franssen @@ -23,9 +22,9 @@ public function findAll(int $offset = 0, int $limit = 0): DomainCollectionInterf */ public function findAllFromTargets(int $offset = 0, int $limit = 0): DomainCollectionInterface; - public function find(UserIdInterface $userId, string $username): Username; + public function find(string $username): Username; - public function exists(UserIdInterface $userId, string $username): bool; + public function exists(string $username): bool; public function save(Username $username): void; diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index 6d133480..6494399d 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -24,11 +24,11 @@ final class Configuration implements ConfigurationInterface ]; public const IDENTITY_MAPPING = [ Entity\Role::class => ['name'], - Entity\UserAttributeValue::class => ['user', 'attributeValue'], + Entity\UserAttributeValue::class => ['attributeValue'], Entity\User::class => ['id'], - Entity\Username::class => ['user', 'username'], + Entity\Username::class => ['username'], Entity\UserRole::class => ['user', 'role'], - Entity\UserEmail::class => ['user', 'email'], + Entity\UserEmail::class => ['email'], ]; public const DEFAULT_ID_CLASS_MAPPING = [ UserIdInterface::class => UserId::class, From 8a6f91b32130cc8ed85d215949830b86efb4443c Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 13:46:29 +0100 Subject: [PATCH 46/79] simplify jwt user provider (#116) --- .../Security/Jwt/SecurityUserProvider.php | 13 ++------- .../Infra/Security/SecurityUserProvider.php | 12 ++++---- .../Security/SecurityUserProviderTest.php | 28 +++++++++++++------ 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/src/User/Infra/Security/Jwt/SecurityUserProvider.php b/src/User/Infra/Security/Jwt/SecurityUserProvider.php index 19c95f6b..18a2962a 100644 --- a/src/User/Infra/Security/Jwt/SecurityUserProvider.php +++ b/src/User/Infra/Security/Jwt/SecurityUserProvider.php @@ -8,9 +8,7 @@ use MsgPhp\Domain\Exception\EntityNotFoundException; use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; use MsgPhp\User\Entity\User; -use MsgPhp\User\Infra\Security\SecurityUser; use MsgPhp\User\Infra\Security\SecurityUserProvider as BaseSecurityUserProvider; -use MsgPhp\User\Infra\Security\UserRolesProviderInterface; use MsgPhp\User\Repository\UserRepositoryInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\User\UserInterface; @@ -23,14 +21,12 @@ final class SecurityUserProvider implements UserProviderWithPayloadSupportsInter private $provider; private $repository; private $factory; - private $roleProvider; - public function __construct(BaseSecurityUserProvider $provider, UserRepositoryInterface $repository, EntityAwareFactoryInterface $factory, UserRolesProviderInterface $roleProvider = null) + public function __construct(BaseSecurityUserProvider $provider, UserRepositoryInterface $repository, EntityAwareFactoryInterface $factory) { $this->provider = $provider; $this->repository = $repository; $this->factory = $factory; - $this->roleProvider = $roleProvider; } public function loadUserByUsernameAndPayload($username, array $payload): UserInterface @@ -41,7 +37,7 @@ public function loadUserByUsernameAndPayload($username, array $payload): UserInt throw new UsernameNotFoundException($e->getMessage()); } - return $this->fromUser($user); + return $this->provider->fromUser($user); } public function loadUserByUsername($username): UserInterface @@ -58,9 +54,4 @@ public function supportsClass($class): bool { return $this->provider->supportsClass($class); } - - private function fromUser(User $user): SecurityUser - { - return new SecurityUser($user, $this->roleProvider ? $this->roleProvider->getRoles($user) : []); - } } diff --git a/src/User/Infra/Security/SecurityUserProvider.php b/src/User/Infra/Security/SecurityUserProvider.php index f5160a8f..75155acf 100644 --- a/src/User/Infra/Security/SecurityUserProvider.php +++ b/src/User/Infra/Security/SecurityUserProvider.php @@ -20,13 +20,13 @@ final class SecurityUserProvider implements UserProviderInterface { private $repository; private $factory; - private $roleProvider; + private $rolesProvider; - public function __construct(UserRepositoryInterface $repository, EntityAwareFactoryInterface $factory, UserRolesProviderInterface $roleProvider = null) + public function __construct(UserRepositoryInterface $repository, EntityAwareFactoryInterface $factory, UserRolesProviderInterface $rolesProvider = null) { $this->repository = $repository; $this->factory = $factory; - $this->roleProvider = $roleProvider; + $this->rolesProvider = $rolesProvider; } public function loadUserByUsername($username): UserInterface @@ -43,7 +43,7 @@ public function loadUserByUsername($username): UserInterface public function refreshUser(UserInterface $user): UserInterface { if (!$user instanceof SecurityUser) { - throw new UnsupportedUserException(sprintf('Unsupported user "%s"', get_class($user))); + throw new UnsupportedUserException(sprintf('Unsupported user "%s".', get_class($user))); } try { @@ -60,8 +60,8 @@ public function supportsClass($class): bool return SecurityUser::class === $class; } - private function fromUser(User $user): SecurityUser + public function fromUser(User $user): SecurityUser { - return new SecurityUser($user, $this->roleProvider ? $this->roleProvider->getRoles($user) : []); + return new SecurityUser($user, $this->rolesProvider ? $this->rolesProvider->getRoles($user) : []); } } diff --git a/src/User/Tests/Infra/Security/SecurityUserProviderTest.php b/src/User/Tests/Infra/Security/SecurityUserProviderTest.php index 3c949202..239ddea2 100644 --- a/src/User/Tests/Infra/Security/SecurityUserProviderTest.php +++ b/src/User/Tests/Infra/Security/SecurityUserProviderTest.php @@ -29,21 +29,21 @@ public function testLoadUserByUsername(): void public function testLoadUserByUsernameWithRoles(): void { - $roleProvider = $this->createMock(UserRolesProviderInterface::class); - $roleProvider->expects($this->any()) + $rolesProvider = $this->createMock(UserRolesProviderInterface::class); + $rolesProvider->expects($this->any()) ->method('getRoles') - ->willReturn(['ROLE_FOO']); - $user = (new SecurityUserProvider($this->createRepository($this->createUser()), $this->createFactory(), $roleProvider))->loadUserByUsername('username'); + ->willReturn($roles = ['ROLE_FOO']); + $user = (new SecurityUserProvider($this->createRepository($this->createUser()), $this->createFactory(), $rolesProvider))->loadUserByUsername('username'); $this->assertSame('id', $user->getUsername()); - $this->assertSame(['ROLE_FOO'], $user->getRoles()); + $this->assertSame($roles, $user->getRoles()); $this->assertSame('', $user->getPassword()); $this->assertNull($user->getSalt()); } public function testLoadUserByUsernameWithUnknownUsername(): void { - $provider = new SecurityUserProvider($this->createRepository(), $this->createFactory()); + $provider = new SecurityUserProvider($this->createRepository(), $this->createMock(EntityAwareFactoryInterface::class)); $this->expectException(UsernameNotFoundException::class); @@ -70,7 +70,7 @@ public function testRefreshUserWithUnknownUser(): void public function testRefreshUserWithUnsupportedUser(): void { - $provider = new SecurityUserProvider($this->createRepository(), $this->createFactory()); + $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class), $this->createMock(EntityAwareFactoryInterface::class)); $this->expectException(UnsupportedUserException::class); @@ -79,12 +79,24 @@ public function testRefreshUserWithUnsupportedUser(): void public function testSupportsClass(): void { - $provider = new SecurityUserProvider($this->createRepository(), $this->createFactory()); + $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class), $this->createMock(EntityAwareFactoryInterface::class)); $this->assertTrue($provider->supportsClass(SecurityUser::class)); $this->assertFalse($provider->supportsClass(UserInterface::class)); } + public function testFromUser(): void + { + $rolesProvider = $this->createMock(UserRolesProviderInterface::class); + $rolesProvider->expects($this->once()) + ->method('getRoles') + ->willReturn($roles = ['ROLE_FOO']); + $provider = new SecurityUserProvider($this->createMock(UserRepositoryInterface::class), $this->createMock(EntityAwareFactoryInterface::class), $rolesProvider); + + $this->assertInstanceOf(SecurityUser::class, $user = $provider->fromUser($this->createMock(User::class))); + $this->assertSame($roles, $user->getRoles()); + } + private function createFactory(): EntityAwareFactoryInterface { $factory = $this->createMock(EntityAwareFactoryInterface::class); From da22d22c0755c313976099957546462a5434b612 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 15:02:03 +0100 Subject: [PATCH 47/79] added isUserType twig helper --- src/UserBundle/Twig/GlobalVariables.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/UserBundle/Twig/GlobalVariables.php b/src/UserBundle/Twig/GlobalVariables.php index 55e0d850..c542c2eb 100644 --- a/src/UserBundle/Twig/GlobalVariables.php +++ b/src/UserBundle/Twig/GlobalVariables.php @@ -54,6 +54,11 @@ public function getUserId(): ?UserIdInterface return $id; } + public function isUserType(User $user, string $class): bool + { + return $user instanceof $class; + } + public static function getSubscribedServices(): array { return [ From 06dea343ab26503a314e177d10bfbec574a53a37 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 15:16:53 +0100 Subject: [PATCH 48/79] added user attribute values field (#120) --- src/Eav/Entity/Fields/AttributesField.php | 39 +++++++++++++ .../Entity/Fields/AttributeValueFieldTest.php | 2 +- .../Entity/Fields/AttributesFieldTest.php | 55 +++++++++++++++++++ .../Doctrine/EntityFieldsMappingTest.php | 2 + .../Entity/Fields/AttributeValuesField.php | 29 ++++++++++ .../Infra/Doctrine/EntityFieldsMapping.php | 9 ++- .../Fields/AttributeValuesFieldTest.php | 36 ++++++++++++ .../Tests/Entity/Fields/EmailsFieldTest.php | 2 +- .../Tests/Entity/Fields/RoleFieldTest.php | 2 +- .../Tests/Entity/Fields/UserFieldTest.php | 2 +- 10 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 src/Eav/Entity/Fields/AttributesField.php create mode 100644 src/Eav/Tests/Entity/Fields/AttributesFieldTest.php create mode 100644 src/User/Entity/Fields/AttributeValuesField.php create mode 100644 src/User/Tests/Entity/Fields/AttributeValuesFieldTest.php diff --git a/src/Eav/Entity/Fields/AttributesField.php b/src/Eav/Entity/Fields/AttributesField.php new file mode 100644 index 00000000..fb723e21 --- /dev/null +++ b/src/Eav/Entity/Fields/AttributesField.php @@ -0,0 +1,39 @@ + + */ +trait AttributesField +{ + /** + * @return DomainCollectionInterface|Attribute[] + */ + public function getAttributes(): DomainCollectionInterface + { + $attributes = []; + + foreach ($this->getAttributeValues() as $attributeValue) { + $attribute = $attributeValue->getAttribute(); + if (isset($attributes[$attributeId = $attribute->getId()->toString()])) { + continue; + } + + $attributes[$attributeId] = $attribute; + } + + return DomainCollectionFactory::create(array_values($attributes)); + } + + /** + * @return DomainCollectionInterface|AttributeValue[] + */ + abstract public function getAttributeValues(): DomainCollectionInterface; +} diff --git a/src/Eav/Tests/Entity/Fields/AttributeValueFieldTest.php b/src/Eav/Tests/Entity/Fields/AttributeValueFieldTest.php index e6957431..e79df84d 100644 --- a/src/Eav/Tests/Entity/Fields/AttributeValueFieldTest.php +++ b/src/Eav/Tests/Entity/Fields/AttributeValueFieldTest.php @@ -11,7 +11,7 @@ final class AttributeValueFieldTest extends TestCase { - public function testField(): void + public function testGetAttributeValue(): void { $value = $this->createMock(AttributeValue::class); $value->expects($this->any()) diff --git a/src/Eav/Tests/Entity/Fields/AttributesFieldTest.php b/src/Eav/Tests/Entity/Fields/AttributesFieldTest.php new file mode 100644 index 00000000..d7de1740 --- /dev/null +++ b/src/Eav/Tests/Entity/Fields/AttributesFieldTest.php @@ -0,0 +1,55 @@ +createMock(AttributeValue::class); + $attributeValue1->expects($this->any()) + ->method('getAttribute') + ->willReturn($attribute1 = $this->createMock(Attribute::class)); + $attributeValue2 = $this->createMock(AttributeValue::class); + $attributeValue2->expects($this->any()) + ->method('getAttribute') + ->willReturn($attribute2 = $this->createMock(Attribute::class)); + $attribute1->expects($this->any()) + ->method('getId') + ->willReturn(new AttributeId('attr1')); + $attribute2->expects($this->any()) + ->method('getId') + ->willReturn(new AttributeId('attr2')); + $object = $this->getObject(new DomainCollection([$attributeValue1, $attributeValue2, $attributeValue1])); + + $this->assertSame([$attribute1, $attribute2], iterator_to_array($object->getAttributes())); + } + + private function getObject($value) + { + return new class($value) { + use AttributesField; + + private $value; + + public function __construct($value) + { + $this->value = $value; + } + + public function getAttributeValues(): DomainCollectionInterface + { + return $this->value; + } + }; + } +} diff --git a/src/Eav/Tests/Infra/Doctrine/EntityFieldsMappingTest.php b/src/Eav/Tests/Infra/Doctrine/EntityFieldsMappingTest.php index 8fcb10b3..c5c8b1cb 100644 --- a/src/Eav/Tests/Infra/Doctrine/EntityFieldsMappingTest.php +++ b/src/Eav/Tests/Infra/Doctrine/EntityFieldsMappingTest.php @@ -4,6 +4,7 @@ namespace MsgPhp\Eav\Tests\Infra\Doctrine; +use MsgPhp\Eav\Entity\Fields\AttributesField; use MsgPhp\Eav\Infra\Doctrine\EntityFieldsMapping; use PHPUnit\Framework\TestCase; @@ -14,6 +15,7 @@ public function testMapping(): void $available = array_flip(array_map(function (string $file): string { return 'MsgPhp\\Eav\\Entity\\'.basename(dirname($file)).'\\'.basename($file, '.php'); }, glob(dirname(dirname(dirname(__DIR__))).'/Entity/{Features,Fields}/*.php', \GLOB_BRACE))); + unset($available[AttributesField::class]); $mapping = array_keys(EntityFieldsMapping::getObjectFieldMapping()); sort($mapping); diff --git a/src/User/Entity/Fields/AttributeValuesField.php b/src/User/Entity/Fields/AttributeValuesField.php new file mode 100644 index 00000000..eeccb6cd --- /dev/null +++ b/src/User/Entity/Fields/AttributeValuesField.php @@ -0,0 +1,29 @@ + + */ +trait AttributeValuesField +{ + use AttributesField; + + /** @var UserAttributeValue[] */ + private $attributeValues = []; + + /** + * @return DomainCollectionInterface|UserAttributeValue[] + */ + public function getAttributeValues(): DomainCollectionInterface + { + return $this->attributeValues instanceof DomainCollectionInterface ? $this->attributeValues : DomainCollectionFactory::create($this->attributeValues); + } +} diff --git a/src/User/Infra/Doctrine/EntityFieldsMapping.php b/src/User/Infra/Doctrine/EntityFieldsMapping.php index f27f2277..a4129f49 100644 --- a/src/User/Infra/Doctrine/EntityFieldsMapping.php +++ b/src/User/Infra/Doctrine/EntityFieldsMapping.php @@ -5,7 +5,7 @@ namespace MsgPhp\User\Infra\Doctrine; use MsgPhp\Domain\Infra\Doctrine\ObjectFieldMappingProviderInterface; -use MsgPhp\User\Entity\{Credential, Features, Fields, Role, User, UserEmail, UserRole}; +use MsgPhp\User\Entity\{Credential, Features, Fields, Role, User, UserAttributeValue, UserEmail, UserRole}; /** * @author Roland Franssen @@ -48,6 +48,13 @@ public static function getObjectFieldMapping(): array 'nullable' => true, ], ], + Fields\AttributeValuesField::class => [ + 'attributeValues' => [ + 'type' => self::TYPE_ONE_TO_MANY, + 'targetEntity' => UserAttributeValue::class, + 'mappedBy' => 'user', + ], + ], Fields\EmailsField::class => [ 'emails' => [ 'type' => self::TYPE_ONE_TO_MANY, diff --git a/src/User/Tests/Entity/Fields/AttributeValuesFieldTest.php b/src/User/Tests/Entity/Fields/AttributeValuesFieldTest.php new file mode 100644 index 00000000..ecc00ec4 --- /dev/null +++ b/src/User/Tests/Entity/Fields/AttributeValuesFieldTest.php @@ -0,0 +1,36 @@ +getObject($attributeValues = [$this->createMock(UserAttributeValue::class)]); + + $this->assertSame($attributeValues, iterator_to_array($object->getAttributeValues())); + + $object = $this->getObject($attributeValues = $this->createMock(DomainCollectionInterface::class)); + + $this->assertSame($attributeValues, $object->getAttributeValues()); + } + + private function getObject($value) + { + return new class($value) { + use AttributeValuesField; + + public function __construct($value) + { + $this->attributeValues = $value; + } + }; + } +} diff --git a/src/User/Tests/Entity/Fields/EmailsFieldTest.php b/src/User/Tests/Entity/Fields/EmailsFieldTest.php index 463b6417..b74a3636 100644 --- a/src/User/Tests/Entity/Fields/EmailsFieldTest.php +++ b/src/User/Tests/Entity/Fields/EmailsFieldTest.php @@ -11,7 +11,7 @@ final class EmailsFieldTest extends TestCase { - public function testField(): void + public function testGetEmails(): void { $object = $this->getObject($emails = [$this->createMock(UserEmail::class)]); diff --git a/src/User/Tests/Entity/Fields/RoleFieldTest.php b/src/User/Tests/Entity/Fields/RoleFieldTest.php index 357dbc65..957654de 100644 --- a/src/User/Tests/Entity/Fields/RoleFieldTest.php +++ b/src/User/Tests/Entity/Fields/RoleFieldTest.php @@ -10,7 +10,7 @@ final class RoleFieldTest extends TestCase { - public function testField(): void + public function testGetRole(): void { $value = $this->createMock(Role::class); $value->expects($this->any()) diff --git a/src/User/Tests/Entity/Fields/UserFieldTest.php b/src/User/Tests/Entity/Fields/UserFieldTest.php index 67f3d3d9..02223ce4 100644 --- a/src/User/Tests/Entity/Fields/UserFieldTest.php +++ b/src/User/Tests/Entity/Fields/UserFieldTest.php @@ -11,7 +11,7 @@ final class UserFieldTest extends TestCase { - public function testField(): void + public function testGetUser(): void { $value = $this->createMock(User::class); $value->expects($this->any()) From 665c2ccc12f8853d7d21a0ccb74cde57cae39561 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 16:56:56 +0100 Subject: [PATCH 49/79] tweak console message --- .../Infra/Console/Context/DoctrineEntityContextFactory.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php b/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php index 387be350..fbf853a5 100644 --- a/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php +++ b/src/Domain/Infra/Console/Context/DoctrineEntityContextFactory.php @@ -58,7 +58,7 @@ public function getContext(InputInterface $input, StyleInterface $io, array $val $context[$key] = $values[$key]; unset($values[$key]); } elseif (null === $value = $input->getOption($this->discriminatorField)) { - $context[$key] = $io->choice('Select entity discriminator', array_keys($metadata->discriminatorMap), $metadata->discriminatorValue); + $context[$key] = $io->choice('Select discriminator', array_keys($metadata->discriminatorMap), $metadata->discriminatorValue); } elseif (isset($metadata->discriminatorMap[$value])) { $context[$key] = $value; } elseif (false !== $found = array_search($value, $metadata->discriminatorMap, true)) { From 389e51a3992890b3584073de3532f8280898336b Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 17:44:43 +0100 Subject: [PATCH 50/79] add user attribute wiring --- src/UserBundle/DependencyInjection/Extension.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index 5e446d60..ad95f81d 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -78,6 +78,11 @@ public function load(array $configs, ContainerBuilder $container): void Command\Handler\EnableUserHandler::class, Command\Handler\RequestUserPasswordHandler::class, ]); + ContainerHelper::removeIf($container, !$container->has(Repository\UserAttributeValueRepositoryInterface::class), [ + Command\Handler\AddUserAttributeValueHandler::class, + Command\Handler\ChangeUserAttributeValueHandler::class, + Command\Handler\DeleteUserAttributeValueHandler::class, + ]); ContainerHelper::removeIf($container, !$container->has(Repository\UserEmailRepositoryInterface::class), [ Command\Handler\AddUserEmailHandler::class, Command\Handler\DeleteUserEmailHandler::class, From 571bc4b55159b19002bd409d148cc051cb38f8e6 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 17:54:04 +0100 Subject: [PATCH 51/79] add user attribute wiring --- src/UserBundle/DependencyInjection/Configuration.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index 6494399d..e22e192d 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -52,6 +52,11 @@ final class Configuration implements ConfigurationInterface Command\RequestUserPasswordCommand::class, ], ], + Entity\UserAttributeValue::class => [ + Command\AddUserAttributeValueCommand::class => true, + Command\ChangeUserAttributeValueCommand::class => true, + Command\DeleteUserAttributeValueCommand::class => true, + ], Entity\UserEmail::class => [ Command\AddUserEmailCommand::class => true, Command\DeleteUserEmailCommand::class => true, From 0f64bbf5fa99bbc2008f3d0e61525ea2795d20ca Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 18:03:53 +0100 Subject: [PATCH 52/79] fix AddUserAttributeValueHandler --- .../Command/Handler/AddUserAttributeValueHandler.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/User/Command/Handler/AddUserAttributeValueHandler.php b/src/User/Command/Handler/AddUserAttributeValueHandler.php index 19c7b14c..91d0e360 100644 --- a/src/User/Command/Handler/AddUserAttributeValueHandler.php +++ b/src/User/Command/Handler/AddUserAttributeValueHandler.php @@ -6,7 +6,7 @@ use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; use MsgPhp\Domain\Message\{DomainMessageBusInterface, MessageDispatchingTrait}; -use MsgPhp\Eav\Entity\Attribute; +use MsgPhp\Eav\Entity\{Attribute, AttributeValue}; use MsgPhp\User\Command\AddUserAttributeValueCommand; use MsgPhp\User\Entity\{User, UserAttributeValue}; use MsgPhp\User\Event\UserAttributeValueAddedEvent; @@ -30,12 +30,17 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB public function __invoke(AddUserAttributeValueCommand $command): void { + $attributeValueContext = $command->context['attributeValue'] ?? $command->context['attribute_value'] ?? []; + if (!isset($attributeValueContext['id'])) { + $attributeValueContext['id'] = $this->factory->nextIdentifier(AttributeValue::class); + } + $userAttributeValue = $this->factory->create(UserAttributeValue::class, [ 'user' => $this->factory->reference(User::class, $this->factory->identify(User::class, $command->userId)), 'attributeValue' => [ - 'attribute' => $this->factory->reference(Attribute::class, $command->attributeId), + 'attribute' => $this->factory->reference(Attribute::class, $this->factory->identify(Attribute::class, $command->attributeId)), 'value' => $command->value, - ], + ] + $attributeValueContext, ] + $command->context); $this->repository->save($userAttributeValue); From cc3e922b606bb8d0c4f8037c9d0d521175578f6d Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 17 Mar 2018 18:04:50 +0100 Subject: [PATCH 53/79] fix DeleteUserAttributeValueHandler --- src/User/Command/Handler/DeleteUserAttributeValueHandler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/User/Command/Handler/DeleteUserAttributeValueHandler.php b/src/User/Command/Handler/DeleteUserAttributeValueHandler.php index 987295a4..dfdd6240 100644 --- a/src/User/Command/Handler/DeleteUserAttributeValueHandler.php +++ b/src/User/Command/Handler/DeleteUserAttributeValueHandler.php @@ -7,6 +7,7 @@ use MsgPhp\Domain\Exception\EntityNotFoundException; use MsgPhp\Domain\Factory\EntityAwareFactoryInterface; use MsgPhp\Domain\Message\{DomainMessageBusInterface, MessageDispatchingTrait}; +use MsgPhp\Eav\Entity\AttributeValue; use MsgPhp\User\Command\DeleteUserAttributeValueCommand; use MsgPhp\User\Event\UserAttributeValueDeletedEvent; use MsgPhp\User\Repository\UserAttributeValueRepositoryInterface; @@ -30,7 +31,7 @@ public function __construct(EntityAwareFactoryInterface $factory, DomainMessageB public function __invoke(DeleteUserAttributeValueCommand $command): void { try { - $userAttributeValue = $this->repository->find($command->attributeValueId); + $userAttributeValue = $this->repository->find($this->factory->identify(AttributeValue::class, $command->attributeValueId)); } catch (EntityNotFoundException $e) { return; } From 69d7b05f8add6ca093af8a7c87e64a6997964a26 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 11:34:42 +0100 Subject: [PATCH 54/79] bundle installation cookbook (#119) --- docs/cookbook/bundle-installation.md | 128 ++++++++++++++++++ docs/index.md | 1 + mkdocs.yml | 10 +- .../DependencyInjection/Configuration.php | 11 ++ 4 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 docs/cookbook/bundle-installation.md diff --git a/docs/cookbook/bundle-installation.md b/docs/cookbook/bundle-installation.md new file mode 100644 index 00000000..e847144b --- /dev/null +++ b/docs/cookbook/bundle-installation.md @@ -0,0 +1,128 @@ +# Bundle installation + +The project bundles are tailored to the [Symfony Framework] and designed to be used standalone. Its main purpose is to +provide the application with services, based on minimal configuration and to be used with [dependency injection]. In +general [autowiring] is leveraged in an effort to favor convention over configuration where possible. + +Various services might be created depending on other enabled bundles. If a supported bundle is enabled its available +infrastructural services are created automatically. + +## Available bundles + +- `msgphp/eav-bundle`: Provides basic entity-attribute-value management +- `msgphp/user-bundle`: Provides basic user management + +## Installing using [Composer] + +```bash +composer require msgphp/-bundle +``` + +### With Symfony Flex + +When [Symfony Flex] is used to mange your Symfony application the minimal bundle configuration is created for you +automatically based on [MsgPHP recipes]. + +## Configuration + +By default a bundle provides the following minimal configuration: + +```yaml +msgphp_: + class_mapping: [] + id_type_mapping: [] + default_id_type: integer + commands: [] +``` + +Depending on your personal preference you can also write the configuration in any other supported format. See +[demo configuration] for a more advanced example. + +### Class mapping + +Configures the bundle with a class mapping to to well which classes of yours should be used for a known class of ours. + +```yaml +msgphp_: + class_mapping: + MsgPhp\SomeClass: App\SomeClass +``` + +The class mapping applies when working with an [object factory](../ddd/factory/object.md#). + +Depending on the bundle a specific class mapping entry might enable one of the bundle its features which are otherwise +disabled by default. + +### Identifier type mapping + +Configures the bundle [domain identifier](../ddd/identifiers.md) types. Each key must be a sub class of +`MsgPhp\Domain\DomainIdInterface` whereas each value must be a known type name. + +It ensures a default class mapping entry is added which maps the identifier to a [concrete implementation](../ddd/identifiers.md#implementations). + +```yaml +msgphp_: + id_type_mapping: + MsgPhp\SomeDomainIdInterface: some_type_name +``` + +By convention any [Doctrine DBAL type] can be used. Additionally `uuid`, `uuid_binary` or `uuid_binary_ordered_time` are +also detected. + +### Default identifier type + +Configures a default identifier type name to use for all known identifiers provided by the bundle. + +```yaml +msgphp_: + default_id_type: uuid +``` + +### Commands + +By default a command handler provided by the bundle might be enabled or disabled depending on an [entity feature](../ddd/entities.md#common-entity-features) +is being used yes or no. + +However, in case of a [event-sourcing command handler](../message-driven/cqrs.md#event-sourcing-command-handler) +the corresponding [domain event](../event-sourcing/events.md) might be supported regardless. Depending on your own +[event handler](../event-sourcing/event-handlers.md) implementation. To keep leveraging default command handlers they +can be explicitly enabled or disabled by command. + +```yaml +msgphp_: + commands: + MsgPhp\SomeCommand: true + MsgPhp\SomeOtherCommand: false +``` + +## Basic configuration example + +Given a bundle provides the following identifiers: + +- `MsgPhp\FooIdInterface` +- `MsgPhp\BarIdInterface` +- `MsgPhp\BazIdInterface` + +```yaml +msgphp_: + default_id_type: uuid + id_type_mapping: + MsgPhp\FooIdInterface: integer + # implied: + # MsgPhp\BarIdInterface: uuid + # MsgPhp\BazIdInterface: uuid + class_mapping: + MsgPhp\BarIdInterface: App\MyBarUuid + # implied: + # MsgPhp\FooIdInterface: MsgPhp\Domain\DomainId + # MsgPhp\BazIdInterface: MsgPhp\Domain\Infra\Uuid +``` + +[Symfony Framework]: https://symfony.com/ +[dependency injection]: https://symfony.com/doc/current/components/dependency_injection.html +[Composer]: https://getcomposer.org/ +[Symfony Flex]: https://symfony.com/doc/current/setup/flex.html +[MsgPHP recipes]: https://github.com/symfony/recipes-contrib/tree/master/msgphp +[autowiring]: https://symfony.com/doc/current/service_container/autowiring.html +[demo configuration]: https://github.com/msgphp/symfony-demo-app/blob/master/config/packages/msgphp.php +[Doctrine DBAL type]: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html diff --git a/docs/index.md b/docs/index.md index aecb7ac0..e48f56c0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,6 +6,7 @@ # News +- **`2018-03-18`** Added initial _cookbook_ chapter - **`2018-02-18`** Added initial _infrastructure_ chapter - **`2018-02-12`** Added [domain identities](ddd/identities.md) chapter - **`2018-01-27`** Added initial _message driven_ chapter diff --git a/mkdocs.yml b/mkdocs.yml index 3ed57c07..ed8cf71b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,11 +39,5 @@ pages: - Doctrine DBAL: infrastructure/doctrine-dbal.md - Doctrine ORM: infrastructure/doctrine-orm.md - Symfony Console: infrastructure/symfony-console.md -- Domain layers: - - Architecture: domain/architecture.md - - User domain: domain/user.md - - EAV domain: domain/eav.md -- Symfony bundles: - - Architecture: symfony-bundle/architecture.md - - User bundle: symfony-bundle/user.md - - EAV bundle: symfony-bundle/eav.md +- Cookbook: + - Bundle installation: cookbook/bundle-installation.md diff --git a/src/EavBundle/DependencyInjection/Configuration.php b/src/EavBundle/DependencyInjection/Configuration.php index 5028aea8..2e3b283f 100644 --- a/src/EavBundle/DependencyInjection/Configuration.php +++ b/src/EavBundle/DependencyInjection/Configuration.php @@ -34,6 +34,7 @@ final class Configuration implements ConfigurationInterface AttributeIdInterface::class => UuidInfra\AttributeId::class, AttributeValueIdInterface::class => UuidInfra\AttributeValueId::class, ]; + private const COMMAND_MAPPING = []; public function getConfigTreeBuilder(): TreeBuilder { @@ -48,6 +49,9 @@ public function getConfigTreeBuilder(): TreeBuilder ->classMappingNode('id_type_mapping') ->subClassKeys([DomainIdInterface::class]) ->end() + ->classMappingNode('commands') + ->typeOfValues('boolean') + ->end() ->scalarNode('default_id_type') ->defaultValue(ConfigHelper::DEFAULT_ID_TYPE) ->cannotBeEmpty() @@ -58,6 +62,13 @@ public function getConfigTreeBuilder(): TreeBuilder self::DEFAULT_ID_CLASS_MAPPING, array_fill_keys(ConfigHelper::UUID_TYPES, self::UUID_CLASS_MAPPING) )) + ->end() + ->validate() + ->always(function (array $config): array { + ConfigHelper::resolveCommandMappingConfig(self::COMMAND_MAPPING, $config['class_mapping'], $config['commands']); + + return $config; + }) ->end(); return $treeBuilder; From f9392f0bb45ecef65ce2ac7e1bf66f7fbd5881a9 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 11:57:37 +0100 Subject: [PATCH 55/79] fix docs --- docs/cookbook/bundle-installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/cookbook/bundle-installation.md b/docs/cookbook/bundle-installation.md index e847144b..8b57306f 100644 --- a/docs/cookbook/bundle-installation.md +++ b/docs/cookbook/bundle-installation.md @@ -115,7 +115,7 @@ msgphp_: MsgPhp\BarIdInterface: App\MyBarUuid # implied: # MsgPhp\FooIdInterface: MsgPhp\Domain\DomainId - # MsgPhp\BazIdInterface: MsgPhp\Domain\Infra\Uuid + # MsgPhp\BazIdInterface: MsgPhp\Domain\Infra\Uuid\DomainId ``` [Symfony Framework]: https://symfony.com/ From b0bb9865ab45863b745ffd211ce6dd3584a38f9b Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 12:10:40 +0100 Subject: [PATCH 56/79] fix docs --- docs/infrastructure/doctrine-orm.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index fb1d5a2f..b947f0bf 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -52,13 +52,13 @@ use MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait; // --- SETUP --- -/** @ORM\Entity */ +/** @ORM\Entity() */ class MyEntity { - /** @ORM\Id @ORM\Column(type="string") */ + /** @ORM\Id() @ORM\Column(type="string") */ public $name; - /** @ORM\Id @ORM\Column(type="integer") */ + /** @ORM\Id() @ORM\Column(type="integer") */ public $year; } @@ -117,7 +117,7 @@ use MsgPhp\Domain\Infra\Doctrine\{DomainIdentityMapping, EntityAwareFactory}; // --- SETUP --- /** - * @ORM\Entity + * @ORM\Entity() * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="discriminator", type="string") * @ORM\DiscriminatorMap({"self" = "MyEntity", "other" = "MyOtherEntity"}) @@ -127,7 +127,7 @@ class MyEntity public const TYPE_SELF = 'self'; public const TYPE_OTHER = 'other'; - /** @ORM\Id */ + /** @ORM\Id() @ORM\Column(type="integer") */ public $id; } @@ -186,10 +186,10 @@ use MsgPhp\Domain\Infra\Doctrine\Hydration\{ScalarHydrator, SingleScalarHydrator // --- SETUP --- -/** @ORM\Entity */ +/** @ORM\Entity() */ class MyEntity { - /** @var DomainId @ORM\Id @ORM\Column(type="msgphp_domain_id") */ + /** @var DomainId @ORM\Id() @ORM\Column(type="msgphp_domain_id") */ public $id; } From 89892d48e1cdf284ddd7fbe1ef4ede6116e442db Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 12:45:22 +0100 Subject: [PATCH 57/79] setup reference chapter --- docs/index.md | 2 +- docs/reference/doctrine-identifier-types.md | 11 ++++++++++ docs/reference/identifiers.md | 24 +++++++++++++++++++++ mkdocs.yml | 3 +++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 docs/reference/doctrine-identifier-types.md create mode 100644 docs/reference/identifiers.md diff --git a/docs/index.md b/docs/index.md index e48f56c0..aaf864c5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,7 +6,7 @@ # News -- **`2018-03-18`** Added initial _cookbook_ chapter +- **`2018-03-18`** Added initial _cookbook_ and _reference_ chapters - **`2018-02-18`** Added initial _infrastructure_ chapter - **`2018-02-12`** Added [domain identities](ddd/identities.md) chapter - **`2018-01-27`** Added initial _message driven_ chapter diff --git a/docs/reference/doctrine-identifier-types.md b/docs/reference/doctrine-identifier-types.md new file mode 100644 index 00000000..a19e7293 --- /dev/null +++ b/docs/reference/doctrine-identifier-types.md @@ -0,0 +1,11 @@ +# Doctrine identifier types + +Reference of available [Doctrine identifier types](../infrastructure/doctrine-dbal.md#domain-identifier-type) across all +domains. + +Type name | Type class +--- | --- +`msgphp_domain_id` | `MsgPhp\Domain\Infra\Doctrine\Type\DomainIdType` +`msgphp_attribute_id` | `MsgPhp\Eav\Infra\Doctrine\Type\AttributeIdType` +`msgphp_attribute_value_id` | `MsgPhp\Eav\Infra\Doctrine\Type\AttributeValueIdType` +`msgphp_user_id` | `MsgPhp\User\Infra\Doctrine\Type\UserIdType` diff --git a/docs/reference/identifiers.md b/docs/reference/identifiers.md new file mode 100644 index 00000000..c6d74ff3 --- /dev/null +++ b/docs/reference/identifiers.md @@ -0,0 +1,24 @@ +# Domain identifiers + +Reference of available [identifiers](../ddd/identifiers.md) per domain. + +## Base domain + +- `MsgPhp\Domain\DomainIdInterface` + - UUID types: `MsgPhp\Domain\Infra\Uuid\DomainId` + - Default type: `MsgPhp\Domain\DomainId` + +## EAV domain + +- `MsgPhp\Eav\AttributeIdInterface` + - UUID types: `MsgPhp\Eav\Infra\Uuid\AttributeId` + - Default type: `MsgPhp\Eav\AttributeId` +- `MsgPhp\Eav\AttributeValueIdInterface` + - UUID types: `MsgPhp\Eav\Infra\Uuid\AttributeValueId` + - Default type: `MsgPhp\Eav\AttributeValueId` + +## User domain + +- `MsgPhp\User\UserIdInterface` + - UUID types: `MsgPhp\User\Infra\Uuid\UserId` + - Default type: `MsgPhp\User\UserId` diff --git a/mkdocs.yml b/mkdocs.yml index ed8cf71b..a4522842 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -41,3 +41,6 @@ pages: - Symfony Console: infrastructure/symfony-console.md - Cookbook: - Bundle installation: cookbook/bundle-installation.md +- Reference: + - Domain identifiers: reference/identifiers.md + - Doctrine identifier types: reference/doctrine-identifier-types.md From b3246f9a282e3746e4d098709b276f02b340b331 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 13:05:23 +0100 Subject: [PATCH 58/79] fix docs --- docs/cookbook/bundle-installation.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/cookbook/bundle-installation.md b/docs/cookbook/bundle-installation.md index 8b57306f..e403ca0e 100644 --- a/docs/cookbook/bundle-installation.md +++ b/docs/cookbook/bundle-installation.md @@ -9,8 +9,8 @@ infrastructural services are created automatically. ## Available bundles -- `msgphp/eav-bundle`: Provides basic entity-attribute-value management -- `msgphp/user-bundle`: Provides basic user management +- `msgphp/eav-bundle`: Provides basic entity-attribute-value management (the EAV domain) +- `msgphp/user-bundle`: Provides basic user management (the user domain) ## Installing using [Composer] @@ -35,7 +35,7 @@ msgphp_: commands: [] ``` -Depending on your personal preference you can also write the configuration in any other supported format. See +Depending on your personal preference you can also write the configuration in any other supported format. See the [demo configuration] for a more advanced example. ### Class mapping @@ -50,7 +50,7 @@ msgphp_: The class mapping applies when working with an [object factory](../ddd/factory/object.md#). -Depending on the bundle a specific class mapping entry might enable one of the bundle its features which are otherwise +Depending on the bundle a specific class mapping entry might enable one of the bundle its features which is otherwise disabled by default. ### Identifier type mapping @@ -66,12 +66,15 @@ msgphp_: MsgPhp\SomeDomainIdInterface: some_type_name ``` -By convention any [Doctrine DBAL type] can be used. Additionally `uuid`, `uuid_binary` or `uuid_binary_ordered_time` are -also detected. +By convention any [Doctrine DBAL type] can be used. Additionally the following UUID types are detected as well: + +- `uuid` +- `uuid_binary` +- `uuid_binary_ordered_time` ### Default identifier type -Configures a default identifier type name to use for all known identifiers provided by the bundle. +Configures a default type name to use for all known domain identifiers provided by the bundle. ```yaml msgphp_: @@ -97,7 +100,7 @@ msgphp_: ## Basic configuration example -Given a bundle provides the following identifiers: +Given a bundle provides the following domain identifiers: - `MsgPhp\FooIdInterface` - `MsgPhp\BarIdInterface` @@ -118,6 +121,8 @@ msgphp_: # MsgPhp\BazIdInterface: MsgPhp\Domain\Infra\Uuid\DomainId ``` +See also the [domain identifier reference](../reference/identifiers.md). + [Symfony Framework]: https://symfony.com/ [dependency injection]: https://symfony.com/doc/current/components/dependency_injection.html [Composer]: https://getcomposer.org/ From 264fbc20fbc94b2859c9bdc5f8a90f5b07efdf7b Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 13:08:53 +0100 Subject: [PATCH 59/79] fix docs --- docs/reference/identifiers.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference/identifiers.md b/docs/reference/identifiers.md index c6d74ff3..912cce13 100644 --- a/docs/reference/identifiers.md +++ b/docs/reference/identifiers.md @@ -5,20 +5,20 @@ Reference of available [identifiers](../ddd/identifiers.md) per domain. ## Base domain - `MsgPhp\Domain\DomainIdInterface` + - Scalar types: `MsgPhp\Domain\DomainId` - UUID types: `MsgPhp\Domain\Infra\Uuid\DomainId` - - Default type: `MsgPhp\Domain\DomainId` ## EAV domain - `MsgPhp\Eav\AttributeIdInterface` + - Scalar types: `MsgPhp\Eav\AttributeId` - UUID types: `MsgPhp\Eav\Infra\Uuid\AttributeId` - - Default type: `MsgPhp\Eav\AttributeId` - `MsgPhp\Eav\AttributeValueIdInterface` + - Scalar types: `MsgPhp\Eav\AttributeValueId` - UUID types: `MsgPhp\Eav\Infra\Uuid\AttributeValueId` - - Default type: `MsgPhp\Eav\AttributeValueId` ## User domain - `MsgPhp\User\UserIdInterface` + - Scalar types: `MsgPhp\User\UserId` - UUID types: `MsgPhp\User\Infra\Uuid\UserId` - - Default type: `MsgPhp\User\UserId` From 7b972354cfb94b3252124c743763f7a31d52040a Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 13:35:03 +0100 Subject: [PATCH 60/79] fix docs --- docs/cookbook/bundle-installation.md | 4 ++-- docs/ddd/identifiers.md | 2 ++ docs/infrastructure/doctrine-dbal.md | 3 +++ docs/infrastructure/uuid.md | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/cookbook/bundle-installation.md b/docs/cookbook/bundle-installation.md index e403ca0e..d5ad50ee 100644 --- a/docs/cookbook/bundle-installation.md +++ b/docs/cookbook/bundle-installation.md @@ -72,6 +72,8 @@ By convention any [Doctrine DBAL type] can be used. Additionally the following U - `uuid_binary` - `uuid_binary_ordered_time` +See also the [reference](../reference/identifiers.md) page for all available identifiers provided per domain. + ### Default identifier type Configures a default type name to use for all known domain identifiers provided by the bundle. @@ -121,8 +123,6 @@ msgphp_: # MsgPhp\BazIdInterface: MsgPhp\Domain\Infra\Uuid\DomainId ``` -See also the [domain identifier reference](../reference/identifiers.md). - [Symfony Framework]: https://symfony.com/ [dependency injection]: https://symfony.com/doc/current/components/dependency_injection.html [Composer]: https://getcomposer.org/ diff --git a/docs/ddd/identifiers.md b/docs/ddd/identifiers.md index 1d02ec88..df2825c9 100644 --- a/docs/ddd/identifiers.md +++ b/docs/ddd/identifiers.md @@ -37,6 +37,8 @@ should be returned. ## Implementations +See also the [reference](../reference/identifiers.md) page for all available identifiers provided per domain. + ### `MsgPhp\Domain\DomainId` A first class citizen domain identifier. It leverages `string|null` as underlying data type. diff --git a/docs/infrastructure/doctrine-dbal.md b/docs/infrastructure/doctrine-dbal.md index 0eccb642..cc460ce9 100644 --- a/docs/infrastructure/doctrine-dbal.md +++ b/docs/infrastructure/doctrine-dbal.md @@ -63,6 +63,9 @@ MyDomainIdType::setClass(DomainUuid::class); MyDomainIdType::setDataType(UuidType::NAME); ``` +See also the [reference](../reference/doctrine-identifier-types.md) page for all available identifier types provided per +domain. + [dbal-project]: http://www.doctrine-project.org/projects/dbal.html [doctrine/dbal]: https://packagist.org/packages/doctrine/dbal [api-type]: http://www.doctrine-project.org/api/dbal/2.5/class-Doctrine.DBAL.Types.Type.html diff --git a/docs/infrastructure/uuid.md b/docs/infrastructure/uuid.md index b6f0752f..b851ac3b 100644 --- a/docs/infrastructure/uuid.md +++ b/docs/infrastructure/uuid.md @@ -13,6 +13,8 @@ leverages type `Ramsey\Uuid\UuidInterface` as underlying data type. - `$uuid`: The underlying UUID. In case of `null` a UUID version 4 value is generated upfront. Meaning the identifier will never be considered empty. +See also the [reference](../reference/identifiers.md) page for all available UUID identifiers provided per domain. + ### Basic example ```php From 077dfddbb9a2865cfda5ebbf56065b29b27eb1ee Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 13:35:50 +0100 Subject: [PATCH 61/79] cleanup docs --- docs/symfony-bundle/architecture.md | 3 --- docs/symfony-bundle/eav.md | 3 --- docs/symfony-bundle/user.md | 3 --- 3 files changed, 9 deletions(-) delete mode 100644 docs/symfony-bundle/architecture.md delete mode 100644 docs/symfony-bundle/eav.md delete mode 100644 docs/symfony-bundle/user.md diff --git a/docs/symfony-bundle/architecture.md b/docs/symfony-bundle/architecture.md deleted file mode 100644 index bc803a02..00000000 --- a/docs/symfony-bundle/architecture.md +++ /dev/null @@ -1,3 +0,0 @@ -# Bundle architecture - -TODO diff --git a/docs/symfony-bundle/eav.md b/docs/symfony-bundle/eav.md deleted file mode 100644 index 090e4e71..00000000 --- a/docs/symfony-bundle/eav.md +++ /dev/null @@ -1,3 +0,0 @@ -# Entity-Attribute-Value bundle - -TODO diff --git a/docs/symfony-bundle/user.md b/docs/symfony-bundle/user.md deleted file mode 100644 index 6fd43480..00000000 --- a/docs/symfony-bundle/user.md +++ /dev/null @@ -1,3 +0,0 @@ -# User bundle - -TODO From 3478b1be97ff6eceecb2f71c141081d3a5bb0e27 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 14:17:54 +0100 Subject: [PATCH 62/79] fix docs --- docs/ddd/entities.md | 9 ++-- docs/infrastructure/doctrine-dbal.md | 6 +-- docs/reference/entities.md | 64 ++++++++++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 docs/reference/entities.md diff --git a/docs/ddd/entities.md b/docs/ddd/entities.md index 8480731b..4a3fa3a6 100644 --- a/docs/ddd/entities.md +++ b/docs/ddd/entities.md @@ -1,13 +1,14 @@ # Entities -Entity objects are provided per domain layer and usually follow a [POPO] design. - -To simplify entity definitions common fields and features are provided in the form of PHP [traits]. Entity fields can be -compared to a read-operation, whereas entity features represent a write-operation. +Entity objects are provided per domain layer and usually follow a [POPO] design. To simplify its definition common +fields and features are provided in the form of PHP [traits]. Fields can be compared to a read-operation, whereas +features represent a read/write-operation. They are defined in a dedicated namespace for discovery, respectively `Msgphp\Domain\Entity\Fields\` and `MsgPhp\Domain\Entity\Features\`. Additionally more specific fields and features can be provided per domain layer. +See also the [reference](../reference/entities.md) page for all available entities provided per domain. + ## Common entity fields ### `CreatedAtField` diff --git a/docs/infrastructure/doctrine-dbal.md b/docs/infrastructure/doctrine-dbal.md index cc460ce9..d35f740d 100644 --- a/docs/infrastructure/doctrine-dbal.md +++ b/docs/infrastructure/doctrine-dbal.md @@ -22,6 +22,9 @@ default [`Type`][api-type] implementation and can be used either generic or as a - `$type`: A doctrine type name to use as underlying data type. If not set `Type::INTEGER` is used. - `static getDataType(): string` +See also the [reference](../reference/doctrine-identifier-types.md) page for all available identifier types provided per +domain. + ### Basic example ```php @@ -63,9 +66,6 @@ MyDomainIdType::setClass(DomainUuid::class); MyDomainIdType::setDataType(UuidType::NAME); ``` -See also the [reference](../reference/doctrine-identifier-types.md) page for all available identifier types provided per -domain. - [dbal-project]: http://www.doctrine-project.org/projects/dbal.html [doctrine/dbal]: https://packagist.org/packages/doctrine/dbal [api-type]: http://www.doctrine-project.org/api/dbal/2.5/class-Doctrine.DBAL.Types.Type.html diff --git a/docs/reference/entities.md b/docs/reference/entities.md new file mode 100644 index 00000000..ec1c9e34 --- /dev/null +++ b/docs/reference/entities.md @@ -0,0 +1,64 @@ +# Domain entities + +Reference of available [entities](../ddd/entities.md) per domain. + +## Base domain + +### Entity fields + +- `MsgPhp\Domain\Entity\Fields\CreatedAtField` +- `MsgPhp\Domain\Entity\Fields\EnabledField` +- `MsgPhp\Domain\Entity\Fields\LastUpdatedAtField` + +### Entity features + +- `MsgPhp\Domain\Entity\Features\CanBeConfirmed` +- `MsgPhp\Domain\Entity\Features\CanBeEnabled` + +## EAV domain + +### Entity fields + +- `MsgPhp\Eav\Entity\Fields\AttributesField` +- `MsgPhp\Eav\Entity\Fields\AttributeValueField` + +### Mapped super classes + +- `MsgPhp\Eav\Entity\Attribute` +- `MsgPhp\Eav\Entity\AttributeValue` + +## User domain + +### Entity fields + +- `MsgPhp\User\Entity\Fields\AttributeValuesField` +- `MsgPhp\User\Entity\Fields\EmailsField` +- `MsgPhp\User\Entity\Fields\RoleField` +- `MsgPhp\User\Entity\Fields\RolesField` +- `MsgPhp\User\Entity\Fields\UserField` + +### Entity features + +- `MsgPhp\User\Entity\Features\ResettablePassword` + +#### Credentials + +- `MsgPhp\User\Entity\Features\EmailCredential` +- `MsgPhp\User\Entity\Features\EmailPasswordCredential` +- `MsgPhp\User\Entity\Features\EmailSaltedPasswordCredential` +- `MsgPhp\User\Entity\Features\NicknameCredential` +- `MsgPhp\User\Entity\Features\NicknamePasswordCredential` +- `MsgPhp\User\Entity\Features\NicknameSaltedPasswordCredential` +- `MsgPhp\User\Entity\Features\TokenCredential` + +### Mapped super classes + +- `MsgPhp\User\Entity\Role` +- `MsgPhp\User\Entity\User` +- `MsgPhp\User\Entity\UserAttributeValue` +- `MsgPhp\User\Entity\UserEmail` +- `MsgPhp\User\Entity\UserRole` + +### Entities + +- `MsgPhp\User\Entity\Username` diff --git a/mkdocs.yml b/mkdocs.yml index a4522842..3997e04f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,4 +43,5 @@ pages: - Bundle installation: cookbook/bundle-installation.md - Reference: - Domain identifiers: reference/identifiers.md + - Domain entities: reference/entities.md - Doctrine identifier types: reference/doctrine-identifier-types.md From ac09feed4351f348c5f08857588717c2c223383f Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 14:26:16 +0100 Subject: [PATCH 63/79] tweak docs --- docs/reference/entities.md | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/reference/entities.md b/docs/reference/entities.md index ec1c9e34..2a5dc438 100644 --- a/docs/reference/entities.md +++ b/docs/reference/entities.md @@ -17,17 +17,30 @@ Reference of available [entities](../ddd/entities.md) per domain. ## EAV domain +### Entities + +Class | Abstract | Required +--- | --- | --- +`MsgPhp\Eav\Entity\Attribute` | ✔ | ✔ +`MsgPhp\Eav\Entity\AttributeValue` | ✔ | ✔ + ### Entity fields - `MsgPhp\Eav\Entity\Fields\AttributesField` - `MsgPhp\Eav\Entity\Fields\AttributeValueField` -### Mapped super classes +## User domain -- `MsgPhp\Eav\Entity\Attribute` -- `MsgPhp\Eav\Entity\AttributeValue` +### Entities -## User domain +Class | Abstract | Required +--- | --- | --- +`MsgPhp\User\Entity\Role` | ✔ | ✗ +`MsgPhp\User\Entity\User` | ✔ | ✔ +`MsgPhp\User\Entity\UserAttributeValue` | ✔ | ✗ +`MsgPhp\User\Entity\UserEmail` | ✔ | ✗ +`MsgPhp\User\Entity\Username` | ✗ | ✗ +`MsgPhp\User\Entity\UserRole` | ✔ | ✗ ### Entity fields @@ -41,7 +54,7 @@ Reference of available [entities](../ddd/entities.md) per domain. - `MsgPhp\User\Entity\Features\ResettablePassword` -#### Credentials +#### Credential types - `MsgPhp\User\Entity\Features\EmailCredential` - `MsgPhp\User\Entity\Features\EmailPasswordCredential` @@ -50,15 +63,3 @@ Reference of available [entities](../ddd/entities.md) per domain. - `MsgPhp\User\Entity\Features\NicknamePasswordCredential` - `MsgPhp\User\Entity\Features\NicknameSaltedPasswordCredential` - `MsgPhp\User\Entity\Features\TokenCredential` - -### Mapped super classes - -- `MsgPhp\User\Entity\Role` -- `MsgPhp\User\Entity\User` -- `MsgPhp\User\Entity\UserAttributeValue` -- `MsgPhp\User\Entity\UserEmail` -- `MsgPhp\User\Entity\UserRole` - -### Entities - -- `MsgPhp\User\Entity\Username` From 636d46abc7c9071ba47f7935723037a12bed3053 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 18 Mar 2018 15:55:10 +0100 Subject: [PATCH 64/79] Update .codecov.yml --- .codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codecov.yml b/.codecov.yml index da1d60c6..1acfb8df 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,4 +5,4 @@ ignore: - "src/*/Event/*Event.php" - "src/*/Event/Domain/*Event.php" - "src/*/Infra/Doctrine/Type/*Type.php" - - "src/Domain/Infra/DependencyInjection/Bundle/*Helper.php" + - "src/Domain/Infra/DependencyInjection/*Helper.php" From 6646b956a67eb75ab7dcd2bd0b2baecd700ce4b1 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Fri, 23 Mar 2018 18:06:45 +0100 Subject: [PATCH 65/79] remove unneeded parent call --- src/User/Infra/Console/Command/CreateUserCommand.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/User/Infra/Console/Command/CreateUserCommand.php b/src/User/Infra/Console/Command/CreateUserCommand.php index 6b6fe100..fd6368be 100644 --- a/src/User/Infra/Console/Command/CreateUserCommand.php +++ b/src/User/Infra/Console/Command/CreateUserCommand.php @@ -50,8 +50,6 @@ public function onMessageReceived($message): void protected function configure(): void { - parent::configure(); - $this->setDescription('Create a user'); $this->contextFactory->configure($this->getDefinition()); } From ca3dcd4440a70ed75aec669977a5f4c5e07107b8 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 24 Mar 2018 13:20:59 +0100 Subject: [PATCH 66/79] setup domain projections (#125) --- docs/ddd/identities.md | 6 +- docs/domain/architecture.md | 3 - docs/domain/eav.md | 3 - docs/domain/user.md | 3 - docs/index.md | 1 + docs/infrastructure/api-platform.md | 56 ++++++++++ docs/infrastructure/elasticsearch.md | 78 ++++++++++++++ docs/infrastructure/psr-container.md | 98 +++++++++++++++++ docs/infrastructure/symfony-console.md | 25 ++++- docs/projection/document-transformers.md | 18 ++++ docs/projection/documents.md | 34 ++++++ docs/projection/models.md | 49 +++++++++ docs/projection/repositories.md | 77 ++++++++++++++ docs/projection/synchronization.md | 75 +++++++++++++ docs/projection/type-registry.md | 57 ++++++++++ mkdocs.yml | 12 ++- .../DomainProjectionDataProvider.php | 43 ++++++++ ...InitializeDomainProjectionTypesCommand.php | 52 +++++++++ .../SynchronizeDomainProjectionsCommand.php | 63 +++++++++++ .../DomainProjectionRepository.php | 94 ++++++++++++++++ .../DomainProjectionTypeRegistry.php | 100 ++++++++++++++++++ .../Infra/Psr/DoctrineAwareClassContainer.php | 31 ++++++ .../DomainProjectionDocumentTransformer.php | 42 ++++++++ .../Projection/DomainProjectionDocument.php | 67 ++++++++++++ ...ProjectionDocumentTransformerInterface.php | 16 +++ .../Projection/DomainProjectionInterface.php | 13 +++ .../DomainProjectionRepositoryInterface.php | 24 +++++ .../DomainProjectionSynchronization.php | 61 +++++++++++ .../DomainProjectionTypeRegistryInterface.php | 20 ++++ src/Domain/composer.json | 2 + 30 files changed, 1209 insertions(+), 14 deletions(-) delete mode 100644 docs/domain/architecture.md delete mode 100644 docs/domain/eav.md delete mode 100644 docs/domain/user.md create mode 100644 docs/infrastructure/api-platform.md create mode 100644 docs/infrastructure/elasticsearch.md create mode 100644 docs/infrastructure/psr-container.md create mode 100644 docs/projection/document-transformers.md create mode 100644 docs/projection/documents.md create mode 100644 docs/projection/models.md create mode 100644 docs/projection/repositories.md create mode 100644 docs/projection/synchronization.md create mode 100644 docs/projection/type-registry.md create mode 100644 src/Domain/Infra/ApiPlatform/DomainProjectionDataProvider.php create mode 100644 src/Domain/Infra/Console/Command/InitializeDomainProjectionTypesCommand.php create mode 100644 src/Domain/Infra/Console/Command/SynchronizeDomainProjectionsCommand.php create mode 100644 src/Domain/Infra/Elasticsearch/DomainProjectionRepository.php create mode 100644 src/Domain/Infra/Elasticsearch/DomainProjectionTypeRegistry.php create mode 100644 src/Domain/Infra/Psr/DoctrineAwareClassContainer.php create mode 100644 src/Domain/Infra/Psr/DomainProjectionDocumentTransformer.php create mode 100644 src/Domain/Projection/DomainProjectionDocument.php create mode 100644 src/Domain/Projection/DomainProjectionDocumentTransformerInterface.php create mode 100644 src/Domain/Projection/DomainProjectionInterface.php create mode 100644 src/Domain/Projection/DomainProjectionRepositoryInterface.php create mode 100644 src/Domain/Projection/DomainProjectionSynchronization.php create mode 100644 src/Domain/Projection/DomainProjectionTypeRegistryInterface.php diff --git a/docs/ddd/identities.md b/docs/ddd/identities.md index 346679f5..e324086c 100644 --- a/docs/ddd/identities.md +++ b/docs/ddd/identities.md @@ -1,5 +1,8 @@ # Identities +`MsgPhp\Domain\DomainIdentityHelper` is a utility domain service. Its purpose is to ease working with domain identities +and the [identity mapping](identity-mapping.md). + A domain identity is a composite value (`array`) of one or more individual identifier values, indexed by an identifier field name. Its usage is to uniquely identify a domain object, thus qualifying it an entity object. @@ -8,9 +11,6 @@ primitive value. A single identifier value might represent an actual identity in case it's composed by a single identifier field. -`MsgPhp\Domain\DomainIdentityHelper` is a domain a helper service. Its purpose is to ease working with the -[identity mapping](identity-mapping.md). - ## API ### `isIdentifier($value): bool` diff --git a/docs/domain/architecture.md b/docs/domain/architecture.md deleted file mode 100644 index e3370fd0..00000000 --- a/docs/domain/architecture.md +++ /dev/null @@ -1,3 +0,0 @@ -# Domain layer architecture - -TODO diff --git a/docs/domain/eav.md b/docs/domain/eav.md deleted file mode 100644 index 2d6ed3e8..00000000 --- a/docs/domain/eav.md +++ /dev/null @@ -1,3 +0,0 @@ -# Entity-Attribute-Value domain layer - -TODO diff --git a/docs/domain/user.md b/docs/domain/user.md deleted file mode 100644 index d8cb9a5e..00000000 --- a/docs/domain/user.md +++ /dev/null @@ -1,3 +0,0 @@ -# User domain layer - -TODO diff --git a/docs/index.md b/docs/index.md index aaf864c5..fde0f148 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,6 +6,7 @@ # News +- **`2018-03-24`** Added initial _projection_ chapter - **`2018-03-18`** Added initial _cookbook_ and _reference_ chapters - **`2018-02-18`** Added initial _infrastructure_ chapter - **`2018-02-12`** Added [domain identities](ddd/identities.md) chapter diff --git a/docs/infrastructure/api-platform.md b/docs/infrastructure/api-platform.md new file mode 100644 index 00000000..1f71ad07 --- /dev/null +++ b/docs/infrastructure/api-platform.md @@ -0,0 +1,56 @@ +# API Platform + +An overview of available infrastructural code when using [API Platform]. + +- Requires [api-platform/core] + +## Domain projection data provider + +When working with [projections](../projection/models.md) an [API Data Provider] is provided by `MsgPhp\Domain\Infra\ApiPlatform\DomainProjectionDataProvider`. +It uses any [projection repository](../projection/repositories.md) in an effort to provide API resources. + +### Minimal configuration + +See also [API Platform Configuration]. + +```yaml +api_platform: + resource_class_directories: + - '%kernel.project_dir%/src/Api/Projection' +``` + +### Basic example + +```php +id = $document['id'] ?? null; + + return $projection; + } +} +``` + +[API Platform]: https://api-platform.com/ +[api-platform/core]: https://packagist.org/packages/api-platform/core +[API Data Provider]: https://api-platform.com/docs/core/data-providers +[API Platform Configuration]: https://api-platform.com/docs/core/configuration diff --git a/docs/infrastructure/elasticsearch.md b/docs/infrastructure/elasticsearch.md new file mode 100644 index 00000000..ac4f1f04 --- /dev/null +++ b/docs/infrastructure/elasticsearch.md @@ -0,0 +1,78 @@ +# Elasticsearch + +An overview of available infrastructural code when using Elasticsearch's [PHP Api][elasticsearch-project]. + +- Requires [elasticsearch/elasticsearch] + +## Domain projection type registry + +An Elasticsearch tailored [domain projection type registry](../projection/type-registry.md) is provided by `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionTypeRegistry`. +It works directly with any [`Client`][api-client] and a known configuration of type information. + +- `__construct(Client $client, string $index, array $mappings, array $settings = [], LoggerInterface $logger = null)` + - `$client`: The Client to work with + - `$index`: The index to use + - `$mappings` / `$settings`: Index management information. [Read more][index management] + - `$logger`: An optional [PSR logger] + +### Basic example + +```php + [ + 'some_field' => null, // defaults to ['type' => 'text'] + 'other_field' => 'some', // defaults to ['type' => 'some'] + 'other_field2' => [ + // ... + ], + ], +]); +``` + +## Domain projection repository + +An Elasticsearch tailored [domain projection repository](../projection/repositories.md) is provided by `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionRepository`. +It works directly with any [`Client`][api-client]. + +- `__construct(Client $client, string $index)` + - `$client`: The Client to work with + - `$index`: The index to use + +### Basic example + +```php +transform(new MyEntity()); +``` + +### Manual container example + +In practice you often get a `$container` from configuration, e.g. when working with Symfony's [Service Locators]. This +example shows how to manually create a container using PHP7 anonymous classes. + +```php +has($id)) { + throw new class('Entry not found') extends \RuntimeException implements NotFoundExceptionInterface { + }; + } + + return function (MyEntity $object) { + $document = DomainProjectionDocument::create(MyProjection::class, null, [ + 'some_field' => $object->someField, + ]); + $document->source = $object; + + return $document; + }; + } +}; +$transformer = new DomainProjectionDocumentTransformer($container); +``` + +[container-project]: https://www.php-fig.org/psr/psr-11/ +[psr/container]: https://packagist.org/packages/psr/container +[api-container]: https://www.php-fig.org/psr/psr-11/#31-psrcontainercontainerinterface +[Service Locators]: https://symfony.com/doc/current/service_container/service_locators.html +[anonymous classes]: https://secure.php.net/manual/en/language.oop5.anonymous.php diff --git a/docs/infrastructure/symfony-console.md b/docs/infrastructure/symfony-console.md index 32cd3bd9..ccedb7f9 100644 --- a/docs/infrastructure/symfony-console.md +++ b/docs/infrastructure/symfony-console.md @@ -4,6 +4,28 @@ An overview of available infrastructural code when using [Symfony Console][conso - Requires [symfony/console] +## Commands + +Various standard [console commands] are provided depending on used domain infrastructure. They are defined in the +`MsgPhp\Domain\Infra\Console\Command\` namespace. + +### `InitializeDomainProjectionTypesCommand` + +Initializes a [projection type registry](../projection/type-registry.md). + +```bash +bin/console domain:projection:initialize-types [--force] +``` + +### `SynchronizeDomainProjectionsCommand` + +Synchronizes domain objects and their [projections](../projection/models.md) using the [projection synchronization](../projection/synchronization.md) +utility service. + +```bash +bin/console domain:projection:synchronize +``` + ## Context factory A context factory is bound to `MsgPhp\Domain\Infra\Console\Context\ContextFactoryInterface`. Its purpose is to leverage @@ -123,8 +145,9 @@ provide a discriminator value into the resulting context when working with [inhe [console-project]: https://symfony.com/doc/current/components/console.html [symfony/console]: https://packagist.org/packages/symfony/console +[console commands]: https://symfony.com/doc/current/console.html [api-inputdefinition]: https://api.symfony.com/master/Symfony/Component/Console/Input/InputDefinition.html [api-inputinterface]: https://api.symfony.com/master/Symfony/Component/Console/Input/InputInterface.html [api-styleinterface]: https://api.symfony.com/master/Symfony/Component/Console/Style/StyleInterface.html -[api-contextelement]: https://msgphp.github.io/api/MsgPhp/Domain/Infra/Console/ContextBuilder/ContextElement.html +[api-contextelement]: https://msgphp.github.io/api/MsgPhp/Domain/Infra/Console/Context/ContextElement.html [orm-inheritance]: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html diff --git a/docs/projection/document-transformers.md b/docs/projection/document-transformers.md new file mode 100644 index 00000000..86cd1371 --- /dev/null +++ b/docs/projection/document-transformers.md @@ -0,0 +1,18 @@ +# Projection document transformers + +A projection document transformer is bound to `MsgPhp\Domain\Projection\DomainProjectionDocumentTransformerInterface`. +Its purpose is to transform domain objects into [projection documents](documents.md). + +## API + +### `transform(object $object): DomainProjectionDocument` + +Transforms the domain object into a projection document. + +## Implementations + +### `MsgPhp\Domain\Infra\Psr\DomainProjectionDocumentTransformer` + +A PSR container tailored projection document transformer. + +- [Read more](../infrastructure/psr-container.md#domain-projection-document-transformer) diff --git a/docs/projection/documents.md b/docs/projection/documents.md new file mode 100644 index 00000000..787489fe --- /dev/null +++ b/docs/projection/documents.md @@ -0,0 +1,34 @@ +# Projection documents + +A projection document is a domain value object of type `MsgPhp\Domain\Projection\DomainProjectionDocument`. Its purpose +is to hold a projection document its (meta)data and current state. + +## API + +### `static create(string $type, string $id = null, array $body = []): DomainProjectionDocument` + +Creates a projection document for a known projection type. In case `$id` is not given it implies an auto-generated +value. A projection `$type` usually refers to a known [projection](models.md) by class name. + +## Basic example + +```php + 'value', +]); +``` diff --git a/docs/projection/models.md b/docs/projection/models.md new file mode 100644 index 00000000..3b630291 --- /dev/null +++ b/docs/projection/models.md @@ -0,0 +1,49 @@ +# Projections + +A domain projection is a model object and bound to `MsgPhp\Domain\Projection\DomainProjectionInterface`. Its purpose is +to convert raw model data (a document) into a projection, enabling to be stored separate. + +The document is usually a transformation from a domain object (e.g. an entity) and therefor projections should be +considered read-only and disposable, as they can be re-created / synchronized at any time from a source of truth. + +A practical use case for domain projections are APIs, where each API resource is a so called projection from a +corresponding entity. It enables decoupling and thus optimized API responses. + +For integration with [API Platform] see also the [domain projection data provider](../infrastructure/api-platform.md#domain-projection-data-provider). + +## API + +### `static fromDocument(array $document): DomainProjectionInterface` + +Creates a projection from raw document data. + +## Basic example + +```php +someField = $document['some_field'] ?? null; + + return $projection; + } +} + +// --- USAGE --- + +$projection = MyProjection::fromDocument([ + 'some_field' => 'value', +]); +``` + +[API Platform]: https://api-platform.com/ diff --git a/docs/projection/repositories.md b/docs/projection/repositories.md new file mode 100644 index 00000000..e75b2871 --- /dev/null +++ b/docs/projection/repositories.md @@ -0,0 +1,77 @@ +# Projection repositories + +A domain projection repository is bound to `MsgPhp\Domain\Projection\DomainProjectionRepositoryInterface`. Its purpose +is to store and query [projection documents](documents.md). + +## API + +### `findAll(string $type, int $offset = 0, int $limit = 0): iterable` + +Find all [projections](models.md) by type. + +--- + +### `find(string $type, string $id): ?DomainProjectionInterface` + +Find a single [projection](models.md) by type and ID. In case its document cannot be found `null` should be returned. + +--- + +### `clear(string $type): void` + +Delete all projection documents by type. + +--- + +### `save(DomainProjectionDocument $document): void` + +Save a projection document. The document will be available on any subsequent query. + +--- + +### `delete(DomainProjectionDocument $document): void` + +Delete a projection document. The document will be unavailable on any subsequent query. + +## Implementations + +### `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionRepository` + +An Elasticsearch tailored projection repository. + +- [Read more](../infrastructure/elasticsearch.md#domain-projection-repository) + +## Basic example + +```php +find(MyProjection::class, $id); + +if (null === $projection) { + $document = DomainProjectionDocument::create(MyProjection::class, $id, [ + 'some_field' => 'value', + ]); + $repository->save($document); +} +``` diff --git a/docs/projection/synchronization.md b/docs/projection/synchronization.md new file mode 100644 index 00000000..468d6679 --- /dev/null +++ b/docs/projection/synchronization.md @@ -0,0 +1,75 @@ +# Projection synchronization + +`MsgPhp\Domain\Projection\DomainProjectionSynchronization` is a utility domain service. Its purpose is to ease +synchronizing provided [projections](models.md). + +So called _providers_ actually provide domain objects, which are in turn transformed to [documents](documents.md) using +any [transformer](document-transformers.md). A document is then stored using any [repository](repositories.md). + +## API + +### `synchronize(): iterable` + +Yields a projection document for each provided domain object regarding its state. The actual document status can be +read from [`ProjectionDocument::$status`][api-projection-document-status]. + +[api-projection-document-status]: https://msgphp.github.io/api/MsgPhp/Domain/Projection/DomainProjectionSynchronization.html#property_status + +## Basic example + +```php +id = $id; + } +} + +/** @var DomainProjectionDocumentTransformerInterface $transformer */ +$transformer = ...; +/** @var DomainProjectionTypeRegistryInterface $typeRegistry */ +$typeRegistry = ...; +/** @var DomainProjectionRepositoryInterface $repository */ +$repository = ...; +$synchronization = new DomainProjectionSynchronization($typeRegistry, $repository, $transformer, [ + function (): iterable { + yield new MyEntity(1); + yield new MyEntity(2); + }, +]); + +// --- USAGE --- + +foreach ($synchronization->synchronize() as $document) { + if (DomainProjectionDocument::STATUS_VALID === $document->status) { + echo 'Synchronized projection for '.get_class($document->source).' with ID '.$document->source->id.PHP_EOL; + continue; + } + + echo 'Invalid projection for '.get_class($document->source).' with ID '.$document->source->id.PHP_EOL; + + if (null !== $document->error) { + echo 'An error occurred for '.get_class($document->source).' with ID '.$document->source->id.PHP_EOL; + echo $document->error->getMessage().' in '.$document->error->getFile().' at '.$document->error->getLine().PHP_EOL; + } +} +``` + +## Command Line Interface + +A synchronization can be ran using the CLI when working with Symfony Console. + +- [Read more](../infrastructure/symfony-console.md#synchronizedomainprojectionscommand) diff --git a/docs/projection/type-registry.md b/docs/projection/type-registry.md new file mode 100644 index 00000000..3c35f37d --- /dev/null +++ b/docs/projection/type-registry.md @@ -0,0 +1,57 @@ +# Projection type registry + +A projection type registry is bound to `MsgPhp\Domain\Projection\DomainProjectionTypeRegistryInterface`. +Its purpose is to manage all available [projection](models.md) type information. + +## API + +### `all(): array` + +Returns all available projection types for this registry. + +--- + +### `initialize(): void` + +Initializes the type registry. Usually needs to be called only once per environment, or after any type information has +changed. + +--- + +### `destroy(): void` + +Destroys the type registry and thus requires to be re-initialized after. + +## Implementations + +### `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionTypeRegistry` + +An Elasticsearch tailored projection type registry. + +- [Read more](../infrastructure/elasticsearch.md#domain-projection-type-registry) + +## Basic example + +```php +destroy(); +$typeRegistry->initialize(); + +echo 'Initialized types: '.implode(', ', $typeRegistry->all()); +``` + +## Command Line Interface + +The type registry can be initialized using the CLI when working with Symfony Console. + +- [Read more](../infrastructure/symfony-console.md#initializedomainprojectiontypescommand) diff --git a/mkdocs.yml b/mkdocs.yml index 3997e04f..0e84eca7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,12 +33,22 @@ pages: - Message bus: message-driven/message-bus.md - Message dispatcher: message-driven/message-dispatcher.md - CQRS: message-driven/cqrs.md +- Projection: + - Models: projection/models.md + - Documents: projection/documents.md + - Document transformers: projection/document-transformers.md + - Type registry: projection/type-registry.md + - Repositories: projection/repositories.md + - Synchronization: projection/synchronization.md - Infrastructure: - - UUID: infrastructure/uuid.md + - API Platform: infrastructure/api-platform.md - Doctrine Collections: infrastructure/doctrine-collections.md - Doctrine DBAL: infrastructure/doctrine-dbal.md - Doctrine ORM: infrastructure/doctrine-orm.md + - Elasticsearch: infrastructure/elasticsearch.md + - PSR Container: infrastructure/psr-container.md - Symfony Console: infrastructure/symfony-console.md + - UUID: infrastructure/uuid.md - Cookbook: - Bundle installation: cookbook/bundle-installation.md - Reference: diff --git a/src/Domain/Infra/ApiPlatform/DomainProjectionDataProvider.php b/src/Domain/Infra/ApiPlatform/DomainProjectionDataProvider.php new file mode 100644 index 00000000..061f43ec --- /dev/null +++ b/src/Domain/Infra/ApiPlatform/DomainProjectionDataProvider.php @@ -0,0 +1,43 @@ + + */ +final class DomainProjectionDataProvider implements CollectionDataProviderInterface, ItemDataProviderInterface, RestrictedDataProviderInterface +{ + private $typeRegistry; + private $repository; + + public function __construct(DomainProjectionTypeRegistryInterface $typeRegistry, DomainProjectionRepositoryInterface $repository) + { + $this->typeRegistry = $typeRegistry; + $this->repository = $repository; + } + + public function supports(string $resourceClass, string $operationName = null, array $context = []): bool + { + return in_array($resourceClass, $this->typeRegistry->all(), true); + } + + /** + * @return DomainProjectionInterface[] + */ + public function getCollection(string $resourceClass, string $operationName = null): iterable + { + return $this->repository->findAll($resourceClass); + } + + public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): ?DomainProjectionInterface + { + return $this->repository->find($resourceClass, $id); + } +} diff --git a/src/Domain/Infra/Console/Command/InitializeDomainProjectionTypesCommand.php b/src/Domain/Infra/Console/Command/InitializeDomainProjectionTypesCommand.php new file mode 100644 index 00000000..58348b18 --- /dev/null +++ b/src/Domain/Infra/Console/Command/InitializeDomainProjectionTypesCommand.php @@ -0,0 +1,52 @@ + + */ +final class InitializeDomainProjectionTypesCommand extends Command +{ + protected static $defaultName = 'domain:projection:initialize-types'; + + private $typeRegistry; + + public function __construct(DomainProjectionTypeRegistryInterface $typeRegistry) + { + $this->typeRegistry = $typeRegistry; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDescription('Initializes all projection types') + ->addOption('force', null, InputOption::VALUE_NONE, 'Force initialization by destroying types first'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + if ($input->getOption('force')) { + $this->typeRegistry->destroy(); + } + + $this->typeRegistry->initialize(); + + $io->success('Projection types successfully initialized'); + $io->listing($this->typeRegistry->all()); + + return 0; + } +} diff --git a/src/Domain/Infra/Console/Command/SynchronizeDomainProjectionsCommand.php b/src/Domain/Infra/Console/Command/SynchronizeDomainProjectionsCommand.php new file mode 100644 index 00000000..f6d57ee7 --- /dev/null +++ b/src/Domain/Infra/Console/Command/SynchronizeDomainProjectionsCommand.php @@ -0,0 +1,63 @@ + + */ +final class SynchronizeDomainProjectionsCommand extends Command +{ + protected static $defaultName = 'domain:projection:synchronize'; + + private $synchronization; + private $logger; + + public function __construct(DomainProjectionSynchronization $synchronization, LoggerInterface $logger = null) + { + $this->synchronization = $synchronization; + $this->logger = $logger; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDescription('Synchronizes all projections'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $succeed = $failed = 0; + + foreach ($this->synchronization->synchronize() as $document) { + if (DomainProjectionDocument::STATUS_VALID === $document->status) { + ++$succeed; + } else { + ++$failed; + } + + if (null !== $document->error && null !== $this->logger) { + $this->logger->error($document->error->getMessage(), ['exception' => $document->error]); + } + } + + $io->success($succeed.' projection '.(1 === $succeed ? 'document' : 'documents').' synchronized'); + + if ($failed) { + $io->error($failed.' projection '.(1 === $failed ? 'document' : 'documents').' failed'); + } + + return 0; + } +} diff --git a/src/Domain/Infra/Elasticsearch/DomainProjectionRepository.php b/src/Domain/Infra/Elasticsearch/DomainProjectionRepository.php new file mode 100644 index 00000000..280c9897 --- /dev/null +++ b/src/Domain/Infra/Elasticsearch/DomainProjectionRepository.php @@ -0,0 +1,94 @@ + + */ +final class DomainProjectionRepository implements DomainProjectionRepositoryInterface +{ + private $client; + private $index; + + public function __construct(Client $client, string $index) + { + $this->client = $client; + $this->index = $index; + } + + /** + * @return DomainProjectionInterface[] + */ + public function findAll(string $type, int $offset = 0, int $limit = 0): iterable + { + $params = [ + 'index' => $this->index, + 'type' => $type, + 'body' => [ + 'from' => $offset, + 'query' => ['match_all' => new \stdClass()], + ], + ]; + + if ($limit) { + $params['body']['size'] = $limit; + } + + $documents = $this->client->search($params); + + foreach ($documents['hits']['hits'] ?? [] as $document) { + yield $document['_type']::fromDocument($document['_source']); + } + } + + public function find(string $type, string $id): ?DomainProjectionInterface + { + try { + $document = $this->client->get([ + 'index' => $this->index, + 'type' => $type, + 'id' => $id, + ]); + + return $document['_type']::fromDocument($document['_source']); + } catch (Missing404Exception $e) { + return null; + } + } + + public function clear(string $type): void + { + $this->client->deleteByQuery([ + 'index' => $this->index, + 'type' => $type, + 'body' => [ + 'query' => ['match_all' => new \stdClass()], + ], + ]); + } + + public function save(DomainProjectionDocument $document): void + { + $params = ['index' => $this->index, 'type' => $document->getType(), 'body' => $document->getBody()]; + if (null !== $id = $document->getId()) { + $params['id'] = $id; + } + + $this->client->index($params); + } + + public function delete(DomainProjectionDocument $document): void + { + $this->client->delete([ + 'index' => $this->index, + 'type' => $document->getType(), + 'id' => $document->getId(), + ]); + } +} diff --git a/src/Domain/Infra/Elasticsearch/DomainProjectionTypeRegistry.php b/src/Domain/Infra/Elasticsearch/DomainProjectionTypeRegistry.php new file mode 100644 index 00000000..289b2d44 --- /dev/null +++ b/src/Domain/Infra/Elasticsearch/DomainProjectionTypeRegistry.php @@ -0,0 +1,100 @@ + + */ +final class DomainProjectionTypeRegistry implements DomainProjectionTypeRegistryInterface +{ + private $client; + private $index; + private $mappings; + private $settings; + private $logger; + private $types; + + public function __construct(Client $client, string $index, array $mappings, array $settings = [], LoggerInterface $logger = null) + { + foreach ($mappings as $type => $mapping) { + if (!isset($mapping['properties']) || !is_array($mapping['properties'])) { + continue; + } + + foreach ($mapping['properties'] as $property => $info) { + if (!is_array($info)) { + $info = ['type' => $info ?? 'text']; + } + + $mappings[$type]['properties'][$property] = $info + ['type' => 'text']; + } + } + + $this->client = $client; + $this->index = $index; + $this->mappings = $mappings; + $this->settings = $settings; + $this->logger = $logger; + } + + /** + * @return string[] + */ + public function all(): array + { + if (null === $this->types) { + $this->types = []; + foreach (array_keys($this->mappings) as $type) { + if (is_subclass_of($type, DomainProjectionInterface::class)) { + $this->types[] = $type; + } + } + } + + return $this->types; + } + + public function initialize(): void + { + $indices = $this->client->indices(); + + if ($indices->exists($params = ['index' => $this->index])) { + return; + } + + if ($this->settings) { + $params['body']['settings'] = $this->settings; + } + + if ($this->mappings) { + $params['body']['mappings'] = $this->mappings; + } + + $indices->create($params); + + if (null !== $this->logger) { + $this->logger->info('Initialized Elasticsearch index "{index}".', ['index' => $this->index]); + } + } + + public function destroy(): void + { + $indices = $this->client->indices(); + + if (!$indices->exists($params = ['index' => $this->index])) { + return; + } + + $indices->delete($params); + + if (null !== $this->logger) { + $this->logger->info('Destroyed Elasticsearch index "{index}".', ['index' => $this->index]); + } + } +} diff --git a/src/Domain/Infra/Psr/DoctrineAwareClassContainer.php b/src/Domain/Infra/Psr/DoctrineAwareClassContainer.php new file mode 100644 index 00000000..e0aed3d0 --- /dev/null +++ b/src/Domain/Infra/Psr/DoctrineAwareClassContainer.php @@ -0,0 +1,31 @@ + + */ +final class DoctrineAwareClassContainer implements ContainerInterface +{ + private $container; + + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + public function has($id) + { + return $this->container->has(ClassUtils::getRealClass($id)); + } + + public function get($id) + { + return $this->container->get(ClassUtils::getRealClass($id)); + } +} diff --git a/src/Domain/Infra/Psr/DomainProjectionDocumentTransformer.php b/src/Domain/Infra/Psr/DomainProjectionDocumentTransformer.php new file mode 100644 index 00000000..2966be04 --- /dev/null +++ b/src/Domain/Infra/Psr/DomainProjectionDocumentTransformer.php @@ -0,0 +1,42 @@ + + */ +final class DomainProjectionDocumentTransformer implements DomainProjectionDocumentTransformerInterface +{ + private $transformers; + + public function __construct(ContainerInterface $transformers) + { + $this->transformers = $transformers; + } + + public function transform($object): DomainProjectionDocument + { + if (!$this->transformers->has($class = get_class($object))) { + throw new \LogicException(sprintf('No projection document transformer available for class "%s".', $class)); + } + + if (!is_callable($transformer = $this->transformers->get($class))) { + throw new \LogicException(sprintf('Projection document transformer for class "%s" must be a callable, got "%s".', $class, gettype($transformer))); + } + + $document = $transformer($object); + + if (!$document instanceof DomainProjectionDocument) { + throw new \LogicException(sprintf('Projection document transformer for class "%s" must return an instance of "%s", got "%s".', $class, DomainProjectionDocument::class, is_object($document) ? get_class($document) : gettype($document))); + } + + $document->source = $object; + + return $document; + } +} diff --git a/src/Domain/Projection/DomainProjectionDocument.php b/src/Domain/Projection/DomainProjectionDocument.php new file mode 100644 index 00000000..8c9ca180 --- /dev/null +++ b/src/Domain/Projection/DomainProjectionDocument.php @@ -0,0 +1,67 @@ + + */ +final class DomainProjectionDocument +{ + private const DATA_TYPE_KEY = 'document_type'; + private const DATA_ID_KEY = 'document_id'; + + public const STATUS_UNKNOWN = 1; + public const STATUS_VALID = 2; + public const STATUS_FAILED_TRANSFORMATION = 3; + public const STATUS_FAILED_SAVING = 4; + + /** @var int */ + public $status = self::STATUS_UNKNOWN; + + /** @var array */ + public $data = []; + + /** @var object|null */ + public $source; + + /** @var \Exception|null $error */ + public $error; + + public static function create(string $type, string $id = null, array $body = []): self + { + $document = new self(); + $document->data[self::DATA_TYPE_KEY] = $type; + $document->data[self::DATA_ID_KEY] = $id; + $document->data += $body; + + return $document; + } + + public function getType(): string + { + if (!isset($this->data[self::DATA_TYPE_KEY])) { + throw new \LogicException('Document type not set.'); + } + + if (!is_subclass_of($type = $this->data[self::DATA_TYPE_KEY], DomainProjectionInterface::class)) { + throw new \LogicException(sprintf('Document type must be a sub class of "%s", got "%s".', DomainProjectionInterface::class, $type)); + } + + return $type; + } + + public function getId(): ?string + { + return $this->data[self::DATA_ID_KEY] ?? null; + } + + public function getBody(): array + { + $data = $this->data; + unset($data[self::DATA_TYPE_KEY], $data[self::DATA_ID_KEY]); + + return $data; + } +} diff --git a/src/Domain/Projection/DomainProjectionDocumentTransformerInterface.php b/src/Domain/Projection/DomainProjectionDocumentTransformerInterface.php new file mode 100644 index 00000000..f3881a50 --- /dev/null +++ b/src/Domain/Projection/DomainProjectionDocumentTransformerInterface.php @@ -0,0 +1,16 @@ + + */ +interface DomainProjectionDocumentTransformerInterface +{ + /** + * @param object $object + */ + public function transform($object): DomainProjectionDocument; +} diff --git a/src/Domain/Projection/DomainProjectionInterface.php b/src/Domain/Projection/DomainProjectionInterface.php new file mode 100644 index 00000000..c129204f --- /dev/null +++ b/src/Domain/Projection/DomainProjectionInterface.php @@ -0,0 +1,13 @@ + + */ +interface DomainProjectionInterface +{ + public static function fromDocument(array $document): self; +} diff --git a/src/Domain/Projection/DomainProjectionRepositoryInterface.php b/src/Domain/Projection/DomainProjectionRepositoryInterface.php new file mode 100644 index 00000000..e85aafdf --- /dev/null +++ b/src/Domain/Projection/DomainProjectionRepositoryInterface.php @@ -0,0 +1,24 @@ + + */ +interface DomainProjectionRepositoryInterface +{ + /** + * @return DomainProjectionInterface[] + */ + public function findAll(string $type, int $offset = 0, int $limit = 0): iterable; + + public function find(string $type, string $id): ?DomainProjectionInterface; + + public function clear(string $type): void; + + public function save(DomainProjectionDocument $document): void; + + public function delete(DomainProjectionDocument $document): void; +} diff --git a/src/Domain/Projection/DomainProjectionSynchronization.php b/src/Domain/Projection/DomainProjectionSynchronization.php new file mode 100644 index 00000000..98386006 --- /dev/null +++ b/src/Domain/Projection/DomainProjectionSynchronization.php @@ -0,0 +1,61 @@ + + */ +final class DomainProjectionSynchronization +{ + private $typeRegistry; + private $repository; + private $providers; + private $transformer; + + public function __construct(DomainProjectionTypeRegistryInterface $typeRegistry, DomainProjectionRepositoryInterface $repository, DomainProjectionDocumentTransformerInterface $transformer, iterable $providers) + { + $this->typeRegistry = $typeRegistry; + $this->repository = $repository; + $this->providers = $providers; + $this->transformer = $transformer; + } + + /** + * @return DomainProjectionDocument[] + */ + public function synchronize(): iterable + { + foreach ($this->typeRegistry->all() as $type) { + $this->repository->clear($type); + } + + foreach ($this->providers as $provider) { + foreach ($provider() as $object) { + try { + $document = $this->transformer->transform($object); + } catch (\Exception $e) { + $document = new DomainProjectionDocument(); + $document->status = DomainProjectionDocument::STATUS_FAILED_TRANSFORMATION; + $document->source = $object; + $document->error = $e; + + yield $document; + continue; + } + + try { + $document->status = DomainProjectionDocument::STATUS_VALID; + + $this->repository->save($document); + } catch (\Exception $e) { + $document->status = DomainProjectionDocument::STATUS_FAILED_SAVING; + $document->error = $e; + } finally { + yield $document; + } + } + } + } +} diff --git a/src/Domain/Projection/DomainProjectionTypeRegistryInterface.php b/src/Domain/Projection/DomainProjectionTypeRegistryInterface.php new file mode 100644 index 00000000..8d4f148a --- /dev/null +++ b/src/Domain/Projection/DomainProjectionTypeRegistryInterface.php @@ -0,0 +1,20 @@ + + */ +interface DomainProjectionTypeRegistryInterface +{ + /** + * @return string[] + */ + public function all(): array; + + public function initialize(): void; + + public function destroy(): void; +} diff --git a/src/Domain/composer.json b/src/Domain/composer.json index 51f38d2f..1022f641 100644 --- a/src/Domain/composer.json +++ b/src/Domain/composer.json @@ -21,8 +21,10 @@ "php": "^7.1.0" }, "require-dev": { + "api-platform/core": "^2.2", "doctrine/doctrine-bundle": "^1.8", "doctrine/orm": "^2.6", + "elasticsearch/elasticsearch": "^6.0", "psr/container": "^1.0", "ramsey/uuid": "^3.7", "ramsey/uuid-doctrine": "^1.4", From 9b85f95f6bdb293ef531d0cf77bd1833ea0c725a Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 24 Mar 2018 15:38:18 +0100 Subject: [PATCH 67/79] fix doc title case --- docs/code-conventions.md | 24 ++++++++++----------- docs/cookbook/bundle-installation.md | 2 +- docs/ddd/factory/entity-aware.md | 2 +- docs/ddd/factory/object.md | 2 +- docs/ddd/factory/static.md | 2 +- docs/ddd/identity-mapping.md | 2 +- docs/event-sourcing/event-handlers.md | 2 +- docs/infrastructure/doctrine-collections.md | 2 +- docs/infrastructure/doctrine-dbal.md | 2 +- docs/infrastructure/doctrine-orm.md | 6 +++--- docs/infrastructure/elasticsearch.md | 2 +- docs/infrastructure/psr-container.md | 2 +- docs/infrastructure/symfony-console.md | 2 +- docs/message-driven/cqrs.md | 2 +- docs/message-driven/message-bus.md | 2 +- docs/message-driven/message-dispatcher.md | 2 +- docs/projection/document-transformers.md | 2 +- docs/projection/documents.md | 2 +- docs/projection/repositories.md | 2 +- docs/projection/synchronization.md | 2 +- docs/projection/type-registry.md | 2 +- docs/reference/doctrine-identifier-types.md | 2 +- docs/reference/entities.md | 20 ++++++++--------- 23 files changed, 45 insertions(+), 45 deletions(-) diff --git a/docs/code-conventions.md b/docs/code-conventions.md index 78e1d979..03ee5b68 100644 --- a/docs/code-conventions.md +++ b/docs/code-conventions.md @@ -1,18 +1,18 @@ -# Code conventions +# Code Conventions A brief description of code conventions this project follows. -## General principles +## General Principles - Follows [SOLID] principles - Reduce [lines of code] where possible -- Reduce coupling ([law of demeter]) +- Reduce coupling ([Law of Demeter]) - Favor latest stable PHP7 features - Checks must pass (code style, static analysis & unit tests) -## Code style +## Code Style (CS) -- Follows [PSR2] with [Symfony style] +- Follows [PSR-2] with [Symfony Sstyle] - `use` statements are declared in alpha-order - `use` statements for `MsgPhp\` namespace are grouped by deepest common namespace @@ -37,7 +37,7 @@ use Other\SomeOtherA; use Other\SomeOtherB; ``` -## Static analysis +## Static Analysis (SA) - Follows [PHPStan] level max - Exclude- and ignore-rules are discussed per case / topic @@ -49,17 +49,17 @@ use Other\SomeOtherB; - Interfaces must have a description with its purpose (at the class- as well as the method-level) - No usage of `@inheritdoc` -## PHP 7.2 forward compatibility +## PHP 7.2 Forward Compatibility - Intended object values are type hinted (`@param object $value` and `@return object`) -## Unit tests +## Unit Tests -- All of the above, _in general_, applies to unit tests as well +- All of the above, _in general_, apply to unit tests as well [SOLID]: https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) [lines of code]: https://en.wikipedia.org/wiki/Source_lines_of_code -[law of demeter]: https://en.wikipedia.org/wiki/Law_of_Demeter -[PSR2]: https://www.php-fig.org/psr/psr-2/ -[Symfony style]: https://symfony.com/doc/master/contributing/code/standards.html +[Law of Demeter]: https://en.wikipedia.org/wiki/Law_of_Demeter +[PSR-2]: https://www.php-fig.org/psr/psr-2/ +[Symfony Style]: https://symfony.com/doc/master/contributing/code/standards.html [PHPStan]: https://github.com/phpstan/phpstan diff --git a/docs/cookbook/bundle-installation.md b/docs/cookbook/bundle-installation.md index d5ad50ee..d7075aee 100644 --- a/docs/cookbook/bundle-installation.md +++ b/docs/cookbook/bundle-installation.md @@ -1,4 +1,4 @@ -# Bundle installation +# Bundle Installation The project bundles are tailored to the [Symfony Framework] and designed to be used standalone. Its main purpose is to provide the application with services, based on minimal configuration and to be used with [dependency injection]. In diff --git a/docs/ddd/factory/entity-aware.md b/docs/ddd/factory/entity-aware.md index aed9feab..53a60dd7 100644 --- a/docs/ddd/factory/entity-aware.md +++ b/docs/ddd/factory/entity-aware.md @@ -1,4 +1,4 @@ -# Entity aware factory +# Entity Aware Factory An entity aware factory is an [object factory](object.md) and additionally bound to `MsgPhp\Domain\Factory\EntityAwareFactoryInterface`. Its purpose is to factorize entity related objects. diff --git a/docs/ddd/factory/object.md b/docs/ddd/factory/object.md index 8f7a6aeb..a96d52a6 100644 --- a/docs/ddd/factory/object.md +++ b/docs/ddd/factory/object.md @@ -1,4 +1,4 @@ -# Object factory +# Object Factory A domain object factory is bound to `MsgPhp\Domain\Factory\DomainObjectFactoryInterface`. Its purpose is to initialize any domain object based on a given class name and context. diff --git a/docs/ddd/factory/static.md b/docs/ddd/factory/static.md index b5a0e018..0ccb0dbb 100644 --- a/docs/ddd/factory/static.md +++ b/docs/ddd/factory/static.md @@ -1,4 +1,4 @@ -# Static factory +# Static Factory A static factory is a utility class, it cannot be initialized as a new instance using `new ...();`. Its purpose is to factorize a known implementation for a given class. diff --git a/docs/ddd/identity-mapping.md b/docs/ddd/identity-mapping.md index 10c164ab..5f0408f6 100644 --- a/docs/ddd/identity-mapping.md +++ b/docs/ddd/identity-mapping.md @@ -1,4 +1,4 @@ -# Identity mapping +# Identity Mapping An identity mapping is a domain service and is bound to `MsgPhp\Domain\DomainIdentityMappingInterface`. It tells about the identifier metadata for a known domain object. diff --git a/docs/event-sourcing/event-handlers.md b/docs/event-sourcing/event-handlers.md index cd3fb585..1d8b7a03 100644 --- a/docs/event-sourcing/event-handlers.md +++ b/docs/event-sourcing/event-handlers.md @@ -1,4 +1,4 @@ -# Event handlers +# Event Handlers A domain event handler is bound to `MsgPhp\Domain\Event\DomainEventHandlerInterface`. Its purpose is to implement the handling of [domain events](events.md) within a certain context. diff --git a/docs/infrastructure/doctrine-collections.md b/docs/infrastructure/doctrine-collections.md index ccb16c0a..18f116e1 100644 --- a/docs/infrastructure/doctrine-collections.md +++ b/docs/infrastructure/doctrine-collections.md @@ -4,7 +4,7 @@ An overview of available infrastructural code when using Doctrine's [Collections - Requires [doctrine/collections] -## Domain collection +## Domain Collection A Doctrine tailored [domain collection](../ddd/collections.md) is provided by `MsgPhp\Domain\Infra\Doctrine\DomainCollection`. It leverages type `Doctrine\Common\Collections\Collection` as underlying data type. diff --git a/docs/infrastructure/doctrine-dbal.md b/docs/infrastructure/doctrine-dbal.md index d35f740d..cb37abe7 100644 --- a/docs/infrastructure/doctrine-dbal.md +++ b/docs/infrastructure/doctrine-dbal.md @@ -4,7 +4,7 @@ An overview of available infrastructural code when using Doctrine's [Database Ab - Requires [doctrine/dbal] -## Domain identifier type +## Domain Identifier Type A translation between the database type and a [identifier](../ddd/identifiers.md) type in PHP is provided by `MsgPhp\Domain\Infra\Doctrine\DomainIdType`. Its purpose is to abstract the underlying data type of the identifier diff --git a/docs/infrastructure/doctrine-orm.md b/docs/infrastructure/doctrine-orm.md index b947f0bf..b840fa4c 100644 --- a/docs/infrastructure/doctrine-orm.md +++ b/docs/infrastructure/doctrine-orm.md @@ -4,7 +4,7 @@ An overview of available infrastructural code when using Doctrine's [Object Rela - Requires [doctrine/orm] -## Domain identity mapping +## Domain Identity Mapping A Doctrine tailored [domain identity mapping](../ddd/identity-mapping.md) is provided by `MsgPhp\Domain\Infra\Doctrine\DomainIdentityMapping`. It uses Doctrine's [`EntityManagerInterface`][api-em] to provide @@ -29,7 +29,7 @@ $em = ...; $mapping = new DomainIdentityMapping($em); ``` -## Domain repository +## Domain Repository A Doctrine tailored [repository trait](../ddd/repositories.md) is provided by `MsgPhp\Domain\Infra\Doctrine\DomainEntityRepositoryTrait`. It uses Doctrine's [`EntityManagerInterface`][api-em] as @@ -91,7 +91,7 @@ if ($repository->exists($id = ['name' => ..., 'year' => ...])) { } ``` -## Entity aware factory +## Entity Aware Factory A Doctrine tailored [entity aware factory](../ddd/factory/entity-aware.md) is provided by `MsgPhp\Domain\Infra\Doctrine\EntityAwareFactory`. It decorates any entity aware factory and uses Doctrine's diff --git a/docs/infrastructure/elasticsearch.md b/docs/infrastructure/elasticsearch.md index ac4f1f04..6e382901 100644 --- a/docs/infrastructure/elasticsearch.md +++ b/docs/infrastructure/elasticsearch.md @@ -4,7 +4,7 @@ An overview of available infrastructural code when using Elasticsearch's [PHP Ap - Requires [elasticsearch/elasticsearch] -## Domain projection type registry +## Domain Projection Type Registry An Elasticsearch tailored [domain projection type registry](../projection/type-registry.md) is provided by `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionTypeRegistry`. It works directly with any [`Client`][api-client] and a known configuration of type information. diff --git a/docs/infrastructure/psr-container.md b/docs/infrastructure/psr-container.md index 1b20b1e5..f7ee7b78 100644 --- a/docs/infrastructure/psr-container.md +++ b/docs/infrastructure/psr-container.md @@ -4,7 +4,7 @@ An overview of available infrastructural code when using PSR [Containers][contai - Requires [psr/container] -## Domain projection document transformer +## Domain Projection Document Transformer A PSR tailored [domain projection document transformer](../projection/document-transformers.md) is provided by `MsgPhp\Domain\Infra\Psr\DomainProjectionDocumentTransformer`. It decorates any [`ContainerInterface`][api-container] and uses callable factories as container entries, identified by a diff --git a/docs/infrastructure/symfony-console.md b/docs/infrastructure/symfony-console.md index ccedb7f9..5c36da73 100644 --- a/docs/infrastructure/symfony-console.md +++ b/docs/infrastructure/symfony-console.md @@ -26,7 +26,7 @@ utility service. bin/console domain:projection:synchronize ``` -## Context factory +## Context Factory A context factory is bound to `MsgPhp\Domain\Infra\Console\Context\ContextFactoryInterface`. Its purpose is to leverage a CLI command in an effort to interactively built an arbitrary array value (the context). diff --git a/docs/message-driven/cqrs.md b/docs/message-driven/cqrs.md index 0b4dcf8d..5cd06089 100644 --- a/docs/message-driven/cqrs.md +++ b/docs/message-driven/cqrs.md @@ -3,7 +3,7 @@ Commands are domain objects and provided per domain layer. They usually follow a [POPO] design. Its purpose is to describe an action to be taken. For commands being messages they can be dispatched using any [message bus](message-bus.md). -## Event-sourcing command handler +## Event-Sourcing Command Handler An event-sourcing command handler utility trait is provided by `MsgPhp\Domain\Command\EventSourcingCommandHandlerTrait`. Its purpose is to ease the handling of command messages by sourcing a [domain event](../event-sourcing/events.md) to its diff --git a/docs/message-driven/message-bus.md b/docs/message-driven/message-bus.md index 6d4d0062..e164ecfb 100644 --- a/docs/message-driven/message-bus.md +++ b/docs/message-driven/message-bus.md @@ -1,4 +1,4 @@ -# Message bus +# Message Bus A domain message bus is bound to `MsgPhp\Domain\Message\DomainMessageBusInterface`. Its purpose is to dispatch any type of message object and helps you to use [CQRS](cqrs.md) and [event sourcing](../event-sourcing/event-handlers.md). diff --git a/docs/message-driven/message-dispatcher.md b/docs/message-driven/message-dispatcher.md index 97a139db..67084e94 100644 --- a/docs/message-driven/message-dispatcher.md +++ b/docs/message-driven/message-dispatcher.md @@ -1,4 +1,4 @@ -# Message dispatcher +# Message Dispatcher The domain message dispatcher is a utility trait. Its purpose is to dispatch a factorized message object using a [object factory](../ddd/factory/object.md) and a [message bus](message-bus.md). diff --git a/docs/projection/document-transformers.md b/docs/projection/document-transformers.md index 86cd1371..537bc3be 100644 --- a/docs/projection/document-transformers.md +++ b/docs/projection/document-transformers.md @@ -1,4 +1,4 @@ -# Projection document transformers +# Projection Document Transformers A projection document transformer is bound to `MsgPhp\Domain\Projection\DomainProjectionDocumentTransformerInterface`. Its purpose is to transform domain objects into [projection documents](documents.md). diff --git a/docs/projection/documents.md b/docs/projection/documents.md index 787489fe..b382ce4c 100644 --- a/docs/projection/documents.md +++ b/docs/projection/documents.md @@ -1,4 +1,4 @@ -# Projection documents +# Projection Documents A projection document is a domain value object of type `MsgPhp\Domain\Projection\DomainProjectionDocument`. Its purpose is to hold a projection document its (meta)data and current state. diff --git a/docs/projection/repositories.md b/docs/projection/repositories.md index e75b2871..ae16e0f7 100644 --- a/docs/projection/repositories.md +++ b/docs/projection/repositories.md @@ -1,4 +1,4 @@ -# Projection repositories +# Projection Repositories A domain projection repository is bound to `MsgPhp\Domain\Projection\DomainProjectionRepositoryInterface`. Its purpose is to store and query [projection documents](documents.md). diff --git a/docs/projection/synchronization.md b/docs/projection/synchronization.md index 468d6679..f726ea7f 100644 --- a/docs/projection/synchronization.md +++ b/docs/projection/synchronization.md @@ -1,4 +1,4 @@ -# Projection synchronization +# Projection Synchronization `MsgPhp\Domain\Projection\DomainProjectionSynchronization` is a utility domain service. Its purpose is to ease synchronizing provided [projections](models.md). diff --git a/docs/projection/type-registry.md b/docs/projection/type-registry.md index 3c35f37d..406430e8 100644 --- a/docs/projection/type-registry.md +++ b/docs/projection/type-registry.md @@ -1,4 +1,4 @@ -# Projection type registry +# Projection Type Registry A projection type registry is bound to `MsgPhp\Domain\Projection\DomainProjectionTypeRegistryInterface`. Its purpose is to manage all available [projection](models.md) type information. diff --git a/docs/reference/doctrine-identifier-types.md b/docs/reference/doctrine-identifier-types.md index a19e7293..581c7823 100644 --- a/docs/reference/doctrine-identifier-types.md +++ b/docs/reference/doctrine-identifier-types.md @@ -1,4 +1,4 @@ -# Doctrine identifier types +# Doctrine Identifier Types Reference of available [Doctrine identifier types](../infrastructure/doctrine-dbal.md#domain-identifier-type) across all domains. diff --git a/docs/reference/entities.md b/docs/reference/entities.md index 2a5dc438..6fa40ed3 100644 --- a/docs/reference/entities.md +++ b/docs/reference/entities.md @@ -1,21 +1,21 @@ -# Domain entities +# Domain Entities Reference of available [entities](../ddd/entities.md) per domain. -## Base domain +## Base Domain -### Entity fields +### Entity Fields - `MsgPhp\Domain\Entity\Fields\CreatedAtField` - `MsgPhp\Domain\Entity\Fields\EnabledField` - `MsgPhp\Domain\Entity\Fields\LastUpdatedAtField` -### Entity features +### Entity Features - `MsgPhp\Domain\Entity\Features\CanBeConfirmed` - `MsgPhp\Domain\Entity\Features\CanBeEnabled` -## EAV domain +## EAV Domain ### Entities @@ -24,12 +24,12 @@ Class | Abstract | Required `MsgPhp\Eav\Entity\Attribute` | ✔ | ✔ `MsgPhp\Eav\Entity\AttributeValue` | ✔ | ✔ -### Entity fields +### Entity Fields - `MsgPhp\Eav\Entity\Fields\AttributesField` - `MsgPhp\Eav\Entity\Fields\AttributeValueField` -## User domain +## User Domain ### Entities @@ -42,7 +42,7 @@ Class | Abstract | Required `MsgPhp\User\Entity\Username` | ✗ | ✗ `MsgPhp\User\Entity\UserRole` | ✔ | ✗ -### Entity fields +### Entity Fields - `MsgPhp\User\Entity\Fields\AttributeValuesField` - `MsgPhp\User\Entity\Fields\EmailsField` @@ -50,11 +50,11 @@ Class | Abstract | Required - `MsgPhp\User\Entity\Fields\RolesField` - `MsgPhp\User\Entity\Fields\UserField` -### Entity features +### Entity Features - `MsgPhp\User\Entity\Features\ResettablePassword` -#### Credential types +#### Credential Types - `MsgPhp\User\Entity\Features\EmailCredential` - `MsgPhp\User\Entity\Features\EmailPasswordCredential` From cca9082bad24ae0f947323f2314dc24bb7e81405 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 24 Mar 2018 15:40:21 +0100 Subject: [PATCH 68/79] fix doc title case --- mkdocs.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 0e84eca7..021c84f6 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,30 +14,30 @@ markdown_extensions: pages: - Home: index.md -- Code conventions: code-conventions.md -- Domain-driven-design: +- Code Conventions: code-conventions.md +- Domain-Driven-Design: - Identifiers: ddd/identifiers.md - Identities: ddd/identities.md - Identity mapping: ddd/identity-mapping.md - Collections: ddd/collections.md - Factories: - - Object factory: ddd/factory/object.md - - Entity aware factory: ddd/factory/entity-aware.md - - Static factory: ddd/factory/static.md + - Object Factory: ddd/factory/object.md + - Entity Aware Factory: ddd/factory/entity-aware.md + - Static Factory: ddd/factory/static.md - Repositories: ddd/repositories.md - Entities: ddd/entities.md -- Event sourcing: +- Event-Sourcing: - Events: event-sourcing/events.md - Event handlers: event-sourcing/event-handlers.md -- Message driven: - - Message bus: message-driven/message-bus.md - - Message dispatcher: message-driven/message-dispatcher.md +- Message Driven: + - Message Bus: message-driven/message-bus.md + - Message Dispatcher: message-driven/message-dispatcher.md - CQRS: message-driven/cqrs.md - Projection: - Models: projection/models.md - Documents: projection/documents.md - - Document transformers: projection/document-transformers.md - - Type registry: projection/type-registry.md + - Document Transformers: projection/document-transformers.md + - Type Registry: projection/type-registry.md - Repositories: projection/repositories.md - Synchronization: projection/synchronization.md - Infrastructure: @@ -50,8 +50,8 @@ pages: - Symfony Console: infrastructure/symfony-console.md - UUID: infrastructure/uuid.md - Cookbook: - - Bundle installation: cookbook/bundle-installation.md + - Bundle Installation: cookbook/bundle-installation.md - Reference: - - Domain identifiers: reference/identifiers.md - - Domain entities: reference/entities.md - - Doctrine identifier types: reference/doctrine-identifier-types.md + - Domain Identifiers: reference/identifiers.md + - Domain Entities: reference/entities.md + - Doctrine Identifier Types: reference/doctrine-identifier-types.md From ee218598cec2b25b896b0b4e7e164c6453ab42cf Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 24 Mar 2018 15:51:15 +0100 Subject: [PATCH 69/79] fix doc title case --- docs/cookbook/bundle-installation.md | 8 ++++---- docs/ddd/entities.md | 4 ++-- docs/event-sourcing/events.md | 2 -- docs/infrastructure/api-platform.md | 2 +- docs/infrastructure/elasticsearch.md | 2 +- docs/infrastructure/uuid.md | 2 +- docs/reference/entities.md | 4 ++++ docs/reference/identifiers.md | 12 ++++++++---- mkdocs.yml | 4 ++-- 9 files changed, 23 insertions(+), 17 deletions(-) diff --git a/docs/cookbook/bundle-installation.md b/docs/cookbook/bundle-installation.md index d7075aee..1c066852 100644 --- a/docs/cookbook/bundle-installation.md +++ b/docs/cookbook/bundle-installation.md @@ -38,7 +38,7 @@ msgphp_: Depending on your personal preference you can also write the configuration in any other supported format. See the [demo configuration] for a more advanced example. -### Class mapping +### `class_mapping` Configures the bundle with a class mapping to to well which classes of yours should be used for a known class of ours. @@ -53,7 +53,7 @@ The class mapping applies when working with an [object factory](../ddd/factory/o Depending on the bundle a specific class mapping entry might enable one of the bundle its features which is otherwise disabled by default. -### Identifier type mapping +### `id_type_mapping` Configures the bundle [domain identifier](../ddd/identifiers.md) types. Each key must be a sub class of `MsgPhp\Domain\DomainIdInterface` whereas each value must be a known type name. @@ -74,7 +74,7 @@ By convention any [Doctrine DBAL type] can be used. Additionally the following U See also the [reference](../reference/identifiers.md) page for all available identifiers provided per domain. -### Default identifier type +### `default_id_type` Configures a default type name to use for all known domain identifiers provided by the bundle. @@ -83,7 +83,7 @@ msgphp_: default_id_type: uuid ``` -### Commands +### `commands` By default a command handler provided by the bundle might be enabled or disabled depending on an [entity feature](../ddd/entities.md#common-entity-features) is being used yes or no. diff --git a/docs/ddd/entities.md b/docs/ddd/entities.md index 4a3fa3a6..cad7ed2e 100644 --- a/docs/ddd/entities.md +++ b/docs/ddd/entities.md @@ -9,7 +9,7 @@ They are defined in a dedicated namespace for discovery, respectively `Msgphp\Do See also the [reference](../reference/entities.md) page for all available entities provided per domain. -## Common entity fields +## Entity Fields ### `CreatedAtField` @@ -29,7 +29,7 @@ A datetime value representing an entity was last updated at. Requires `$lastUpda - `getLastUpdatedAt(): \DateTimeInterface` -## Common entity features +## Entity Features ### `CanBeConfirmed` diff --git a/docs/event-sourcing/events.md b/docs/event-sourcing/events.md index 481272c9..5f082d1f 100644 --- a/docs/event-sourcing/events.md +++ b/docs/event-sourcing/events.md @@ -5,8 +5,6 @@ which represent something that happens. When handled it might lead to an applica ## Implementations -Domain events provided and handled by default [entity features](../ddd/entities.md#common-entity-features): - ### `MsgPhp\Domain\Event\ConfirmEvent` Triggers a confirmation. Handled by default with `MsgPhp\Domain\Entity\Features\CanBeConfirmed::handleConfirmEvent()`. diff --git a/docs/infrastructure/api-platform.md b/docs/infrastructure/api-platform.md index 1f71ad07..df8ed43f 100644 --- a/docs/infrastructure/api-platform.md +++ b/docs/infrastructure/api-platform.md @@ -4,7 +4,7 @@ An overview of available infrastructural code when using [API Platform]. - Requires [api-platform/core] -## Domain projection data provider +## Domain Projection Data Provider When working with [projections](../projection/models.md) an [API Data Provider] is provided by `MsgPhp\Domain\Infra\ApiPlatform\DomainProjectionDataProvider`. It uses any [projection repository](../projection/repositories.md) in an effort to provide API resources. diff --git a/docs/infrastructure/elasticsearch.md b/docs/infrastructure/elasticsearch.md index 6e382901..c7e82d87 100644 --- a/docs/infrastructure/elasticsearch.md +++ b/docs/infrastructure/elasticsearch.md @@ -47,7 +47,7 @@ $typeRegistry = new DomainProjectionTypeRegistry($client, 'some_index', [ ]); ``` -## Domain projection repository +## Domain Projection Repository An Elasticsearch tailored [domain projection repository](../projection/repositories.md) is provided by `MsgPhp\Domain\Infra\Elasticsearch\DomainProjectionRepository`. It works directly with any [`Client`][api-client]. diff --git a/docs/infrastructure/uuid.md b/docs/infrastructure/uuid.md index b851ac3b..51aedf0e 100644 --- a/docs/infrastructure/uuid.md +++ b/docs/infrastructure/uuid.md @@ -4,7 +4,7 @@ An overview of available infrastructural code when working with [UUIDs][uuid]. - Requires [ramsey/uuid] -## Domain identifier +## Domain Identifier A UUID tailored [domain identifier](../ddd/identifiers.md) is provided by `MsgPhp\Domain\Infra\Uuid\DomainId`. It leverages type `Ramsey\Uuid\UuidInterface` as underlying data type. diff --git a/docs/reference/entities.md b/docs/reference/entities.md index 6fa40ed3..a24fc177 100644 --- a/docs/reference/entities.md +++ b/docs/reference/entities.md @@ -15,6 +15,8 @@ Reference of available [entities](../ddd/entities.md) per domain. - `MsgPhp\Domain\Entity\Features\CanBeConfirmed` - `MsgPhp\Domain\Entity\Features\CanBeEnabled` +--- + ## EAV Domain ### Entities @@ -29,6 +31,8 @@ Class | Abstract | Required - `MsgPhp\Eav\Entity\Fields\AttributesField` - `MsgPhp\Eav\Entity\Fields\AttributeValueField` +--- + ## User Domain ### Entities diff --git a/docs/reference/identifiers.md b/docs/reference/identifiers.md index 912cce13..df3e3b53 100644 --- a/docs/reference/identifiers.md +++ b/docs/reference/identifiers.md @@ -1,14 +1,16 @@ -# Domain identifiers +# Domain Identifiers Reference of available [identifiers](../ddd/identifiers.md) per domain. -## Base domain +## Base Domain - `MsgPhp\Domain\DomainIdInterface` - Scalar types: `MsgPhp\Domain\DomainId` - UUID types: `MsgPhp\Domain\Infra\Uuid\DomainId` -## EAV domain +--- + +## EAV Domain - `MsgPhp\Eav\AttributeIdInterface` - Scalar types: `MsgPhp\Eav\AttributeId` @@ -17,7 +19,9 @@ Reference of available [identifiers](../ddd/identifiers.md) per domain. - Scalar types: `MsgPhp\Eav\AttributeValueId` - UUID types: `MsgPhp\Eav\Infra\Uuid\AttributeValueId` -## User domain +--- + +## User Domain - `MsgPhp\User\UserIdInterface` - Scalar types: `MsgPhp\User\UserId` diff --git a/mkdocs.yml b/mkdocs.yml index 021c84f6..62435617 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,7 +18,7 @@ pages: - Domain-Driven-Design: - Identifiers: ddd/identifiers.md - Identities: ddd/identities.md - - Identity mapping: ddd/identity-mapping.md + - Identity Mapping: ddd/identity-mapping.md - Collections: ddd/collections.md - Factories: - Object Factory: ddd/factory/object.md @@ -28,7 +28,7 @@ pages: - Entities: ddd/entities.md - Event-Sourcing: - Events: event-sourcing/events.md - - Event handlers: event-sourcing/event-handlers.md + - Event Handlers: event-sourcing/event-handlers.md - Message Driven: - Message Bus: message-driven/message-bus.md - Message Dispatcher: message-driven/message-dispatcher.md From 6e6ef7c122a0814b32e8c6b6b57605d66f72fa4a Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 24 Mar 2018 17:03:52 +0100 Subject: [PATCH 70/79] doc fixes --- docs/ddd/collections.md | 2 +- docs/ddd/factory/entity-aware.md | 8 ++++---- docs/ddd/factory/object.md | 2 +- docs/ddd/identifiers.md | 2 +- docs/ddd/repositories.md | 16 ++++++++-------- docs/infrastructure/doctrine-dbal.md | 2 +- docs/infrastructure/elasticsearch.md | 4 ++-- docs/infrastructure/symfony-console.md | 4 ++-- docs/projection/repositories.md | 10 +++++----- docs/projection/synchronization.md | 2 +- 10 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/ddd/collections.md b/docs/ddd/collections.md index 31434b8c..0a675d0c 100644 --- a/docs/ddd/collections.md +++ b/docs/ddd/collections.md @@ -14,7 +14,7 @@ a primitive iterable value. It may hold any type of element values. ### `static fromValue(?iterable $value): DomainCollectionInterface` -Factorizes a new collection from its primitive value. Using `null` implies an empty collection. +Returns a factorized collection from any primitive iterable. Using `null` implies an empty collection. --- diff --git a/docs/ddd/factory/entity-aware.md b/docs/ddd/factory/entity-aware.md index 53a60dd7..a606579f 100644 --- a/docs/ddd/factory/entity-aware.md +++ b/docs/ddd/factory/entity-aware.md @@ -13,20 +13,20 @@ Inherited from `MsgPhp\Domain\Factory\DomainObjectFactoryInterface::create()`. ### `reference(string $class, $id): object` -Factorize a reference object for a known existing entity object. The object must be of type `$class`. Any type of -[identity](../identities.md) value can be passed as `$id`. +Returns a factorized reference object for a known existing entity object. The object must be of type `$class`. Any type +of [identity](../identities.md) value can be passed as `$id`. --- ### `identify(string $class, $value): DomainIdInterface` -Factorize an [identifier](../identifiers.md) for the given entity class from a known primitive value. +Returns a factorized [identifier](../identifiers.md) for the given entity class from a known primitive value. --- ### `nextIdentifier(string $class): DomainIdInterface` -Factorize the next [identifier](../identifiers.md) for the given entity class. Depending on the implementation its value +Returns the nex [identifier](../identifiers.md) for the given entity class. Depending on the implementation its value might be considered empty if it's not capable to calculate one upfront. ## Implementations diff --git a/docs/ddd/factory/object.md b/docs/ddd/factory/object.md index a96d52a6..e056df2e 100644 --- a/docs/ddd/factory/object.md +++ b/docs/ddd/factory/object.md @@ -7,7 +7,7 @@ any domain object based on a given class name and context. ### `create(string $class, array $context = []): object` -Factorizes a new domain object by class name. Optionally a context can be provided for the factory to act upon. +Returns a factorized domain object by class name. Optionally a context can be provided for the factory to act upon. ## Implementations diff --git a/docs/ddd/identifiers.md b/docs/ddd/identifiers.md index df2825c9..e850cbfc 100644 --- a/docs/ddd/identifiers.md +++ b/docs/ddd/identifiers.md @@ -14,7 +14,7 @@ utilize a primitive identifier value. ### `static fromValue($value): DomainIdInterface` -Factorizes a new identifier from its primitive value. Using `null` might imply an empty identifier. +Returns a factorized identifier from any primitive value. Using `null` might imply an empty identifier. --- diff --git a/docs/ddd/repositories.md b/docs/ddd/repositories.md index 8a3b4c0f..6b35af33 100644 --- a/docs/ddd/repositories.md +++ b/docs/ddd/repositories.md @@ -7,13 +7,13 @@ infrastructure (e.g. Doctrine), to rapidly create one. This page describes the A ### `doFindAll(int $offset = 0, int $limit = 0): DomainCollectionInterface` -Find all entities available. An unlimited collection is implied by `$limit` set to zero. +Finds all entities available. An unlimited collection is implied by `$limit` set to zero. --- ### `doFindAllByFields(array $fields, int $offset = 0, int $limit = 0): DomainCollectionInterface` -Find all entities matching all specified fields. Supported field values should be `null`, `scalar`, `array` (one of a +Finds all entities matching all specified fields. Supported field values should be `null`, `scalar`, `array` (one of a known literal list) and `object` (foreign entity or an [identifier](identifiers.md)). An unlimited collection is implied by `$limit` set to zero. @@ -21,38 +21,38 @@ by `$limit` set to zero. ### `doFind($id): object` -Find a single entity by its identity. Supported identity values should be `scalar`, `array` (composite [identity](identities.md)) +Finds a single entity by its identity. Supported identity values should be `scalar`, `array` (composite [identity](identities.md)) and `object` (foreign entity or an [identifier](identifiers.md)). --- ### `doFindByFields(array $fields): object` -Find the first entity matching all specified fields. See `doFindAllByFields()` for supported field values. +Finds the first entity matching all specified fields. See `doFindAllByFields()` for supported field values. --- ### `doExists($id): bool` -Verify if an entity exists by its identity. See `doFind()` for supported identity values. +Verifies if an entity exists by its identity. See `doFind()` for supported identity values. --- ### `doExistsByFields(array $fields): bool` -Verify if an entity exists matching all specified fields. See `doFindAllByFields()` for supported field values. +Verifies if an entity matching all specified fields exists. See `doFindAllByFields()` for supported field values. --- ### `doSave(object $entity): void` -Persist an entity in the identity map. The entity will be available on any subsequent query. +Persists an entity into the identity map. The entity will be available on any subsequent query. --- ### `doDelete(object $entity): void` -Remove an entity from the identity map. The entity will be unavailable on any subsequent query. +Removes an entity from the identity map. The entity will be unavailable on any subsequent query. ## Implementations diff --git a/docs/infrastructure/doctrine-dbal.md b/docs/infrastructure/doctrine-dbal.md index cb37abe7..fea38226 100644 --- a/docs/infrastructure/doctrine-dbal.md +++ b/docs/infrastructure/doctrine-dbal.md @@ -19,7 +19,7 @@ default [`Type`][api-type] implementation and can be used either generic or as a is used. - `static getClass(): string` - `static setDataType(string $type): void` - - `$type`: A doctrine type name to use as underlying data type. If not set `Type::INTEGER` is used. + - `$type`: A Doctrine type name to use as underlying data type. If not set `Type::INTEGER` is used. - `static getDataType(): string` See also the [reference](../reference/doctrine-identifier-types.md) page for all available identifier types provided per diff --git a/docs/infrastructure/elasticsearch.md b/docs/infrastructure/elasticsearch.md index c7e82d87..54824937 100644 --- a/docs/infrastructure/elasticsearch.md +++ b/docs/infrastructure/elasticsearch.md @@ -10,9 +10,9 @@ An Elasticsearch tailored [domain projection type registry](../projection/type-r It works directly with any [`Client`][api-client] and a known configuration of type information. - `__construct(Client $client, string $index, array $mappings, array $settings = [], LoggerInterface $logger = null)` - - `$client`: The Client to work with + - `$client`: The client to work with - `$index`: The index to use - - `$mappings` / `$settings`: Index management information. [Read more][index management] + - `$mappings` / `$settings`: Index management information. [Read more][index management]. - `$logger`: An optional [PSR logger] ### Basic example diff --git a/docs/infrastructure/symfony-console.md b/docs/infrastructure/symfony-console.md index 5c36da73..1b4efe99 100644 --- a/docs/infrastructure/symfony-console.md +++ b/docs/infrastructure/symfony-console.md @@ -35,14 +35,14 @@ a CLI command in an effort to interactively built an arbitrary array value (the #### `configure(InputDefinition $definition): void` -Configure a command input definition. See also [`InputDefinition`][api-inputdefinition]. Should be called before using +Configures a command input definition. See also [`InputDefinition`][api-inputdefinition]. Should be called before using `getContext()`. --- #### `getContext(InputInterface $input, StyleInterface $io, array $values = []): array` -Resolve the actual context from the console IO. See also [`InputInterface`][api-inputinterface] and [`StyleInterface`][api-styleinterface]. +Resolves the actual context from the console IO. See also [`InputInterface`][api-inputinterface] and [`StyleInterface`][api-styleinterface]. Any element value provided by `$values` takes precedence and should be used as-is. ### Implementations diff --git a/docs/projection/repositories.md b/docs/projection/repositories.md index ae16e0f7..c4dd4c4c 100644 --- a/docs/projection/repositories.md +++ b/docs/projection/repositories.md @@ -7,31 +7,31 @@ is to store and query [projection documents](documents.md). ### `findAll(string $type, int $offset = 0, int $limit = 0): iterable` -Find all [projections](models.md) by type. +Finds all [projections](models.md) by type. --- ### `find(string $type, string $id): ?DomainProjectionInterface` -Find a single [projection](models.md) by type and ID. In case its document cannot be found `null` should be returned. +Finds a single [projection](models.md) by type and ID. In case its document cannot be found `null` should be returned. --- ### `clear(string $type): void` -Delete all projection documents by type. +Deletes all projection documents by type. --- ### `save(DomainProjectionDocument $document): void` -Save a projection document. The document will be available on any subsequent query. +Saves a projection document. The document will be available on any subsequent query. --- ### `delete(DomainProjectionDocument $document): void` -Delete a projection document. The document will be unavailable on any subsequent query. +Deletes a projection document. The document will be unavailable on any subsequent query. ## Implementations diff --git a/docs/projection/synchronization.md b/docs/projection/synchronization.md index f726ea7f..05eca350 100644 --- a/docs/projection/synchronization.md +++ b/docs/projection/synchronization.md @@ -13,7 +13,7 @@ any [transformer](document-transformers.md). A document is then stored using any Yields a projection document for each provided domain object regarding its state. The actual document status can be read from [`ProjectionDocument::$status`][api-projection-document-status]. -[api-projection-document-status]: https://msgphp.github.io/api/MsgPhp/Domain/Projection/DomainProjectionSynchronization.html#property_status +[api-projection-document-status]: https://msgphp.github.io/api/MsgPhp/Domain/Projection/DomainProjectionDocument.html#property_status ## Basic example From c0f467a9bbff25466acceb3eb47a4502bebf3663 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 24 Mar 2018 17:16:23 +0100 Subject: [PATCH 71/79] added bin/smoke-test --- CONTRIBUTING.md | 4 ++-- bin/smoke-test | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100755 bin/smoke-test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d22baef..86b3b5b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ bin/phpunit bin/phpunit user-bundle ``` -## Code style +## Code Style ```bash # all packages @@ -20,7 +20,7 @@ bin/cs bin/cs user-bundle ``` -## Static analysis +## Static Analysis ```bash # all packages diff --git a/bin/smoke-test b/bin/smoke-test new file mode 100755 index 00000000..28c6fcd1 --- /dev/null +++ b/bin/smoke-test @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +echo -e "\e[34mUpdating dependencies...\e[0m" +bin/composer update --no-suggest --no-progress && composer update --no-suggest --no-progress +[[ $? -ne 0 ]] && exit 1 + +echo -e "\e[34mCode Style / Static Analysis\e[0m" +bin/cs && bin/sa +[[ $? -ne 0 ]] && exit 1 + +echo -e "\e[34mUnit Tests\e[0m" +bin/phpunit From b9550551ff9cec636c1a5a4638b491fc824c3ae4 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 24 Mar 2018 18:14:20 +0100 Subject: [PATCH 72/79] update contrib docs --- CONTRIBUTING.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 86b3b5b4..93971880 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,8 @@ To checkout another open pull request from this repository use: bin/pr ``` -It will add a new git remote `github-pr-XXX` pointing to the author's SSH URL and checkout their branch locally using the same name. +It will add a new git remote `github-pr-XXX` pointing to the author's SSH URL and checkout their branch locally using +the same name. ## Setup a project @@ -48,4 +49,13 @@ To setup a test project use: bin/create-project ``` -It will create a new Symfony skeleton application and ask you which bundles to install. +It will create a new Symfony skeleton application and ask you which bundles to install. The bundles will be +automatically symlinked to your local clone. + +## Perform a smoke test + +To quickly see if CI is likely to pass use: + +```bash +bin/smoke-test +``` From 3fd8ca49499fab1043a11d58bed98119155da0ae Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sat, 24 Mar 2018 18:23:29 +0100 Subject: [PATCH 73/79] update contrib docs --- CONTRIBUTING.md | 6 +++--- bin/create-project | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93971880..1a553843 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,7 +41,7 @@ bin/pr It will add a new git remote `github-pr-XXX` pointing to the author's SSH URL and checkout their branch locally using the same name. -## Setup a project +## Setup a test project To setup a test project use: @@ -49,8 +49,8 @@ To setup a test project use: bin/create-project ``` -It will create a new Symfony skeleton application and ask you which bundles to install. The bundles will be -automatically symlinked to your local clone. +It will create a new Symfony skeleton application and ask you which bundles to install. The bundles can be automatically +symlinked to your local clone after. ## Perform a smoke test diff --git a/bin/create-project b/bin/create-project index ed45ee81..0269166b 100755 --- a/bin/create-project +++ b/bin/create-project @@ -35,9 +35,9 @@ composer require --prefer-dist --no-suggest ${COMPONENTS} ${BUNDLES} popd &> /dev/null -echo -en "\e[34mLink dependencies? [yN]\e[0m" +echo -en "\e[34mLink dependencies? [Yn]\e[0m" read answer -if [[ $answer =~ ^y|Y|yes|YES$ ]] ; then +if [[ ${answer:-y} =~ ^y|Y|yes|YES$ ]] ; then composer link -h &> /dev/null if [[ $? != 0 ]] ; then composer require --dev --no-progress --prefer-dist --no-interaction --no-suggest ro0nl/link From 025d4fac0dfacf86034d2dac17c64d46c76a2bb7 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 25 Mar 2018 12:01:05 +0200 Subject: [PATCH 74/79] update readme --- README.md | 1 + src/Domain/README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 36c50a0f..21e33175 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Domain | Bundle ## Blog posts +- [Domain-driven-design: Moving forward with API Platform and Elasticsearch](https://medium.com/@ro0NL/domain-driven-design-moving-forward-with-api-platform-and-elasticsearch-f1705614f9e2) - [Domain-driven-design: Writing domain layers. The fast way.](https://medium.com/@ro0NL/domain-driven-design-writing-domain-layers-the-fast-way-60ef87399374) - [Initializing objects with CLI and the power of Symfony Console](https://medium.com/@ro0NL/initializing-objects-with-cli-and-the-power-of-symfony-console-2a008d5611f) - [Commanding a decoupled User entity](https://medium.com/@ro0NL/commanding-a-decoupled-user-entity-aee8723c43e5) diff --git a/src/Domain/README.md b/src/Domain/README.md index b606f2ca..1e099e70 100644 --- a/src/Domain/README.md +++ b/src/Domain/README.md @@ -24,6 +24,7 @@ composer require msgphp/domain ## Blog posts +- [Domain-driven-design: Moving forward with API Platform and Elasticsearch](https://medium.com/@ro0NL/domain-driven-design-moving-forward-with-api-platform-and-elasticsearch-f1705614f9e2) - [Domain-driven-design: Writing domain layers. The fast way.](https://medium.com/@ro0NL/domain-driven-design-writing-domain-layers-the-fast-way-60ef87399374) - [Initializing objects with CLI and the power of Symfony Console](https://medium.com/@ro0NL/initializing-objects-with-cli-and-the-power-of-symfony-console-2a008d5611f) From 1633276e523041a90df9d4caae054cdcee8cb2c3 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 25 Mar 2018 16:10:18 +0200 Subject: [PATCH 75/79] tweak ProjectionDocument (#126) --- docs/ddd/factory/entity-aware.md | 4 +- docs/projection/documents.md | 40 ++++++++++++++++--- .../Projection/DomainProjectionDocument.php | 39 +++++++++--------- 3 files changed, 55 insertions(+), 28 deletions(-) diff --git a/docs/ddd/factory/entity-aware.md b/docs/ddd/factory/entity-aware.md index a606579f..36707804 100644 --- a/docs/ddd/factory/entity-aware.md +++ b/docs/ddd/factory/entity-aware.md @@ -5,9 +5,9 @@ An entity aware factory is an [object factory](object.md) and additionally bound ## API -### `create(string $class, array $context = []): object` +### Extends -Inherited from `MsgPhp\Domain\Factory\DomainObjectFactoryInterface::create()`. +- [`DomainObjectFactoryInterface`](object.md) --- diff --git a/docs/projection/documents.md b/docs/projection/documents.md index b382ce4c..a8e6098a 100644 --- a/docs/projection/documents.md +++ b/docs/projection/documents.md @@ -1,14 +1,39 @@ # Projection Documents A projection document is a domain value object of type `MsgPhp\Domain\Projection\DomainProjectionDocument`. Its purpose -is to hold a projection document its (meta)data and current state. +is to hold a projection document its data and current state. ## API -### `static create(string $type, string $id = null, array $body = []): DomainProjectionDocument` +### Properties -Creates a projection document for a known projection type. In case `$id` is not given it implies an auto-generated -value. A projection `$type` usually refers to a known [projection](models.md) by class name. +- `int $status`: The current document status. See also [default statuses][api-statuses]. +- `?\Exception $error`: An occurred error, if any +- `?object $source`: The origin object source, if any + +--- + +### `getType(): string` + +Gets the projection type. Always must refer to a [projection](models.md) class name. + +--- + +### `getId(): ?string` + +Gets the document ID, if any. Otherwise an auto-generated value is implied. + +--- + +### `getBody(): array` + +Gets the document body. + +--- + +### `toProjection(): DomainProjectionInterface` + +Transforms the document into its projection model. ## Basic example @@ -28,7 +53,12 @@ class MyProjection implements DomainProjectionInterface } } -$document = DomainProjectionDocument::create(MyProjection::class, null, [ +$document = new DomainProjectionDocument(MyProjection::class, null, [ 'some_field' => 'value', ]); + +/** @var MyProjection $projection */ +$projection = $document->toProjection(); ``` + +[api-statuses]: https://msgphp.github.io/api/MsgPhp/Domain/Projection/DomainProjectionDocument.html#page-content diff --git a/src/Domain/Projection/DomainProjectionDocument.php b/src/Domain/Projection/DomainProjectionDocument.php index 8c9ca180..a2a01b59 100644 --- a/src/Domain/Projection/DomainProjectionDocument.php +++ b/src/Domain/Projection/DomainProjectionDocument.php @@ -9,9 +9,6 @@ */ final class DomainProjectionDocument { - private const DATA_TYPE_KEY = 'document_type'; - private const DATA_ID_KEY = 'document_id'; - public const STATUS_UNKNOWN = 1; public const STATUS_VALID = 2; public const STATUS_FAILED_TRANSFORMATION = 3; @@ -20,48 +17,48 @@ final class DomainProjectionDocument /** @var int */ public $status = self::STATUS_UNKNOWN; - /** @var array */ - public $data = []; - /** @var object|null */ public $source; /** @var \Exception|null $error */ public $error; - public static function create(string $type, string $id = null, array $body = []): self - { - $document = new self(); - $document->data[self::DATA_TYPE_KEY] = $type; - $document->data[self::DATA_ID_KEY] = $id; - $document->data += $body; + private $type; + private $id; + private $body = []; - return $document; + public function __construct(string $type = null, string $id = null, array $body = []) + { + $this->type = $type; + $this->id = $id; + $this->body = $body; } public function getType(): string { - if (!isset($this->data[self::DATA_TYPE_KEY])) { + if (null === $this->type) { throw new \LogicException('Document type not set.'); } - if (!is_subclass_of($type = $this->data[self::DATA_TYPE_KEY], DomainProjectionInterface::class)) { - throw new \LogicException(sprintf('Document type must be a sub class of "%s", got "%s".', DomainProjectionInterface::class, $type)); + if (!is_subclass_of($this->type, DomainProjectionInterface::class)) { + throw new \LogicException(sprintf('Document type must be a sub class of "%s", got "%s".', DomainProjectionInterface::class, $this->type)); } - return $type; + return $this->type; } public function getId(): ?string { - return $this->data[self::DATA_ID_KEY] ?? null; + return $this->id; } public function getBody(): array { - $data = $this->data; - unset($data[self::DATA_TYPE_KEY], $data[self::DATA_ID_KEY]); + return $this->body; + } - return $data; + public function toProjection(): DomainProjectionInterface + { + return $this->getType()::fromDocument($this->body); } } From b5ebe6f15f48114a2e7af333bb1c632b5c1fdd4b Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Sun, 25 Mar 2018 19:50:39 +0200 Subject: [PATCH 76/79] fix class mapping config (#127) --- .../Infra/DependencyInjection/ContainerHelper.php | 2 +- .../DependencyInjection/Configuration.php | 13 ++++++++----- src/UserBundle/DependencyInjection/Extension.php | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Domain/Infra/DependencyInjection/ContainerHelper.php b/src/Domain/Infra/DependencyInjection/ContainerHelper.php index 7698307d..dc8c94b2 100644 --- a/src/Domain/Infra/DependencyInjection/ContainerHelper.php +++ b/src/Domain/Infra/DependencyInjection/ContainerHelper.php @@ -220,7 +220,7 @@ public static function configureDoctrineOrmRepositories(ContainerBuilder $contai } foreach ($repositoryMapping as $repository => $class) { - if (null === $class || !isset($classMapping[$class])) { + if (!isset($classMapping[$class])) { self::removeDefinitionWithAliases($container, $repository); continue; } diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index e22e192d..01b336fe 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -94,6 +94,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->cannotBeEmpty() ->end() ->arrayNode('username_lookup') + ->requiresAtLeastOneElement() ->arrayPrototype() ->children() ->scalarNode('target') @@ -141,15 +142,17 @@ public function getConfigTreeBuilder(): TreeBuilder throw new \LogicException(sprintf('Username lookup mapping for "%s" cannot be overwritten.', $userClass)); } - if (null !== $userCredential['username_field']) { + if (isset($userCredential['username_field'])) { $usernameLookup[$userClass][] = ['target' => $userClass, 'field' => $userCredential['username_field']]; } + + $config['class_mapping'][Entity\Username::class] = Entity\Username::class; + } + + if (isset($userCredential['class'])) { + $config['class_mapping'][CredentialInterface::class] = $userCredential['class']; } - $config['class_mapping'] += [ - CredentialInterface::class => $userCredential['class'], - Entity\Username::class => $usernameLookup ? Entity\Username::class : null, - ]; $config['username_field'] = $userCredential['username_field']; $config['username_lookup'] = $usernameLookup; diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index ad95f81d..36fc46db 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -245,7 +245,7 @@ private static function getDoctrineMappingFiles(array $config, ContainerBuilder unset($files[$baseDir.'/User.Entity.UserAttributeValue.orm.xml']); } - if (null === $config['class_mapping'][Entity\Username::class]) { + if (!$config['username_lookup']) { unset($files[$baseDir.'/User.Entity.Username.orm.xml']); } From d70f9c46d298e21316bcfea6e27eb6b2aceea40a Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Tue, 27 Mar 2018 21:25:27 +0200 Subject: [PATCH 77/79] fix cross domain config --- src/UserBundle/DependencyInjection/Configuration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index 01b336fe..dd6f20d6 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -24,7 +24,7 @@ final class Configuration implements ConfigurationInterface ]; public const IDENTITY_MAPPING = [ Entity\Role::class => ['name'], - Entity\UserAttributeValue::class => ['attributeValue'], + 'Msgphp\\User\\Entity\\UserAttributeValue' => ['attributeValue'], Entity\User::class => ['id'], Entity\Username::class => ['username'], Entity\UserRole::class => ['user', 'role'], @@ -52,7 +52,7 @@ final class Configuration implements ConfigurationInterface Command\RequestUserPasswordCommand::class, ], ], - Entity\UserAttributeValue::class => [ + 'Msgphp\\User\\Entity\\UserAttributeValue' => [ Command\AddUserAttributeValueCommand::class => true, Command\ChangeUserAttributeValueCommand::class => true, Command\DeleteUserAttributeValueCommand::class => true, From 9626570298ac3bb6d406f06bf7241189e16ca8b0 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Tue, 27 Mar 2018 21:32:22 +0200 Subject: [PATCH 78/79] fix cross domain config --- src/UserBundle/DependencyInjection/Configuration.php | 4 ++-- src/UserBundle/DependencyInjection/Extension.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UserBundle/DependencyInjection/Configuration.php b/src/UserBundle/DependencyInjection/Configuration.php index dd6f20d6..83d76d09 100644 --- a/src/UserBundle/DependencyInjection/Configuration.php +++ b/src/UserBundle/DependencyInjection/Configuration.php @@ -24,7 +24,7 @@ final class Configuration implements ConfigurationInterface ]; public const IDENTITY_MAPPING = [ Entity\Role::class => ['name'], - 'Msgphp\\User\\Entity\\UserAttributeValue' => ['attributeValue'], + 'MsgPhp\\User\\Entity\\UserAttributeValue' => ['attributeValue'], Entity\User::class => ['id'], Entity\Username::class => ['username'], Entity\UserRole::class => ['user', 'role'], @@ -52,7 +52,7 @@ final class Configuration implements ConfigurationInterface Command\RequestUserPasswordCommand::class, ], ], - 'Msgphp\\User\\Entity\\UserAttributeValue' => [ + 'MsgPhp\\User\\Entity\\UserAttributeValue' => [ Command\AddUserAttributeValueCommand::class => true, Command\ChangeUserAttributeValueCommand::class => true, Command\DeleteUserAttributeValueCommand::class => true, diff --git a/src/UserBundle/DependencyInjection/Extension.php b/src/UserBundle/DependencyInjection/Extension.php index 36fc46db..e6384ac2 100644 --- a/src/UserBundle/DependencyInjection/Extension.php +++ b/src/UserBundle/DependencyInjection/Extension.php @@ -230,7 +230,7 @@ private function prepareDoctrineOrm(array $config, LoaderInterface $loader, Cont DoctrineInfra\Repository\RoleRepository::class => Entity\Role::class, DoctrineInfra\Repository\UserRepository::class => Entity\User::class, DoctrineInfra\Repository\UsernameRepository::class => Entity\Username::class, - DoctrineInfra\Repository\UserAttributeValueRepository::class => Entity\UserAttributeValue::class, + DoctrineInfra\Repository\UserAttributeValueRepository::class => 'MsgPhp\\User\\Entity\\UserAttributeValue', DoctrineInfra\Repository\UserRoleRepository::class => Entity\UserRole::class, DoctrineInfra\Repository\UserEmailRepository::class => Entity\UserEmail::class, ]); From 32b1a15f32a21dc52ae61927bc724889ea29f649 Mon Sep 17 00:00:00 2001 From: Roland Franssen Date: Wed, 28 Mar 2018 14:56:41 +0200 Subject: [PATCH 79/79] update readme --- README.md | 1 + src/Domain/README.md | 1 + src/Eav/README.md | 1 + src/EavBundle/README.md | 1 + src/User/README.md | 1 + src/UserBundle/README.md | 1 + 6 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 21e33175..92c69c59 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ MsgPHP is a project that aims to provide (common) message based domain layers fo - Read the [main documentation](https://msgphp.github.io/docs/) - Browse the [API documentation](https://msgphp.github.io/api/) - Try the Symfony [demo application](https://github.com/msgphp/symfony-demo-app) +- Get support on [Symfony's Slack `#msgphp` channel](https://symfony.com/slack-invite) or [raise an issue](https://github.com/msgphp/msgphp/issues/new) ## Packages diff --git a/src/Domain/README.md b/src/Domain/README.md index 1e099e70..c6b1e737 100644 --- a/src/Domain/README.md +++ b/src/Domain/README.md @@ -33,6 +33,7 @@ composer require msgphp/domain - Read the [main documentation](https://msgphp.github.io/docs/) - Browse the [API documentation](https://msgphp.github.io/api/MsgPhp/Domain.html) - Try the Symfony [demo application](https://github.com/msgphp/symfony-demo-app) +- Get support on [Symfony's Slack `#msgphp` channel](https://symfony.com/slack-invite) or [raise an issue](https://github.com/msgphp/msgphp/issues/new) ## Contributing diff --git a/src/Eav/README.md b/src/Eav/README.md index 926a6f53..1ba56165 100644 --- a/src/Eav/README.md +++ b/src/Eav/README.md @@ -25,6 +25,7 @@ composer require msgphp/eav - Read the [main documentation](https://msgphp.github.io/docs/) - Browse the [API documentation](https://msgphp.github.io/api/MsgPhp/Eav.html) - Try the Symfony [demo application](https://github.com/msgphp/symfony-demo-app) +- Get support on [Symfony's Slack `#msgphp` channel](https://symfony.com/slack-invite) or [raise an issue](https://github.com/msgphp/msgphp/issues/new) ## Contributing diff --git a/src/EavBundle/README.md b/src/EavBundle/README.md index de52b4fa..94ac3f67 100644 --- a/src/EavBundle/README.md +++ b/src/EavBundle/README.md @@ -69,6 +69,7 @@ doctrine: - Read the [main documentation](https://msgphp.github.io/docs/) - Browse the [API documentation](https://msgphp.github.io/api/MsgPhp/EavBundle.html) - Try the Symfony [demo application](https://github.com/msgphp/symfony-demo-app) +- Get support on [Symfony's Slack `#msgphp` channel](https://symfony.com/slack-invite) or [raise an issue](https://github.com/msgphp/msgphp/issues/new) ## Contributing diff --git a/src/User/README.md b/src/User/README.md index 50ffba29..136cc9ec 100644 --- a/src/User/README.md +++ b/src/User/README.md @@ -38,6 +38,7 @@ composer require msgphp/user - Read the [main documentation](https://msgphp.github.io/docs/) - Browse the [API documentation](https://msgphp.github.io/api/MsgPhp/User.html) - Try the Symfony [demo application](https://github.com/msgphp/symfony-demo-app) +- Get support on [Symfony's Slack `#msgphp` channel](https://symfony.com/slack-invite) or [raise an issue](https://github.com/msgphp/msgphp/issues/new) ## Contributing diff --git a/src/UserBundle/README.md b/src/UserBundle/README.md index 975d12f4..1c04c745 100644 --- a/src/UserBundle/README.md +++ b/src/UserBundle/README.md @@ -136,6 +136,7 @@ Validators from `MsgPhp\User\Infra\Validator\*` are registered as a service. - Read the [main documentation](https://msgphp.github.io/docs/) - Browse the [API documentation](https://msgphp.github.io/api/MsgPhp/UserBundle.html) - Try the Symfony [demo application](https://github.com/msgphp/symfony-demo-app) +- Get support on [Symfony's Slack `#msgphp` channel](https://symfony.com/slack-invite) or [raise an issue](https://github.com/msgphp/msgphp/issues/new) ## Contributing