Skip to content

[RFC] Factor loading and caching out of Translator #14622

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions src/Symfony/Component/Translation/Provider/Cache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?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\Translation\Provider;

use Symfony\Component\Config\ConfigCacheFactory;
use Symfony\Component\Config\ConfigCacheFactoryInterface;
use Symfony\Component\Config\ConfigCacheInterface;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogueInterface;

/**
* Caches the results provided by a MessageCatalogueProviderInterface
* in a ConfigCacheInterface instance.
*
* Registered resources are tracked so different cache locations can be
* used for different resource sets.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class Cache implements MessageCatalogueProviderInterface
{
/**
* @var string
*/
private $cacheDir;

/**
* @var ConfigCacheFactoryInterface|null
*/
private $configCacheFactory;

/**
* @var array
*/
private $resources = array();

/**
* @var MessageCatalogueProviderInterface
*/
private $decorated;

public function __construct(MessageCatalogueProviderInterface $decorated, $cacheDir, $debug)
{
$this->decorated = $decorated;
$this->cacheDir = $cacheDir;
$this->configCacheFactory = new ConfigCacheFactory($debug);
}

/**
* Sets the ConfigCache factory to use.
*
* @param ConfigCacheFactoryInterface $configCacheFactory
*/
public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory)
{
$this->configCacheFactory = $configCacheFactory;
}

public function addLoader($format, LoaderInterface $loader)
{
$this->decorated->addLoader($format, $loader);
}

public function getLoaders()
{
return $this->decorated->getLoaders();
}

public function addResource($format, $resource, $locale, $domain = null)
{
// track resources for cache file path only
$this->resources[$locale][] = array($format, $resource, $domain ?: 'messages');
$this->decorated->addResource($format, $resource, $locale, $domain);
}

public function provideCatalogue($locale, $fallbackLocales = array())
{
$tmpCatalogue = null;

$self = $this; // required for PHP 5.3 where "$this" cannot be use()d in anonymous functions. Change in Symfony 3.0.
$cache = $this->configCacheFactory->cache($this->getCatalogueCachePath($locale, $fallbackLocales),
function (ConfigCacheInterface $cache) use ($self, $locale, $fallbackLocales, &$tmpCatalogue) {
$tmpCatalogue = $self->dumpCatalogue($locale, $fallbackLocales, $cache);
}
);

if ($tmpCatalogue !== null) {
/* Catalogue has been initialized as it was written out to cache. */
return $tmpCatalogue;
}

/* Read catalogue from cache. */
return 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, $fallbackLocales, ConfigCacheInterface $cache)
{
$catalogue = $this->decorated->provideCatalogue($locale, $fallbackLocales);
$fallbackContent = $this->getFallbackContent($catalogue);

$content = sprintf(<<<EOF
<?php

use Symfony\Component\Translation\MessageCatalogue;

\$catalogue = new MessageCatalogue('%s', %s);

%s
return \$catalogue;

EOF
,
$locale,
var_export($catalogue->all(), true),
$fallbackContent
);

$cache->write($content, $catalogue->getResources());

return $catalogue;
}

private function getFallbackContent(MessageCatalogueInterface $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(<<<EOF
\$catalogue%s = new MessageCatalogue('%s', %s);
\$catalogue%s->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, $fallbackLocales)
{
$catalogueHash = sha1(serialize(array(
'resources' => isset($this->resources[$locale]) ? $this->resources[$locale] : array(),
'fallback_locales' => $fallbackLocales,
)));

return $this->cacheDir.'/catalogue.'.$locale.'.'.$catalogueHash.'.php';
}
}
97 changes: 97 additions & 0 deletions src/Symfony/Component/Translation/Provider/DefaultProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?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\Translation\Provider;

use Symfony\Component\Translation\Exception\NotFoundResourceException;
use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\MessageCatalogueInterface;

/**
* The default implementation for MessageCatalogueProviderInterface.
*
* Loaders and Resources can be registered and will be used to load catalogues.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
class DefaultProvider implements MessageCatalogueProviderInterface
{
/**
* @var LoaderInterface[]
*/
private $loaders = array();

/**
* @var array
*/
private $resources = array();

public function addLoader($format, LoaderInterface $loader)
{
$this->loaders[$format] = $loader;
}

public function getLoaders()
{
return $this->loaders;
}

public function addResource($format, $resource, $locale, $domain = null)
{
if (null === $domain) {
$domain = 'messages';
}

$this->resources[$locale][] = array('format' => $format, 'resource' => $resource, 'domain' => $domain);
}

public function provideCatalogue($locale, $fallbackLocales = array())
{
$catalogue = new MessageCatalogue($locale);

try {
$this->doLoadCatalogue($catalogue, $locale);
} catch (NotFoundResourceException $e) {
if (!$fallbackLocales) {
throw $e;
}
}
$this->loadFallbackCatalogues($catalogue, $fallbackLocales);

return $catalogue;
}

private function doLoadCatalogue(MessageCatalogueInterface $catalogue, $locale)
{
if (isset($this->resources[$locale])) {
foreach ($this->resources[$locale] as $resource) {
$format = $resource['format'];
if (!isset($this->loaders[$format])) {
throw new \RuntimeException(sprintf('The "%s" translation loader is not registered.', $format));
}
$catalogue->addCatalogue($this->loaders[$format]->load($resource['resource'], $locale, $resource['domain']));
}
}
}

private function loadFallbackCatalogues(MessageCatalogueInterface $catalogue, $fallbackLocales)
{
$current = $catalogue;

foreach ($fallbackLocales as $fallback) {
$fallbackCatalogue = new MessageCatalogue($fallback);
$this->doLoadCatalogue($fallbackCatalogue, $fallback);
$current->addFallbackCatalogue($fallbackCatalogue);
$current = $fallbackCatalogue;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?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\Translation\Provider;

use Symfony\Component\Translation\Loader\LoaderInterface;
use Symfony\Component\Translation\MessageCatalogueInterface;

/**
* MessageCatalogueProviderInterface describes a class that can take
* Loaders (@see LoaderInterface), some resources and then provide
* a MessageCatalogue chain for a given primary and possibly additional
* fallback locales.
*
* @author Matthias Pigulla <mp@webfactory.de>
*/
interface MessageCatalogueProviderInterface
{
/**
* Adds a Loader.
*
* @param string $format The name of the loader (@see addResource())
* @param LoaderInterface $loader A LoaderInterface instance
*/
public function addLoader($format, LoaderInterface $loader);

/**
* Gets the loaders.
*
* @return array LoaderInterface[]
*/
public function getLoaders();

/**
* Adds a Resource.
*
* @param string $format The name of the loader (@see addLoader())
* @param mixed $resource The resource name
* @param string $locale The locale
* @param string $domain The domain
*
* @throws \InvalidArgumentException If the locale contains invalid characters
*/
public function addResource($format, $resource, $locale, $domain = null);

/**
* Provide a MessageCatalogue chain.
*
* @param $locale The primary locale
* @param $fallbackLocales Locales (in order) for the fallback catalogues
*
* @return MessageCatalogueInterface
*/
public function provideCatalogue($locale, $fallbackLocales = array());
}
Loading