add model based loader and dumper for Translation
This commit is contained in:
parent
747299cfea
commit
28a55e1cb3
|
@ -113,6 +113,7 @@ Now you can read more about:
|
||||||
* [The Fixtures](fixtures.markdown);
|
* [The Fixtures](fixtures.markdown);
|
||||||
* [The PropelParamConverter](param_converter.markdown);
|
* [The PropelParamConverter](param_converter.markdown);
|
||||||
* [The UniqueObjectValidator](unique_object_validator.markdown).
|
* [The UniqueObjectValidator](unique_object_validator.markdown).
|
||||||
|
* [The ModelTranslation](model_translation.markdown).
|
||||||
|
|
||||||
|
|
||||||
## Bundle Inheritance ##
|
## Bundle Inheritance ##
|
||||||
|
|
83
Resources/doc/model_translation.markdown
Normal file
83
Resources/doc/model_translation.markdown
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
ModelTranslation
|
||||||
|
================
|
||||||
|
|
||||||
|
The `PropelBundle` provides a model-based implementation of the Translation components' loader and dumper.
|
||||||
|
To make us of this `ModelTranslation` you only need to add the translation resource.
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
services:
|
||||||
|
translation.loader.propel:
|
||||||
|
class: Propel\PropelBundle\Translation\ModelTranslation
|
||||||
|
arguments:
|
||||||
|
# The model to be used.
|
||||||
|
- 'Acme\Model\Translation\Translation'
|
||||||
|
# The column mappings to interact with the model.
|
||||||
|
-
|
||||||
|
columns:
|
||||||
|
key: 'key'
|
||||||
|
translation: 'translation'
|
||||||
|
locale: 'locale'
|
||||||
|
domain: 'domain'
|
||||||
|
updated_at: 'updated_at'
|
||||||
|
calls:
|
||||||
|
- [ 'registerResources', [ '@translator' ] ]
|
||||||
|
tags:
|
||||||
|
- { name: 'translation.loader', alias: 'propel' }
|
||||||
|
# The dumper tag is optional.
|
||||||
|
- { name: 'translation.dumper', alias: 'propel' }
|
||||||
|
```
|
||||||
|
|
||||||
|
This will add another resource to the translator to be scanned for translations.
|
||||||
|
|
||||||
|
## Translation model
|
||||||
|
|
||||||
|
An example model schema for the translation model:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<database name="translation" defaultIdMethod="native" namespace="Propel\PropelBundle\Tests\Fixtures\Model">
|
||||||
|
<table name="translation">
|
||||||
|
<column name="id" type="integer" autoIncrement="true" primaryKey="true" />
|
||||||
|
<column name="key" type="varchar" size="255" required="true" primaryString="true" />
|
||||||
|
<column name="translation" type="longvarchar" lazyLoad="true" required="true" />
|
||||||
|
<column name="locale" type="varchar" size="255" required="true" />
|
||||||
|
<column name="domain" type="varchar" size="255" required="true" />
|
||||||
|
<column name="updated_at" type="timestamp" />
|
||||||
|
|
||||||
|
<index>
|
||||||
|
<index-column name="domain" />
|
||||||
|
</index>
|
||||||
|
<index>
|
||||||
|
<index-column name="locale" />
|
||||||
|
<index-column name="domain" />
|
||||||
|
</index>
|
||||||
|
|
||||||
|
<unique>
|
||||||
|
<unique-column name="key" />
|
||||||
|
<unique-column name="locale" />
|
||||||
|
<unique-column name="domain" />
|
||||||
|
</unique>
|
||||||
|
</table>
|
||||||
|
</database>
|
||||||
|
```
|
||||||
|
|
||||||
|
### VersionableBehavior
|
||||||
|
|
||||||
|
In order to make use of the `VersionableBehavior` (or similar), you can map the `updated_at` column to the `version_created_at` column:
|
||||||
|
|
||||||
|
``` yaml
|
||||||
|
services:
|
||||||
|
translation.loader.propel:
|
||||||
|
class: Propel\PropelBundle\Translation\ModelTranslation
|
||||||
|
arguments:
|
||||||
|
- 'Acme\Model\Translation\Translation'
|
||||||
|
-
|
||||||
|
columns:
|
||||||
|
updated_at: 'version_created_at'
|
||||||
|
calls:
|
||||||
|
- [ 'registerResources', [ '@translator' ] ]
|
||||||
|
tags:
|
||||||
|
- { name: 'translation.loader', alias: 'propel' }
|
||||||
|
```
|
||||||
|
|
||||||
|
[Back to index](index.markdown)
|
25
Tests/Fixtures/translation_schema.xml
Normal file
25
Tests/Fixtures/translation_schema.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<database name="translation" defaultIdMethod="native" namespace="Propel\PropelBundle\Tests\Fixtures\Model">
|
||||||
|
<table name="translation">
|
||||||
|
<column name="id" type="integer" autoIncrement="true" primaryKey="true" />
|
||||||
|
<column name="key" type="varchar" size="255" required="true" primaryString="true" />
|
||||||
|
<column name="message" type="longvarchar" lazyLoad="true" required="true" />
|
||||||
|
<column name="locale" type="varchar" size="255" required="true" />
|
||||||
|
<column name="domain" type="varchar" size="255" required="true" />
|
||||||
|
<column name="updated_at" type="timestamp" />
|
||||||
|
|
||||||
|
<index>
|
||||||
|
<index-column name="domain" />
|
||||||
|
</index>
|
||||||
|
<index>
|
||||||
|
<index-column name="locale" />
|
||||||
|
<index-column name="domain" />
|
||||||
|
</index>
|
||||||
|
|
||||||
|
<unique>
|
||||||
|
<unique-column name="key" />
|
||||||
|
<unique-column name="locale" />
|
||||||
|
<unique-column name="domain" />
|
||||||
|
</unique>
|
||||||
|
</table>
|
||||||
|
</database>
|
|
@ -20,10 +20,10 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
|
||||||
*/
|
*/
|
||||||
class TestCase extends \PHPUnit_Framework_TestCase
|
class TestCase extends \PHPUnit_Framework_TestCase
|
||||||
{
|
{
|
||||||
protected function setUp()
|
public static function setUpBeforeClass()
|
||||||
{
|
{
|
||||||
if (!file_exists($file = __DIR__ . '/../vendor/propel/propel1/runtime/lib/Propel.php')) {
|
if (!file_exists($file = __DIR__ . '/../vendor/propel/propel1/runtime/lib/Propel.php')) {
|
||||||
$this->markTestSkipped('Propel is not available.');
|
self::markTestSkipped('Propel is not available.');
|
||||||
}
|
}
|
||||||
|
|
||||||
require_once $file;
|
require_once $file;
|
||||||
|
|
206
Tests/Translation/ModelTranslationTest.php
Normal file
206
Tests/Translation/ModelTranslationTest.php
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Propel\PropelBundle\Tests\Translation;
|
||||||
|
|
||||||
|
use Propel\PropelBundle\Tests\TestCase;
|
||||||
|
use Propel\PropelBundle\Tests\Fixtures\Model\Translation as Entry;
|
||||||
|
|
||||||
|
use Propel\PropelBundle\Translation\ModelTranslation;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\MessageCatalogue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Toni Uebernickel <tuebernickel@gmail.com>
|
||||||
|
*
|
||||||
|
* @covers Propel\PropelBundle\Translation\ModelTranslation
|
||||||
|
*/
|
||||||
|
class ModelTranslationTest extends TestCase
|
||||||
|
{
|
||||||
|
const MODEL_CLASS = 'Propel\PropelBundle\Tests\Fixtures\Model\Translation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \PropelPDO
|
||||||
|
*/
|
||||||
|
protected $con;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->loadPropelQuickBuilder();
|
||||||
|
|
||||||
|
$schema = file_get_contents(__DIR__.'/../Fixtures/translation_schema.xml');
|
||||||
|
|
||||||
|
$builder = new \PropelQuickBuilder();
|
||||||
|
$builder->setSchema($schema);
|
||||||
|
if (class_exists('Propel\PropelBundle\Tests\Fixtures\Model\map\TranslationTableMap')) {
|
||||||
|
$builder->setClassTargets(array());
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->con = $builder->build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegisterResources()
|
||||||
|
{
|
||||||
|
$translation = new Entry();
|
||||||
|
$translation
|
||||||
|
->setKey('example.key')
|
||||||
|
->setMessage('This is an example translation.')
|
||||||
|
->setLocale('en_US')
|
||||||
|
->setDomain('test')
|
||||||
|
->setUpdatedAt(new \DateTime())
|
||||||
|
->save()
|
||||||
|
;
|
||||||
|
|
||||||
|
$resource = $this->getResource();
|
||||||
|
|
||||||
|
$translator = $this->getMock('Symfony\Component\Translation\Translator', array(), array('en_US'));
|
||||||
|
$translator
|
||||||
|
->expects($this->once())
|
||||||
|
->method('addResource')
|
||||||
|
->with('propel', $resource, 'en_US', 'test')
|
||||||
|
;
|
||||||
|
|
||||||
|
$resource->registerResources($translator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsFreshWithoutEntries()
|
||||||
|
{
|
||||||
|
$resource = $this->getResource();
|
||||||
|
|
||||||
|
$this->assertTrue($resource->isFresh(date('U')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsFreshUpdates()
|
||||||
|
{
|
||||||
|
$date = new \DateTime('-2 minutes');
|
||||||
|
|
||||||
|
$translation = new Entry();
|
||||||
|
$translation
|
||||||
|
->setKey('example.key')
|
||||||
|
->setMessage('This is an example translation.')
|
||||||
|
->setLocale('en_US')
|
||||||
|
->setDomain('test')
|
||||||
|
->setUpdatedAt($date)
|
||||||
|
->save()
|
||||||
|
;
|
||||||
|
|
||||||
|
$resource = $this->getResource();
|
||||||
|
|
||||||
|
$timestamp = (int) $date->format('U');
|
||||||
|
|
||||||
|
$this->assertFalse($resource->isFresh($timestamp - 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoadInvalidResource()
|
||||||
|
{
|
||||||
|
$invalidResource = $this->getMock('Symfony\Component\Config\Resource\ResourceInterface');
|
||||||
|
|
||||||
|
$resource = $this->getResource();
|
||||||
|
$catalogue = $resource->load($invalidResource, 'en_US');
|
||||||
|
|
||||||
|
$this->assertEmpty($catalogue->getResources());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoadFiltersLocaleAndDomain()
|
||||||
|
{
|
||||||
|
$date = new \DateTime();
|
||||||
|
|
||||||
|
$translation = new Entry();
|
||||||
|
$translation
|
||||||
|
->setKey('example.key')
|
||||||
|
->setMessage('This is an example translation.')
|
||||||
|
->setLocale('en_US')
|
||||||
|
->setDomain('test')
|
||||||
|
->setUpdatedAt($date)
|
||||||
|
->save()
|
||||||
|
;
|
||||||
|
|
||||||
|
// different locale
|
||||||
|
$translation = new Entry();
|
||||||
|
$translation
|
||||||
|
->setKey('example.key')
|
||||||
|
->setMessage('Das ist eine Beispielübersetzung.')
|
||||||
|
->setLocale('de_DE')
|
||||||
|
->setDomain('test')
|
||||||
|
->setUpdatedAt($date)
|
||||||
|
->save()
|
||||||
|
;
|
||||||
|
|
||||||
|
// different domain
|
||||||
|
$translation = new Entry();
|
||||||
|
$translation
|
||||||
|
->setKey('example.key')
|
||||||
|
->setMessage('This is an example translation.')
|
||||||
|
->setLocale('en_US')
|
||||||
|
->setDomain('test2')
|
||||||
|
->setUpdatedAt($date)
|
||||||
|
->save()
|
||||||
|
;
|
||||||
|
|
||||||
|
$resource = $this->getResource();
|
||||||
|
$catalogue = $resource->load($resource, 'en_US', 'test');
|
||||||
|
|
||||||
|
$this->assertInstanceOf('Symfony\Component\Translation\MessageCatalogue', $catalogue);
|
||||||
|
$this->assertEquals('en_US', $catalogue->getLocale());
|
||||||
|
|
||||||
|
$expected = array(
|
||||||
|
'test' => array(
|
||||||
|
'example.key' => 'This is an example translation.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $catalogue->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDump()
|
||||||
|
{
|
||||||
|
$catalogue = new MessageCatalogue('en_US', array(
|
||||||
|
'test' => array(
|
||||||
|
'example.key' => 'This is an example translation.',
|
||||||
|
),
|
||||||
|
'test2' => array(
|
||||||
|
'example.key' => 'This is an example translation.',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
$resource = $this->getResource();
|
||||||
|
$this->assertEmpty($resource->load($resource, 'en_US', 'test')->all());
|
||||||
|
|
||||||
|
$resource->dump($catalogue);
|
||||||
|
|
||||||
|
$stmt = $this->con->prepare('SELECT `key`, `message`, `locale`, `domain` FROM `translation`;');
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
|
||||||
|
$result[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected = array(
|
||||||
|
array(
|
||||||
|
'key' => 'example.key',
|
||||||
|
'message' => 'This is an example translation.',
|
||||||
|
'locale' => 'en_US',
|
||||||
|
'domain' => 'test',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'example.key',
|
||||||
|
'message' => 'This is an example translation.',
|
||||||
|
'locale' => 'en_US',
|
||||||
|
'domain' => 'test2',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals($expected, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getResource()
|
||||||
|
{
|
||||||
|
return new ModelTranslation(self::MODEL_CLASS, array(
|
||||||
|
'columns' => array(
|
||||||
|
'translation' => 'message',
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
197
Translation/ModelTranslation.php
Normal file
197
Translation/ModelTranslation.php
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Propel\PropelBundle\Translation;
|
||||||
|
|
||||||
|
use Symfony\Component\Config\Resource\ResourceInterface;
|
||||||
|
|
||||||
|
use Symfony\Component\Translation\Dumper\DumperInterface;
|
||||||
|
use Symfony\Component\Translation\Loader\LoaderInterface;
|
||||||
|
use Symfony\Component\Translation\MessageCatalogue;
|
||||||
|
use Symfony\Component\Translation\Translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A translation loader retrieving data from a Propel model.
|
||||||
|
*
|
||||||
|
* @author Toni Uebernickel <tuebernickel@gmail.com>
|
||||||
|
*/
|
||||||
|
class ModelTranslation implements DumperInterface, LoaderInterface, ResourceInterface
|
||||||
|
{
|
||||||
|
protected $className;
|
||||||
|
protected $query;
|
||||||
|
|
||||||
|
protected $options = array(
|
||||||
|
'columns' => array(
|
||||||
|
// The key and its translation ..
|
||||||
|
'key' => 'key',
|
||||||
|
'translation' => 'translation',
|
||||||
|
// .. for the given locale ..
|
||||||
|
'locale' => 'locale',
|
||||||
|
// .. under this domain.
|
||||||
|
'domain' => 'domain',
|
||||||
|
// The datetime of the last update.
|
||||||
|
'updated_at' => 'updated_at',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
private $resourcesStatement;
|
||||||
|
|
||||||
|
public function __construct($className, array $options = array(), \ModelCriteria $query = null)
|
||||||
|
{
|
||||||
|
$this->className = $className;
|
||||||
|
$this->options = array_replace_recursive($this->options, $options);
|
||||||
|
|
||||||
|
if (!$query) {
|
||||||
|
$query = \PropelQuery::from($this->className);
|
||||||
|
}
|
||||||
|
$this->query = $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function registerResources(Translator $translator)
|
||||||
|
{
|
||||||
|
$stmt = $this->getResourcesStatement();
|
||||||
|
|
||||||
|
if (false === $stmt->execute()) {
|
||||||
|
throw new \RuntimeException('Could not fetch translation data from database.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt->bindColumn('locale', $locale);
|
||||||
|
$stmt->bindColumn('domain', $domain);
|
||||||
|
|
||||||
|
while ($stmt->fetch()) {
|
||||||
|
$translator->addResource('propel', $this, $locale, $domain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function load($resource, $locale, $domain = 'messages')
|
||||||
|
{
|
||||||
|
// The loader only accepts itself as a resource.
|
||||||
|
if ($resource !== $this) {
|
||||||
|
return new MessageCatalogue($locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = clone $this->query;
|
||||||
|
$query
|
||||||
|
->filterBy($this->getColumnPhpname('locale'), $locale)
|
||||||
|
->filterBy($this->getColumnPhpname('domain'), $domain)
|
||||||
|
;
|
||||||
|
|
||||||
|
$translations = $query->find();
|
||||||
|
|
||||||
|
$catalogue = new MessageCatalogue($locale);
|
||||||
|
foreach ($translations as $eachTranslation) {
|
||||||
|
$key = $eachTranslation->getByName($this->getColumnPhpname('key'));
|
||||||
|
$message = $eachTranslation->getByName($this->getColumnPhpname('translation'));
|
||||||
|
|
||||||
|
$catalogue->set($key, $message, $domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $catalogue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function dump(MessageCatalogue $messages, $options = array())
|
||||||
|
{
|
||||||
|
$connection = \Propel::getConnection($this->query->getDbName());
|
||||||
|
$connection->beginTransaction();
|
||||||
|
|
||||||
|
$now = new \DateTime();
|
||||||
|
|
||||||
|
$locale = $messages->getLocale();
|
||||||
|
foreach ($messages->getDomains() as $eachDomain) {
|
||||||
|
foreach ($messages->all($eachDomain) as $eachKey => $eachTranslation) {
|
||||||
|
$query = clone $this->query;
|
||||||
|
$query
|
||||||
|
->filterBy($this->getColumnPhpname('locale'), $locale)
|
||||||
|
->filterBy($this->getColumnPhpname('domain'), $eachDomain)
|
||||||
|
->filterBy($this->getColumnPhpname('key'), $eachKey)
|
||||||
|
;
|
||||||
|
|
||||||
|
$translation = $query->findOneOrCreate($connection);
|
||||||
|
$translation->setByName($this->getColumnPhpname('translation'), (string) $eachTranslation);
|
||||||
|
$translation->setByName($this->getColumnPhpname('updated_at'), $now);
|
||||||
|
|
||||||
|
$translation->save($connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$connection->commit()) {
|
||||||
|
$connection->rollBack();
|
||||||
|
|
||||||
|
throw new \RuntimeException(sprintf('An error occurred while committing the transaction. [%s: %s]', $connection->errorCode(), $connection->errorInfo()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function isFresh($timestamp)
|
||||||
|
{
|
||||||
|
$query = clone $this->query;
|
||||||
|
$query->filterBy($this->getColumnPhpname('updated_at'), new \DateTime('@'.$timestamp), \ModelCriteria::GREATER_THAN);
|
||||||
|
|
||||||
|
return !$query->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return sprintf('PropelModelTranslation::%s', $this->className);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getResource()
|
||||||
|
{
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and caches a PDO Statement to receive available resources.
|
||||||
|
*
|
||||||
|
* @return \PDOStatement
|
||||||
|
*/
|
||||||
|
private function getResourcesStatement()
|
||||||
|
{
|
||||||
|
if ($this->resourcesStatement instanceof \PDOStatement) {
|
||||||
|
return $this->resourcesStatement;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = vsprintf('SELECT DISTINCT `%s` AS `locale`, `%s` AS `domain` FROM `%s`', array(
|
||||||
|
// SELECT ..
|
||||||
|
$this->query->getTableMap()->getColumn($this->getColumnname('locale'))->getName(),
|
||||||
|
$this->query->getTableMap()->getColumn($this->getColumnname('domain'))->getName(),
|
||||||
|
// FROM ..
|
||||||
|
$this->query->getTableMap()->getName(),
|
||||||
|
));
|
||||||
|
|
||||||
|
$connection = \Propel::getConnection($this->query->getDbName(), \Propel::CONNECTION_READ);
|
||||||
|
|
||||||
|
$stmt = $connection->prepare($sql);
|
||||||
|
$stmt->setFetchMode(\PDO::FETCH_BOUND);
|
||||||
|
|
||||||
|
$this->resourcesStatement = $stmt;
|
||||||
|
|
||||||
|
return $stmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getColumnname($column)
|
||||||
|
{
|
||||||
|
return $this->options['columns'][$column];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getColumnPhpname($column)
|
||||||
|
{
|
||||||
|
return $this->query->getTableMap()->getColumn($this->getColumnname($column))->getPhpName();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue