diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aeab6d7..c04c3488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Changelog 3.x === +3.4 +--- + +* Configuration for `generate_url_type` is now on top level instead of on the `cache_manager`, and applies to the InvalidationListener too. + Configuring `cache_manager.generate_url_type` is deprecated and will be removed in version 4. + 3.3 --- diff --git a/Resources/doc/reference/configuration.rst b/Resources/doc/reference/configuration.rst index 058cf222..24f8c4b2 100644 --- a/Resources/doc/reference/configuration.rst +++ b/Resources/doc/reference/configuration.rst @@ -7,6 +7,7 @@ for the bundle. .. toctree:: :maxdepth: 2 + configuration/general configuration/proxy-client configuration/cache-manager configuration/headers diff --git a/Resources/doc/reference/configuration/cache-manager.rst b/Resources/doc/reference/configuration/cache-manager.rst index 6a04edc0..74585762 100644 --- a/Resources/doc/reference/configuration/cache-manager.rst +++ b/Resources/doc/reference/configuration/cache-manager.rst @@ -38,23 +38,5 @@ your own service that implements ``FOS\HttpCache\ProxyClient``. custom_proxy_client: acme.caching.proxy_client When you specify a custom proxy client, the bundle does not know about the -capabilities of the client. The ``generate_url_type`` defaults to true and -:doc:`tag support ` is only active if explicitly enabled. - -``generate_url_type`` ---------------------- - -**type**: ``enum`` **Symfony 2 options**: ``auto`` or one of the constants in UrlGeneratorInterface - -The ``$referenceType`` to be used when generating URLs in the ``invalidateRoute()`` -and ``refreshRoute()`` calls. If you use ``ABSOLUTE_PATH`` to only generate -paths, you need to configure the ``base_url`` on the proxy client. When set to -``auto``, the value is determined based on whether ``base_url`` is set on the -default proxy client. - -.. code-block:: yaml - - # app/config/config.yml - fos_http_cache: - cache_manager: - generate_url_type: !php/const Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_PATH +capabilities of the client. The ``generate_url_type`` defaults to absolute +URL and :doc:`tag support ` is only active if explicitly enabled. diff --git a/Resources/doc/reference/configuration/general.rst b/Resources/doc/reference/configuration/general.rst new file mode 100644 index 00000000..e29e04ce --- /dev/null +++ b/Resources/doc/reference/configuration/general.rst @@ -0,0 +1,20 @@ +General Configuration +===================== + +There is one global option you can configure for this bundle. + +``generate_url_type`` +--------------------- + +**type**: ``enum`` **options**: ``auto`` or one of the constants in UrlGeneratorInterface + +The ``$referenceType`` to be used when generating URLs to invalidate or refresh +from routes. If you use ``ABSOLUTE_PATH`` to only generate +paths, you need to configure the ``base_url`` on the proxy client. When set to +``auto``, the value is determined based on the configuration. + +.. code-block:: yaml + + # app/config/config.yml + fos_http_cache: + generate_url_type: !php/const Symfony\Component\Routing\Generator\UrlGeneratorInterface::ABSOLUTE_PATH diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 72fb70e9..52f8b0a9 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -11,7 +11,6 @@ namespace FOS\HttpCacheBundle\DependencyInjection; -use DateTime; use FOS\HttpCache\ProxyClient\Varnish; use FOS\HttpCache\SymfonyCache\PurgeListener; use FOS\HttpCache\SymfonyCache\PurgeTagsListener; @@ -194,8 +193,35 @@ function ($v) { return $v; }) + ->end() + ->validate() + ->ifTrue( + function (array $v): bool { + return !empty($v['cache_manager']['generate_url_type']) && 'auto' !== $v['cache_manager']['generate_url_type'] && array_key_exists('generate_url_type', $v); + } + ) + ->then(function (array $v) { + throw new InvalidConfigurationException('Only configure "generate_url_type" and do not set the deprecated "cache_manager.generate_url_type" option'); + }) + ->end() ; + $rootNode + ->children() + ->enumNode('generate_url_type') + ->values([ + 'auto', + UrlGeneratorInterface::ABSOLUTE_PATH, + UrlGeneratorInterface::ABSOLUTE_URL, + UrlGeneratorInterface::NETWORK_PATH, + UrlGeneratorInterface::RELATIVE_PATH, + ]) + // TODO NEXT MAJOR: remove the cache_manager.generate_url_type and enable this as default + // ->defaultValue('auto') + ->info('Set what URLs to generate on CacheManager::invalidate/refresh and InvalidationListener. Auto tries to guess the right mode based on your proxy client.') + ->end() + ->end() + ; $this->addCacheableResponseSection($rootNode); $this->addCacheControlSection($rootNode); $this->addProxyClientSection($rootNode); @@ -759,6 +785,7 @@ private function addCacheManagerSection(ArrayNodeDefinition $rootNode): void ->cannotBeEmpty() ->end() ->enumNode('generate_url_type') + ->setDeprecated('friends-of-symfony/http-cache-bundle', '3.4', 'Configure the url type on top level to also have it apply to the InvalidationListener in addition to the CacheManager') ->values([ 'auto', UrlGeneratorInterface::ABSOLUTE_PATH, @@ -767,7 +794,7 @@ private function addCacheManagerSection(ArrayNodeDefinition $rootNode): void UrlGeneratorInterface::RELATIVE_PATH, ]) ->defaultValue('auto') - ->info('Set what URLs to generate on invalidate/refresh Route. Auto means path if base_url is set on the default proxy client, full URL otherwise.') + ->info('Set what URLs to generate on invalidate/refresh Route. Auto tries to guess the right mode based on your proxy client.') ->end() ->end() ; diff --git a/src/DependencyInjection/FOSHttpCacheExtension.php b/src/DependencyInjection/FOSHttpCacheExtension.php index 37abc6b6..417017a5 100644 --- a/src/DependencyInjection/FOSHttpCacheExtension.php +++ b/src/DependencyInjection/FOSHttpCacheExtension.php @@ -81,22 +81,9 @@ public function load(array $configs, ContainerBuilder $container): void 'fos_http_cache.default_proxy_client' ); } - if ('auto' === $config['cache_manager']['generate_url_type']) { - if (array_key_exists('custom_proxy_client', $config['cache_manager'])) { - $generateUrlType = UrlGeneratorInterface::ABSOLUTE_URL; - } else { - $defaultClient = $this->getDefaultProxyClient($config['proxy_client']); - if ('noop' !== $defaultClient - && array_key_exists('base_url', $config['proxy_client'][$defaultClient])) { - $generateUrlType = UrlGeneratorInterface::ABSOLUTE_PATH; - } elseif ('cloudfront' === $defaultClient) { - $generateUrlType = UrlGeneratorInterface::ABSOLUTE_PATH; - } else { - $generateUrlType = UrlGeneratorInterface::ABSOLUTE_URL; - } - } - } else { - $generateUrlType = $config['cache_manager']['generate_url_type']; + $generateUrlType = (array_key_exists('generate_url_type', $config)) ? $config['generate_url_type'] : $config['cache_manager']['generate_url_type']; + if ('auto' === $generateUrlType) { + $generateUrlType = $this->determineGenerateUrlType($config); } $container->setParameter('fos_http_cache.cache_manager.generate_url_type', $generateUrlType); $loader->load('cache_manager.php'); @@ -129,6 +116,11 @@ public function load(array $configs, ContainerBuilder $container): void if (!empty($config['invalidation']['rules'])) { $this->loadInvalidatorRules($container, $config['invalidation']['rules']); } + $generateUrlType = (array_key_exists('generate_url_type', $config)) ? $config['generate_url_type'] : UrlGeneratorInterface::ABSOLUTE_PATH; + if ('auto' === $generateUrlType) { + $generateUrlType = $this->determineGenerateUrlType($config); + } + $container->setParameter('fos_http_cache.invalidation.generate_url_type', $generateUrlType); } if ($config['user_context']['enabled']) { @@ -745,4 +737,22 @@ private function getDefaultProxyClient(array $config): string throw new InvalidConfigurationException('No proxy client configured'); } + + private function determineGenerateUrlType(array $config): int + { + if (array_key_exists('cache_manager', $config) && array_key_exists('custom_proxy_client', $config['cache_manager'])) { + return UrlGeneratorInterface::ABSOLUTE_URL; + } + + $defaultClient = $this->getDefaultProxyClient($config['proxy_client']); + if ('noop' !== $defaultClient + && array_key_exists('base_url', $config['proxy_client'][$defaultClient])) { + return UrlGeneratorInterface::ABSOLUTE_PATH; + } + if ('cloudfront' === $defaultClient) { + return UrlGeneratorInterface::ABSOLUTE_PATH; + } + + return UrlGeneratorInterface::ABSOLUTE_URL; + } } diff --git a/src/EventListener/InvalidationListener.php b/src/EventListener/InvalidationListener.php index 58fb372b..16e2c102 100644 --- a/src/EventListener/InvalidationListener.php +++ b/src/EventListener/InvalidationListener.php @@ -40,6 +40,7 @@ public function __construct( private readonly UrlGeneratorInterface $urlGenerator, private readonly RuleMatcherInterface $mustInvalidateRule, private ?ExpressionLanguage $expressionLanguage = null, + private readonly int $generateUrlType = UrlGeneratorInterface::ABSOLUTE_PATH, ) { } @@ -127,7 +128,7 @@ private function handleInvalidation(Request $request, Response $response): void $requestParams = $request->attributes->get('_route_params'); foreach ($invalidatorConfigs as $route => $config) { - $path = $this->urlGenerator->generate($route, $requestParams); + $path = $this->urlGenerator->generate($route, $requestParams, $this->generateUrlType); // If extra route parameters should be ignored, strip the query // string generated by the Symfony router from the path if (isset($config['ignore_extra_params']) diff --git a/src/Resources/config/cache_manager.php b/src/Resources/config/cache_manager.php index 5f64f5dd..5663fff3 100644 --- a/src/Resources/config/cache_manager.php +++ b/src/Resources/config/cache_manager.php @@ -4,7 +4,6 @@ return static function (ContainerConfigurator $container) { $services = $container->services(); - $parameters = $container->parameters(); $services->set('fos_http_cache.cache_manager', \FOS\HttpCacheBundle\CacheManager::class) ->public() diff --git a/src/Resources/config/invalidation_listener.php b/src/Resources/config/invalidation_listener.php index c84bb562..36d0b9cd 100644 --- a/src/Resources/config/invalidation_listener.php +++ b/src/Resources/config/invalidation_listener.php @@ -4,7 +4,6 @@ return static function (ContainerConfigurator $container) { $services = $container->services(); - $parameters = $container->parameters(); $services->set('fos_http_cache.event_listener.invalidation', \FOS\HttpCacheBundle\EventListener\InvalidationListener::class) ->args([ @@ -12,6 +11,7 @@ service('router'), service('fos_http_cache.rule_matcher.must_invalidate'), service('fos_http_cache.invalidation.expression_language')->ignoreOnInvalid(), + '%fos_http_cache.invalidation.generate_url_type%', ]) ->tag('kernel.event_subscriber'); }; diff --git a/tests/Resources/Fixtures/config/full.php b/tests/Resources/Fixtures/config/full.php index d48bef14..f080d198 100644 --- a/tests/Resources/Fixtures/config/full.php +++ b/tests/Resources/Fixtures/config/full.php @@ -9,7 +9,10 @@ * file that was distributed with this source code. */ +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; + $container->loadFromExtension('fos_http_cache', [ + 'generate_url_type' => UrlGeneratorInterface::ABSOLUTE_URL, 'cacheable' => [ 'response' => [ 'additional_status' => [100, 500], diff --git a/tests/Resources/Fixtures/config/full.xml b/tests/Resources/Fixtures/config/full.xml index 0946d4ba..f667ddab 100644 --- a/tests/Resources/Fixtures/config/full.xml +++ b/tests/Resources/Fixtures/config/full.xml @@ -1,7 +1,9 @@ - + 100 diff --git a/tests/Resources/Fixtures/config/full.yml b/tests/Resources/Fixtures/config/full.yml index 3391373d..8784480a 100644 --- a/tests/Resources/Fixtures/config/full.yml +++ b/tests/Resources/Fixtures/config/full.yml @@ -1,4 +1,5 @@ fos_http_cache: + generate_url_type: 0 cacheable: response: diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php index 7ed2284d..5e6163ca 100644 --- a/tests/Unit/DependencyInjection/ConfigurationTest.php +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -21,6 +21,7 @@ use Symfony\Component\Config\Definition\Processor; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; class ConfigurationTest extends AbstractExtensionConfigurationTestCase { @@ -57,6 +58,7 @@ public function testEmptyConfiguration(): void public function testSupportsAllConfigFormats(): void { $expectedConfiguration = [ + 'generate_url_type' => UrlGeneratorInterface::ABSOLUTE_URL, 'cacheable' => [ 'response' => [ 'additional_status' => [100, 500], @@ -120,7 +122,7 @@ public function testSupportsAllConfigFormats(): void 'cache_manager' => [ 'enabled' => true, 'custom_proxy_client' => 'acme.proxy_client', - 'generate_url_type' => 'auto', + 'generate_url_type' => 'auto', // this is ignored by the extension when the top level value is set ], 'tags' => [ 'enabled' => 'auto', @@ -463,6 +465,22 @@ public function testEmptyServerConfigurationIsNotAllowed(): void (new Processor())->processConfiguration($configuration, ['fos_http_cache' => $params]); } + public function testConfiguringGenerateUrlTwiceNotAllowed(): void + { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Only configure "generate_url_type" and do not set the deprecated "cache_manager.generate_url_type" option'); + + $params = $this->getEmptyConfig(); + $params['generate_url_type'] = 'auto'; + $params['cache_manager'] = [ + 'custom_proxy_client' => 'noop', + 'generate_url_type' => UrlGeneratorInterface::ABSOLUTE_PATH, + ]; + + $configuration = new Configuration(false); + (new Processor())->processConfiguration($configuration, ['fos_http_cache' => $params]); + } + public function testDefaultIsNotConsideredAsServerConfig(): void { $params = $this->getEmptyConfig(); diff --git a/tests/Unit/EventListener/InvalidationListenerTest.php b/tests/Unit/EventListener/InvalidationListenerTest.php index e5fdeb30..debe62d4 100644 --- a/tests/Unit/EventListener/InvalidationListenerTest.php +++ b/tests/Unit/EventListener/InvalidationListenerTest.php @@ -16,8 +16,7 @@ use FOS\HttpCacheBundle\Configuration\InvalidateRoute; use FOS\HttpCacheBundle\EventListener\InvalidationListener; use FOS\HttpCacheBundle\Http\RuleMatcherInterface; -use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; -use Mockery\MockInterface; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Output\OutputInterface; @@ -32,21 +31,17 @@ class InvalidationListenerTest extends TestCase { - use MockeryPHPUnitIntegration; - - private CacheManager&MockInterface $cacheManager; - private UrlGeneratorInterface&MockInterface $urlGenerator; - private RuleMatcherInterface&MockInterface $mustInvalidateRule; + private CacheManager&MockObject $cacheManager; + private UrlGeneratorInterface&MockObject $urlGenerator; + private RuleMatcherInterface&MockObject $mustInvalidateRule; private InvalidationListener $listener; public function setUp(): void { - $this->cacheManager = \Mockery::mock(CacheManager::class); - $this->urlGenerator = \Mockery::mock(UrlGeneratorInterface::class); - $this->mustInvalidateRule = \Mockery::mock(RuleMatcherInterface::class) - ->shouldReceive('matches') - ->andReturn(true) - ->getMock(); + $this->cacheManager = $this->createMock(CacheManager::class); + $this->urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $this->mustInvalidateRule = $this->createMock(RuleMatcherInterface::class); + $this->mustInvalidateRule->method('matches')->willReturn(true); $this->listener = new InvalidationListener( $this->cacheManager, @@ -58,8 +53,11 @@ public function setUp(): void public function testNoRoutesInvalidatedWhenResponseIsUnsuccessful(): void { $this->cacheManager - ->shouldReceive('invalidateRoute')->never() - ->shouldReceive('flush')->once(); + ->expects($this->never()) + ->method('invalidateRoute'); + $this->cacheManager + ->expects($this->once()) + ->method('flush'); $request = new Request(); $request->attributes->set('_route', 'my_route'); @@ -70,11 +68,21 @@ public function testNoRoutesInvalidatedWhenResponseIsUnsuccessful(): void public function testOnKernelTerminate(): void { + $invalidatePathIndex = 0; + $this->cacheManager + ->expects($this->exactly(2)) + ->method('invalidatePath') + ->willReturnCallback(function (string $path) use (&$invalidatePathIndex): CacheManager { + self::assertSame([ + '/retrieve/something/123', + '/retrieve/something/123/bla', + ][$invalidatePathIndex++], $path); + + return $this->cacheManager; + }); $this->cacheManager - ->shouldReceive('invalidatePath')->with('/retrieve/something/123') - ->shouldReceive('invalidatePath')->with('/retrieve/something/123/bla') - ->shouldReceive('flush')->once() - ->getMock(); + ->expects($this->once()) + ->method('flush'); $routes = new RouteCollection(); $routes->add('route_invalidator', new Route('/edit/something/{id}/{special}')); @@ -82,16 +90,21 @@ public function testOnKernelTerminate(): void $routes->add('route_invalidated_special', new Route('/retrieve/something/{id}/{special}')); $requestParams = ['id' => 123, 'special' => 'bla']; + $generateIndex = 0; $this->urlGenerator - ->shouldDeferMissing() - ->shouldReceive('generate') - ->with('route_invalidated', $requestParams) - ->andReturn('/retrieve/something/123?special=bla') - - ->shouldReceive('generate') - ->with('route_invalidated_special', $requestParams) - ->andReturn('/retrieve/something/123/bla') - ->getMock(); + ->expects($this->exactly(2)) + ->method('generate') + ->willReturnCallback(function (string $name, array $params, int $referenceType) use ($requestParams, &$generateIndex): string { + [$expectedName, $expectedParams, $expectedReferenceType, $returnValue] = [ + ['route_invalidated', $requestParams, UrlGeneratorInterface::ABSOLUTE_PATH, '/retrieve/something/123?special=bla'], + ['route_invalidated_special', $requestParams, UrlGeneratorInterface::ABSOLUTE_PATH, '/retrieve/something/123/bla'], + ][$generateIndex++]; + self::assertSame($expectedName, $name); + self::assertSame($expectedParams, $params); + self::assertSame($expectedReferenceType, $referenceType); + + return $returnValue; + }); $requestMatcher = new AttributesRequestMatcher( ['_route' => 'route_invalidator'] @@ -110,9 +123,55 @@ public function testOnKernelTerminate(): void $this->listener->onKernelTerminate($event); } + public function testAbsoluteUrl(): void + { + $this->cacheManager + ->expects($this->once()) + ->method('invalidatePath') + ->with('http://localhost/retrieve/something/123') + ->willReturnSelf(); + $this->cacheManager + ->expects($this->once()) + ->method('flush'); + + $routes = new RouteCollection(); + $routes->add('route_invalidated', new Route('/retrieve/something/{id}')); + + $requestParams = ['id' => 123, 'special' => 'bla']; + $this->urlGenerator + ->expects($this->once()) + ->method('generate') + ->with('route_invalidated', $requestParams, UrlGeneratorInterface::ABSOLUTE_URL) + ->willReturn('http://localhost/retrieve/something/123?special=bla'); + + $requestMatcher = new AttributesRequestMatcher( + ['_route' => 'route_invalidator'] + ); + + $request = new Request(); + $request->attributes->set('_route', 'route_invalidator'); + $request->attributes->set('_route_params', $requestParams); + + $event = $this->getEvent($request); + + $listener = new InvalidationListener( + $this->cacheManager, + $this->urlGenerator, + $this->mustInvalidateRule, + null, + UrlGeneratorInterface::ABSOLUTE_URL, + ); + $listener->addRule($requestMatcher, [ + 'route_invalidated' => ['ignore_extra_params' => true], + ]); + $listener->onKernelTerminate($event); + } + public function testOnKernelException(): void { - $this->cacheManager->shouldReceive('flush')->once(); + $this->cacheManager + ->expects($this->once()) + ->method('flush'); $event = $this->getEvent(new Request()); $this->listener->onKernelException($event); } @@ -127,11 +186,22 @@ public function testInvalidatePath(): void $event = $this->getEvent($request); + $invalidatePathIndex = 0; + $this->cacheManager + ->expects($this->exactly(3)) + ->method('invalidatePath') + ->willReturnCallback(function (string $path) use (&$invalidatePathIndex): CacheManager { + self::assertSame([ + '/some/path', + '/other/path', + 'http://absolute.com/path', + ][$invalidatePathIndex++], $path); + + return $this->cacheManager; + }); $this->cacheManager - ->shouldReceive('invalidatePath')->with('/some/path')->once() - ->shouldReceive('invalidatePath')->with('/other/path')->once() - ->shouldReceive('invalidatePath')->with('http://absolute.com/path')->once() - ->shouldReceive('flush')->once(); + ->expects($this->once()) + ->method('flush'); $this->listener->onKernelTerminate($event); } @@ -147,26 +217,49 @@ public function testInvalidateRoute(): void $event = $this->getEvent($request); + $invalidateRouteIndex = 0; $this->cacheManager - ->shouldReceive('invalidateRoute')->with('some_route', [])->once() - ->shouldReceive('invalidateRoute')->with('other_route', ['id' => 123])->once() - ->shouldReceive('flush')->once(); + ->expects($this->exactly(2)) + ->method('invalidateRoute') + ->willReturnCallback(function (string $name, array $params) use (&$invalidateRouteIndex): CacheManager { + [$expectedName, $expectedParams] = [ + ['some_route', []], + ['other_route', ['id' => 123]], + ][$invalidateRouteIndex++]; + self::assertSame($expectedName, $name); + self::assertSame($expectedParams, $params); + + return $this->cacheManager; + }); + $this->cacheManager + ->expects($this->once()) + ->method('flush'); $this->listener->onKernelTerminate($event); } public function testOnConsoleTerminate(): void { - $this->cacheManager->shouldReceive('flush')->once()->andReturn(2); - - $output = \Mockery::mock(OutputInterface::class) - ->shouldReceive('getVerbosity')->once()->andReturn(OutputInterface::VERBOSITY_VERBOSE) - ->shouldReceive('writeln')->with('Sent 2 invalidation request(s)')->once() - ->getMock(); - - $event = \Mockery::mock(ConsoleEvent::class) - ->shouldReceive('getOutput')->andReturn($output) - ->getMock(); + $this->cacheManager + ->expects($this->once()) + ->method('flush') + ->willReturn(2); + + $output = $this->createMock(OutputInterface::class); + $output + ->expects($this->once()) + ->method('getVerbosity') + ->willReturn(OutputInterface::VERBOSITY_VERBOSE); + $output + ->expects($this->once()) + ->method('writeln') + ->with('Sent 2 invalidation request(s)'); + + $event = $this->createMock(ConsoleEvent::class); + $event + ->expects($this->exactly(2)) + ->method('getOutput') + ->willReturn($output); $this->listener->onConsoleTerminate($event); } @@ -174,7 +267,7 @@ public function testOnConsoleTerminate(): void protected function getEvent(Request $request, ?Response $response = null): TerminateEvent { return new TerminateEvent( - \Mockery::mock(HttpKernelInterface::class), + $this->createMock(HttpKernelInterface::class), $request, $response ?? new Response() );