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 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/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/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/Ecotone/composer.json b/packages/Ecotone/composer.json index caa7fc741..8573eb968 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/Lite/EcotoneLite.php b/packages/Ecotone/src/Lite/EcotoneLite.php index 7bdd90211..4db80050d 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; @@ -15,7 +14,6 @@ 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\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ModulePackageList; use Ecotone\Messaging\Config\ServiceCacheConfiguration; @@ -24,6 +22,8 @@ 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,63 +202,46 @@ 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); - $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)); - } - - if (! $definitionHolder) { - $messagingConfiguration = MessagingSystemConfiguration::prepareWithAnnotationFinder( - $annotationFinder, - $configurationVariableService, - $serviceConfiguration, - $enableTesting - ); - - $messagingConfiguration->withExternalContainer($externalContainer); - $definitionHolder = ContainerConfig::buildDefinitionHolder($messagingConfiguration); - - 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::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); 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/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/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/EcotoneContainer.php b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php new file mode 100644 index 000000000..e7015cb10 --- /dev/null +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneContainer.php @@ -0,0 +1,109 @@ +container->has($normalizedId)) { + return $this->container->get($normalizedId); + } + + return ExternalReferenceResolver::resolve($this->externalContainer, $id, ContainerImplementation::EXCEPTION_ON_INVALID_REFERENCE); + } + + public function has(string $id): bool + { + return $this->container->has(ServiceIdNormalizer::normalize($id)) || $this->externalContainer->has($id); + } + + 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); + } + + /** + * @return string[] + */ + 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); + } + + /** + * @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)) { + 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 new file mode 100644 index 000000000..8413bcdac --- /dev/null +++ b/packages/Ecotone/src/SymfonyContainer/EcotoneSymfonyContainerFactory.php @@ -0,0 +1,208 @@ + $additionalRuntimeServices + */ + public static function bootstrap( + ServiceCacheConfiguration $serviceCacheConfiguration, + ConfigurationVariableService $configurationVariableService, + ?ContainerInterface $externalContainer, + callable $messagingConfigurationFactory, + ?string $configHash = null, + array $additionalRuntimeServices = [], + ): EcotoneContainer { + $runtimeServices = self::defaultRuntimeServices($serviceCacheConfiguration, $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 + */ + public static function build( + ContainerBuilder $builder, + ServiceCacheConfiguration $serviceCacheConfiguration, + ?ContainerInterface $externalContainer = null, + array $runtimeServices = [], + ?string $configHash = null, + ): EcotoneContainer { + $symfonyBuilder = new SymfonyContainerBuilder(); + $implementation = new SymfonyContainerImplementation( + $symfonyBuilder, + array_keys($runtimeServices), + preserveRuntimeInstances: ! $serviceCacheConfiguration->shouldUseCache(), + ); + $definitionsHolder = $builder->compile(); + $implementation->process($builder); + $symfonyBuilder->setParameter( + SymfonyContainerImplementation::CONSOLE_COMMANDS_PARAMETER, + serialize($definitionsHolder->getRegisteredCommands()), + ); + $symfonyBuilder->setParameter(SymfonyContainerImplementation::CONFIG_HASH_PARAMETER, $configHash); + + if ($serviceCacheConfiguration->shouldUseCache()) { + $symfonyBuilder->compile(); + self::dumpToCache($symfonyBuilder, $serviceCacheConfiguration); + return self::loadCached($serviceCacheConfiguration, $externalContainer, $runtimeServices) + ?? throw ConfigurationException::create("Failed to load dumped Ecotone container from {$serviceCacheConfiguration->getPath()}"); + } + + 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 + */ + public static function loadCached( + ServiceCacheConfiguration $serviceCacheConfiguration, + ?ContainerInterface $externalContainer = null, + array $runtimeServices = [], + ): ?EcotoneContainer { + $containerFile = self::containerFilePath($serviceCacheConfiguration); + if (! file_exists($containerFile)) { + return null; + } + + $container = require $containerFile; + if (! $container instanceof SymfonyBaseContainer) { + return null; + } + + 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( + SymfonyContainerBuilder $symfonyBuilder, + ServiceCacheConfiguration $serviceCacheConfiguration, + ): void { + $cacheDirectory = $serviceCacheConfiguration->getPath(); + if (! is_dir($cacheDirectory)) { + mkdir($cacheDirectory, 0777, true); + } + $dumper = new PhpDumper($symfonyBuilder); + $placeholderClassName = 'EcotoneCachedContainerPlaceholder'; + $containerCode = $dumper->dump(['class' => $placeholderClassName]); + $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 <<getPath() . DIRECTORY_SEPARATOR . 'ecotone_container.php'; + } + + /** + * @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/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/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 @@ +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) { + 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); + } else { + $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 ($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(); + } + 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 || in_array($id, $this->syntheticServiceIds, true)) { + return new SymfonyReference(ServiceIdNormalizer::normalize($id)); + } + + return new SymfonyReference($this->registerExternalReferenceDelegate($id, $reference->getInvalidBehavior())); + } + + private function registerExternalReferenceDelegate(string $id, int $invalidBehavior): string + { + $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( + $delegateId, + (new SymfonyDefinition(stdClass::class)) + ->setFactory([ExternalReferenceResolver::class, 'resolve']) + ->setArguments([new SymfonyReference(self::EXTERNAL_CONTAINER_ID), $id, $invalidBehavior]) + ->setPublic(true) + ); + } + $this->externalReferences[$id] = $id; + + 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 = $this->runtimeInstanceDefinition($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( + $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/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/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/EcotoneLiteCachedContainerTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php new file mode 100644 index 000000000..befcf507c --- /dev/null +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneLiteCachedContainerTest.php @@ -0,0 +1,102 @@ +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); + } + + 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 = $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]()); + } +} + +/** + * 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/EcotoneSymfonyContainerFactoryCacheTest.php b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php new file mode 100644 index 000000000..8bd8fbed1 --- /dev/null +++ b/packages/Ecotone/tests/SymfonyContainer/EcotoneSymfonyContainerFactoryCacheTest.php @@ -0,0 +1,159 @@ +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_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_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); + + 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); + } + + 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); + } + + 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); + } +} + +/** + * licence Apache-2.0 + */ +class ACachedService +{ + public function __construct(public string $name, public mixed $dependency = null) + { + } +} diff --git a/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php new file mode 100644 index 000000000..3b600313d --- /dev/null +++ b/packages/Ecotone/tests/SymfonyContainer/SymfonyContainerImplementationTest.php @@ -0,0 +1,142 @@ + 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_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_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(); + $externalContainer = InMemoryPSRContainer::createFromAssociativeArray([ + 'externallyDefined' => $logger, + ]); + $container = self::buildContainerFromDefinitions(['aReference' => new Reference('externallyDefined')], $externalContainer); + + self::assertSame($logger, $container->get('aReference')); + } +} + +/** + * licence Apache-2.0 + */ +class WithReferenceToUnknown +{ + public function __construct(public mixed $dependency) + { + } +} diff --git a/packages/Laravel/src/EcotoneProvider.php b/packages/Laravel/src/EcotoneProvider.php index 0fe516e32..2c39a0652 100644 --- a/packages/Laravel/src/EcotoneProvider.php +++ b/packages/Laravel/src/EcotoneProvider.php @@ -2,31 +2,24 @@ 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\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; 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,13 +94,11 @@ 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); - }); - } + $container->registerBridgesInto( + fn (string $referenceName, string $interfaceName, callable $factory) => $this->app->singleton($referenceName, fn () => $factory()), + ); $this->app->singleton( ConfigurationVariableService::REFERENCE_NAME, @@ -127,7 +118,7 @@ function () { ); if ($this->app->runningInConsole()) { - foreach ($definitionHolder->getRegisteredCommands() as $oneTimeCommandConfiguration) { + foreach ($container->getRegisteredConsoleCommands() as $oneTimeCommandConfiguration) { $commandName = $oneTimeCommandConfiguration->getName(); foreach ($oneTimeCommandConfiguration->getParameters() as $parameter) { @@ -193,112 +184,45 @@ 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); - 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 ($definitionHolder) { - return [new ServiceCacheConfiguration($cacheDirectory, true), $definitionHolder]; - } + if ($useProductionCache && $cacheDirectory) { + $serviceCacheConfiguration = new ServiceCacheConfiguration($cacheDirectory, true); + $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, ); - - $definitionHolder = null; - - $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) { - $configuration = MessagingSystemConfiguration::prepareWithAnnotationFinder( + $annotationFinder = $cacheLayout->annotationFinder; + $serviceCacheConfiguration = $cacheLayout->serviceCacheConfiguration; + $cacheHash = $cacheLayout->configHash; + + $container = EcotoneSymfonyContainerFactory::bootstrap( + $serviceCacheConfiguration, + new LaravelConfigurationVariableService(), + $externalContainer, + fn () => MessagingSystemConfiguration::prepareWithAnnotationFinder( $annotationFinder, new LaravelConfigurationVariableService(), $applicationConfiguration, enableTestPackage: $enableTesting - ); - $definitionHolder = ContainerConfig::buildDefinitionHolder($configuration); - - if ($serviceCacheConfiguration->shouldUseCache()) { - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - file_put_contents($messagingSystemCachePath, serialize($definitionHolder)); - } - } + ), + $cacheHash, + ); - 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); + } +} diff --git a/packages/LiteApplication/composer.json b/packages/LiteApplication/composer.json index 8feb1ffec..4b08dc54f 100644 --- a/packages/LiteApplication/composer.json +++ b/packages/LiteApplication/composer.json @@ -49,8 +49,7 @@ }, "require": { "ecotone/ecotone": "~1.316.1", - "ecotone/jms-converter": "~1.316.1", - "php-di/php-di": "^7.0.5" + "ecotone/jms-converter": "~1.316.1" }, "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..74303bf9a --- /dev/null +++ b/packages/LiteApplication/src/AutowiringContainer.php @@ -0,0 +1,90 @@ +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->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..643274d80 100644 --- a/packages/LiteApplication/src/EcotoneLiteApplication.php +++ b/packages/LiteApplication/src/EcotoneLiteApplication.php @@ -4,19 +4,12 @@ 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\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,52 +45,28 @@ 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 { - /** @var MessagingSystemConfiguration $messagingConfiguration */ - $messagingConfiguration = MessagingSystemConfiguration::prepare( - $pathToRootCatalog, - InMemoryConfigurationVariableService::create($configurationVariables), - $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(InMemoryPSRContainer::createFromAssociativeArray(array_merge($classesToRegister, $objectsToRegister))); - $containerBuilder->addCompilerPass($messagingConfiguration); - $containerBuilder->addCompilerPass(new RegisterInterfaceToCallReferences()); - $containerBuilder->addCompilerPass(new PhpDiContainerImplementation($builder, $classesToRegister)); - $containerBuilder->compile(); - - $container = $builder->build(); - } - - $container->set(ServiceCacheConfiguration::class, $serviceCacheConfiguration); + $externalContainer = new AutowiringContainer(InMemoryPSRContainer::createFromAssociativeArray(array_merge($classesToRegister, $objectsToRegister))); $configurationVariableService = InMemoryConfigurationVariableService::create($configurationVariables); - $container->set(ConfigurationVariableService::REFERENCE_NAME, $configurationVariableService); - foreach ($objectsToRegister as $referenceName => $object) { - $container->set($referenceName, $object); - } - foreach ($classesToRegister as $referenceName => $object) { - $container->set(PhpDiContainerImplementation::EXTERNAL_PREFIX.$referenceName, $object); - } + $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); return $container->get(ConfiguredMessagingSystem::class); } @@ -111,11 +80,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; - } - -} 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( diff --git a/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php b/packages/Symfony/DependencyInjection/Compiler/AliasExternalReferenceForTesting.php index 008b2c88b..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,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 = ExternalReferenceResolver::TESTING_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/Compiler/CacheClearer.php b/packages/Symfony/DependencyInjection/Compiler/CacheClearer.php index 4a149709c..29f4f70c9 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' && ! 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)); } } 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 +101,52 @@ 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(); + $containerBuilder->addCompilerPass(new ValidityCheckPass()); + $ecotoneContainer = EcotoneSymfonyContainerFactory::build($containerBuilder, $serviceCacheConfiguration); + + $container->register('ecotone.container', EcotoneContainer::class) + ->setFactory([EcotoneContainerLoader::class, 'load']) + ->setArguments([$cacheDirectory, new Reference('service_container')]) + ->setPublic(true); + + $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']) + ->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->register(ExternalReferenceResolver::TESTING_ALIAS_PREFIX . 'logger', LoggerInterface::class) + ->setFactory([RuntimeInstanceProvider::class, 'provide']) + ->setArguments([new Reference('logger')]) + ->addTag('monolog.logger', ['channel' => 'ecotone']) + ->setPublic(true); - $container->getDefinition(LoggingGateway::class)->addTag('monolog.logger', ['channel' => 'ecotone']); + $container->setParameter('ecotone.external_references', $ecotoneContainer->getExternalReferenceIds()); - foreach ($definitionHolder->getRegisteredCommands() as $oneTimeCommandConfiguration) { + foreach ($ecotoneContainer->getRegisteredConsoleCommands() as $oneTimeCommandConfiguration) { $definition = new Definition(); $definition->setClass(MessagingEntrypointCommand::class); $definition->addArgument($oneTimeCommandConfiguration->getName()); @@ -111,6 +157,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); } } 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]]; - } - } -} 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 3c83b1fb8..a013cad0e 100644 --- a/packages/Tempest/src/MessagingSystemInitializer.php +++ b/packages/Tempest/src/MessagingSystemInitializer.php @@ -6,16 +6,13 @@ 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\MessagingSystemConfiguration; use Ecotone\Messaging\Config\ServiceCacheConfiguration; use Ecotone\Messaging\Config\ServiceConfiguration; use Ecotone\Messaging\ConfigurationVariableService; -use Psr\Log\LoggerInterface; +use Ecotone\SymfonyContainer\ContainerCacheLayout; +use Ecotone\SymfonyContainer\EcotoneSymfonyContainerFactory; use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; @@ -30,17 +27,15 @@ final class MessagingSystemInitializer implements Initializer { public const MESSAGING_SYSTEM_FILE_NAME = 'messaging_system'; - 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 @@ -55,7 +50,7 @@ public static function getProxyDirectory(): ?string public static function clearDefinitionHolder(): void { - self::$definitionHolder = null; + self::$registeredCommands = null; self::$configHash = null; self::$proxyDirectory = null; } @@ -70,36 +65,20 @@ 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 = $ecotoneContainer->getRegisteredConsoleCommands(); 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($ecotoneContainer->getDefinedServiceIds()); return $ecotoneContainer->get(ConfiguredMessagingSystem::class); } @@ -129,16 +108,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, @@ -186,87 +155,46 @@ 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); - if ($definitionHolder instanceof ContainerDefinitionsHolder) { - $persistedHash = $this->readPersistedConfigHash($cacheDirectory); - - return [new ServiceCacheConfiguration($cacheDirectory, true), $definitionHolder, $persistedHash]; - } + if ($useProductionCache && $cacheDirectory) { + $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; - $definitionHolder = null; - $messagingSystemCachePath = $serviceCacheConfiguration->getPath() . DIRECTORY_SEPARATOR . self::MESSAGING_SYSTEM_FILE_NAME; - - if ($serviceCacheConfiguration->shouldUseCache() && file_exists($messagingSystemCachePath)) { - $definitionHolder = unserialize(file_get_contents($messagingSystemCachePath)); - } - - if (! $definitionHolder instanceof ContainerDefinitionsHolder) { - $configuration = MessagingSystemConfiguration::prepareWithAnnotationFinder( + $ecotoneContainer = EcotoneSymfonyContainerFactory::bootstrap( + $serviceCacheConfiguration, + new TempestConfigurationVariableService(), + $externalContainer, + fn () => MessagingSystemConfiguration::prepareWithAnnotationFinder( $annotationFinder, new TempestConfigurationVariableService(), $applicationConfiguration, enableTestPackage: $enableTesting, - ); - $definitionHolder = ContainerConfig::buildDefinitionHolder($configuration); - - if ($serviceCacheConfiguration->shouldUseCache()) { - MessagingSystemConfiguration::prepareCacheDirectory($serviceCacheConfiguration); - file_put_contents($messagingSystemCachePath, serialize($definitionHolder)); - - if ($useProductionCache && $cacheHash !== null) { - $this->persistConfigHash($cacheDirectory, $cacheHash); - } - } - } - - return [$serviceCacheConfiguration, $definitionHolder, $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); + ), + $cacheHash, + ); - return $hash !== false && $hash !== '' ? $hash : null; + return [$ecotoneContainer, $cacheHash]; } } 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; + } }