8000 [Cache][FrameworkBundle] add `cache:pool:invalidate-tags` command · symfony/symfony@0657f14 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0657f14

Browse files
kbondchalasr
authored andcommitted
[Cache][FrameworkBundle] add cache:pool:invalidate-tags command
1 parent d88c30d commit 0657f14

File tree

8 files changed

+318
-0
lines changed

8 files changed

+318
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Environment variable `SYMFONY_IDE` is read by default when `framework.ide` config is not set.
88
* Load PHP configuration files by default in the `MicroKernelTrait`
9+
* Add `cache:pool:invalidate-tags` command
910

1011
6.0
1112
---
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Command;
13+
14+
use Symfony\Component\Console\Attribute\AsCommand;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Completion\CompletionInput;
17+
use Symfony\Component\Console\Completion\CompletionSuggestions;
18+
use Symfony\Component\Console\Input\InputArgument;
19+
use Symfony\Component\Console\Input\InputInterface;
20+
use Symfony\Component\Console\Input\InputOption;
21+
use Symfony\Component\Console\Output\OutputInterface;
22+
use Symfony\Component\Console\Style\SymfonyStyle;
23+
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
24+
use Symfony\Contracts\Cache\TagAwareCacheInterface;
25+
use Symfony\Contracts\Service\ServiceProviderInterface;
26+
27+
/**
28+
* @author Kevin Bond <kevinbond@gmail.com>
29+
*/
30+
#[AsCommand(name: 'cache:pool:invalidate-tags', description: 'Invalidate cache tags for all or a specific pool')]
31+
final class CachePoolInvalidateTagsCommand extends Command
32+
{
33+
private ServiceProviderInterface $pools;
34+
private array $poolNames;
35+
36+
public function __construct(ServiceProviderInterface $pools)
37+
{
38+
parent::__construct();
39+
40+
$this->pools = $pools;
41+
$this->poolNames = array_keys($pools->getProvidedServices());
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
protected function configure(): void
48+
{
49+
$this
50+
->addArgument('tags', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The tags to invalidate')
51+
->addOption('pool', 'p', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'The pools to invalidate on')
52+
->setHelp(<<<'EOF'
53+
The <info>%command.name%</info> command invalidates tags from taggable pools. By default, all pools
54+
have the passed tags invalidated. Pass <info>--pool=my_pool</info> to invalidate tags on a specific pool.
55+
56+
php %command.full_name% tag1 tag2
57+
php %command.full_name% tag1 tag2 --pool=cache2 --pool=cache1
58+
EOF)
59+
;
60+
}
61+
62+
/**
63+
* {@inheritdoc}
64+
*/
65+
protected function execute(InputInterface $input, OutputInterface $output): int
66+
{
67+
$io = new SymfonyStyle($input, $output);
68+
$pools = $input->getOption('pool') ?: $this->poolNames;
69+
$tags = $input->getArgument('tags');
70+
$tagList = implode(', ', $tags);
71+
$errors = false;
72+
73+
foreach ($pools as $name) {
74+
$io->comment(sprintf('Invalidating tag(s): <info>%s</info> from pool <comment>%s</comment>.', $tagList, $name));
75+
76+
try {
77+
$pool = $this->pools->get($name);
78+
} catch (ServiceNotFoundException) {
79+
$io->error(sprintf('Pool "%s" not found.', $name));
80+
$errors = true;
81+
82+
continue;
83+
}
84+
85+
if (!$pool instanceof TagAwareCacheInterface) {
86+
$io->error(sprintf('Pool "%s" is not taggable.', $name));
87+
$errors = true;
88+
89+
continue;
90+
}
91+
92+
if (!$pool->invalidateTags($tags)) {
93+
$io->error(sprintf('Cache tag(s) "%s" could not be invalidated for pool "%s".', $tagList, $name));
94+
$errors = true;
95+
}
96+
}
97+
98+
if ($errors) {
99+
$io->error('Done but with errors.');
100+
101+
return self::FAILURE;
102+
}
103+
104+
$io->success('Successfully invalidated cache tags.');
105+
106+
return self::SUCCESS;
107+
}
108+
109+
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
110+
{
111+
if ($input->mustSuggestOptionValuesFor('pool')) {
112+
$suggestions->suggestValues($this->poolNames);
113+
}
114+
}
115+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class UnusedTagsPass implements CompilerPassInterface
2727
'auto_alias',
2828
'cache.pool',
2929
'cache.pool.clearer',
30+
'cache.taggable',
3031
'chatter.transport_factory',
3132
'config_cache.resource_checker',
3233
'console.command',

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2145,9 +2145,11 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
21452145

21462146
if ($isRedisTagAware && 'cache.app' === $name) {
21472147
$container->setAlias('cache.app.taggable', $name);
2148+
$definition->addTag('cache.taggable', ['pool' => $name]);
21482149
} elseif ($isRedisTagAware) {
21492150
$tagAwareId = $name;
21502151
$container->setAlias('.'.$name.'.inner', $name);
2152+
$definition->addTag('cache.taggable', ['pool' => $name]);
21512153
} elseif ($pool['tags']) {
21522154
if (true !== $pool['tags'] && ($config['pools'][$pool['tags']]['tags'] ?? false)) {
21532155
$pool['tags'] = '.'.$pool['tags'].'.inner';
@@ -2156,6 +2158,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
21562158
->addArgument(new Reference('.'.$name.'.inner'))
21572159
->addArgument(true !== $pool['tags'] ? new Reference($pool['tags']) : null)
21582160
->setPublic($pool['public'])
2161+
->addTag('cache.taggable', ['pool' => $name])
21592162
;
21602163

21612164
if (method_exists(TagAwareAdapter::class, 'setLogger')) {
@@ -2172,6 +2175,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con
21722175
$tagAwareId = '.'.$name.'.taggable';
21732176
$container->register($tagAwareId, TagAwareAdapter::class)
21742177
->addArgument(new Reference($name))
2178+
->addTag('cache.taggable', ['pool' => $name])
21752179
;
21762180
}
21772181

src/Symfony/Bundle/FrameworkBundle/Resources/config/cache.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
->set('cache.app.taggable', TagAwareAdapter::class)
4141
->args([service('cache.app')])
42+
->tag('cache.taggable', ['pool' => 'cache.app'])
4243

4344
->set('cache.system')
4445
->parent('cache.adapter.system')

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Bundle\FrameworkBundle\Command\CacheClearCommand;
1717
use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand;
1818
use Symfony\Bundle\FrameworkBundle\Command\CachePoolDeleteCommand;
19+
use Symfony\Bundle\FrameworkBundle\Command\CachePoolInvalidateTagsCommand;
1920
use Symfony\Bundle\FrameworkBundle\Command\CachePoolListCommand;
2021
use Symfony\Bundle\FrameworkBundle\Command\CachePoolPruneCommand;
2122
use Symfony\Bundle\FrameworkBundle\Command\CacheWarmupCommand;
@@ -93,6 +94,12 @@
9394
])
9495
->tag('console.command')
9596

97+
->set('console.command.cache_pool_invalidate_tags', CachePoolInvalidateTagsCommand::class)
98+
->args([
99+
tagged_locator('cache.taggable', 'pool'),
100+
])
101+
->tag('console.command')
102+
96103
->set('console.command.cache_pool_delete', CachePoolDeleteCommand::class)
97104
->args([
98105
service('cache.global_clearer'),
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\Tests\Command;
13+
14+
use Symfony\Bundle\FrameworkBundle\Command\CachePoolInvalidateTagsCommand;
15+
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Tester\CommandCompletionTester;
18+
use Symfony\Component\Console\Tester\CommandTester;
19+
use Symfony\Component\DependencyInjection\ServiceLocator;
20+
use Symfony\Contracts\Cache\TagAwareCacheInterface;
21+
22+
class CachePoolInvalidateTagsCommandTest extends TestCase
23+
{
24+
public function testComplete()
25+
{
26+
$tester = new CommandCompletionTester($this->createCommand(['foo' => null, 'bar' => null]));
27+
28+
$suggestions = $tester->complete(['--pool=']);
29+
30+
$this->assertSame(['foo', 'bar'], $suggestions);
31+
}
32+
33+
public function testInvalidatesTagsForAllPoolsByDefault()
34+
{
35+
$tagsToInvalidate = ['tag1', 'tag2'];
36+
37+
$foo = $this->createMock(TagAwareCacheInterface::class);
38+
$foo->expects($this->once())->method('invalidateTags')->with($tagsToInvalidate)->willReturn(true);
39+
40+
$bar = $this->createMock(TagAwareCacheInterface::class);
41+
$bar->expects($this->once())->method('invalidateTags')->with($tagsToInvalidate)->willReturn(true);
42+
43+
$tester = new CommandTester($this->createCommand([
44+
'foo' => $foo,
45+
'bar' => $bar,
46+
]));
47+
48+
$ret = $tester->execute(['tags' => $tagsToInvalidate]);
49+
50+
$this->assertSame(Command::SUCCESS, $ret);
51+
}
52+
53+
public function testCanInvalidateSpecificPools()
54+
{
55+
$tagsToInvalidate = ['tag1', 'tag2'];
56+
57+
$foo = $this->createMock(TagAwareCacheInterface::class);
58+
$foo->expects($this->once())->method('invalidateTags')->with($tagsToInvalidate)->willReturn(true);
59+
60+
$bar = $this->createMock(TagAwareCacheInterface::class);
61+
$bar->expects($this->never())->method('invalidateTags');
62+
63+
$tester = new CommandTester($this->createCommand([
64+
'foo' => $foo,
65+
'bar' => $bar,
66+
]));
67+
68+
$ret = $tester->execute(['tags' => $tagsToInvalidate, '--pool' => ['foo']]);
69+
70+
$this->assertSame(Command::SUCCESS, $ret);
71+
}
72+
73+
public function testCommandFailsIfPoolNotFound()
74+
{
75+
$tagsToInvalidate = ['tag1', 'tag2'];
76+
77+
$foo = $this->createMock(TagAwareCacheInterface::class);
78+
$foo->expects($this->once())->method('invalidateTags')->with($tagsToInvalidate)->willReturn(true);
79+
80+
$bar = $this->createMock(TagAwareCacheInterface::class);
81+
$bar->expects($this->never())->method('invalidateTags');
82+
83+
$tester = new CommandTester($this->createCommand([
84+
'foo' => $foo,
85+
'bar' => $bar,
86+
]));
87+
88+
$ret = $tester->execute(['tags' => $tagsToInvalidate, '--pool' => ['invalid', 'foo']]);
89+
90+
$this->assertSame(Command::FAILURE, $ret);
91+
}
92+
93+
public function testCommandFailsIfPoolNotTaggable()
94+
{
95+
$tagsToInvalidate = ['tag1', 'tag2'];
96+
97+
$foo = new \stdClass();
98+
99+
$bar = $this->createMock(TagAwareCacheInterface::class);
100+
$bar->expects($this->once())->method('invalidateTags')->with($tagsToInvalidate)->willReturn(true);
101+
102+
$tester = new CommandTester($this->createCommand([
103+
'foo' => $foo,
104+
'bar' => $bar,
105+
]));
106+
107+
$ret = $tester->execute(['tags' => $tagsToInvalidate]);
108+
109+
$this->assertSame(Command::FAILURE, $ret);
110+
}
111+
112+
public function testCommandFailsIfInvalidatingTagsFails()
113+
{
114+
$tagsToInvalidate = ['tag1', 'tag2'];
115+
116+
$foo = $this->createMock(TagAwareCacheInterface::class);
117+
$foo->expects($this->once())->method('invalidateTags')->with($tagsToInvalidate)->willReturn(false);
118+
119+
$bar = $this->createMock(TagAwareCacheInterface::class);
120+
$bar->expects($this->once())->method('invalidateTags')->with($tagsToInvalidate)->willReturn(true);
121+
122+
$tester = new CommandTester($this->createCommand([
123+
'foo' => $foo,
124+
'bar' => $bar,
125+
]));
126+
127+
$ret = $tester->execute(['tags' => $tagsToInvalidate]);
128+
129+
$this->assertSame(Command::FAILURE, $ret);
130+
}
131+
132+
private function createCommand(array $services): CachePoolInvalidateTagsCommand
133+
{
134+
return new CachePoolInvalidateTagsCommand(
135+
new ServiceLocator(array_map(fn ($service) => fn () => $service, $services))
136+
);
137+
}
138+
}

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
use Symfony\Component\Cache\DependencyInjection\CachePoolPass;
3232
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
3333
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
34+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
35+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
3436
use Symfony\Component\DependencyInjection\ChildDefinition;
3537
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
3638
use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass;
@@ -1627,6 +1629,55 @@ public function appRedisTagAwareConfigProvider(): array
16271629
];
16281630
}
16291631

1632+
public function testCacheTaggableTagAppliedToPools()
1633+
{
1634+
$container = $this->createContainerFromFile('cache');
1635+
1636+
$servicesToCheck = [
1637+
'cache.app.taggable' => 'cache.app',
1638+
'cache.redis_tag_aware.bar' => 'cache.redis_tag_aware.bar',
1639+
'.cache.foobar.taggable' => 'cache.foobar',
1640+
];
1641+
1642+
foreach ($servicesToCheck as $id => $expectedPool) {
1643+
$this->assertTrue($container->hasDefinition($id));
1644+
1645+
$def = $container->getDefinition($id);
1646+
1647+
$this->assertTrue($def->hasTag('cache.taggable'));
1648+
$this->assertSame($expectedPool, $def->getTag('cache.taggable')[0]['pool'] ?? null);
1649+
}
1650+
}
1651+
1652+
/**
1653+
* @dataProvider appRedisTagAwareConfigProvider
1654+
*/
1655+
public function testCacheTaggableTagAppliedToRedisAwareAppPool(string $configFile)
1656+
{
1657+
$container = $this->createContainerFromFile($configFile);
1658+
1659+
$def = $container->getDefinition('cache.app');
1660+
1661+
$this->assertTrue($def->hasTag('cache.taggable'));
1662+
$this->assertSame('cache.app', $def->getTag('cache.taggable')[0]['pool'] ?? null);
1663+
}
1664+
1665+
public function testCachePoolInvalidateTagsCommandRegistered()
1666+
{
1667+
$container = $this->createContainerFromFile('cache');
1668+
$this->assertTrue($container->hasDefinition('console.command.cache_pool_invalidate_tags'));
1669+
1670+
$locator = $container->getDefinition('console.command.cache_pool_invalidate_tags')->getArgument(0);
1671+
$this->assertInstanceOf(ServiceLocatorArgument::class, $locator);
1672+
1673+
$iterator = $locator->getTaggedIteratorArgument();
1674+
$this->assertInstanceOf(TaggedIteratorArgument::class, $iterator);
1675+
1676+
$this->assertSame('cache.taggable', $iterator->getTag());
1677+
$this->assertSame('pool', $iterator->getIndexAttribute());
1678+
$this->assertTrue($iterator->needsIndexes());
1679+
}
1680+
16301681
public function testRemovesResourceCheckerConfigCacheF 43CC actoryArgumentOnlyIfNoDebug()
16311682
{
16321683
$container = $this->createContainer(['kernel.debug' => true]);

0 commit comments

Comments
 (0)
0