8000 feature #23451 [Cache] Add (filesystem|phpfiles) cache (adapter|simpl… · symfony/symfony@44d1162 · GitHub
[go: up one dir, main page]

Skip to content

Commit 44d1162

Browse files
feature #23451 [Cache] Add (filesystem|phpfiles) cache (adapter|simple) prune method and prune command (robfrawley)
This PR was merged into the 3.4 branch. Discussion ---------- [Cache] Add (filesystem|phpfiles) cache (adapter|simple) prune method and prune command | Q | A | ------------- | --- | Branch? | 3.4 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #21764, #21764 (comment) | License | MIT | Doc PR | symfony/symfony-docs#8209 As requested in #21764 (comment), this PR adds a `prune()` method to [`FilesystemTrait`](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Cache/Traits/FilesystemTrait.php). This placement seems reasonable as it exposes the method in [`FilesystemAdapter`](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php) and [`FilesystemCache`](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Cache/Simple/FilesystemCache.php). The return value is a `bool` representing either a partial or complete failure (when `false`) *or* complete success (when `true`). Once the API for the `prune` method is confirmed, I'll introduce a documentation PR, as well. --- *Stale-detection implementation:* The file modification time is used to determine if a cache item should be pruned. This seems reasonable, given the use of [`touch` in the common trait](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Cache/Traits/FilesystemCommonTrait.php#L90). Interestingly, though, the [`doFetch` method](https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Cache/Traits/FilesystemTrait.php#L38) uses the timestamp saved at the top of the file itself to determine the stale state. Should this latter implementation be used for `prune` as well (or is the current one ok), for example: ```php foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD) as $file) { if ($h = @fopen($file, 'rb')) { if ($time >= (int) $expiresAt = fgets($h)) { fclose($h); if (isset($expiresAt[0])) { $okay = (@Unlink($file) && !file_exists($file)) && $okay; } } } } ``` Commits ------- f0d0c5f add (filesystem|phpfiles) cache (adapter|simple) prune method and prune command
2 parents 68d9df6 + f0d0c5f commit 44d1162

22 files changed

+574
-5
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ CHANGELOG
2020
`Symfony\Component\Translation\DependencyInjection\TranslatorPass` instead
2121
* Added `command` attribute to the `console.command` tag which takes the command
2222
name as value, using it makes the command lazy
23+
* Added `cache:pool:prune` command to allow manual stale cache item pruning of supported PSR-6 and PSR-16 cache pool
24+
implementations
2325

2426
3.3.0
2527
-----
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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\Cache\PruneableInterface;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Input\InputInterface;
17+
use Symfony\Component\Console\Output\OutputInterface;
18+
use Symfony\Component\Console\Style\SymfonyStyle;
19+
20+
/**
21+
* Cache pool pruner command.
22+
*
23+
* @author Rob Frawley 2nd <rmf@src.run>
24+
*/
25+
final class CachePoolPruneCommand extends Command
26+
{
27+
private $pools;
28+
29+
/**
30+
* @param iterable|PruneableInterface[] $pools
31+
*/
32+
public function __construct($pools)
33+
{
34+
parent::__construct();
35+
36+
$this->pools = $pools;
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
protected function configure()
43+
{
44+
$this
45+
->setName('cache:pool:prune')
46+
->setDescription('Prune cache pools')
47+
->setHelp(<<<'EOF'
48+
The <info>%command.name%</info> command deletes all expired items from all pruneable pools.
49+
50+
%command.full_name%
51+
EOF
52+
)
53+
;
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
protected function execute(InputInterface $input, OutputInterface $output)
60+
{
61+
$io = new SymfonyStyle($input, $output);
62+
63+
foreach ($this->pools as $name => $pool) {
64+
$io->comment(sprintf('Pruning cache pool: <info>%s</info>', $name));
65+
$pool->prune();
66+
}
67+
68+
$io->success('Successfully pruned cache pool(s).');
69+
}
70+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\Cache\PruneableInterface;
15+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
16+
use Symfony\Component\DependencyInjection\ContainerBuilder;
17+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
18+
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
21+
/**
22+
* @author Rob Frawley 2nd <rmf@src.run>
23+
*/
24+
class CachePoolPrunerPass implements CompilerPassInterface
25+
{
26+
private $cacheCommandServiceId;
27+
private $cachePoolTag;
28+
29+
public function __construct($cacheCommandServiceId = 'cache.command.pool_pruner', $cachePoolTag = 'cache.pool')
30+
{
31+
$this->cacheCommandServiceId = $cacheCommandServiceId;
32+
$this->cachePoolTag = $cachePoolTag;
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
public function process(ContainerBuilder $container)
39+
{
40+
if (!$container->hasDefinition($this->cacheCommandServiceId)) {
41+
return;
42+
}
43+
44+
$services = array();
45+
46+
foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) {
47+
$class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass());
48+
49+
if (!$reflection = $container->getReflectionClass($class)) {
50+
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
51+
}
52+
53+
if ($reflection->implementsInterface(PruneableInterface::class)) {
54+
$services[$id] = new Reference($id);
55+
}
56+
}
57+
58+
$container->getDefinition($this->cacheCommandServiceId)->replaceArgument(0, new IteratorArgument($services));
59+
}
60+
}

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CacheCollectorPass;
1717
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPass;
1818
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolClearerPass;
19+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPrunerPass;
1920
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass;
2021
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass;
2122
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
@@ -108,6 +109,7 @@ public function build(ContainerBuilder $container)
108109
$container->addCompilerPass(new CachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 32);
109110
$this->addCompilerPassIfExists($container, ValidateWorkflowsPass::class);
110111
$container->addCompilerPass(new CachePoolClearerPass(), PassConfig::TYPE_AFTER_REMOVING);
112+
$container->addCompilerPass(new CachePoolPrunerPass(), PassConfig::TYPE_AFTER_REMOVING);
111113
$this->addCompilerPassIfExists($container, FormPass::class);
112114

113115
if ($container->getParameter('kernel.debug')) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@
100100
</call>
101101
</service>
102102

103+
<service id="cache.command.pool_pruner" class="Symfony\Bundle\FrameworkBundle\Command\CachePoolPruneCommand">
104+
<argument type="iterator" />
105+
<tag name="console.command" command="cache:pool:prune" />
106+
</service>
107+
103108
<service id="cache.default_clearer" class="Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer" public="true">
104109
<tag name="kernel.cache_clearer" />
105110
</service>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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\CachePoolPruneCommand;
15+
use Symfony\Bundle\FrameworkBundle\Console\Application;
16+
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
17+
use Symfony\Component\Cache\PruneableInterface;
18+
use Symfony\Component\Console\Tester\CommandTester;
19+
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
20+
use Symfony\Component\HttpKernel\KernelInterface;
21+
22+
class CachePruneCommandTest extends TestCase
23+
{
24+
public function testCommandWithPools()
25+
{
26+
$tester = $this->getCommandTester($this->getKernel(), $this->getRewindableGenerator());
27+
$tester->execute(array());
28+
}
29+
30+
public function testCommandWithNoPools()
31+
{
32+
$tester = $this->getCommandTester($this->getKernel(), $this->getEmptyRewindableGenerator());
33+
$tester->execute(array());
34+
}
35+
36+
/**
37+
* @return RewindableGenerator
38+
*/
39+
private function getRewindableGenerator()
40+
{
41+
return new RewindableGenerator(function () {
42+
yield 'foo_pool' => $this->getPruneableInterfaceMock();
43+
yield 'bar_pool' => $this->getPruneableInterfaceMock();
44+
}, 2);
45+
}
46+
47+
/**
48+
* @return RewindableGenerator
49+
*/
50+
private function getEmptyRewindableGenerator()
51+
{
52+
return new RewindableGenerator(function () {
53+
return new \ArrayIterator(array());
54+
}, 0);
55+
}
56+
57+
/**
58+
* @return \PHPUnit_Framework_MockObject_MockObject|KernelInterface
59+
*/
60+
private function getKernel()
61+
{
62+
$container = $this
63+
->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')
64+
->getMock();
65+
66+
$kernel = $this
67+
->getMockBuilder(KernelInterface::class)
68+
->getMock();
69+
70+
$kernel
71+
->expects($this->any())
72+
->method('getContainer')
73+
->willReturn($container);
74+
75+
$kernel
76+
->expects($this->once())
77+
->method('getBundles')
78+
->willReturn(array());
79+
80+
return $kernel;
81+
}
82+
83+
/**
84+
* @return \PHPUnit_Framework_MockObject_MockObject|PruneableInterface
85+
*/
86+
private function getPruneableInterfaceMock()
87+
{
88+
$pruneable = $this
89+
->getMockBuilder(PruneableInterface::class)
90+
->getMock();
91+
92+
$pruneable
93+
->expects($this->atLeastOnce())
94+
->method('prune');
95+
96+
return $pruneable;
97+
}
98+
99+
/**
100+
* @param KernelInterface $kernel
101+
* @param RewindableGenerator $generator
102+
*
103+
* @return CommandTester
104+
*/
105+
private function getCommandTester(KernelInterface $kernel, RewindableGenerator $generator)
106+
{
107+
$application = new Application($kernel);
108+
$application->add(new CachePoolPruneCommand($generator));
109+
110+
return new CommandTester($application->find('cache:pool:prune'));
111+
}
112+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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\DependencyInjection\Compiler;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPrunerPass;
16+
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
17+
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
18+
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
19+
use Symfony\Component\DependencyInjection\ContainerBuilder;
20+
use Symfony\Component\DependencyInjection\Reference;
21+
22+
class CachePoolPrunerPassTest extends TestCase
23+
{
24+
public function testCompilerPassReplacesCommandArgument()
25+
{
26+
$container = new ContainerBuilder();
27+
$container->register('cache.command.pool_pruner')->addArgument(array());
28+
$container->register('pool.foo', FilesystemAdapter::class)->addTag('cache.pool');
29+
$ 741A container->register('pool.bar', PhpFilesAdapter::class)->addTag('cache.pool');
30+
31+
$pass = new CachePoolPrunerPass();
32+
$pass->process($container);
33+
34+
$expected = array(
35+
'pool.foo' => new Reference('pool.foo'),
36+
'pool.bar' => new Reference('pool.bar'),
37+
);
38+
$argument = $container->getDefinition('cache.command.pool_pruner')->getArgument(0);
39+
40+
$this->assertInstanceOf(IteratorArgument::class, $argument);
41+
$this->assertEquals($expected, $argument->getValues());
42+
}
43+
44+
public function testCompilePassIsIgnoredIfCommandDoesNotExist()
45+
{
46+
$container = $this
47+
->getMockBuilder(ContainerBuilder::class)
48+
->setMethods(array('hasDefinition', 'getDefinition', 'findTaggedServiceIds'))
49+
->getMock();
50+
51+
$container
52+
->expects($this->atLeastOnce())
53+
->method('hasDefinition')
54+
->with('cache.command.pool_pruner')
55+
->will($this->returnValue(false));
56+
57+
$container
58+
->expects($this->never())
59+
->method('getDefinition');
60+
61+
$container
62+
->expects($this->never())
63+
->method('findTaggedServiceIds');
64+
65+
$pass = new CachePoolPrunerPass();
66+
$pass->process($container);
67+
}
68+
69+
/**
70+
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
71+
* @expectedExceptionMessage Class "Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\NotFound" used for service "pool.not-found" cannot be found.
72+
*/
73+
public function testCompilerPassThrowsOnInvalidDefinitionClass()
74+
{
75+
$container = new ContainerBuilder();
76+
$container->register('cache.command.pool_pruner')->addArgument(array());
77+
$container->register('pool.not-found', NotFound::class)->addTag('cache.pool');
78+
79+
$pass = new CachePoolPrunerPass();
80+
$pass->process($container);
81+
}
82+
}

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"require": {
1919
"php": ">=5.5.9",
2020
"ext-xml": "*",
21-
"symfony/cache": "~3.3|~4.0",
21+
"symfony/cache": "~3.4|~4.0",
2222
"symfony/class-loader": "~3.2",
2323
"symfony/dependency-injection": "~3.3|~4.0",
2424
"symfony/config": "~3.3|~4.0",

src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14+
use Symfony\Component\Cache\PruneableInterface;
1415
use Symfony\Component\Cache\Traits\FilesystemTrait;
1516

16-
class FilesystemAdapter extends AbstractAdapter
17+
class FilesystemAdapter extends AbstractAdapter implements PruneableInterface
1718
{
1819
use FilesystemTrait;
1920

src/Symfony/Component/Cache/Adapter/PhpFilesAdapter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
namespace Symfony\Component\Cache\Adapter;
1313

1414
use Symfony\Component\Cache\Exception\CacheException;
15+
use Symfony\Component\Cache\PruneableInterface;
1516
use Symfony\Component\Cache\Traits\PhpFilesTrait;
1617

17-
class PhpFilesAdapter extends AbstractAdapter
18+
class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
1819
{
1920
use PhpFilesTrait;
2021

0 commit comments

Comments
 (0)
0