diff --git a/composer.json b/composer.json index b5e5cb9a..fa6d9f81 100644 --- a/composer.json +++ b/composer.json @@ -19,8 +19,8 @@ ], "require": { "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", - "patchlevel/event-sourcing": "^3.18.0", - "patchlevel/hydrator": "^1.18.0", + "patchlevel/event-sourcing": "^3.19.1", + "patchlevel/hydrator": "^1.21.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", diff --git a/composer.lock b/composer.lock index 624aa0f2..7081495b 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": "fd9dadf670426ef444497d6f5e2d4959", + "content-hash": "6fa36ab818510e4f0c226a416f580b48", "packages": [ { "name": "brick/math", @@ -416,20 +416,20 @@ }, { "name": "patchlevel/event-sourcing", - "version": "3.18.0", + "version": "3.19.1", "source": { "type": "git", "url": "https://github.com/patchlevel/event-sourcing.git", - "reference": "d17688b827baaa87626fb5fc3f9d58faa47a13d2" + "reference": "bb9a5a7e8b9abcc6d5d527471d6d3b9bf125dc67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/patchlevel/event-sourcing/zipball/d17688b827baaa87626fb5fc3f9d58faa47a13d2", - "reference": "d17688b827baaa87626fb5fc3f9d58faa47a13d2", + "url": "https://api.github.com/repos/patchlevel/event-sourcing/zipball/bb9a5a7e8b9abcc6d5d527471d6d3b9bf125dc67", + "reference": "bb9a5a7e8b9abcc6d5d527471d6d3b9bf125dc67", "shasum": "" }, "require": { - "doctrine/dbal": "^4.0.0", + "doctrine/dbal": "^4.4.0", "doctrine/migrations": "^3.3.2", "patchlevel/hydrator": "^1.8.0", "patchlevel/worker": "^1.4.0", @@ -486,27 +486,39 @@ "description": "A lightweight but also all-inclusive event sourcing library with a focus on developer experience", "homepage": "https://event-sourcing.patchlevel.io", "keywords": [ + "Domain Driven Design", + "aggregates", + "cqrs", + "dcb", "ddd", - "event-sourcing" + "dynamic consistency boundary", + "event driven", + "event-sourcing", + "events", + "message driven", + "messages", + "patchlevel", + "processor", + "projection" ], "support": { "issues": "https://github.com/patchlevel/event-sourcing/issues", - "source": "https://github.com/patchlevel/event-sourcing/tree/3.18.0" + "source": "https://github.com/patchlevel/event-sourcing/tree/3.19.1" }, - "time": "2026-02-23T09:53:37+00:00" + "time": "2026-04-06T08:13:41+00:00" }, { "name": "patchlevel/hydrator", - "version": "1.19.0", + "version": "1.22.0", "source": { "type": "git", "url": "https://github.com/patchlevel/hydrator.git", - "reference": "5b79db6c2089cd3ddb5f08666179d85bda3160d7" + "reference": "92de0c4b4a3a80a5df8e0c8f066c613777e5510e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/patchlevel/hydrator/zipball/5b79db6c2089cd3ddb5f08666179d85bda3160d7", - "reference": "5b79db6c2089cd3ddb5f08666179d85bda3160d7", + "url": "https://api.github.com/repos/patchlevel/hydrator/zipball/92de0c4b4a3a80a5df8e0c8f066c613777e5510e", + "reference": "92de0c4b4a3a80a5df8e0c8f066c613777e5510e", "shasum": "" }, "require": { @@ -555,9 +567,9 @@ ], "support": { "issues": "https://github.com/patchlevel/hydrator/issues", - "source": "https://github.com/patchlevel/hydrator/tree/1.19.0" + "source": "https://github.com/patchlevel/hydrator/tree/1.22.0" }, - "time": "2026-03-29T12:04:47+00:00" + "time": "2026-04-06T09:17:40+00:00" }, { "name": "patchlevel/worker", @@ -6041,12 +6053,12 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "61130731cdf896e60028ed82193a1bc3c50d032a" + "reference": "db78064456eb735e368677828095fb7fe5aeda6f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/61130731cdf896e60028ed82193a1bc3c50d032a", - "reference": "61130731cdf896e60028ed82193a1bc3c50d032a", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/db78064456eb735e368677828095fb7fe5aeda6f", + "reference": "db78064456eb735e368677828095fb7fe5aeda6f", "shasum": "" }, "conflict": { @@ -6097,9 +6109,9 @@ "aureuserp/aureuserp": "<1.3.0.0-beta1", "austintoddj/canvas": "<=3.4.2", "auth0/auth0-php": ">=3.3,<=8.18", - "auth0/login": "<7.20", - "auth0/symfony": "<=5.5", - "auth0/wordpress": "<=5.4", + "auth0/login": "<=7.20", + "auth0/symfony": "<=5.7", + "auth0/wordpress": "<=5.5", "automad/automad": "<2.0.0.0-alpha5", "automattic/jetpack": "<9.8", "avideo/avideo": "<=26", @@ -6467,7 +6479,7 @@ "knplabs/knp-snappy": "<=1.4.2", "kohana/core": "<3.3.3", "koillection/koillection": "<1.6.12", - "krayin/laravel-crm": "<=1.3", + "krayin/laravel-crm": "<=2.2", "kreait/firebase-php": ">=3.2,<3.8.1", "kumbiaphp/kumbiapp": "<=1.1.1", "la-haute-societe/tcpdf": "<6.2.22", @@ -6730,7 +6742,7 @@ "roadiz/documents": "<2.3.42|>=2.4,<2.5.44|>=2.6,<2.6.28|>=2.7,<2.7.9", "robrichards/xmlseclibs": "<3.1.5", "roots/soil": "<4.1", - "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11", + "roundcube/roundcubemail": "<1.5.10|>=1.6,<1.6.11|>=1.7.0.0-beta,<1.7.0.0-RC5-dev", "rudloff/alltube": "<3.0.3", "rudloff/rtmpdump-bin": "<=2.3.1", "s-cart/core": "<=9.0.5", @@ -7082,7 +7094,7 @@ "type": "tidelift" } ], - "time": "2026-04-02T00:32:12+00:00" + "time": "2026-04-04T07:24:55+00:00" }, { "name": "sanmai/di-container", diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index b6fb37a4..9e727fb6 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -5,6 +5,7 @@ namespace Patchlevel\EventSourcingBundle\DependencyInjection; use Patchlevel\EventSourcing\Repository\AggregateOutdated; +use Patchlevel\Hydrator\Extension\Cryptography\Cipher\OpensslCipherKeyFactory; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Throwable; @@ -87,6 +88,17 @@ * }, * clock: array{freeze: ?string, service: ?string}, * aggregate_handlers: array{enabled: bool, bus: string|null}, + * hydrator: array{ + * enabled: bool, + * default_lazy: bool, + * cryptography: array{ + * enabled: bool, + * algorithm: string, + * }, + * lifecycle: array{ + * enabled: bool, + * }, + * }, * } */ final class Configuration implements ConfigurationInterface @@ -361,6 +373,25 @@ public function getConfigTreeBuilder(): TreeBuilder ) ->end() + ->arrayNode('hydrator') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('default_lazy')->defaultFalse()->end() + ->arrayNode('cryptography') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('algorithm')->defaultValue(OpensslCipherKeyFactory::DEFAULT_METHOD)->end() + ->end() + ->end() + ->arrayNode('lifecycle') + ->canBeEnabled() + ->addDefaultsIfNotSet() + ->end() + ->end() + ->end() + ->end(); // @codingStandardsIgnoreEnd diff --git a/src/DependencyInjection/HydratorCompilerPass.php b/src/DependencyInjection/HydratorCompilerPass.php new file mode 100644 index 00000000..9fcd52d0 --- /dev/null +++ b/src/DependencyInjection/HydratorCompilerPass.php @@ -0,0 +1,30 @@ +has(StackHydratorBuilder::class)) { + return; + } + + $builder = $container->getDefinition(StackHydratorBuilder::class); + $extensions = $container->findTaggedServiceIds('event_sourcing.hydrator.extension'); + + foreach (array_keys($extensions) as $subscriberServiceName) { + $builder->addMethodCall('useExtension', [new Reference($subscriberServiceName)]); + } + } +} diff --git a/src/DependencyInjection/PatchlevelEventSourcingExtension.php b/src/DependencyInjection/PatchlevelEventSourcingExtension.php index bf88497e..c98a1c38 100644 --- a/src/DependencyInjection/PatchlevelEventSourcingExtension.php +++ b/src/DependencyInjection/PatchlevelEventSourcingExtension.php @@ -45,6 +45,7 @@ use Patchlevel\EventSourcing\Console\Command\WatchCommand; use Patchlevel\EventSourcing\Console\DoctrineHelper; use Patchlevel\EventSourcing\Cryptography\DoctrineCipherKeyStore; +use Patchlevel\EventSourcing\Cryptography\ExtensionDoctrineCipherKeyStore; use Patchlevel\EventSourcing\EventBus\AttributeListenerProvider; use Patchlevel\EventSourcing\EventBus\Consumer; use Patchlevel\EventSourcing\EventBus\DefaultConsumer; @@ -126,6 +127,7 @@ use Patchlevel\EventSourcingBundle\DataCollector\MessageCollectorEventBus; use Patchlevel\EventSourcingBundle\Doctrine\DbalConnectionFactory; use Patchlevel\EventSourcingBundle\EventBus\SymfonyEventBus; +use Patchlevel\EventSourcingBundle\Normalizer\SymfonyExtension; use Patchlevel\EventSourcingBundle\Normalizer\SymfonyGuesser; use Patchlevel\EventSourcingBundle\QueryBus\SymfonyQueryBus; use Patchlevel\EventSourcingBundle\RequestListener\AutoSetupListener; @@ -134,6 +136,7 @@ use Patchlevel\EventSourcingBundle\Subscription\ResetServicesListener; use Patchlevel\EventSourcingBundle\Subscription\StaticInMemorySubscriptionStoreFactory; use Patchlevel\EventSourcingBundle\ValueResolver\AggregateRootIdValueResolver; +use Patchlevel\Hydrator\CoreExtension; use Patchlevel\Hydrator\Cryptography\Cipher\Cipher; use Patchlevel\Hydrator\Cryptography\Cipher\CipherKeyFactory; use Patchlevel\Hydrator\Cryptography\Cipher\OpensslCipher; @@ -141,6 +144,11 @@ use Patchlevel\Hydrator\Cryptography\PayloadCryptographer; use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer; use Patchlevel\Hydrator\Cryptography\Store\CipherKeyStore; +use Patchlevel\Hydrator\Extension as HydratorExtension; +use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer; +use Patchlevel\Hydrator\Extension\Cryptography\Cryptographer; +use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension; +use Patchlevel\Hydrator\Extension\Lifecycle\LifecycleExtension; use Patchlevel\Hydrator\Guesser\BuiltInGuesser; use Patchlevel\Hydrator\Guesser\ChainGuesser; use Patchlevel\Hydrator\Guesser\Guesser; @@ -148,6 +156,8 @@ use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; use Patchlevel\Hydrator\Metadata\MetadataFactory; use Patchlevel\Hydrator\MetadataHydrator; +use Patchlevel\Hydrator\StackHydrator; +use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Worker\Event\WorkerRunningEvent; use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -177,7 +187,7 @@ public function load(array $configs, ContainerBuilder $container): void return; } - $this->configureHydrator($container); + $this->configureHydrator($config, $container); $this->configureUpcaster($container); $this->configureSerializer($config, $container); $this->configureMessageDecorator($container); @@ -622,38 +632,93 @@ static function (ChildDefinition $definition): void { ]); } - private function configureHydrator(ContainerBuilder $container): void + /** @param Config $config */ + private function configureHydrator(array $config, ContainerBuilder $container): void { - $container->register(ChainGuesser::class) - ->setArguments([new TaggedIteratorArgument('event_sourcing.hydrator.guesser')]); + if (!$config['hydrator']['enabled']) { // legacy MetadataHydrator + $container->register(ChainGuesser::class) + ->setArguments([new TaggedIteratorArgument('event_sourcing.hydrator.guesser')]); - $container->register(BuiltInGuesser::class) - ->addTag('event_sourcing.hydrator.guesser', ['priority' => -100]); + $container->register(BuiltInGuesser::class) + ->addTag('event_sourcing.hydrator.guesser', ['priority' => -64]); - $container->register(SymfonyGuesser::class) - ->addTag('event_sourcing.hydrator.guesser', ['priority' => -50]); + $container->register(SymfonyGuesser::class) + ->addTag('event_sourcing.hydrator.guesser', ['priority' => -32]); - $container->registerForAutoconfiguration(Guesser::class) - ->addTag('event_sourcing.hydrator.guesser'); + $container->registerForAutoconfiguration(Guesser::class) + ->addTag('event_sourcing.hydrator.guesser'); - $container->register(AttributeMetadataFactory::class) - ->setArguments([ - null, - new Reference(ChainGuesser::class), - ]); + $container->register(AttributeMetadataFactory::class) + ->setArguments([ + null, + new Reference(ChainGuesser::class), + ]); - $container->setAlias(MetadataFactory::class, AttributeMetadataFactory::class); + $container->setAlias(MetadataFactory::class, AttributeMetadataFactory::class); - $container->register(MetadataHydrator::class) - ->setArguments([ - new Reference(MetadataFactory::class), - new Reference( - PayloadCryptographer::class, - ContainerInterface::IGNORE_ON_INVALID_REFERENCE, - ), - ]); + $container->register(MetadataHydrator::class) + ->setArguments([ + new Reference(MetadataFactory::class), + new Reference( + PayloadCryptographer::class, + ContainerInterface::IGNORE_ON_INVALID_REFERENCE, + ), + ]); + + $container->setAlias(Hydrator::class, MetadataHydrator::class); + + return; + } + + $container->registerForAutoconfiguration(HydratorExtension::class) + ->addTag('event_sourcing.hydrator.extension'); + + $container->register(CoreExtension::class) + ->addTag('event_sourcing.hydrator.extension'); + + $container->register(SymfonyExtension::class) + ->addTag('event_sourcing.hydrator.extension'); + + if ($config['hydrator']['cryptography']['enabled']) { + $container->register(ExtensionDoctrineCipherKeyStore::class) + ->setArguments([new Reference('event_sourcing.dbal_connection')]) + ->addTag('event_sourcing.doctrine_schema_configurator'); + + $container->setAlias( + \Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyStore::class, + ExtensionDoctrineCipherKeyStore::class, + ); + + $container->register(BaseCryptographer::class) + ->setFactory([BaseCryptographer::class, 'createWithOpenssl']) + ->setArguments([ + new Reference(\Patchlevel\Hydrator\Extension\Cryptography\Store\CipherKeyStore::class), + $config['hydrator']['cryptography']['algorithm'], + ]); + + $container->setAlias(Cryptographer::class, BaseCryptographer::class); + + $container->register(CryptographyExtension::class) + ->setArguments([ + new Reference(Cryptographer::class), + new Reference(PayloadCryptographer::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE), + true, + ]) + ->addTag('event_sourcing.hydrator.extension'); + } + + if ($config['hydrator']['lifecycle']['enabled']) { + $container->register(LifecycleExtension::class) + ->addTag('event_sourcing.hydrator.extension'); + } + + $builder = $container->register(StackHydratorBuilder::class); + $builder->addMethodCall('enableDefaultLazy', [$config['hydrator']['default_lazy']]); + + $container->register(StackHydrator::class) + ->setFactory([new Reference(StackHydratorBuilder::class), 'build']); - $container->setAlias(Hydrator::class, MetadataHydrator::class); + $container->setAlias(Hydrator::class, StackHydrator::class); } private function configureUpcaster(ContainerBuilder $container): void diff --git a/src/Normalizer/SymfonyExtension.php b/src/Normalizer/SymfonyExtension.php new file mode 100644 index 00000000..a6faf8bd --- /dev/null +++ b/src/Normalizer/SymfonyExtension.php @@ -0,0 +1,16 @@ +addGuesser(new SymfonyGuesser(), -32); + } +} diff --git a/src/PatchlevelEventSourcingBundle.php b/src/PatchlevelEventSourcingBundle.php index 40d129c9..78a641e5 100644 --- a/src/PatchlevelEventSourcingBundle.php +++ b/src/PatchlevelEventSourcingBundle.php @@ -7,6 +7,7 @@ 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\SubscriberGuardCompilePass; @@ -25,5 +26,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new HandlerServiceLocatorCompilerPass(), priority: -100); $container->addCompilerPass(new TranslatorCompilerPass()); $container->addCompilerPass(new DoctrineCleanupCompilerPass()); + $container->addCompilerPass(new HydratorCompilerPass()); } } diff --git a/tests/Fixtures/DummyExtension.php b/tests/Fixtures/DummyExtension.php new file mode 100644 index 00000000..89bd3420 --- /dev/null +++ b/tests/Fixtures/DummyExtension.php @@ -0,0 +1,16 @@ +has('event_sourcing.command.migration_diff')); } - public function testHydrator(): void + public function testLegacyHydrator(): void { $container = new ContainerBuilder(); @@ -1415,10 +1421,10 @@ public function testHydrator(): void self::assertEquals( [ BuiltInGuesser::class => [ - ['priority' => -100], + ['priority' => -64], ], SymfonyGuesser::class => [ - ['priority' => -50], + ['priority' => -32], ], DummyGuesser::class => [ [], @@ -1428,6 +1434,51 @@ public function testHydrator(): void ); } + public function testHydrator(): void + { + $container = new ContainerBuilder(); + + $container->setDefinition(DummyExtension::class, new Definition(DummyExtension::class)) + ->setAutoconfigured(true); + + $this->compileContainer( + $container, + [ + 'patchlevel_event_sourcing' => [ + 'connection' => ['service' => 'doctrine.dbal.eventstore_connection'], + 'hydrator' => [ + 'enabled' => true, + 'lifecycle' => ['enabled' => true], + 'cryptography' => ['enabled' => true], + ], + ], + ], + ); + + self::assertInstanceOf(StackHydrator::class, $container->get(Hydrator::class)); + + self::assertEquals( + [ + CoreExtension::class => [ + [], + ], + SymfonyExtension::class => [ + [], + ], + DummyExtension::class => [ + [], + ], + LifecycleExtension::class => [ + [], + ], + CryptographyExtension::class => [ + [], + ], + ], + $container->findTaggedServiceIds('event_sourcing.hydrator.extension'), + ); + } + public function testCryptography(): void { $container = new ContainerBuilder();