[Config] Delegate creation of ConfigCache instances to a factory.

This commit is contained in:
Matthias Pigulla 2015-04-02 15:33:59 +02:00 committed by Fabien Potencier
parent aa82fb065d
commit 6fbe9b1064
13 changed files with 348 additions and 73 deletions

View File

@ -16,6 +16,12 @@ Router
but in 2.7 you would get an error if `bar` parameter but in 2.7 you would get an error if `bar` parameter
doesn't exist or unexpected result otherwise. 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 Form
---- ----
@ -515,3 +521,10 @@ PropertyAccess
new UnexpectedTypeException($value, $path, $pathIndex); 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.

View File

@ -1,6 +1,12 @@
CHANGELOG CHANGELOG
========= =========
2.7.0
-----
* added `ConfigCacheInterface`, `ConfigCacheFactoryInterface` and a basic `ConfigCacheFactory`
implementation to delegate creation of ConfigCache instances
2.2.0 2.2.0
----- -----

View File

@ -23,14 +23,12 @@ use Symfony\Component\Filesystem\Filesystem;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
*/ */
class ConfigCache class ConfigCache implements ConfigCacheInterface
{ {
private $debug; private $debug;
private $file; private $file;
/** /**
* Constructor.
*
* @param string $file The absolute cache path * @param string $file The absolute cache path
* @param bool $debug Whether debugging is enabled or not * @param bool $debug Whether debugging is enabled or not
*/ */
@ -44,8 +42,21 @@ class ConfigCache
* Gets the cache file path. * Gets the cache file path.
* *
* @return string 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() 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->file;
} }

View File

@ -0,0 +1,48 @@
<?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\Config;
/**
* Basic implementation for ConfigCacheFactoryInterface
* that will simply create an instance of ConfigCache.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
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;
}
}

View File

@ -0,0 +1,32 @@
<?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\Config;
/**
* Interface for a ConfigCache factory. This factory creates
* an instance of ConfigCacheInterface and initializes the
* cache if necessary.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
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);
}

View File

@ -0,0 +1,49 @@
<?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\Config;
use Symfony\Component\Config\Resource\ResourceInterface;
/**
* Interface for ConfigCache
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
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);
}

View File

@ -47,7 +47,7 @@ class ConfigCacheTest extends \PHPUnit_Framework_TestCase
{ {
$cache = new ConfigCache($this->cacheFile, true); $cache = new ConfigCache($this->cacheFile, true);
$this->assertSame($this->cacheFile, (string) $cache); $this->assertSame($this->cacheFile, $cache->getPath());
} }
public function testCacheIsNotFreshIfFileDoesNotExist() public function testCacheIsNotFreshIfFileDoesNotExist()

View File

@ -252,7 +252,7 @@ abstract class Kernel implements KernelInterface, TerminableInterface
} }
/** /**
* {@inheritDoc} * {@inheritdoc}
* *
* @throws \RuntimeException if a custom resource is hidden by a resource in a derived bundle * @throws \RuntimeException if a custom resource is hidden by a resource in a derived bundle
*/ */
@ -546,7 +546,7 @@ abstract class Kernel implements KernelInterface, TerminableInterface
$fresh = false; $fresh = false;
} }
require_once $cache; require_once $cache->getPath();
$this->container = new $class(); $this->container = new $class();
$this->container->set('kernel', $this); $this->container->set('kernel', $this);
@ -695,7 +695,7 @@ abstract class Kernel implements KernelInterface, TerminableInterface
$dumper->setProxyDumper(new ProxyDumper()); $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) { if (!$this->debug) {
$content = static::stripComments($content); $content = static::stripComments($content);
} }

View File

@ -26,7 +26,7 @@
"symfony/phpunit-bridge": "~2.7|~3.0.0", "symfony/phpunit-bridge": "~2.7|~3.0.0",
"symfony/browser-kit": "~2.3|~3.0.0", "symfony/browser-kit": "~2.3|~3.0.0",
"symfony/class-loader": "~2.1|~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/console": "~2.3|~3.0.0",
"symfony/css-selector": "~2.0,>=2.0.5|~3.0.0", "symfony/css-selector": "~2.0,>=2.0.5|~3.0.0",
"symfony/dependency-injection": "~2.2|~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/translation": "~2.0,>=2.0.5|~3.0.0",
"symfony/var-dumper": "~2.6|~3.0.0" "symfony/var-dumper": "~2.6|~3.0.0"
}, },
"conflict": {
"symfony/config": "<2.7"
},
"suggest": { "suggest": {
"symfony/browser-kit": "", "symfony/browser-kit": "",
"symfony/class-loader": "", "symfony/class-loader": "",

View File

@ -12,7 +12,9 @@
namespace Symfony\Component\Routing; namespace Symfony\Component\Routing;
use Symfony\Component\Config\Loader\LoaderInterface; 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 Psr\Log\LoggerInterface;
use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface; use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@ -71,6 +73,11 @@ class Router implements RouterInterface, RequestMatcherInterface
*/ */
protected $logger; protected $logger;
/**
* @var ConfigCacheFactoryInterface|null
*/
private $configCacheFactory;
/** /**
* @var ExpressionFunctionProviderInterface[] * @var ExpressionFunctionProviderInterface[]
*/ */
@ -209,6 +216,16 @@ class Router implements RouterInterface, RequestMatcherInterface
return $this->context; 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} * {@inheritdoc}
*/ */
@ -262,24 +279,29 @@ class Router implements RouterInterface, RequestMatcherInterface
} }
$class = $this->options['matcher_cache_class']; $class = $this->options['matcher_cache_class'];
$cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']); $baseClass = $this->options['matcher_base_class'];
if (!$cache->isFresh()) { $expressionLanguageProviders = $this->expressionLanguageProviders;
$dumper = $this->getMatcherDumperInstance(); $that = $this; // required for PHP 5.3 where "$this" cannot be use()d in anonymous functions. Change in Symfony 3.0.
if (method_exists($dumper, 'addExpressionLanguageProvider')) {
foreach ($this->expressionLanguageProviders as $provider) { $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$class.'.php',
$dumper->addExpressionLanguageProvider($provider); 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' => $baseClass,
);
$cache->write($dumper->dump($options), $that->getRouteCollection()->getResources());
} }
);
$options = array( require_once $cache->getPath();
'class' => $class,
'base_class' => $this->options['matcher_base_class'],
);
$cache->write($dumper->dump($options), $this->getRouteCollection()->getResources());
}
require_once $cache;
return $this->matcher = new $class($this->context); return $this->matcher = new $class($this->context);
} }
@ -299,19 +321,22 @@ class Router implements RouterInterface, RequestMatcherInterface
$this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->logger); $this->generator = new $this->options['generator_class']($this->getRouteCollection(), $this->context, $this->logger);
} else { } else {
$class = $this->options['generator_cache_class']; $class = $this->options['generator_cache_class'];
$cache = new ConfigCache($this->options['cache_dir'].'/'.$class.'.php', $this->options['debug']); $baseClass = $this->options['generator_base_class'];
if (!$cache->isFresh()) { $that = $this; // required for PHP 5.3 where "$this" cannot be use()d in anonymous functions. Change in Symfony 3.0.
$dumper = $this->getGeneratorDumperInstance(); $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/'.$class.'.php',
function (ConfigCacheInterface $cache) use ($that, $class, $baseClass) {
$dumper = $that->getGeneratorDumperInstance();
$options = array( $options = array(
'class' => $class, 'class' => $class,
'base_class' => $this->options['generator_base_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();
$this->generator = new $class($this->context, $this->logger); $this->generator = new $class($this->context, $this->logger);
} }
@ -329,18 +354,37 @@ class Router implements RouterInterface, RequestMatcherInterface
} }
/** /**
* 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 * @return GeneratorDumperInterface
*/ */
protected function getGeneratorDumperInstance() public function getGeneratorDumperInstance()
{ {
return new $this->options['generator_dumper_class']($this->getRouteCollection()); 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 * @return MatcherDumperInterface
*/ */
protected function getMatcherDumperInstance() public function getMatcherDumperInstance()
{ {
return new $this->options['matcher_dumper_class']($this->getRouteCollection()); 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;
}
} }

View File

@ -20,7 +20,7 @@
}, },
"require-dev": { "require-dev": {
"symfony/phpunit-bridge": "~2.7|~3.0.0", "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/http-foundation": "~2.3|~3.0.0",
"symfony/yaml": "~2.0,>=2.0.5|~3.0.0", "symfony/yaml": "~2.0,>=2.0.5|~3.0.0",
"symfony/expression-language": "~2.4|~3.0.0", "symfony/expression-language": "~2.4|~3.0.0",
@ -28,6 +28,9 @@
"doctrine/common": "~2.2", "doctrine/common": "~2.2",
"psr/log": "~1.0" "psr/log": "~1.0"
}, },
"conflict": {
"symfony/config": "<2.7"
},
"suggest": { "suggest": {
"symfony/config": "For using the all-in-one router or any loader", "symfony/config": "For using the all-in-one router or any loader",
"symfony/yaml": "For using the YAML loader", "symfony/yaml": "For using the YAML loader",

View File

@ -13,7 +13,9 @@ namespace Symfony\Component\Translation;
use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\Exception\NotFoundResourceException; 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. * Translator.
@ -64,6 +66,11 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
*/ */
private $debug; private $debug;
/**
* @var ConfigCacheFactoryInterface|null
*/
private $configCacheFactory;
/** /**
* Constructor. * Constructor.
* *
@ -84,6 +91,16 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
$this->debug = $debug; $this->debug = $debug;
} }
/**
* Sets the ConfigCache factory to use.
*
* @param ConfigCacheFactoryInterface $configCacheFactory
*/
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
{
$this->configCacheFactory = $configCacheFactory;
}
/** /**
* Adds a Loader. * Adds a Loader.
* *
@ -315,7 +332,7 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
return $messages; return $messages;
} }
/* /**
* @param string $locale * @param string $locale
*/ */
protected function loadCatalogue($locale) protected function loadCatalogue($locale)
@ -346,21 +363,71 @@ class Translator implements TranslatorInterface, TranslatorBagInterface
/** /**
* @param string $locale * @param string $locale
* @param bool $forceRefresh
*/ */
private function initializeCacheCatalogue($locale, $forceRefresh = false) private function initializeCacheCatalogue($locale)
{ {
if (isset($this->catalogues[$locale])) { if (isset($this->catalogues[$locale])) {
/* Catalogue already initialized. */
return; return;
} }
$this->assertValidLocale($locale); $this->assertValidLocale($locale);
$cache = new ConfigCache($this->cacheDir.'/catalogue.'.$locale.'.php', $this->debug); $cacheFile = $this->cacheDir.'/catalogue.'.$locale.'.php';
if ($forceRefresh || !$cache->isFresh()) { $self = $this; // required for PHP 5.3 where "$this" cannot be use()d in anonymous functions. Change in Symfony 3.0.
$this->initializeCatalogue($locale); $cache = $this->getConfigCacheFactory()->cache($cacheFile,
$fallbackContent = $this->getFallbackContent($this->catalogues[$locale]); function (ConfigCacheInterface $cache) use ($self, $locale) {
$self->dumpCatalogue($locale, $cache);
}
);
$content = sprintf(<<<EOF if (isset($this->catalogues[$locale])) {
/* Catalogue has been initialized as it was written out to cache. */
return;
}
/* Read catalogue from cache. */
$catalogue = include $cache->getPath();
/*
* Gracefully handle the case when the cached catalogue is in an "old" format, without a resourcesHash
*/
$resourcesHash = null;
if (is_array($catalogue)) {
list($catalogue, $resourcesHash) = $catalogue;
}
if ($this->debug && $resourcesHash !== $this->getResourcesHash($locale)) {
/*
* 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(<<<EOF
<?php <?php
use Symfony\Component\Translation\MessageCatalogue; use Symfony\Component\Translation\MessageCatalogue;
@ -372,33 +439,14 @@ use Symfony\Component\Translation\MessageCatalogue;
return array(\$catalogue, \$resourcesHash); return array(\$catalogue, \$resourcesHash);
EOF EOF
, ,
$this->getResourcesHash($locale), $this->getResourcesHash($locale),
$locale, $locale,
var_export($this->catalogues[$locale]->all(), true), var_export($this->catalogues[$locale]->all(), true),
$fallbackContent $fallbackContent
); );
$cache->write($content, $this->catalogues[$locale]->getResources()); $cache->write($content, $this->catalogues[$locale]->getResources());
return;
}
$catalogue = include $cache;
/*
* Old cache returns only the catalogue, without resourcesHash
*/
$resourcesHash = null;
if (is_array($catalogue)) {
list($catalogue, $resourcesHash) = $catalogue;
}
if ($this->debug && $resourcesHash !== $this->getResourcesHash($locale)) {
return $this->initializeCacheCatalogue($locale, true);
}
$this->catalogues[$locale] = $catalogue;
} }
private function getFallbackContent(MessageCatalogue $catalogue) private function getFallbackContent(MessageCatalogue $catalogue)
@ -514,4 +562,19 @@ EOF
throw new \InvalidArgumentException(sprintf('Invalid "%s" locale.', $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;
}
} }

View File

@ -20,11 +20,14 @@
}, },
"require-dev": { "require-dev": {
"symfony/phpunit-bridge": "~2.7|~3.0.0", "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/intl": "~2.3|~3.0.0",
"symfony/yaml": "~2.2|~3.0.0", "symfony/yaml": "~2.2|~3.0.0",
"psr/log": "~1.0" "psr/log": "~1.0"
}, },
"conflict": {
"symfony/config": "<2.7"
},
"suggest": { "suggest": {
"symfony/config": "", "symfony/config": "",
"symfony/yaml": "", "symfony/yaml": "",