diff --git a/config/definition.php b/config/definition.php new file mode 100644 index 00000000..a1e65943 --- /dev/null +++ b/config/definition.php @@ -0,0 +1,137 @@ +rootNode(); + + $rootNode + ->fixXmlConfig('mapping_path') + ->fixXmlConfig('route_ignore_pattern') + ->children() + ->arrayNode('mapping_paths') + ->info('List of files or directories where Purgatory will look for additional purge definitions.') + ->scalarPrototype()->end() + ->defaultValue([]) + ->end() + ->arrayNode('route_ignore_patterns') + ->info('Route names that match the given regular expressions will be ignored.') + ->example(['/^_profiler/', '/^_wdt/']) + ->scalarPrototype()->end() + ->defaultValue([]) + ->end() + ->arrayNode('doctrine_middleware') + ->canBeDisabled() + ->children() + ->integerNode('priority') + ->info('Explicitly set the priority of Purgatory\'s Doctrine middleware.') + ->defaultNull() + ->end() + ->end() + ->end() + ->arrayNode('doctrine_event_listener_priorities') + ->info('Explicitly set the priorities of Purgatory\'s Doctrine event listener.') + ->beforeNormalization() + ->ifTrue(static fn (mixed $priority): bool => \is_int($priority)) + ->then(static fn (int $priority): array => [ + DoctrineEvents::preRemove => $priority, + DoctrineEvents::postPersist => $priority, + DoctrineEvents::postUpdate => $priority, + DoctrineEvents::postFlush => $priority, + ]) + ->end() + ->addDefaultsIfNotSet() + ->children() + ->integerNode(DoctrineEvents::preRemove)->defaultNull()->end() + ->integerNode(DoctrineEvents::postPersist)->defaultNull()->end() + ->integerNode(DoctrineEvents::postUpdate)->defaultNull()->end() + ->integerNode(DoctrineEvents::postFlush) + ->info('This event is not registered when the Doctrine middleware is enabled.') + ->defaultNull() + ->end() + ->end() + ->end() + ->arrayNode('purger') + ->fixXmlConfig('host') + ->addDefaultsIfNotSet() + ->beforeNormalization() + ->ifString() + ->then(static fn (string $purger): array => ['name' => $purger]) + ->end() + ->validate() + ->ifTrue(static fn (array $purger): bool => 'varnish' === $purger['name'] && !class_exists(HttpClient::class)) + ->thenInvalid('The Varnish purger requires Symfony\'s HTTP client component to be installed. Try running "composer require symfony/http-client".') + ->end() + ->children() + ->scalarNode('name') + ->info(\sprintf('The ID of a service that implements the "%s" interface', PurgerInterface::class)) + ->example('symfony') + ->defaultNull() + ->end() + ->arrayNode('hosts') + ->info('The hosts from which URLs should be purged') + ->scalarPrototype() + ->validate()->always(static fn (string $host): string => rtrim($host, '/'))->end() + ->end() + ->defaultValue([]) + ->end() + ->scalarNode('http_client') + ->info('The service ID of the HTTP client to use, must be an instance of Symfony\'s HTTP client') + ->defaultNull() + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->arrayNode('messenger') + ->addDefaultsIfNotSet() + ->beforeNormalization() + ->ifString() + ->then(static fn (string $messenger): array => ['transport' => $messenger]) + ->end() + ->validate() + ->ifTrue(static fn (array $messenger): bool => !interface_exists(MessageBusInterface::class) && array_filter($messenger)) + ->thenInvalid('Messenger support cannot be enabled as the component is not installed. Try running "composer require symfony/messenger".') + ->end() + ->validate() + ->ifTrue(static fn (array $messenger): bool => !$messenger['transport'] && $messenger['bus']) + ->thenInvalid('Cannot set the messenger bus without defining the transport.') + ->end() + ->validate() + ->ifTrue(static fn (array $messenger): bool => !$messenger['transport'] && $messenger['batch_size']) + ->thenInvalid('Cannot set the batch size without defining the transport.') + ->end() + ->children() + ->scalarNode('transport') + ->info('Set the name of the messenger transport to use') + ->defaultNull() + ->cannotBeEmpty() + ->end() + ->scalarNode('bus') + ->info('Set the name of the messenger bus to use') + ->defaultNull() + ->cannotBeEmpty() + ->end() + ->integerNode('batch_size') + ->info('Set the number of urls to dispatch per message') + ->defaultNull() + ->validate() + ->ifTrue(static fn (int $batchSize): bool => !($batchSize > 0)) + ->thenInvalid('The batch size must be a number greater than 0.') + ->end() + ->end() + ->end() + ->end() + ->booleanNode('profiler_integration') + ->info('Enables the data collector and profiler panel if the profiler is enabled.') + ->defaultTrue() + ->end() + ->end() + ; +}; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 2d57336b..60531db3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -72,23 +72,29 @@ parameters: count: 1 path: src/Cache/RouteMetadata/YamlMetadataProvider.php + - + message: '#^Comparison operation "\<" between 80101 and 80100 is always false\.$#' + identifier: smaller.alwaysFalse + count: 1 + path: src/PurgatoryBundle.php + - message: '#^Parameter \#2 \$configurator of method Symfony\\Component\\DependencyInjection\\ContainerBuilder\:\:registerAttributeForAutoconfiguration\(\) expects callable\(Symfony\\Component\\DependencyInjection\\ChildDefinition, Sofascore\\PurgatoryBundle\\Attribute\\AsExpressionLanguageFunction, Reflector\)\: void, Closure\(Symfony\\Component\\DependencyInjection\\ChildDefinition, Sofascore\\PurgatoryBundle\\Attribute\\AsExpressionLanguageFunction, ReflectionClass\|ReflectionMethod\)\: void given\.$#' identifier: argument.type count: 1 - path: src/DependencyInjection/PurgatoryExtension.php + path: src/PurgatoryBundle.php - message: '#^Parameter \#2 \$configurator of method Symfony\\Component\\DependencyInjection\\ContainerBuilder\:\:registerAttributeForAutoconfiguration\(\) expects callable\(Symfony\\Component\\DependencyInjection\\ChildDefinition, Sofascore\\PurgatoryBundle\\Attribute\\AsRouteParamService, Reflector\)\: void, Closure\(Symfony\\Component\\DependencyInjection\\ChildDefinition, Sofascore\\PurgatoryBundle\\Attribute\\AsRouteParamService, ReflectionClass\|ReflectionMethod\)\: void given\.$#' identifier: argument.type count: 1 - path: src/DependencyInjection/PurgatoryExtension.php + path: src/PurgatoryBundle.php - message: '#^Parameter \#2 \$configurator of method Symfony\\Component\\DependencyInjection\\ContainerBuilder\:\:registerAttributeForAutoconfiguration\(\) expects callable\(Symfony\\Component\\DependencyInjection\\ChildDefinition, Sofascore\\PurgatoryBundle\\Attribute\\PurgeOn, Reflector\)\: void, Closure\(Symfony\\Component\\DependencyInjection\\ChildDefinition, Sofascore\\PurgatoryBundle\\Attribute\\PurgeOn, ReflectionClass\|ReflectionMethod\)\: void given\.$#' identifier: argument.type count: 1 - path: src/DependencyInjection/PurgatoryExtension.php + path: src/PurgatoryBundle.php - message: '#^Trait Sofascore\\PurgatoryBundle\\Test\\InteractsWithPurgatory is used zero times and is not analysed\.$#' diff --git a/phpstan.dist.neon b/phpstan.dist.neon index caee468d..36310321 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -6,5 +6,4 @@ parameters: paths: - src excludePaths: - - src/DependencyInjection/Configuration.php - src/Doctrine/DBAL/PurgatoryConnection.php diff --git a/src/DependencyInjection/BundleExtensionWrapper.php b/src/DependencyInjection/BundleExtensionWrapper.php new file mode 100644 index 00000000..f4c58fbd --- /dev/null +++ b/src/DependencyInjection/BundleExtensionWrapper.php @@ -0,0 +1,58 @@ + $config + */ + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface + { + return $this->extension->getConfiguration($config, $container); + } + + public function getAlias(): string + { + return $this->extension->getAlias(); + } + + public function prepend(ContainerBuilder $container): void + { + $this->extension->prepend($container); + } + + public function load(array $configs, ContainerBuilder $container): void + { + $this->extension->load($configs, $container); + } + + public function getNamespace(): string + { + return 'http://sofascore.com/schema/dic/purgatory'; + } + + public function getXsdValidationBasePath(): string + { + return \dirname(__DIR__, 2).'/config/schema'; + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php deleted file mode 100644 index 57db1d9c..00000000 --- a/src/DependencyInjection/Configuration.php +++ /dev/null @@ -1,146 +0,0 @@ -getRootNode(); - - $rootNode - ->fixXmlConfig('mapping_path') - ->fixXmlConfig('route_ignore_pattern') - ->children() - ->arrayNode('mapping_paths') - ->info('List of files or directories where Purgatory will look for additional purge definitions.') - ->scalarPrototype()->end() - ->defaultValue([]) - ->end() - ->arrayNode('route_ignore_patterns') - ->info('Route names that match the given regular expressions will be ignored.') - ->example(['/^_profiler/', '/^_wdt/']) - ->scalarPrototype()->end() - ->defaultValue([]) - ->end() - ->arrayNode('doctrine_middleware') - ->canBeDisabled() - ->children() - ->integerNode('priority') - ->info('Explicitly set the priority of Purgatory\'s Doctrine middleware.') - ->defaultNull() - ->end() - ->end() - ->end() - ->arrayNode('doctrine_event_listener_priorities') - ->info('Explicitly set the priorities of Purgatory\'s Doctrine event listener.') - ->beforeNormalization() - ->ifTrue(static fn (mixed $priority): bool => \is_int($priority)) - ->then(static fn (int $priority): array => [ - DoctrineEvents::preRemove => $priority, - DoctrineEvents::postPersist => $priority, - DoctrineEvents::postUpdate => $priority, - DoctrineEvents::postFlush => $priority, - ]) - ->end() - ->addDefaultsIfNotSet() - ->children() - ->integerNode(DoctrineEvents::preRemove)->defaultNull()->end() - ->integerNode(DoctrineEvents::postPersist)->defaultNull()->end() - ->integerNode(DoctrineEvents::postUpdate)->defaultNull()->end() - ->integerNode(DoctrineEvents::postFlush) - ->info('This event is not registered when the Doctrine middleware is enabled.') - ->defaultNull() - ->end() - ->end() - ->end() - ->arrayNode('purger') - ->fixXmlConfig('host') - ->addDefaultsIfNotSet() - ->beforeNormalization() - ->ifString() - ->then(static fn (string $purger): array => ['name' => $purger]) - ->end() - ->validate() - ->ifTrue(static fn (array $purger): bool => 'varnish' === $purger['name'] && !class_exists(HttpClient::class)) - ->thenInvalid('The Varnish purger requires Symfony\'s HTTP client component to be installed. Try running "composer require symfony/http-client".') - ->end() - ->children() - ->scalarNode('name') - ->info(\sprintf('The ID of a service that implements the "%s" interface', PurgerInterface::class)) - ->example('symfony') - ->defaultNull() - ->end() - ->arrayNode('hosts') - ->info('The hosts from which URLs should be purged') - ->scalarPrototype() - ->validate()->always(static fn (string $host): string => rtrim($host, '/'))->end() - ->end() - ->defaultValue([]) - ->end() - ->scalarNode('http_client') - ->info('The service ID of the HTTP client to use, must be an instance of Symfony\'s HTTP client') - ->defaultNull() - ->cannotBeEmpty() - ->end() - ->end() - ->end() - ->arrayNode('messenger') - ->addDefaultsIfNotSet() - ->beforeNormalization() - ->ifString() - ->then(static fn (string $messenger): array => ['transport' => $messenger]) - ->end() - ->validate() - ->ifTrue(static fn (array $messenger): bool => !interface_exists(MessageBusInterface::class) && array_filter($messenger)) - ->thenInvalid('Messenger support cannot be enabled as the component is not installed. Try running "composer require symfony/messenger".') - ->end() - ->validate() - ->ifTrue(static fn (array $messenger): bool => !$messenger['transport'] && $messenger['bus']) - ->thenInvalid('Cannot set the messenger bus without defining the transport.') - ->end() - ->validate() - ->ifTrue(static fn (array $messenger): bool => !$messenger['transport'] && $messenger['batch_size']) - ->thenInvalid('Cannot set the batch size without defining the transport.') - ->end() - ->children() - ->scalarNode('transport') - ->info('Set the name of the messenger transport to use') - ->defaultNull() - ->cannotBeEmpty() - ->end() - ->scalarNode('bus') - ->info('Set the name of the messenger bus to use') - ->defaultNull() - ->cannotBeEmpty() - ->end() - ->integerNode('batch_size') - ->info('Set the number of urls to dispatch per message') - ->defaultNull() - ->validate() - ->ifTrue(static fn (int $batchSize): bool => !($batchSize > 0)) - ->thenInvalid('The batch size must be a number greater than 0.') - ->end() - ->end() - ->end() - ->end() - ->booleanNode('profiler_integration') - ->info('Enables the data collector and profiler panel if the profiler is enabled.') - ->defaultTrue() - ->end() - ->end() - ; - - return $treeBuilder; - } -} diff --git a/src/DependencyInjection/CompilerPass/ControllerClassMapPass.php b/src/DependencyInjection/ControllerClassMapCompilerPass.php similarity index 84% rename from src/DependencyInjection/CompilerPass/ControllerClassMapPass.php rename to src/DependencyInjection/ControllerClassMapCompilerPass.php index bf375090..698d96cc 100644 --- a/src/DependencyInjection/CompilerPass/ControllerClassMapPass.php +++ b/src/DependencyInjection/ControllerClassMapCompilerPass.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass; +namespace Sofascore\PurgatoryBundle\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -final class ControllerClassMapPass implements CompilerPassInterface +final class ControllerClassMapCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { diff --git a/src/DependencyInjection/PurgatoryExtension.php b/src/DependencyInjection/PurgatoryExtension.php deleted file mode 100644 index 18e6167f..00000000 --- a/src/DependencyInjection/PurgatoryExtension.php +++ /dev/null @@ -1,268 +0,0 @@ -processConfiguration( - new Configuration(), - $container->getExtensionConfig($this->getAlias()), - ); - - if (null !== $transport = $mergedConfig['messenger']['transport']) { - $container->prependExtensionConfig('framework', [ - 'messenger' => [ - 'routing' => [ - PurgeMessage::class => $transport, - ], - ], - ]); - } - } - - /** - * @param array $mergedConfig - */ - protected function loadInternal(array $mergedConfig, ContainerBuilder $container): void - { - $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__, 2).'/config')); - $loader->load('services.php'); - - if ($mergedConfig['profiler_integration']) { - $loader->load('services_debug.php'); - } - - $container->registerAttributeForAutoconfiguration( - PurgeOn::class, - static function (ChildDefinition $definition, PurgeOn $attribute, \ReflectionClass|\ReflectionMethod $reflection): void { - $definition->addTag( - name: 'purgatory.purge_on', - attributes: [ - 'class' => $reflection instanceof \ReflectionMethod ? $reflection->class : $reflection->name, - ], - ); - }, - ); - - $container->registerAttributeForAutoconfiguration( - AsRouteParamService::class, - static function (ChildDefinition $definition, AsRouteParamService $attribute, \ReflectionClass|\ReflectionMethod $reflection): void { - $definition->addTag( - name: 'purgatory.route_parameter_service', - attributes: [ - 'alias' => $attribute->alias, - 'method' => $reflection instanceof \ReflectionMethod - ? $reflection->name - : ($reflection->hasMethod('__invoke') ? '__invoke' - : throw new RuntimeException(\sprintf('Invalid route parameter service, the method "%s::__invoke()" does not exist.', $reflection->name))), - ], - ); - }, - ); - - $container->registerAttributeForAutoconfiguration( - AsExpressionLanguageFunction::class, - static function (ChildDefinition $definition, AsExpressionLanguageFunction $attribute, \ReflectionClass|\ReflectionMethod $reflection): void { - $definition->addTag( - name: 'purgatory.expression_language_function', - attributes: [ - 'function' => $attribute->functionName, - 'method' => $reflection instanceof \ReflectionMethod - ? $reflection->name - : ($reflection->hasMethod('__invoke') ? '__invoke' - : throw new RuntimeException(\sprintf('Invalid expression language function, the method "%s::__invoke()" does not exist.', $reflection->name))), - ], - ); - }, - ); - - /** @var array{name: ?string, hosts: list, http_client: ?string} $purgerConfig */ - $purgerConfig = $mergedConfig['purger']; - $container->setParameter('.sofascore.purgatory.purger.name', $purgerConfig['name']); - $container->setParameter('.sofascore.purgatory.purger.hosts', $purgerConfig['hosts']); - - if (null !== $purgerConfig['http_client']) { - $container->getDefinition('sofascore.purgatory.purger.varnish') - ->replaceArgument(0, new Reference($purgerConfig['http_client'])); - } - - /** @var list $mappingPaths */ - $mappingPaths = $mergedConfig['mapping_paths']; - if ($files = iterator_to_array($this->registerMappingFiles($container, $mappingPaths), false)) { - if (!class_exists(YamlParser::class)) { - throw new LogicException('You cannot use YAML configuration because the Symfony Yaml component is not installed. Try running "composer require symfony/yaml".'); - } - - $container->getDefinition('sofascore.purgatory.route_metadata_provider.yaml') - ->replaceArgument(1, $files); - } else { - $container->removeDefinition('sofascore.purgatory.route_metadata_provider.yaml'); - } - - $container->getDefinition('sofascore.purgatory.route_metadata_provider.attribute') - ->setArgument(2, $mergedConfig['route_ignore_patterns']); - - /** @var array $doctrineEventListenerPriorities */ - $doctrineEventListenerPriorities = $mergedConfig['doctrine_event_listener_priorities']; - - /** @var array{enabled: bool, priority: ?int} $doctrineMiddlewareConfig */ - $doctrineMiddlewareConfig = $mergedConfig['doctrine_middleware']; - if ($doctrineMiddlewareConfig['enabled']) { - $container->getDefinition('sofascore.purgatory.doctrine_middleware') - ->addTag( - name: 'doctrine.middleware', - attributes: null !== $doctrineMiddlewareConfig['priority'] ? ['priority' => $doctrineMiddlewareConfig['priority']] : [], - ); - - unset($doctrineEventListenerPriorities[DoctrineEvents::postFlush]); - } else { - $container->removeDefinition('sofascore.purgatory.doctrine_middleware'); - } - - $listenerDefinition = $container->getDefinition('sofascore.purgatory.entity_change_listener'); - foreach ($doctrineEventListenerPriorities as $event => $priority) { - $listenerDefinition->addTag( - name: 'doctrine.event_listener', - attributes: ['event' => $event] + (null !== $priority ? ['priority' => $priority] : []), - ); - } - - /** @var array{transport: ?string, bus: ?string, batch_size: ?positive-int} $messengerConfig */ - $messengerConfig = $mergedConfig['messenger']; - if (null !== $messengerConfig['transport']) { - $container->setParameter('.sofascore.purgatory.purger.async_transport', $messengerConfig['transport']); - if (null !== $messengerConfig['bus']) { - $container->getDefinition('sofascore.purgatory.purger.async') - ->replaceArgument(0, new Reference($messengerConfig['bus'])); - } - if (null !== $messengerConfig['batch_size']) { - $container->getDefinition('sofascore.purgatory.purger.async') - ->setArgument(1, $messengerConfig['batch_size']); - } - $container->getDefinition('sofascore.purgatory.purge_message_handler') - ->addTag( - name: 'messenger.message_handler', - attributes: null !== $messengerConfig['bus'] ? ['bus' => $messengerConfig['bus']] : [], - ); - } else { - $container->setParameter('.sofascore.purgatory.purger.async_transport', null); - $container->removeDefinition('sofascore.purgatory.purger.async'); - $container->removeDefinition('sofascore.purgatory.purge_message_handler'); - } - - $container->registerForAutoconfiguration(SubscriptionResolverInterface::class) - ->addTag('purgatory.subscription_resolver'); - - $container->registerForAutoconfiguration(InverseValuesBuilderInterface::class) - ->addTag('purgatory.inverse_values_builder'); - - $container->registerForAutoconfiguration(TargetResolverInterface::class) - ->addTag('purgatory.target_resolver'); - - $container->registerForAutoconfiguration(RouteProviderInterface::class) - ->addTag('purgatory.route_provider'); - - $container->registerForAutoconfiguration(ValuesResolverInterface::class) - ->addTag('purgatory.route_param_value_resolver'); - - if (!class_exists(HttpClient::class)) { - $container->removeDefinition('sofascore.purgatory.purger.varnish'); - } - - if (!class_exists(ExpressionLanguage::class)) { - $container->removeDefinition('sofascore.purgatory.expression_language'); - } - } - - /** - * @param list $mappingPaths - * - * @return \Generator - */ - private function registerMappingFiles(ContainerBuilder $container, array $mappingPaths): \Generator - { - $registerMappingFilesFromDir = static function (string $dir): iterable { - foreach (Finder::create()->followLinks()->files()->in($dir)->name('/\.ya?ml$/')->sortByName() as $file) { - yield $file->getRealPath(); - } - }; - - if ($container->hasParameter('.kernel.config_dir')) { - /** @var string $configDir */ - $configDir = $container->getParameter('.kernel.config_dir'); - } else { - /** @var string $projectDir */ - $projectDir = $container->getParameter('kernel.project_dir'); - $configDir = $projectDir.'/config'; - } - - if ($container->fileExists($dir = $configDir.'/purgatory', '/^$/')) { - yield from $registerMappingFilesFromDir($dir); - } - - foreach ($mappingPaths as $path) { - if (is_dir($path)) { - $container->addResource(new DirectoryResource($path, '/^$/')); - yield from $registerMappingFilesFromDir($path); - } elseif ($container->fileExists($path)) { - yield $path; - } else { - throw new RuntimeException(\sprintf('Could not open file or directory "%s".', $path)); - } - } - } - - public function process(ContainerBuilder $container): void - { - if (!$container->hasDefinition('cache.system')) { - $container->removeDefinition('sofascore.purgatory.cache.expression_language'); - } - - if (!$container->hasDefinition('profiler') || !$container->hasDefinition('twig')) { - $container->removeDefinition('sofascore.purgatory.data_collector'); - $container->removeDefinition('sofascore.purgatory.purger.traceable'); - $container->removeDefinition('sofascore.purgatory.purger.sync.traceable'); - } - } - - public function getNamespace(): string - { - return 'http://sofascore.com/schema/dic/purgatory'; - } - - public function getXsdValidationBasePath(): string - { - return __DIR__.'/../../config/schema'; - } -} diff --git a/src/DependencyInjection/CompilerPass/RegisterExpressionLanguageProvidersPass.php b/src/DependencyInjection/RegisterExpressionLanguageProvidersCompilerPass.php similarity index 93% rename from src/DependencyInjection/CompilerPass/RegisterExpressionLanguageProvidersPass.php rename to src/DependencyInjection/RegisterExpressionLanguageProvidersCompilerPass.php index 468d0df9..783be56b 100644 --- a/src/DependencyInjection/CompilerPass/RegisterExpressionLanguageProvidersPass.php +++ b/src/DependencyInjection/RegisterExpressionLanguageProvidersCompilerPass.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass; +namespace Sofascore\PurgatoryBundle\DependencyInjection; use Sofascore\PurgatoryBundle\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -11,7 +11,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -final class RegisterExpressionLanguageProvidersPass implements CompilerPassInterface +final class RegisterExpressionLanguageProvidersCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { diff --git a/src/DependencyInjection/CompilerPass/RegisterPurgerPass.php b/src/DependencyInjection/RegisterPurgerCompilerPass.php similarity index 93% rename from src/DependencyInjection/CompilerPass/RegisterPurgerPass.php rename to src/DependencyInjection/RegisterPurgerCompilerPass.php index 752114a2..7fbeac67 100644 --- a/src/DependencyInjection/CompilerPass/RegisterPurgerPass.php +++ b/src/DependencyInjection/RegisterPurgerCompilerPass.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass; +namespace Sofascore\PurgatoryBundle\DependencyInjection; use Sofascore\PurgatoryBundle\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -final class RegisterPurgerPass implements CompilerPassInterface +final class RegisterPurgerCompilerPass implements CompilerPassInterface { /** * {@inheritDoc} diff --git a/src/DependencyInjection/CompilerPass/RegisterRouteParamServicesPass.php b/src/DependencyInjection/RegisterRouteParamServicesCompilerPass.php similarity index 92% rename from src/DependencyInjection/CompilerPass/RegisterRouteParamServicesPass.php rename to src/DependencyInjection/RegisterRouteParamServicesCompilerPass.php index b16ffb6b..d12586e8 100644 --- a/src/DependencyInjection/CompilerPass/RegisterRouteParamServicesPass.php +++ b/src/DependencyInjection/RegisterRouteParamServicesCompilerPass.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass; +namespace Sofascore\PurgatoryBundle\DependencyInjection; use Sofascore\PurgatoryBundle\Exception\RuntimeException; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -11,7 +11,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -final class RegisterRouteParamServicesPass implements CompilerPassInterface +final class RegisterRouteParamServicesCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { diff --git a/src/PurgatoryBundle.php b/src/PurgatoryBundle.php index f69357b4..c72a7986 100644 --- a/src/PurgatoryBundle.php +++ b/src/PurgatoryBundle.php @@ -4,25 +4,309 @@ namespace Sofascore\PurgatoryBundle; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\ControllerClassMapPass; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterExpressionLanguageProvidersPass; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterPurgerPass; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterRouteParamServicesPass; +use Doctrine\ORM\Events as DoctrineEvents; +use Sofascore\PurgatoryBundle\Attribute\AsExpressionLanguageFunction; +use Sofascore\PurgatoryBundle\Attribute\AsRouteParamService; +use Sofascore\PurgatoryBundle\Attribute\PurgeOn; +use Sofascore\PurgatoryBundle\Cache\PropertyResolver\InverseValuesBuilder\InverseValuesBuilderInterface; +use Sofascore\PurgatoryBundle\Cache\PropertyResolver\SubscriptionResolverInterface; +use Sofascore\PurgatoryBundle\Cache\TargetResolver\TargetResolverInterface; +use Sofascore\PurgatoryBundle\DependencyInjection\BundleExtensionWrapper; +use Sofascore\PurgatoryBundle\DependencyInjection\ControllerClassMapCompilerPass; +use Sofascore\PurgatoryBundle\DependencyInjection\RegisterExpressionLanguageProvidersCompilerPass; +use Sofascore\PurgatoryBundle\DependencyInjection\RegisterPurgerCompilerPass; +use Sofascore\PurgatoryBundle\DependencyInjection\RegisterRouteParamServicesCompilerPass; +use Sofascore\PurgatoryBundle\Exception\LogicException; +use Sofascore\PurgatoryBundle\Exception\RuntimeException; +use Sofascore\PurgatoryBundle\Purger\Messenger\PurgeMessage; +use Sofascore\PurgatoryBundle\RouteParamValueResolver\ValuesResolverInterface; +use Sofascore\PurgatoryBundle\RouteProvider\RouteProviderInterface; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator; +use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Finder\Finder; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\HttpKernel\Bundle\AbstractBundle; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Yaml\Parser as YamlParser; -final class PurgatoryBundle extends Bundle +final class PurgatoryBundle extends AbstractBundle implements CompilerPassInterface { + private ?BundleExtensionWrapper $wrappedExtension = null; + public function build(ContainerBuilder $container): void { - $container->addCompilerPass(new ControllerClassMapPass()); - $container->addCompilerPass(new RegisterExpressionLanguageProvidersPass()); - $container->addCompilerPass(new RegisterPurgerPass()); - $container->addCompilerPass(new RegisterRouteParamServicesPass()); + // @TODO Remove when Symfony <8.1 support is dropped, {@see https://github.com/symfony/symfony/pull/62800} + if (Kernel::VERSION_ID < 80100) { + $container->addCompilerPass($this, PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000); + } + + $container->addCompilerPass(new ControllerClassMapCompilerPass()); + $container->addCompilerPass(new RegisterExpressionLanguageProvidersCompilerPass()); + $container->addCompilerPass(new RegisterPurgerCompilerPass()); + $container->addCompilerPass(new RegisterRouteParamServicesCompilerPass()); + } + + public function configure(DefinitionConfigurator $definition): void + { + $definition->import(\dirname(__DIR__).'/config/definition.php'); + } + + public function prependExtension(ContainerConfigurator $configurator, ContainerBuilder $container): void + { + /** @var ConfigurationInterface $configuration */ + $configuration = $this->getContainerExtension()->getConfiguration([], $container); + /** @var array{messenger: array{transport: ?string}} $config */ + $config = (new Processor())->processConfiguration( + $configuration, + $container->getExtensionConfig($this->extensionAlias), + ); + + if (null !== $transport = $config['messenger']['transport']) { + $container->prependExtensionConfig('framework', [ + 'messenger' => [ + 'routing' => [ + PurgeMessage::class => $transport, + ], + ], + ]); + } } - public function getPath(): string + /** + * @param array $config + */ + public function loadExtension(array $config, ContainerConfigurator $configurator, ContainerBuilder $container): void { - return \dirname(__DIR__); + $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/config')); + $loader->load('services.php'); + + if ($config['profiler_integration']) { + $loader->load('services_debug.php'); + } + + $container->registerAttributeForAutoconfiguration( + PurgeOn::class, + static function (ChildDefinition $definition, PurgeOn $attribute, \ReflectionClass|\ReflectionMethod $reflection): void { + $definition->addTag( + name: 'purgatory.purge_on', + attributes: [ + 'class' => $reflection instanceof \ReflectionMethod ? $reflection->class : $reflection->name, + ], + ); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsRouteParamService::class, + static function (ChildDefinition $definition, AsRouteParamService $attribute, \ReflectionClass|\ReflectionMethod $reflection): void { + $definition->addTag( + name: 'purgatory.route_parameter_service', + attributes: [ + 'alias' => $attribute->alias, + 'method' => $reflection instanceof \ReflectionMethod + ? $reflection->name + : ($reflection->hasMethod('__invoke') ? '__invoke' + : throw new RuntimeException(\sprintf('Invalid route parameter service, the method "%s::__invoke()" does not exist.', $reflection->name))), + ], + ); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsExpressionLanguageFunction::class, + static function (ChildDefinition $definition, AsExpressionLanguageFunction $attribute, \ReflectionClass|\ReflectionMethod $reflection): void { + $definition->addTag( + name: 'purgatory.expression_language_function', + attributes: [ + 'function' => $attribute->functionName, + 'method' => $reflection instanceof \ReflectionMethod + ? $reflection->name + : ($reflection->hasMethod('__invoke') ? '__invoke' + : throw new RuntimeException(\sprintf('Invalid expression language function, the method "%s::__invoke()" does not exist.', $reflection->name))), + ], + ); + }, + ); + + /** @var array{name: ?string, hosts: list, http_client: ?string} $purgerConfig */ + $purgerConfig = $config['purger']; + $container->setParameter('.sofascore.purgatory.purger.name', $purgerConfig['name']); + $container->setParameter('.sofascore.purgatory.purger.hosts', $purgerConfig['hosts']); + + if (null !== $purgerConfig['http_client']) { + $container->getDefinition('sofascore.purgatory.purger.varnish') + ->replaceArgument(0, new Reference($purgerConfig['http_client'])); + } + + /** @var list $mappingPaths */ + $mappingPaths = $config['mapping_paths']; + if ($files = iterator_to_array($this->registerMappingFiles($container, $mappingPaths), false)) { + if (!class_exists(YamlParser::class)) { + throw new LogicException('You cannot use YAML configuration because the Symfony Yaml component is not installed. Try running "composer require symfony/yaml".'); + } + + $container->getDefinition('sofascore.purgatory.route_metadata_provider.yaml') + ->replaceArgument(1, $files); + } else { + $container->removeDefinition('sofascore.purgatory.route_metadata_provider.yaml'); + } + + $container->getDefinition('sofascore.purgatory.route_metadata_provider.attribute') + ->setArgument(2, $config['route_ignore_patterns']); + + /** @var array $doctrineEventListenerPriorities */ + $doctrineEventListenerPriorities = $config['doctrine_event_listener_priorities']; + + /** @var array{enabled: bool, priority: ?int} $doctrineMiddlewareConfig */ + $doctrineMiddlewareConfig = $config['doctrine_middleware']; + if ($doctrineMiddlewareConfig['enabled']) { + $container->getDefinition('sofascore.purgatory.doctrine_middleware') + ->addTag( + name: 'doctrine.middleware', + attributes: null !== $doctrineMiddlewareConfig['priority'] ? ['priority' => $doctrineMiddlewareConfig['priority']] : [], + ); + + unset($doctrineEventListenerPriorities[DoctrineEvents::postFlush]); + } else { + $container->removeDefinition('sofascore.purgatory.doctrine_middleware'); + } + + $listenerDefinition = $container->getDefinition('sofascore.purgatory.entity_change_listener'); + foreach ($doctrineEventListenerPriorities as $event => $priority) { + $listenerDefinition->addTag( + name: 'doctrine.event_listener', + attributes: ['event' => $event] + (null !== $priority ? ['priority' => $priority] : []), + ); + } + + /** @var array{transport: ?string, bus: ?string, batch_size: ?positive-int} $messengerConfig */ + $messengerConfig = $config['messenger']; + if (null !== $messengerConfig['transport']) { + $container->setParameter('.sofascore.purgatory.purger.async_transport', $messengerConfig['transport']); + if (null !== $messengerConfig['bus']) { + $container->getDefinition('sofascore.purgatory.purger.async') + ->replaceArgument(0, new Reference($messengerConfig['bus'])); + } + if (null !== $messengerConfig['batch_size']) { + $container->getDefinition('sofascore.purgatory.purger.async') + ->setArgument(1, $messengerConfig['batch_size']); + } + $container->getDefinition('sofascore.purgatory.purge_message_handler') + ->addTag( + name: 'messenger.message_handler', + attributes: null !== $messengerConfig['bus'] ? ['bus' => $messengerConfig['bus']] : [], + ); + } else { + $container->setParameter('.sofascore.purgatory.purger.async_transport', null); + $container->removeDefinition('sofascore.purgatory.purger.async'); + $container->removeDefinition('sofascore.purgatory.purge_message_handler'); + } + + $container->registerForAutoconfiguration(SubscriptionResolverInterface::class) + ->addTag('purgatory.subscription_resolver'); + + $container->registerForAutoconfiguration(InverseValuesBuilderInterface::class) + ->addTag('purgatory.inverse_values_builder'); + + $container->registerForAutoconfiguration(TargetResolverInterface::class) + ->addTag('purgatory.target_resolver'); + + $container->registerForAutoconfiguration(RouteProviderInterface::class) + ->addTag('purgatory.route_provider'); + + $container->registerForAutoconfiguration(ValuesResolverInterface::class) + ->addTag('purgatory.route_param_value_resolver'); + + if (!class_exists(HttpClient::class)) { + $container->removeDefinition('sofascore.purgatory.purger.varnish'); + } + + if (!class_exists(ExpressionLanguage::class)) { + $container->removeDefinition('sofascore.purgatory.expression_language'); + } + } + + /** + * @param list $mappingPaths + * + * @return \Generator + */ + private function registerMappingFiles(ContainerBuilder $container, array $mappingPaths): \Generator + { + $registerMappingFilesFromDir = static function (string $dir): iterable { + foreach (Finder::create()->followLinks()->files()->in($dir)->name('/\.ya?ml$/')->sortByName() as $file) { + yield $file->getRealPath(); + } + }; + + if ($container->hasParameter('.kernel.config_dir')) { + /** @var string $configDir */ + $configDir = $container->getParameter('.kernel.config_dir'); + } else { + /** @var string $projectDir */ + $projectDir = $container->getParameter('kernel.project_dir'); + $configDir = $projectDir.'/config'; + } + + if ($container->fileExists($dir = $configDir.'/purgatory', '/^$/')) { + yield from $registerMappingFilesFromDir($dir); + } + + foreach ($mappingPaths as $path) { + if (is_dir($path)) { + $container->addResource(new DirectoryResource($path, '/^$/')); + yield from $registerMappingFilesFromDir($path); + } elseif ($container->fileExists($path)) { + yield $path; + } else { + throw new RuntimeException(\sprintf('Could not open file or directory "%s".', $path)); + } + } + } + + public function process(ContainerBuilder $container): void + { + if (!$container->hasDefinition('cache.system')) { + $container->removeDefinition('sofascore.purgatory.cache.expression_language'); + } + + if (!$container->hasDefinition('profiler') || !$container->hasDefinition('twig')) { + $container->removeDefinition('sofascore.purgatory.data_collector'); + $container->removeDefinition('sofascore.purgatory.purger.traceable'); + $container->removeDefinition('sofascore.purgatory.purger.sync.traceable'); + } + } + + /** + * @internal + * + * @see https://github.com/symfony/symfony/pull/64955 + * + * @return BundleExtensionWrapper + */ + public function getContainerExtension(): ExtensionInterface + { + if (null === $this->wrappedExtension) { + /** @var ExtensionInterface&ConfigurationExtensionInterface&PrependExtensionInterface $extension */ + $extension = parent::getContainerExtension(); + + $this->wrappedExtension = new BundleExtensionWrapper($extension); + } + + return $this->wrappedExtension; } } diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php deleted file mode 100644 index dc916654..00000000 --- a/tests/DependencyInjection/ConfigurationTest.php +++ /dev/null @@ -1,216 +0,0 @@ -processConfiguration(new Configuration(), ['purgatory' => []]); - - self::assertSame([ - 'mapping_paths' => [], - 'route_ignore_patterns' => [], - 'doctrine_middleware' => [ - 'enabled' => true, - 'priority' => null, - ], - 'doctrine_event_listener_priorities' => [ - 'preRemove' => null, - 'postPersist' => null, - 'postUpdate' => null, - 'postFlush' => null, - ], - 'purger' => [ - 'name' => null, - 'hosts' => [], - 'http_client' => null, - ], - 'messenger' => [ - 'transport' => null, - 'bus' => null, - 'batch_size' => null, - ], - 'profiler_integration' => true, - ], $config); - } - - public function testPurgerHostsValidation(): void - { - $config = (new Processor())->processConfiguration(new Configuration(), [ - 'purgatory' => [ - 'purger' => [ - 'hosts' => [ - 'http://foo.bar', - 'https://baz-qux/', - ], - ], - ], - ]); - - self::assertSame([ - 'http://foo.bar', - 'https://baz-qux', - ], $config['purger']['hosts']); - } - - public function testMessengerBusWithoutTransportValidation(): void - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Cannot set the messenger bus without defining the transport.'); - - (new Processor())->processConfiguration(new Configuration(), [ - 'purgatory' => [ - 'messenger' => [ - 'bus' => 'some_id', - ], - ], - ]); - } - - public function testMessengerBatchSizeWithoutTransportValidation(): void - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('Cannot set the batch size without defining the transport.'); - - (new Processor())->processConfiguration(new Configuration(), [ - 'purgatory' => [ - 'messenger' => [ - 'batch_size' => 1, - ], - ], - ]); - } - - #[TestWith([0])] - #[TestWith([-1])] - public function testMessengerBatchSizeGreaterThanZeroValidation(int $batchSize): void - { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage('The batch size must be a number greater than 0.'); - - (new Processor())->processConfiguration(new Configuration(), [ - 'purgatory' => [ - 'messenger' => [ - 'transport' => 'foo', - 'batch_size' => $batchSize, - ], - ], - ]); - } - - public function testDoctrineListenerPrioritiesConifgurationForSingleValue(): void - { - $configuration = (new Processor())->processConfiguration(new Configuration(), [ - 'purgatory' => [ - 'doctrine_event_listener_priorities' => 100, - ], - ]); - - self::assertSame(100, $configuration['doctrine_event_listener_priorities']['preRemove']); - self::assertSame(100, $configuration['doctrine_event_listener_priorities']['postPersist']); - self::assertSame(100, $configuration['doctrine_event_listener_priorities']['postUpdate']); - self::assertSame(100, $configuration['doctrine_event_listener_priorities']['postFlush']); - } - - #[DataProvider('provideXMLCases')] - #[RequiresMethod(XmlFileLoader::class, 'load')] - public function testXMLConfiguration(string $file, array $expectedConfig): void - { - $container = new ContainerBuilder(); - $container->registerExtension(new PurgatoryExtension()); - $locator = new FileLocator(__DIR__.'/Fixtures/xml'); - - $xmlFileLoader = new XmlFileLoader($container, $locator); - $xmlFileLoader->load($file); - - $config = (new Processor())->processConfiguration(new Configuration(), $container->getExtensionConfig('purgatory')); - - self::assertSame($expectedConfig, $config); - } - - public static function provideXMLCases(): iterable - { - yield 'all' => [ - 'all.xml', - [ - 'profiler_integration' => false, - 'doctrine_middleware' => [ - 'priority' => 5, - 'enabled' => true, - ], - 'doctrine_event_listener_priorities' => [ - 'preRemove' => 10, - 'postPersist' => 20, - 'postUpdate' => 30, - 'postFlush' => 40, - ], - 'purger' => [ - 'name' => 'varnish', - 'http_client' => 'foo.client', - 'hosts' => [ - 'http://foo.bar', - 'http://baz.qux', - ], - ], - 'messenger' => [ - 'transport' => 'async', - 'bus' => 'command_bus', - 'batch_size' => 100, - ], - 'mapping_paths' => [ - '%kernel.project_dir%/one.yaml', - '%kernel.project_dir%/two.yaml', - ], - 'route_ignore_patterns' => [ - 0 => '/^_profiler/', - 1 => '/^_wdt/', - ], - ], - ]; - yield 'short listener' => [ - 'short_options.xml', - [ - 'doctrine_middleware' => [ - 'enabled' => false, - 'priority' => null, - ], - 'doctrine_event_listener_priorities' => [ - 'preRemove' => 10, - 'postPersist' => 10, - 'postUpdate' => 10, - 'postFlush' => 10, - ], - 'mapping_paths' => [], - 'route_ignore_patterns' => [], - 'purger' => [ - 'name' => null, - 'hosts' => [], - 'http_client' => null, - ], - 'messenger' => [ - 'transport' => null, - 'bus' => null, - 'batch_size' => null, - ], - 'profiler_integration' => true, - ], - ]; - } -} diff --git a/tests/DependencyInjection/CompilerPass/ControllerClassMapPassTest.php b/tests/DependencyInjection/ControllerClassMapCompilerPassTest.php similarity index 82% rename from tests/DependencyInjection/CompilerPass/ControllerClassMapPassTest.php rename to tests/DependencyInjection/ControllerClassMapCompilerPassTest.php index dc932bd1..c2660d9e 100644 --- a/tests/DependencyInjection/CompilerPass/ControllerClassMapPassTest.php +++ b/tests/DependencyInjection/ControllerClassMapCompilerPassTest.php @@ -2,19 +2,19 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection\CompilerPass; +namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Sofascore\PurgatoryBundle\Cache\RouteMetadata\AttributeMetadataProvider; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\ControllerClassMapPass; +use Sofascore\PurgatoryBundle\DependencyInjection\ControllerClassMapCompilerPass; use Sofascore\PurgatoryBundle\Tests\DependencyInjection\Fixtures\DummyController; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Routing\RouterInterface; -#[CoversClass(ControllerClassMapPass::class)] -final class ControllerClassMapPassTest extends TestCase +#[CoversClass(ControllerClassMapCompilerPass::class)] +final class ControllerClassMapCompilerPassTest extends TestCase { #[TestWith([DummyController::class, DummyController::class, true])] #[TestWith([DummyController::class, DummyController::class, false])] @@ -36,7 +36,7 @@ public function testPurgeOnCollection(string $id, string $class, bool $hasAttrib [], ]); - $compilerPass = new ControllerClassMapPass(); + $compilerPass = new ControllerClassMapCompilerPass(); $compilerPass->process($container); $classMap = $definition->getArgument(1); diff --git a/tests/DependencyInjection/CompilerPass/RegisterExpressionLanguageProvidersPassTest.php b/tests/DependencyInjection/RegisterExpressionLanguageProvidersCompilerPassTest.php similarity index 86% rename from tests/DependencyInjection/CompilerPass/RegisterExpressionLanguageProvidersPassTest.php rename to tests/DependencyInjection/RegisterExpressionLanguageProvidersCompilerPassTest.php index af6e0967..a8e5780c 100644 --- a/tests/DependencyInjection/CompilerPass/RegisterExpressionLanguageProvidersPassTest.php +++ b/tests/DependencyInjection/RegisterExpressionLanguageProvidersCompilerPassTest.php @@ -2,21 +2,21 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection\CompilerPass; +namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterExpressionLanguageProvidersPass; -use Sofascore\PurgatoryBundle\DependencyInjection\PurgatoryExtension; +use Sofascore\PurgatoryBundle\DependencyInjection\RegisterExpressionLanguageProvidersCompilerPass; use Sofascore\PurgatoryBundle\Exception\RuntimeException; +use Sofascore\PurgatoryBundle\PurgatoryBundle; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; -#[CoversClass(RegisterExpressionLanguageProvidersPass::class)] -final class RegisterExpressionLanguageProvidersPassTest extends TestCase +#[CoversClass(RegisterExpressionLanguageProvidersCompilerPass::class)] +final class RegisterExpressionLanguageProvidersCompilerPassTest extends TestCase { private ContainerBuilder $container; @@ -24,7 +24,10 @@ protected function setUp(): void { $this->container = new ContainerBuilder(); $this->container->setParameter('kernel.project_dir', __DIR__); - (new PurgatoryExtension())->load([], $this->container); + $this->container->setParameter('kernel.build_dir', __DIR__); + $this->container->setParameter('kernel.environment', 'dev'); + + (new PurgatoryBundle())->getContainerExtension()->load([], $this->container); } protected function tearDown(): void @@ -52,7 +55,7 @@ public function testProcess(): void $this->container->register(id: 'other_provider', class: \stdClass::class) ->addTag('purgatory.expression_language_provider'); - $compilerPass = new RegisterExpressionLanguageProvidersPass(); + $compilerPass = new RegisterExpressionLanguageProvidersCompilerPass(); $compilerPass->process($this->container); self::assertTrue($this->container->hasDefinition('sofascore.purgatory.expression_language_provider')); @@ -103,7 +106,7 @@ public function testExpressionLangProviderIsRemovedWhenThereIsNoExpressionLangSe { $this->container->removeDefinition('sofascore.purgatory.expression_language'); - $compilerPass = new RegisterExpressionLanguageProvidersPass(); + $compilerPass = new RegisterExpressionLanguageProvidersCompilerPass(); $compilerPass->process($this->container); self::assertFalse($this->container->hasDefinition('sofascore.purgatory.expression_language_provider')); @@ -111,7 +114,7 @@ public function testExpressionLangProviderIsRemovedWhenThereIsNoExpressionLangSe public function testExpressionLangProviderIsRemovedWhenThereAreNoFunctions(): void { - $compilerPass = new RegisterExpressionLanguageProvidersPass(); + $compilerPass = new RegisterExpressionLanguageProvidersCompilerPass(); $compilerPass->process($this->container); self::assertFalse($this->container->hasDefinition('sofascore.purgatory.expression_language_provider')); @@ -131,7 +134,7 @@ public function testExceptionIsThrownWhenSameFunctionNameIsUsedMultipleTimes(): attributes: ['function' => 'one', 'method' => '__invoke'], ); - $compilerPass = new RegisterExpressionLanguageProvidersPass(); + $compilerPass = new RegisterExpressionLanguageProvidersCompilerPass(); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('The function name "one" is already used by "foo::__invoke".'); diff --git a/tests/DependencyInjection/CompilerPass/RegisterPurgerPassTest.php b/tests/DependencyInjection/RegisterPurgerCompilerPassTest.php similarity index 77% rename from tests/DependencyInjection/CompilerPass/RegisterPurgerPassTest.php rename to tests/DependencyInjection/RegisterPurgerCompilerPassTest.php index 1bc9860e..5cf4a379 100644 --- a/tests/DependencyInjection/CompilerPass/RegisterPurgerPassTest.php +++ b/tests/DependencyInjection/RegisterPurgerCompilerPassTest.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection\CompilerPass; +namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterPurgerPass; -use Sofascore\PurgatoryBundle\DependencyInjection\PurgatoryExtension; +use Sofascore\PurgatoryBundle\DependencyInjection\RegisterPurgerCompilerPass; use Sofascore\PurgatoryBundle\Exception\RuntimeException; +use Sofascore\PurgatoryBundle\PurgatoryBundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\HttpCache\Store; -#[CoversClass(RegisterPurgerPass::class)] -final class RegisterPurgerPassTest extends TestCase +#[CoversClass(RegisterPurgerCompilerPass::class)] +final class RegisterPurgerCompilerPassTest extends TestCase { private ContainerBuilder $container; @@ -21,9 +21,11 @@ protected function setUp(): void { $this->container = new ContainerBuilder(); $this->container->setParameter('kernel.project_dir', __DIR__); + $this->container->setParameter('kernel.build_dir', __DIR__); + $this->container->setParameter('kernel.environment', 'dev'); $this->container->register('http_cache.store', Store::class); - (new PurgatoryExtension())->load([], $this->container); + (new PurgatoryBundle())->getContainerExtension()->load([], $this->container); } protected function tearDown(): void @@ -33,7 +35,7 @@ protected function tearDown(): void public function testDefaultPurgerIsSetToSymfonyPurgerIfHttpCacheStoreExists(): void { - (new RegisterPurgerPass())->process($this->container); + (new RegisterPurgerCompilerPass())->process($this->container); self::assertSame('sofascore.purgatory.purger.symfony', (string) $this->container->getAlias('sofascore.purgatory.purger')); self::assertTrue($this->container->hasDefinition('sofascore.purgatory.purger.symfony')); @@ -46,7 +48,7 @@ public function testDefaultPurgerIsSetToVoidPurgerIfHttpCacheStoreDoesNotExist() { $this->container->removeDefinition('http_cache.store'); - (new RegisterPurgerPass())->process($this->container); + (new RegisterPurgerCompilerPass())->process($this->container); self::assertSame('sofascore.purgatory.purger.void', (string) $this->container->getAlias('sofascore.purgatory.purger')); self::assertFalse($this->container->hasDefinition('sofascore.purgatory.purger.symfony')); @@ -59,7 +61,7 @@ public function testRegisterPurgerWhenPurgerNameIsSetAndHttpCacheStoreExists(): { $this->container->setParameter('.sofascore.purgatory.purger.name', 'in-memory'); - (new RegisterPurgerPass())->process($this->container); + (new RegisterPurgerCompilerPass())->process($this->container); self::assertSame('sofascore.purgatory.purger.in_memory', (string) $this->container->getAlias('sofascore.purgatory.purger')); self::assertTrue($this->container->hasDefinition('sofascore.purgatory.purger.symfony')); @@ -70,7 +72,7 @@ public function testRegisterPurgerWhenPurgerNameIsSetAndHttpCacheStoreDoesNotExi $this->container->setParameter('.sofascore.purgatory.purger.name', 'in-memory'); $this->container->removeDefinition('http_cache.store'); - (new RegisterPurgerPass())->process($this->container); + (new RegisterPurgerCompilerPass())->process($this->container); self::assertSame('sofascore.purgatory.purger.in_memory', (string) $this->container->getAlias('sofascore.purgatory.purger')); self::assertFalse($this->container->hasDefinition('sofascore.purgatory.purger.symfony')); @@ -80,7 +82,7 @@ public function testIdAsPurgerName(): void { $this->container->setParameter('.sofascore.purgatory.purger.name', 'sofascore.purgatory.purger.in_memory'); - (new RegisterPurgerPass())->process($this->container); + (new RegisterPurgerCompilerPass())->process($this->container); self::assertSame('sofascore.purgatory.purger.in_memory', (string) $this->container->getAlias('sofascore.purgatory.purger')); } @@ -92,6 +94,6 @@ public function testExceptionIsThrownOnInvalidService(): void $this->expectException(RuntimeException::class); $this->expectExceptionMessage('The configured purger service "invalid" does not exist.'); - (new RegisterPurgerPass())->process($this->container); + (new RegisterPurgerCompilerPass())->process($this->container); } } diff --git a/tests/DependencyInjection/CompilerPass/RegisterRouteParamServicesPassTest.php b/tests/DependencyInjection/RegisterRouteParamServicesCompilerPassTest.php similarity index 86% rename from tests/DependencyInjection/CompilerPass/RegisterRouteParamServicesPassTest.php rename to tests/DependencyInjection/RegisterRouteParamServicesCompilerPassTest.php index 9a451c55..8416bf71 100644 --- a/tests/DependencyInjection/CompilerPass/RegisterRouteParamServicesPassTest.php +++ b/tests/DependencyInjection/RegisterRouteParamServicesCompilerPassTest.php @@ -2,21 +2,21 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection\CompilerPass; +namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterRouteParamServicesPass; -use Sofascore\PurgatoryBundle\DependencyInjection\PurgatoryExtension; +use Sofascore\PurgatoryBundle\DependencyInjection\RegisterRouteParamServicesCompilerPass; use Sofascore\PurgatoryBundle\Exception\RuntimeException; +use Sofascore\PurgatoryBundle\PurgatoryBundle; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; -#[CoversClass(RegisterRouteParamServicesPass::class)] -final class RegisterRouteParamServicesPassTest extends TestCase +#[CoversClass(RegisterRouteParamServicesCompilerPass::class)] +final class RegisterRouteParamServicesCompilerPassTest extends TestCase { private ContainerBuilder $container; @@ -24,7 +24,10 @@ protected function setUp(): void { $this->container = new ContainerBuilder(); $this->container->setParameter('kernel.project_dir', __DIR__); - (new PurgatoryExtension())->load([], $this->container); + $this->container->setParameter('kernel.build_dir', __DIR__); + $this->container->setParameter('kernel.environment', 'dev'); + + (new PurgatoryBundle())->getContainerExtension()->load([], $this->container); } protected function tearDown(): void @@ -49,7 +52,7 @@ public function testProcess(): void attributes: ['alias' => 'three', 'method' => 'anotherMethod'], ); - $compilerPass = new RegisterRouteParamServicesPass(); + $compilerPass = new RegisterRouteParamServicesCompilerPass(); $compilerPass->process($this->container); self::assertTrue($this->container->hasDefinition('sofascore.purgatory.route_parameter_resolver.dynamic')); @@ -91,7 +94,7 @@ public function testProcess(): void public function testDynamicResolverIsRemovedWhenThereAreNoServices(): void { - $compilerPass = new RegisterRouteParamServicesPass(); + $compilerPass = new RegisterRouteParamServicesCompilerPass(); $compilerPass->process($this->container); self::assertFalse($this->container->hasDefinition('sofascore.purgatory.route_parameter_resolver.dynamic')); @@ -108,7 +111,7 @@ public function testExceptionIsThrownWhenSameAliasIsUsedMultipleTimes(): void attributes: ['alias' => 'one', 'method' => '__invoke'], ); - $compilerPass = new RegisterRouteParamServicesPass(); + $compilerPass = new RegisterRouteParamServicesCompilerPass(); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('The alias "one" is already used by "foo::__invoke".'); diff --git a/tests/DependencyInjection/PurgatoryExtensionTest.php b/tests/PurgatoryBundleTest.php similarity index 64% rename from tests/DependencyInjection/PurgatoryExtensionTest.php rename to tests/PurgatoryBundleTest.php index 07560a5d..d033fde1 100644 --- a/tests/DependencyInjection/PurgatoryExtensionTest.php +++ b/tests/PurgatoryBundleTest.php @@ -2,16 +2,17 @@ declare(strict_types=1); -namespace Sofascore\PurgatoryBundle\Tests\DependencyInjection; +namespace Sofascore\PurgatoryBundle\Tests; use Doctrine\ORM\Events as DoctrineEvents; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresMethod; use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Sofascore\PurgatoryBundle\DataCollector\PurgatoryDataCollector; -use Sofascore\PurgatoryBundle\DependencyInjection\CompilerPass\RegisterPurgerPass; -use Sofascore\PurgatoryBundle\DependencyInjection\PurgatoryExtension; use Sofascore\PurgatoryBundle\Exception\RuntimeException; +use Sofascore\PurgatoryBundle\PurgatoryBundle; use Sofascore\PurgatoryBundle\Purger\Messenger\PurgeMessage; use Sofascore\PurgatoryBundle\Purger\PurgerInterface; use Sofascore\PurgatoryBundle\Tests\DependencyInjection\Fixtures\DummyController; @@ -26,19 +27,225 @@ use Sofascore\PurgatoryBundle\Tests\DependencyInjection\Fixtures\DummyTargetResolver; use Sofascore\PurgatoryBundle\Tests\DependencyInjection\Fixtures\DummyValuesResolver; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; +use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpKernel\Kernel; -#[CoversClass(PurgatoryExtension::class)] -final class PurgatoryExtensionTest extends TestCase +#[CoversClass(PurgatoryBundle::class)] +final class PurgatoryBundleTest extends TestCase { - public function testControllerWithPurgeOnIsTagged(): void + public function testDefaultConfig(): void + { + $config = $this->processConfiguration(['purgatory' => []]); + + self::assertSame([ + 'mapping_paths' => [], + 'route_ignore_patterns' => [], + 'doctrine_middleware' => [ + 'enabled' => true, + 'priority' => null, + ], + 'doctrine_event_listener_priorities' => [ + 'preRemove' => null, + 'postPersist' => null, + 'postUpdate' => null, + 'postFlush' => null, + ], + 'purger' => [ + 'name' => null, + 'hosts' => [], + 'http_client' => null, + ], + 'messenger' => [ + 'transport' => null, + 'bus' => null, + 'batch_size' => null, + ], + 'profiler_integration' => true, + ], $config); + } + + public function testPurgerHostsValidation(): void + { + $config = $this->processConfiguration([ + 'purgatory' => [ + 'purger' => [ + 'hosts' => [ + 'http://foo.bar', + 'https://baz-qux/', + ], + ], + ], + ]); + + self::assertSame([ + 'http://foo.bar', + 'https://baz-qux', + ], $config['purger']['hosts']); + } + + public function testMessengerBusWithoutTransportValidation(): void + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Cannot set the messenger bus without defining the transport.'); + + $this->processConfiguration([ + 'purgatory' => [ + 'messenger' => [ + 'bus' => 'some_id', + ], + ], + ]); + } + + public function testMessengerBatchSizeWithoutTransportValidation(): void + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Cannot set the batch size without defining the transport.'); + + $this->processConfiguration([ + 'purgatory' => [ + 'messenger' => [ + 'batch_size' => 1, + ], + ], + ]); + } + + #[TestWith([0])] + #[TestWith([-1])] + public function testMessengerBatchSizeGreaterThanZeroValidation(int $batchSize): void + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The batch size must be a number greater than 0.'); + + $this->processConfiguration([ + 'purgatory' => [ + 'messenger' => [ + 'transport' => 'foo', + 'batch_size' => $batchSize, + ], + ], + ]); + } + + public function testDoctrineListenerPrioritiesConifgurationForSingleValue(): void + { + $config = $this->processConfiguration([ + 'purgatory' => [ + 'doctrine_event_listener_priorities' => 100, + ], + ]); + + self::assertSame(100, $config['doctrine_event_listener_priorities']['preRemove']); + self::assertSame(100, $config['doctrine_event_listener_priorities']['postPersist']); + self::assertSame(100, $config['doctrine_event_listener_priorities']['postUpdate']); + self::assertSame(100, $config['doctrine_event_listener_priorities']['postFlush']); + } + + #[DataProvider('provideXMLCases')] + #[RequiresMethod(XmlFileLoader::class, 'load')] + public function testXMLConfiguration(string $file, array $expectedConfig): void { $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container->registerExtension((new PurgatoryBundle())->getContainerExtension()); + $locator = new FileLocator(__DIR__.'/DependencyInjection/Fixtures/xml'); + + $xmlFileLoader = new XmlFileLoader($container, $locator); + $xmlFileLoader->load($file); + + $config = $this->processConfiguration($container->getExtensionConfig('purgatory')); + + self::assertSame($expectedConfig, $config); + } + + public static function provideXMLCases(): iterable + { + yield 'all' => [ + 'all.xml', + [ + 'profiler_integration' => false, + 'doctrine_middleware' => [ + 'priority' => 5, + 'enabled' => true, + ], + 'doctrine_event_listener_priorities' => [ + 'preRemove' => 10, + 'postPersist' => 20, + 'postUpdate' => 30, + 'postFlush' => 40, + ], + 'purger' => [ + 'name' => 'varnish', + 'http_client' => 'foo.client', + 'hosts' => [ + 'http://foo.bar', + 'http://baz.qux', + ], + ], + 'messenger' => [ + 'transport' => 'async', + 'bus' => 'command_bus', + 'batch_size' => 100, + ], + 'mapping_paths' => [ + '%kernel.project_dir%/one.yaml', + '%kernel.project_dir%/two.yaml', + ], + 'route_ignore_patterns' => [ + 0 => '/^_profiler/', + 1 => '/^_wdt/', + ], + ], + ]; + yield 'short listener' => [ + 'short_options.xml', + [ + 'doctrine_middleware' => [ + 'enabled' => false, + 'priority' => null, + ], + 'doctrine_event_listener_priorities' => [ + 'preRemove' => 10, + 'postPersist' => 10, + 'postUpdate' => 10, + 'postFlush' => 10, + ], + 'mapping_paths' => [], + 'route_ignore_patterns' => [], + 'purger' => [ + 'name' => null, + 'hosts' => [], + 'http_client' => null, + ], + 'messenger' => [ + 'transport' => null, + 'bus' => null, + 'batch_size' => null, + ], + 'profiler_integration' => true, + ], + ]; + } + + private function processConfiguration(array $configs): array + { + $configuration = (new PurgatoryBundle())->getContainerExtension()->getConfiguration([], new ContainerBuilder()); + + return (new Processor())->processConfiguration($configuration, $configs); + } + + public function testControllerWithPurgeOnIsTagged(): void + { + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyController::class) ->setAutoconfigured(true) @@ -67,9 +274,8 @@ public function testControllerWithPurgeOnIsTagged(): void public function testServiceWithAsRouteParamServiceIsTagged(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyRouteParamService::class) ->setAutoconfigured(true) @@ -88,9 +294,8 @@ public function testServiceWithAsRouteParamServiceIsTagged(): void public function testExceptionIsThrownWhenRouteParamServiceMethodDoesNotExist(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyInvalidRouteParamService::class) ->setAutoconfigured(true) @@ -106,9 +311,8 @@ public function testExceptionIsThrownWhenRouteParamServiceMethodDoesNotExist(): public function testServiceWithAsExpressionLanguageFunctionIsTagged(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyExpressionLanguageFunction::class) ->setAutoconfigured(true) @@ -127,9 +331,8 @@ public function testServiceWithAsExpressionLanguageFunctionIsTagged(): void public function testExceptionIsThrownWhenExpressionLanguageFunctionMethodDoesNotExist(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyInvalidExpressionLanguageFunction::class) ->setAutoconfigured(true) @@ -145,10 +348,9 @@ public function testExceptionIsThrownWhenExpressionLanguageFunctionMethodDoesNot public function testPurgerConfig(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); + $container = self::getContainer(); - $extension = new PurgatoryExtension(); + $extension = $container->getExtension('purgatory'); $extension->load([ 'purgatory' => [ 'purger' => [ @@ -167,10 +369,9 @@ public function testPurgerConfig(): void public function testDefaultPurgerIsSetToVoidPurger(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); + $container = self::getContainer(); - $extension = new PurgatoryExtension(); + $extension = $container->getExtension('purgatory'); $extension->load([ 'purgatory' => [ 'purger' => [ @@ -191,10 +392,9 @@ public function testDefaultPurgerIsSetToVoidPurger(): void #[TestWith([['http_client' => 'foo.client'], 'foo.client'])] public function testCorrectHttpClientIsSet(array $config, string $expectedHttpClient): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); + $container = self::getContainer(); - $extension = new PurgatoryExtension(); + $extension = $container->getExtension('purgatory'); $extension->load([ 'purgatory' => [ 'purger' => $config, @@ -205,23 +405,23 @@ public function testCorrectHttpClientIsSet(array $config, string $expectedHttpCl self::assertSame($expectedHttpClient, (string) $container->getDefinition('sofascore.purgatory.purger.varnish')->getArgument(0)); } - #[TestWith([[], ['config/purgatory/one.yaml', 'config/purgatory/two.yml'], __DIR__.'/Fixtures/app/config/purgatory'])] + #[TestWith([[], ['config/purgatory/one.yaml', 'config/purgatory/two.yml'], __DIR__.'/DependencyInjection/Fixtures/app/config/purgatory'])] #[TestWith([ - [__DIR__.'/Fixtures/app/config/three.yaml'], + [__DIR__.'/DependencyInjection/Fixtures/app/config/three.yaml'], ['config/purgatory/one.yaml', 'config/purgatory/two.yml', 'config/three.yaml'], - __DIR__.'/Fixtures/app/config/three.yaml', + __DIR__.'/DependencyInjection/Fixtures/app/config/three.yaml', ])] #[TestWith([ - [__DIR__.'/Fixtures/app/config/additional'], + [__DIR__.'/DependencyInjection/Fixtures/app/config/additional'], ['config/purgatory/one.yaml', 'config/purgatory/two.yml', 'config/additional/five.yaml', 'config/additional/four.yml'], - __DIR__.'/Fixtures/app/config/additional', + __DIR__.'/DependencyInjection/Fixtures/app/config/additional', ])] public function testMappingPathsAreSet(array $mappingPaths, array $expectedFiles, string $expectedResource): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__.'/Fixtures/app'); + $container = self::getContainer(); + $container->setParameter('kernel.project_dir', __DIR__.'/DependencyInjection/Fixtures/app'); - $extension = new PurgatoryExtension(); + $extension = $container->getExtension('purgatory'); $extension->load([ 'purgatory' => [ 'mapping_paths' => $mappingPaths, @@ -230,7 +430,7 @@ public function testMappingPathsAreSet(array $mappingPaths, array $expectedFiles self::assertTrue($container->hasDefinition('sofascore.purgatory.route_metadata_provider.yaml')); self::assertSame( - array_map(static fn (string $file): string => __DIR__.'/Fixtures/app/'.$file, $expectedFiles), + array_map(static fn (string $file): string => __DIR__.'/DependencyInjection/Fixtures/app/'.$file, $expectedFiles), $container->getDefinition('sofascore.purgatory.route_metadata_provider.yaml')->getArgument(1), ); @@ -242,17 +442,17 @@ public function testMappingPathsAreSet(array $mappingPaths, array $expectedFiles public function testMappingFilesAreLoadedFromProjectConfigDirWhenConfigDirParamIsNotSet(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__.'/Fixtures/app'); + $container = self::getContainer(); + $container->setParameter('kernel.project_dir', __DIR__.'/DependencyInjection/Fixtures/app'); - $extension = new PurgatoryExtension(); + $extension = $container->getExtension('purgatory'); $extension->load([], $container); self::assertTrue($container->hasDefinition('sofascore.purgatory.route_metadata_provider.yaml')); self::assertSame( [ - __DIR__.'/Fixtures/app/config/purgatory/one.yaml', - __DIR__.'/Fixtures/app/config/purgatory/two.yml', + __DIR__.'/DependencyInjection/Fixtures/app/config/purgatory/one.yaml', + __DIR__.'/DependencyInjection/Fixtures/app/config/purgatory/two.yml', ], $container->getDefinition('sofascore.purgatory.route_metadata_provider.yaml')->getArgument(1), ); @@ -260,17 +460,17 @@ public function testMappingFilesAreLoadedFromProjectConfigDirWhenConfigDirParamI public function testMappingFilesAreLoadedOnlyFromKernelConfigDirWhenSet(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__.'/Fixtures/app'); - $container->setParameter('.kernel.config_dir', __DIR__.'/Fixtures/app/apps/sub/config'); + $container = self::getContainer(); + $container->setParameter('kernel.project_dir', __DIR__.'/DependencyInjection/Fixtures/app'); + $container->setParameter('.kernel.config_dir', __DIR__.'/DependencyInjection/Fixtures/app/apps/sub/config'); - $extension = new PurgatoryExtension(); + $extension = $container->getExtension('purgatory'); $extension->load([], $container); self::assertTrue($container->hasDefinition('sofascore.purgatory.route_metadata_provider.yaml')); self::assertSame( [ - __DIR__.'/Fixtures/app/apps/sub/config/purgatory/six.yaml', + __DIR__.'/DependencyInjection/Fixtures/app/apps/sub/config/purgatory/six.yaml', ], $container->getDefinition('sofascore.purgatory.route_metadata_provider.yaml')->getArgument(1), ); @@ -278,18 +478,18 @@ public function testMappingFilesAreLoadedOnlyFromKernelConfigDirWhenSet(): void public function testMappingFilesAreLoadedOnceWhenKernelConfigDirMatchesProjectConfigDir(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__.'/Fixtures/app'); - $container->setParameter('.kernel.config_dir', __DIR__.'/Fixtures/app/config'); + $container = self::getContainer(); + $container->setParameter('kernel.project_dir', __DIR__.'/DependencyInjection/Fixtures/app'); + $container->setParameter('.kernel.config_dir', __DIR__.'/DependencyInjection/Fixtures/app/config'); - $extension = new PurgatoryExtension(); + $extension = $container->getExtension('purgatory'); $extension->load([], $container); self::assertTrue($container->hasDefinition('sofascore.purgatory.route_metadata_provider.yaml')); self::assertSame( [ - __DIR__.'/Fixtures/app/config/purgatory/one.yaml', - __DIR__.'/Fixtures/app/config/purgatory/two.yml', + __DIR__.'/DependencyInjection/Fixtures/app/config/purgatory/one.yaml', + __DIR__.'/DependencyInjection/Fixtures/app/config/purgatory/two.yml', ], $container->getDefinition('sofascore.purgatory.route_metadata_provider.yaml')->getArgument(1), ); @@ -297,10 +497,8 @@ public function testMappingFilesAreLoadedOnceWhenKernelConfigDirMatchesProjectCo public function testYamlMetadataProviderIsRemovedWhenThereAreNoFiles(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - - $extension = new PurgatoryExtension(); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $extension->load([], $container); self::assertFalse($container->hasDefinition('sofascore.purgatory.route_metadata_provider.yaml')); @@ -308,10 +506,8 @@ public function testYamlMetadataProviderIsRemovedWhenThereAreNoFiles(): void public function testExceptionIsThrownOnInvalidMappingPath(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - - $extension = new PurgatoryExtension(); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('Could not open file or directory "foobarbaz.yaml".'); @@ -325,10 +521,8 @@ public function testExceptionIsThrownOnInvalidMappingPath(): void public function testRouteIgnorePatternsIsSet(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - - $extension = new PurgatoryExtension(); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $extension->load([ 'purgatory' => [ 'route_ignore_patterns' => ['/^_profiler/'], @@ -345,10 +539,8 @@ public function testRouteIgnorePatternsIsSet(): void #[TestWith([['doctrine_middleware' => ['priority' => 10]], [['priority' => 10]]])] public function testDoctrineMiddlewareTagIsSet(array $middlewarePriority, array $expectedTag): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - - $extension = new PurgatoryExtension(); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $extension->load([ 'purgatory' => $middlewarePriority, ], $container); @@ -363,10 +555,8 @@ public function testDoctrineMiddlewareTagIsSet(array $middlewarePriority, array public function testDoctrineMiddlewareIsRemovedWhenDisabled(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - - $extension = new PurgatoryExtension(); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $extension->load([ 'purgatory' => ['doctrine_middleware' => false], ], $container); @@ -425,10 +615,8 @@ public function testDoctrineMiddlewareIsRemovedWhenDisabled(): void ]])] public function testDoctrineEventListenerTagIsSet(array $config, array $expectedTag): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - - $extension = new PurgatoryExtension(); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $extension->load([ 'purgatory' => $config, ], $container); @@ -441,9 +629,8 @@ public function testDoctrineEventListenerTagIsSet(array $config, array $expected public function testMessengerWhenTransportIsNotSet(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $extension->prepend($container); @@ -463,9 +650,8 @@ public function testMessengerWhenTransportIsNotSet(): void #[TestWith([['batch_size' => 3], [new Reference('messenger.default_bus'), 3], []])] public function testMessengerWhenTransportIsSet(array $extraConfig, array $expectedArguments, array $expectedTagAttributes): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->loadFromExtension($extension->getAlias(), [ 'messenger' => [ @@ -504,9 +690,8 @@ public function testMessengerWhenTransportIsSet(array $extraConfig, array $expec public function testSubscriptionResolverIsTagged(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummySubscriptionResolver::class) ->setAutoconfigured(true) @@ -521,9 +706,8 @@ public function testSubscriptionResolverIsTagged(): void public function testInverseValuesBuilderIsTagged(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyInverseValuesBuilder::class) ->setAutoconfigured(true) @@ -538,9 +722,8 @@ public function testInverseValuesBuilderIsTagged(): void public function testTargetResolverIsTagged(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyTargetResolver::class) ->setAutoconfigured(true) @@ -555,9 +738,8 @@ public function testTargetResolverIsTagged(): void public function testRouteProviderIsTagged(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyRouteProvider::class) ->setAutoconfigured(true) @@ -572,9 +754,8 @@ public function testRouteProviderIsTagged(): void public function testRouteParamValuesResolverIsTagged(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register(DummyValuesResolver::class) ->setAutoconfigured(true) @@ -590,15 +771,11 @@ public function testRouteParamValuesResolverIsTagged(): void /** * @param list $extensions */ - #[TestWith([[new PurgatoryExtension()], false])] - #[TestWith([[new FrameworkExtension(), new PurgatoryExtension()], true])] - #[TestWith([[new PurgatoryExtension(), new FrameworkExtension()], true])] + #[DataProvider('provideExpressionLanguageCacheIsRemovedWhenExpectedCases')] public function testExpressionLanguageCacheIsRemovedWhenExpected(array $extensions, bool $hasCache): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); + $container = self::getContainer(); $container->setParameter('kernel.debug', true); - $container->setParameter('kernel.build_dir', __DIR__); $container->setParameter('kernel.container_class', 'App'); $container->getCompilerPassConfig()->setRemovingPasses([]); @@ -613,6 +790,15 @@ public function testExpressionLanguageCacheIsRemovedWhenExpected(array $extensio self::assertSame($hasCache, $container->hasDefinition('sofascore.purgatory.cache.expression_language')); } + public static function provideExpressionLanguageCacheIsRemovedWhenExpectedCases(): iterable + { + $extension = self::getContainer()->getExtension('purgatory'); + + yield [[$extension], false]; + yield [[new FrameworkExtension(), $extension], true]; + yield [[$extension, new FrameworkExtension()], true]; + } + #[TestWith([['profiler_integration' => true], true, true, false])] #[TestWith([['profiler_integration' => true, 'messenger' => 'async'], true, true, true])] #[TestWith([['profiler_integration' => false], false, false, false])] @@ -622,9 +808,8 @@ public function testProfilerIntegration( bool $hasTraceablePurger, bool $hasTraceableSyncPurger, ): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->getCompilerPassConfig()->setRemovingPasses([]); @@ -645,9 +830,8 @@ public function testProfilerIntegration( #[TestWith([['profiler_integration' => false]])] public function testProfilerIntegrationWithoutProfiler(array $config): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->getCompilerPassConfig()->setRemovingPasses([]); @@ -662,10 +846,8 @@ public function testProfilerIntegrationWithoutProfiler(array $config): void public function testDataCollectorServiceCanBeInstantiatedWhenPurgerNameIsNull(): void { - $container = new ContainerBuilder(); - $container->setParameter('kernel.project_dir', __DIR__); - $container->registerExtension($extension = new PurgatoryExtension()); - $container->addCompilerPass(new RegisterPurgerPass()); + $container = self::getContainer(); + $extension = $container->getExtension('purgatory'); $container->register('profiler', \stdClass::class); $container->register('twig', \stdClass::class); @@ -679,4 +861,27 @@ public function testDataCollectorServiceCanBeInstantiatedWhenPurgerNameIsNull(): self::assertInstanceOf(PurgatoryDataCollector::class, $container->get('sofascore.purgatory.data_collector.public')); } + + private static function getContainer(): ContainerBuilder + { + $bundle = new PurgatoryBundle(); + $extension = $bundle->getContainerExtension(); + + $container = new ContainerBuilder(); + $container->setParameter('kernel.project_dir', __DIR__); + $container->setParameter('kernel.build_dir', __DIR__); + $container->setParameter('kernel.environment', 'dev'); + + $container->registerExtension($extension); + $bundle->build($container); + + // On Symfony 8.1+ the kernel registers the bundle as a compiler pass instead of build(), + // but since these tests don't use a kernel, it must be done manually, {@see https://github.com/symfony/symfony/pull/62800} + // @TODO Make unconditional when Symfony <8.1 support is dropped + if (Kernel::VERSION_ID >= 80100) { + $container->addCompilerPass($bundle, PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000); + } + + return $container; + } }