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) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy