From 6fbe9b10640034069fb1430b29315ecaec6da82e Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Thu, 2 Apr 2015 15:33:59 +0200 Subject: [PATCH] [Config] Delegate creation of ConfigCache instances to a factory. --- UPGRADE-2.7.md | 13 ++ src/Symfony/Component/Config/CHANGELOG.md | 6 + src/Symfony/Component/Config/ConfigCache.php | 17 ++- .../Component/Config/ConfigCacheFactory.php | 48 +++++++ .../Config/ConfigCacheFactoryInterface.php | 32 +++++ .../Component/Config/ConfigCacheInterface.php | 49 +++++++ .../Config/Tests/ConfigCacheTest.php | 2 +- src/Symfony/Component/HttpKernel/Kernel.php | 6 +- .../Component/HttpKernel/composer.json | 5 +- src/Symfony/Component/Routing/Router.php | 102 ++++++++++---- src/Symfony/Component/Routing/composer.json | 5 +- .../Component/Translation/Translator.php | 129 +++++++++++++----- .../Component/Translation/composer.json | 5 +- 13 files changed, 347 insertions(+), 72 deletions(-) create mode 100644 src/Symfony/Component/Config/ConfigCacheFactory.php create mode 100644 src/Symfony/Component/Config/ConfigCacheFactoryInterface.php create mode 100644 src/Symfony/Component/Config/ConfigCacheInterface.php diff --git a/UPGRADE-2.7.md b/UPGRADE-2.7.md index 160f1a573c81..f491c7d471ba 100644 --- a/UPGRADE-2.7.md +++ b/UPGRADE-2.7.md @@ -16,6 +16,12 @@ Router but in 2.7 you would get an error if `bar` parameter doesn't exist or unexpected result otherwise. + * The `getMatcherDumperInstance()` and `getGeneratorDumperInstance()` methods in the + `Symfony\Component\Routing\Router` have been changed from `protected` to `public`. + If you override these methods in a subclass, you will need to change your + methods to `public` as well. Note however that this is a temporary change needed for + PHP 5.3 compatibility only. It will be reverted in Symfony 3.0. + Form ---- @@ -515,3 +521,10 @@ PropertyAccess new UnexpectedTypeException($value, $path, $pathIndex); ``` + +Config +------ + + * The `__toString()` method of the `\Symfony\Component\Config\ConfigCache` is marked as + deprecated in favor of the new `getPath()` method. + diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md index 59b30a3a7a6d..e1b19e6cb6f9 100644 --- a/src/Symfony/Component/Config/CHANGELOG.md +++ b/src/Symfony/Component/Config/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +2.7.0 +----- + + * added `ConfigCacheInterface`, `ConfigCacheFactoryInterface` and a basic `ConfigCacheFactory` + implementation to delegate creation of ConfigCache instances + 2.2.0 ----- diff --git a/src/Symfony/Component/Config/ConfigCache.php b/src/Symfony/Component/Config/ConfigCache.php index ebf107f74f35..d851d0b37a7f 100644 --- a/src/Symfony/Component/Config/ConfigCache.php +++ b/src/Symfony/Component/Config/ConfigCache.php @@ -23,14 +23,12 @@ * * @author Fabien Potencier */ -class ConfigCache +class ConfigCache implements ConfigCacheInterface { private $debug; private $file; /** - * Constructor. - * * @param string $file The absolute cache path * @param bool $debug Whether debugging is enabled or not */ @@ -44,8 +42,21 @@ public function __construct($file, $debug) * Gets the cache file path. * * @return string The cache file path + * @deprecated since 2.7, to be removed in 3.0. Use getPath() instead. */ 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; } diff --git a/src/Symfony/Component/Config/ConfigCacheFactory.php b/src/Symfony/Component/Config/ConfigCacheFactory.php new file mode 100644 index 000000000000..3c76087a6492 --- /dev/null +++ b/src/Symfony/Component/Config/ConfigCacheFactory.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +/** + * Basic implementation for ConfigCacheFactoryInterface + * that will simply create an instance of ConfigCache. + * + * @author Matthias Pigulla + */ +class ConfigCacheFactory implements ConfigCacheFactoryInterface +{ + /** + * @var bool Debug flag passed to the ConfigCache + */ + private $debug; + + /** + * @param bool $debug The debug flag to pass to ConfigCache + */ + public function __construct($debug) + { + $this->debug = $debug; + } + + /** + * {@inheritdoc} + */ + public function cache($file, $callback) + { + $cache = new ConfigCache($file, $this->debug); + + if (!$cache->isFresh()) { + call_user_func($callback, $cache); + } + + return $cache; + } +} diff --git a/src/Symfony/Component/Config/ConfigCacheFactoryInterface.php b/src/Symfony/Component/Config/ConfigCacheFactoryInterface.php new file mode 100644 index 000000000000..750d2cc7899d --- /dev/null +++ b/src/Symfony/Component/Config/ConfigCacheFactoryInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Config; + +/** + * Interface for a ConfigCache factory. This factory creates + * an instance of ConfigCacheInterface and initializes the + * cache if necessary. + * + * @author Matthias Pigulla + */ +interface ConfigCacheFactoryInterface +{ + /** + * Creates a cache instance and (re-)initializes it if necessary. + * + * @param string $file The absolute cache file path + * @param callable $callable The callable to be executed when the cache needs to be filled (i. e. is not fresh). The cache will be passed as the only parameter to this callback + * + * @return ConfigCacheInterface $configCache The cache instance + */ + public function cache($file, $callable); +} diff --git a/src/Symfony/Component/Config/ConfigCacheInterface.php b/src/Symfony/Component/Config/ConfigCacheInterface.php new file mode 100644 index 000000000000..067e2a133007 --- /dev/null +++ b/src/Symfony/Component/Config/ConfigCacheInterface.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 ConfigCache + * + * @author Matthias Pigulla + */ +interface ConfigCacheInterface +{ + /** + * Gets the cache file path. + * + * @return string The cache file path + */ + public function getPath(); + + /** + * Checks if the cache is still fresh. + * + * This check should take the metadata passed to the write() method into consideration. + * + * @return bool Whether the cache is still fresh. + */ + public function isFresh(); + + /** + * Writes the given content into the cache file. Metadata will be stored + * independently and can be used to check cache freshness at a later time. + * + * @param string $content The content to write into the cache + * @param ResourceInterface[]|null $metadata An array of ResourceInterface instances + * + * @throws \RuntimeException When the cache file cannot be written + */ + public function write($content, array $metadata = null); +} diff --git a/src/Symfony/Component/Config/Tests/ConfigCacheTest.php b/src/Symfony/Component/Config/Tests/ConfigCacheTest.php index 8271885775fa..cd83c1e52621 100644 --- a/src/Symfony/Component/Config/Tests/ConfigCacheTest.php +++ b/src/Symfony/Component/Config/Tests/ConfigCacheTest.php @@ -47,7 +47,7 @@ public function testToString() { $cache = new ConfigCache($this->cacheFile, true); - $this->assertSame($this->cacheFile, (string) $cache); + $this->assertSame($this->cacheFile, $cache->getPath()); } public function testCacheIsNotFreshIfFileDoesNotExist() diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 1dcb886a027b..fe7657ff27b1 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -252,7 +252,7 @@ public function getBundle($name, $first = true) } /** - * {@inheritDoc} + * {@inheritdoc} * * @throws \RuntimeException if a custom resource is hidden by a resource in a derived bundle */ @@ -546,7 +546,7 @@ protected function initializeContainer() $fresh = false; } - require_once $cache; + require_once $cache->getPath(); $this->container = new $class(); $this->container->set('kernel', $this); @@ -695,7 +695,7 @@ protected function dumpContainer(ConfigCache $cache, ContainerBuilder $container $dumper->setProxyDumper(new ProxyDumper()); } - $content = $dumper->dump(array('class' => $class, 'base_class' => $baseClass, 'file' => (string) $cache)); + $content = $dumper->dump(array('class' => $class, 'base_class' => $baseClass, 'file' => $cache->getPath())); if (!$this->debug) { $content = static::stripComments($content); } diff --git a/src/Symfony/Component/HttpKernel/composer.json b/src/Symfony/Component/HttpKernel/composer.json index 9921aa8f7c09..b90cd1e01dae 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.0,>=2.0.5|~3.0.0", + "symfony/config": "~2.7", "symfony/console": "~2.3|~3.0.0", "symfony/css-selector": "~2.0,>=2.0.5|~3.0.0", "symfony/dependency-injection": "~2.2|~3.0.0", @@ -40,6 +40,9 @@ "symfony/translation": "~2.0,>=2.0.5|~3.0.0", "symfony/var-dumper": "~2.6|~3.0.0" }, + "conflict": { + "symfony/config": "<2.7" + }, "suggest": { "symfony/browser-kit": "", "symfony/class-loader": "", diff --git a/src/Symfony/Component/Routing/Router.php b/src/Symfony/Component/Routing/Router.php index c7c926d43c41..e2651ef8b93a 100644 --- a/src/Symfony/Component/Routing/Router.php +++ b/src/Symfony/Component/Routing/Router.php @@ -12,7 +12,9 @@ namespace Symfony\Component\Routing; use Symfony\Component\Config\Loader\LoaderInterface; -use Symfony\Component\Config\ConfigCache; +use Symfony\Component\Config\ConfigCacheInterface; +use Symfony\Component\Config\ConfigCacheFactoryInterface; +use Symfony\Component\Config\ConfigCacheFactory; use Psr\Log\LoggerInterface; use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -71,6 +73,11 @@ class Router implements RouterInterface, RequestMatcherInterface */ protected $logger; + /** + * @var ConfigCacheFactoryInterface|null + */ + private $configCacheFactory; + /** * @var ExpressionFunctionProviderInterface[] */ @@ -209,6 +216,16 @@ public function getContext() return $this->context; } + /** + * Sets the ConfigCache factory to use. + * + * @param ConfigCacheFactoryInterface $configCacheFactory The factory to use. + */ + public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) + { + $this->configCacheFactory = $configCacheFactory; + } + /** * {@inheritdoc} */ @@ -262,24 +279,29 @@ public function getMatcher() } $class = $this->options['matcher_cache_class']; - $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']); - if (!$cache->isFresh()) { - $dumper = $this->getMatcherDumperInstance(); - if (method_exists($dumper, 'addExpressionLanguageProvider')) { - foreach ($this->expressionLanguageProviders as $provider) { - $dumper->addExpressionLanguageProvider($provider); + $baseClass = $this->options['matcher_base_class']; + $expressionLanguageProviders = $this->expressionLanguageProviders; + $that = $this; // required for PHP 5.3 where "$this" cannot be use()d in anonymous functions. Change in Symfony 3.0. + + $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$class.'.php', + function (ConfigCacheInterface $cache) use ($that, $class, $baseClass, $expressionLanguageProviders) { + $dumper = $that->getMatcherDumperInstance(); + if (method_exists($dumper, 'addExpressionLanguageProvider')) { + foreach ($expressionLanguageProviders as $provider) { + $dumper->addExpressionLanguageProvider($provider); + } } - } - $options = array( - 'class' => $class, - 'base_class' => $this->options['matcher_base_class'], - ); + $options = array( + 'class' => $class, + 'base_class' => $baseClass, + ); - $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources()); - } + $cache->write($dumper->dump($options), $that->getRouteCollection()->getResources()); + } + ); - require_once $cache; + require_once $cache->getPath(); return $this->matcher = new $class($this->context); } @@ -299,19 +321,22 @@ public function getGenerator() $this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->logger); } else { $class = $this->options['generator_cache_class']; - $cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']); - if (!$cache->isFresh()) { - $dumper = $this->getGeneratorDumperInstance(); - - $options = array( - 'class' => $class, - 'base_class' => $this->options['generator_base_class'], - ); - - $cache->write($dumper->dump($options), $this->getRouteCollection()->getResources()); - } + $baseClass = $this->options['generator_base_class']; + $that = $this; // required for PHP 5.3 where "$this" cannot be use()d in anonymous functions. Change in Symfony 3.0. + $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$class.'.php', + function (ConfigCacheInterface $cache) use ($that, $class, $baseClass) { + $dumper = $that->getGeneratorDumperInstance(); + + $options = array( + 'class' => $class, + 'base_class' => $baseClass, + ); + + $cache->write($dumper->dump($options), $that->getRouteCollection()->getResources()); + } + ); - require_once $cache; + require_once $cache->getPath(); $this->generator = new $class($this->context, $this->logger); } @@ -329,18 +354,37 @@ public function addExpressionLanguageProvider(ExpressionFunctionProviderInterfac } /** + * This method is public because it needs to be callable from a closure in PHP 5.3. It should be converted back to protected in 3.0. + * @internal * @return GeneratorDumperInterface */ - protected function getGeneratorDumperInstance() + public function getGeneratorDumperInstance() { return new $this->options['generator_dumper_class']($this->getRouteCollection()); } /** + * This method is public because it needs to be callable from a closure in PHP 5.3. It should be converted back to protected in 3.0. + * @internal * @return MatcherDumperInterface */ - protected function getMatcherDumperInstance() + public function getMatcherDumperInstance() { return new $this->options['matcher_dumper_class']($this->getRouteCollection()); } + + /** + * Provides the ConfigCache factory implementation, falling back to a + * default implementation if necessary. + * + * @return ConfigCacheFactoryInterface $configCacheFactory + */ + private function getConfigCacheFactory() + { + if (null === $this->configCacheFactory) { + $this->configCacheFactory = new ConfigCacheFactory($this->options['debug']); + } + + return $this->configCacheFactory; + } } diff --git a/src/Symfony/Component/Routing/composer.json b/src/Symfony/Component/Routing/composer.json index 252f2bc0e746..7f21b979ffa9 100644 --- a/src/Symfony/Component/Routing/composer.json +++ b/src/Symfony/Component/Routing/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "symfony/phpunit-bridge": "~2.7|~3.0.0", - "symfony/config": "~2.2|~3.0.0", + "symfony/config": "~2.7|~3.0.0", "symfony/http-foundation": "~2.3|~3.0.0", "symfony/yaml": "~2.0,>=2.0.5|~3.0.0", "symfony/expression-language": "~2.4|~3.0.0", @@ -28,6 +28,9 @@ "doctrine/common": "~2.2", "psr/log": "~1.0" }, + "conflict": { + "symfony/config": "<2.7" + }, "suggest": { "symfony/config": "For using the all-in-one router or any loader", "symfony/yaml": "For using the YAML loader", diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 001788479f5f..c0d3629b3624 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -13,7 +13,9 @@ use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Exception\NotFoundResourceException; -use Symfony\Component\Config\ConfigCache; +use Symfony\Component\Config\ConfigCacheInterface; +use Symfony\Component\Config\ConfigCacheFactoryInterface; +use Symfony\Component\Config\ConfigCacheFactory; /** * Translator. @@ -64,6 +66,11 @@ class Translator implements TranslatorInterface, TranslatorBagInterface */ private $debug; + /** + * @var ConfigCacheFactoryInterface|null + */ + private $configCacheFactory; + /** * Constructor. * @@ -84,6 +91,16 @@ public function __construct($locale, MessageSelector $selector = null, $cacheDir $this->debug = $debug; } + /** + * Sets the ConfigCache factory to use. + * + * @param ConfigCacheFactoryInterface $configCacheFactory + */ + public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) + { + $this->configCacheFactory = $configCacheFactory; + } + /** * Adds a Loader. * @@ -315,7 +332,7 @@ public function getMessages($locale = null) return $messages; } - /* + /** * @param string $locale */ protected function loadCatalogue($locale) @@ -346,48 +363,33 @@ protected function initializeCatalogue($locale) /** * @param string $locale - * @param bool $forceRefresh */ - private function initializeCacheCatalogue($locale, $forceRefresh = false) + private function initializeCacheCatalogue($locale) { if (isset($this->catalogues[$locale])) { + /* Catalogue already initialized. */ return; } $this->assertValidLocale($locale); - $cache = new ConfigCache($this->cacheDir.'/catalogue.'.$locale.'.php', $this->debug); - if ($forceRefresh || !$cache->isFresh()) { - $this->initializeCatalogue($locale); - $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]); - - $content = sprintf(<<getResourcesHash($locale), - $locale, - var_export($this->catalogues[$locale]->all(), true), - $fallbackContent - ); - - $cache->write($content, $this->catalogues[$locale]->getResources()); + $cacheFile = $this->cacheDir.'/catalogue.'.$locale.'.php'; + $self = $this; // required for PHP 5.3 where "$this" cannot be use()d in anonymous functions. Change in Symfony 3.0. + $cache = $this->getConfigCacheFactory()->cache($cacheFile, + function (ConfigCacheInterface $cache) use ($self, $locale) { + $self->dumpCatalogue($locale, $cache); + } + ); + if (isset($this->catalogues[$locale])) { + /* Catalogue has been initialized as it was written out to cache. */ return; } - $catalogue = include $cache; + /* Read catalogue from cache. */ + $catalogue = include $cache->getPath(); /* - * Old cache returns only the catalogue, without resourcesHash + * Gracefully handle the case when the cached catalogue is in an "old" format, without a resourcesHash */ $resourcesHash = null; if (is_array($catalogue)) { @@ -395,10 +397,56 @@ private function initializeCacheCatalogue($locale, $forceRefresh = false) } if ($this->debug && $resourcesHash !== $this->getResourcesHash($locale)) { - return $this->initializeCacheCatalogue($locale, true); + /* + * This approach of resource checking has the disadvantage that a second + * type of freshness check happens based on content *inside* the cache, while + * the idea of ConfigCache is to make this check transparent to the client (and keeps + * the resources in a .meta file). + * + * Thus, we might run into the unfortunate situation that we just thought (a few lines above) + * that the cache is fresh -- and now that we look into it, we figure it's not. + * + * For now, just unlink the cache and try again. See + * https://github.com/symfony/symfony/pull/11862#issuecomment-54634631 and/or + * https://github.com/symfony/symfony/issues/7176 for possible better approaches. + */ + unlink($cacheFile); + $this->initializeCacheCatalogue($locale); + } else { + /* Initialize with catalogue from cache. */ + $this->catalogues[$locale] = $catalogue; } + } + + /** + * This method is public because it needs to be callable from a closure in PHP 5.3. It should be made protected (or even private, if possible) in 3.0. + * @internal + */ + public function dumpCatalogue($locale, ConfigCacheInterface $cache) + { + $this->initializeCatalogue($locale); + $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]); + + $content = sprintf(<<catalogues[$locale] = $catalogue; +use Symfony\Component\Translation\MessageCatalogue; + +\$resourcesHash = '%s'; +\$catalogue = new MessageCatalogue('%s', %s); + +%s +return array(\$catalogue, \$resourcesHash); + +EOF + , + $this->getResourcesHash($locale), + $locale, + var_export($this->catalogues[$locale]->all(), true), + $fallbackContent + ); + + $cache->write($content, $this->catalogues[$locale]->getResources()); } private function getFallbackContent(MessageCatalogue $catalogue) @@ -514,4 +562,19 @@ protected function assertValidLocale($locale) throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale)); } } + + /** + * Provides the ConfigCache factory implementation, falling back to a + * default implementation if necessary. + * + * @return ConfigCacheFactoryInterface $configCacheFactory + */ + private function getConfigCacheFactory() + { + if (!$this->configCacheFactory) { + $this->configCacheFactory = new ConfigCacheFactory($this->debug); + } + + return $this->configCacheFactory; + } } diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 50f76efaeb01..6f8499900e7f 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -20,11 +20,14 @@ }, "require-dev": { "symfony/phpunit-bridge": "~2.7|~3.0.0", - "symfony/config": "~2.3,>=2.3.12|~3.0.0", + "symfony/config": "~2.7", "symfony/intl": "~2.3|~3.0.0", "symfony/yaml": "~2.2|~3.0.0", "psr/log": "~1.0" }, + "conflict": { + "symfony/config": "<2.7" + }, "suggest": { "symfony/config": "", "symfony/yaml": "",