diff --git a/src/Symfony/Bridge/Doctrine/Tests/Translation/DoctrineMessageCatalogueTest.php b/src/Symfony/Bridge/Doctrine/Tests/Translation/DoctrineMessageCatalogueTest.php new file mode 100644 index 0000000000000..17083a67ec4ed --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Translation/DoctrineMessageCatalogueTest.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\Bridge\Doctrine\Tests\Translation; + +use Symfony\Bridge\Doctrine\Translation\DoctrineMessageCatalogue; +use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Translation\Tests\MessageCatalogueTest; + +class DoctrineMessageCatalogueTest extends MessageCatalogueTest +{ + protected function setUp() + { + if (!interface_exists('Doctrine\Common\Cache\Cache')) { + $this->markTestSkipped('The "Doctrine Cache" is not available'); + } + } + + public function testAll() + { + if (!interface_exists('Doctrine\Common\Cache\MultiGetCache')) { + $this->markTestSkipped('The "Doctrine MultiGetCache" is not available'); + } + + parent::testAll(); + } + + protected function getCatalogue($locale, $messages = array()) + { + $catalogue = new DoctrineMessageCatalogue($locale, new ArrayCache()); + foreach ($messages as $domain => $domainMessages) { + $catalogue->add($domainMessages, $domain); + } + + return $catalogue; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Translation/TranslatorDoctrineCacheTest.php b/src/Symfony/Bridge/Doctrine/Tests/Translation/TranslatorDoctrineCacheTest.php new file mode 100644 index 0000000000000..996cb70c574e9 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Translation/TranslatorDoctrineCacheTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\Translation; + +use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageSelector; +use Symfony\Bridge\Doctrine\Translation\DoctrineMessageCache; +use Doctrine\Common\Cache\ArrayCache; +use Symfony\Component\Translation\Loader\PhpFileLoader; +use Symfony\Component\Translation\Dumper\PhpFileDumper; +use Symfony\Component\Translation\Tests\TranslatorCacheTest; + +class TranslatorDoctrineCacheTest extends TranslatorCacheTest +{ + private $cache; + + protected function setUp() + { + if (!interface_exists('Doctrine\Common\Cache\Cache')) { + $this->markTestSkipped('The "Doctrine Cache" is not available'); + } + + $this->cache = new ArrayCache(); + } + + protected function getTranslator($locale, $debug) + { + $cache = new DoctrineMessageCache($this->cache, $debug); + + return new Translator($locale, null, $cache); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Translation/DoctrineMessageCache.php b/src/Symfony/Bridge/Doctrine/Translation/DoctrineMessageCache.php new file mode 100644 index 0000000000000..fbad33e622bd4 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Translation/DoctrineMessageCache.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Translation; + +use Doctrine\Common\Cache\Cache; +use Symfony\Component\Translation\MessageCacheInterface; +use Symfony\Component\Translation\MessageCatalogueInterface; + +/** + * @author Abdellatif Ait Boudad + */ +class DoctrineMessageCache implements MessageCacheInterface +{ + const CACHE_CATALOGUE_HASH = 'catalogue_hash'; + const CACHE_DUMP_TIME = 'time'; + const CACHE_META_DATA = 'meta'; + const CATALOGUE_FALLBACK_LOCALE = 'fallback_locale'; + + /** + * @var bool + */ + private $debug; + + /** + * @var Cache + */ + private $cache; + + /** + * @param Cache $cache + * @param bool $debug + */ + public function __construct(Cache $cache, $debug = false) + { + $this->cache = $cache; + $this->debug = $debug; + } + + /** + * {@inheritdoc} + */ + public function isFresh($locale, array $options = array()) + { + $catalogueIdentifier = $this->getCatalogueIdentifier($locale, $options); + if (!$this->cache->contains($this->getCatalogueHashKey($catalogueIdentifier))) { + return false; + } + + if ($this->debug) { + $time = $this->cache->fetch($this->getDumpTimeKey($locale)); + $meta = unserialize($this->cache->fetch($this->getMetaDataKey($locale))); + foreach ($meta as $resource) { + if (!$resource->isFresh($time)) { + return false; + } + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function load($locale, array $options = array()) + { + $messages = new DoctrineMessageCatalogue($locale, $this->cache, $this->getCatalogueIdentifier($locale, $options)); + $catalogue = $messages; + while ($fallbackLocale = $this->cache->fetch($this->getFallbackLocaleKey($catalogue->getLocale()))) { + $fallback = new DoctrineMessageCatalogue($fallbackLocale, $this->cache, $this->getCatalogueIdentifier($locale, $options)); + $catalogue->addFallbackCatalogue($fallback); + $catalogue = $fallback; + } + + return $messages; + } + + /** + * {@inheritdoc} + */ + public function dump(MessageCatalogueInterface $messages, array $options = array()) + { + $resourcesHash = $this->getCatalogueIdentifier($messages->getLocale(), $options); + while ($messages) { + $catalogue = new DoctrineMessageCatalogue($messages->getLocale(), $this->cache, $resourcesHash); + $catalogue->addCatalogue($messages); + + $this->dumpMetaDataCatalogue($messages->getLocale(), $messages->getResources(), $resourcesHash); + if ($fallback = $messages->getFallbackCatalogue()) { + $this->cache->save($this->getFallbackLocaleKey($messages->getLocale()), $fallback->getLocale()); + } + + $messages = $messages->getFallbackCatalogue(); + } + } + + private function getCatalogueIdentifier($locale, $options) + { + return sha1(serialize(array( + 'resources' => $options['resources'], + 'fallback_locales' => $options['fallback_locales'], + ))); + } + + private function dumpMetaDataCatalogue($locale, $metadata, $resourcesHash) + { + // $catalogueIdentifier = $this->getCatalogueIdentifier($locale, $options); + $this->cache->save($this->getMetaDataKey($locale), serialize($metadata)); + $this->cache->save($this->getCatalogueHashKey($resourcesHash), $resourcesHash); + $this->cache->save($this->getDumpTimeKey($locale), time()); + } + + private function getDumpTimeKey($locale) + { + return self::CACHE_DUMP_TIME.'_'.$locale; + } + + private function getMetaDataKey($locale) + { + return self::CACHE_META_DATA.'_'.$locale; + } + + private function getCatalogueHashKey($locale) + { + return self::CACHE_CATALOGUE_HASH.'_'.$locale; + } + + private function getFallbackLocaleKey($locale) + { + return self::CATALOGUE_FALLBACK_LOCALE.'_'.$locale; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Translation/DoctrineMessageCatalogue.php b/src/Symfony/Bridge/Doctrine/Translation/DoctrineMessageCatalogue.php new file mode 100644 index 0000000000000..82c25c24ff08c --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Translation/DoctrineMessageCatalogue.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Translation; + +use Doctrine\Common\Cache\Cache; +use Doctrine\Common\Cache\MultiGetCache; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * @author Abdellatif Ait boudad + */ +class DoctrineMessageCatalogue extends MessageCatalogue +{ + const PREFIX = 'sf2_translation'; + const CATALOGUE_DOMAINS = 'domains'; + const CATALOGUE_DOMAIN_METADATA = 'domain_meta_'; + + /** + * @var Cache + */ + private $cache; + + /** + * @var string + */ + private $prefix; + + /** + * @var array + */ + private $domains = array(); + + /** + * @param string $locale + * @param Cache $cache + * @param string $prefix + */ + public function __construct($locale, Cache $cache, $prefix = self::PREFIX) + { + parent::__construct($locale); + if (0 === strlen($prefix)) { + throw new \InvalidArgumentException('$prefix cannot be empty.'); + } + + $this->cache = $cache; + $this->prefix = $prefix.'_'.$locale.'_'; + + if ($cache->contains($domainsId = $this->prefix.self::CATALOGUE_DOMAINS)) { + $this->domains = $cache->fetch($domainsId); + } + } + + /** + * {@inheritdoc} + */ + public function getDomains() + { + return $this->domains; + } + + /** + * {@inheritdoc} + */ + public function all($domain = null) + { + if (!$this->cache instanceof MultiGetCache) { + return array(); + } + + $domains = $this->domains; + if (null !== $domain) { + $domains = array($domain); + } + + $messages = array(); + foreach ($domains as $domainMeta) { + $domainIdentity = $this->getDomainMetaDataId($domainMeta); + if ($this->cache->contains($domainIdentity)) { + $keys = $this->cache->fetch($domainIdentity); + $values = $this->cache->fetchMultiple(array_keys($keys)); + foreach ($keys as $key => $id) { + if (isset($values[$key])) { + $messages[$domainMeta][$id] = $values[$key]; + } + } + } + } + + if (null === $domain) { + return $messages; + } + + return isset($messages[$domain]) ? $messages[$domain] : array(); + } + + /** + * {@inheritdoc} + */ + public function set($id, $translation, $domain = 'messages') + { + $this->add(array($id => $translation), $domain); + } + + /** + * {@inheritdoc} + */ + public function has($id, $domain = 'messages') + { + if ($this->defines($id, $domain)) { + return true; + } + + $fallbackCatalogue = $this->getFallbackCatalogue(); + if (null !== $fallbackCatalogue) { + return $fallbackCatalogue->has($id, $domain); + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function defines($id, $domain = 'messages') + { + $key = $this->getCacheId($domain, $id); + + return $this->cache->contains($key); + } + + /** + * {@inheritdoc} + */ + public function get($id, $domain = 'messages') + { + if ($this->defines($id, $domain)) { + return $this->cache->fetch($this->getCacheId($domain, $id)); + } + + $fallbackCatalogue = $this->getFallbackCatalogue(); + if (null !== $fallbackCatalogue) { + return $fallbackCatalogue->get($id, $domain); + } + + return $id; + } + + /** + * {@inheritdoc} + */ + public function replace($messages, $domain = 'messages') + { + $domainMetaData = array(); + $domainMetaKey = $this->getDomainMetaDataId($domain); + if ($this->cache->contains($domainMetaKey)) { + $domainMetaData = $this->cache->fetch($domainMetaKey); + } + + foreach ($domainMetaData as $key => $id) { + if (!isset($messages[$id])) { + unset($domainMetaData[$key]); + $this->cache->delete($key); + } + } + + $this->cache->save($domainMetaKey, $domainMetaData); + $this->add($messages, $domain); + } + + /** + * {@inheritdoc} + */ + public function add($messages, $domain = 'messages') + { + if (!isset($this->domains[$domain])) { + $this->addDomain($domain); + } + + $domainMetaData = array(); + $domainMetaKey = $this->getDomainMetaDataId($domain); + if ($this->cache->contains($domainMetaKey)) { + $domainMetaData = $this->cache->fetch($domainMetaKey); + } + + foreach ($messages as $id => $translation) { + $key = $this->getCacheId($domain, $id); + $domainMetaData[$key] = $id; + $this->cache->save($key, $translation); + } + + $this->addDomainMetaData($domain, $domainMetaData); + } + + private function addDomain($domain) + { + $this->domains[] = $domain; + $this->cache->save($this->prefix.self::CATALOGUE_DOMAINS, $this->domains); + } + + private function addDomainMetaData($domain, $keys = array()) + { + $domainIdentity = $this->getDomainMetaDataId($domain); + $this->cache->save($domainIdentity, $keys); + } + + private function getCacheId($id, $domain = 'messages') + { + return $this->prefix.$domain.'_'.sha1($id); + } + + private function getDomainMetaDataId($domain) + { + return $this->prefix.self::CATALOGUE_DOMAIN_METADATA.$domain; + } +} diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index 8b2ce6b8abbda..5df5191e8c721 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -29,17 +29,20 @@ "symfony/security": "~2.2", "symfony/expression-language": "~2.2", "symfony/validator": "~2.5,>=2.5.5", - "symfony/translation": "~2.0,>=2.0.5", + "symfony/translation": "~2.7", + "symfony/config": "~2.3,>=2.3.12", "doctrine/data-fixtures": "1.0.*", "doctrine/dbal": "~2.2", - "doctrine/orm": "~2.2,>=2.2.3" + "doctrine/orm": "~2.2,>=2.2.3", + "doctrine/cache": "~1.0" }, "suggest": { "symfony/form": "", "symfony/validator": "", "doctrine/data-fixtures": "", "doctrine/dbal": "", - "doctrine/orm": "" + "doctrine/orm": "", + "doctrine/cache": "" }, "autoload": { "psr-4": { "Symfony\\Bridge\\Doctrine\\": "" } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 8a73424d9463f..5aed33b0e3415 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -582,6 +582,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode) ->defaultValue(array('en')) ->end() ->booleanNode('logging')->defaultValue($this->debug)->end() + ->scalarNode('cache')->defaultValue('translation.cache.default')->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index a7d8aab22079c..fa085e3a8fcab 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -665,6 +665,14 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $container->setParameter('translator.logging', $config['logging']); + if (isset($config['cache'])) { + $container->setParameter( + 'translator.cache.prefix', + 'translator_'.hash('sha256', $container->getParameter('kernel.root_dir')) + ); + $container->setAlias('translation.cache', $config['cache']); + } + // Discover translation directories $dirs = array(); if (class_exists('Symfony\Component\Validator\Validation')) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index fa7aa2b2bd808..7f44aade68c8c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -187,6 +187,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml index 0007a360c6e46..5661f8956ad78 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml @@ -33,6 +33,7 @@ Symfony\Bundle\FrameworkBundle\Translation\TranslationLoader Symfony\Component\Translation\Extractor\ChainExtractor Symfony\Component\Translation\Writer\TranslationWriter + @@ -44,7 +45,7 @@ %kernel.cache_dir%/translations %kernel.debug% - + @@ -157,5 +158,11 @@ + + + + %kernel.cache_dir%/translations + %kernel.debug% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 617be624fdb9f..d9fe145f66d66 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -146,6 +146,7 @@ protected static function getBundleDefaultConfig() 'enabled' => false, 'fallbacks' => array('en'), 'logging' => true, + 'cache' => 'translation.cache.default', ), 'validation' => array( 'enabled' => false, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 918c676afe1a1..a07c06079a363 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -247,6 +247,7 @@ public function testTranslator() $calls = $container->getDefinition('translator.default')->getMethodCalls(); $this->assertEquals(array('fr'), $calls[0][1][0]); + $this->assertContains('translator_', $container->getParameter('translator.cache.prefix')); } public function testTranslatorMultipleFallbacks() diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index 9cc92f9c2d616..67e6105e74c0d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -15,6 +15,7 @@ use Symfony\Component\Translation\Translator as BaseTranslator; use Symfony\Component\Translation\MessageSelector; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\Translation\MessageCacheInterface; /** * Translator. @@ -46,14 +47,15 @@ class Translator extends BaseTranslator implements WarmableInterface * * debug: Whether to enable debugging or not (false by default) * * resource_files: List of translation resources available grouped by locale. * - * @param ContainerInterface $container A ContainerInterface instance - * @param MessageSelector $selector The message selector for pluralization - * @param array $loaderIds An array of loader Ids - * @param array $options An array of options + * @param ContainerInterface $container A ContainerInterface instance + * @param MessageSelector $selector The message selector for pluralization + * @param array $loaderIds An array of loader Ids + * @param array $options An array of options + * @param MessageCacheInterface $cache The message cache * * @throws \InvalidArgumentException */ - public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array()) + public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array(), MessageCacheInterface $cache = null) { $this->container = $container; $this->loaderIds = $loaderIds; @@ -69,7 +71,8 @@ public function __construct(ContainerInterface $container, MessageSelector $sele $this->loadResources(); } - parent::__construct($container->getParameter('kernel.default_locale'), $selector, $this->options['cache_dir'], $this->options['debug']); + $cache = $cache ?: $this->options['cache_dir']; + parent::__construct($container->getParameter('kernel.default_locale'), $selector, $cache, $this->options['debug']); } /** diff --git a/src/Symfony/Component/Translation/MessageCache.php b/src/Symfony/Component/Translation/MessageCache.php new file mode 100644 index 0000000000000..cd36c143d2f22 --- /dev/null +++ b/src/Symfony/Component/Translation/MessageCache.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Config\ConfigCacheInterface; +use Symfony\Component\Config\ConfigCacheFactoryInterface; +use Symfony\Component\Config\ConfigCacheFactory; + +/** + * @author Abdellatif Ait Boudad + */ +class MessageCache implements MessageCacheInterface +{ + /** + * @var string + */ + private $cacheDir; + + /** + * @var bool + */ + private $debug; + + /** + * @var ConfigCacheFactoryInterface + */ + private $configCacheFactory; + + /** + * @param string $cacheDir + * @param bool $debug + * @param ConfigCacheFactoryInterface $configCacheFactory + */ + public function __construct($cacheDir, $debug = false, ConfigCacheFactoryInterface $configCacheFactory = null) + { + $this->cacheDir = $cacheDir; + $this->debug = $debug; + + if (null === $configCacheFactory) { + $configCacheFactory = new ConfigCacheFactory($debug); + } + + $this->configCacheFactory = $configCacheFactory; + } + + /** + * {@inheritdoc} + */ + public function isFresh($locale, array $options = array()) + { + $cache = $this->configCacheFactory->cache($this->getCatalogueCachePath($locale, $options), function ($cache) {}); + + return $cache->isFresh(); + } + + /** + * {@inheritdoc} + */ + public function load($locale, array $options = array()) + { + $cache = $this->configCacheFactory->cache($this->getCatalogueCachePath($locale, $options), function ($cache) {}); + + return include $cache->getPath(); + } + + /** + * {@inheritdoc} + */ + public function dump(MessageCatalogueInterface $messages, array $options = array()) + { + $self = $this; + $this->configCacheFactory->cache($this->getCatalogueCachePath($messages->getLocale(), $options), + function (ConfigCacheInterface $cache) use ($self, $messages) { + $self->dumpCatalogue($messages, $cache); + } + ); + } + + /** + * 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($catalogue, ConfigCacheInterface $cache) + { + $fallbackContent = $this->getFallbackContent($catalogue); + $content = sprintf(<<getLocale(), + var_export($catalogue->all(), true), + $fallbackContent + ); + $cache->write($content, $catalogue->getResources()); + } + + private function getFallbackContent(MessageCatalogue $catalogue) + { + $fallbackContent = ''; + $current = ''; + $replacementPattern = '/[^a-z0-9_]/i'; + $fallbackCatalogue = $catalogue->getFallbackCatalogue(); + while ($fallbackCatalogue) { + $fallback = $fallbackCatalogue->getLocale(); + $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback)); + $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current)); + $fallbackContent .= sprintf(<<addFallbackCatalogue(\$catalogue%s); +EOF + , + $fallbackSuffix, + $fallback, + var_export($fallbackCatalogue->all(), true), + $currentSuffix, + $fallbackSuffix + ); + $current = $fallbackCatalogue->getLocale(); + $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); + } + + return $fallbackContent; + } + + private function getCatalogueCachePath($locale, $options) + { + $catalogueHash = sha1(serialize(array( + 'resources' => $options['resources'], + 'fallback_locales' => $options['fallback_locales'], + ))); + + return sprintf('%s/catalogue.%s.%s.php', $this->cacheDir, $locale, $catalogueHash); + } +} diff --git a/src/Symfony/Component/Translation/MessageCacheInterface.php b/src/Symfony/Component/Translation/MessageCacheInterface.php new file mode 100644 index 0000000000000..2981b1abc1d3a --- /dev/null +++ b/src/Symfony/Component/Translation/MessageCacheInterface.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\Component\Translation; + +/** + * @author Abdellatif Ait Boudad + */ +interface MessageCacheInterface +{ + /** + * Returns true if the cache is still fresh. + * + * @param string $locale + * @param array $options + * + * @return bool + */ + public function isFresh($locale, array $options = array()); + + /** + * Loads a catalogue. + * + * @param string $locale The locale + * + * @return MessageCatalogueInterface A MessageCatalogue instance + */ + public function load($locale); + + /** + * Dumps the message catalogue. + * + * @param MessageCatalogueInterface $messages The message catalogue + * @param array $options Options that are used by the dumper + */ + public function dump(MessageCatalogueInterface $messages, array $options = array()); +} diff --git a/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php b/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php index 7d956553d98c6..84a4f08b25adb 100644 --- a/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php +++ b/src/Symfony/Component/Translation/Tests/MessageCatalogueTest.php @@ -17,21 +17,21 @@ class MessageCatalogueTest extends \PHPUnit_Framework_TestCase { public function testGetLocale() { - $catalogue = new MessageCatalogue('en'); + $catalogue = $this->getCatalogue('en'); $this->assertEquals('en', $catalogue->getLocale()); } public function testGetDomains() { - $catalogue = new MessageCatalogue('en', array('domain1' => array(), 'domain2' => array())); + $catalogue = $this->getCatalogue('en', array('domain1' => array(), 'domain2' => array())); $this->assertEquals(array('domain1', 'domain2'), $catalogue->getDomains()); } public function testAll() { - $catalogue = new MessageCatalogue('en', $messages = array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); + $catalogue = $this->getCatalogue('en', $messages = array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); $this->assertEquals(array('foo' => 'foo'), $catalogue->all('domain1')); $this->assertEquals(array(), $catalogue->all('domain88')); @@ -40,7 +40,7 @@ public function testAll() public function testHas() { - $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); + $catalogue = $this->getCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); $this->assertTrue($catalogue->has('foo', 'domain1')); $this->assertFalse($catalogue->has('bar', 'domain1')); @@ -49,7 +49,7 @@ public function testHas() public function testGetSet() { - $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); + $catalogue = $this->getCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); $catalogue->set('foo1', 'foo1', 'domain1'); $this->assertEquals('foo', $catalogue->get('foo', 'domain1')); @@ -58,7 +58,7 @@ public function testGetSet() public function testAdd() { - $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); + $catalogue = $this->getCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); $catalogue->add(array('foo1' => 'foo1'), 'domain1'); $this->assertEquals('foo', $catalogue->get('foo', 'domain1')); @@ -74,7 +74,7 @@ public function testAdd() public function testReplace() { - $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); + $catalogue = $this->getCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); $catalogue->replace($messages = array('foo1' => 'foo1'), 'domain1'); $this->assertEquals($messages, $catalogue->all('domain1')); @@ -88,10 +88,10 @@ public function testAddCatalogue() $r1 = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface'); $r1->expects($this->any())->method('__toString')->will($this->returnValue('r1')); - $catalogue = new MessageCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); + $catalogue = $this->getCatalogue('en', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); $catalogue->addResource($r); - $catalogue1 = new MessageCatalogue('en', array('domain1' => array('foo1' => 'foo1'))); + $catalogue1 = $this->getCatalogue('en', array('domain1' => array('foo1' => 'foo1'))); $catalogue1->addResource($r1); $catalogue->addCatalogue($catalogue1); @@ -110,10 +110,10 @@ public function testAddFallbackCatalogue() $r1 = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface'); $r1->expects($this->any())->method('__toString')->will($this->returnValue('r1')); - $catalogue = new MessageCatalogue('en_US', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); + $catalogue = $this->getCatalogue('en_US', array('domain1' => array('foo' => 'foo'), 'domain2' => array('bar' => 'bar'))); $catalogue->addResource($r); - $catalogue1 = new MessageCatalogue('en', array('domain1' => array('foo' => 'bar', 'foo1' => 'foo1'))); + $catalogue1 = $this->getCatalogue('en', array('domain1' => array('foo' => 'bar', 'foo1' => 'foo1'))); $catalogue1->addResource($r1); $catalogue->addFallbackCatalogue($catalogue1); @@ -129,8 +129,8 @@ public function testAddFallbackCatalogue() */ public function testAddFallbackCatalogueWithCircularReference() { - $main = new MessageCatalogue('en_US'); - $fallback = new MessageCatalogue('fr_FR'); + $main = $this->getCatalogue('en_US'); + $fallback = $this->getCatalogue('fr_FR'); $fallback->addFallbackCatalogue($main); $main->addFallbackCatalogue($fallback); @@ -141,13 +141,13 @@ public function testAddFallbackCatalogueWithCircularReference() */ public function testAddCatalogueWhenLocaleIsNotTheSameAsTheCurrentOne() { - $catalogue = new MessageCatalogue('en'); - $catalogue->addCatalogue(new MessageCatalogue('fr', array())); + $catalogue = $this->getCatalogue('en'); + $catalogue->addCatalogue($this->getCatalogue('fr', array())); } public function testGetAddResource() { - $catalogue = new MessageCatalogue('en'); + $catalogue = $this->getCatalogue('en'); $r = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface'); $r->expects($this->any())->method('__toString')->will($this->returnValue('r')); $catalogue->addResource($r); @@ -161,7 +161,7 @@ public function testGetAddResource() public function testMetadataDelete() { - $catalogue = new MessageCatalogue('en'); + $catalogue = $this->getCatalogue('en'); $this->assertEquals(array(), $catalogue->getMetadata('', ''), 'Metadata is empty'); $catalogue->deleteMetadata('key', 'messages'); $catalogue->deleteMetadata('', 'messages'); @@ -170,7 +170,7 @@ public function testMetadataDelete() public function testMetadataSetGetDelete() { - $catalogue = new MessageCatalogue('en'); + $catalogue = $this->getCatalogue('en'); $catalogue->setMetadata('key', 'value'); $this->assertEquals('value', $catalogue->getMetadata('key', 'messages'), "Metadata 'key' = 'value'"); @@ -186,15 +186,20 @@ public function testMetadataSetGetDelete() public function testMetadataMerge() { - $cat1 = new MessageCatalogue('en'); + $cat1 = $this->getCatalogue('en'); $cat1->setMetadata('a', 'b'); $this->assertEquals(array('messages' => array('a' => 'b')), $cat1->getMetadata('', ''), 'Cat1 contains messages metadata.'); - $cat2 = new MessageCatalogue('en'); + $cat2 = $this->getCatalogue('en'); $cat2->setMetadata('b', 'c', 'domain'); $this->assertEquals(array('domain' => array('b' => 'c')), $cat2->getMetadata('', ''), 'Cat2 contains domain metadata.'); $cat1->addCatalogue($cat2); $this->assertEquals(array('messages' => array('a' => 'b'), 'domain' => array('b' => 'c')), $cat1->getMetadata('', ''), 'Cat1 contains merged metadata.'); } + + protected function getCatalogue($locale, $messages = array()) + { + return new MessageCatalogue($locale, $messages); + } } diff --git a/src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php b/src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php index d5d4639984ce5..d559817a85b99 100644 --- a/src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php +++ b/src/Symfony/Component/Translation/Tests/TranslatorCacheTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Translator; use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCache; class TranslatorCacheTest extends \PHPUnit_Framework_TestCase { @@ -62,13 +63,13 @@ public function testThatACacheIsUsed($debug) $msgid = 'test'; // Prime the cache - $translator = new Translator($locale, null, $this->tmpDir, $debug); + $translator = $this->getTranslator($locale, $debug); $translator->addLoader($format, new ArrayLoader()); $translator->addResource($format, array($msgid => 'OK'), $locale); $translator->trans($msgid); // Try again and see we get a valid result whilst no loader can be used - $translator = new Translator($locale, null, $this->tmpDir, $debug); + $translator = $this->getTranslator($locale, $debug); $translator->addLoader($format, $this->createFailingLoader()); $translator->addResource($format, array($msgid => 'OK'), $locale); $this->assertEquals('OK', $translator->trans($msgid), '-> caching does not work in '.($debug ? 'debug' : 'production')); @@ -85,7 +86,7 @@ public function testRefreshCacheWhenResourcesChange() )))) ; - $translator = new Translator('fr', null, $this->tmpDir, true); + $translator = $this->getTranslator('fr', true); $translator->setLocale('fr'); $translator->addLoader('loader', $loader); $translator->addResource('loader', 'foo', 'fr'); @@ -101,7 +102,7 @@ public function testRefreshCacheWhenResourcesChange() )))) ; - $translator = new Translator('fr', null, $this->tmpDir, true); + $translator = $this->getTranslator('fr', true); $translator->setLocale('fr'); $translator->addLoader('loader', $loader); $translator->addResource('loader', 'bar', 'fr'); @@ -126,7 +127,7 @@ public function testCatalogueIsReloadedWhenResourcesAreNoLongerFresh() $format = 'some_format'; $msgid = 'test'; - $catalogue = new MessageCatalogue($locale, array()); + $catalogue = $this->getCatalogue($locale, array()); $catalogue->addResource(new StaleResource()); // better use a helper class than a mock, because it gets serialized in the cache and re-loaded /** @var LoaderInterface|\PHPUnit_Framework_MockObject_MockObject $loader */ @@ -138,13 +139,13 @@ public function testCatalogueIsReloadedWhenResourcesAreNoLongerFresh() ; // 1st pass - $translator = new Translator($locale, null, $this->tmpDir, true); + $translator = $this->getTranslator($locale, true); $translator->addLoader($format, $loader); $translator->addResource($format, null, $locale); $translator->trans($msgid); // 2nd pass - $translator = new Translator($locale, null, $this->tmpDir, true); + $translator = $this->getTranslator($locale, true); $translator->addLoader($format, $loader); $translator->addResource($format, null, $locale); $translator->trans($msgid); @@ -160,7 +161,7 @@ public function testDifferentTranslatorsForSameLocaleDoNotInterfere($debug) $msgid = 'test'; // Create a Translator and prime its cache - $translator = new Translator($locale, null, $this->tmpDir, $debug); + $translator = $this->getTranslator($locale, $debug); $translator->addLoader($format, new ArrayLoader()); $translator->addResource($format, array($msgid => 'FAIL'), $locale); $translator->trans($msgid); @@ -169,7 +170,7 @@ public function testDifferentTranslatorsForSameLocaleDoNotInterfere($debug) * Create another Translator with the same locale but a different resource. * It should not use the first translator's cache but return the value from its own resource. */ - $translator = new Translator($locale, null, $this->tmpDir, $debug); + $translator = $this->getTranslator($locale, $debug); $translator->addLoader($format, new ArrayLoader()); $translator->addResource($format, array($msgid => 'OK'), $locale); @@ -191,19 +192,19 @@ public function testDifferentTranslatorsForSameLocaleDoNotOverwriteEachOthersCac $msgid = 'test'; // Create a Translator and prime its cache - $translator = new Translator($locale, null, $this->tmpDir, $debug); + $translator = $this->getTranslator($locale, $debug); $translator->addLoader($format, new ArrayLoader()); $translator->addResource($format, array($msgid => 'OK'), $locale); $translator->trans($msgid); // Create another Translator with a different catalogue for the same locale - $translator = new Translator($locale, null, $this->tmpDir, $debug); + $translator = $this->getTranslator($locale, $debug); $translator->addLoader($format, new ArrayLoader()); $translator->addResource($format, array($msgid => 'FAIL'), $locale); $translator->trans($msgid); // Now the first translator must still have a useable cache. - $translator = new Translator($locale, null, $this->tmpDir, $debug); + $translator = $this->getTranslator($locale, $debug); $translator->addLoader($format, $this->createFailingLoader()); $translator->addResource($format, array($msgid => 'OK'), $locale); $this->assertEquals('OK', $translator->trans($msgid), '-> the cache was overwritten by another translator instance in '.($debug ? 'debug' : 'production')); @@ -216,7 +217,7 @@ public function testDifferentCacheFilesAreUsedForDifferentSetsOfFallbackLocales( * catalogues, we must take the set of fallback locales into consideration when * loading a catalogue from the cache. */ - $translator = new Translator('a', null, $this->tmpDir); + $translator = $this->getTranslator('a', false); $translator->setFallbackLocales(array('b')); $translator->addLoader('array', new ArrayLoader()); @@ -230,7 +231,7 @@ public function testDifferentCacheFilesAreUsedForDifferentSetsOfFallbackLocales( $this->assertEquals('bar', $translator->trans('bar')); // Use a fresh translator with no fallback locales, result should be the same - $translator = new Translator('a', null, $this->tmpDir); + $translator = $this->getTranslator('a', false); $translator->addLoader('array', new ArrayLoader()); $translator->addResource('array', array('foo' => 'foo (a)'), 'a'); @@ -254,7 +255,7 @@ public function testPrimaryAndFallbackCataloguesContainTheSameMessagesRegardless * Create a translator that loads two catalogues for two different locales. * The catalogues contain distinct sets of messages. */ - $translator = new Translator('a', null, $this->tmpDir); + $translator = $this->getTranslator('a', false); $translator->setFallbackLocales(array('b')); $translator->addLoader('array', new ArrayLoader()); @@ -272,7 +273,7 @@ public function testPrimaryAndFallbackCataloguesContainTheSameMessagesRegardless * Now, repeat the same test. * Behind the scenes, the cache is used. But that should not matter, right? */ - $translator = new Translator('a', null, $this->tmpDir); + $translator = $this->getTranslator('a', false); $translator->setFallbackLocales(array('b')); $translator->addLoader('array', new ArrayLoader()); @@ -298,18 +299,25 @@ public function testRefreshCacheWhenResourcesAreNoLongerFresh() ->will($this->returnValue($this->getCatalogue('fr', array(), array($resource)))); // prime the cache - $translator = new Translator('fr', null, $this->tmpDir, true); + $translator = $this->getTranslator('fr', true); $translator->addLoader('loader', $loader); $translator->addResource('loader', 'foo', 'fr'); $translator->trans('foo'); // prime the cache second time - $translator = new Translator('fr', null, $this->tmpDir, true); + $translator = $this->getTranslator('fr', true); $translator->addLoader('loader', $loader); $translator->addResource('loader', 'foo', 'fr'); $translator->trans('foo'); } + protected function getTranslator($locale, $debug) + { + $cache = new MessageCache($this->tmpDir, $debug); + + return new Translator($locale, null, $cache); + } + protected function getCatalogue($locale, $messages, $resources = array()) { $catalogue = new MessageCatalogue($locale); diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 7ed987d41ee8d..5157fef30824e 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -13,9 +13,6 @@ use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\Exception\NotFoundResourceException; -use Symfony\Component\Config\ConfigCacheInterface; -use Symfony\Component\Config\ConfigCacheFactoryInterface; -use Symfony\Component\Config\ConfigCacheFactory; /** * Translator. @@ -57,37 +54,29 @@ class Translator implements TranslatorInterface, TranslatorBagInterface private $selector; /** - * @var string - */ - private $cacheDir; - - /** - * @var bool - */ - private $debug; - - /** - * @var ConfigCacheFactoryInterface|null + * @var MessageCacheInterface */ - private $configCacheFactory; + private $cache; /** - * Constructor. - * - * @param string $locale The locale - * @param MessageSelector|null $selector The message selector for pluralization - * @param string|null $cacheDir The directory to use for the cache - * @param bool $debug Use cache in debug mode ? + * @param string $locale The locale + * @param MessageSelector|null $selector The message selector for pluralization + * @param string|null|MessageCacheInterface $cache The message cache or a directory to use for the default cache + * @param bool $debug Use cache in debug mode ? * * @throws \InvalidArgumentException If a locale contains invalid characters * * @api */ - public function __construct($locale, MessageSelector $selector = null, $cacheDir = null, $debug = false) + public function __construct($locale, MessageSelector $selector = null, $cache = null, $debug = false) { $this->setLocale($locale); $this->selector = $selector ?: new MessageSelector(); - $this->cacheDir = $cacheDir; + if (null !== $cache && !$cache instanceof MessageCacheInterface) { + $cache = new MessageCache($cache, $debug); + } + + $this->cache = $cache; $this->debug = $debug; } @@ -311,7 +300,11 @@ public function getMessages($locale = null) */ protected function loadCatalogue($locale) { - if (null === $this->cacheDir) { + if (isset($this->catalogues[$locale])) { + return; + } + + if (null === $this->cache) { $this->initializeCatalogue($locale); } else { $this->initializeCacheCatalogue($locale); @@ -345,91 +338,19 @@ private function initializeCacheCatalogue($locale) return; } - $this->assertValidLocale($locale); - $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($this->getCatalogueCachePath($locale), - 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; - } - - /* Read catalogue from cache. */ - $this->catalogues[$locale] = include $cache->getPath(); - } - - /** - * 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]->all(), true), - $fallbackContent - ); - - $cache->write($content, $this->catalogues[$locale]->getResources()); - } - - private function getFallbackContent(MessageCatalogue $catalogue) - { - $fallbackContent = ''; - $current = ''; - $replacementPattern = '/[^a-z0-9_]/i'; - $fallbackCatalogue = $catalogue->getFallbackCatalogue(); - while ($fallbackCatalogue) { - $fallback = $fallbackCatalogue->getLocale(); - $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback)); - $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current)); - - $fallbackContent .= sprintf(<<addFallbackCatalogue(\$catalogue%s); - -EOF - , - $fallbackSuffix, - $fallback, - var_export($fallbackCatalogue->all(), true), - $currentSuffix, - $fallbackSuffix - ); - $current = $fallbackCatalogue->getLocale(); - $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); - } - - return $fallbackContent; - } - - private function getCatalogueCachePath($locale) - { - $catalogueHash = sha1(serialize(array( + $options = array( 'resources' => isset($this->resources[$locale]) ? $this->resources[$locale] : array(), 'fallback_locales' => $this->fallbackLocales, - ))); + ); - return $this->cacheDir.'/catalogue.'.$locale.'.'.$catalogueHash.'.php'; + if (!$this->cache->isFresh($locale, $options)) { + $this->initializeCatalogue($locale); + foreach ($this->catalogues as $locale => $catalogue) { + $this->cache->dump($catalogue, $options); + } + } else { + $this->catalogues[$locale] = $this->cache->load($locale, $options); + } } private function doLoadCatalogue($locale)