From eb671031496595547e42d25c567839ccf6fe4438 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 09:48:57 +0200 Subject: [PATCH 01/17] feat: add Symfony-based container implementation to main package --- .../src/SymfonyContainer/EcotoneContainer.php | 40 +++++ .../EcotoneSymfonyContainerFactory.php | 43 +++++ .../ExternalReferenceResolver.php | 32 ++++ .../SymfonyContainerImplementation.php | 159 ++++++++++++++++++ .../SymfonyContainerImplementationTest.php | 37 ++++ 5 files changed, 311 insertions(+) create mode 100644 packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php create mode 100644 packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php create mode 100644 packages/Ecotone/src/SymfonyContainer/ExternalReferenceResolver.php create mode 100644 packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php create mode 100644 packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php new file mode 100644 index 000000000..db565e440 --- /dev/null +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php @@ -0,0 +1,40 @@ +container->has($id)) { + return $this->container->get($id); + } + + return ExternalReferenceResolver::resolve($this->externalContainer, $id, ContainerImplementation::EXCEPTION_ON_INVALID_REFERENCE); + } + + public function has(string $id): bool + { + return $this->container->has($id) || $this->externalContainer->has($id); + } + + public function set(string $id, mixed $service): void + { + $this->container->set($id, $service); + } +} diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php new file mode 100644 index 000000000..7dc3975bf --- /dev/null +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -0,0 +1,43 @@ +addCompilerPass(new SymfonyContainerImplementation($symfonyBuilder)); + $builder->compile(); + $symfonyBuilder->compile(); + + return self::wrapWithExternalFallback($symfonyBuilder, $externalContainer); + } + + private static function wrapWithExternalFallback( + SymfonyContainerInterface $symfonyContainer, + ?ContainerInterface $externalContainer, + ): EcotoneContainer { + $externalContainer ??= InMemoryPSRContainer::createEmpty(); + $container = new EcotoneContainer($symfonyContainer, $externalContainer); + $container->set(SymfonyContainerImplementation::EXTERNAL_CONTAINER_ID, $externalContainer); + $container->set(ContainerInterface::class, $container); + + return $container; + } +} diff --git a/packages/Ecotone/src/SymfonyContainer/ExternalReferenceResolver.php b/packages/Ecotone/src/SymfonyContainer/ExternalReferenceResolver.php new file mode 100644 index 000000000..eacf5abd0 --- /dev/null +++ b/packages/Ecotone/src/SymfonyContainer/ExternalReferenceResolver.php @@ -0,0 +1,32 @@ +has($id)) { + return $externalContainer->get($id); + } + if ($externalContainer->has(self::TESTING_ALIAS_PREFIX . $id)) { + return $externalContainer->get(self::TESTING_ALIAS_PREFIX . $id); + } + if ($invalidBehavior === ContainerImplementation::NULL_ON_INVALID_REFERENCE) { + return null; + } + + throw new InvalidArgumentException("Reference {$id} was not found in definitions"); + } +} diff --git a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php new file mode 100644 index 000000000..7ad3fdaca --- /dev/null +++ b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php @@ -0,0 +1,159 @@ +symfonyBuilder->setDefinition( + self::EXTERNAL_CONTAINER_ID, + (new SymfonyDefinition(ContainerInterface::class))->setSynthetic(true)->setPublic(true) + ); + $this->symfonyBuilder->setDefinition( + ContainerInterface::class, + (new SymfonyDefinition(ContainerInterface::class))->setSynthetic(true)->setPublic(true) + ); + + $this->definitions = $builder->getDefinitions(); + foreach ($this->definitions as $id => $definition) { + $symfonyDefinition = $this->resolveArgument($definition); + if ($symfonyDefinition instanceof SymfonyReference) { + $this->symfonyBuilder->setAlias($id, (string) $symfonyDefinition)->setPublic(true); + } else { + $this->symfonyBuilder->setDefinition($id, $symfonyDefinition); + } + } + $this->symfonyBuilder->setParameter(self::EXTERNAL_REFERENCES_PARAMETER, array_values($this->externalReferences)); + } + + private function resolveArgument($argument): mixed + { + if ($argument instanceof DefinedObject) { + $argument = $argument->getDefinition(); + } + if ($argument instanceof AttributeDefinition) { + $argument = DefinitionHelper::resolvePotentialComplexAttribute($argument); + } + if ($argument instanceof Definition) { + return $this->convertDefinition($argument); + } elseif (is_array($argument)) { + $resolvedArguments = []; + foreach ($argument as $index => $value) { + $resolvedArguments[$index] = $this->resolveArgument($value); + } + return $resolvedArguments; + } elseif ($argument instanceof Reference) { + return $this->resolveReference($argument); + } else { + return $argument; + } + } + + private function resolveReference(Reference $reference): SymfonyReference + { + $id = $reference->getId(); + if (isset($this->definitions[$id]) || $id === ContainerInterface::class) { + return new SymfonyReference($id); + } + + return new SymfonyReference($this->registerExternalReferenceDelegate($id, $reference->getInvalidBehavior())); + } + + private function registerExternalReferenceDelegate(string $id, int $invalidBehavior): string + { + $delegateId = $invalidBehavior === self::NULL_ON_INVALID_REFERENCE + ? $id . '.ecotone.nullable' + : $id; + + if (! $this->symfonyBuilder->hasDefinition($delegateId)) { + $this->symfonyBuilder->setDefinition( + $delegateId, + (new SymfonyDefinition(stdClass::class)) + ->setFactory([ExternalReferenceResolver::class, 'resolve']) + ->setArguments([new SymfonyReference(self::EXTERNAL_CONTAINER_ID), $id, $invalidBehavior]) + ->setShared(false) + ->setPublic(true) + ); + } + $this->externalReferences[$id] = $id; + + return $delegateId; + } + + private function convertDefinition(Definition $ecotoneDefinition): SymfonyDefinition + { + $sfDefinition = new SymfonyDefinition( + $ecotoneDefinition->getClassName(), + $this->normalizeNamedArgument($this->resolveArgument($ecotoneDefinition->getArguments())) + ); + if ($ecotoneDefinition->hasFactory()) { + $sfDefinition->setFactory($this->resolveFactoryArgument($ecotoneDefinition->getFactory())); + } + foreach ($ecotoneDefinition->getMethodCalls() as $methodCall) { + $sfDefinition->addMethodCall( + $methodCall->getMethodName(), + $this->normalizeNamedArgument($this->resolveArgument($methodCall->getArguments())) + ); + } + return $sfDefinition->setPublic(true); + } + + private function normalizeNamedArgument(array $arguments): array + { + foreach ($arguments as $index => $argument) { + if (is_string($index)) { + $arguments['$' . $index] = $argument; + unset($arguments[$index]); + } + } + return $arguments; + } + + private function resolveFactoryArgument(array $factory): array + { + if (method_exists($factory[0], $factory[1]) && (new ReflectionMethod($factory[0], $factory[1]))->isStatic()) { + return $factory; + } + + return [$this->resolveReference(new Reference($factory[0])), $factory[1]]; + } +} diff --git a/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php new file mode 100644 index 000000000..09484fda4 --- /dev/null +++ b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php @@ -0,0 +1,37 @@ + $logger, + ]); + $container = self::buildContainerFromDefinitions(['aReference' => new Reference('externallyDefined')], $externalContainer); + + self::assertSame($logger, $container->get('aReference')); + } +} From cadba1f86212d330c94e91a793788f77cced4f39 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 09:51:35 +0200 Subject: [PATCH 02/17] feat: add dumped container cache and symfony/dependency-injection requirement --- packages/Ecotone/composer.json | 1 + .../EcotoneSymfonyContainerFactory.php | 49 ++++++++++++ ...cotoneSymfonyContainerFactoryCacheTest.php | 76 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php diff --git a/packages/Ecotone/composer.json b/packages/Ecotone/composer.json index 13ecbb428..d8b891206 100644 --- a/packages/Ecotone/composer.json +++ b/packages/Ecotone/composer.json @@ -57,6 +57,7 @@ "psr/clock": "^1.0", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^2.0|^3.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/uid": "^6.4|^7.0|^8.0" }, "require-dev": { diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php index 7dc3975bf..b1286248b 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -5,11 +5,13 @@ namespace Ecotone\SymfonyContainer; use Ecotone\Lite\InMemoryPSRContainer; +use Ecotone\Messaging\Config\ConfigurationException; use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; +use Symfony\Component\DependencyInjection\Dumper\PhpDumper; /** * licence Apache-2.0 @@ -26,9 +28,56 @@ public static function build( $builder->compile(); $symfonyBuilder->compile(); + if ($serviceCacheConfiguration->shouldUseCache()) { + self::dumpToCache($symfonyBuilder, $serviceCacheConfiguration); + return self::loadCached($serviceCacheConfiguration, $externalContainer) + ?? throw ConfigurationException::create("Failed to load dumped Ecotone container from {$serviceCacheConfiguration->getPath()}"); + } + return self::wrapWithExternalFallback($symfonyBuilder, $externalContainer); } + public static function loadCached( + ServiceCacheConfiguration $serviceCacheConfiguration, + ?ContainerInterface $externalContainer = null, + ): ?EcotoneContainer { + $containerFile = self::containerFilePath($serviceCacheConfiguration); + $className = self::containerClassName($serviceCacheConfiguration); + if (! class_exists($className, false)) { + if (! file_exists($containerFile)) { + return null; + } + require_once $containerFile; + } + + return self::wrapWithExternalFallback(new $className(), $externalContainer); + } + + private static function dumpToCache( + SymfonyContainerBuilder $symfonyBuilder, + ServiceCacheConfiguration $serviceCacheConfiguration, + ): void { + $cacheDirectory = $serviceCacheConfiguration->getPath(); + if (! is_dir($cacheDirectory)) { + mkdir($cacheDirectory, 0777, true); + } + $dumper = new PhpDumper($symfonyBuilder); + file_put_contents( + self::containerFilePath($serviceCacheConfiguration), + $dumper->dump(['class' => self::containerClassName($serviceCacheConfiguration)]), + ); + } + + private static function containerFilePath(ServiceCacheConfiguration $serviceCacheConfiguration): string + { + return $serviceCacheConfiguration->getPath() . DIRECTORY_SEPARATOR . 'ecotone_container.php'; + } + + private static function containerClassName(ServiceCacheConfiguration $serviceCacheConfiguration): string + { + return 'EcotoneCachedContainer_' . md5($serviceCacheConfiguration->getPath()); + } + private static function wrapWithExternalFallback( SymfonyContainerInterface $symfonyContainer, ?ContainerInterface $externalContainer, diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php new file mode 100644 index 000000000..fbdad2ebb --- /dev/null +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php @@ -0,0 +1,76 @@ +uniqueCacheDirectory(), true); + $builder = new ContainerBuilder(); + $builder->replace('aService', new Definition(ACachedService::class, ['someName'])); + EcotoneSymfonyContainerFactory::build($builder, $cacheConfiguration); + + $loaded = EcotoneSymfonyContainerFactory::loadCached($cacheConfiguration); + + self::assertNotNull($loaded); + self::assertEquals(new ACachedService('someName'), $loaded->get('aService')); + } + + public function test_it_returns_null_when_no_dumped_container_exists(): void + { + $cacheConfiguration = new ServiceCacheConfiguration($this->uniqueCacheDirectory(), true); + + self::assertNull(EcotoneSymfonyContainerFactory::loadCached($cacheConfiguration)); + } + + public function test_it_resolves_external_references_in_cache_loaded_container(): void + { + $cacheConfiguration = new ServiceCacheConfiguration($this->uniqueCacheDirectory(), true); + $builder = new ContainerBuilder(); + $builder->replace('aService', new Definition(ACachedService::class, ['someName', new Reference('externallyDefined')])); + EcotoneSymfonyContainerFactory::build( + $builder, + $cacheConfiguration, + InMemoryPSRContainer::createFromAssociativeArray(['externallyDefined' => StubLogger::create()]), + ); + + $logger = StubLogger::create(); + $loaded = EcotoneSymfonyContainerFactory::loadCached( + $cacheConfiguration, + InMemoryPSRContainer::createFromAssociativeArray(['externallyDefined' => $logger]), + ); + + self::assertSame($logger, $loaded->get('aService')->dependency); + } + + private function uniqueCacheDirectory(): string + { + return sys_get_temp_dir() . '/ecotone_container_cache_test/' . uniqid('', true); + } +} + +/** + * licence Apache-2.0 + */ +class ACachedService +{ + public function __construct(public string $name, public mixed $dependency = null) + { + } +} From 0a5e55152615d073df9ff5c5c1c9d75f7d3cd92a Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 10:19:48 +0200 Subject: [PATCH 03/17] feat: switch EcotoneLite to Symfony-based container with dumped cache --- packages/Ecotone/src/Lite/EcotoneLite.php | 35 +++++----- .../src/SymfonyContainer/EcotoneContainer.php | 9 +-- .../EcotoneSymfonyContainerFactory.php | 29 ++++++-- .../RuntimeInstanceProvider.php | 16 +++++ .../SymfonyContainer/ServiceIdNormalizer.php | 18 +++++ .../SymfonyContainerImplementation.php | 68 ++++++++++++++----- .../EcotoneLiteCachedContainerTest.php | 62 +++++++++++++++++ .../SymfonyContainerImplementationTest.php | 62 +++++++++++++++++ 8 files changed, 257 insertions(+), 42 deletions(-) create mode 100644 packages/Ecotone/src/SymfonyContainer/RuntimeInstanceProvider.php create mode 100644 packages/Ecotone/src/SymfonyContainer/ServiceIdNormalizer.php create mode 100644 packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php diff --git a/packages/Ecotone/src/Lite/EcotoneLite.php b/packages/Ecotone/src/Lite/EcotoneLite.php index 7bdd90211..cdd827eac 100644 --- a/packages/Ecotone/src/Lite/EcotoneLite.php +++ b/packages/Ecotone/src/Lite/EcotoneLite.php @@ -15,7 +15,9 @@ use Ecotone\Lite\Test\TestConfiguration; use Ecotone\Messaging\Channel\MessageChannelBuilder; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; -use Ecotone\Messaging\Config\Container\ContainerConfig; +use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; +use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; +use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceCacheConfiguration; @@ -24,6 +26,7 @@ use Ecotone\Messaging\InMemoryConfigurationVariableService; use Ecotone\Messaging\Support\Assert; use Ecotone\Modelling\BaseEventSourcingConfiguration; +use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use function json_decode; @@ -218,15 +221,17 @@ private static function prepareConfiguration(ContainerInterface|array $container ); $configurationVariableService = InMemoryConfigurationVariableService::create($configurationVariables); - $definitionHolder = null; - $messagingSystemCachePath = $serviceCacheConfiguration->getPath() . DIRECTORY_SEPARATOR . 'messaging'; - - if ($serviceCacheConfiguration->shouldUseCache() && file_exists($messagingSystemCachePath)) { - /** It may fail on deserialization, then return `false` and we can build new one */ - $definitionHolder = unserialize(file_get_contents($messagingSystemCachePath)); + $runtimeServices = [ + ServiceCacheConfiguration::REFERENCE_NAME => $serviceCacheConfiguration, + ConfigurationVariableService::REFERENCE_NAME => $configurationVariableService, + ]; + $container = null; + + if ($serviceCacheConfiguration->shouldUseCache()) { + $container = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); } - if (! $definitionHolder) { + if (! $container) { $messagingConfiguration = MessagingSystemConfiguration::prepareWithAnnotationFinder( $annotationFinder, $configurationVariableService, @@ -236,19 +241,17 @@ private static function prepareConfiguration(ContainerInterface|array $container $messagingConfiguration->withExternalContainer($externalContainer); - $definitionHolder = ContainerConfig::buildDefinitionHolder($messagingConfiguration); + $ecotoneBuilder = new ContainerBuilder(); + $ecotoneBuilder->addCompilerPass($messagingConfiguration); + $ecotoneBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); + $ecotoneBuilder->addCompilerPass(new ValidityCheckPass()); if ($serviceCacheConfiguration->shouldUseCache()) { - Assert::notNull($messagingSystemCachePath, 'Cache path should be defined'); - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - file_put_contents($messagingSystemCachePath, serialize($definitionHolder)); } - } - $container = new LazyInMemoryContainer($definitionHolder->getDefinitions(), $externalContainer); - $container->set(ServiceCacheConfiguration::class, $serviceCacheConfiguration); - $container->set(ConfigurationVariableService::REFERENCE_NAME, $configurationVariableService); + $container = EcotoneSymfonyContainerFactory::build($ecotoneBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices); + } $messagingSystem = $container->get(ConfiguredMessagingSystem::class); diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php index db565e440..dfaef929a 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php @@ -21,8 +21,9 @@ public function __construct( public function get(string $id): mixed { - if ($this->container->has($id)) { - return $this->container->get($id); + $normalizedId = ServiceIdNormalizer::normalize($id); + if ($this->container->has($normalizedId)) { + return $this->container->get($normalizedId); } return ExternalReferenceResolver::resolve($this->externalContainer, $id, ContainerImplementation::EXCEPTION_ON_INVALID_REFERENCE); @@ -30,11 +31,11 @@ public function get(string $id): mixed public function has(string $id): bool { - return $this->container->has($id) || $this->externalContainer->has($id); + return $this->container->has(ServiceIdNormalizer::normalize($id)) || $this->externalContainer->has($id); } public function set(string $id, mixed $service): void { - $this->container->set($id, $service); + $this->container->set(ServiceIdNormalizer::normalize($id), $service); } } diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php index b1286248b..795f22a08 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -18,28 +18,40 @@ */ final class EcotoneSymfonyContainerFactory { + /** + * @param array $runtimeServices + */ public static function build( ContainerBuilder $builder, ServiceCacheConfiguration $serviceCacheConfiguration, ?ContainerInterface $externalContainer = null, + array $runtimeServices = [], ): EcotoneContainer { $symfonyBuilder = new SymfonyContainerBuilder(); - $builder->addCompilerPass(new SymfonyContainerImplementation($symfonyBuilder)); + $builder->addCompilerPass(new SymfonyContainerImplementation( + $symfonyBuilder, + array_keys($runtimeServices), + preserveRuntimeInstances: ! $serviceCacheConfiguration->shouldUseCache(), + )); $builder->compile(); - $symfonyBuilder->compile(); if ($serviceCacheConfiguration->shouldUseCache()) { + $symfonyBuilder->compile(); self::dumpToCache($symfonyBuilder, $serviceCacheConfiguration); - return self::loadCached($serviceCacheConfiguration, $externalContainer) + return self::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices) ?? throw ConfigurationException::create("Failed to load dumped Ecotone container from {$serviceCacheConfiguration->getPath()}"); } - return self::wrapWithExternalFallback($symfonyBuilder, $externalContainer); + return self::wrapWithExternalFallback($symfonyBuilder, $externalContainer, $runtimeServices); } + /** + * @param array $runtimeServices + */ public static function loadCached( ServiceCacheConfiguration $serviceCacheConfiguration, ?ContainerInterface $externalContainer = null, + array $runtimeServices = [], ): ?EcotoneContainer { $containerFile = self::containerFilePath($serviceCacheConfiguration); $className = self::containerClassName($serviceCacheConfiguration); @@ -50,7 +62,7 @@ public static function loadCached( require_once $containerFile; } - return self::wrapWithExternalFallback(new $className(), $externalContainer); + return self::wrapWithExternalFallback(new $className(), $externalContainer, $runtimeServices); } private static function dumpToCache( @@ -78,14 +90,21 @@ private static function containerClassName(ServiceCacheConfiguration $serviceCac return 'EcotoneCachedContainer_' . md5($serviceCacheConfiguration->getPath()); } + /** + * @param array $runtimeServices + */ private static function wrapWithExternalFallback( SymfonyContainerInterface $symfonyContainer, ?ContainerInterface $externalContainer, + array $runtimeServices = [], ): EcotoneContainer { $externalContainer ??= InMemoryPSRContainer::createEmpty(); $container = new EcotoneContainer($symfonyContainer, $externalContainer); $container->set(SymfonyContainerImplementation::EXTERNAL_CONTAINER_ID, $externalContainer); $container->set(ContainerInterface::class, $container); + foreach ($runtimeServices as $id => $service) { + $container->set($id, $service); + } return $container; } diff --git a/packages/Ecotone/src/SymfonyContainer/RuntimeInstanceProvider.php b/packages/Ecotone/src/SymfonyContainer/RuntimeInstanceProvider.php new file mode 100644 index 000000000..2f38df5bc --- /dev/null +++ b/packages/Ecotone/src/SymfonyContainer/RuntimeInstanceProvider.php @@ -0,0 +1,16 @@ +symfonyBuilder->setDefinition( - self::EXTERNAL_CONTAINER_ID, - (new SymfonyDefinition(ContainerInterface::class))->setSynthetic(true)->setPublic(true) - ); - $this->symfonyBuilder->setDefinition( - ContainerInterface::class, - (new SymfonyDefinition(ContainerInterface::class))->setSynthetic(true)->setPublic(true) - ); + $this->registerSyntheticService(self::EXTERNAL_CONTAINER_ID, ContainerInterface::class); + $this->registerSyntheticService(ContainerInterface::class, ContainerInterface::class); + foreach ($this->syntheticServiceIds as $syntheticServiceId) { + $this->registerSyntheticService(ServiceIdNormalizer::normalize($syntheticServiceId), stdClass::class); + } $this->definitions = $builder->getDefinitions(); foreach ($this->definitions as $id => $definition) { $symfonyDefinition = $this->resolveArgument($definition); if ($symfonyDefinition instanceof SymfonyReference) { - $this->symfonyBuilder->setAlias($id, (string) $symfonyDefinition)->setPublic(true); + $this->symfonyBuilder->setAlias(ServiceIdNormalizer::normalize($id), (string) $symfonyDefinition)->setPublic(true); } else { - $this->symfonyBuilder->setDefinition($id, $symfonyDefinition); + $this->symfonyBuilder->setDefinition(ServiceIdNormalizer::normalize($id), $symfonyDefinition); } } $this->symfonyBuilder->setParameter(self::EXTERNAL_REFERENCES_PARAMETER, array_values($this->externalReferences)); } + private function registerSyntheticService(string $id, string $className): void + { + $this->symfonyBuilder->setDefinition( + $id, + (new SymfonyDefinition($className))->setSynthetic(true)->setPublic(true) + ); + } + private function resolveArgument($argument): mixed { + if ($argument instanceof DefinedObjectWrapper && $this->preserveRuntimeInstances) { + return $this->convertRuntimeInstanceDefinition($argument); + } if ($argument instanceof DefinedObject) { $argument = $argument->getDefinition(); } @@ -91,8 +110,8 @@ private function resolveArgument($argument): mixed private function resolveReference(Reference $reference): SymfonyReference { $id = $reference->getId(); - if (isset($this->definitions[$id]) || $id === ContainerInterface::class) { - return new SymfonyReference($id); + if (isset($this->definitions[$id]) || $id === ContainerInterface::class || in_array($id, $this->syntheticServiceIds, true)) { + return new SymfonyReference(ServiceIdNormalizer::normalize($id)); } return new SymfonyReference($this->registerExternalReferenceDelegate($id, $reference->getInvalidBehavior())); @@ -100,9 +119,9 @@ private function resolveReference(Reference $reference): SymfonyReference private function registerExternalReferenceDelegate(string $id, int $invalidBehavior): string { - $delegateId = $invalidBehavior === self::NULL_ON_INVALID_REFERENCE - ? $id . '.ecotone.nullable' - : $id; + $delegateId = ServiceIdNormalizer::normalize($id) . ($invalidBehavior === self::NULL_ON_INVALID_REFERENCE + ? self::NULLABLE_EXTERNAL_DELEGATE_SUFFIX + : self::EXTERNAL_DELEGATE_SUFFIX); if (! $this->symfonyBuilder->hasDefinition($delegateId)) { $this->symfonyBuilder->setDefinition( @@ -119,6 +138,21 @@ private function registerExternalReferenceDelegate(string $id, int $invalidBehav return $delegateId; } + private function convertRuntimeInstanceDefinition(DefinedObjectWrapper $definedObjectWrapper): SymfonyDefinition + { + $instance = $definedObjectWrapper->instance(); + $sfDefinition = (new SymfonyDefinition(get_class($instance))) + ->setFactory([RuntimeInstanceProvider::class, 'provide']) + ->setArguments([$instance]); + foreach ($definedObjectWrapper->getMethodCalls() as $methodCall) { + $sfDefinition->addMethodCall( + $methodCall->getMethodName(), + $this->normalizeNamedArgument($this->resolveArgument($methodCall->getArguments())) + ); + } + return $sfDefinition->setPublic(true); + } + private function convertDefinition(Definition $ecotoneDefinition): SymfonyDefinition { $sfDefinition = new SymfonyDefinition( diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php new file mode 100644 index 000000000..ffb671428 --- /dev/null +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php @@ -0,0 +1,62 @@ +withCacheDirectoryPath($cacheDirectory) + ->withSkippedModulePackageNames(ModulePackageList::allPackages()); + $handler = new CachedCommandHandlerService(); + + $messagingSystem = EcotoneLite::bootstrap( + [CachedCommandHandlerService::class], + [CachedCommandHandlerService::class => $handler], + $configuration, + useCachedVersion: true, + ); + $messagingSystem->getCommandBus()->sendWithRouting('cache.command', 'first'); + + self::assertNotEmpty(glob($cacheDirectory . '/ecotone/*/ecotone_container.php')); + + $warmBootedMessagingSystem = EcotoneLite::bootstrap( + [CachedCommandHandlerService::class], + [CachedCommandHandlerService::class => $handler], + $configuration, + useCachedVersion: true, + ); + $warmBootedMessagingSystem->getCommandBus()->sendWithRouting('cache.command', 'second'); + + self::assertSame(['first', 'second'], $handler->received); + } +} + +/** + * licence Apache-2.0 + */ +class CachedCommandHandlerService +{ + public array $received = []; + + #[CommandHandler('cache.command')] + public function handle(#[Payload] string $payload): void + { + $this->received[] = $payload; + } +} diff --git a/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php index 09484fda4..45fde3e0e 100644 --- a/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php @@ -5,12 +5,17 @@ namespace Test\Ecotone\SymfonyContainer; use Ecotone\Lite\InMemoryPSRContainer; +use Ecotone\Messaging\Config\Container\Compiler\ContainerImplementation; +use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; use Ecotone\Messaging\Config\Container\ContainerBuilder; +use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\Container\Reference; +use Ecotone\Messaging\Config\DefinedObjectWrapper; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use Ecotone\Test\StubLogger; use Psr\Container\ContainerInterface; +use Test\Ecotone\Lite\ADefinedObject; use Test\Ecotone\Lite\ContainerImplementationTestCase; /** @@ -24,6 +29,53 @@ protected static function getContainerFrom(ContainerBuilder $builder, ?Container return EcotoneSymfonyContainerFactory::build($builder, ServiceCacheConfiguration::noCache(), $externalContainer); } + public function test_it_resolves_service_ids_containing_anonymous_class_names(): void + { + $anonymousService = new class () { + }; + $serviceId = 'channel-' . get_class($anonymousService) . '.will_load'; + + $container = self::buildContainerFromDefinitions([ + $serviceId => new Definition(get_class($anonymousService)), + ]); + + self::assertEquals($anonymousService, $container->get($serviceId)); + } + + public function test_it_does_not_report_unknown_external_references_as_available(): void + { + $container = self::buildContainerFromDefinitions([ + 'def1' => new Definition(WithReferenceToUnknown::class, [new Reference('unknownService', ContainerImplementation::NULL_ON_INVALID_REFERENCE)]), + 'def2' => new Definition(WithReferenceToUnknown::class, [new Reference('anotherUnknownService')]), + ]); + + self::assertNull($container->get('def1')->dependency); + self::assertFalse($container->has('unknownService')); + self::assertFalse($container->has('anotherUnknownService')); + } + + public function test_it_preserves_identity_of_registered_defined_object_instances(): void + { + $definedObjectInstance = new ADefinedObject('aName', null); + $builder = new ContainerBuilder(); + $builder->replace('aService', $definedObjectInstance); + $builder->addCompilerPass(new RegisterInterfaceToCallReferences()); + + $container = static::getContainerFrom($builder); + + self::assertSame($definedObjectInstance, $container->get('aService')); + } + + public function test_it_preserves_identity_of_nested_defined_object_instances(): void + { + $definedObjectInstance = new ADefinedObject('aName', null); + $container = self::buildContainerFromDefinitions([ + 'aService' => new Definition(WithReferenceToUnknown::class, [new DefinedObjectWrapper($definedObjectInstance)]), + ]); + + self::assertSame($definedObjectInstance, $container->get('aService')->dependency); + } + public function test_it_resolves_references_from_external_container(): void { $logger = StubLogger::create(); @@ -35,3 +87,13 @@ public function test_it_resolves_references_from_external_container(): void self::assertSame($logger, $container->get('aReference')); } } + +/** + * licence Apache-2.0 + */ +class WithReferenceToUnknown +{ + public function __construct(public mixed $dependency) + { + } +} From d93aed5a8300d26862360f3be1440d8ab2d2d3f2 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 10:27:00 +0200 Subject: [PATCH 04/17] feat: expose registered console commands as container parameter --- .../src/SymfonyContainer/EcotoneContainer.php | 5 +++++ .../EcotoneSymfonyContainerFactory.php | 11 ++++++++--- .../SymfonyContainerImplementation.php | 1 + .../EcotoneLiteCachedContainerTest.php | 17 +++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php index dfaef929a..f371d96b4 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php @@ -38,4 +38,9 @@ public function set(string $id, mixed $service): void { $this->container->set(ServiceIdNormalizer::normalize($id), $service); } + + public function getParameter(string $name): mixed + { + return $this->container->getParameter($name); + } } diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php index 795f22a08..a3cbd64c5 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -28,12 +28,17 @@ public static function build( array $runtimeServices = [], ): EcotoneContainer { $symfonyBuilder = new SymfonyContainerBuilder(); - $builder->addCompilerPass(new SymfonyContainerImplementation( + $implementation = new SymfonyContainerImplementation( $symfonyBuilder, array_keys($runtimeServices), preserveRuntimeInstances: ! $serviceCacheConfiguration->shouldUseCache(), - )); - $builder->compile(); + ); + $definitionsHolder = $builder->compile(); + $implementation->process($builder); + $symfonyBuilder->setParameter( + SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER, + serialize($definitionsHolder->getRegisteredCommands()), + ); if ($serviceCacheConfiguration->shouldUseCache()) { $symfonyBuilder->compile(); diff --git a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php index a3a077444..82893637e 100644 --- a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php +++ b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php @@ -33,6 +33,7 @@ class SymfonyContainerImplementation implements ContainerImplementation { public const EXTERNAL_CONTAINER_ID = 'ecotone.external_container'; public const EXTERNAL_REFERENCES_PARAMETER = 'ecotone.external_references'; + public const CONSOLE_COMMANDS_PARAMETER = 'ecotone.console_commands'; public const EXTERNAL_DELEGATE_SUFFIX = '.ecotone.external'; public const NULLABLE_EXTERNAL_DELEGATE_SUFFIX = '.ecotone.external.nullable'; diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php index ffb671428..0373a7439 100644 --- a/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php @@ -10,6 +10,8 @@ use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Modelling\Attribute\CommandHandler; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Test\Ecotone\Messaging\Fixture\Annotation\MessageEndpoint\OneTimeCommand\OneTimeWithResultExample; /** * licence Apache-2.0 @@ -45,6 +47,21 @@ public function test_bootstrap_with_cache_dumps_symfony_container_and_warm_boot_ self::assertSame(['first', 'second'], $handler->received); } + + public function test_registered_console_commands_are_available_as_container_parameter(): void + { + $messagingSystem = EcotoneLite::bootstrap( + [OneTimeWithResultExample::class], + [OneTimeWithResultExample::class => new OneTimeWithResultExample()], + ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackages()), + ); + + $container = $messagingSystem->getServiceFromContainer(ContainerInterface::class); + + $consoleCommands = unserialize($container->getParameter('ecotone.console_commands')); + self::assertContains('doSomething', array_map(fn ($command) => $command->getName(), $consoleCommands)); + } } /** From 75ecb9b105783c25d5b066b6cf9bcb610d0d700f Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 10:30:48 +0200 Subject: [PATCH 05/17] fix: cache external reference resolutions like LazyInMemoryContainer did --- .../SymfonyContainerImplementation.php | 1 - .../SymfonyContainerImplementationTest.php | 23 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php index 82893637e..a9fe718c6 100644 --- a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php +++ b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php @@ -130,7 +130,6 @@ private function registerExternalReferenceDelegate(string $id, int $invalidBehav (new SymfonyDefinition(stdClass::class)) ->setFactory([ExternalReferenceResolver::class, 'resolve']) ->setArguments([new SymfonyReference(self::EXTERNAL_CONTAINER_ID), $id, $invalidBehavior]) - ->setShared(false) ->setPublic(true) ); } diff --git a/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php index 45fde3e0e..698b73ee1 100644 --- a/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php @@ -17,6 +17,7 @@ use Psr\Container\ContainerInterface; use Test\Ecotone\Lite\ADefinedObject; use Test\Ecotone\Lite\ContainerImplementationTestCase; +use Test\Ecotone\Lite\WithNoDependencies; /** * licence Apache-2.0 @@ -76,6 +77,28 @@ public function test_it_preserves_identity_of_nested_defined_object_instances(): self::assertSame($definedObjectInstance, $container->get('aService')->dependency); } + public function test_it_resolves_external_references_into_single_shared_instance(): void + { + $externalContainer = new class () implements ContainerInterface { + public function get(string $id): mixed + { + return new WithNoDependencies(); + } + + public function has(string $id): bool + { + return $id === 'externallyDefined'; + } + }; + + $container = self::buildContainerFromDefinitions([ + 'def1' => new Definition(WithReferenceToUnknown::class, [new Reference('externallyDefined')]), + 'def2' => new Definition(WithReferenceToUnknown::class, [new Reference('externallyDefined')]), + ], $externalContainer); + + self::assertSame($container->get('def1')->dependency, $container->get('def2')->dependency); + } + public function test_it_resolves_references_from_external_container(): void { $logger = StubLogger::create(); From d6465a8bf9ca6cd698ffb5014195acb781294a74 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 10:34:09 +0200 Subject: [PATCH 06/17] feat: switch Laravel integration to side-car Symfony container with bridge bindings --- packages/Laravel/src/EcotoneProvider.php | 116 +++++------------- .../src/LaravelPsrContainerAdapter.php | 30 +++++ 2 files changed, 64 insertions(+), 82 deletions(-) create mode 100644 packages/Laravel/src/LaravelPsrContainerAdapter.php diff --git a/packages/Laravel/src/EcotoneProvider.php b/packages/Laravel/src/EcotoneProvider.php index 0fe516e32..5b4e74e8f 100644 --- a/packages/Laravel/src/EcotoneProvider.php +++ b/packages/Laravel/src/EcotoneProvider.php @@ -2,31 +2,28 @@ namespace Ecotone\Laravel; -use function class_exists; - use const DIRECTORY_SEPARATOR; use Ecotone\AnnotationFinder\AnnotationFinderFactory; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; use Ecotone\Messaging\Config\ConsoleCommandResultSet; -use Ecotone\Messaging\Config\Container\Compiler\ContainerImplementation; -use Ecotone\Messaging\Config\Container\ContainerConfig; -use Ecotone\Messaging\Config\Container\Definition; -use Ecotone\Messaging\Config\Container\Reference; +use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; +use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; +use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\ConfigurationVariableService; use Ecotone\Messaging\Gateway\ConsoleCommandRunner; use Ecotone\Messaging\Handler\Recoverability\RetryTemplateBuilder; +use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; +use Ecotone\SymfonyContainer\SymfonyContainerImplementation; use Illuminate\Console\Events\CommandFinished; use Illuminate\Foundation\Console\ClosureCommand; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Config; use Illuminate\Support\ServiceProvider; -use InvalidArgumentException; -use ReflectionMethod; /** * licence Apache-2.0 @@ -101,12 +98,13 @@ public function register() $applicationConfiguration = $applicationConfiguration->withExtensionObjects([new EloquentRepositoryBuilder()]); $applicationConfiguration = MessagingSystemConfiguration::addCorePackage($applicationConfiguration, $enableTesting); - [$serviceCacheConfiguration, $definitionHolder] = $this->prepareFromCache($useProductionCache, $rootCatalog, $applicationConfiguration, $enableTesting, $cacheDirectory); + [$serviceCacheConfiguration, $container] = $this->prepareFromCache($useProductionCache, $rootCatalog, $applicationConfiguration, $enableTesting, $cacheDirectory); - foreach ($definitionHolder->getDefinitions() as $id => $definition) { - $this->app->singleton($id, function () use ($definition) { - return $this->resolveArgument($definition); - }); + $messagingSystem = $container->get(ConfiguredMessagingSystem::class); + $this->app->singleton(ConfiguredMessagingSystem::class, fn () => $messagingSystem); + foreach ($messagingSystem->getGatewayList() as $gatewayReference) { + $gatewayReferenceName = $gatewayReference->getReferenceName(); + $this->app->singleton($gatewayReferenceName, fn () => $container->get($gatewayReferenceName)); } $this->app->singleton( @@ -127,7 +125,8 @@ function () { ); if ($this->app->runningInConsole()) { - foreach ($definitionHolder->getRegisteredCommands() as $oneTimeCommandConfiguration) { + $registeredCommands = unserialize($container->getParameter(SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER)); + foreach ($registeredCommands as $oneTimeCommandConfiguration) { $commandName = $oneTimeCommandConfiguration->getName(); foreach ($oneTimeCommandConfiguration->getParameters() as $parameter) { @@ -193,63 +192,20 @@ public static function getCacheDirectoryPath(): string return App::storagePath() . DIRECTORY_SEPARATOR . 'framework' . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'data'; } - private function instantiateDefinition(Definition $definition): object - { - $arguments = $this->resolveArgument($definition->getArguments()); - if ($definition->hasFactory()) { - $factory = $definition->getFactory(); - if (method_exists($factory[0], $factory[1]) && (new ReflectionMethod($factory[0], $factory[1]))->isStatic()) { - // static call - return $factory(...$arguments); - } else { - // method call from a service instance - $service = $this->app->make($factory[0]); - return $service->{$factory[1]}(...$arguments); - } - } else { - $class = $definition->getClassName(); - return new $class(...$arguments); - } - } - - private function resolveArgument(mixed $argument): mixed - { - if (is_array($argument)) { - return array_map(fn ($argument) => $this->resolveArgument($argument), $argument); - } elseif ($argument instanceof Definition) { - $object = $this->instantiateDefinition($argument); - foreach ($argument->getMethodCalls() as $methodCall) { - $object->{$methodCall->getMethodName()}(...$this->resolveArgument($methodCall->getArguments())); - } - return $object; - } elseif ($argument instanceof Reference) { - if ($this->app->has($argument->getId())) { - return $this->app->get($argument->getId()); - } - if ($argument->getInvalidBehavior() === ContainerImplementation::NULL_ON_INVALID_REFERENCE) { - return null; - } - if (class_exists($argument->getId())) { - return $this->app->make($argument->getId()); - } - throw new InvalidArgumentException("Reference to {$argument->getId()} is not found"); - } else { - return $argument; - } - } - public function prepareFromCache(mixed $useProductionCache, string $rootCatalog, ServiceConfiguration $applicationConfiguration, mixed $enableTesting, string $cacheDirectory): array { - if ($useProductionCache && $cacheDirectory) { - $messagingFile = $cacheDirectory . DIRECTORY_SEPARATOR . self::MESSAGING_SYSTEM_FILE_NAME; + $externalContainer = new LaravelPsrContainerAdapter($this->app); + $runtimeServices = [ + ConfigurationVariableService::REFERENCE_NAME => new LaravelConfigurationVariableService(), + ]; - if (file_exists($messagingFile)) { - /** It may fail on deserialization, then return `false` and we can build new one */ - $definitionHolder = unserialize(file_get_contents($messagingFile)); + if ($useProductionCache && $cacheDirectory) { + $serviceCacheConfiguration = new ServiceCacheConfiguration($cacheDirectory, true); + $runtimeServices[ServiceCacheConfiguration::REFERENCE_NAME] = $serviceCacheConfiguration; - if ($definitionHolder) { - return [new ServiceCacheConfiguration($cacheDirectory, true), $definitionHolder]; - } + $container = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); + if ($container) { + return [$serviceCacheConfiguration, $container]; } } @@ -273,32 +229,28 @@ public function prepareFromCache(mixed $useProductionCache, string $rootCatalog, $useProductionCache ? $cacheDirectory : ($cacheDirectory . DIRECTORY_SEPARATOR . $cacheHash), true, ); + $runtimeServices[ServiceCacheConfiguration::REFERENCE_NAME] = $serviceCacheConfiguration; - $definitionHolder = null; + $container = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); - $messagingSystemCachePath = $serviceCacheConfiguration->getPath() . DIRECTORY_SEPARATOR . self::MESSAGING_SYSTEM_FILE_NAME; - - if ($serviceCacheConfiguration->shouldUseCache() && file_exists($messagingSystemCachePath)) { - /** It may fail on deserialization, then return `false` and we can build new one */ - $definitionHolder = unserialize(file_get_contents($messagingSystemCachePath)); - } - - if (! $definitionHolder) { + if (! $container) { $configuration = MessagingSystemConfiguration::prepareWithAnnotationFinder( $annotationFinder, new LaravelConfigurationVariableService(), $applicationConfiguration, enableTestPackage: $enableTesting ); - $definitionHolder = ContainerConfig::buildDefinitionHolder($configuration); - if ($serviceCacheConfiguration->shouldUseCache()) { - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - file_put_contents($messagingSystemCachePath, serialize($definitionHolder)); - } + $ecotoneBuilder = new ContainerBuilder(); + $ecotoneBuilder->addCompilerPass($configuration); + $ecotoneBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); + $ecotoneBuilder->addCompilerPass(new ValidityCheckPass()); + + MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); + $container = EcotoneSymfonyContainerFactory::build($ecotoneBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices); } - return [$serviceCacheConfiguration, $definitionHolder]; + return [$serviceCacheConfiguration, $container]; } /** diff --git a/packages/Laravel/src/LaravelPsrContainerAdapter.php b/packages/Laravel/src/LaravelPsrContainerAdapter.php new file mode 100644 index 000000000..49055fcca --- /dev/null +++ b/packages/Laravel/src/LaravelPsrContainerAdapter.php @@ -0,0 +1,30 @@ +app->make($id); + } + + public function has(string $id): bool + { + return $this->app->has($id) || class_exists($id); + } +} From 3375c529b3e5b8b6df0a4bed880a7230a91c0bc2 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 10:47:41 +0200 Subject: [PATCH 07/17] feat: switch Symfony bundle to side-car Ecotone container with bridge services --- .../src/SymfonyContainer/EcotoneContainer.php | 8 ++ .../AliasExternalReferenceForTesting.php | 8 +- .../EcotoneContainerLoader.php | 25 ++++++ .../DependencyInjection/EcotoneExtension.php | 89 +++++++++++++++++-- 4 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 packages/Symfony/DependencyInjection/EcotoneContainerLoader.php diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php index f371d96b4..35d0018b0 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php @@ -43,4 +43,12 @@ public function getParameter(string $name): mixed { return $this->container->getParameter($name); } + + /** + * @return string[] + */ + public function getServiceIds(): array + { + return $this->container->getServiceIds(); + } } diff --git a/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php b/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php index 008b2c88b..d70d8097a 100644 --- a/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php +++ b/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php @@ -18,8 +18,12 @@ public function process(ContainerBuilder $container): void } foreach ($container->getParameter('ecotone.external_references') as $id) { - if ($container->hasDefinition($id)) { - $container->setAlias(InMemoryContainerImplementation::ALIAS_PREFIX.$id, $id) + $aliasId = InMemoryContainerImplementation::ALIAS_PREFIX . $id; + if ($container->has($aliasId)) { + continue; + } + if ($container->hasDefinition($id) || $container->hasAlias($id)) { + $container->setAlias($aliasId, $id) ->setPublic(true); } } diff --git a/packages/Symfony/DependencyInjection/EcotoneContainerLoader.php b/packages/Symfony/DependencyInjection/EcotoneContainerLoader.php new file mode 100644 index 000000000..27b74d14c --- /dev/null +++ b/packages/Symfony/DependencyInjection/EcotoneContainerLoader.php @@ -0,0 +1,25 @@ +has(\Psr\Container\ContainerInterface::class)) { + $container->setAlias(\Psr\Container\ContainerInterface::class, 'service_container'); + } $container->register(\Ecotone\Messaging\ConfigurationVariableService::class, SymfonyConfigurationVariableService::class)->setAutowired(true)->setPublic(true); $container->register(ServiceCacheConfiguration::REFERENCE_NAME, ServiceCacheConfiguration::class) @@ -92,15 +103,61 @@ public function load(array $configs, ContainerBuilder $container): void enableTestPackage: $config['test'] ); + $cacheDirectory = $container->getParameter('kernel.build_dir') . DIRECTORY_SEPARATOR . 'ecotone'; + $serviceCacheConfiguration = new ServiceCacheConfiguration($cacheDirectory, true); + $containerBuilder = new \Ecotone\Messaging\Config\Container\ContainerBuilder(); + $containerBuilder->register(ServiceCacheConfiguration::REFERENCE_NAME, $serviceCacheConfiguration); $containerBuilder->addCompilerPass($messagingConfiguration); $containerBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); - $containerBuilder->addCompilerPass(new SymfonyContainerAdapter($container)); - $definitionHolder = $containerBuilder->compile(); + $ecotoneContainer = EcotoneSymfonyContainerFactory::build($containerBuilder, $serviceCacheConfiguration); + + $container->register('ecotone.container', EcotoneContainer::class) + ->setFactory([EcotoneContainerLoader::class, 'load']) + ->setArguments([$cacheDirectory, new Reference('service_container')]) + ->setPublic(true); + + $container->register(ConfiguredMessagingSystem::class, MessagingSystemContainer::class) + ->setFactory([new Reference('ecotone.container'), 'get']) + ->setArguments([ConfiguredMessagingSystem::class]) + ->setPublic(true); + + foreach ($messagingConfiguration->getRegisteredGateways() as $gatewayProxyBuilder) { + $referenceName = $gatewayProxyBuilder->getReferenceName(); + $container->register($referenceName, $gatewayProxyBuilder->getInterfaceName()) + ->setFactory([new Reference('ecotone.container'), 'get']) + ->setArguments([$referenceName]) + ->setPublic(true); + } + + $container->register(ProxyFactory::class, ProxyFactory::class) + ->setFactory([new Reference('ecotone.container'), 'get']) + ->setArguments([ProxyFactory::class]) + ->setPublic(true); + + foreach ($ecotoneContainer->getServiceIds() as $serviceId) { + if ($container->has($serviceId) || ! (class_exists($serviceId) || interface_exists($serviceId))) { + continue; + } + $container->register($serviceId, $serviceId) + ->setFactory([new Reference('ecotone.container'), 'get']) + ->setArguments([$serviceId]) + ->setPublic(true); + } - $container->getDefinition(LoggingGateway::class)->addTag('monolog.logger', ['channel' => 'ecotone']); + $container->register(ExternalReferenceResolver::TESTING_ALIAS_PREFIX . 'logger', LoggerInterface::class) + ->setFactory([RuntimeInstanceProvider::class, 'provide']) + ->setArguments([new Reference('logger')]) + ->addTag('monolog.logger', ['channel' => 'ecotone']) + ->setPublic(true); - foreach ($definitionHolder->getRegisteredCommands() as $oneTimeCommandConfiguration) { + $container->setParameter( + 'ecotone.external_references', + $ecotoneContainer->getParameter(SymfonyContainerImplementation::EXTERNAL_REFERENCES_PARAMETER), + ); + + $registeredCommands = unserialize($ecotoneContainer->getParameter(SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER)); + foreach ($registeredCommands as $oneTimeCommandConfiguration) { $definition = new Definition(); $definition->setClass(MessagingEntrypointCommand::class); $definition->addArgument($oneTimeCommandConfiguration->getName()); @@ -111,6 +168,26 @@ public function load(array $configs, ContainerBuilder $container): void $container->setDefinition($oneTimeCommandConfiguration->getChannelName(), $definition); } - $container->setParameter('ecotone.messaging_system_configuration.required_references', $messagingConfiguration->getRequiredReferencesForValidation()); + if (! $container->hasDefinition(ConsoleCommandRunner::class)) { + $container->register(ConsoleCommandRunner::class, ConsoleCommandRunner::class) + ->setFactory([new Reference('ecotone.container'), 'get']) + ->setArguments([ConsoleCommandRunner::class]) + ->setPublic(true); + } + + $unresolvedRequiredReferences = []; + foreach ($messagingConfiguration->getRequiredReferencesForValidation() as $referenceId => $errorMessage) { + if (! $ecotoneContainer->has($referenceId)) { + $unresolvedRequiredReferences[$referenceId] = $errorMessage; + continue; + } + if (! $container->has($referenceId)) { + $container->register($referenceId, class_exists($referenceId) || interface_exists($referenceId) ? $referenceId : null) + ->setFactory([new Reference('ecotone.container'), 'get']) + ->setArguments([$referenceId]) + ->setPublic(true); + } + } + $container->setParameter('ecotone.messaging_system_configuration.required_references', $unresolvedRequiredReferences); } } From 4023e5e9cf47fa69b21b2fbf28b74d1431b996b3 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 11:01:53 +0200 Subject: [PATCH 08/17] feat: switch Tempest integration to side-car Symfony container --- .../EcotoneSymfonyContainerFactory.php | 30 ++--- ...cotoneSymfonyContainerFactoryCacheTest.php | 20 ++++ .../Compiler/CacheClearer.php | 2 +- .../PDO/TempestDynamicDriverConnection.php | 3 +- .../Config/TempestTenantDatabaseSwitcher.php | 6 +- .../src/EcotoneConsoleCommandDiscovery.php | 8 +- .../src/MessagingSystemInitializer.php | 109 ++++++++---------- .../src/TempestPsrContainerAdapter.php | 19 ++- 8 files changed, 111 insertions(+), 86 deletions(-) diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php index a3cbd64c5..49bd1c1bd 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -59,12 +59,17 @@ public static function loadCached( array $runtimeServices = [], ): ?EcotoneContainer { $containerFile = self::containerFilePath($serviceCacheConfiguration); - $className = self::containerClassName($serviceCacheConfiguration); + if (! file_exists($containerFile)) { + return null; + } + + $containerCode = file_get_contents($containerFile); + if ($containerCode === false || preg_match('/^class (EcotoneCachedContainer_[a-f0-9]+)/m', $containerCode, $matches) !== 1) { + return null; + } + $className = $matches[1]; if (! class_exists($className, false)) { - if (! file_exists($containerFile)) { - return null; - } - require_once $containerFile; + require $containerFile; } return self::wrapWithExternalFallback(new $className(), $externalContainer, $runtimeServices); @@ -79,10 +84,14 @@ private static function dumpToCache( mkdir($cacheDirectory, 0777, true); } $dumper = new PhpDumper($symfonyBuilder); - file_put_contents( - self::containerFilePath($serviceCacheConfiguration), - $dumper->dump(['class' => self::containerClassName($serviceCacheConfiguration)]), + $placeholderClassName = 'EcotoneCachedContainerPlaceholder'; + $containerCode = $dumper->dump(['class' => $placeholderClassName]); + $containerCode = str_replace( + $placeholderClassName, + 'EcotoneCachedContainer_' . md5($containerCode), + $containerCode, ); + file_put_contents(self::containerFilePath($serviceCacheConfiguration), $containerCode); } private static function containerFilePath(ServiceCacheConfiguration $serviceCacheConfiguration): string @@ -90,11 +99,6 @@ private static function containerFilePath(ServiceCacheConfiguration $serviceCach return $serviceCacheConfiguration->getPath() . DIRECTORY_SEPARATOR . 'ecotone_container.php'; } - private static function containerClassName(ServiceCacheConfiguration $serviceCacheConfiguration): string - { - return 'EcotoneCachedContainer_' . md5($serviceCacheConfiguration->getPath()); - } - /** * @param array $runtimeServices */ diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php index fbdad2ebb..2c38b5a88 100644 --- a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php @@ -32,6 +32,26 @@ public function test_it_loads_dumped_container_from_cache(): void self::assertEquals(new ACachedService('someName'), $loaded->get('aService')); } + public function test_it_rebuilds_fresh_container_when_cache_files_are_removed_in_same_process(): void + { + $cacheConfiguration = new ServiceCacheConfiguration($this->uniqueCacheDirectory(), true); + $builder = new ContainerBuilder(); + $builder->replace('aService', new Definition(ACachedService::class, ['first'])); + EcotoneSymfonyContainerFactory::build($builder, $cacheConfiguration); + + foreach (glob($cacheConfiguration->getPath() . '/*') as $file) { + unlink($file); + } + + self::assertNull(EcotoneSymfonyContainerFactory::loadCached($cacheConfiguration)); + + $rebuiltBuilder = new ContainerBuilder(); + $rebuiltBuilder->replace('aService', new Definition(ACachedService::class, ['second'])); + $rebuilt = EcotoneSymfonyContainerFactory::build($rebuiltBuilder, $cacheConfiguration); + + self::assertSame('second', $rebuilt->get('aService')->name); + } + public function test_it_returns_null_when_no_dumped_container_exists(): void { $cacheConfiguration = new ServiceCacheConfiguration($this->uniqueCacheDirectory(), true); diff --git a/packages/Symfony/DependencyInjection/Compiler/CacheClearer.php b/packages/Symfony/DependencyInjection/Compiler/CacheClearer.php index 4a149709c..66f1bd8b4 100644 --- a/packages/Symfony/DependencyInjection/Compiler/CacheClearer.php +++ b/packages/Symfony/DependencyInjection/Compiler/CacheClearer.php @@ -43,7 +43,7 @@ private function deleteDirectory(string $directory): void if (is_dir($filePath)) { $this->deleteDirectory($filePath); rmdir($filePath); - } else { + } elseif ($file !== 'ecotone_container.php') { unlink($filePath); } } diff --git a/packages/Tempest/src/Config/PDO/TempestDynamicDriverConnection.php b/packages/Tempest/src/Config/PDO/TempestDynamicDriverConnection.php index 20af7a5b1..5f7ad5d1f 100644 --- a/packages/Tempest/src/Config/PDO/TempestDynamicDriverConnection.php +++ b/packages/Tempest/src/Config/PDO/TempestDynamicDriverConnection.php @@ -10,6 +10,7 @@ use Doctrine\DBAL\Driver\Result as ResultInterface; use Doctrine\DBAL\Driver\Statement as StatementInterface; use PDO; +use ReflectionProperty; use Tempest\Container\GenericContainer; use Tempest\Database\Connection\Connection; @@ -27,7 +28,7 @@ final class TempestDynamicDriverConnection implements DriverConnection private function pdo(): PDO { $connection = GenericContainer::instance()->get(Connection::class); - $property = new \ReflectionProperty($connection, 'pdo'); + $property = new ReflectionProperty($connection, 'pdo'); return $property->getValue($connection); } diff --git a/packages/Tempest/src/Config/TempestTenantDatabaseSwitcher.php b/packages/Tempest/src/Config/TempestTenantDatabaseSwitcher.php index 97d5ae506..2813a2d5f 100644 --- a/packages/Tempest/src/Config/TempestTenantDatabaseSwitcher.php +++ b/packages/Tempest/src/Config/TempestTenantDatabaseSwitcher.php @@ -15,6 +15,7 @@ use Tempest\Database\Transactions\GenericTransactionManager; use Tempest\EventBus\EventBus; use Tempest\Mapper\SerializerFactory; +use Throwable; /** * licence Apache-2.0 @@ -23,7 +24,8 @@ final class TempestTenantDatabaseSwitcher { public function __construct( private readonly DatabaseConfig $defaultDatabaseConfig, - ) {} + ) { + } public static function create(): self { @@ -99,7 +101,7 @@ private function closeDoctrineDefaultConnection(GenericContainer $container): vo $factory = $container->get(DbalConnectionFactory::class); $doctrineConnection = $factory->createContext()->getDbalConnection(); $doctrineConnection->close(); - } catch (\Throwable) { + } catch (Throwable) { } } } diff --git a/packages/Tempest/src/EcotoneConsoleCommandDiscovery.php b/packages/Tempest/src/EcotoneConsoleCommandDiscovery.php index 29e368f07..ccc35aeb6 100644 --- a/packages/Tempest/src/EcotoneConsoleCommandDiscovery.php +++ b/packages/Tempest/src/EcotoneConsoleCommandDiscovery.php @@ -33,7 +33,7 @@ public function discover(DiscoveryLocation $location, ClassReflector $class): vo public function apply(): void { - if (MessagingSystemInitializer::getDefinitionHolder() === null) { + if (MessagingSystemInitializer::getRegisteredCommands() === null) { if (! $this->container->has(EcotoneConfig::class)) { return; } @@ -41,14 +41,12 @@ public function apply(): void (new MessagingSystemInitializer())->initialize($this->container); } - $definitionHolder = MessagingSystemInitializer::getDefinitionHolder(); + $commands = MessagingSystemInitializer::getRegisteredCommands(); - if ($definitionHolder === null) { + if ($commands === null) { return; } - $commands = $definitionHolder->getRegisteredCommands(); - if ($commands === []) { return; } diff --git a/packages/Tempest/src/MessagingSystemInitializer.php b/packages/Tempest/src/MessagingSystemInitializer.php index 33d4dd38a..b66f1fda9 100644 --- a/packages/Tempest/src/MessagingSystemInitializer.php +++ b/packages/Tempest/src/MessagingSystemInitializer.php @@ -7,19 +7,21 @@ use const DIRECTORY_SEPARATOR; use Ecotone\AnnotationFinder\AnnotationFinderFactory; -use Ecotone\Lite\LazyInMemoryContainer; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; -use Ecotone\Messaging\Config\Container\Compiler\ContainerDefinitionsHolder; -use Ecotone\Messaging\Config\Container\ContainerConfig; +use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; +use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; +use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\ConfigurationVariableService; -use Psr\Log\LoggerInterface; +use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; +use Ecotone\SymfonyContainer\SymfonyContainerImplementation; use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; use Tempest\Discovery\Composer; +use Throwable; /** * licence Apache-2.0 @@ -31,15 +33,15 @@ final class MessagingSystemInitializer implements Initializer private const CONFIG_HASH_FILE_NAME = 'messaging_system_hash'; - private static ?ContainerDefinitionsHolder $definitionHolder = null; + private static ?array $registeredCommands = null; private static ?string $configHash = null; private static ?string $proxyDirectory = null; - public static function getDefinitionHolder(): ?ContainerDefinitionsHolder + public static function getRegisteredCommands(): ?array { - return self::$definitionHolder; + return self::$registeredCommands; } public static function getConfigHash(): ?string @@ -54,7 +56,7 @@ public static function getProxyDirectory(): ?string public static function clearDefinitionHolder(): void { - self::$definitionHolder = null; + self::$registeredCommands = null; self::$configHash = null; self::$proxyDirectory = null; } @@ -69,36 +71,27 @@ public function initialize(Container $container): ConfiguredMessagingSystem $applicationConfiguration = $this->buildServiceConfiguration($config, $environment, $cacheDirectory, $container); - [$serviceCacheConfiguration, $definitionHolder, $configHash] = $this->prepareFromCache( + [$ecotoneContainer, $configHash] = $this->prepareFromCache( $useProductionCache, $rootPath, $applicationConfiguration, $config->test, $cacheDirectory, + $container, ); - self::$definitionHolder = $definitionHolder; + self::$registeredCommands = unserialize($ecotoneContainer->getParameter(SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER)); self::$configHash = $configHash; self::$proxyDirectory = $cacheDirectory . DIRECTORY_SEPARATOR . 'console_proxies'; - $ecotoneContainer = new LazyInMemoryContainer( - $definitionHolder->getDefinitions(), - new TempestPsrContainerAdapter($container), - ); - - $ecotoneContainer->set( - ConfigurationVariableService::REFERENCE_NAME, - new TempestConfigurationVariableService(), - ); - - $ecotoneContainer->set( - ServiceCacheConfiguration::REFERENCE_NAME, - $serviceCacheConfiguration, - ); - - $this->wireLogger($container, $ecotoneContainer); - - EcotoneServiceInitializer::markCompiled(array_keys($definitionHolder->getDefinitions())); + EcotoneServiceInitializer::markCompiled(array_filter( + $ecotoneContainer->getServiceIds(), + fn (string $serviceId) => ! str_ends_with($serviceId, SymfonyContainerImplementation::EXTERNAL_DELEGATE_SUFFIX) + && ! str_ends_with($serviceId, SymfonyContainerImplementation::NULLABLE_EXTERNAL_DELEGATE_SUFFIX) + && $serviceId !== SymfonyContainerImplementation::EXTERNAL_CONTAINER_ID + && $serviceId !== 'service_container' + && $serviceId !== \Psr\Container\ContainerInterface::class, + )); return $ecotoneContainer->get(ConfiguredMessagingSystem::class); } @@ -116,7 +109,7 @@ private function deriveNamespacesFromComposer(Container $container): array { try { $composer = $container->get(Composer::class); - } catch (\Throwable) { + } catch (Throwable) { return []; } @@ -128,16 +121,6 @@ private function deriveNamespacesFromComposer(Container $container): array return $namespaces; } - private function wireLogger(Container $container, LazyInMemoryContainer $ecotoneContainer): void - { - try { - $logger = $container->get(LoggerInterface::class); - $ecotoneContainer->set('logger', $logger); - $ecotoneContainer->set(LoggerInterface::class, $logger); - } catch (\Throwable) { - } - } - private function buildServiceConfiguration( EcotoneConfig $config, string $environment, @@ -185,18 +168,20 @@ private function prepareFromCache( ServiceConfiguration $applicationConfiguration, bool $enableTesting, string $cacheDirectory, + Container $container, ): array { - if ($useProductionCache && $cacheDirectory) { - $messagingFile = $cacheDirectory . DIRECTORY_SEPARATOR . self::MESSAGING_SYSTEM_FILE_NAME; - - if (file_exists($messagingFile)) { - $definitionHolder = unserialize(file_get_contents($messagingFile)); + $externalContainer = new TempestPsrContainerAdapter($container); + $runtimeServices = [ + ConfigurationVariableService::REFERENCE_NAME => new TempestConfigurationVariableService(), + ]; - if ($definitionHolder instanceof ContainerDefinitionsHolder) { - $persistedHash = $this->readPersistedConfigHash($cacheDirectory); + if ($useProductionCache && $cacheDirectory) { + $serviceCacheConfiguration = new ServiceCacheConfiguration($cacheDirectory, true); + $runtimeServices[ServiceCacheConfiguration::REFERENCE_NAME] = $serviceCacheConfiguration; - return [new ServiceCacheConfiguration($cacheDirectory, true), $definitionHolder, $persistedHash]; - } + $ecotoneContainer = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); + if ($ecotoneContainer) { + return [$ecotoneContainer, $this->readPersistedConfigHash($cacheDirectory)]; } } @@ -220,34 +205,32 @@ private function prepareFromCache( $useProductionCache ? $cacheDirectory : ($cacheDirectory . DIRECTORY_SEPARATOR . $cacheHash), true, ); + $runtimeServices[ServiceCacheConfiguration::REFERENCE_NAME] = $serviceCacheConfiguration; - $definitionHolder = null; - $messagingSystemCachePath = $serviceCacheConfiguration->getPath() . DIRECTORY_SEPARATOR . self::MESSAGING_SYSTEM_FILE_NAME; - - if ($serviceCacheConfiguration->shouldUseCache() && file_exists($messagingSystemCachePath)) { - $definitionHolder = unserialize(file_get_contents($messagingSystemCachePath)); - } + $ecotoneContainer = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); - if (! $definitionHolder instanceof ContainerDefinitionsHolder) { + if (! $ecotoneContainer) { $configuration = MessagingSystemConfiguration::prepareWithAnnotationFinder( $annotationFinder, new TempestConfigurationVariableService(), $applicationConfiguration, enableTestPackage: $enableTesting, ); - $definitionHolder = ContainerConfig::buildDefinitionHolder($configuration); - if ($serviceCacheConfiguration->shouldUseCache()) { - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - file_put_contents($messagingSystemCachePath, serialize($definitionHolder)); + $ecotoneBuilder = new ContainerBuilder(); + $ecotoneBuilder->addCompilerPass($configuration); + $ecotoneBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); + $ecotoneBuilder->addCompilerPass(new ValidityCheckPass()); + + MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); + $ecotoneContainer = EcotoneSymfonyContainerFactory::build($ecotoneBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices); - if ($useProductionCache && $cacheHash !== null) { - $this->persistConfigHash($cacheDirectory, $cacheHash); - } + if ($useProductionCache && $cacheHash !== null) { + $this->persistConfigHash($cacheDirectory, $cacheHash); } } - return [$serviceCacheConfiguration, $definitionHolder, $cacheHash]; + return [$ecotoneContainer, $cacheHash]; } private function persistConfigHash(string $cacheDirectory, string $configHash): void diff --git a/packages/Tempest/src/TempestPsrContainerAdapter.php b/packages/Tempest/src/TempestPsrContainerAdapter.php index 2cbb6a34c..d81b60430 100644 --- a/packages/Tempest/src/TempestPsrContainerAdapter.php +++ b/packages/Tempest/src/TempestPsrContainerAdapter.php @@ -5,9 +5,11 @@ namespace Ecotone\Tempest; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use ReflectionClass; use ReflectionException; use Tempest\Container\Container; +use Throwable; /** * licence Apache-2.0 @@ -20,15 +22,25 @@ public function __construct(private Container $container) public function get(string $id): mixed { - return $this->container->get($id); + return $this->container->get($this->mapServiceId($id)); } public function has(string $id): bool { + $id = $this->mapServiceId($id); if ($this->container->has($id)) { return true; } + if ($id === LoggerInterface::class) { + try { + $this->container->get($id); + return true; + } catch (Throwable) { + return false; + } + } + if (! class_exists($id) && ! interface_exists($id)) { return false; } @@ -40,4 +52,9 @@ public function has(string $id): bool return false; } } + + private function mapServiceId(string $id): string + { + return $id === 'logger' ? LoggerInterface::class : $id; + } } From f945b2a3c6c357a1aeddf03ab571b9ebd6574f3a Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 11:06:33 +0200 Subject: [PATCH 09/17] feat: switch LiteApplication to shared Symfony container, drop PHP-DI --- .../PhpDiContainerImplementationTest.php | 29 ----- composer.json | 1 - packages/LiteApplication/composer.json | 3 +- .../src/AutowiringContainer.php | 94 ++++++++++++++++ .../src/EcotoneLiteApplication.php | 71 +++++------- .../LiteApplication/src/LiteDIContainer.php | 63 ----------- .../src/PhpDiContainerImplementation.php | 106 ------------------ 7 files changed, 120 insertions(+), 247 deletions(-) delete mode 100644 Monorepo/CrossModuleTests/Tests/PhpDiContainerImplementationTest.php create mode 100644 packages/LiteApplication/src/AutowiringContainer.php delete mode 100644 packages/LiteApplication/src/LiteDIContainer.php delete mode 100644 packages/LiteApplication/src/PhpDiContainerImplementation.php diff --git a/Monorepo/CrossModuleTests/Tests/PhpDiContainerImplementationTest.php b/Monorepo/CrossModuleTests/Tests/PhpDiContainerImplementationTest.php deleted file mode 100644 index b63367c32..000000000 --- a/Monorepo/CrossModuleTests/Tests/PhpDiContainerImplementationTest.php +++ /dev/null @@ -1,29 +0,0 @@ -toString(); - $container->enableCompilation($cacheDirectory, $containerClass); - $builder->addCompilerPass(new PhpDiContainerImplementation($container)); - $builder->compile(); - $container->build(); - - require_once $cacheDirectory . '/' . $containerClass . '.php'; - return new $containerClass(); - } -} diff --git a/composer.json b/composer.json index bae2d0f26..58c07b45f 100644 --- a/composer.json +++ b/composer.json @@ -160,7 +160,6 @@ "symfony/console": "^6.4|^7.0|^8.0", "symfony/framework-bundle": "^6.4|^7.0|^8.0", "symfony/dependency-injection": "^6.4|^7.0|^8.0", - "php-di/php-di": "^7.0.5", "open-telemetry/sdk": "^1.0.0", "psr/container": "^1.1.1|^2.0.1", "psr/clock": "^1.0", diff --git a/packages/LiteApplication/composer.json b/packages/LiteApplication/composer.json index 490573423..85c0c9977 100644 --- a/packages/LiteApplication/composer.json +++ b/packages/LiteApplication/composer.json @@ -49,8 +49,7 @@ }, "require": { "ecotone/ecotone": "~1.315.0", - "ecotone/jms-converter": "~1.315.0", - "php-di/php-di": "^7.0.5" + "ecotone/jms-converter": "~1.315.0" }, "require-dev": { "phpunit/phpunit": "^10.5|^11.0", diff --git a/packages/LiteApplication/src/AutowiringContainer.php b/packages/LiteApplication/src/AutowiringContainer.php new file mode 100644 index 000000000..0e31f1710 --- /dev/null +++ b/packages/LiteApplication/src/AutowiringContainer.php @@ -0,0 +1,94 @@ +ecotoneContainer = $ecotoneContainer; + } + + public function get(string $id): mixed + { + if ($this->innerContainer->has($id)) { + return $this->innerContainer->get($id); + } + if (isset($this->resolvedObjects[$id])) { + return $this->resolvedObjects[$id]; + } + + return $this->resolvedObjects[$id] = $this->instantiate($id); + } + + public function has(string $id): bool + { + if ($this->innerContainer->has($id)) { + return true; + } + if (! class_exists($id)) { + return false; + } + + return (new ReflectionClass($id))->isInstantiable(); + } + + private function instantiate(string $className): object + { + if (! class_exists($className) || ! (new ReflectionClass($className))->isInstantiable()) { + throw new InvalidArgumentException("Service {$className} is not registered and can not be auto-wired"); + } + + $reflection = new ReflectionClass($className); + $constructor = $reflection->getConstructor(); + if (! $constructor) { + return $reflection->newInstance(); + } + + $arguments = []; + foreach ($constructor->getParameters() as $parameter) { + $type = $parameter->getType(); + if ($type instanceof ReflectionNamedType && ! $type->isBuiltin()) { + $typeName = $type->getName(); + if ($this->innerContainer->has($typeName)) { + $arguments[] = $this->innerContainer->get($typeName); + continue; + } + if ($this->ecotoneContainer?->has($typeName)) { + $arguments[] = $this->ecotoneContainer->get($typeName); + continue; + } + if ($this->has($typeName)) { + $arguments[] = $this->get($typeName); + continue; + } + } + if ($parameter->isDefaultValueAvailable()) { + $arguments[] = $parameter->getDefaultValue(); + continue; + } + + throw new InvalidArgumentException("Can not auto-wire parameter {$parameter->getName()} of {$className}"); + } + + return $reflection->newInstanceArgs($arguments); + } +} diff --git a/packages/LiteApplication/src/EcotoneLiteApplication.php b/packages/LiteApplication/src/EcotoneLiteApplication.php index 5e93e6e08..013ab9a0f 100644 --- a/packages/LiteApplication/src/EcotoneLiteApplication.php +++ b/packages/LiteApplication/src/EcotoneLiteApplication.php @@ -4,19 +4,16 @@ namespace Ecotone\Lite; -use DI\ContainerBuilder as PhpDiContainerBuilder; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; +use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\ConfigurationVariableService; use Ecotone\Messaging\InMemoryConfigurationVariableService; - -use function file_put_contents; - -use Symfony\Component\Uid\Uuid; +use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; /** * licence Apache-2.0 @@ -52,53 +49,42 @@ public static function bootstrap( $serviceConfiguration->getCacheDirectoryPath() . DIRECTORY_SEPARATOR . 'ecotone', $cacheConfiguration ); - $file = $serviceCacheConfiguration->getPath() . '/CompiledContainer.php'; - if ($serviceCacheConfiguration->shouldUseCache() && file_exists($file)) { - $container = require $file; - } else { + + $externalContainer = new AutowiringContainer(InMemoryPSRContainer::createFromAssociativeArray(array_merge($classesToRegister, $objectsToRegister))); + $configurationVariableService = InMemoryConfigurationVariableService::create($configurationVariables); + $runtimeServices = [ + ServiceCacheConfiguration::REFERENCE_NAME => $serviceCacheConfiguration, + ConfigurationVariableService::REFERENCE_NAME => $configurationVariableService, + ]; + + $container = null; + if ($serviceCacheConfiguration->shouldUseCache()) { + $container = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); + } + + if (! $container) { /** @var MessagingSystemConfiguration $messagingConfiguration */ $messagingConfiguration = MessagingSystemConfiguration::prepare( $pathToRootCatalog, - InMemoryConfigurationVariableService::create($configurationVariables), + $configurationVariableService, $serviceConfiguration, ); - - $containerClass = 'CompiledContainer_'.self::hash(Uuid::v7()->toRfc4122()); - $builder = new PhpDiContainerBuilder(); - $builder->useAttributes(false); - $builder->useAutowiring(true); - if ($serviceCacheConfiguration->shouldUseCache()) { - $builder->enableCompilation($serviceCacheConfiguration->getPath(), $containerClass); - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - file_put_contents($file, <<withExternalContainer($externalContainer); $containerBuilder = new ContainerBuilder(); - $messagingConfiguration->withExternalContainer(InMemoryPSRContainer::createFromAssociativeArray(array_merge($classesToRegister, $objectsToRegister))); $containerBuilder->addCompilerPass($messagingConfiguration); $containerBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); - $containerBuilder->addCompilerPass(new PhpDiContainerImplementation($builder, $classesToRegister)); - $containerBuilder->compile(); - - $container = $builder->build(); - } + $containerBuilder->addCompilerPass(new ValidityCheckPass()); - $container->set(ServiceCacheConfiguration::class, $serviceCacheConfiguration); - - $configurationVariableService = InMemoryConfigurationVariableService::create($configurationVariables); - $container->set(ConfigurationVariableService::REFERENCE_NAME, $configurationVariableService); + if ($serviceCacheConfiguration->shouldUseCache()) { + MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); + } - foreach ($objectsToRegister as $referenceName => $object) { - $container->set($referenceName, $object); - } - foreach ($classesToRegister as $referenceName => $object) { - $container->set(PhpDiContainerImplementation::EXTERNAL_PREFIX.$referenceName, $object); + $container = EcotoneSymfonyContainerFactory::build($containerBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices); } + $externalContainer->setEcotoneContainer($container); + return $container->get(ConfiguredMessagingSystem::class); } @@ -111,11 +97,4 @@ public static function boostrap(array $objectsToRegister = [], array $configurat { return self::bootstrap($objectsToRegister, $configurationVariables, $serviceConfiguration, $cacheConfiguration, $pathToRootCatalog); } - - private static function hash($value) - { - $hash = substr(base64_encode(hash('sha256', serialize($value), true)), 0, 7); - - return str_replace(['/', '+'], ['_', '_'], $hash); - } } diff --git a/packages/LiteApplication/src/LiteDIContainer.php b/packages/LiteApplication/src/LiteDIContainer.php deleted file mode 100644 index 2d6135dc7..000000000 --- a/packages/LiteApplication/src/LiteDIContainer.php +++ /dev/null @@ -1,63 +0,0 @@ -getCacheDirectoryPath() . DIRECTORY_SEPARATOR . 'ecotone', - $useCache - ); - - if ($useCache) { - $builder = $builder - ->enableCompilation($serviceCacheConfiguration->getPath()) - /** @TODO verify if using __DIR__ is correct */ - ->writeProxiesToFile(true, __DIR__ . '/ecotone/proxies'); - } - - $this->container = $builder->build(); - $this->container->set(ConfigurationVariableService::REFERENCE_NAME, InMemoryConfigurationVariableService::create($configurationVariables)); - $this->container->set(ServiceCacheConfiguration::class, $serviceCacheConfiguration); - foreach ($classInstancesToRegister as $referenceName => $classInstance) { - $this->container->set($referenceName, $classInstance); - } - } - - public function get($id) - { - return $this->container->get($id); - } - - public function has($id): bool - { - return $this->container->has($id); - } - - public function set(string $id, object $service) - { - $this->container->set($id, $service); - } - - public function resolve(string $referenceName): Type - { - return Type::create($referenceName); - } -} diff --git a/packages/LiteApplication/src/PhpDiContainerImplementation.php b/packages/LiteApplication/src/PhpDiContainerImplementation.php deleted file mode 100644 index ecf2e45a4..000000000 --- a/packages/LiteApplication/src/PhpDiContainerImplementation.php +++ /dev/null @@ -1,106 +0,0 @@ -getDefinitions(); - foreach ($definitions as $id => $definition) { - $phpDiDefinitions[$id] = $this->resolveArgument($definition); - } - foreach ($this->classesToRegister as $id => $class) { - if (! isset($phpDiDefinitions[$id])) { - $phpDiDefinitions[$id] = \DI\get(self::EXTERNAL_PREFIX . $id); - } - } - - $this->containerBuilder->addDefinitions($phpDiDefinitions); - } - - private function resolveArgument($argument): mixed - { - if ($argument instanceof DefinedObject) { - $argument = $argument->getDefinition(); - } - if ($argument instanceof AttributeDefinition) { - $argument = DefinitionHelper::resolvePotentialComplexAttribute($argument); - } - if ($argument instanceof Definition) { - return $this->convertDefinition($argument); - } elseif (is_array($argument)) { - $resolvedArguments = []; - foreach ($argument as $index => $value) { - $resolvedArguments[$index] = $this->resolveArgument($value); - } - return $resolvedArguments; - } elseif ($argument instanceof Reference) { - if ($argument->getInvalidBehavior() === ContainerImplementation::NULL_ON_INVALID_REFERENCE) { - return \DI\factory(function (ContainerInterface $c, string $id) { - return $c->has($id) ? $c->get($id) : null; - })->parameter('id', $argument->getId()); - } else { - return \DI\get($argument->getId()); - } - } else { - return $argument; - } - } - - private function convertDefinition(Definition $definition) - { - if ($definition->hasFactory()) { - return $this->convertFactory($definition); - } - $phpdi = \DI\create($definition->getClassName()) - ->constructor(...$this->resolveArgument($definition->getArguments())); - foreach ($definition->getMethodCalls() as $methodCall) { - $phpdi->method($methodCall->getMethodName(), ...$this->resolveArgument($methodCall->getArguments())); - } - return $phpdi; - } - - private function convertFactory(Definition $definition) - { - $factory = \DI\factory($definition->getFactory()); - [$class, $method] = $definition->getFactory(); - // Transform indexed factory to named factory - $reflector = new ReflectionMethod($class, $method); - $parameters = $reflector->getParameters(); - foreach ($definition->getArguments() as $index => $argument) { - $p = $parameters[$index] ?? null; - $factory->parameter($p ? $p->name : $index, $this->resolveArgument($argument)); - } - return $factory; - } - -} From 85a57693bd2d679db0c51d213e15b1259003126f Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 11:16:55 +0200 Subject: [PATCH 10/17] refactor: remove in-memory and PHP-DI container implementations --- .../Tests/SimpleSymfonyKernel.php | 47 ------- .../Tests/SymfonyContainerAdapterTest.php | 31 ---- .../Lite/InMemoryContainerImplementation.php | 119 ---------------- .../src/Lite/LazyInMemoryContainer.php | 102 -------------- .../Config/Container/ContainerConfig.php | 15 +- .../SymfonyContainerImplementation.php | 24 +++- .../Ecotone/src/Test/ComponentTestBuilder.php | 15 +- .../Lite/LiteContainerImplementationTest.php | 39 ------ .../AliasExternalReferenceForTesting.php | 4 +- .../SymfonyContainerAdapter.php | 132 ------------------ 10 files changed, 41 insertions(+), 487 deletions(-) delete mode 100644 Monorepo/CrossModuleTests/Tests/SimpleSymfonyKernel.php delete mode 100644 Monorepo/CrossModuleTests/Tests/SymfonyContainerAdapterTest.php delete mode 100644 packages/Ecotone/src/Lite/InMemoryContainerImplementation.php delete mode 100644 packages/Ecotone/src/Lite/LazyInMemoryContainer.php delete mode 100644 packages/Ecotone/tests/Lite/LiteContainerImplementationTest.php delete mode 100644 packages/Symfony/DependencyInjection/SymfonyContainerAdapter.php diff --git a/Monorepo/CrossModuleTests/Tests/SimpleSymfonyKernel.php b/Monorepo/CrossModuleTests/Tests/SimpleSymfonyKernel.php deleted file mode 100644 index 9cd4853c1..000000000 --- a/Monorepo/CrossModuleTests/Tests/SimpleSymfonyKernel.php +++ /dev/null @@ -1,47 +0,0 @@ -cacheKey = $cacheKey ?? Uuid::uuid4()->toString(); - parent::__construct('prod', false); - } - - public function registerContainerConfiguration(LoaderInterface $loader): void - { - $loader->load(function (\Symfony\Component\DependencyInjection\ContainerBuilder $container) { - $this->ecotoneBuilder->addCompilerPass(new SymfonyContainerAdapter($container)); - $this->ecotoneBuilder->compile(); - }); - } - - public function getCacheDir(): string - { - return __DIR__ . "/cache/symfony_{$this->cacheKey}"; - } - - public function getLogDir(): string - { - return $this->getCacheDir() . "/log"; - } - - public function registerBundles(): iterable - { - return []; - } - -} \ No newline at end of file diff --git a/Monorepo/CrossModuleTests/Tests/SymfonyContainerAdapterTest.php b/Monorepo/CrossModuleTests/Tests/SymfonyContainerAdapterTest.php deleted file mode 100644 index 8b629bf40..000000000 --- a/Monorepo/CrossModuleTests/Tests/SymfonyContainerAdapterTest.php +++ /dev/null @@ -1,31 +0,0 @@ -shutdown(); - self::$bootedKernel = null; - } - } - - protected static function getContainerFrom(ContainerBuilder $builder, ?ContainerInterface $externalContainer = null): ContainerInterface - { - self::$bootedKernel = new SimpleSymfonyKernel($builder); - self::$bootedKernel->boot(); - - return self::$bootedKernel->getContainer(); - } -} \ No newline at end of file diff --git a/packages/Ecotone/src/Lite/InMemoryContainerImplementation.php b/packages/Ecotone/src/Lite/InMemoryContainerImplementation.php deleted file mode 100644 index 56424f7f5..000000000 --- a/packages/Ecotone/src/Lite/InMemoryContainerImplementation.php +++ /dev/null @@ -1,119 +0,0 @@ -container->set(ContainerInterface::class, $this->container); - foreach ($builder->getDefinitions() as $id => $definition) { - if (! $this->container->has($id)) { - $object = $this->resolveArgument($definition, $builder); - $this->container->set($id, $object); - } - } - } - - private function resolveArgument(mixed $argument, ContainerBuilder $builder): mixed - { - if (is_array($argument)) { - return array_map(fn ($argument) => $this->resolveArgument($argument, $builder), $argument); - } elseif ($argument instanceof Definition) { - $object = $this->instantiateDefinition($argument, $builder); - foreach ($argument->getMethodCalls() as $methodCall) { - $object->{$methodCall->getMethodName()}(...$this->resolveArgument($methodCall->getArguments(), $builder)); - } - return $object; - } elseif ($argument instanceof Reference) { - return $this->resolveReference($argument, $builder); - } else { - if (is_object($argument) && ! ($argument instanceof DefinedObject)) { - if (! str_starts_with(get_class($argument), 'Test\\')) { - // We accept only not-dumpable instances from the 'Test\' namespace - throw new InvalidArgumentException('Argument is not a self defined object: ' . get_class($argument)); - } - } - return $argument; - } - } - - private function instantiateDefinition(Definition $definition, ContainerBuilder $builder): mixed - { - if ($definition instanceof DefinedObjectWrapper) { - return $definition->instance(); - } - - $arguments = $this->resolveArgument($definition->getArguments(), $builder); - if ($definition->hasFactory()) { - $factory = $definition->getFactory(); - if (method_exists($factory[0], $factory[1]) && (new ReflectionMethod($factory[0], $factory[1]))->isStatic()) { - // static call - return $factory(...$arguments); - } else { - // method call from a service instance - $service = $this->resolveReference(new Reference($factory[0]), $builder); - return $service->{$factory[1]}(...$arguments); - } - } else { - $class = $definition->getClassName(); - return new $class(...$arguments); - } - } - - private function resolveReference(Reference $reference, ContainerBuilder $builder): mixed - { - $id = $reference->getId(); - if ($this->container->has($id)) { - return $this->container->get($id); - } - if ($builder->has($id)) { - $object = $this->resolveArgument($builder->getDefinition($id), $builder); - $this->container->set($id, $object); - - return $this->container->get($reference->getId()); - } - if ($this->externalContainer?->has($id)) { - return $this->externalContainer->get($id); - } - if ($this->externalContainer?->has(self::ALIAS_PREFIX . $id)) { - return $this->externalContainer->get(self::ALIAS_PREFIX . $id); - } - if ($reference->getInvalidBehavior() === self::NULL_ON_INVALID_REFERENCE) { - return null; - } - throw new InvalidArgumentException("Reference {$id} was not found in definitions"); - } - - -} diff --git a/packages/Ecotone/src/Lite/LazyInMemoryContainer.php b/packages/Ecotone/src/Lite/LazyInMemoryContainer.php deleted file mode 100644 index 1b3219fc8..000000000 --- a/packages/Ecotone/src/Lite/LazyInMemoryContainer.php +++ /dev/null @@ -1,102 +0,0 @@ -resolvedObjects[ContainerInterface::class] = $this; - } - - public function get(string $id): mixed - { - return $this->resolveReference(new Reference($id)); - } - - public function has(string $id): bool - { - return isset($this->definitions[$id]) || isset($this->resolvedObjects[$id]) || ($this->externalContainer?->has($id) ?? false); - } - - public function set(string $id, mixed $object): void - { - $this->resolvedObjects[$id] = $object; - } - - private function resolveArgument(mixed $argument): mixed - { - if (is_array($argument)) { - return array_map(fn ($a) => $this->resolveArgument($a), $argument); - } elseif ($argument instanceof Definition) { - $object = $this->instantiateDefinition($argument); - foreach ($argument->getMethodCalls() as $methodCall) { - $object->{$methodCall->getMethodName()}(...$this->resolveArgument($methodCall->getArguments())); - } - return $object; - } elseif ($argument instanceof Reference) { - return $this->resolveReference($argument); - } elseif ($argument instanceof DefinedObject) { - return $this->resolveArgument($argument->getDefinition()); - } else { - return $argument; - } - } - private function instantiateDefinition(Definition $definition): mixed - { - if ($definition instanceof DefinedObjectWrapper) { - return $definition->instance(); - } - - $arguments = $this->resolveArgument($definition->getArguments()); - if ($definition->hasFactory()) { - $factory = $definition->getFactory(); - if (method_exists($factory[0], $factory[1]) && (new ReflectionMethod($factory[0], $factory[1]))->isStatic()) { - // static call - return $factory(...$arguments); - } else { - // method call from a service instance - $service = $this->resolveReference(new Reference($factory[0])); - return $service->{$factory[1]}(...$arguments); - } - } else { - $class = $definition->getClassName(); - return new $class(...$arguments); - } - } - - private function resolveReference(Reference $reference): mixed - { - $id = $reference->getId(); - if (isset($this->resolvedObjects[$id])) { - return $this->resolvedObjects[$id]; - } - if (isset($this->definitions[$id])) { - return $this->resolvedObjects[$id] = $this->resolveArgument($this->definitions[$id]); - } - if ($this->externalContainer?->has($id)) { - return $this->resolvedObjects[$id] = $this->externalContainer->get($id); - } - if ($this->externalContainer?->has(InMemoryContainerImplementation::ALIAS_PREFIX . $id)) { - return $this->externalContainer->get(InMemoryContainerImplementation::ALIAS_PREFIX . $id); - } - if ($reference->getInvalidBehavior() === ContainerImplementation::NULL_ON_INVALID_REFERENCE) { - return null; - } - throw new InvalidArgumentException("Reference {$id} was not found in definitions"); - } -} diff --git a/packages/Ecotone/src/Messaging/Config/Container/ContainerConfig.php b/packages/Ecotone/src/Messaging/Config/Container/ContainerConfig.php index d2f7009da..3e6446e89 100644 --- a/packages/Ecotone/src/Messaging/Config/Container/ContainerConfig.php +++ b/packages/Ecotone/src/Messaging/Config/Container/ContainerConfig.php @@ -2,7 +2,6 @@ namespace Ecotone\Messaging\Config\Container; -use Ecotone\Lite\LazyInMemoryContainer; use Ecotone\Messaging\Config\Configuration; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; use Ecotone\Messaging\Config\Container\Compiler\ContainerDefinitionsHolder; @@ -12,6 +11,7 @@ use Ecotone\Messaging\ConfigurationVariableService; use Ecotone\Messaging\Handler\Gateway\ProxyFactory; use Ecotone\Messaging\InMemoryConfigurationVariableService; +use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use Psr\Container\ContainerInterface; /** @@ -29,10 +29,15 @@ public static function buildMessagingSystemInMemoryContainer( $containerBuilder->addCompilerPass($configuration); $containerBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); $containerBuilder->addCompilerPass(new ValidityCheckPass()); - $containerBuilder->compile(); - $container = new LazyInMemoryContainer($containerBuilder->getDefinitions(), $externalContainer); - $container->set(ConfigurationVariableService::REFERENCE_NAME, $configurationVariableService ?? InMemoryConfigurationVariableService::createEmpty()); - $container->set(ProxyFactory::class, $proxyFactory ?? new ProxyFactory(ServiceCacheConfiguration::noCache())); + $container = EcotoneSymfonyContainerFactory::build( + $containerBuilder, + ServiceCacheConfiguration::noCache(), + $externalContainer, + [ + ConfigurationVariableService::REFERENCE_NAME => $configurationVariableService ?? InMemoryConfigurationVariableService::createEmpty(), + ProxyFactory::class => $proxyFactory ?? new ProxyFactory(ServiceCacheConfiguration::noCache()), + ], + ); return $container->get(ConfiguredMessagingSystem::class); } diff --git a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php index a9fe718c6..8653a8876 100644 --- a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php +++ b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php @@ -64,6 +64,9 @@ public function process(ContainerBuilder $builder): void $this->definitions = $builder->getDefinitions(); foreach ($this->definitions as $id => $definition) { + if (in_array($id, $this->syntheticServiceIds, true)) { + continue; + } $symfonyDefinition = $this->resolveArgument($definition); if ($symfonyDefinition instanceof SymfonyReference) { $this->symfonyBuilder->setAlias(ServiceIdNormalizer::normalize($id), (string) $symfonyDefinition)->setPublic(true); @@ -84,8 +87,13 @@ private function registerSyntheticService(string $id, string $className): void private function resolveArgument($argument): mixed { - if ($argument instanceof DefinedObjectWrapper && $this->preserveRuntimeInstances) { - return $this->convertRuntimeInstanceDefinition($argument); + if ($this->preserveRuntimeInstances) { + if ($argument instanceof DefinedObjectWrapper) { + return $this->convertRuntimeInstanceDefinition($argument); + } + if ($argument instanceof DefinedObject) { + return $this->runtimeInstanceDefinition($argument); + } } if ($argument instanceof DefinedObject) { $argument = $argument->getDefinition(); @@ -138,12 +146,18 @@ private function registerExternalReferenceDelegate(string $id, int $invalidBehav return $delegateId; } + private function runtimeInstanceDefinition(object $instance): SymfonyDefinition + { + return (new SymfonyDefinition(get_class($instance))) + ->setFactory([RuntimeInstanceProvider::class, 'provide']) + ->setArguments([$instance]) + ->setPublic(true); + } + private function convertRuntimeInstanceDefinition(DefinedObjectWrapper $definedObjectWrapper): SymfonyDefinition { $instance = $definedObjectWrapper->instance(); - $sfDefinition = (new SymfonyDefinition(get_class($instance))) - ->setFactory([RuntimeInstanceProvider::class, 'provide']) - ->setArguments([$instance]); + $sfDefinition = $this->runtimeInstanceDefinition($instance); foreach ($definedObjectWrapper->getMethodCalls() as $methodCall) { $sfDefinition->addMethodCall( $methodCall->getMethodName(), diff --git a/packages/Ecotone/src/Test/ComponentTestBuilder.php b/packages/Ecotone/src/Test/ComponentTestBuilder.php index 1df4b6eec..63d61fd41 100644 --- a/packages/Ecotone/src/Test/ComponentTestBuilder.php +++ b/packages/Ecotone/src/Test/ComponentTestBuilder.php @@ -3,7 +3,6 @@ namespace Ecotone\Test; use Ecotone\AnnotationFinder\FileSystem\FileSystemAnnotationFinder; -use Ecotone\Lite\InMemoryContainerImplementation; use Ecotone\Lite\InMemoryPSRContainer; use Ecotone\Lite\Test\FlowTestSupport; use Ecotone\Lite\Test\MessagingTestSupport; @@ -30,12 +29,16 @@ use Ecotone\Modelling\CommandBus; use Ecotone\Modelling\EventBus; use Ecotone\Modelling\QueryBus; +use Ecotone\SymfonyContainer\EcotoneContainer; +use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; /** * licence Apache-2.0 */ class ComponentTestBuilder { + private ?EcotoneContainer $builtContainer = null; + private function __construct( private InMemoryPSRContainer $container, private MessagingSystemConfiguration $messagingSystemConfiguration @@ -163,11 +166,13 @@ public function build(): FlowTestSupport $containerBuilder = new ContainerBuilder(); $containerBuilder->addCompilerPass($this->messagingSystemConfiguration); $containerBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); - $containerBuilder->addCompilerPass(new InMemoryContainerImplementation($this->container)); - $containerBuilder->compile(); + $this->builtContainer = EcotoneSymfonyContainerFactory::build($containerBuilder, ServiceCacheConfiguration::noCache(), $this->container); + foreach ($this->builtContainer->getServiceIds() as $serviceId) { + $this->builtContainer->get($serviceId); + } /** @var ConfiguredMessagingSystem $configuredMessagingSystem */ - $configuredMessagingSystem = $this->container->get(ConfiguredMessagingSystem::class); + $configuredMessagingSystem = $this->builtContainer->get(ConfiguredMessagingSystem::class); return new FlowTestSupport( $configuredMessagingSystem->getGatewayByName(CommandBus::class), @@ -183,6 +188,6 @@ public function build(): FlowTestSupport public function getGatewayByName(string $name) { - return $this->container->get($name); + return ($this->builtContainer ?? $this->container)->get($name); } } diff --git a/packages/Ecotone/tests/Lite/LiteContainerImplementationTest.php b/packages/Ecotone/tests/Lite/LiteContainerImplementationTest.php deleted file mode 100644 index 1e7367c7f..000000000 --- a/packages/Ecotone/tests/Lite/LiteContainerImplementationTest.php +++ /dev/null @@ -1,39 +0,0 @@ -addCompilerPass(new InMemoryContainerImplementation($container, $externalContainer)); - $builder->compile(); - return $container; - } - - public function test_it_replace_provided_dependencies(): void - { - $logger = StubLogger::create(); - $externalContainer = InMemoryPSRContainer::createFromAssociativeArray([ - 'externallyDefined' => $logger, - ]); - $container = self::buildContainerFromDefinitions(['aReference' => new Reference('externallyDefined')], $externalContainer); - - self::assertSame($logger, $container->get('aReference')); - } -} diff --git a/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php b/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php index d70d8097a..b5bd4559f 100644 --- a/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php +++ b/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php @@ -2,7 +2,7 @@ namespace Ecotone\SymfonyBundle\DependencyInjection\Compiler; -use Ecotone\Lite\InMemoryContainerImplementation; +use Ecotone\SymfonyContainer\ExternalReferenceResolver; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -18,7 +18,7 @@ public function process(ContainerBuilder $container): void } foreach ($container->getParameter('ecotone.external_references') as $id) { - $aliasId = InMemoryContainerImplementation::ALIAS_PREFIX . $id; + $aliasId = ExternalReferenceResolver::TESTING_ALIAS_PREFIX . $id; if ($container->has($aliasId)) { continue; } diff --git a/packages/Symfony/DependencyInjection/SymfonyContainerAdapter.php b/packages/Symfony/DependencyInjection/SymfonyContainerAdapter.php deleted file mode 100644 index 16dd003ad..000000000 --- a/packages/Symfony/DependencyInjection/SymfonyContainerAdapter.php +++ /dev/null @@ -1,132 +0,0 @@ - \Symfony\Component\DependencyInjection\ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, - ContainerImplementation::NULL_ON_INVALID_REFERENCE => \Symfony\Component\DependencyInjection\ContainerInterface::NULL_ON_INVALID_REFERENCE, - ]; - /** - * @var Definition[]|Reference[] $definitions - */ - private array $definitions; - - private array $externalReferences = []; - public function __construct(private SymfonyContainerBuilder $symfonyBuilder) - { - } - - public function process(ContainerBuilder $builder): void - { - $this->symfonyBuilder->setAlias(ContainerInterface::class, 'service_container'); - $this->symfonyBuilder->register(ConfiguredMessagingSystem::class, MessagingSystemContainer::class); - - $this->definitions = $builder->getDefinitions(); - foreach ($this->definitions as $id => $definition) { - $symfonyDefinition = $this->resolveArgument($definition); - if ($symfonyDefinition instanceof SymfonyReference) { - $this->symfonyBuilder->setAlias($id, (string)$symfonyDefinition)->setPublic(true); - } else { - $this->symfonyBuilder->setDefinition($id, $symfonyDefinition); - } - } - $this->symfonyBuilder->setParameter('ecotone.external_references', $this->externalReferences); - } - - public function getExternalReferences(): array - { - return $this->externalReferences; - } - - private function resolveArgument($argument): mixed - { - if ($argument instanceof DefinedObject) { - $argument = $argument->getDefinition(); - } - if ($argument instanceof AttributeDefinition) { - $argument = DefinitionHelper::resolvePotentialComplexAttribute($argument); - } - if ($argument instanceof Definition) { - return $this->convertDefinition($argument); - } elseif (is_array($argument)) { - $resolvedArguments = []; - foreach ($argument as $index => $value) { - $resolvedArguments[$index] = $this->resolveArgument($value); - } - return $resolvedArguments; - } elseif ($argument instanceof Reference) { - if (! isset($this->definitions[$argument->getId()])) { - $this->externalReferences[$argument->getId()] = $argument->getId(); - } - return new SymfonyReference($argument->getId(), self::$invalidBehaviorMap[$argument->getInvalidBehavior()]); - } else { - return $argument; - } - } - - private function convertDefinition(Definition $ecotoneDefinition) - { - $sfDefinition = new SymfonyDefinition( - $ecotoneDefinition->getClassName(), - $this->normalizeNamedArgument($this->resolveArgument($ecotoneDefinition->getArguments())) - ); - if ($ecotoneDefinition->hasFactory()) { - $sfDefinition->setFactory($this->resolveFactoryArgument($ecotoneDefinition->getFactory())); - } - foreach ($ecotoneDefinition->getMethodCalls() as $methodCall) { - $sfDefinition->addMethodCall( - $methodCall->getMethodName(), - $this->normalizeNamedArgument($this->resolveArgument($methodCall->getArguments())) - ); - } - return $sfDefinition->setPublic(true); - } - - private function normalizeNamedArgument(array $arguments): array - { - foreach ($arguments as $index => $argument) { - if (is_string($index)) { - $arguments['$'.$index] = $argument; - unset($arguments[$index]); - } - } - return $arguments; - } - - private function resolveFactoryArgument(array $factory): array - { - if (method_exists($factory[0], $factory[1]) && (new ReflectionMethod($factory[0], $factory[1]))->isStatic()) { - // static call - return $factory; - } else { - // method call from a service instance - return [new SymfonyReference($factory[0]), $factory[1]]; - } - } -} From 08fe241dde1f62b43e2f475d3949db36dc96c38f Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 12:12:30 +0200 Subject: [PATCH 11/17] refactor: expose typed container accessors for integration metadata --- .../src/SymfonyContainer/EcotoneContainer.php | 40 ++++++++++++++++++ .../EcotoneSymfonyContainerFactory.php | 2 + .../SymfonyContainerImplementation.php | 1 + .../EcotoneLiteCachedContainerTest.php | 2 +- ...cotoneSymfonyContainerFactoryCacheTest.php | 13 ++++++ .../SymfonyContainerImplementationTest.php | 20 +++++++++ packages/Laravel/src/EcotoneProvider.php | 4 +- .../DependencyInjection/EcotoneExtension.php | 9 +--- .../src/MessagingSystemInitializer.php | 41 ++----------------- 9 files changed, 84 insertions(+), 48 deletions(-) diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php index 35d0018b0..495f7e94c 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php @@ -51,4 +51,44 @@ public function getServiceIds(): array { return $this->container->getServiceIds(); } + + /** + * @return string[] + */ + public function getDefinedServiceIds(): array + { + return array_values(array_filter( + $this->getServiceIds(), + fn (string $serviceId) => ! str_ends_with($serviceId, SymfonyContainerImplementation::EXTERNAL_DELEGATE_SUFFIX) + && ! str_ends_with($serviceId, SymfonyContainerImplementation::NULLABLE_EXTERNAL_DELEGATE_SUFFIX) + && $serviceId !== SymfonyContainerImplementation::EXTERNAL_CONTAINER_ID + && $serviceId !== 'service_container' + && $serviceId !== ContainerInterface::class, + )); + } + + /** + * @return \Ecotone\Messaging\Config\ConsoleCommandConfiguration[] + */ + public function getRegisteredConsoleCommands(): array + { + return unserialize($this->container->getParameter(SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER)); + } + + /** + * @return string[] + */ + public function getExternalReferenceIds(): array + { + return $this->container->getParameter(SymfonyContainerImplementation::EXTERNAL_REFERENCES_PARAMETER); + } + + public function getConfigHash(): ?string + { + if (! $this->container->hasParameter(SymfonyContainerImplementation::CONFIG_HASH_PARAMETER)) { + return null; + } + + return $this->container->getParameter(SymfonyContainerImplementation::CONFIG_HASH_PARAMETER); + } } diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php index 49bd1c1bd..13061b096 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -26,6 +26,7 @@ public static function build( ServiceCacheConfiguration $serviceCacheConfiguration, ?ContainerInterface $externalContainer = null, array $runtimeServices = [], + ?string $configHash = null, ): EcotoneContainer { $symfonyBuilder = new SymfonyContainerBuilder(); $implementation = new SymfonyContainerImplementation( @@ -39,6 +40,7 @@ public static function build( SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER, serialize($definitionsHolder->getRegisteredCommands()), ); + $symfonyBuilder->setParameter(SymfonyContainerImplementation::CONFIG_HASH_PARAMETER, $configHash); if ($serviceCacheConfiguration->shouldUseCache()) { $symfonyBuilder->compile(); diff --git a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php index 8653a8876..588228ef4 100644 --- a/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php +++ b/packages/Ecotone/src/SymfonyContainer/SymfonyContainerImplementation.php @@ -34,6 +34,7 @@ class SymfonyContainerImplementation implements ContainerImplementation public const EXTERNAL_CONTAINER_ID = 'ecotone.external_container'; public const EXTERNAL_REFERENCES_PARAMETER = 'ecotone.external_references'; public const CONSOLE_COMMANDS_PARAMETER = 'ecotone.console_commands'; + public const CONFIG_HASH_PARAMETER = 'ecotone.config_hash'; public const EXTERNAL_DELEGATE_SUFFIX = '.ecotone.external'; public const NULLABLE_EXTERNAL_DELEGATE_SUFFIX = '.ecotone.external.nullable'; diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php index 0373a7439..4cbe385a0 100644 --- a/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php @@ -59,7 +59,7 @@ public function test_registered_console_commands_are_available_as_container_para $container = $messagingSystem->getServiceFromContainer(ContainerInterface::class); - $consoleCommands = unserialize($container->getParameter('ecotone.console_commands')); + $consoleCommands = $container->getRegisteredConsoleCommands(); self::assertContains('doSomething', array_map(fn ($command) => $command->getName(), $consoleCommands)); } } diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php index 2c38b5a88..0543395c1 100644 --- a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php @@ -52,6 +52,19 @@ public function test_it_rebuilds_fresh_container_when_cache_files_are_removed_in self::assertSame('second', $rebuilt->get('aService')->name); } + public function test_it_exposes_config_hash_on_built_and_cache_loaded_container(): void + { + $cacheConfiguration = new ServiceCacheConfiguration($this->uniqueCacheDirectory(), true); + $builder = new ContainerBuilder(); + $builder->replace('aService', new Definition(ACachedService::class, ['someName'])); + + $built = EcotoneSymfonyContainerFactory::build($builder, $cacheConfiguration, configHash: 'abc123'); + self::assertSame('abc123', $built->getConfigHash()); + + $loaded = EcotoneSymfonyContainerFactory::loadCached($cacheConfiguration); + self::assertSame('abc123', $loaded->getConfigHash()); + } + public function test_it_returns_null_when_no_dumped_container_exists(): void { $cacheConfiguration = new ServiceCacheConfiguration($this->uniqueCacheDirectory(), true); diff --git a/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php index 698b73ee1..3b600313d 100644 --- a/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php @@ -99,6 +99,26 @@ public function has(string $id): bool self::assertSame($container->get('def1')->dependency, $container->get('def2')->dependency); } + public function test_it_exposes_defined_service_ids_without_internal_ids(): void + { + $container = self::buildContainerFromDefinitions([ + 'def1' => new Definition(WithNoDependencies::class), + 'def2' => new Definition(WithReferenceToUnknown::class, [new Reference('externalService', ContainerImplementation::NULL_ON_INVALID_REFERENCE)]), + 'def3' => new Definition(WithReferenceToUnknown::class, [new Reference('anotherExternalService')]), + ]); + + $definedServiceIds = $container->getDefinedServiceIds(); + + self::assertContains('def1', $definedServiceIds); + self::assertContains('def2', $definedServiceIds); + self::assertNotContains('service_container', $definedServiceIds); + self::assertNotContains('ecotone.external_container', $definedServiceIds); + self::assertNotContains(ContainerInterface::class, $definedServiceIds); + foreach ($definedServiceIds as $serviceId) { + self::assertStringNotContainsString('.ecotone.external', $serviceId); + } + } + public function test_it_resolves_references_from_external_container(): void { $logger = StubLogger::create(); diff --git a/packages/Laravel/src/EcotoneProvider.php b/packages/Laravel/src/EcotoneProvider.php index 5b4e74e8f..66a56d955 100644 --- a/packages/Laravel/src/EcotoneProvider.php +++ b/packages/Laravel/src/EcotoneProvider.php @@ -17,7 +17,6 @@ use Ecotone\Messaging\Gateway\ConsoleCommandRunner; use Ecotone\Messaging\Handler\Recoverability\RetryTemplateBuilder; use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; -use Ecotone\SymfonyContainer\SymfonyContainerImplementation; use Illuminate\Console\Events\CommandFinished; use Illuminate\Foundation\Console\ClosureCommand; use Illuminate\Support\Facades\App; @@ -125,8 +124,7 @@ function () { ); if ($this->app->runningInConsole()) { - $registeredCommands = unserialize($container->getParameter(SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER)); - foreach ($registeredCommands as $oneTimeCommandConfiguration) { + foreach ($container->getRegisteredConsoleCommands() as $oneTimeCommandConfiguration) { $commandName = $oneTimeCommandConfiguration->getName(); foreach ($oneTimeCommandConfiguration->getParameters() as $parameter) { diff --git a/packages/Symfony/DependencyInjection/EcotoneExtension.php b/packages/Symfony/DependencyInjection/EcotoneExtension.php index 8a0b1fc3b..89adc9e2c 100644 --- a/packages/Symfony/DependencyInjection/EcotoneExtension.php +++ b/packages/Symfony/DependencyInjection/EcotoneExtension.php @@ -18,7 +18,6 @@ use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use Ecotone\SymfonyContainer\ExternalReferenceResolver; use Ecotone\SymfonyContainer\RuntimeInstanceProvider; -use Ecotone\SymfonyContainer\SymfonyContainerImplementation; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -151,13 +150,9 @@ public function load(array $configs, ContainerBuilder $container): void ->addTag('monolog.logger', ['channel' => 'ecotone']) ->setPublic(true); - $container->setParameter( - 'ecotone.external_references', - $ecotoneContainer->getParameter(SymfonyContainerImplementation::EXTERNAL_REFERENCES_PARAMETER), - ); + $container->setParameter('ecotone.external_references', $ecotoneContainer->getExternalReferenceIds()); - $registeredCommands = unserialize($ecotoneContainer->getParameter(SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER)); - foreach ($registeredCommands as $oneTimeCommandConfiguration) { + foreach ($ecotoneContainer->getRegisteredConsoleCommands() as $oneTimeCommandConfiguration) { $definition = new Definition(); $definition->setClass(MessagingEntrypointCommand::class); $definition->addArgument($oneTimeCommandConfiguration->getName()); diff --git a/packages/Tempest/src/MessagingSystemInitializer.php b/packages/Tempest/src/MessagingSystemInitializer.php index b66f1fda9..5a7afc6c8 100644 --- a/packages/Tempest/src/MessagingSystemInitializer.php +++ b/packages/Tempest/src/MessagingSystemInitializer.php @@ -16,7 +16,6 @@ use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\ConfigurationVariableService; use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; -use Ecotone\SymfonyContainer\SymfonyContainerImplementation; use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; @@ -31,8 +30,6 @@ final class MessagingSystemInitializer implements Initializer { public const MESSAGING_SYSTEM_FILE_NAME = 'messaging_system'; - private const CONFIG_HASH_FILE_NAME = 'messaging_system_hash'; - private static ?array $registeredCommands = null; private static ?string $configHash = null; @@ -80,18 +77,11 @@ public function initialize(Container $container): ConfiguredMessagingSystem $container, ); - self::$registeredCommands = unserialize($ecotoneContainer->getParameter(SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER)); + self::$registeredCommands = $ecotoneContainer->getRegisteredConsoleCommands(); self::$configHash = $configHash; self::$proxyDirectory = $cacheDirectory . DIRECTORY_SEPARATOR . 'console_proxies'; - EcotoneServiceInitializer::markCompiled(array_filter( - $ecotoneContainer->getServiceIds(), - fn (string $serviceId) => ! str_ends_with($serviceId, SymfonyContainerImplementation::EXTERNAL_DELEGATE_SUFFIX) - && ! str_ends_with($serviceId, SymfonyContainerImplementation::NULLABLE_EXTERNAL_DELEGATE_SUFFIX) - && $serviceId !== SymfonyContainerImplementation::EXTERNAL_CONTAINER_ID - && $serviceId !== 'service_container' - && $serviceId !== \Psr\Container\ContainerInterface::class, - )); + EcotoneServiceInitializer::markCompiled($ecotoneContainer->getDefinedServiceIds()); return $ecotoneContainer->get(ConfiguredMessagingSystem::class); } @@ -181,7 +171,7 @@ private function prepareFromCache( $ecotoneContainer = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); if ($ecotoneContainer) { - return [$ecotoneContainer, $this->readPersistedConfigHash($cacheDirectory)]; + return [$ecotoneContainer, $ecotoneContainer->getConfigHash()]; } } @@ -223,32 +213,9 @@ private function prepareFromCache( $ecotoneBuilder->addCompilerPass(new ValidityCheckPass()); MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - $ecotoneContainer = EcotoneSymfonyContainerFactory::build($ecotoneBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices); - - if ($useProductionCache && $cacheHash !== null) { - $this->persistConfigHash($cacheDirectory, $cacheHash); - } + $ecotoneContainer = EcotoneSymfonyContainerFactory::build($ecotoneBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices, $cacheHash); } return [$ecotoneContainer, $cacheHash]; } - - private function persistConfigHash(string $cacheDirectory, string $configHash): void - { - $hashFile = $cacheDirectory . DIRECTORY_SEPARATOR . self::CONFIG_HASH_FILE_NAME; - file_put_contents($hashFile, $configHash); - } - - private function readPersistedConfigHash(string $cacheDirectory): ?string - { - $hashFile = $cacheDirectory . DIRECTORY_SEPARATOR . self::CONFIG_HASH_FILE_NAME; - - if (! file_exists($hashFile)) { - return null; - } - - $hash = file_get_contents($hashFile); - - return $hash !== false && $hash !== '' ? $hash : null; - } } From 35c49880b95b5432e7287f62ac819662c16deb15 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 12:17:28 +0200 Subject: [PATCH 12/17] refactor: add bootstrap entry point consolidating cache-or-build flow --- packages/Ecotone/src/Lite/EcotoneLite.php | 50 +++++++------------ .../EcotoneSymfonyContainerFactory.php | 41 +++++++++++++++ ...cotoneSymfonyContainerFactoryCacheTest.php | 28 +++++++++++ packages/Laravel/src/EcotoneProvider.php | 23 +++------ .../src/EcotoneLiteApplication.php | 45 ++++++----------- .../src/MessagingSystemInitializer.php | 23 +++------ 6 files changed, 118 insertions(+), 92 deletions(-) diff --git a/packages/Ecotone/src/Lite/EcotoneLite.php b/packages/Ecotone/src/Lite/EcotoneLite.php index cdd827eac..2dbb6fe34 100644 --- a/packages/Ecotone/src/Lite/EcotoneLite.php +++ b/packages/Ecotone/src/Lite/EcotoneLite.php @@ -15,9 +15,6 @@ use Ecotone\Lite\Test\TestConfiguration; use Ecotone\Messaging\Channel\MessageChannelBuilder; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; -use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; -use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; -use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceCacheConfiguration; @@ -221,37 +218,24 @@ private static function prepareConfiguration(ContainerInterface|array $container ); $configurationVariableService = InMemoryConfigurationVariableService::create($configurationVariables); - $runtimeServices = [ - ServiceCacheConfiguration::REFERENCE_NAME => $serviceCacheConfiguration, - ConfigurationVariableService::REFERENCE_NAME => $configurationVariableService, - ]; - $container = null; - - if ($serviceCacheConfiguration->shouldUseCache()) { - $container = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); - } - - if (! $container) { - $messagingConfiguration = MessagingSystemConfiguration::prepareWithAnnotationFinder( - $annotationFinder, - $configurationVariableService, - $serviceConfiguration, - $enableTesting - ); - - $messagingConfiguration->withExternalContainer($externalContainer); - - $ecotoneBuilder = new ContainerBuilder(); - $ecotoneBuilder->addCompilerPass($messagingConfiguration); - $ecotoneBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); - $ecotoneBuilder->addCompilerPass(new ValidityCheckPass()); - if ($serviceCacheConfiguration->shouldUseCache()) { - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - } - - $container = EcotoneSymfonyContainerFactory::build($ecotoneBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices); - } + $container = EcotoneSymfonyContainerFactory::bootstrap( + $serviceCacheConfiguration, + $configurationVariableService, + $externalContainer, + function () use ($annotationFinder, $configurationVariableService, $serviceConfiguration, $enableTesting, $externalContainer) { + $messagingConfiguration = MessagingSystemConfiguration::prepareWithAnnotationFinder( + $annotationFinder, + $configurationVariableService, + $serviceConfiguration, + $enableTesting + ); + $messagingConfiguration->withExternalContainer($externalContainer); + + return $messagingConfiguration; + }, + $cacheHash, + ); $messagingSystem = $container->get(ConfiguredMessagingSystem::class); diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php index 13061b096..326abc1e8 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -6,8 +6,13 @@ use Ecotone\Lite\InMemoryPSRContainer; use Ecotone\Messaging\Config\ConfigurationException; +use Ecotone\Messaging\Config\Container\Compiler\CompilerPass; +use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; +use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; use Ecotone\Messaging\Config\Container\ContainerBuilder; +use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; +use Ecotone\Messaging\ConfigurationVariableService; use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; @@ -18,6 +23,42 @@ */ final class EcotoneSymfonyContainerFactory { + /** + * @param callable(): CompilerPass $messagingConfigurationFactory invoked only on cache miss + * @param array $additionalRuntimeServices + */ + public static function bootstrap( + ServiceCacheConfiguration $serviceCacheConfiguration, + ConfigurationVariableService $configurationVariableService, + ?ContainerInterface $externalContainer, + callable $messagingConfigurationFactory, + ?string $configHash = null, + array $additionalRuntimeServices = [], + ): EcotoneContainer { + $runtimeServices = [ + ServiceCacheConfiguration::REFERENCE_NAME => $serviceCacheConfiguration, + ConfigurationVariableService::REFERENCE_NAME => $configurationVariableService, + ] + $additionalRuntimeServices; + + if ($serviceCacheConfiguration->shouldUseCache()) { + $container = self::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); + if ($container) { + return $container; + } + } + + $builder = new ContainerBuilder(); + $builder->addCompilerPass($messagingConfigurationFactory()); + $builder->addCompilerPass(new RegisterInterfaceToCallReferences()); + $builder->addCompilerPass(new ValidityCheckPass()); + + if ($serviceCacheConfiguration->shouldUseCache()) { + MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); + } + + return self::build($builder, $serviceCacheConfiguration, $externalContainer, $runtimeServices, $configHash); + } + /** * @param array $runtimeServices */ diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php index 0543395c1..392929512 100644 --- a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php @@ -5,10 +5,13 @@ namespace Test\Ecotone\SymfonyContainer; use Ecotone\Lite\InMemoryPSRContainer; +use Ecotone\Messaging\Config\Container\Compiler\CompilerPass; use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\Container\Definition; use Ecotone\Messaging\Config\Container\Reference; use Ecotone\Messaging\Config\ServiceCacheConfiguration; +use Ecotone\Messaging\ConfigurationVariableService; +use Ecotone\Messaging\InMemoryConfigurationVariableService; use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use Ecotone\Test\StubLogger; use PHPUnit\Framework\TestCase; @@ -92,6 +95,31 @@ public function test_it_resolves_external_references_in_cache_loaded_container() self::assertSame($logger, $loaded->get('aService')->dependency); } + public function test_bootstrap_builds_once_and_warm_boots_without_invoking_configuration_factory(): void + { + $cacheConfiguration = new ServiceCacheConfiguration($this->uniqueCacheDirectory(), true); + $configurationVariableService = InMemoryConfigurationVariableService::createEmpty(); + $factoryInvocations = 0; + $configurationFactory = function () use (&$factoryInvocations) { + $factoryInvocations++; + return new class () implements CompilerPass { + public function process(ContainerBuilder $builder): void + { + $builder->register('aService', new Definition(ACachedService::class, ['fromFactory'])); + } + }; + }; + + $coldBooted = EcotoneSymfonyContainerFactory::bootstrap($cacheConfiguration, $configurationVariableService, null, $configurationFactory); + self::assertSame(1, $factoryInvocations); + self::assertSame('fromFactory', $coldBooted->get('aService')->name); + self::assertSame($configurationVariableService, $coldBooted->get(ConfigurationVariableService::REFERENCE_NAME)); + + $warmBooted = EcotoneSymfonyContainerFactory::bootstrap($cacheConfiguration, $configurationVariableService, null, $configurationFactory); + self::assertSame(1, $factoryInvocations); + self::assertSame('fromFactory', $warmBooted->get('aService')->name); + } + private function uniqueCacheDirectory(): string { return sys_get_temp_dir() . '/ecotone_container_cache_test/' . uniqid('', true); diff --git a/packages/Laravel/src/EcotoneProvider.php b/packages/Laravel/src/EcotoneProvider.php index 66a56d955..380455fac 100644 --- a/packages/Laravel/src/EcotoneProvider.php +++ b/packages/Laravel/src/EcotoneProvider.php @@ -227,26 +227,19 @@ public function prepareFromCache(mixed $useProductionCache, string $rootCatalog, $useProductionCache ? $cacheDirectory : ($cacheDirectory . DIRECTORY_SEPARATOR . $cacheHash), true, ); - $runtimeServices[ServiceCacheConfiguration::REFERENCE_NAME] = $serviceCacheConfiguration; - $container = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); - - if (! $container) { - $configuration = MessagingSystemConfiguration::prepareWithAnnotationFinder( + $container = EcotoneSymfonyContainerFactory::bootstrap( + $serviceCacheConfiguration, + new LaravelConfigurationVariableService(), + $externalContainer, + fn () => MessagingSystemConfiguration::prepareWithAnnotationFinder( $annotationFinder, new LaravelConfigurationVariableService(), $applicationConfiguration, enableTestPackage: $enableTesting - ); - - $ecotoneBuilder = new ContainerBuilder(); - $ecotoneBuilder->addCompilerPass($configuration); - $ecotoneBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); - $ecotoneBuilder->addCompilerPass(new ValidityCheckPass()); - - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - $container = EcotoneSymfonyContainerFactory::build($ecotoneBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices); - } + ), + $cacheHash, + ); return [$serviceCacheConfiguration, $container]; } diff --git a/packages/LiteApplication/src/EcotoneLiteApplication.php b/packages/LiteApplication/src/EcotoneLiteApplication.php index 013ab9a0f..07b991b3a 100644 --- a/packages/LiteApplication/src/EcotoneLiteApplication.php +++ b/packages/LiteApplication/src/EcotoneLiteApplication.php @@ -52,36 +52,23 @@ public static function bootstrap( $externalContainer = new AutowiringContainer(InMemoryPSRContainer::createFromAssociativeArray(array_merge($classesToRegister, $objectsToRegister))); $configurationVariableService = InMemoryConfigurationVariableService::create($configurationVariables); - $runtimeServices = [ - ServiceCacheConfiguration::REFERENCE_NAME => $serviceCacheConfiguration, - ConfigurationVariableService::REFERENCE_NAME => $configurationVariableService, - ]; - $container = null; - if ($serviceCacheConfiguration->shouldUseCache()) { - $container = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); - } - - if (! $container) { - /** @var MessagingSystemConfiguration $messagingConfiguration */ - $messagingConfiguration = MessagingSystemConfiguration::prepare( - $pathToRootCatalog, - $configurationVariableService, - $serviceConfiguration, - ); - $messagingConfiguration->withExternalContainer($externalContainer); - - $containerBuilder = new ContainerBuilder(); - $containerBuilder->addCompilerPass($messagingConfiguration); - $containerBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); - $containerBuilder->addCompilerPass(new ValidityCheckPass()); - - if ($serviceCacheConfiguration->shouldUseCache()) { - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - } - - $container = EcotoneSymfonyContainerFactory::build($containerBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices); - } + $container = EcotoneSymfonyContainerFactory::bootstrap( + $serviceCacheConfiguration, + $configurationVariableService, + $externalContainer, + function () use ($pathToRootCatalog, $configurationVariableService, $serviceConfiguration, $externalContainer) { + /** @var MessagingSystemConfiguration $messagingConfiguration */ + $messagingConfiguration = MessagingSystemConfiguration::prepare( + $pathToRootCatalog, + $configurationVariableService, + $serviceConfiguration, + ); + $messagingConfiguration->withExternalContainer($externalContainer); + + return $messagingConfiguration; + }, + ); $externalContainer->setEcotoneContainer($container); diff --git a/packages/Tempest/src/MessagingSystemInitializer.php b/packages/Tempest/src/MessagingSystemInitializer.php index 5a7afc6c8..1baf775b4 100644 --- a/packages/Tempest/src/MessagingSystemInitializer.php +++ b/packages/Tempest/src/MessagingSystemInitializer.php @@ -195,26 +195,19 @@ private function prepareFromCache( $useProductionCache ? $cacheDirectory : ($cacheDirectory . DIRECTORY_SEPARATOR . $cacheHash), true, ); - $runtimeServices[ServiceCacheConfiguration::REFERENCE_NAME] = $serviceCacheConfiguration; - $ecotoneContainer = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); - - if (! $ecotoneContainer) { - $configuration = MessagingSystemConfiguration::prepareWithAnnotationFinder( + $ecotoneContainer = EcotoneSymfonyContainerFactory::bootstrap( + $serviceCacheConfiguration, + new TempestConfigurationVariableService(), + $externalContainer, + fn () => MessagingSystemConfiguration::prepareWithAnnotationFinder( $annotationFinder, new TempestConfigurationVariableService(), $applicationConfiguration, enableTestPackage: $enableTesting, - ); - - $ecotoneBuilder = new ContainerBuilder(); - $ecotoneBuilder->addCompilerPass($configuration); - $ecotoneBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); - $ecotoneBuilder->addCompilerPass(new ValidityCheckPass()); - - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - $ecotoneContainer = EcotoneSymfonyContainerFactory::build($ecotoneBuilder, $serviceCacheConfiguration, $externalContainer, $runtimeServices, $cacheHash); - } + ), + $cacheHash, + ); return [$ecotoneContainer, $cacheHash]; } From 5027f9b5c6543d393ec167ea1e6e3e91774e225f Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 12:24:25 +0200 Subject: [PATCH 13/17] refactor: share cache layout resolution across integrations --- packages/Ecotone/src/Lite/EcotoneLite.php | 24 +++---- .../SymfonyContainer/ContainerCacheLayout.php | 69 +++++++++++++++++++ .../EcotoneSymfonyContainerFactory.php | 30 ++++++-- .../ContainerCacheLayoutTest.php | 63 +++++++++++++++++ ...cotoneSymfonyContainerFactoryCacheTest.php | 22 ++++++ packages/Laravel/src/EcotoneProvider.php | 40 ++++------- .../src/EcotoneLiteApplication.php | 3 - .../src/MessagingSystemInitializer.php | 44 ++++-------- 8 files changed, 218 insertions(+), 77 deletions(-) create mode 100644 packages/Ecotone/src/SymfonyContainer/ContainerCacheLayout.php create mode 100644 packages/Ecotone/tests/SymfonyContainer/ContainerCacheLayoutTest.php diff --git a/packages/Ecotone/src/Lite/EcotoneLite.php b/packages/Ecotone/src/Lite/EcotoneLite.php index 2dbb6fe34..dd4857a95 100644 --- a/packages/Ecotone/src/Lite/EcotoneLite.php +++ b/packages/Ecotone/src/Lite/EcotoneLite.php @@ -4,7 +4,6 @@ namespace Ecotone\Lite; -use Ecotone\AnnotationFinder\AnnotationFinderFactory; use Ecotone\AnnotationFinder\FileSystem\FileSystemAnnotationFinder; use Ecotone\AnnotationFinder\FileSystem\RootCatalogNotFound; use Ecotone\Dbal\Configuration\DbalConfiguration; @@ -23,6 +22,7 @@ use Ecotone\Messaging\InMemoryConfigurationVariableService; use Ecotone\Messaging\Support\Assert; use Ecotone\Modelling\BaseEventSourcingConfiguration; +use Ecotone\SymfonyContainer\ContainerCacheLayout; use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use function json_decode; @@ -202,20 +202,18 @@ private static function prepareConfiguration(ContainerInterface|array $container $externalContainer = $containerOrAvailableServices instanceof ContainerInterface ? $containerOrAvailableServices : InMemoryPSRContainer::createFromAssociativeArray($containerOrAvailableServices); $serviceConfiguration = MessagingSystemConfiguration::addCorePackage($serviceConfiguration, $enableTesting); - $annotationFinder = AnnotationFinderFactory::createForAttributes( - realpath($pathToRootCatalog), - $serviceConfiguration->getNamespaces(), - $serviceConfiguration->getEnvironment(), - $serviceConfiguration->getLoadedCatalog() ?? '', - MessagingSystemConfiguration::getModuleClassesFor($serviceConfiguration), - $classesToResolve, - $enableTesting - ); - $cacheHash = $annotationFinder->getCacheMessagingFileNameBasedOnConfig($pathToRootCatalog, $serviceConfiguration, $configurationVariables, $enableTesting); - $serviceCacheConfiguration = new ServiceCacheConfiguration( - $serviceConfiguration->getCacheDirectoryPath() . DIRECTORY_SEPARATOR . $cacheHash, + $cacheLayout = ContainerCacheLayout::resolve( + $pathToRootCatalog, + $serviceConfiguration, + $serviceConfiguration->getCacheDirectoryPath(), self::shouldUseAutomaticCache($useCachedVersion, $pathToRootCatalog), + configurationVariables: $configurationVariables, + classesToResolve: $classesToResolve, + enableTesting: $enableTesting, ); + $annotationFinder = $cacheLayout->annotationFinder; + $serviceCacheConfiguration = $cacheLayout->serviceCacheConfiguration; + $cacheHash = $cacheLayout->configHash; $configurationVariableService = InMemoryConfigurationVariableService::create($configurationVariables); diff --git a/packages/Ecotone/src/SymfonyContainer/ContainerCacheLayout.php b/packages/Ecotone/src/SymfonyContainer/ContainerCacheLayout.php new file mode 100644 index 000000000..836b0109e --- /dev/null +++ b/packages/Ecotone/src/SymfonyContainer/ContainerCacheLayout.php @@ -0,0 +1,69 @@ + $configurationVariables + * @param string[] $classesToResolve + */ + public static function resolve( + string $rootCatalog, + ServiceConfiguration $serviceConfiguration, + string $cacheDirectory, + bool $shouldUseCache, + bool $useHashSubDirectory = true, + array $configurationVariables = [], + array $classesToResolve = [], + bool $enableTesting = false, + ): self { + $realRootCatalog = realpath($rootCatalog) ?: $rootCatalog; + $annotationFinder = AnnotationFinderFactory::createForAttributes( + $realRootCatalog, + $serviceConfiguration->getNamespaces(), + $serviceConfiguration->getEnvironment(), + $serviceConfiguration->getLoadedCatalog() ?? '', + MessagingSystemConfiguration::getModuleClassesFor($serviceConfiguration), + $classesToResolve, + $enableTesting, + ); + $configHash = $annotationFinder->getCacheMessagingFileNameBasedOnConfig( + $realRootCatalog, + $serviceConfiguration, + $configurationVariables, + $enableTesting, + ); + + return new self( + $annotationFinder, + new ServiceCacheConfiguration( + $useHashSubDirectory ? $cacheDirectory . DIRECTORY_SEPARATOR . $configHash : $cacheDirectory, + $shouldUseCache, + ), + $configHash, + ); + } +} diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php index 326abc1e8..8b0fe7554 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -35,10 +35,7 @@ public static function bootstrap( ?string $configHash = null, array $additionalRuntimeServices = [], ): EcotoneContainer { - $runtimeServices = [ - ServiceCacheConfiguration::REFERENCE_NAME => $serviceCacheConfiguration, - ConfigurationVariableService::REFERENCE_NAME => $configurationVariableService, - ] + $additionalRuntimeServices; + $runtimeServices = self::defaultRuntimeServices($serviceCacheConfiguration, $configurationVariableService) + $additionalRuntimeServices; if ($serviceCacheConfiguration->shouldUseCache()) { $container = self::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); @@ -93,6 +90,31 @@ public static function build( return self::wrapWithExternalFallback($symfonyBuilder, $externalContainer, $runtimeServices); } + public static function loadCachedWithDefaults( + ServiceCacheConfiguration $serviceCacheConfiguration, + ConfigurationVariableService $configurationVariableService, + ?ContainerInterface $externalContainer = null, + ): ?EcotoneContainer { + return self::loadCached( + $serviceCacheConfiguration, + $externalContainer, + self::defaultRuntimeServices($serviceCacheConfiguration, $configurationVariableService), + ); + } + + /** + * @return array + */ + private static function defaultRuntimeServices( + ServiceCacheConfiguration $serviceCacheConfiguration, + ConfigurationVariableService $configurationVariableService, + ): array { + return [ + ServiceCacheConfiguration::REFERENCE_NAME => $serviceCacheConfiguration, + ConfigurationVariableService::REFERENCE_NAME => $configurationVariableService, + ]; + } + /** * @param array $runtimeServices */ diff --git a/packages/Ecotone/tests/SymfonyContainer/ContainerCacheLayoutTest.php b/packages/Ecotone/tests/SymfonyContainer/ContainerCacheLayoutTest.php new file mode 100644 index 000000000..798a5bef4 --- /dev/null +++ b/packages/Ecotone/tests/SymfonyContainer/ContainerCacheLayoutTest.php @@ -0,0 +1,63 @@ +withSkippedModulePackageNames(ModulePackageList::allPackages()); + $cacheDirectory = sys_get_temp_dir() . '/ecotone_cache_layout_test'; + + $cacheLayout = ContainerCacheLayout::resolve( + __DIR__ . '/../../', + $serviceConfiguration, + $cacheDirectory, + shouldUseCache: true, + classesToResolve: [self::class], + ); + $sameConfigurationLayout = ContainerCacheLayout::resolve( + __DIR__ . '/../../', + $serviceConfiguration, + $cacheDirectory, + shouldUseCache: true, + classesToResolve: [self::class], + ); + + self::assertInstanceOf(AnnotationFinder::class, $cacheLayout->annotationFinder); + self::assertNotEmpty($cacheLayout->configHash); + self::assertSame($cacheLayout->configHash, $sameConfigurationLayout->configHash); + self::assertSame($cacheDirectory . DIRECTORY_SEPARATOR . $cacheLayout->configHash, $cacheLayout->serviceCacheConfiguration->getPath()); + self::assertTrue($cacheLayout->serviceCacheConfiguration->shouldUseCache()); + } + + public function test_it_resolves_fixed_cache_directory_without_hash_sub_directory(): void + { + $cacheDirectory = sys_get_temp_dir() . '/ecotone_cache_layout_test_fixed'; + + $cacheLayout = ContainerCacheLayout::resolve( + __DIR__ . '/../../', + ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackages()), + $cacheDirectory, + shouldUseCache: true, + useHashSubDirectory: false, + classesToResolve: [self::class], + ); + + self::assertSame($cacheDirectory, $cacheLayout->serviceCacheConfiguration->getPath()); + } +} diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php index 392929512..8bd8fbed1 100644 --- a/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php @@ -120,6 +120,28 @@ public function process(ContainerBuilder $builder): void self::assertSame('fromFactory', $warmBooted->get('aService')->name); } + public function test_load_cached_with_defaults_provides_runtime_services(): void + { + $cacheConfiguration = new ServiceCacheConfiguration($this->uniqueCacheDirectory(), true); + $configurationVariableService = InMemoryConfigurationVariableService::createEmpty(); + EcotoneSymfonyContainerFactory::bootstrap( + $cacheConfiguration, + $configurationVariableService, + null, + fn () => new class () implements CompilerPass { + public function process(ContainerBuilder $builder): void + { + } + }, + ); + + $loaded = EcotoneSymfonyContainerFactory::loadCachedWithDefaults($cacheConfiguration, $configurationVariableService); + + self::assertNotNull($loaded); + self::assertSame($configurationVariableService, $loaded->get(ConfigurationVariableService::REFERENCE_NAME)); + self::assertSame($cacheConfiguration, $loaded->get(ServiceCacheConfiguration::REFERENCE_NAME)); + } + private function uniqueCacheDirectory(): string { return sys_get_temp_dir() . '/ecotone_container_cache_test/' . uniqid('', true); diff --git a/packages/Laravel/src/EcotoneProvider.php b/packages/Laravel/src/EcotoneProvider.php index 380455fac..a666ab849 100644 --- a/packages/Laravel/src/EcotoneProvider.php +++ b/packages/Laravel/src/EcotoneProvider.php @@ -4,18 +4,15 @@ use const DIRECTORY_SEPARATOR; -use Ecotone\AnnotationFinder\AnnotationFinderFactory; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; use Ecotone\Messaging\Config\ConsoleCommandResultSet; -use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; -use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; -use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\ConfigurationVariableService; use Ecotone\Messaging\Gateway\ConsoleCommandRunner; use Ecotone\Messaging\Handler\Recoverability\RetryTemplateBuilder; +use Ecotone\SymfonyContainer\ContainerCacheLayout; use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use Illuminate\Console\Events\CommandFinished; use Illuminate\Foundation\Console\ClosureCommand; @@ -193,40 +190,27 @@ public static function getCacheDirectoryPath(): string public function prepareFromCache(mixed $useProductionCache, string $rootCatalog, ServiceConfiguration $applicationConfiguration, mixed $enableTesting, string $cacheDirectory): array { $externalContainer = new LaravelPsrContainerAdapter($this->app); - $runtimeServices = [ - ConfigurationVariableService::REFERENCE_NAME => new LaravelConfigurationVariableService(), - ]; if ($useProductionCache && $cacheDirectory) { $serviceCacheConfiguration = new ServiceCacheConfiguration($cacheDirectory, true); - $runtimeServices[ServiceCacheConfiguration::REFERENCE_NAME] = $serviceCacheConfiguration; - - $container = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); + $container = EcotoneSymfonyContainerFactory::loadCachedWithDefaults($serviceCacheConfiguration, new LaravelConfigurationVariableService(), $externalContainer); if ($container) { return [$serviceCacheConfiguration, $container]; } } - $annotationFinder = AnnotationFinderFactory::createForAttributes( - realpath($rootCatalog), - $applicationConfiguration->getNamespaces(), - $applicationConfiguration->getEnvironment(), - $applicationConfiguration->getLoadedCatalog() ?? '', - MessagingSystemConfiguration::getModuleClassesFor($applicationConfiguration), - isRunningForTesting: $enableTesting, - ); - - $cacheHash = $annotationFinder->getCacheMessagingFileNameBasedOnConfig( - realpath($rootCatalog), + $cacheLayout = ContainerCacheLayout::resolve( + $rootCatalog, $applicationConfiguration, - Config::all(), - $enableTesting - ); - - $serviceCacheConfiguration = new ServiceCacheConfiguration( - $useProductionCache ? $cacheDirectory : ($cacheDirectory . DIRECTORY_SEPARATOR . $cacheHash), - true, + $cacheDirectory, + shouldUseCache: true, + useHashSubDirectory: ! $useProductionCache, + configurationVariables: Config::all(), + enableTesting: (bool) $enableTesting, ); + $annotationFinder = $cacheLayout->annotationFinder; + $serviceCacheConfiguration = $cacheLayout->serviceCacheConfiguration; + $cacheHash = $cacheLayout->configHash; $container = EcotoneSymfonyContainerFactory::bootstrap( $serviceCacheConfiguration, diff --git a/packages/LiteApplication/src/EcotoneLiteApplication.php b/packages/LiteApplication/src/EcotoneLiteApplication.php index 07b991b3a..d038c42b7 100644 --- a/packages/LiteApplication/src/EcotoneLiteApplication.php +++ b/packages/LiteApplication/src/EcotoneLiteApplication.php @@ -5,9 +5,6 @@ namespace Ecotone\Lite; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; -use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; -use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; -use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; diff --git a/packages/Tempest/src/MessagingSystemInitializer.php b/packages/Tempest/src/MessagingSystemInitializer.php index 1baf775b4..a013cad0e 100644 --- a/packages/Tempest/src/MessagingSystemInitializer.php +++ b/packages/Tempest/src/MessagingSystemInitializer.php @@ -6,15 +6,12 @@ use const DIRECTORY_SEPARATOR; -use Ecotone\AnnotationFinder\AnnotationFinderFactory; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; -use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; -use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; -use Ecotone\Messaging\Config\Container\ContainerBuilder; use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\ConfigurationVariableService; +use Ecotone\SymfonyContainer\ContainerCacheLayout; use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use Tempest\Container\Container; use Tempest\Container\Initializer; @@ -161,40 +158,29 @@ private function prepareFromCache( Container $container, ): array { $externalContainer = new TempestPsrContainerAdapter($container); - $runtimeServices = [ - ConfigurationVariableService::REFERENCE_NAME => new TempestConfigurationVariableService(), - ]; if ($useProductionCache && $cacheDirectory) { - $serviceCacheConfiguration = new ServiceCacheConfiguration($cacheDirectory, true); - $runtimeServices[ServiceCacheConfiguration::REFERENCE_NAME] = $serviceCacheConfiguration; - - $ecotoneContainer = EcotoneSymfonyContainerFactory::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices); + $ecotoneContainer = EcotoneSymfonyContainerFactory::loadCachedWithDefaults( + new ServiceCacheConfiguration($cacheDirectory, true), + new TempestConfigurationVariableService(), + $externalContainer, + ); if ($ecotoneContainer) { return [$ecotoneContainer, $ecotoneContainer->getConfigHash()]; } } - $annotationFinder = AnnotationFinderFactory::createForAttributes( - realpath($rootCatalog) ?: $rootCatalog, - $applicationConfiguration->getNamespaces(), - $applicationConfiguration->getEnvironment(), - $applicationConfiguration->getLoadedCatalog() ?? '', - MessagingSystemConfiguration::getModuleClassesFor($applicationConfiguration), - isRunningForTesting: $enableTesting, - ); - - $cacheHash = $annotationFinder->getCacheMessagingFileNameBasedOnConfig( - realpath($rootCatalog) ?: $rootCatalog, + $cacheLayout = ContainerCacheLayout::resolve( + $rootCatalog, $applicationConfiguration, - [], - $enableTesting, - ); - - $serviceCacheConfiguration = new ServiceCacheConfiguration( - $useProductionCache ? $cacheDirectory : ($cacheDirectory . DIRECTORY_SEPARATOR . $cacheHash), - true, + $cacheDirectory, + shouldUseCache: true, + useHashSubDirectory: ! $useProductionCache, + enableTesting: $enableTesting, ); + $annotationFinder = $cacheLayout->annotationFinder; + $serviceCacheConfiguration = $cacheLayout->serviceCacheConfiguration; + $cacheHash = $cacheLayout->configHash; $ecotoneContainer = EcotoneSymfonyContainerFactory::bootstrap( $serviceCacheConfiguration, From 77d80b26774eda8049d777dce527bca7cdf46cc0 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Thu, 11 Jun 2026 12:32:13 +0200 Subject: [PATCH 14/17] refactor: share gateway bridge registration and align container pass list --- packages/Ecotone/src/Lite/EcotoneLite.php | 8 +++---- .../src/SymfonyContainer/EcotoneContainer.php | 15 ++++++++++++ .../EcotoneLiteCachedContainerTest.php | 23 +++++++++++++++++++ packages/Laravel/src/EcotoneProvider.php | 9 +++----- .../DependencyInjection/EcotoneExtension.php | 16 ++++--------- 5 files changed, 49 insertions(+), 22 deletions(-) diff --git a/packages/Ecotone/src/Lite/EcotoneLite.php b/packages/Ecotone/src/Lite/EcotoneLite.php index dd4857a95..4db80050d 100644 --- a/packages/Ecotone/src/Lite/EcotoneLite.php +++ b/packages/Ecotone/src/Lite/EcotoneLite.php @@ -239,11 +239,9 @@ function () use ($annotationFinder, $configurationVariableService, $serviceConfi if ($allowGatewaysToBeRegisteredInContainer) { Assert::isTrue(method_exists($externalContainer, 'set'), 'Gateways registration was enabled however given container has no `set` method. Please add it or turn off the option.'); - $externalContainer->set(ConfiguredMessagingSystem::class, $messagingSystem); - foreach ($messagingSystem->getGatewayList() as $gatewayReference) { - $gatewayReferenceName = $gatewayReference->getReferenceName(); - $externalContainer->set($gatewayReferenceName, $messagingSystem->getGatewayByName($gatewayReferenceName)); - } + $container->registerBridgesInto( + fn (string $referenceName, string $interfaceName, callable $factory) => $externalContainer->set($referenceName, $factory()), + ); } elseif ($externalContainer->has(ConfiguredMessagingSystem::class)) { /** @var ConfiguredMessagingSystem $alreadyConfiguredMessaging */ $alreadyConfiguredMessaging = $externalContainer->get(ConfiguredMessagingSystem::class); diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php index 495f7e94c..e7015cb10 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php @@ -4,6 +4,7 @@ namespace Ecotone\SymfonyContainer; +use Ecotone\Messaging\Config\ConfiguredMessagingSystem; use Ecotone\Messaging\Config\Container\Compiler\ContainerImplementation; use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; @@ -83,6 +84,20 @@ public function getExternalReferenceIds(): array return $this->container->getParameter(SymfonyContainerImplementation::EXTERNAL_REFERENCES_PARAMETER); } + /** + * @param callable(string $referenceName, string $interfaceName, callable(): object $factory): void $register + */ + public function registerBridgesInto(callable $register): void + { + /** @var ConfiguredMessagingSystem $messagingSystem */ + $messagingSystem = $this->get(ConfiguredMessagingSystem::class); + $register(ConfiguredMessagingSystem::class, ConfiguredMessagingSystem::class, fn () => $messagingSystem); + foreach ($messagingSystem->getGatewayList() as $gatewayReference) { + $referenceName = $gatewayReference->getReferenceName(); + $register($referenceName, $gatewayReference->getInterfaceName(), fn () => $this->get($referenceName)); + } + } + public function getConfigHash(): ?string { if (! $this->container->hasParameter(SymfonyContainerImplementation::CONFIG_HASH_PARAMETER)) { diff --git a/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php index 4cbe385a0..befcf507c 100644 --- a/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php @@ -6,9 +6,11 @@ use Ecotone\Lite\EcotoneLite; use Ecotone\Messaging\Attribute\Parameter\Payload; +use Ecotone\Messaging\Config\ConfiguredMessagingSystem; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Modelling\Attribute\CommandHandler; +use Ecotone\Modelling\CommandBus; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Test\Ecotone\Messaging\Fixture\Annotation\MessageEndpoint\OneTimeCommand\OneTimeWithResultExample; @@ -62,6 +64,27 @@ public function test_registered_console_commands_are_available_as_container_para $consoleCommands = $container->getRegisteredConsoleCommands(); self::assertContains('doSomething', array_map(fn ($command) => $command->getName(), $consoleCommands)); } + + public function test_gateway_bridges_can_be_registered_into_framework_container(): void + { + $messagingSystem = EcotoneLite::bootstrap( + [CachedCommandHandlerService::class], + [CachedCommandHandlerService::class => new CachedCommandHandlerService()], + ServiceConfiguration::createWithDefaults() + ->withSkippedModulePackageNames(ModulePackageList::allPackages()), + ); + $container = $messagingSystem->getServiceFromContainer(ContainerInterface::class); + + $registeredBridges = []; + $container->registerBridgesInto(function (string $referenceName, string $interfaceName, callable $factory) use (&$registeredBridges) { + $registeredBridges[$referenceName] = [$interfaceName, $factory]; + }); + + self::assertArrayHasKey(ConfiguredMessagingSystem::class, $registeredBridges); + self::assertArrayHasKey(CommandBus::class, $registeredBridges); + self::assertSame(CommandBus::class, $registeredBridges[CommandBus::class][0]); + self::assertInstanceOf(CommandBus::class, $registeredBridges[CommandBus::class][1]()); + } } /** diff --git a/packages/Laravel/src/EcotoneProvider.php b/packages/Laravel/src/EcotoneProvider.php index a666ab849..2c39a0652 100644 --- a/packages/Laravel/src/EcotoneProvider.php +++ b/packages/Laravel/src/EcotoneProvider.php @@ -96,12 +96,9 @@ public function register() [$serviceCacheConfiguration, $container] = $this->prepareFromCache($useProductionCache, $rootCatalog, $applicationConfiguration, $enableTesting, $cacheDirectory); - $messagingSystem = $container->get(ConfiguredMessagingSystem::class); - $this->app->singleton(ConfiguredMessagingSystem::class, fn () => $messagingSystem); - foreach ($messagingSystem->getGatewayList() as $gatewayReference) { - $gatewayReferenceName = $gatewayReference->getReferenceName(); - $this->app->singleton($gatewayReferenceName, fn () => $container->get($gatewayReferenceName)); - } + $container->registerBridgesInto( + fn (string $referenceName, string $interfaceName, callable $factory) => $this->app->singleton($referenceName, fn () => $factory()), + ); $this->app->singleton( ConfigurationVariableService::REFERENCE_NAME, diff --git a/packages/Symfony/DependencyInjection/EcotoneExtension.php b/packages/Symfony/DependencyInjection/EcotoneExtension.php index 89adc9e2c..416c5daf1 100644 --- a/packages/Symfony/DependencyInjection/EcotoneExtension.php +++ b/packages/Symfony/DependencyInjection/EcotoneExtension.php @@ -2,10 +2,9 @@ namespace Ecotone\SymfonyBundle\DependencyInjection; -use Ecotone\Messaging\Config\ConfiguredMessagingSystem; use Ecotone\Messaging\Config\Container\Compiler\RegisterInterfaceToCallReferences; +use Ecotone\Messaging\Config\Container\Compiler\ValidityCheckPass; use Ecotone\Messaging\Config\MessagingSystemConfiguration; -use Ecotone\Messaging\Config\MessagingSystemContainer; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\Gateway\ConsoleCommandRunner; @@ -109,6 +108,7 @@ public function load(array $configs, ContainerBuilder $container): void $containerBuilder->register(ServiceCacheConfiguration::REFERENCE_NAME, $serviceCacheConfiguration); $containerBuilder->addCompilerPass($messagingConfiguration); $containerBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); + $containerBuilder->addCompilerPass(new ValidityCheckPass()); $ecotoneContainer = EcotoneSymfonyContainerFactory::build($containerBuilder, $serviceCacheConfiguration); $container->register('ecotone.container', EcotoneContainer::class) @@ -116,18 +116,12 @@ public function load(array $configs, ContainerBuilder $container): void ->setArguments([$cacheDirectory, new Reference('service_container')]) ->setPublic(true); - $container->register(ConfiguredMessagingSystem::class, MessagingSystemContainer::class) - ->setFactory([new Reference('ecotone.container'), 'get']) - ->setArguments([ConfiguredMessagingSystem::class]) - ->setPublic(true); - - foreach ($messagingConfiguration->getRegisteredGateways() as $gatewayProxyBuilder) { - $referenceName = $gatewayProxyBuilder->getReferenceName(); - $container->register($referenceName, $gatewayProxyBuilder->getInterfaceName()) + $ecotoneContainer->registerBridgesInto(function (string $referenceName, string $interfaceName) use ($container) { + $container->register($referenceName, $interfaceName) ->setFactory([new Reference('ecotone.container'), 'get']) ->setArguments([$referenceName]) ->setPublic(true); - } + }); $container->register(ProxyFactory::class, ProxyFactory::class) ->setFactory([new Reference('ecotone.container'), 'get']) From 6fe311b13d5974bf9b4209502d69c1c65ea07162 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Fri, 12 Jun 2026 07:23:31 +0200 Subject: [PATCH 15/17] fix: prefer Ecotone definitions over user instances when autowiring in LiteApplication --- .../src/AutowiringContainer.php | 4 --- .../src/EcotoneLiteApplication.php | 1 - .../Ticketing/InMemoryTicketRepository.php | 32 +++++++++++++++++++ .../tests/Fixture/Ticketing/Ticket.php | 31 ++++++++++++++++++ .../Fixture/Ticketing/TicketNotifier.php | 23 +++++++++++++ .../Fixture/Ticketing/TicketRepository.php | 16 ++++++++++ .../tests/Unit/EcotoneLiteApplicationTest.php | 20 ++++++++++++ 7 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 packages/LiteApplication/tests/Fixture/Ticketing/InMemoryTicketRepository.php create mode 100644 packages/LiteApplication/tests/Fixture/Ticketing/Ticket.php create mode 100644 packages/LiteApplication/tests/Fixture/Ticketing/TicketNotifier.php create mode 100644 packages/LiteApplication/tests/Fixture/Ticketing/TicketRepository.php diff --git a/packages/LiteApplication/src/AutowiringContainer.php b/packages/LiteApplication/src/AutowiringContainer.php index 0e31f1710..74303bf9a 100644 --- a/packages/LiteApplication/src/AutowiringContainer.php +++ b/packages/LiteApplication/src/AutowiringContainer.php @@ -68,10 +68,6 @@ private function instantiate(string $className): object $type = $parameter->getType(); if ($type instanceof ReflectionNamedType && ! $type->isBuiltin()) { $typeName = $type->getName(); - if ($this->innerContainer->has($typeName)) { - $arguments[] = $this->innerContainer->get($typeName); - continue; - } if ($this->ecotoneContainer?->has($typeName)) { $arguments[] = $this->ecotoneContainer->get($typeName); continue; diff --git a/packages/LiteApplication/src/EcotoneLiteApplication.php b/packages/LiteApplication/src/EcotoneLiteApplication.php index d038c42b7..643274d80 100644 --- a/packages/LiteApplication/src/EcotoneLiteApplication.php +++ b/packages/LiteApplication/src/EcotoneLiteApplication.php @@ -8,7 +8,6 @@ use Ecotone\Messaging\Config\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; -use Ecotone\Messaging\ConfigurationVariableService; use Ecotone\Messaging\InMemoryConfigurationVariableService; use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; diff --git a/packages/LiteApplication/tests/Fixture/Ticketing/InMemoryTicketRepository.php b/packages/LiteApplication/tests/Fixture/Ticketing/InMemoryTicketRepository.php new file mode 100644 index 000000000..8e32ddb28 --- /dev/null +++ b/packages/LiteApplication/tests/Fixture/Ticketing/InMemoryTicketRepository.php @@ -0,0 +1,32 @@ +tickets[array_pop($identifiers)] ?? null; + } + + public function save(array $identifiers, object $aggregate, array $metadata, ?int $versionBeforeHandling): void + { + $this->tickets[array_pop($identifiers)] = $aggregate; + } +} diff --git a/packages/LiteApplication/tests/Fixture/Ticketing/Ticket.php b/packages/LiteApplication/tests/Fixture/Ticketing/Ticket.php new file mode 100644 index 000000000..f19f79588 --- /dev/null +++ b/packages/LiteApplication/tests/Fixture/Ticketing/Ticket.php @@ -0,0 +1,31 @@ +ticketId; + } +} diff --git a/packages/LiteApplication/tests/Fixture/Ticketing/TicketNotifier.php b/packages/LiteApplication/tests/Fixture/Ticketing/TicketNotifier.php new file mode 100644 index 000000000..a663a20fa --- /dev/null +++ b/packages/LiteApplication/tests/Fixture/Ticketing/TicketNotifier.php @@ -0,0 +1,23 @@ +ticketRepository->getBy($ticketId)->getId(); + } +} diff --git a/packages/LiteApplication/tests/Fixture/Ticketing/TicketRepository.php b/packages/LiteApplication/tests/Fixture/Ticketing/TicketRepository.php new file mode 100644 index 000000000..1e8875cf1 --- /dev/null +++ b/packages/LiteApplication/tests/Fixture/Ticketing/TicketRepository.php @@ -0,0 +1,16 @@ +withNamespaces(["Test\Ecotone\Lite\Fixture\Ticketing"]) + ->withSkippedModulePackageNames(ModulePackageList::allPackages()), + pathToRootCatalog: __DIR__ . '/../../', + classesToRegister: [TicketRepository::class => new InMemoryTicketRepository()], + ); + + $ecotoneLite->getCommandBus()->sendWithRouting('ticket.register', 'ticket-1'); + + $this->assertSame( + 'ticket-1', + $ecotoneLite->getQueryBus()->sendWithRouting('ticket.getRegistered', 'ticket-1') + ); + } + private function getCachedConfiguration(string $cacheDirectory): ConfiguredMessagingSystem { return EcotoneLiteApplication::boostrap( From 5a15c77efb5f9c708d46c32503e55cae35002e45 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Fri, 12 Jun 2026 07:57:11 +0200 Subject: [PATCH 16/17] fix(ci): isolate temp directory per component to avoid parallel phpstan cache races --- .github/workflows/split-testing.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/split-testing.yml b/.github/workflows/split-testing.yml index 013fa5907..856127d0f 100644 --- a/.github/workflows/split-testing.yml +++ b/.github/workflows/split-testing.yml @@ -175,6 +175,12 @@ jobs: local run_tests_cmd="$3" local overall_exit_code=0 + # Isolate temp directory per component, so parallel PHPStan runs do not race on a shared /tmp/phpstan cache + local tmp_slug=$(echo "$dir" | tr '/.-' '___' | tr '[:upper:]' '[:lower:]') + local component_tmp_dir="/tmp/ecotone_component_${tmp_slug}" + mkdir -p "$component_tmp_dir" + run_tests_cmd="TMPDIR='${component_tmp_dir}' ${run_tests_cmd}" + # Install dependencies and determine if MySQL pass is needed (direct or transitive doctrine/dbal) if ! _run_tests "Install composer dependencies ($dir)" "cd '${dir}' && ${compose_up_cmd}"; then overall_exit_code=1 From a20e01aa9313cfbce9e2e295b4fc04fbbbb1fad9 Mon Sep 17 00:00:00 2001 From: Dariusz Gafka Date: Fri, 12 Jun 2026 17:20:00 +0200 Subject: [PATCH 17/17] optimization --- .../EcotoneSymfonyContainerFactory.php | 49 ++++++++++++++----- .../SymfonyContainer/ServiceIdNormalizer.php | 4 ++ .../Compiler/CacheClearer.php | 2 +- .../Compiler/CacheWarmer.php | 7 ++- 4 files changed, 46 insertions(+), 16 deletions(-) diff --git a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php index 8b0fe7554..8413bcdac 100644 --- a/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -14,6 +14,7 @@ use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\ConfigurationVariableService; use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\Container as SymfonyBaseContainer; use Symfony\Component\DependencyInjection\ContainerBuilder as SymfonyContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; @@ -128,16 +129,20 @@ public static function loadCached( return null; } - $containerCode = file_get_contents($containerFile); - if ($containerCode === false || preg_match('/^class (EcotoneCachedContainer_[a-f0-9]+)/m', $containerCode, $matches) !== 1) { + $container = require $containerFile; + if (! $container instanceof SymfonyBaseContainer) { return null; } - $className = $matches[1]; - if (! class_exists($className, false)) { - require $containerFile; - } - return self::wrapWithExternalFallback(new $className(), $externalContainer, $runtimeServices); + return self::wrapWithExternalFallback($container, $externalContainer, $runtimeServices); + } + + /** + * @return string[] dumped container files, to be included in opcache preloading + */ + public static function dumpedContainerFiles(ServiceCacheConfiguration $serviceCacheConfiguration): array + { + return glob($serviceCacheConfiguration->getPath() . DIRECTORY_SEPARATOR . 'EcotoneCachedContainer_*.php') ?: []; } private static function dumpToCache( @@ -151,12 +156,30 @@ private static function dumpToCache( $dumper = new PhpDumper($symfonyBuilder); $placeholderClassName = 'EcotoneCachedContainerPlaceholder'; $containerCode = $dumper->dump(['class' => $placeholderClassName]); - $containerCode = str_replace( - $placeholderClassName, - 'EcotoneCachedContainer_' . md5($containerCode), - $containerCode, - ); - file_put_contents(self::containerFilePath($serviceCacheConfiguration), $containerCode); + $className = 'EcotoneCachedContainer_' . md5($containerCode); + $containerCode = str_replace($placeholderClassName, $className, $containerCode); + + foreach (self::dumpedContainerFiles($serviceCacheConfiguration) as $staleContainerFile) { + @unlink($staleContainerFile); + } + file_put_contents($cacheDirectory . DIRECTORY_SEPARATOR . $className . '.php', $containerCode); + file_put_contents(self::containerFilePath($serviceCacheConfiguration), self::loaderStub($className)); + } + + private static function loaderStub(string $className): string + { + return <<deleteDirectory($filePath); rmdir($filePath); - } elseif ($file !== 'ecotone_container.php') { + } elseif ($file !== 'ecotone_container.php' && ! str_starts_with($file, 'EcotoneCachedContainer_')) { unlink($filePath); } } diff --git a/packages/Symfony/DependencyInjection/Compiler/CacheWarmer.php b/packages/Symfony/DependencyInjection/Compiler/CacheWarmer.php index 1f5da9c39..3756c7579 100644 --- a/packages/Symfony/DependencyInjection/Compiler/CacheWarmer.php +++ b/packages/Symfony/DependencyInjection/Compiler/CacheWarmer.php @@ -3,7 +3,9 @@ namespace Ecotone\SymfonyBundle\DependencyInjection\Compiler; use Ecotone\Messaging\Config\ConfiguredMessagingSystem; +use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Handler\Gateway\ProxyFactory; +use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; /** @@ -13,7 +15,8 @@ class CacheWarmer implements CacheWarmerInterface { public function __construct( private ConfiguredMessagingSystem $configuredMessagingSystem, - private ProxyFactory $proxyFactory + private ProxyFactory $proxyFactory, + private ServiceCacheConfiguration $serviceCacheConfiguration, ) { } @@ -29,6 +32,6 @@ public function warmUp(string $cacheDir, ?string $buildDir = null): array $files[] = $this->proxyFactory->generateCachedProxyFileFor($gatewayReference, true); } - return $files; + return array_merge($files, EcotoneSymfonyContainerFactory::dumpedContainerFiles($this->serviceCacheConfiguration)); } }