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, https://github.com/symfony/symfony/issues/21764#issuecomment-313425723
| License | MIT
| Doc PR | symfony/symfony-docs#8209
As requested in https://github.com/symfony/symfony/issues/21764#issuecomment-313425723, 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
This commit is contained in:
commit
44d1162df4
@ -20,6 +20,8 @@ CHANGELOG
|
||||
`Symfony\Component\Translation\DependencyInjection\TranslatorPass` instead
|
||||
* Added `command` attribute to the `console.command` tag which takes the command
|
||||
name as value, using it makes the command lazy
|
||||
* Added `cache:pool:prune` command to allow manual stale cache item pruning of supported PSR-6 and PSR-16 cache pool
|
||||
implementations
|
||||
|
||||
3.3.0
|
||||
-----
|
||||
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Command;
|
||||
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Cache pool pruner command.
|
||||
*
|
||||
* @author Rob Frawley 2nd <rmf@src.run>
|
||||
*/
|
||||
final class CachePoolPruneCommand extends Command
|
||||
{
|
||||
private $pools;
|
||||
|
||||
/**
|
||||
* @param iterable|PruneableInterface[] $pools
|
||||
*/
|
||||
public function __construct($pools)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->pools = $pools;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('cache:pool:prune')
|
||||
->setDescription('Prune cache pools')
|
||||
->setHelp(<<<'EOF'
|
||||
The <info>%command.name%</info> command deletes all expired items from all pruneable pools.
|
||||
|
||||
%command.full_name%
|
||||
EOF
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
foreach ($this->pools as $name => $pool) {
|
||||
$io->comment(sprintf('Pruning cache pool: <info>%s</info>', $name));
|
||||
$pool->prune();
|
||||
}
|
||||
|
||||
$io->success('Successfully pruned cache pool(s).');
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
/**
|
||||
* @author Rob Frawley 2nd <rmf@src.run>
|
||||
*/
|
||||
class CachePoolPrunerPass implements CompilerPassInterface
|
||||
{
|
||||
private $cacheCommandServiceId;
|
||||
private $cachePoolTag;
|
||||
|
||||
public function __construct($cacheCommandServiceId = 'cache.command.pool_pruner', $cachePoolTag = 'cache.pool')
|
||||
{
|
||||
$this->cacheCommandServiceId = $cacheCommandServiceId;
|
||||
$this->cachePoolTag = $cachePoolTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
if (!$container->hasDefinition($this->cacheCommandServiceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$services = array();
|
||||
|
||||
foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) {
|
||||
$class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass());
|
||||
|
||||
if (!$reflection = $container->getReflectionClass($class)) {
|
||||
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
|
||||
}
|
||||
|
||||
if ($reflection->implementsInterface(PruneableInterface::class)) {
|
||||
$services[$id] = new Reference($id);
|
||||
}
|
||||
}
|
||||
|
||||
$container->getDefinition($this->cacheCommandServiceId)->replaceArgument(0, new IteratorArgument($services));
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddDebugLogProce
|
||||
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CacheCollectorPass;
|
||||
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPass;
|
||||
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolClearerPass;
|
||||
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPrunerPass;
|
||||
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass;
|
||||
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass;
|
||||
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass;
|
||||
@ -108,6 +109,7 @@ class FrameworkBundle extends Bundle
|
||||
$container->addCompilerPass(new CachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 32);
|
||||
$this->addCompilerPassIfExists($container, ValidateWorkflowsPass::class);
|
||||
$container->addCompilerPass(new CachePoolClearerPass(), PassConfig::TYPE_AFTER_REMOVING);
|
||||
$container->addCompilerPass(new CachePoolPrunerPass(), PassConfig::TYPE_AFTER_REMOVING);
|
||||
$this->addCompilerPassIfExists($container, FormPass::class);
|
||||
|
||||
if ($container->getParameter('kernel.debug')) {
|
||||
|
@ -100,6 +100,11 @@
|
||||
</call>
|
||||
</service>
|
||||
|
||||
<service id="cache.command.pool_pruner" class="Symfony\Bundle\FrameworkBundle\Command\CachePoolPruneCommand">
|
||||
<argument type="iterator" />
|
||||
<tag name="console.command" command="cache:pool:prune" />
|
||||
</service>
|
||||
|
||||
<service id="cache.default_clearer" class="Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer" public="true">
|
||||
<tag name="kernel.cache_clearer" />
|
||||
</service>
|
||||
|
@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Tests\Command;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Command\CachePoolPruneCommand;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
class CachePruneCommandTest extends TestCase
|
||||
{
|
||||
public function testCommandWithPools()
|
||||
{
|
||||
$tester = $this->getCommandTester($this->getKernel(), $this->getRewindableGenerator());
|
||||
$tester->execute(array());
|
||||
}
|
||||
|
||||
public function testCommandWithNoPools()
|
||||
{
|
||||
$tester = $this->getCommandTester($this->getKernel(), $this->getEmptyRewindableGenerator());
|
||||
$tester->execute(array());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RewindableGenerator
|
||||
*/
|
||||
private function getRewindableGenerator()
|
||||
{
|
||||
return new RewindableGenerator(function () {
|
||||
yield 'foo_pool' => $this->getPruneableInterfaceMock();
|
||||
yield 'bar_pool' => $this->getPruneableInterfaceMock();
|
||||
}, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RewindableGenerator
|
||||
*/
|
||||
private function getEmptyRewindableGenerator()
|
||||
{
|
||||
return new RewindableGenerator(function () {
|
||||
return new \ArrayIterator(array());
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \PHPUnit_Framework_MockObject_MockObject|KernelInterface
|
||||
*/
|
||||
private function getKernel()
|
||||
{
|
||||
$container = $this
|
||||
->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')
|
||||
->getMock();
|
||||
|
||||
$kernel = $this
|
||||
->getMockBuilder(KernelInterface::class)
|
||||
->getMock();
|
||||
|
||||
$kernel
|
||||
->expects($this->any())
|
||||
->method('getContainer')
|
||||
->willReturn($container);
|
||||
|
||||
$kernel
|
||||
->expects($this->once())
|
||||
->method('getBundles')
|
||||
->willReturn(array());
|
||||
|
||||
return $kernel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \PHPUnit_Framework_MockObject_MockObject|PruneableInterface
|
||||
*/
|
||||
private function getPruneableInterfaceMock()
|
||||
{
|
||||
$pruneable = $this
|
||||
->getMockBuilder(PruneableInterface::class)
|
||||
->getMock();
|
||||
|
||||
$pruneable
|
||||
->expects($this->atLeastOnce())
|
||||
->method('prune');
|
||||
|
||||
return $pruneable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param KernelInterface $kernel
|
||||
* @param RewindableGenerator $generator
|
||||
*
|
||||
* @return CommandTester
|
||||
*/
|
||||
private function getCommandTester(KernelInterface $kernel, RewindableGenerator $generator)
|
||||
{
|
||||
$application = new Application($kernel);
|
||||
$application->add(new CachePoolPruneCommand($generator));
|
||||
|
||||
return new CommandTester($application->find('cache:pool:prune'));
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\CachePoolPrunerPass;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
|
||||
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
class CachePoolPrunerPassTest extends TestCase
|
||||
{
|
||||
public function testCompilerPassReplacesCommandArgument()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('cache.command.pool_pruner')->addArgument(array());
|
||||
$container->register('pool.foo', FilesystemAdapter::class)->addTag('cache.pool');
|
||||
$container->register('pool.bar', PhpFilesAdapter::class)->addTag('cache.pool');
|
||||
|
||||
$pass = new CachePoolPrunerPass();
|
||||
$pass->process($container);
|
||||
|
||||
$expected = array(
|
||||
'pool.foo' => new Reference('pool.foo'),
|
||||
'pool.bar' => new Reference('pool.bar'),
|
||||
);
|
||||
$argument = $container->getDefinition('cache.command.pool_pruner')->getArgument(0);
|
||||
|
||||
$this->assertInstanceOf(IteratorArgument::class, $argument);
|
||||
$this->assertEquals($expected, $argument->getValues());
|
||||
}
|
||||
|
||||
public function testCompilePassIsIgnoredIfCommandDoesNotExist()
|
||||
{
|
||||
$container = $this
|
||||
->getMockBuilder(ContainerBuilder::class)
|
||||
->setMethods(array('hasDefinition', 'getDefinition', 'findTaggedServiceIds'))
|
||||
->getMock();
|
||||
|
||||
$container
|
||||
->expects($this->atLeastOnce())
|
||||
->method('hasDefinition')
|
||||
->with('cache.command.pool_pruner')
|
||||
->will($this->returnValue(false));
|
||||
|
||||
$container
|
||||
->expects($this->never())
|
||||
->method('getDefinition');
|
||||
|
||||
$container
|
||||
->expects($this->never())
|
||||
->method('findTaggedServiceIds');
|
||||
|
||||
$pass = new CachePoolPrunerPass();
|
||||
$pass->process($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
|
||||
* @expectedExceptionMessage Class "Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\NotFound" used for service "pool.not-found" cannot be found.
|
||||
*/
|
||||
public function testCompilerPassThrowsOnInvalidDefinitionClass()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$container->register('cache.command.pool_pruner')->addArgument(array());
|
||||
$container->register('pool.not-found', NotFound::class)->addTag('cache.pool');
|
||||
|
||||
$pass = new CachePoolPrunerPass();
|
||||
$pass->process($container);
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@
|
||||
"require": {
|
||||
"php": ">=5.5.9",
|
||||
"ext-xml": "*",
|
||||
"symfony/cache": "~3.3|~4.0",
|
||||
"symfony/cache": "~3.4|~4.0",
|
||||
"symfony/class-loader": "~3.2",
|
||||
"symfony/dependency-injection": "~3.3|~4.0",
|
||||
"symfony/config": "~3.3|~4.0",
|
||||
|
@ -11,9 +11,10 @@
|
||||
|
||||
namespace Symfony\Component\Cache\Adapter;
|
||||
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
use Symfony\Component\Cache\Traits\FilesystemTrait;
|
||||
|
||||
class FilesystemAdapter extends AbstractAdapter
|
||||
class FilesystemAdapter extends AbstractAdapter implements PruneableInterface
|
||||
{
|
||||
use FilesystemTrait;
|
||||
|
||||
|
@ -12,9 +12,10 @@
|
||||
namespace Symfony\Component\Cache\Adapter;
|
||||
|
||||
use Symfony\Component\Cache\Exception\CacheException;
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
use Symfony\Component\Cache\Traits\PhpFilesTrait;
|
||||
|
||||
class PhpFilesAdapter extends AbstractAdapter
|
||||
class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
|
||||
{
|
||||
use PhpFilesTrait;
|
||||
|
||||
|
@ -1,6 +1,14 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
||||
* added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning
|
||||
* added FilesystemTrait::prune() and PhpFilesTrait::prune() implementations
|
||||
* now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, and PhpFilesCache implement PruneableInterface and support
|
||||
manual stale cache pruning
|
||||
|
||||
3.3.0
|
||||
-----
|
||||
|
||||
|
23
src/Symfony/Component/Cache/PruneableInterface.php
Normal file
23
src/Symfony/Component/Cache/PruneableInterface.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\Component\Cache;
|
||||
|
||||
/**
|
||||
* Interface for adapters and simple cache implementations that allow pruning expired items.
|
||||
*/
|
||||
interface PruneableInterface
|
||||
{
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function prune();
|
||||
}
|
@ -11,9 +11,10 @@
|
||||
|
||||
namespace Symfony\Component\Cache\Simple;
|
||||
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
use Symfony\Component\Cache\Traits\FilesystemTrait;
|
||||
|
||||
class FilesystemCache extends AbstractCache
|
||||
class FilesystemCache extends AbstractCache implements PruneableInterface
|
||||
{
|
||||
use FilesystemTrait;
|
||||
|
||||
|
@ -12,9 +12,10 @@
|
||||
namespace Symfony\Component\Cache\Simple;
|
||||
|
||||
use Symfony\Component\Cache\Exception\CacheException;
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
use Symfony\Component\Cache\Traits\PhpFilesTrait;
|
||||
|
||||
class PhpFilesCache extends AbstractCache
|
||||
class PhpFilesCache extends AbstractCache implements PruneableInterface
|
||||
{
|
||||
use PhpFilesTrait;
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
|
||||
use Cache\IntegrationTests\CachePoolTest;
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
|
||||
abstract class AdapterTestCase extends CachePoolTest
|
||||
{
|
||||
@ -22,6 +23,10 @@ abstract class AdapterTestCase extends CachePoolTest
|
||||
if (!array_key_exists('testDeferredSaveWithoutCommit', $this->skippedTests) && defined('HHVM_VERSION')) {
|
||||
$this->skippedTests['testDeferredSaveWithoutCommit'] = 'Destructors are called late on HHVM.';
|
||||
}
|
||||
|
||||
if (!array_key_exists('testPrune', $this->skippedTests) && !$this->createCachePool() instanceof PruneableInterface) {
|
||||
$this->skippedTests['testPrune'] = 'Not a pruneable cache pool.';
|
||||
}
|
||||
}
|
||||
|
||||
public function testDefaultLifeTime()
|
||||
@ -67,6 +72,59 @@ abstract class AdapterTestCase extends CachePoolTest
|
||||
}
|
||||
$this->assertFalse($item->isHit());
|
||||
}
|
||||
|
||||
public function testPrune()
|
||||
{
|
||||
if (isset($this->skippedTests[__FUNCTION__])) {
|
||||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
|
||||
}
|
||||
|
||||
if (!method_exists($this, 'isPruned')) {
|
||||
$this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.');
|
||||
}
|
||||
|
||||
$cache = $this->createCachePool();
|
||||
|
||||
$doSet = function ($name, $value, \DateInterval $expiresAfter = null) use ($cache) {
|
||||
$item = $cache->getItem($name);
|
||||
$item->set($value);
|
||||
|
||||
if ($expiresAfter) {
|
||||
$item->expiresAfter($expiresAfter);
|
||||
}
|
||||
|
||||
$cache->save($item);
|
||||
};
|
||||
|
||||
$doSet('foo', 'foo-val');
|
||||
$doSet('bar', 'bar-val', new \DateInterval('PT20S'));
|
||||
$doSet('baz', 'baz-val', new \DateInterval('PT40S'));
|
||||
$doSet('qux', 'qux-val', new \DateInterval('PT80S'));
|
||||
|
||||
$cache->prune();
|
||||
$this->assertFalse($this->isPruned($cache, 'foo'));
|
||||
$this->assertFalse($this->isPruned($cache, 'bar'));
|
||||
$this->assertFalse($this->isPruned($cache, 'baz'));
|
||||
$this->assertFalse($this->isPruned($cache, 'qux'));
|
||||
|
||||
sleep(30);
|
||||
$cache->prune();
|
||||
$this->assertFalse($this->isPruned($cache, 'foo'));
|
||||
$this->assertTrue($this->isPruned($cache, 'bar'));
|
||||
$this->assertFalse($this->isPruned($cache, 'baz'));
|
||||
$this->assertFalse($this->isPruned($cache, 'qux'));
|
||||
|
||||
sleep(30);
|
||||
$cache->prune();
|
||||
$this->assertFalse($this->isPruned($cache, 'foo'));
|
||||
$this->assertTrue($this->isPruned($cache, 'baz'));
|
||||
$this->assertFalse($this->isPruned($cache, 'qux'));
|
||||
|
||||
sleep(30);
|
||||
$cache->prune();
|
||||
$this->assertFalse($this->isPruned($cache, 'foo'));
|
||||
$this->assertTrue($this->isPruned($cache, 'qux'));
|
||||
}
|
||||
}
|
||||
|
||||
class NotUnserializable implements \Serializable
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
|
||||
|
||||
/**
|
||||
@ -49,4 +50,12 @@ class FilesystemAdapterTest extends AdapterTestCase
|
||||
}
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
protected function isPruned(CacheItemPoolInterface $cache, $name)
|
||||
{
|
||||
$getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile');
|
||||
$getFileMethod->setAccessible(true);
|
||||
|
||||
return !file_exists($getFileMethod->invoke($cache, $name));
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Cache\Tests\Adapter;
|
||||
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
|
||||
|
||||
/**
|
||||
@ -35,4 +36,12 @@ class PhpFilesAdapterTest extends AdapterTestCase
|
||||
{
|
||||
FilesystemAdapterTest::rmdir(sys_get_temp_dir().'/symfony-cache');
|
||||
}
|
||||
|
||||
protected function isPruned(CacheItemPoolInterface $cache, $name)
|
||||
{
|
||||
$getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile');
|
||||
$getFileMethod->setAccessible(true);
|
||||
|
||||
return !file_exists($getFileMethod->invoke($cache, $name));
|
||||
}
|
||||
}
|
||||
|
@ -12,9 +12,19 @@
|
||||
namespace Symfony\Component\Cache\Tests\Simple;
|
||||
|
||||
use Cache\IntegrationTests\SimpleCacheTest;
|
||||
use Symfony\Component\Cache\PruneableInterface;
|
||||
|
||||
abstract class CacheTestCase extends SimpleCacheTest
|
||||
{
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
if (!array_key_exists('testPrune', $this->skippedTests) && !$this->createSimpleCache() instanceof PruneableInterface) {
|
||||
$this->skippedTests['testPrune'] = 'Not a pruneable cache pool.';
|
||||
}
|
||||
}
|
||||
|
||||
public static function validKeys()
|
||||
{
|
||||
if (defined('HHVM_VERSION')) {
|
||||
@ -59,6 +69,48 @@ abstract class CacheTestCase extends SimpleCacheTest
|
||||
}
|
||||
$this->assertNull($value);
|
||||
}
|
||||
|
||||
public function testPrune()
|
||||
{
|
||||
if (isset($this->skippedTests[__FUNCTION__])) {
|
||||
$this->markTestSkipped($this->skippedTests[__FUNCTION__]);
|
||||
}
|
||||
|
||||
if (!method_exists($this, 'isPruned')) {
|
||||
$this->fail('Test classes for pruneable caches must implement `isPruned($cache, $name)` method.');
|
||||
}
|
||||
|
||||
$cache = $this->createSimpleCache();
|
||||
|
||||
$cache->set('foo', 'foo-val');
|
||||
$cache->set('bar', 'bar-val', new \DateInterval('PT20S'));
|
||||
$cache->set('baz', 'baz-val', new \DateInterval('PT40S'));
|
||||
$cache->set('qux', 'qux-val', new \DateInterval('PT80S'));
|
||||
|
||||
$cache->prune();
|
||||
$this->assertFalse($this->isPruned($cache, 'foo'));
|
||||
$this->assertFalse($this->isPruned($cache, 'bar'));
|
||||
$this->assertFalse($this->isPruned($cache, 'baz'));
|
||||
$this->assertFalse($this->isPruned($cache, 'qux'));
|
||||
|
||||
sleep(30);
|
||||
$cache->prune();
|
||||
$this->assertFalse($this->isPruned($cache, 'foo'));
|
||||
$this->assertTrue($this->isPruned($cache, 'bar'));
|
||||
$this->assertFalse($this->isPruned($cache, 'baz'));
|
||||
$this->assertFalse($this->isPruned($cache, 'qux'));
|
||||
|
||||
sleep(30);
|
||||
$cache->prune();
|
||||
$this->assertFalse($this->isPruned($cache, 'foo'));
|
||||
$this->assertTrue($this->isPruned($cache, 'baz'));
|
||||
$this->assertFalse($this->isPruned($cache, 'qux'));
|
||||
|
||||
sleep(30);
|
||||
$cache->prune();
|
||||
$this->assertFalse($this->isPruned($cache, 'foo'));
|
||||
$this->assertTrue($this->isPruned($cache, 'qux'));
|
||||
}
|
||||
}
|
||||
|
||||
class NotUnserializable implements \Serializable
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Cache\Tests\Simple;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Symfony\Component\Cache\Simple\FilesystemCache;
|
||||
|
||||
/**
|
||||
@ -22,4 +23,12 @@ class FilesystemCacheTest extends CacheTestCase
|
||||
{
|
||||
return new FilesystemCache('', $defaultLifetime);
|
||||
}
|
||||
|
||||
protected function isPruned(CacheInterface $cache, $name)
|
||||
{
|
||||
$getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile');
|
||||
$getFileMethod->setAccessible(true);
|
||||
|
||||
return !file_exists($getFileMethod->invoke($cache, $name));
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
namespace Symfony\Component\Cache\Tests\Simple;
|
||||
|
||||
use Psr\SimpleCache\CacheInterface;
|
||||
use Symfony\Component\Cache\Simple\PhpFilesCache;
|
||||
|
||||
/**
|
||||
@ -30,4 +31,12 @@ class PhpFilesCacheTest extends CacheTestCase
|
||||
|
||||
return new PhpFilesCache('sf-cache');
|
||||
}
|
||||
|
||||
protected function isPruned(CacheInterface $cache, $name)
|
||||
{
|
||||
$getFileMethod = (new \ReflectionObject($cache))->getMethod('getFile');
|
||||
$getFileMethod->setAccessible(true);
|
||||
|
||||
return !file_exists($getFileMethod->invoke($cache, $name));
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ use Symfony\Component\Cache\Exception\CacheException;
|
||||
|
||||
/**
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Rob Frawley 2nd <rmf@src.run>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@ -22,6 +23,30 @@ trait FilesystemTrait
|
||||
{
|
||||
use FilesystemCommonTrait;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function prune()
|
||||
{
|
||||
$time = time();
|
||||
$pruned = true;
|
||||
|
||||
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
|
||||
if (!$h = @fopen($file, 'rb')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($time >= (int) $expiresAt = fgets($h)) {
|
||||
fclose($h);
|
||||
$pruned = isset($expiresAt[0]) && @unlink($file) && !file_exists($file) && $pruned;
|
||||
} else {
|
||||
fclose($h);
|
||||
}
|
||||
}
|
||||
|
||||
return $pruned;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -17,6 +17,7 @@ use Symfony\Component\Cache\Exception\InvalidArgumentException;
|
||||
/**
|
||||
* @author Piotr Stankowski <git@trakos.pl>
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
* @author Rob Frawley 2nd <rmf@src.run>
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
@ -31,6 +32,35 @@ trait PhpFilesTrait
|
||||
return function_exists('opcache_invalidate') && ini_get('opcache.enable');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function prune()
|
||||
{
|
||||
$time = time();
|
||||
$pruned = true;
|
||||
$allowCompile = 'cli' !== PHP_SAPI || ini_get('opcache.enable_cli');
|
||||
|
||||
set_error_handler($this->includeHandler);
|
||||
try {
|
||||
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
|
||||
list($expiresAt) = include $file;
|
||||
|
||||
if ($time >= $expiresAt) {
|
||||
$pruned = @unlink($file) && !file_exists($file) && $pruned;
|
||||
|
||||
if ($allowCompile) {
|
||||
@opcache_invalidate($file, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
return $pruned;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
Reference in New Issue
Block a user