From 20d372212081de55107a3ad08d842ddbcb1578eb Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Wed, 9 Sep 2015 13:50:25 +0200 Subject: [PATCH] Implement service-based Resource (cache) validation --- UPGRADE-2.8.md | 32 ++++ UPGRADE-3.0.md | 7 + .../Compiler/ConfigCachePass.php | 45 ++++++ .../FrameworkBundle/FrameworkBundle.php | 2 + .../Resources/config/routing.xml | 3 + .../Resources/config/services.xml | 16 ++ .../Resources/config/translation.xml | 3 + .../CacheClearCommandTest.php | 16 +- .../Compiler/ConfigCachePassTest.php | 68 +++++++++ .../FrameworkExtensionTest.php | 4 +- .../Tests/Translation/TranslatorTest.php | 28 ---- .../Bundle/FrameworkBundle/composer.json | 2 +- src/Symfony/Component/Config/CHANGELOG.md | 3 + src/Symfony/Component/Config/ConfigCache.php | 101 +++---------- .../Component/Config/ConfigCacheFactory.php | 7 +- .../Resource/BCResourceInterfaceChecker.php | 36 +++++ .../Config/Resource/DirectoryResource.php | 2 +- .../Config/Resource/FileExistenceResource.php | 2 +- .../Config/Resource/FileResource.php | 2 +- .../Config/Resource/ResourceInterface.php | 11 +- .../Resource/SelfCheckingResourceChecker.php | 36 +++++ .../SelfCheckingResourceInterface.php | 30 ++++ .../Config/ResourceCheckerConfigCache.php | 140 ++++++++++++++++++ .../ResourceCheckerConfigCacheFactory.php | 51 +++++++ .../Config/ResourceCheckerInterface.php | 49 ++++++ .../Config/Tests/ConfigCacheTest.php | 106 +++++-------- .../Config/Tests/Resource/ResourceStub.php | 39 +++++ .../Tests/ResourceCheckerConfigCacheTest.php | 118 +++++++++++++++ .../Config/EnvParametersResource.php | 4 +- .../Component/HttpKernel/composer.json | 2 +- .../Translation/Tests/TranslatorCacheTest.php | 6 +- .../Component/Translation/composer.json | 2 +- 32 files changed, 767 insertions(+), 206 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ConfigCachePass.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ConfigCachePassTest.php create mode 100644 src/Symfony/Component/Config/Resource/BCResourceInterfaceChecker.php create mode 100644 src/Symfony/Component/Config/Resource/SelfCheckingResourceChecker.php create mode 100644 src/Symfony/Component/Config/Resource/SelfCheckingResourceInterface.php create mode 100644 src/Symfony/Component/Config/ResourceCheckerConfigCache.php create mode 100644 src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php create mode 100644 src/Symfony/Component/Config/ResourceCheckerInterface.php create mode 100644 src/Symfony/Component/Config/Tests/Resource/ResourceStub.php create mode 100644 src/Symfony/Component/Config/Tests/ResourceCheckerConfigCacheTest.php diff --git a/UPGRADE-2.8.md b/UPGRADE-2.8.md index 24861929c8ae..b84f3fbe8b87 100644 --- a/UPGRADE-2.8.md +++ b/UPGRADE-2.8.md @@ -402,3 +402,35 @@ FrameworkBundle session: cookie_httponly: false ``` + +Config +------ + + * The `\Symfony\Component\Config\Resource\ResourceInterface::isFresh()` method has been + deprecated and will be removed in Symfony 3.0 because it assumes that resource + implementations are able to check themselves for freshness. + + If you have custom resources that implement this method, change them to implement the + `\Symfony\Component\Config\Resource\SelfCheckingResourceInterface` sub-interface instead + of `\Symfony\Component\Config\Resource\ResourceInterface`. + + Before: + + ```php + use Symfony\Component\Config\Resource\ResourceInterface; + + class MyCustomResource implements ResourceInterface { ... } + ``` + + After: + + ```php + use Symfony\Component\Config\Resource\SelfCheckingResourceInterface; + + class MyCustomResource implements SelfCheckingResourceInterface { ... } + ``` + + Additionally, if you have implemented cache validation strategies *using* `isFresh()` + yourself, you should have a look at the new cache validation system based on + `ResourceChecker`s. + diff --git a/UPGRADE-3.0.md b/UPGRADE-3.0.md index de9a230a5546..4f8028dc873c 100644 --- a/UPGRADE-3.0.md +++ b/UPGRADE-3.0.md @@ -1201,3 +1201,10 @@ UPGRADE FROM 2.x to 3.0 * `Process::setStdin()` and `Process::getStdin()` have been removed. Use `Process::setInput()` and `Process::getInput()` that works the same way. * `Process::setInput()` and `ProcessBuilder::setInput()` do not accept non-scalar types. + +### Config + + * `\Symfony\Component\Config\Resource\ResourceInterface::isFresh()` has been removed. Also, + cache validation through this method (which was still supported in 2.8 for BC) does no longer + work because the `\Symfony\Component\Config\Resource\BCResourceInterfaceChecker` helper class + has been removed as well. diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ConfigCachePass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ConfigCachePass.php new file mode 100644 index 000000000000..a8e1c549a2d6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ConfigCachePass.php @@ -0,0 +1,45 @@ + + * + * 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\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Adds services tagged config_cache.resource_checker to the config_cache_factory service, ordering them by priority. + * + * @author Matthias Pigulla + * @author Benjamin Klotz + */ +class ConfigCachePass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + $resourceCheckers = array(); + + foreach ($container->findTaggedServiceIds('config_cache.resource_checker') as $id => $tags) { + $priority = isset($tags[0]['priority']) ? $tags[0]['priority'] : 0; + $resourceCheckers[$priority][] = new Reference($id); + } + + if (empty($resourceCheckers)) { + return; + } + + // sort by priority and flatten + krsort($resourceCheckers); + $resourceCheckers = call_user_func_array('array_merge', $resourceCheckers); + + $container->getDefinition('config_cache_factory')->replaceArgument(0, $resourceCheckers); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 499f39c6c95c..7b1c77f225e5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -28,6 +28,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationExtractorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationDumperPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SerializerPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigCachePass; use Symfony\Component\Debug\ErrorHandler; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\PassConfig; @@ -92,6 +93,7 @@ public function build(ContainerBuilder $container) if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new ContainerBuilderDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new CompilerDebugDumpPass(), PassConfig::TYPE_AFTER_REMOVING); + $container->addCompilerPass(new ConfigCachePass()); } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index df9e24abab88..a750027406ec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -75,6 +75,9 @@ + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index 2021505726fe..f0d54b0d76a4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -63,5 +63,21 @@ %kernel.secret% + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml index 0007a360c6e4..ff4c18f212f2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml @@ -45,6 +45,9 @@ %kernel.debug% + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php index 745e015d57d6..0cfc92241b82 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php @@ -5,7 +5,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\Command\CacheClearCommand\Fixture\TestAppKernel; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; -use Symfony\Component\Config\ConfigCache; +use Symfony\Component\Config\ConfigCacheFactory; use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; @@ -47,15 +47,13 @@ public function testCacheIsFreshAfterCacheClearedWithWarmup() $metaFiles = $finder->files()->in($this->kernel->getCacheDir())->name('*.php.meta'); // simply check that cache is warmed up $this->assertGreaterThanOrEqual(1, count($metaFiles)); + $configCacheFactory = new ConfigCacheFactory(true); + $that = $this; + foreach ($metaFiles as $file) { - $configCache = new ConfigCache(substr($file, 0, -5), true); - $this->assertTrue( - $configCache->isFresh(), - sprintf( - 'Meta file "%s" is not fresh', - (string) $file - ) - ); + $configCacheFactory->cache(substr($file, 0, -5), function () use ($that, $file) { + $that->fail(sprintf('Meta file "%s" is not fresh', (string) $file)); + }); } // check that app kernel file present in meta file of container's cache diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ConfigCachePassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ConfigCachePassTest.php new file mode 100644 index 000000000000..c0eef3d627ce --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ConfigCachePassTest.php @@ -0,0 +1,68 @@ + + * + * 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 Symfony\Component\DependencyInjection\Reference; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ConfigCachePass; + +class ConfigCachePassTest extends \PHPUnit_Framework_TestCase +{ + public function testThatCheckersAreProcessedInPriorityOrder() + { + $services = array( + 'checker_2' => array(0 => array('priority' => 100)), + 'checker_1' => array(0 => array('priority' => 200)), + 'checker_3' => array(), + ); + + $definition = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + $container = $this->getMock( + 'Symfony\Component\DependencyInjection\ContainerBuilder', + array('findTaggedServiceIds', 'getDefinition', 'hasDefinition') + ); + + $container->expects($this->atLeastOnce()) + ->method('findTaggedServiceIds') + ->will($this->returnValue($services)); + $container->expects($this->atLeastOnce()) + ->method('getDefinition') + ->with('config_cache_factory') + ->will($this->returnValue($definition)); + + $definition->expects($this->once()) + ->method('replaceArgument') + ->with(0, array( + new Reference('checker_1'), + new Reference('checker_2'), + new Reference('checker_3'), + )); + + $pass = new ConfigCachePass(); + $pass->process($container); + } + + public function testThatCheckersCanBeMissing() + { + $definition = $this->getMock('Symfony\Component\DependencyInjection\Definition'); + $container = $this->getMock( + 'Symfony\Component\DependencyInjection\ContainerBuilder', + array('findTaggedServiceIds') + ); + + $container->expects($this->atLeastOnce()) + ->method('findTaggedServiceIds') + ->will($this->returnValue(array())); + + $pass = new ConfigCachePass(); + $pass->process($container); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 6e220740a510..ad0c427100dd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -250,7 +250,7 @@ public function testTranslator() ); $calls = $container->getDefinition('translator.default')->getMethodCalls(); - $this->assertEquals(array('fr'), $calls[0][1][0]); + $this->assertEquals(array('fr'), $calls[1][1][0]); } public function testTranslatorMultipleFallbacks() @@ -258,7 +258,7 @@ public function testTranslatorMultipleFallbacks() $container = $this->createContainerFromFile('translator_fallbacks'); $calls = $container->getDefinition('translator.default')->getMethodCalls(); - $this->assertEquals(array('en', 'fr'), $calls[0][1][0]); + $this->assertEquals(array('en', 'fr'), $calls[1][1][0]); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index c7b61cb88170..5bc45682cfd2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -104,34 +104,6 @@ public function testTransWithCachingWithInvalidLocale() $translator->trans('foo'); } - public function testLoadResourcesWithCaching() - { - $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); - $resourceFiles = array( - 'fr' => array( - __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', - ), - ); - - // prime the cache - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml'); - $translator->setLocale('fr'); - - $this->assertEquals('répertoire', $translator->trans('folder')); - - // do it another time as the cache is primed now - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), 'yml'); - $translator->setLocale('fr'); - - $this->assertEquals('répertoire', $translator->trans('folder')); - - // refresh cache when resources is changed in debug mode. - $translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'debug' => true), 'yml'); - $translator->setLocale('fr'); - - $this->assertEquals('folder', $translator->trans('folder')); - } - public function testLoadResourcesWithoutCaching() { $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index d191b010e72c..496351170b83 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -19,7 +19,7 @@ "php": ">=5.3.9", "symfony/asset": "~2.7|~3.0.0", "symfony/dependency-injection": "~2.8", - "symfony/config": "~2.4", + "symfony/config": "~2.8", "symfony/event-dispatcher": "~2.8|~3.0.0", "symfony/http-foundation": "~2.4.9|~2.5,>=2.5.4|~3.0.0", "symfony/http-kernel": "~2.8", diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md index 0c71e2cc1eb0..3bc22a57bda5 100644 --- a/src/Symfony/Component/Config/CHANGELOG.md +++ b/src/Symfony/Component/Config/CHANGELOG.md @@ -20,6 +20,9 @@ Before: `InvalidArgumentException` (variable must contain at least two distinct elements). After: the code will work as expected and it will restrict the values of the `variable` option to just `value`. + + * deprecated the `ResourceInterface::isFresh()` method. If you implement custom resource types and they + can be validated that way, make them implement the new `SelfCheckingResourceInterface`. 2.7.0 ----- diff --git a/src/Symfony/Component/Config/ConfigCache.php b/src/Symfony/Component/Config/ConfigCache.php index cc99bc9211cd..5ede07b55230 100644 --- a/src/Symfony/Component/Config/ConfigCache.php +++ b/src/Symfony/Component/Config/ConfigCache.php @@ -11,22 +11,27 @@ namespace Symfony\Component\Config; -use Symfony\Component\Config\Resource\ResourceInterface; -use Symfony\Component\Filesystem\Exception\IOException; -use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Config\Resource\BCResourceInterfaceChecker; +use Symfony\Component\Config\Resource\SelfCheckingResourceChecker; /** - * ConfigCache manages PHP cache files. + * ConfigCache caches arbitrary content in files on disk. * - * When debug is enabled, it knows when to flush the cache - * thanks to an array of ResourceInterface instances. + * When in debug mode, those metadata resources that implement + * \Symfony\Component\Config\Resource\SelfCheckingResourceInterface will + * be used to check cache freshness. + * + * During a transition period, also instances of + * \Symfony\Component\Config\Resource\ResourceInterface will be checked + * by means of the isFresh() method. This behaviour is deprecated since 2.8 + * and will be removed in 3.0. * * @author Fabien Potencier + * @author Matthias Pigulla */ -class ConfigCache implements ConfigCacheInterface +class ConfigCache extends ResourceCheckerConfigCache { private $debug; - private $file; /** * @param string $file The absolute cache path @@ -34,7 +39,10 @@ class ConfigCache implements ConfigCacheInterface */ public function __construct($file, $debug) { - $this->file = $file; + parent::__construct($file, array( + new SelfCheckingResourceChecker(), + new BCResourceInterfaceChecker(), + )); $this->debug = (bool) $debug; } @@ -49,90 +57,23 @@ public function __toString() { @trigger_error('ConfigCache::__toString() is deprecated since version 2.7 and will be removed in 3.0. Use the getPath() method instead.', E_USER_DEPRECATED); - return $this->file; - } - - /** - * Gets the cache file path. - * - * @return string The cache file path - */ - public function getPath() - { - return $this->file; + return $this->getPath(); } /** * Checks if the cache is still fresh. * - * This method always returns true when debug is off and the + * This implementation always returns true when debug is off and the * cache file exists. * * @return bool true if the cache is fresh, false otherwise */ public function isFresh() { - if (!is_file($this->file)) { - return false; - } - - if (!$this->debug) { + if (!$this->debug && is_file($this->getPath())) { return true; } - $metadata = $this->getMetaFile(); - if (!is_file($metadata)) { - return false; - } - - $time = filemtime($this->file); - $meta = unserialize(file_get_contents($metadata)); - foreach ($meta as $resource) { - if (!$resource->isFresh($time)) { - return false; - } - } - - return true; - } - - /** - * Writes cache. - * - * @param string $content The content to write in the cache - * @param ResourceInterface[] $metadata An array of ResourceInterface instances - * - * @throws \RuntimeException When cache file can't be written - */ - public function write($content, array $metadata = null) - { - $mode = 0666; - $umask = umask(); - $filesystem = new Filesystem(); - $filesystem->dumpFile($this->file, $content, null); - try { - $filesystem->chmod($this->file, $mode, $umask); - } catch (IOException $e) { - // discard chmod failure (some filesystem may not support it) - } - - if (null !== $metadata && true === $this->debug) { - $filesystem->dumpFile($this->getMetaFile(), serialize($metadata), null); - try { - $filesystem->chmod($this->getMetaFile(), $mode, $umask); - } catch (IOException $e) { - // discard chmod failure (some filesystem may not support it) - } - } - } - - /** - * Gets the meta file path. - * - * @return string The meta file path - */ - private function getMetaFile() - { - return $this->file.'.meta'; + return parent::isFresh(); } } diff --git a/src/Symfony/Component/Config/ConfigCacheFactory.php b/src/Symfony/Component/Config/ConfigCacheFactory.php index 5a8f4562388b..396536e2d8ed 100644 --- a/src/Symfony/Component/Config/ConfigCacheFactory.php +++ b/src/Symfony/Component/Config/ConfigCacheFactory.php @@ -12,8 +12,11 @@ namespace Symfony\Component\Config; /** - * Basic implementation for ConfigCacheFactoryInterface - * that will simply create an instance of ConfigCache. + * Basic implementation of ConfigCacheFactoryInterface that + * creates an instance of the default ConfigCache. + * + * This factory and/or cache do not support cache validation + * by means of ResourceChecker instances (that is, service-based). * * @author Matthias Pigulla */ diff --git a/src/Symfony/Component/Config/Resource/BCResourceInterfaceChecker.php b/src/Symfony/Component/Config/Resource/BCResourceInterfaceChecker.php new file mode 100644 index 000000000000..631755e8fe92 --- /dev/null +++ b/src/Symfony/Component/Config/Resource/BCResourceInterfaceChecker.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * Resource checker for the ResourceInterface. Exists for BC. + * + * @author Matthias Pigulla + * + * @deprecated since 2.8, to be removed in 3.0. + */ +class BCResourceInterfaceChecker extends SelfCheckingResourceChecker +{ + public function supports(ResourceInterface $metadata) + { + /* As all resources must be instanceof ResourceInterface, + we support them all. */ + return true; + } + + public function isFresh(ResourceInterface $resource, $timestamp) + { + @trigger_error('Resource checking through ResourceInterface::isFresh() is deprecated since 2.8 and will be removed in 3.0', E_USER_DEPRECATED); + + return parent::isFresh($resource, $timestamp); // For now, $metadata features the isFresh() method, so off we go (quack quack) + } +} diff --git a/src/Symfony/Component/Config/Resource/DirectoryResource.php b/src/Symfony/Component/Config/Resource/DirectoryResource.php index 515fb5c42d6a..1921e5d37778 100644 --- a/src/Symfony/Component/Config/Resource/DirectoryResource.php +++ b/src/Symfony/Component/Config/Resource/DirectoryResource.php @@ -16,7 +16,7 @@ * * @author Fabien Potencier */ -class DirectoryResource implements ResourceInterface, \Serializable +class DirectoryResource implements SelfCheckingResourceInterface, \Serializable { private $resource; private $pattern; diff --git a/src/Symfony/Component/Config/Resource/FileExistenceResource.php b/src/Symfony/Component/Config/Resource/FileExistenceResource.php index 4e68e8ac105b..ba1584638186 100644 --- a/src/Symfony/Component/Config/Resource/FileExistenceResource.php +++ b/src/Symfony/Component/Config/Resource/FileExistenceResource.php @@ -19,7 +19,7 @@ * * @author Charles-Henri Bruyand */ -class FileExistenceResource implements ResourceInterface, \Serializable +class FileExistenceResource implements SelfCheckingResourceInterface, \Serializable { private $resource; diff --git a/src/Symfony/Component/Config/Resource/FileResource.php b/src/Symfony/Component/Config/Resource/FileResource.php index 4c00ae4140ab..bd0ce03eafe8 100644 --- a/src/Symfony/Component/Config/Resource/FileResource.php +++ b/src/Symfony/Component/Config/Resource/FileResource.php @@ -18,7 +18,7 @@ * * @author Fabien Potencier */ -class FileResource implements ResourceInterface, \Serializable +class FileResource implements SelfCheckingResourceInterface, \Serializable { /** * @var string|false diff --git a/src/Symfony/Component/Config/Resource/ResourceInterface.php b/src/Symfony/Component/Config/Resource/ResourceInterface.php index db03d127a401..c0f0e50e0688 100644 --- a/src/Symfony/Component/Config/Resource/ResourceInterface.php +++ b/src/Symfony/Component/Config/Resource/ResourceInterface.php @@ -21,7 +21,13 @@ interface ResourceInterface /** * Returns a string representation of the Resource. * - * @return string A string representation of the Resource + * This method is necessary to allow for resource de-duplication, for example by means + * of array_unique(). The string returned need not have a particular meaning, but has + * to be identical for different ResourceInterface instances referring to the same + * resource; and it should be unlikely to collide with that of other, unrelated + * resource instances. + * + * @return string A string representation unique to the underlying Resource */ public function __toString(); @@ -31,6 +37,9 @@ public function __toString(); * @param int $timestamp The last time the resource was loaded * * @return bool True if the resource has not been updated, false otherwise + * + * @deprecated since 2.8, to be removed in 3.0. If your resource can check itself for + * freshness implement the SelfCheckingResourceInterface instead. */ public function isFresh($timestamp); diff --git a/src/Symfony/Component/Config/Resource/SelfCheckingResourceChecker.php b/src/Symfony/Component/Config/Resource/SelfCheckingResourceChecker.php new file mode 100644 index 000000000000..d72203bc1a42 --- /dev/null +++ b/src/Symfony/Component/Config/Resource/SelfCheckingResourceChecker.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +use Symfony\Component\Config\ResourceCheckerInterface; + +/** + * Resource checker for instances of SelfCheckingResourceInterface. + * + * As these resources perform the actual check themselves, we can provide + * this class as a standard way of validating them. + * + * @author Matthias Pigulla + */ +class SelfCheckingResourceChecker implements ResourceCheckerInterface +{ + public function supports(ResourceInterface $metadata) + { + return $metadata instanceof SelfCheckingResourceInterface; + } + + public function isFresh(ResourceInterface $resource, $timestamp) + { + /* @var SelfCheckingResourceInterface $resource */ + return $resource->isFresh($timestamp); + } +} diff --git a/src/Symfony/Component/Config/Resource/SelfCheckingResourceInterface.php b/src/Symfony/Component/Config/Resource/SelfCheckingResourceInterface.php new file mode 100644 index 000000000000..b3260f2be3e5 --- /dev/null +++ b/src/Symfony/Component/Config/Resource/SelfCheckingResourceInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Resource; + +/** + * Interface for Resources that can check for freshness autonomously, + * without special support from external services. + * + * @author Matthias Pigulla + */ +interface SelfCheckingResourceInterface extends ResourceInterface +{ + /** + * Returns true if the resource has not been updated since the given timestamp. + * + * @param int $timestamp The last time the resource was loaded + * + * @return bool True if the resource has not been updated, false otherwise + */ + public function isFresh($timestamp); +} diff --git a/src/Symfony/Component/Config/ResourceCheckerConfigCache.php b/src/Symfony/Component/Config/ResourceCheckerConfigCache.php new file mode 100644 index 000000000000..3cef7819071b --- /dev/null +++ b/src/Symfony/Component/Config/ResourceCheckerConfigCache.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; + +/** + * ResourceCheckerConfigCache uses instances of ResourceCheckerInterface + * to check whether cached data is still fresh. + * + * @author Matthias Pigulla + */ +class ResourceCheckerConfigCache implements ConfigCacheInterface +{ + /** + * @var string + */ + private $file; + + /** + * @var ResourceCheckerInterface[] + */ + private $resourceCheckers; + + /** + * @param string $file The absolute cache path + * @param ResourceCheckerInterface[] $resourceCheckers The ResourceCheckers to use for the freshness check + */ + public function __construct($file, array $resourceCheckers = array()) + { + $this->file = $file; + $this->resourceCheckers = $resourceCheckers; + } + + /** + * {@inheritdoc} + */ + public function getPath() + { + return $this->file; + } + + /** + * Checks if the cache is still fresh. + * + * This implementation will make a decision solely based on the ResourceCheckers + * passed in the constructor. + * + * The first ResourceChecker that supports a given resource is considered authoritative. + * Resources with no matching ResourceChecker will silently be ignored and considered fresh. + * + * @return bool true if the cache is fresh, false otherwise + */ + public function isFresh() + { + if (!is_file($this->file)) { + return false; + } + + if (!$this->resourceCheckers) { + return true; // shortcut - if we don't have any checkers we don't need to bother with the meta file at all + } + + $metadata = $this->getMetaFile(); + if (!is_file($metadata)) { + return true; + } + + $time = filemtime($this->file); + $meta = unserialize(file_get_contents($metadata)); + + foreach ($meta as $resource) { + /* @var ResourceInterface $resource */ + foreach ($this->resourceCheckers as $checker) { + if (!$checker->supports($resource)) { + continue; // next checker + } + if ($checker->isFresh($resource, $time)) { + break; // no need to further check this resource + } + + return false; // cache is stale + } + // no suitable checker found, ignore this resource + } + + return true; + } + + /** + * Writes cache. + * + * @param string $content The content to write in the cache + * @param ResourceInterface[] $metadata An array of metadata + * + * @throws \RuntimeException When cache file can't be written + */ + public function write($content, array $metadata = null) + { + $mode = 0666; + $umask = umask(); + $filesystem = new Filesystem(); + $filesystem->dumpFile($this->file, $content, null); + try { + $filesystem->chmod($this->file, $mode, $umask); + } catch (IOException $e) { + // discard chmod failure (some filesystem may not support it) + } + + if (null !== $metadata) { + $filesystem->dumpFile($this->getMetaFile(), serialize($metadata), null); + try { + $filesystem->chmod($this->getMetaFile(), $mode, $umask); + } catch (IOException $e) { + // discard chmod failure (some filesystem may not support it) + } + } + } + + /** + * Gets the meta file path. + * + * @return string The meta file path + */ + private function getMetaFile() + { + return $this->file.'.meta'; + } +} diff --git a/src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php b/src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php new file mode 100644 index 000000000000..61d732cc1c45 --- /dev/null +++ b/src/Symfony/Component/Config/ResourceCheckerConfigCacheFactory.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +/** + * A ConfigCacheFactory implementation that validates the + * cache with an arbitrary set of ResourceCheckers. + * + * @author Matthias Pigulla + */ +class ResourceCheckerConfigCacheFactory implements ConfigCacheFactoryInterface +{ + /** + * @var ResourceCheckerInterface[] + */ + private $resourceCheckers = array(); + + /** + * @param ResourceCheckerInterface[] $resourceCheckers + */ + public function __construct(array $resourceCheckers = array()) + { + $this->resourceCheckers = $resourceCheckers; + } + + /** + * {@inheritdoc} + */ + public function cache($file, $callback) + { + if (!is_callable($callback)) { + throw new \InvalidArgumentException(sprintf('Invalid type for callback argument. Expected callable, but got "%s".', gettype($callback))); + } + + $cache = new ResourceCheckerConfigCache($file, $this->resourceCheckers); + if (!$cache->isFresh()) { + call_user_func($callback, $cache); + } + + return $cache; + } +} diff --git a/src/Symfony/Component/Config/ResourceCheckerInterface.php b/src/Symfony/Component/Config/ResourceCheckerInterface.php new file mode 100644 index 000000000000..aaa5c5065e30 --- /dev/null +++ b/src/Symfony/Component/Config/ResourceCheckerInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +use Symfony\Component\Config\Resource\ResourceInterface; + +/** + * Interface for ResourceCheckers. + * + * When a ResourceCheckerConfigCache instance is checked for freshness, all its associated + * metadata resources are passed to ResourceCheckers. The ResourceCheckers + * can then inspect the resources and decide whether the cache can be considered + * fresh or not. + * + * @author Matthias Pigulla + * @author Benjamin Klotz + */ +interface ResourceCheckerInterface +{ + /** + * Queries the ResourceChecker whether it can validate a given + * resource or not. + * + * @param ResourceInterface $metadata The resource to be checked for freshness + * + * @return bool True if the ResourceChecker can handle this resource type, false if not + */ + public function supports(ResourceInterface $metadata); + + /** + * Validates the resource. + * + * @param ResourceInterface $resource The resource to be validated. + * @param int $timestamp The timestamp at which the cache associated with this resource was created. + * + * @return bool True if the resource has not changed since the given timestamp, false otherwise. + */ + public function isFresh(ResourceInterface $resource, $timestamp); + +} diff --git a/src/Symfony/Component/Config/Tests/ConfigCacheTest.php b/src/Symfony/Component/Config/Tests/ConfigCacheTest.php index f3f2a446a2bf..faa37f0e9db0 100644 --- a/src/Symfony/Component/Config/Tests/ConfigCacheTest.php +++ b/src/Symfony/Component/Config/Tests/ConfigCacheTest.php @@ -12,29 +12,20 @@ namespace Symfony\Component\Config\Tests; use Symfony\Component\Config\ConfigCache; -use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Tests\Resource\ResourceStub; class ConfigCacheTest extends \PHPUnit_Framework_TestCase { - private $resourceFile = null; - private $cacheFile = null; - private $metaFile = null; - protected function setUp() { - $this->resourceFile = tempnam(sys_get_temp_dir(), '_resource'); $this->cacheFile = tempnam(sys_get_temp_dir(), 'config_'); - $this->metaFile = $this->cacheFile.'.meta'; - - $this->makeCacheFresh(); - $this->generateMetaFile(); } protected function tearDown() { - $files = array($this->cacheFile, $this->metaFile, $this->resourceFile); + $files = array($this->cacheFile, $this->cacheFile.'.meta'); foreach ($files as $file) { if (file_exists($file)) { @@ -43,96 +34,65 @@ protected function tearDown() } } - public function testGetPath() + /** + * @dataProvider debugModes + */ + public function testCacheIsNotValidIfNothingHasBeenCached($debug) { - $cache = new ConfigCache($this->cacheFile, true); - - $this->assertSame($this->cacheFile, $cache->getPath()); - } - - public function testCacheIsNotFreshIfFileDoesNotExist() - { - unlink($this->cacheFile); - - $cache = new ConfigCache($this->cacheFile, false); + unlink($this->cacheFile); // remove tempnam() side effect + $cache = new ConfigCache($this->cacheFile, $debug); $this->assertFalse($cache->isFresh()); } - public function testCacheIsAlwaysFreshIfFileExistsWithDebugDisabled() + public function testIsAlwaysFreshInProduction() { - $this->makeCacheStale(); + $staleResource = new ResourceStub(); + $staleResource->setFresh(false); $cache = new ConfigCache($this->cacheFile, false); + $cache->write('', array($staleResource)); $this->assertTrue($cache->isFresh()); } - public function testCacheIsNotFreshWithoutMetaFile() + /** + * @dataProvider debugModes + */ + public function testIsFreshWhenNoResourceProvided($debug) { - unlink($this->metaFile); - - $cache = new ConfigCache($this->cacheFile, true); - - $this->assertFalse($cache->isFresh()); - } - - public function testCacheIsFreshIfResourceIsFresh() - { - $cache = new ConfigCache($this->cacheFile, true); - + $cache = new ConfigCache($this->cacheFile, $debug); + $cache->write('', array()); $this->assertTrue($cache->isFresh()); } - public function testCacheIsNotFreshIfOneOfTheResourcesIsNotFresh() + public function testFreshResourceInDebug() { - $this->makeCacheStale(); + $freshResource = new ResourceStub(); + $freshResource->setFresh(true); $cache = new ConfigCache($this->cacheFile, true); + $cache->write('', array($freshResource)); - $this->assertFalse($cache->isFresh()); - } - - public function testWriteDumpsFile() - { - unlink($this->cacheFile); - unlink($this->metaFile); - - $cache = new ConfigCache($this->cacheFile, false); - $cache->write('FOOBAR'); - - $this->assertFileExists($this->cacheFile, 'Cache file is created'); - $this->assertSame('FOOBAR', file_get_contents($this->cacheFile)); - $this->assertFileNotExists($this->metaFile, 'Meta file is not created'); + $this->assertTrue($cache->isFresh()); } - public function testWriteDumpsMetaFileWithDebugEnabled() + public function testStaleResourceInDebug() { - unlink($this->cacheFile); - unlink($this->metaFile); - - $metadata = array(new FileResource($this->resourceFile)); + $staleResource = new ResourceStub(); + $staleResource->setFresh(false); $cache = new ConfigCache($this->cacheFile, true); - $cache->write('FOOBAR', $metadata); - - $this->assertFileExists($this->cacheFile, 'Cache file is created'); - $this->assertFileExists($this->metaFile, 'Meta file is created'); - $this->assertSame(serialize($metadata), file_get_contents($this->metaFile)); - } - - private function makeCacheFresh() - { - touch($this->resourceFile, filemtime($this->cacheFile) - 3600); - } + $cache->write('', array($staleResource)); - private function makeCacheStale() - { - touch($this->cacheFile, filemtime($this->resourceFile) - 3600); + $this->assertFalse($cache->isFresh()); } - private function generateMetaFile() + public function debugModes() { - file_put_contents($this->metaFile, serialize(array(new FileResource($this->resourceFile)))); + return array( + array(true), + array(false), + ); } } diff --git a/src/Symfony/Component/Config/Tests/Resource/ResourceStub.php b/src/Symfony/Component/Config/Tests/Resource/ResourceStub.php new file mode 100644 index 000000000000..78799d7b9196 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/Resource/ResourceStub.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests\Resource; + +use Symfony\Component\Config\Resource\SelfCheckingResourceInterface; + +class ResourceStub implements SelfCheckingResourceInterface +{ + private $fresh = true; + + public function setFresh($isFresh) + { + $this->fresh = $isFresh; + } + + public function __toString() + { + return 'stub'; + } + + public function isFresh($timestamp) + { + return $this->fresh; + } + + public function getResource() + { + return 'stub'; + } +} diff --git a/src/Symfony/Component/Config/Tests/ResourceCheckerConfigCacheTest.php b/src/Symfony/Component/Config/Tests/ResourceCheckerConfigCacheTest.php new file mode 100644 index 000000000000..6a915ffed724 --- /dev/null +++ b/src/Symfony/Component/Config/Tests/ResourceCheckerConfigCacheTest.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config\Tests; + +use Symfony\Component\Config\Tests\Resource\ResourceStub; +use Symfony\Component\Config\ResourceCheckerConfigCache; + +class ResourceCheckerConfigCacheTest extends \PHPUnit_Framework_TestCase +{ + private $cacheFile = null; + + protected function setUp() + { + $this->cacheFile = tempnam(sys_get_temp_dir(), 'config_'); + } + + protected function tearDown() + { + $files = array($this->cacheFile, "{$this->cacheFile}.meta"); + + foreach ($files as $file) { + if (file_exists($file)) { + unlink($file); + } + } + } + + public function testGetPath() + { + $cache = new ResourceCheckerConfigCache($this->cacheFile); + + $this->assertSame($this->cacheFile, $cache->getPath()); + } + + public function testCacheIsNotFreshIfEmpty() + { + $checker = $this->getMock('\Symfony\Component\Config\ResourceCheckerInterface') + ->expects($this->never())->method('supports'); + + /* If there is nothing in the cache, it needs to be filled (and thus it's not fresh). + It does not matter if you provide checkers or not. */ + + unlink($this->cacheFile); // remove tempnam() side effect + $cache = new ResourceCheckerConfigCache($this->cacheFile, array($checker)); + + $this->assertFalse($cache->isFresh()); + } + + public function testCacheIsFreshIfNocheckerProvided() + { + /* For example in prod mode, you may choose not to run any checkers + at all. In that case, the cache should always be considered fresh. */ + $cache = new ResourceCheckerConfigCache($this->cacheFile); + $this->assertTrue($cache->isFresh()); + } + + public function testResourcesWithoutcheckersAreIgnoredAndConsideredFresh() + { + /* As in the previous test, but this time we have a resource. */ + $cache = new ResourceCheckerConfigCache($this->cacheFile); + $cache->write('', array(new ResourceStub())); + + $this->assertTrue($cache->isFresh()); // no (matching) ResourceChecker passed + } + + public function testIsFreshWithchecker() + { + $checker = $this->getMock('\Symfony\Component\Config\ResourceCheckerInterface'); + + $checker->expects($this->once()) + ->method('supports') + ->willReturn(true); + + $checker->expects($this->once()) + ->method('isFresh') + ->willReturn(true); + + $cache = new ResourceCheckerConfigCache($this->cacheFile, array($checker)); + $cache->write('', array(new ResourceStub())); + + $this->assertTrue($cache->isFresh()); + } + + public function testIsNotFreshWithchecker() + { + $checker = $this->getMock('\Symfony\Component\Config\ResourceCheckerInterface'); + + $checker->expects($this->once()) + ->method('supports') + ->willReturn(true); + + $checker->expects($this->once()) + ->method('isFresh') + ->willReturn(false); + + $cache = new ResourceCheckerConfigCache($this->cacheFile, array($checker)); + $cache->write('', array(new ResourceStub())); + + $this->assertFalse($cache->isFresh()); + } + + public function testCacheKeepsContent() + { + $cache = new ResourceCheckerConfigCache($this->cacheFile); + $cache->write('FOOBAR'); + + $this->assertSame('FOOBAR', file_get_contents($cache->getPath())); + } +} diff --git a/src/Symfony/Component/HttpKernel/Config/EnvParametersResource.php b/src/Symfony/Component/HttpKernel/Config/EnvParametersResource.php index 5f54450137a7..b4178a50ee3e 100644 --- a/src/Symfony/Component/HttpKernel/Config/EnvParametersResource.php +++ b/src/Symfony/Component/HttpKernel/Config/EnvParametersResource.php @@ -11,14 +11,14 @@ namespace Symfony\Component\HttpKernel\Config; -use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\Resource\SelfCheckingResourceInterface; /** * EnvParametersResource represents resources stored in prefixed environment variables. * * @author Chris Wilkinson */ -class EnvParametersResource implements ResourceInterface, \Serializable +class EnvParametersResource implements SelfCheckingResourceInterface, \Serializable { /** * @var string diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index d702fac80863..27fd41f6a0c8 100644 --- a/src/Symfony/Component/HttpKernel/composer.json +++ b/src/Symfony/Component/HttpKernel/composer.json @@ -26,7 +26,7 @@ "symfony/phpunit-bridge": "~2.7|~3.0.0", "symfony/browser-kit": "~2.3|~3.0.0", "symfony/class-loader": "~2.1|~3.0.0", - "symfony/config": "~2.7", + "symfony/config": "~2.8", "symfony/console": "~2.3|~3.0.0", "symfony/css-selector": "~2.0,>=2.0.5|~3.0.0", "symfony/dependency-injection": "~2.8|~3.0.0", diff --git a/src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php b/src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php index abe364c7368c..75093580b472 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php @@ -11,7 +11,7 @@ namespace Symfony\Component\Translation\Tests; -use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Config\Resource\SelfCheckingResourceInterface; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Translator; @@ -228,7 +228,7 @@ public function testPrimaryAndFallbackCataloguesContainTheSameMessagesRegardless public function testRefreshCacheWhenResourcesAreNoLongerFresh() { - $resource = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface'); + $resource = $this->getMock('Symfony\Component\Config\Resource\SelfCheckingResourceInterface'); $loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface'); $resource->method('isFresh')->will($this->returnValue(false)); $loader @@ -281,7 +281,7 @@ private function createFailingLoader() } } -class StaleResource implements ResourceInterface +class StaleResource implements SelfCheckingResourceInterface { public function isFresh($timestamp) { diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 1c725d43d18a..ddef54cd9192 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "symfony/phpunit-bridge": "~2.7|~3.0.0", - "symfony/config": "~2.7", + "symfony/config": "~2.8", "symfony/intl": "~2.4|~3.0.0", "symfony/yaml": "~2.2|~3.0.0", "psr/log": "~1.0"