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+
$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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy