From 36fef69f5acd215d3dcc21cf4d5dc76a44e24662 Mon Sep 17 00:00:00 2001 From: David Badura Date: Wed, 10 Dec 2025 14:28:39 +0100 Subject: [PATCH 1/6] use resource tags for aggregates, events and headers --- composer.json | 2 +- .../CommandHandlerCompilerPass.php | 6 +- src/DependencyInjection/Configuration.php | 21 ----- .../HandlerServiceLocatorCompilerPass.php | 6 +- .../PatchlevelEventSourcingExtension.php | 68 ++++++-------- .../RepositoryCompilerPass.php | 12 +-- .../ResourceCompilerPass.php | 32 +++++++ src/PatchlevelEventSourcingBundle.php | 2 + .../PatchlevelEventSourcingBundleTest.php | 91 +++++-------------- 9 files changed, 92 insertions(+), 148 deletions(-) create mode 100644 src/DependencyInjection/ResourceCompilerPass.php diff --git a/composer.json b/composer.json index aeb9f160..bdd76d5e 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,7 @@ "symfony/cache": "^6.4.0 || ^7.0.0 || ^8.0.0", "symfony/config": "^6.4.0 || ^7.0.0 || ^8.0.0", "symfony/console": "^6.4.1 || ^7.0.1 || ^8.0.0", - "symfony/dependency-injection": "^6.4.1 || ^7.0.1 || ^8.0.0", + "symfony/dependency-injection": "^7.3.0 || ^8.0.0", "symfony/event-dispatcher": "^6.4.1 || ^7.0.1 || ^8.0.0", "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0", "symfony/http-kernel": "^6.4.1 || ^7.0.1 || ^8.0.0", diff --git a/src/DependencyInjection/CommandHandlerCompilerPass.php b/src/DependencyInjection/CommandHandlerCompilerPass.php index a078c79b..e335acc6 100644 --- a/src/DependencyInjection/CommandHandlerCompilerPass.php +++ b/src/DependencyInjection/CommandHandlerCompilerPass.php @@ -7,7 +7,6 @@ use Patchlevel\EventSourcing\CommandBus\Handler\CreateAggregateHandler; use Patchlevel\EventSourcing\CommandBus\Handler\UpdateAggregateHandler; use Patchlevel\EventSourcing\CommandBus\HandlerFinder; -use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Repository\RepositoryManager; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -30,10 +29,7 @@ public function process(ContainerBuilder $container): void $container->getParameter('patchlevel_event_sourcing.aggregate_handlers.bus'), ); - /** @var AggregateRootRegistry $aggregateRootRegistry */ - $aggregateRootRegistry = $container->get(AggregateRootRegistry::class); - - foreach ($aggregateRootRegistry->aggregateClasses() as $aggregateName => $aggregateClass) { + foreach ($container->getParameter('event_sourcing.aggregates') as $aggregateName => $aggregateClass) { $parameterResolverId = sprintf('.event_sourcing.handler_parameter_resolver.%s', $aggregateName); foreach (HandlerFinder::findInClass($aggregateClass) as $aggregateHandler) { diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index e16f314e..165dcce6 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -74,9 +74,6 @@ * translators: list * } * }, - * aggregates: list, - * events: list, - * headers: list, * snapshot_stores: array, * migration: array{path: string, namespace: string}, * clock: array{freeze: ?string, service: ?string}, @@ -156,24 +153,6 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() - ->arrayNode('events') - ->beforeNormalization()->castToArray()->end() - ->defaultValue([]) - ->scalarPrototype()->end() - ->end() - - ->arrayNode('aggregates') - ->beforeNormalization()->castToArray()->end() - ->defaultValue([]) - ->scalarPrototype()->end() - ->end() - - ->arrayNode('headers') - ->beforeNormalization()->castToArray()->end() - ->defaultValue([]) - ->scalarPrototype()->end() - ->end() - ->arrayNode('clock') ->addDefaultsIfNotSet() ->children() diff --git a/src/DependencyInjection/HandlerServiceLocatorCompilerPass.php b/src/DependencyInjection/HandlerServiceLocatorCompilerPass.php index 87eb52a6..42a9448c 100644 --- a/src/DependencyInjection/HandlerServiceLocatorCompilerPass.php +++ b/src/DependencyInjection/HandlerServiceLocatorCompilerPass.php @@ -7,7 +7,6 @@ use Patchlevel\EventSourcing\Attribute\Inject; use Patchlevel\EventSourcing\CommandBus\Handler\ServiceNotResolvable; use Patchlevel\EventSourcing\CommandBus\HandlerFinder; -use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcingBundle\CommandBus\SymfonyParameterResolver; use Psr\Container\ContainerInterface; use ReflectionAttribute; @@ -32,10 +31,7 @@ public function process(ContainerBuilder $container): void return; } - /** @var AggregateRootRegistry $aggregateRootRegistry */ - $aggregateRootRegistry = $container->get(AggregateRootRegistry::class); - - foreach ($aggregateRootRegistry->aggregateClasses() as $aggregateName => $aggregateClass) { + foreach ($container->getParameter('event_sourcing.aggregates') as $aggregateName => $aggregateClass) { $parameterResolverId = sprintf('.event_sourcing.handler_parameter_resolver.%s', $aggregateName); $services = []; diff --git a/src/DependencyInjection/PatchlevelEventSourcingExtension.php b/src/DependencyInjection/PatchlevelEventSourcingExtension.php index 22e730b7..60cde81b 100644 --- a/src/DependencyInjection/PatchlevelEventSourcingExtension.php +++ b/src/DependencyInjection/PatchlevelEventSourcingExtension.php @@ -17,6 +17,7 @@ use Doctrine\ORM\Tools\ToolEvents; use Patchlevel\EventSourcing\Attribute\Aggregate; use Patchlevel\EventSourcing\Attribute\Event; +use Patchlevel\EventSourcing\Attribute\Header; use Patchlevel\EventSourcing\Attribute\Processor; use Patchlevel\EventSourcing\Attribute\Projector; use Patchlevel\EventSourcing\Attribute\Subscriber; @@ -61,9 +62,7 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootMetadataAwareMetadataFactory; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootMetadataFactory; use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; -use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegistryFactory; use Patchlevel\EventSourcing\Metadata\Event\AttributeEventMetadataFactory; -use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory; use Patchlevel\EventSourcing\Metadata\Event\EventMetadataFactory; use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; use Patchlevel\EventSourcing\Metadata\Message\AttributeMessageHeaderRegistryFactory; @@ -154,6 +153,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use function class_exists; @@ -165,8 +165,6 @@ final class PatchlevelEventSourcingExtension extends Extension /** @param array $configs */ public function load(array $configs, ContainerBuilder $container): void { - $this->removeNonServices($container); - $configuration = new Configuration(); /** @var Config $config */ @@ -186,7 +184,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->configureConnection($config, $container); $this->configureStore($config, $container); $this->configureSnapshots($config, $container); - $this->configureAggregates($config, $container); + $this->configureAggregates($container); $this->configureCommands($container); $this->configureProfiler($container); $this->configureClock($config, $container); @@ -202,11 +200,17 @@ public function load(array $configs, ContainerBuilder $container): void /** @param Config $config */ private function configureSerializer(array $config, ContainerBuilder $container): void { - $container->register(AttributeEventRegistryFactory::class); + $container->registerAttributeForAutoconfiguration( + Event::class, + static function (ChildDefinition $definition, Event $attribute): void { + $definition->addResourceTag('event_sourcing.event', ['name' => $attribute->name]); + }, + ); + + $container->setParameter('event_sourcing.events', []); $container->register(EventRegistry::class) - ->setFactory([new Reference(AttributeEventRegistryFactory::class), 'create']) - ->setArguments([$config['events']]); + ->setArguments([new Parameter('event_sourcing.events')]); $container->register(AttributeEventMetadataFactory::class); $container->setAlias(EventMetadataFactory::class, AttributeEventMetadataFactory::class); @@ -223,12 +227,15 @@ private function configureSerializer(array $config, ContainerBuilder $container) $container->setAlias(EventSerializer::class, DefaultEventSerializer::class); - $container->register(AttributeMessageHeaderRegistryFactory::class); - $container->setAlias(MessageHeaderRegistryFactory::class, AttributeMessageHeaderRegistryFactory::class); + $container->registerAttributeForAutoconfiguration( + Header::class, + static function (ChildDefinition $definition, Header $attribute): void { + $definition->addResourceTag('event_sourcing.header', ['name' => $attribute->name]); + }, + ); $container->register(MessageHeaderRegistry::class) - ->setFactory([new Reference(MessageHeaderRegistryFactory::class), 'create']) - ->setArguments([$config['headers']]); + ->setArguments([new Parameter('event_sourcing.headers')]); $container->register(DefaultHeadersSerializer::class) ->setArguments([ @@ -884,17 +891,22 @@ private function configureSnapshots(array $config, ContainerBuilder $container): $container->setAlias(SnapshotStore::class, DefaultSnapshotStore::class); } - /** @param Config $config */ - private function configureAggregates(array $config, ContainerBuilder $container): void + private function configureAggregates(ContainerBuilder $container): void { + $container->registerAttributeForAutoconfiguration( + Aggregate::class, + static function (ChildDefinition $definition, Aggregate $attribute): void { + $definition->addResourceTag('event_sourcing.aggregate', ['name' => $attribute->name]); + }, + ); + $container->register(AggregateRootMetadataAwareMetadataFactory::class); $container->setAlias(AggregateRootMetadataFactory::class, AggregateRootMetadataAwareMetadataFactory::class); - $container->register(AttributeAggregateRootRegistryFactory::class); + $container->setParameter('event_sourcing.aggregates', []); $container->register(AggregateRootRegistry::class) - ->setFactory([new Reference(AttributeAggregateRootRegistryFactory::class), 'create']) - ->setArguments([$config['aggregates']]); + ->setArguments([new Parameter('event_sourcing.aggregates')]); $container->register(DefaultRepositoryManager::class) ->setArguments([ @@ -1198,26 +1210,4 @@ private function configureValueResolver(ContainerBuilder $container): void $container->register(IdentifierValueResolver::class) ->addTag('controller.argument_value_resolver', ['priority' => 200]); } - - private function removeNonServices(ContainerBuilder $container): void - { - $container->registerAttributeForAutoconfiguration( - Aggregate::class, - static function (ChildDefinition $definition): void { - $definition->setAbstract(true)->addTag( - 'container.excluded', - ['source' => sprintf('with #[%s] attribute', Aggregate::class)], - ); - }, - ); - $container->registerAttributeForAutoconfiguration( - Event::class, - static function (ChildDefinition $definition): void { - $definition->setAbstract(true)->addTag( - 'container.excluded', - ['source' => sprintf('with #[%s] attribute', Event::class)], - ); - }, - ); - } } diff --git a/src/DependencyInjection/RepositoryCompilerPass.php b/src/DependencyInjection/RepositoryCompilerPass.php index 83960f4d..85f74571 100644 --- a/src/DependencyInjection/RepositoryCompilerPass.php +++ b/src/DependencyInjection/RepositoryCompilerPass.php @@ -4,12 +4,10 @@ namespace Patchlevel\EventSourcingBundle\DependencyInjection; -use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry; use Patchlevel\EventSourcing\Repository\Repository; use Patchlevel\EventSourcing\Repository\RepositoryManager; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; @@ -18,20 +16,14 @@ final class RepositoryCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { - $aggregateRootRegistry = $container->get(AggregateRootRegistry::class, ContainerInterface::NULL_ON_INVALID_REFERENCE); - - if (!$aggregateRootRegistry instanceof AggregateRootRegistry) { - return; - } - - foreach ($aggregateRootRegistry->aggregateNames() as $aggregateName) { + foreach ($container->getParameter('event_sourcing.aggregates') as $aggregateName => $aggregateClass) { $aggregateRepositoryName = $aggregateName . 'Repository'; $aggregateRepositoryId = 'event_sourcing.' . $aggregateName . '.repository'; $definition = new Definition(Repository::class); $definition->setPublic(false); $definition->setFactory([new Reference(RepositoryManager::class), 'get']); - $definition->setArgument(0, $aggregateRootRegistry->aggregateClass($aggregateName)); + $definition->setArgument(0, $aggregateClass); $container->setDefinition($aggregateRepositoryId, $definition); $container->registerAliasForArgument($aggregateRepositoryId, Repository::class, $aggregateRepositoryName)->setPublic(false); diff --git a/src/DependencyInjection/ResourceCompilerPass.php b/src/DependencyInjection/ResourceCompilerPass.php new file mode 100644 index 00000000..a86a44e9 --- /dev/null +++ b/src/DependencyInjection/ResourceCompilerPass.php @@ -0,0 +1,32 @@ + 'event_sourcing.aggregates', + 'event_sourcing.event' => 'event_sourcing.events', + 'event_sourcing.header' => 'event_sourcing.headers', + ]; + + public function process(ContainerBuilder $container): void + { + foreach (self::RESSOURCES as $tag => $parameter) { + $map = []; + foreach ($container->findTaggedResourceIds($tag) as $id => $tags) { + foreach ($tags as $tag) { + $map[$tag['name']] = $id; + } + } + + $container->setParameter($parameter, $map); + } + } +} diff --git a/src/PatchlevelEventSourcingBundle.php b/src/PatchlevelEventSourcingBundle.php index 78a641e5..34a53104 100644 --- a/src/PatchlevelEventSourcingBundle.php +++ b/src/PatchlevelEventSourcingBundle.php @@ -4,6 +4,7 @@ namespace Patchlevel\EventSourcingBundle; +use Patchlevel\EventSourcingBundle\DependencyInjection\ResourceCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\CommandHandlerCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\DoctrineCleanupCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\HandlerServiceLocatorCompilerPass; @@ -19,6 +20,7 @@ final class PatchlevelEventSourcingBundle extends Bundle { public function build(ContainerBuilder $container): void { + $container->addCompilerPass(new ResourceCompilerPass(), priority: 100); $container->addCompilerPass(new RepositoryCompilerPass()); $container->addCompilerPass(new SubscriberGuardCompilePass()); $container->addCompilerPass(new CommandHandlerCompilerPass(), priority: 100); diff --git a/tests/Unit/PatchlevelEventSourcingBundleTest.php b/tests/Unit/PatchlevelEventSourcingBundleTest.php index cb4e2736..a7baaef1 100644 --- a/tests/Unit/PatchlevelEventSourcingBundleTest.php +++ b/tests/Unit/PatchlevelEventSourcingBundleTest.php @@ -14,8 +14,6 @@ use Doctrine\Migrations\Tools\Console\Command\StatusCommand; use Doctrine\Persistence\ManagerRegistry; use InvalidArgumentException; -use Patchlevel\EventSourcing\Attribute\Aggregate; -use Patchlevel\EventSourcing\Attribute\Event; use Patchlevel\EventSourcing\Clock\FrozenClock; use Patchlevel\EventSourcing\Clock\SystemClock; use Patchlevel\EventSourcing\CommandBus\CommandBus; @@ -118,7 +116,6 @@ use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydrator; -use PHPUnit\Framework\Attributes\RequiresMethod; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; use Psr\Clock\ClockInterface; @@ -127,7 +124,6 @@ use Psr\SimpleCache\CacheInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; -use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Dumper\XmlDumper; @@ -135,8 +131,6 @@ use Symfony\Component\HttpKernel\DependencyInjection\ServicesResetter; use Symfony\Component\Messenger\MessageBusInterface; -use function sprintf; - final class PatchlevelEventSourcingBundleTest extends TestCase { public function testEmptyConfig(): void @@ -154,7 +148,6 @@ public function testEmptyConfig(): void self::assertFalse($container->has(Store::class)); } - #[RequiresMethod(ContainerBuilder::class, 'getAttributeAutoconfigurators')] public function testMinimalConfig(): void { $container = new ContainerBuilder(); @@ -177,57 +170,6 @@ public function testMinimalConfig(): void self::assertInstanceOf(AttributeEventTagExtractor::class, $container->get(EventTagExtractor::class)); self::assertFalse($container->has(EventBus::class)); - - $attributes = $container->getAttributeAutoconfigurators(); - foreach ([Aggregate::class, Event::class] as $class) { - $definition = new ChildDefinition(''); - - foreach ($attributes[$class] as $attributeCallable) { - $attributeCallable($definition); - } - - $this->assertSame( - [['source' => sprintf('with #[%s] attribute', $class)]], - $definition->getTag('container.excluded'), - ); - $this->assertTrue($definition->isAbstract()); - } - } - - #[RequiresMethod(ContainerBuilder::class, 'getAutoconfiguredAttributes')] - public function testMinimalConfigPreSymf8(): void - { - $container = new ContainerBuilder(); - $this->compileContainer( - $container, - [ - 'patchlevel_event_sourcing' => [ - 'connection' => ['url' => 'sqlite3:///:memory:'], - ], - ], - ); - - self::assertInstanceOf(Connection::class, $container->get('event_sourcing.dbal_connection')); - self::assertInstanceOf(StreamDoctrineDbalStore::class, $container->get(Store::class)); - self::assertInstanceOf(AggregateRootRegistry::class, $container->get(AggregateRootRegistry::class)); - self::assertInstanceOf(DefaultRepositoryManager::class, $container->get(RepositoryManager::class)); - self::assertInstanceOf(EventRegistry::class, $container->get(EventRegistry::class)); - self::assertInstanceOf(SystemClock::class, $container->get('event_sourcing.clock')); - self::assertInstanceOf(DefaultSubscriptionEngine::class, $container->get(SubscriptionEngine::class)); - - self::assertFalse($container->has(EventBus::class)); - - $attributes = $container->getAutoconfiguredAttributes(); - foreach ([Aggregate::class, Event::class] as $class) { - $definition = new ChildDefinition(''); - $attributes[$class]($definition); - - $this->assertSame( - [['source' => sprintf('with #[%s] attribute', $class)]], - $definition->getTag('container.excluded'), - ); - $this->assertTrue($definition->isAbstract()); - } } public function testConnectionService(): void @@ -539,12 +481,14 @@ public function testCommandHandler(): void { $container = new ContainerBuilder(); + $container->setDefinition(Profile::class, new Definition(Profile::class)) + ->setAutoconfigured(true); + $this->compileContainer( $container, [ 'patchlevel_event_sourcing' => [ 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], - 'aggregates' => [__DIR__ . '/../Fixtures'], 'command_bus' => ['service' => 'command.bus'], ], ], @@ -569,12 +513,14 @@ public function testCommandBus(): void { $container = new ContainerBuilder(); + $container->setDefinition(Profile::class, new Definition(Profile::class)) + ->setAutoconfigured(true); + $this->compileContainer( $container, [ 'patchlevel_event_sourcing' => [ 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], - 'aggregates' => [__DIR__ . '/../Fixtures'], 'command_bus' => ['service' => 'command.bus'], ], ], @@ -608,7 +554,6 @@ public function testQueryBus(): void [ 'patchlevel_event_sourcing' => [ 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], - 'aggregates' => [__DIR__ . '/../Fixtures'], 'query_bus' => ['service' => 'query.bus'], ], ], @@ -874,12 +819,14 @@ public function testEventRegistry(): void { $container = new ContainerBuilder(); + $container->setDefinition(ProfileCreated::class, new Definition(ProfileCreated::class)) + ->setAutoconfigured(true); + $this->compileContainer( $container, [ 'patchlevel_event_sourcing' => [ 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], - 'events' => [__DIR__ . '/../Fixtures'], ], ], ); @@ -894,12 +841,14 @@ public function testAggregateRegistry(): void { $container = new ContainerBuilder(); + $container->setDefinition(Profile::class, new Definition(Profile::class)) + ->setAutoconfigured(true); + $this->compileContainer( $container, [ 'patchlevel_event_sourcing' => [ 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], - 'aggregates' => [__DIR__ . '/../Fixtures'], ], ], ); @@ -914,12 +863,14 @@ public function testMessageHeaderRegistry(): void { $container = new ContainerBuilder(); + $container->setDefinition(CustomHeader::class, new Definition(CustomHeader::class)) + ->setAutoconfigured(true); + $this->compileContainer( $container, [ 'patchlevel_event_sourcing' => [ 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], - 'headers' => [__DIR__ . '/../Fixtures'], ], ], ); @@ -935,12 +886,14 @@ public function testRepositoryManager(): void { $container = new ContainerBuilder(); + $container->setDefinition(Profile::class, new Definition(Profile::class)) + ->setAutoconfigured(true); + $this->compileContainer( $container, [ 'patchlevel_event_sourcing' => [ 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], - 'aggregates' => [__DIR__ . '/../Fixtures'], ], ], ); @@ -1425,6 +1378,9 @@ public function testFullBuild(): void $container = new ContainerBuilder(); $container->set('clock', $psrClock); + $container->setDefinition(Profile::class, new Definition(Profile::class)) + ->setAutoconfigured(true); + $this->compileContainer( $container, [ @@ -1435,7 +1391,6 @@ public function testFullBuild(): void ], 'clock' => ['service' => 'clock'], 'event_bus' => ['type' => 'default'], - 'aggregates' => [__DIR__ . '/../Fixtures'], 'migration' => [ 'namespace' => 'Foo', 'path' => 'src', @@ -1468,12 +1423,14 @@ public function testNamedRepository(): void { $container = new ContainerBuilder(); + $container->setDefinition(Profile::class, new Definition(Profile::class)) + ->setAutoconfigured(true); + $this->compileContainer( $container, [ 'patchlevel_event_sourcing' => [ 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], - 'aggregates' => [__DIR__ . '/../Fixtures'], ], ], ); From a84781d992aab774c23446251e5e70c65c4e3af6 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 3 Oct 2026 11:22:41 +0200 Subject: [PATCH 2/6] Fix static analysis and update docs for resource tags Type the registry parameters in the compiler passes, register the missing default for event_sourcing.headers and resolve the class from the service definition so classes with a custom service id are picked up correctly. The docs no longer mention the removed aggregates, events and headers paths. --- docs/configuration.md | 75 +++++-------------- docs/installation.md | 2 - phpstan-baseline.neon | 18 ----- .../CommandHandlerCompilerPass.php | 5 +- .../HandlerServiceLocatorCompilerPass.php | 5 +- .../PatchlevelEventSourcingExtension.php | 4 +- .../RepositoryCompilerPass.php | 5 +- .../ResourceCompilerPass.php | 16 ++-- src/PatchlevelEventSourcingBundle.php | 2 +- .../PatchlevelEventSourcingBundleTest.php | 22 ++++++ 10 files changed, 66 insertions(+), 88 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 285fc9c1..d51726d1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,72 +10,33 @@ This documentation is limited to bundle integration and configuration. We provide a [default configuration](./installation.md#configuration-file) that should work for most projects. ::: -## Aggregate +## Aggregates, Events and Headers -A path must be specified for Event Sourcing to know where to look for your aggregates. -If you want you can use glob patterns to specify multiple paths. +Aggregates, events and custom headers are discovered via Symfony's service autoconfiguration. +Every class marked with the `#[Aggregate]`, `#[Event]` or `#[Header]` attribute that is covered +by a service resource with `autoconfigure` enabled is registered automatically. +These classes are not registered as services, they are only collected and then excluded from the container. -```yaml -patchlevel_event_sourcing: - aggregates: '%kernel.project_dir%/src/*/Domain' -``` -Or use an array to specify multiple paths. +With the default Symfony configuration in `config/services.yaml` this works out of the box: ```yaml -patchlevel_event_sourcing: - aggregates: - - '%kernel.project_dir%/src/Hotel/Domain' - - '%kernel.project_dir%/src/Room/Domain' -``` -:::note -The library will automatically register all classes marked with the `#[Aggregate]` attribute in the specified paths. -::: +services: + _defaults: + autowire: true + autoconfigure: true -:::tip -If you want to learn more about aggregates, read the [library documentation](https://event-sourcing.patchlevel.io/latest/aggregate/). -::: - -## Events - -A path must be specified for Event Sourcing to know where to look for your events. -If you want you can use glob patterns to specify multiple paths. - -```yaml -patchlevel_event_sourcing: - events: '%kernel.project_dir%/src/*/Domain/Event' + App\: + resource: '../src/' ``` -Or use an array to specify multiple paths. - -```yaml -patchlevel_event_sourcing: - events: - - '%kernel.project_dir%/src/Hotel/Domain/Event' - - '%kernel.project_dir%/src/Room/Domain/Event' -``` -:::tip -If you want to learn more about events, read the [library documentation](https://event-sourcing.patchlevel.io/latest/events/). +:::note +Make sure the directories containing your aggregates, events and headers are not listed in the `exclude` option. ::: -## Custom Headers - -If you want to implement custom headers for your application, you must specify the -paths to look for those headers. -If you want you can use glob patterns to specify multiple paths. - -```yaml -patchlevel_event_sourcing: - headers: '%kernel.project_dir%/src/*/Domain/Header' -``` -Or use an array to specify multiple paths. - -```yaml -patchlevel_event_sourcing: - headers: - - '%kernel.project_dir%/src/Hotel/Domain/Header' - - '%kernel.project_dir%/src/Room/Domain/Header' -``` :::tip -If you want to learn more about custom headers, read the [library documentation](https://event-sourcing.patchlevel.io/latest/message/#custom-headers). +If you want to learn more about [aggregates](https://event-sourcing.patchlevel.io/latest/aggregate/), +[events](https://event-sourcing.patchlevel.io/latest/events/) +or [custom headers](https://event-sourcing.patchlevel.io/latest/message/#custom-headers), +read the library documentation. ::: ## Connection diff --git a/docs/installation.md b/docs/installation.md index bef677db..da4c21d4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -32,8 +32,6 @@ Now you have to add following recommended configuration file here `config/packag ```yaml patchlevel_event_sourcing: - aggregates: '%kernel.project_dir%/src' - events: '%kernel.project_dir%/src' connection: url: '%env(EVENTSTORE_URL)%' provide_dedicated_connection: true diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9eb870ca..55599374 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -54,18 +54,6 @@ parameters: count: 1 path: tests/Fixtures/SnapshotableProfile.php - - - message: '#^Call to an undefined method Symfony\\Component\\DependencyInjection\\ContainerBuilder\:\:getAutoconfiguredAttributes\(\)\.$#' - identifier: method.notFound - count: 1 - path: tests/Unit/PatchlevelEventSourcingBundleTest.php - - - - message: '#^Cannot access offset ''Patchlevel\\\\EventSourcing\\\\Attribute\\\\Aggregate''\|''Patchlevel\\\\EventSourcing\\\\Attribute\\\\Event'' on mixed\.$#' - identifier: offsetAccess.nonOffsetAccessible - count: 1 - path: tests/Unit/PatchlevelEventSourcingBundleTest.php - - message: '#^Cannot access offset ''bus'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -83,9 +71,3 @@ parameters: identifier: offsetAccess.nonOffsetAccessible count: 2 path: tests/Unit/PatchlevelEventSourcingBundleTest.php - - - - message: '#^Trying to invoke mixed but it''s not a callable\.$#' - identifier: callable.nonCallable - count: 1 - path: tests/Unit/PatchlevelEventSourcingBundleTest.php diff --git a/src/DependencyInjection/CommandHandlerCompilerPass.php b/src/DependencyInjection/CommandHandlerCompilerPass.php index e335acc6..81b85dbd 100644 --- a/src/DependencyInjection/CommandHandlerCompilerPass.php +++ b/src/DependencyInjection/CommandHandlerCompilerPass.php @@ -29,7 +29,10 @@ public function process(ContainerBuilder $container): void $container->getParameter('patchlevel_event_sourcing.aggregate_handlers.bus'), ); - foreach ($container->getParameter('event_sourcing.aggregates') as $aggregateName => $aggregateClass) { + /** @var array $aggregates */ + $aggregates = $container->getParameter('event_sourcing.aggregates'); + + foreach ($aggregates as $aggregateName => $aggregateClass) { $parameterResolverId = sprintf('.event_sourcing.handler_parameter_resolver.%s', $aggregateName); foreach (HandlerFinder::findInClass($aggregateClass) as $aggregateHandler) { diff --git a/src/DependencyInjection/HandlerServiceLocatorCompilerPass.php b/src/DependencyInjection/HandlerServiceLocatorCompilerPass.php index 42a9448c..e7f9d903 100644 --- a/src/DependencyInjection/HandlerServiceLocatorCompilerPass.php +++ b/src/DependencyInjection/HandlerServiceLocatorCompilerPass.php @@ -31,7 +31,10 @@ public function process(ContainerBuilder $container): void return; } - foreach ($container->getParameter('event_sourcing.aggregates') as $aggregateName => $aggregateClass) { + /** @var array $aggregates */ + $aggregates = $container->getParameter('event_sourcing.aggregates'); + + foreach ($aggregates as $aggregateName => $aggregateClass) { $parameterResolverId = sprintf('.event_sourcing.handler_parameter_resolver.%s', $aggregateName); $services = []; diff --git a/src/DependencyInjection/PatchlevelEventSourcingExtension.php b/src/DependencyInjection/PatchlevelEventSourcingExtension.php index 60cde81b..4aa1b523 100644 --- a/src/DependencyInjection/PatchlevelEventSourcingExtension.php +++ b/src/DependencyInjection/PatchlevelEventSourcingExtension.php @@ -65,9 +65,7 @@ use Patchlevel\EventSourcing\Metadata\Event\AttributeEventMetadataFactory; use Patchlevel\EventSourcing\Metadata\Event\EventMetadataFactory; use Patchlevel\EventSourcing\Metadata\Event\EventRegistry; -use Patchlevel\EventSourcing\Metadata\Message\AttributeMessageHeaderRegistryFactory; use Patchlevel\EventSourcing\Metadata\Message\MessageHeaderRegistry; -use Patchlevel\EventSourcing\Metadata\Message\MessageHeaderRegistryFactory; use Patchlevel\EventSourcing\Metadata\Subscriber\AttributeSubscriberMetadataFactory; use Patchlevel\EventSourcing\Metadata\Subscriber\SubscriberMetadataFactory; use Patchlevel\EventSourcing\QueryBus\QueryBus; @@ -234,6 +232,8 @@ static function (ChildDefinition $definition, Header $attribute): void { }, ); + $container->setParameter('event_sourcing.headers', []); + $container->register(MessageHeaderRegistry::class) ->setArguments([new Parameter('event_sourcing.headers')]); diff --git a/src/DependencyInjection/RepositoryCompilerPass.php b/src/DependencyInjection/RepositoryCompilerPass.php index 85f74571..8233fe2c 100644 --- a/src/DependencyInjection/RepositoryCompilerPass.php +++ b/src/DependencyInjection/RepositoryCompilerPass.php @@ -16,7 +16,10 @@ final class RepositoryCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { - foreach ($container->getParameter('event_sourcing.aggregates') as $aggregateName => $aggregateClass) { + /** @var array $aggregates */ + $aggregates = $container->getParameter('event_sourcing.aggregates'); + + foreach ($aggregates as $aggregateName => $aggregateClass) { $aggregateRepositoryName = $aggregateName . 'Repository'; $aggregateRepositoryId = 'event_sourcing.' . $aggregateName . '.repository'; diff --git a/src/DependencyInjection/ResourceCompilerPass.php b/src/DependencyInjection/ResourceCompilerPass.php index a86a44e9..cece61e6 100644 --- a/src/DependencyInjection/ResourceCompilerPass.php +++ b/src/DependencyInjection/ResourceCompilerPass.php @@ -10,7 +10,7 @@ /** @internal */ final class ResourceCompilerPass implements CompilerPassInterface { - private const RESSOURCES = [ + private const RESOURCES = [ 'event_sourcing.aggregate' => 'event_sourcing.aggregates', 'event_sourcing.event' => 'event_sourcing.events', 'event_sourcing.header' => 'event_sourcing.headers', @@ -18,11 +18,17 @@ final class ResourceCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container): void { - foreach (self::RESSOURCES as $tag => $parameter) { + foreach (self::RESOURCES as $tag => $parameter) { $map = []; - foreach ($container->findTaggedResourceIds($tag) as $id => $tags) { - foreach ($tags as $tag) { - $map[$tag['name']] = $id; + + /** @var array> $taggedResources */ + $taggedResources = $container->findTaggedResourceIds($tag); + + foreach ($taggedResources as $id => $attributes) { + $class = $container->getDefinition($id)->getClass() ?? $id; + + foreach ($attributes as $attribute) { + $map[$attribute['name']] = $class; } } diff --git a/src/PatchlevelEventSourcingBundle.php b/src/PatchlevelEventSourcingBundle.php index 34a53104..b4e74a89 100644 --- a/src/PatchlevelEventSourcingBundle.php +++ b/src/PatchlevelEventSourcingBundle.php @@ -4,13 +4,13 @@ namespace Patchlevel\EventSourcingBundle; -use Patchlevel\EventSourcingBundle\DependencyInjection\ResourceCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\CommandHandlerCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\DoctrineCleanupCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\HandlerServiceLocatorCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\HydratorCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\QueryHandlerCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\RepositoryCompilerPass; +use Patchlevel\EventSourcingBundle\DependencyInjection\ResourceCompilerPass; use Patchlevel\EventSourcingBundle\DependencyInjection\SubscriberGuardCompilePass; use Patchlevel\EventSourcingBundle\DependencyInjection\TranslatorCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; diff --git a/tests/Unit/PatchlevelEventSourcingBundleTest.php b/tests/Unit/PatchlevelEventSourcingBundleTest.php index a7baaef1..0c98d108 100644 --- a/tests/Unit/PatchlevelEventSourcingBundleTest.php +++ b/tests/Unit/PatchlevelEventSourcingBundleTest.php @@ -859,6 +859,28 @@ public function testAggregateRegistry(): void self::assertTrue($aggregateRegistry->hasAggregateClass(Profile::class)); } + public function testAggregateRegistryWithCustomServiceId(): void + { + $container = new ContainerBuilder(); + + $container->setDefinition('app.profile', new Definition(Profile::class)) + ->setAutoconfigured(true); + + $this->compileContainer( + $container, + [ + 'patchlevel_event_sourcing' => [ + 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], + ], + ], + ); + + $aggregateRegistry = $container->get(AggregateRootRegistry::class); + + self::assertInstanceOf(AggregateRootRegistry::class, $aggregateRegistry); + self::assertTrue($aggregateRegistry->hasAggregateClass(Profile::class)); + } + public function testMessageHeaderRegistry(): void { $container = new ContainerBuilder(); From ae7d05c6885f2678d110b410b3a759048d4b7f4b Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 3 Oct 2026 11:29:35 +0200 Subject: [PATCH 3/6] Drop redundant registry parameter defaults --- composer.lock | 2 +- .../PatchlevelEventSourcingExtension.php | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/composer.lock b/composer.lock index eeaeeaba..cbc9cb10 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8e556d60159c0d8a9127c205fd6e149", + "content-hash": "7fd7c37a601fda2218c1e5dd044dbcb1", "packages": [ { "name": "brick/math", diff --git a/src/DependencyInjection/PatchlevelEventSourcingExtension.php b/src/DependencyInjection/PatchlevelEventSourcingExtension.php index 4aa1b523..5e7c7a81 100644 --- a/src/DependencyInjection/PatchlevelEventSourcingExtension.php +++ b/src/DependencyInjection/PatchlevelEventSourcingExtension.php @@ -205,8 +205,6 @@ static function (ChildDefinition $definition, Event $attribute): void { }, ); - $container->setParameter('event_sourcing.events', []); - $container->register(EventRegistry::class) ->setArguments([new Parameter('event_sourcing.events')]); @@ -232,8 +230,6 @@ static function (ChildDefinition $definition, Header $attribute): void { }, ); - $container->setParameter('event_sourcing.headers', []); - $container->register(MessageHeaderRegistry::class) ->setArguments([new Parameter('event_sourcing.headers')]); @@ -903,8 +899,6 @@ static function (ChildDefinition $definition, Aggregate $attribute): void { $container->register(AggregateRootMetadataAwareMetadataFactory::class); $container->setAlias(AggregateRootMetadataFactory::class, AggregateRootMetadataAwareMetadataFactory::class); - $container->setParameter('event_sourcing.aggregates', []); - $container->register(AggregateRootRegistry::class) ->setArguments([new Parameter('event_sourcing.aggregates')]); From bb7fd78d2855a856a8a87f57b23622a17f13f625 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 3 Oct 2026 12:00:04 +0200 Subject: [PATCH 4/6] Handle event aliases, internal headers and duplicates in resource pass Event aliases were dropped, the header registry lost the internal headers and duplicate names silently overwrote each other. This restores the behaviour of the previous attribute registry factories. --- .../PatchlevelEventSourcingExtension.php | 6 +- .../ResourceCompilerPass.php | 66 ++++++++++++++---- tests/Fixtures/NoAggregate.php | 12 ++++ tests/Fixtures/ProfileCreated.php | 2 +- .../PatchlevelEventSourcingBundleTest.php | 68 +++++++++++++++++++ 5 files changed, 137 insertions(+), 17 deletions(-) create mode 100644 tests/Fixtures/NoAggregate.php diff --git a/src/DependencyInjection/PatchlevelEventSourcingExtension.php b/src/DependencyInjection/PatchlevelEventSourcingExtension.php index 5e7c7a81..c8963bc1 100644 --- a/src/DependencyInjection/PatchlevelEventSourcingExtension.php +++ b/src/DependencyInjection/PatchlevelEventSourcingExtension.php @@ -201,7 +201,10 @@ private function configureSerializer(array $config, ContainerBuilder $container) $container->registerAttributeForAutoconfiguration( Event::class, static function (ChildDefinition $definition, Event $attribute): void { - $definition->addResourceTag('event_sourcing.event', ['name' => $attribute->name]); + $definition->addResourceTag('event_sourcing.event', [ + 'name' => $attribute->name, + 'aliases' => $attribute->aliases, + ]); }, ); @@ -231,6 +234,7 @@ static function (ChildDefinition $definition, Header $attribute): void { ); $container->register(MessageHeaderRegistry::class) + ->setFactory([MessageHeaderRegistry::class, 'createWithInternalHeaders']) ->setArguments([new Parameter('event_sourcing.headers')]); $container->register(DefaultHeadersSerializer::class) diff --git a/src/DependencyInjection/ResourceCompilerPass.php b/src/DependencyInjection/ResourceCompilerPass.php index cece61e6..8ec39d09 100644 --- a/src/DependencyInjection/ResourceCompilerPass.php +++ b/src/DependencyInjection/ResourceCompilerPass.php @@ -4,35 +4,71 @@ namespace Patchlevel\EventSourcingBundle\DependencyInjection; +use Patchlevel\EventSourcing\Aggregate\AggregateRoot; +use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootAlreadyInRegistry; +use Patchlevel\EventSourcing\Metadata\AggregateRoot\NoAggregateRoot; +use Patchlevel\EventSourcing\Metadata\Event\EventAlreadyInRegistry; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use function array_key_exists; +use function is_subclass_of; + /** @internal */ final class ResourceCompilerPass implements CompilerPassInterface { - private const RESOURCES = [ - 'event_sourcing.aggregate' => 'event_sourcing.aggregates', - 'event_sourcing.event' => 'event_sourcing.events', - 'event_sourcing.header' => 'event_sourcing.headers', - ]; - public function process(ContainerBuilder $container): void { - foreach (self::RESOURCES as $tag => $parameter) { - $map = []; + $aggregates = []; - /** @var array> $taggedResources */ - $taggedResources = $container->findTaggedResourceIds($tag); + foreach ($this->taggedClasses($container, 'event_sourcing.aggregate') as [$class, $attribute]) { + if (!is_subclass_of($class, AggregateRoot::class)) { + throw new NoAggregateRoot($class); + } - foreach ($taggedResources as $id => $attributes) { - $class = $container->getDefinition($id)->getClass() ?? $id; + if (array_key_exists($attribute['name'], $aggregates)) { + throw new AggregateRootAlreadyInRegistry($attribute['name']); + } - foreach ($attributes as $attribute) { - $map[$attribute['name']] = $class; + $aggregates[$attribute['name']] = $class; + } + + $events = []; + + foreach ($this->taggedClasses($container, 'event_sourcing.event') as [$class, $attribute]) { + foreach ([$attribute['name'], ...$attribute['aliases'] ?? []] as $name) { + if (array_key_exists($name, $events)) { + throw new EventAlreadyInRegistry($name); } + + $events[$name] = $class; } + } - $container->setParameter($parameter, $map); + $headers = []; + + foreach ($this->taggedClasses($container, 'event_sourcing.header') as [$class, $attribute]) { + $headers[$attribute['name']] = $class; + } + + $container->setParameter('event_sourcing.aggregates', $aggregates); + $container->setParameter('event_sourcing.events', $events); + $container->setParameter('event_sourcing.headers', $headers); + } + + /** @return iterable}}> */ + private function taggedClasses(ContainerBuilder $container, string $tag): iterable + { + /** @var array}>> $taggedResources */ + $taggedResources = $container->findTaggedResourceIds($tag); + + foreach ($taggedResources as $id => $attributes) { + /** @var class-string $class */ + $class = $container->getDefinition($id)->getClass() ?? $id; + + foreach ($attributes as $attribute) { + yield [$class, $attribute]; + } } } } diff --git a/tests/Fixtures/NoAggregate.php b/tests/Fixtures/NoAggregate.php new file mode 100644 index 00000000..1f501790 --- /dev/null +++ b/tests/Fixtures/NoAggregate.php @@ -0,0 +1,12 @@ +hasEventClass(ProfileCreated::class)); + self::assertSame('profile.created', $eventRegistry->eventName(ProfileCreated::class)); + self::assertSame(ProfileCreated::class, $eventRegistry->eventClass('profile.registered')); + } + + public function testDuplicateEventName(): void + { + $container = new ContainerBuilder(); + + $container->setDefinition('event.a', new Definition(ProfileCreated::class)) + ->setAutoconfigured(true); + $container->setDefinition('event.b', new Definition(ProfileCreated::class)) + ->setAutoconfigured(true); + + $this->expectException(EventAlreadyInRegistry::class); + + $this->compileContainer( + $container, + [ + 'patchlevel_event_sourcing' => [ + 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], + ], + ], + ); } public function testAggregateRegistry(): void @@ -859,6 +886,46 @@ public function testAggregateRegistry(): void self::assertTrue($aggregateRegistry->hasAggregateClass(Profile::class)); } + public function testDuplicateAggregateName(): void + { + $container = new ContainerBuilder(); + + $container->setDefinition('aggregate.a', new Definition(Profile::class)) + ->setAutoconfigured(true); + $container->setDefinition('aggregate.b', new Definition(Profile::class)) + ->setAutoconfigured(true); + + $this->expectException(AggregateRootAlreadyInRegistry::class); + + $this->compileContainer( + $container, + [ + 'patchlevel_event_sourcing' => [ + 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], + ], + ], + ); + } + + public function testAggregateWithoutAggregateRoot(): void + { + $container = new ContainerBuilder(); + + $container->setDefinition(NoAggregate::class, new Definition(NoAggregate::class)) + ->setAutoconfigured(true); + + $this->expectException(NoAggregateRoot::class); + + $this->compileContainer( + $container, + [ + 'patchlevel_event_sourcing' => [ + 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], + ], + ], + ); + } + public function testAggregateRegistryWithCustomServiceId(): void { $container = new ContainerBuilder(); @@ -902,6 +969,7 @@ public function testMessageHeaderRegistry(): void self::assertInstanceOf(MessageHeaderRegistry::class, $messageHeaderRegistry); self::assertTrue($messageHeaderRegistry->hasHeaderClass(CustomHeader::class)); + self::assertTrue($messageHeaderRegistry->hasHeaderName('playhead')); } public function testRepositoryManager(): void From aee1edf4fc86f89321357e933c0426840ffdb6fa Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 3 Oct 2026 12:43:27 +0200 Subject: [PATCH 5/6] Support header aliases Header aliases are now available in event-sourcing 4.0.x. They are registered after the internal headers and throw if an alias is already in use, same as the attribute registry factory does. --- composer.lock | 8 ++-- .../PatchlevelEventSourcingExtension.php | 6 ++- .../ResourceCompilerPass.php | 21 +++++++++ tests/Fixtures/CustomHeader.php | 2 +- tests/Fixtures/PlayheadAliasHeader.php | 12 +++++ .../PatchlevelEventSourcingBundleTest.php | 44 +++++++++++++++++++ 6 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 tests/Fixtures/PlayheadAliasHeader.php diff --git a/composer.lock b/composer.lock index cbc9cb10..7f643b83 100644 --- a/composer.lock +++ b/composer.lock @@ -420,12 +420,12 @@ "source": { "type": "git", "url": "https://github.com/patchlevel/event-sourcing.git", - "reference": "ade8292ca34e1f8c398e31e4a43c01973548f757" + "reference": "51c07650652266b11c641a3a25d9955757bd3b31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/patchlevel/event-sourcing/zipball/ade8292ca34e1f8c398e31e4a43c01973548f757", - "reference": "ade8292ca34e1f8c398e31e4a43c01973548f757", + "url": "https://api.github.com/repos/patchlevel/event-sourcing/zipball/51c07650652266b11c641a3a25d9955757bd3b31", + "reference": "51c07650652266b11c641a3a25d9955757bd3b31", "shasum": "" }, "require": { @@ -507,7 +507,7 @@ "issues": "https://github.com/patchlevel/event-sourcing/issues", "source": "https://github.com/patchlevel/event-sourcing/tree/4.0.x" }, - "time": "2026-09-24T06:22:35+00:00" + "time": "2026-10-03T10:26:29+00:00" }, { "name": "patchlevel/hydrator", diff --git a/src/DependencyInjection/PatchlevelEventSourcingExtension.php b/src/DependencyInjection/PatchlevelEventSourcingExtension.php index c8963bc1..765a9e01 100644 --- a/src/DependencyInjection/PatchlevelEventSourcingExtension.php +++ b/src/DependencyInjection/PatchlevelEventSourcingExtension.php @@ -229,12 +229,14 @@ static function (ChildDefinition $definition, Event $attribute): void { $container->registerAttributeForAutoconfiguration( Header::class, static function (ChildDefinition $definition, Header $attribute): void { - $definition->addResourceTag('event_sourcing.header', ['name' => $attribute->name]); + $definition->addResourceTag('event_sourcing.header', [ + 'name' => $attribute->name, + 'aliases' => $attribute->aliases, + ]); }, ); $container->register(MessageHeaderRegistry::class) - ->setFactory([MessageHeaderRegistry::class, 'createWithInternalHeaders']) ->setArguments([new Parameter('event_sourcing.headers')]); $container->register(DefaultHeadersSerializer::class) diff --git a/src/DependencyInjection/ResourceCompilerPass.php b/src/DependencyInjection/ResourceCompilerPass.php index 8ec39d09..288ae95d 100644 --- a/src/DependencyInjection/ResourceCompilerPass.php +++ b/src/DependencyInjection/ResourceCompilerPass.php @@ -8,6 +8,8 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootAlreadyInRegistry; use Patchlevel\EventSourcing\Metadata\AggregateRoot\NoAggregateRoot; use Patchlevel\EventSourcing\Metadata\Event\EventAlreadyInRegistry; +use Patchlevel\EventSourcing\Metadata\Message\HeaderAlreadyInRegistry; +use Patchlevel\EventSourcing\Metadata\Message\MessageHeaderRegistry; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -46,9 +48,28 @@ public function process(ContainerBuilder $container): void } $headers = []; + $headerAliases = []; foreach ($this->taggedClasses($container, 'event_sourcing.header') as [$class, $attribute]) { $headers[$attribute['name']] = $class; + + foreach ($attribute['aliases'] ?? [] as $alias) { + if (array_key_exists($alias, $headerAliases)) { + throw new HeaderAlreadyInRegistry($alias); + } + + $headerAliases[$alias] = $class; + } + } + + $headers = MessageHeaderRegistry::createWithInternalHeaders($headers)->headerClasses(); + + foreach ($headerAliases as $alias => $class) { + if (array_key_exists($alias, $headers)) { + throw new HeaderAlreadyInRegistry($alias); + } + + $headers[$alias] = $class; } $container->setParameter('event_sourcing.aggregates', $aggregates); diff --git a/tests/Fixtures/CustomHeader.php b/tests/Fixtures/CustomHeader.php index 1c1dfe97..b74ac392 100644 --- a/tests/Fixtures/CustomHeader.php +++ b/tests/Fixtures/CustomHeader.php @@ -6,7 +6,7 @@ use Patchlevel\EventSourcing\Attribute\Header; -#[Header('custom')] +#[Header('custom', aliases: ['legacyCustom'])] class CustomHeader { public function __construct( diff --git a/tests/Fixtures/PlayheadAliasHeader.php b/tests/Fixtures/PlayheadAliasHeader.php new file mode 100644 index 00000000..54cad216 --- /dev/null +++ b/tests/Fixtures/PlayheadAliasHeader.php @@ -0,0 +1,12 @@ +hasHeaderClass(CustomHeader::class)); self::assertTrue($messageHeaderRegistry->hasHeaderName('playhead')); + self::assertSame('custom', $messageHeaderRegistry->headerName(CustomHeader::class)); + self::assertSame(CustomHeader::class, $messageHeaderRegistry->headerClass('legacyCustom')); + } + + public function testDuplicateHeaderAlias(): void + { + $container = new ContainerBuilder(); + + $container->setDefinition('header.a', new Definition(CustomHeader::class)) + ->setAutoconfigured(true); + $container->setDefinition('header.b', new Definition(CustomHeader::class)) + ->setAutoconfigured(true); + + $this->expectException(HeaderAlreadyInRegistry::class); + + $this->compileContainer( + $container, + [ + 'patchlevel_event_sourcing' => [ + 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], + ], + ], + ); + } + + public function testHeaderAliasCollidesWithInternalHeader(): void + { + $container = new ContainerBuilder(); + + $container->setDefinition(PlayheadAliasHeader::class, new Definition(PlayheadAliasHeader::class)) + ->setAutoconfigured(true); + + $this->expectException(HeaderAlreadyInRegistry::class); + + $this->compileContainer( + $container, + [ + 'patchlevel_event_sourcing' => [ + 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], + ], + ], + ); } public function testRepositoryManager(): void From e3fd26743c3a24bd035bb845a52c9ee5830ed714 Mon Sep 17 00:00:00 2001 From: David Badura Date: Sat, 3 Oct 2026 19:04:41 +0200 Subject: [PATCH 6/6] Drop Symfony 6.4 support for all components --- composer.json | 24 ++++++++++++------------ composer.lock | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index bdd76d5e..e58a4ddd 100644 --- a/composer.json +++ b/composer.json @@ -21,14 +21,14 @@ "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", "patchlevel/event-sourcing": "4.0.x-dev", "patchlevel/hydrator": "^2.0.2", - "symfony/cache": "^6.4.0 || ^7.0.0 || ^8.0.0", - "symfony/config": "^6.4.0 || ^7.0.0 || ^8.0.0", - "symfony/console": "^6.4.1 || ^7.0.1 || ^8.0.0", + "symfony/cache": "^7.3.0 || ^8.0.0", + "symfony/config": "^7.3.0 || ^8.0.0", + "symfony/console": "^7.3.0 || ^8.0.0", "symfony/dependency-injection": "^7.3.0 || ^8.0.0", - "symfony/event-dispatcher": "^6.4.1 || ^7.0.1 || ^8.0.0", - "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0", - "symfony/http-kernel": "^6.4.1 || ^7.0.1 || ^8.0.0", - "symfony/messenger": "^6.4.0 || ^7.0.0 || ^8.0.0", + "symfony/event-dispatcher": "^7.3.0 || ^8.0.0", + "symfony/finder": "^7.3.0 || ^8.0.0", + "symfony/http-kernel": "^7.3.0 || ^8.0.0", + "symfony/messenger": "^7.3.0 || ^8.0.0", "symfony/service-contracts": "^2.5 || ^3.0" }, "require-dev": { @@ -42,10 +42,10 @@ "phpstan/phpstan-symfony": "^2.0.8", "phpunit/phpunit": "^11.5.45", "roave/security-advisories": "dev-master", - "symfony/clock": "^6.4.0 || ^7.0.0 || ^8.0.0", - "symfony/uid": "^6.4.0 || ^7.0.0 || ^8.0.0", - "symfony/var-dumper": "^6.4.0 || ^7.0.0 || ^8.0.0", - "symfony/web-profiler-bundle": "^6.4.0 || ^7.0.0 || ^8.0.0", + "symfony/clock": "^7.3.0 || ^8.0.0", + "symfony/uid": "^7.3.0 || ^8.0.0", + "symfony/var-dumper": "^7.3.0 || ^8.0.0", + "symfony/web-profiler-bundle": "^7.3.0 || ^8.0.0", "wnx/commonmark-markdown-renderer": "^1.4" }, "suggest": { @@ -55,7 +55,7 @@ }, "conflict": { "doctrine/doctrine-bundle": "<2.8", - "symfony/event-dispatcher": "6.4.36 || 7.4.8 || 8.0.8" + "symfony/event-dispatcher": "7.4.8 || 8.0.8" }, "config": { "preferred-install": { diff --git a/composer.lock b/composer.lock index 7f643b83..3de774f9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7fd7c37a601fda2218c1e5dd044dbcb1", + "content-hash": "6e0134267927896dc554ae6c3f44dcd2", "packages": [ { "name": "brick/math",