This commit is contained in:
Xes
2025-08-14 22:41:49 +02:00
parent 2de81ccc46
commit 8ce45119b6
39774 changed files with 4309466 additions and 0 deletions

View File

@@ -0,0 +1,153 @@
<?php
namespace Gedmo\Translatable\Document\MappedSuperclass;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM;
/**
* Gedmo\Translatable\Document\AbstractPersonalTranslation
*
* @MongoODM\MappedSuperclass
*/
abstract class AbstractPersonalTranslation
{
/**
* @var integer $id
*
* @MongoODM\Id
*/
protected $id;
/**
* @var string $locale
*
* @MongoODM\Field(type="string")
*/
protected $locale;
/**
* Related entity with ManyToOne relation
* must be mapped by user
*/
protected $object;
/**
* @var string $field
*
* @MongoODM\Field(type="string")
*/
protected $field;
/**
* @var string $content
*
* @MongoODM\Field(type="string")
*/
protected $content;
/**
* Get id
*
* @return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* Set locale
*
* @param string $locale
*
* @return static
*/
public function setLocale($locale)
{
$this->locale = $locale;
return $this;
}
/**
* Get locale
*
* @return string
*/
public function getLocale()
{
return $this->locale;
}
/**
* Set field
*
* @param string $field
*
* @return static
*/
public function setField($field)
{
$this->field = $field;
return $this;
}
/**
* Get field
*
* @return string
*/
public function getField()
{
return $this->field;
}
/**
* Set object related
*
* @param object $object
*
* @return static
*/
public function setObject($object)
{
$this->object = $object;
return $this;
}
/**
* Get object related
*
* @return string
*/
public function getObject()
{
return $this->object;
}
/**
* Set content
*
* @param string $content
*
* @return static
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* @return string
*/
public function getContent()
{
return $this->content;
}
}

View File

@@ -0,0 +1,185 @@
<?php
namespace Gedmo\Translatable\Document\MappedSuperclass;
use Doctrine\ODM\MongoDB\Mapping\Annotations as MongoODM;
/**
* Gedmo\Translatable\Document\MappedSuperclass\AbstractTranslation
*
* @MongoODM\MappedSuperclass
*/
abstract class AbstractTranslation
{
/**
* @var integer $id
*
* @MongoODM\Id
*/
protected $id;
/**
* @var string $locale
*
* @MongoODM\Field(type="string")
*/
protected $locale;
/**
* @var string $objectClass
*
* @MongoODM\Field(type="string")
*/
protected $objectClass;
/**
* @var string $field
*
* @MongoODM\Field(type="string")
*/
protected $field;
/**
* @var string $foreignKey
*
* @MongoODM\Field(type="string", name="foreign_key")
*/
protected $foreignKey;
/**
* @var string $content
*
* @MongoODM\Field(type="string")
*/
protected $content;
/**
* Get id
*
* @return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* Set locale
*
* @param string $locale
*
* @return static
*/
public function setLocale($locale)
{
$this->locale = $locale;
return $this;
}
/**
* Get locale
*
* @return string
*/
public function getLocale()
{
return $this->locale;
}
/**
* Set field
*
* @param string $field
*
* @return static
*/
public function setField($field)
{
$this->field = $field;
return $this;
}
/**
* Get field
*
* @return string
*/
public function getField()
{
return $this->field;
}
/**
* Set object class
*
* @param string $objectClass
*
* @return static
*/
public function setObjectClass($objectClass)
{
$this->objectClass = $objectClass;
return $this;
}
/**
* Get objectClass
*
* @return string
*/
public function getObjectClass()
{
return $this->objectClass;
}
/**
* Set foreignKey
*
* @param string $foreignKey
*
* @return static
*/
public function setForeignKey($foreignKey)
{
$this->foreignKey = $foreignKey;
return $this;
}
/**
* Get foreignKey
*
* @return string
*/
public function getForeignKey()
{
return $this->foreignKey;
}
/**
* Set content
*
* @param string $content
*
* @return static
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* @return string
*/
public function getContent()
{
return $this->content;
}
}

View File

@@ -0,0 +1,254 @@
<?php
namespace Gedmo\Translatable\Document\Repository;
use Gedmo\Translatable\TranslatableListener;
use Doctrine\ODM\MongoDB\DocumentRepository;
use Doctrine\ODM\MongoDB\Cursor;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\UnitOfWork;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Gedmo\Tool\Wrapper\MongoDocumentWrapper;
use Gedmo\Translatable\Mapping\Event\Adapter\ODM as TranslatableAdapterODM;
/**
* The TranslationRepository has some useful functions
* to interact with translations.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class TranslationRepository extends DocumentRepository
{
/**
* Current TranslatableListener instance used
* in EntityManager
*
* @var TranslatableListener
*/
private $listener;
/**
* {@inheritdoc}
*/
public function __construct(DocumentManager $dm, UnitOfWork $uow, ClassMetadata $class)
{
if ($class->getReflectionClass()->isSubclassOf('Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation')) {
throw new \Gedmo\Exception\UnexpectedValueException('This repository is useless for personal translations');
}
parent::__construct($dm, $uow, $class);
}
/**
* Makes additional translation of $document $field into $locale
* using $value
*
* @param object $document
* @param string $field
* @param string $locale
* @param mixed $value
*
* @return static
*/
public function translate($document, $field, $locale, $value)
{
$meta = $this->dm->getClassMetadata(get_class($document));
$listener = $this->getTranslatableListener();
$config = $listener->getConfiguration($this->dm, $meta->name);
if (!isset($config['fields']) || !in_array($field, $config['fields'])) {
throw new \Gedmo\Exception\InvalidArgumentException("Document: {$meta->name} does not translate field - {$field}");
}
$modRecordValue = (!$listener->getPersistDefaultLocaleTranslation() && $locale === $listener->getDefaultLocale())
|| $listener->getTranslatableLocale($document, $meta, $this->getDocumentManager()) === $locale
;
if ($modRecordValue) {
$meta->getReflectionProperty($field)->setValue($document, $value);
$this->dm->persist($document);
} else {
if (isset($config['translationClass'])) {
$class = $config['translationClass'];
} else {
$ea = new TranslatableAdapterODM();
$class = $listener->getTranslationClass($ea, $config['useObjectClass']);
}
$foreignKey = $meta->getReflectionProperty($meta->identifier)->getValue($document);
$objectClass = $config['useObjectClass'];
$transMeta = $this->dm->getClassMetadata($class);
$trans = $this->findOneBy(compact('locale', 'field', 'objectClass', 'foreignKey'));
if (!$trans) {
$trans = $transMeta->newInstance();
$transMeta->getReflectionProperty('foreignKey')->setValue($trans, $foreignKey);
$transMeta->getReflectionProperty('objectClass')->setValue($trans, $objectClass);
$transMeta->getReflectionProperty('field')->setValue($trans, $field);
$transMeta->getReflectionProperty('locale')->setValue($trans, $locale);
}
$mapping = $meta->getFieldMapping($field);
$type = $this->getType($mapping['type']);
$transformed = $type->convertToDatabaseValue($value);
$transMeta->getReflectionProperty('content')->setValue($trans, $transformed);
if ($this->dm->getUnitOfWork()->isInIdentityMap($document)) {
$this->dm->persist($trans);
} else {
$oid = spl_object_hash($document);
$listener->addPendingTranslationInsert($oid, $trans);
}
}
return $this;
}
/**
* Loads all translations with all translatable
* fields from the given entity
*
* @param object $document
*
* @return array list of translations in locale groups
*/
public function findTranslations($document)
{
$result = array();
$wrapped = new MongoDocumentWrapper($document, $this->dm);
if ($wrapped->hasValidIdentifier()) {
$documentId = $wrapped->getIdentifier();
$translationMeta = $this->getClassMetadata(); // table inheritance support
$config = $this
->getTranslatableListener()
->getConfiguration($this->dm, $wrapped->getMetadata()->name);
if (!$config) {
return $result;
}
$documentClass = $config['useObjectClass'];
$translationClass = isset($config['translationClass']) ?
$config['translationClass'] :
$translationMeta->rootDocumentName;
$qb = $this->dm->createQueryBuilder($translationClass);
$q = $qb->field('foreignKey')->equals($documentId)
->field('objectClass')->equals($documentClass)
->field('content')->exists(true)->notEqual(null)
->sort('locale', 'asc')
->getQuery();
$q->setHydrate(false);
$data = $q->execute();
if ($data instanceof Cursor) {
$data = $data->toArray();
}
if ($data && is_array($data) && count($data)) {
foreach ($data as $row) {
$result[$row['locale']][$row['field']] = $row['content'];
}
}
}
return $result;
}
/**
* Find the object $class by the translated field.
* Result is the first occurrence of translated field.
* Query can be slow, since there are no indexes on such
* columns
*
* @param string $field
* @param string $value
* @param string $class
*
* @return object - instance of $class or null if not found
*/
public function findObjectByTranslatedField($field, $value, $class)
{
$document = null;
$meta = $this->dm->getClassMetadata($class);
if ($meta->hasField($field)) {
$qb = $this->createQueryBuilder();
$q = $qb->field('field')->equals($field)
->field('objectClass')->equals($meta->rootDocumentName)
->field('content')->equals($value)
->getQuery();
$q->setHydrate(false);
$result = $q->execute();
if ($result instanceof Cursor) {
$result = $result->toArray();
}
$id = count($result) ? $result[0]['foreignKey'] : null;
if ($id) {
$document = $this->dm->find($class, $id);
}
}
return $document;
}
/**
* Loads all translations with all translatable
* fields by a given document primary key
*
* @param mixed $id - primary key value of document
*
* @return array
*/
public function findTranslationsByObjectId($id)
{
$result = array();
if ($id) {
$qb = $this->createQueryBuilder();
$q = $qb->field('foreignKey')->equals($id)
->field('content')->exists(true)->notEqual(null)
->sort('locale', 'asc')
->getQuery();
$q->setHydrate(false);
$data = $q->execute();
if ($data instanceof Cursor) {
$data = $data->toArray();
}
if ($data && is_array($data) && count($data)) {
foreach ($data as $row) {
$result[$row['locale']][$row['field']] = $row['content'];
}
}
}
return $result;
}
/**
* Get the currently used TranslatableListener
*
* @throws \Gedmo\Exception\RuntimeException - if listener is not found
*
* @return TranslatableListener
*/
private function getTranslatableListener()
{
if (!$this->listener) {
foreach ($this->dm->getEventManager()->getListeners() as $event => $listeners) {
foreach ($listeners as $hash => $listener) {
if ($listener instanceof TranslatableListener) {
return $this->listener = $listener;
}
}
}
throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
}
return $this->listener;
}
private function getType($type)
{
// due to change in ODM beta 9
return class_exists('Doctrine\ODM\MongoDB\Types\Type') ? \Doctrine\ODM\MongoDB\Types\Type::getType($type)
: \Doctrine\ODM\MongoDB\Mapping\Types\Type::getType($type);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Gedmo\Translatable\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations\UniqueIndex;
use Doctrine\ODM\MongoDB\Mapping\Annotations\Index;
/**
* Gedmo\Translatable\Document\Translation
*
* @Document(repositoryClass="Gedmo\Translatable\Document\Repository\TranslationRepository")
* @UniqueIndex(name="lookup_unique_idx", keys={
* "locale" = "asc",
* "object_class" = "asc",
* "foreign_key" = "asc",
* "field" = "asc"
* })
* @Index(name="translations_lookup_idx", keys={
* "locale" = "asc",
* "object_class" = "asc",
* "foreign_key" = "asc"
* })
*/
class Translation extends MappedSuperclass\AbstractTranslation
{
/**
* All required columns are mapped through inherited superclass
*/
}

View File

@@ -0,0 +1,155 @@
<?php
namespace Gedmo\Translatable\Entity\MappedSuperclass;
use Doctrine\ORM\Mapping as ORM;
/**
* Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation
*
* @ORM\MappedSuperclass
*/
abstract class AbstractPersonalTranslation
{
/**
* @var integer $id
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* @var string $locale
*
* @ORM\Column(type="string", length=8)
*/
protected $locale;
/**
* @var string $field
*
* @ORM\Column(type="string", length=32)
*/
protected $field;
/**
* Related entity with ManyToOne relation
* must be mapped by user
*/
protected $object;
/**
* @var string $content
*
* @ORM\Column(type="text", nullable=true)
*/
protected $content;
/**
* Get id
*
* @return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* Set locale
*
* @param string $locale
*
* @return static
*/
public function setLocale($locale)
{
$this->locale = $locale;
return $this;
}
/**
* Get locale
*
* @return string
*/
public function getLocale()
{
return $this->locale;
}
/**
* Set field
*
* @param string $field
*
* @return static
*/
public function setField($field)
{
$this->field = $field;
return $this;
}
/**
* Get field
*
* @return string $field
*/
public function getField()
{
return $this->field;
}
/**
* Set object related
*
* @param object $object
*
* @return static
*/
public function setObject($object)
{
$this->object = $object;
return $this;
}
/**
* Get related object
*
* @return object
*/
public function getObject()
{
return $this->object;
}
/**
* Set content
*
* @param string $content
*
* @return static
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* @return string
*/
public function getContent()
{
return $this->content;
}
}

View File

@@ -0,0 +1,187 @@
<?php
namespace Gedmo\Translatable\Entity\MappedSuperclass;
use Doctrine\ORM\Mapping as ORM;
/**
* Gedmo\Translatable\Entity\MappedSuperclass\AbstractTranslation
*
* @ORM\MappedSuperclass
*/
abstract class AbstractTranslation
{
/**
* @var integer $id
*
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* @var string $locale
*
* @ORM\Column(type="string", length=8)
*/
protected $locale;
/**
* @var string $objectClass
*
* @ORM\Column(name="object_class", type="string", length=255)
*/
protected $objectClass;
/**
* @var string $field
*
* @ORM\Column(type="string", length=32)
*/
protected $field;
/**
* @var string $foreignKey
*
* @ORM\Column(name="foreign_key", type="string", length=64)
*/
protected $foreignKey;
/**
* @var string $content
*
* @ORM\Column(type="text", nullable=true)
*/
protected $content;
/**
* Get id
*
* @return integer $id
*/
public function getId()
{
return $this->id;
}
/**
* Set locale
*
* @param string $locale
*
* @return static
*/
public function setLocale($locale)
{
$this->locale = $locale;
return $this;
}
/**
* Get locale
*
* @return string
*/
public function getLocale()
{
return $this->locale;
}
/**
* Set field
*
* @param string $field
*
* @return static
*/
public function setField($field)
{
$this->field = $field;
return $this;
}
/**
* Get field
*
* @return string
*/
public function getField()
{
return $this->field;
}
/**
* Set object class
*
* @param string $objectClass
*
* @return static
*/
public function setObjectClass($objectClass)
{
$this->objectClass = $objectClass;
return $this;
}
/**
* Get objectClass
*
* @return string
*/
public function getObjectClass()
{
return $this->objectClass;
}
/**
* Set foreignKey
*
* @param string $foreignKey
*
* @return static
*/
public function setForeignKey($foreignKey)
{
$this->foreignKey = $foreignKey;
return $this;
}
/**
* Get foreignKey
*
* @return string
*/
public function getForeignKey()
{
return $this->foreignKey;
}
/**
* Set content
*
* @param string $content
*
* @return static
*/
public function setContent($content)
{
$this->content = $content;
return $this;
}
/**
* Get content
*
* @return string
*/
public function getContent()
{
return $this->content;
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace Gedmo\Translatable\Entity\Repository;
use Doctrine\ORM\EntityManagerInterface;
use Gedmo\Translatable\TranslatableListener;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query;
use Doctrine\ORM\Mapping\ClassMetadata;
use Gedmo\Tool\Wrapper\EntityWrapper;
use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableAdapterORM;
use Doctrine\DBAL\Types\Type;
/**
* The TranslationRepository has some useful functions
* to interact with translations.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class TranslationRepository extends EntityRepository
{
/**
* Current TranslatableListener instance used
* in EntityManager
*
* @var TranslatableListener
*/
private $listener;
/**
* {@inheritdoc}
*/
public function __construct(EntityManagerInterface $em, ClassMetadata $class)
{
if ($class->getReflectionClass()->isSubclassOf('Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation')) {
throw new \Gedmo\Exception\UnexpectedValueException('This repository is useless for personal translations');
}
parent::__construct($em, $class);
}
/**
* Makes additional translation of $entity $field into $locale
* using $value
*
* @param object $entity
* @param string $field
* @param string $locale
* @param mixed $value
*
* @throws \Gedmo\Exception\InvalidArgumentException
*
* @return static
*/
public function translate($entity, $field, $locale, $value)
{
$meta = $this->_em->getClassMetadata(get_class($entity));
$listener = $this->getTranslatableListener();
$config = $listener->getConfiguration($this->_em, $meta->name);
if (!isset($config['fields']) || !in_array($field, $config['fields'])) {
throw new \Gedmo\Exception\InvalidArgumentException("Entity: {$meta->name} does not translate field - {$field}");
}
$needsPersist = true;
if ($locale === $listener->getTranslatableLocale($entity, $meta, $this->getEntityManager())) {
$meta->getReflectionProperty($field)->setValue($entity, $value);
$this->_em->persist($entity);
} else {
if (isset($config['translationClass'])) {
$class = $config['translationClass'];
} else {
$ea = new TranslatableAdapterORM();
$class = $listener->getTranslationClass($ea, $config['useObjectClass']);
}
$foreignKey = $meta->getReflectionProperty($meta->getSingleIdentifierFieldName())->getValue($entity);
$objectClass = $config['useObjectClass'];
$transMeta = $this->_em->getClassMetadata($class);
$trans = $this->findOneBy(compact('locale', 'objectClass', 'field', 'foreignKey'));
if (!$trans) {
$trans = $transMeta->newInstance();
$transMeta->getReflectionProperty('foreignKey')->setValue($trans, $foreignKey);
$transMeta->getReflectionProperty('objectClass')->setValue($trans, $objectClass);
$transMeta->getReflectionProperty('field')->setValue($trans, $field);
$transMeta->getReflectionProperty('locale')->setValue($trans, $locale);
}
if ($listener->getDefaultLocale() != $listener->getTranslatableLocale($entity, $meta, $this->getEntityManager()) &&
$locale === $listener->getDefaultLocale()) {
$listener->setTranslationInDefaultLocale(spl_object_hash($entity), $field, $trans);
$needsPersist = $listener->getPersistDefaultLocaleTranslation();
}
$type = Type::getType($meta->getTypeOfField($field));
$transformed = $type->convertToDatabaseValue($value, $this->_em->getConnection()->getDatabasePlatform());
$transMeta->getReflectionProperty('content')->setValue($trans, $transformed);
if ($needsPersist) {
if ($this->_em->getUnitOfWork()->isInIdentityMap($entity)) {
$this->_em->persist($trans);
} else {
$oid = spl_object_hash($entity);
$listener->addPendingTranslationInsert($oid, $trans);
}
}
}
return $this;
}
/**
* Loads all translations with all translatable
* fields from the given entity
*
* @param object $entity Must implement Translatable
*
* @return array list of translations in locale groups
*/
public function findTranslations($entity)
{
$result = array();
$wrapped = new EntityWrapper($entity, $this->_em);
if ($wrapped->hasValidIdentifier()) {
$entityId = $wrapped->getIdentifier();
$config = $this
->getTranslatableListener()
->getConfiguration($this->_em, $wrapped->getMetadata()->name);
if (!$config) {
return $result;
}
$entityClass = $config['useObjectClass'];
$translationMeta = $this->getClassMetadata(); // table inheritance support
$translationClass = isset($config['translationClass']) ?
$config['translationClass'] :
$translationMeta->rootEntityName;
$qb = $this->_em->createQueryBuilder();
$qb->select('trans.content, trans.field, trans.locale')
->from($translationClass, 'trans')
->where('trans.foreignKey = :entityId', 'trans.objectClass = :entityClass')
->orderBy('trans.locale');
$q = $qb->getQuery();
$data = $q->execute(
compact('entityId', 'entityClass'),
Query::HYDRATE_ARRAY
);
if ($data && is_array($data) && count($data)) {
foreach ($data as $row) {
$result[$row['locale']][$row['field']] = $row['content'];
}
}
}
return $result;
}
/**
* Find the entity $class by the translated field.
* Result is the first occurrence of translated field.
* Query can be slow, since there are no indexes on such
* columns
*
* @param string $field
* @param string $value
* @param string $class
*
* @return object - instance of $class or null if not found
*/
public function findObjectByTranslatedField($field, $value, $class)
{
$entity = null;
$meta = $this->_em->getClassMetadata($class);
$translationMeta = $this->getClassMetadata(); // table inheritance support
if ($meta->hasField($field)) {
$dql = "SELECT trans.foreignKey FROM {$translationMeta->rootEntityName} trans";
$dql .= ' WHERE trans.objectClass = :class';
$dql .= ' AND trans.field = :field';
$dql .= ' AND trans.content = :value';
$q = $this->_em->createQuery($dql);
$q->setParameters(compact('class', 'field', 'value'));
$q->setMaxResults(1);
$result = $q->getArrayResult();
$id = count($result) ? $result[0]['foreignKey'] : null;
if ($id) {
$entity = $this->_em->find($class, $id);
}
}
return $entity;
}
/**
* Loads all translations with all translatable
* fields by a given entity primary key
*
* @param mixed $id - primary key value of an entity
*
* @return array
*/
public function findTranslationsByObjectId($id)
{
$result = array();
if ($id) {
$translationMeta = $this->getClassMetadata(); // table inheritance support
$qb = $this->_em->createQueryBuilder();
$qb->select('trans.content, trans.field, trans.locale')
->from($translationMeta->rootEntityName, 'trans')
->where('trans.foreignKey = :entityId')
->orderBy('trans.locale');
$q = $qb->getQuery();
$data = $q->execute(
array('entityId' => $id),
Query::HYDRATE_ARRAY
);
if ($data && is_array($data) && count($data)) {
foreach ($data as $row) {
$result[$row['locale']][$row['field']] = $row['content'];
}
}
}
return $result;
}
/**
* Get the currently used TranslatableListener
*
* @throws \Gedmo\Exception\RuntimeException - if listener is not found
*
* @return TranslatableListener
*/
private function getTranslatableListener()
{
if (!$this->listener) {
foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) {
foreach ($listeners as $hash => $listener) {
if ($listener instanceof TranslatableListener) {
return $this->listener = $listener;
}
}
}
throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
}
return $this->listener;
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Gedmo\Translatable\Entity;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\Index;
use Doctrine\ORM\Mapping\UniqueConstraint;
use Doctrine\ORM\Mapping\Entity;
/**
* Gedmo\Translatable\Entity\Translation
*
* @Table(
* name="ext_translations",
* options={"row_format":"DYNAMIC"},
* indexes={@Index(name="translations_lookup_idx", columns={
* "locale", "object_class", "foreign_key"
* })},
* uniqueConstraints={@UniqueConstraint(name="lookup_unique_idx", columns={
* "locale", "object_class", "field", "foreign_key"
* })}
* )
* @Entity(repositoryClass="Gedmo\Translatable\Entity\Repository\TranslationRepository")
*/
class Translation extends MappedSuperclass\AbstractTranslation
{
/**
* All required columns are mapped through inherited superclass
*/
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Gedmo\Translatable\Hydrator\ORM;
use Gedmo\Translatable\TranslatableListener;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator as BaseObjectHydrator;
/**
* If query uses TranslationQueryWalker and is hydrating
* objects - when it requires this custom object hydrator
* in order to skip onLoad event from triggering retranslation
* of the fields
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class ObjectHydrator extends BaseObjectHydrator
{
/**
* State of skipOnLoad for listener between hydrations
*
* @see ObjectHydrator::prepare()
* @see ObjectHydrator::cleanup()
*
* @var bool
*/
private $savedSkipOnLoad;
/**
* {@inheritdoc}
*/
protected function prepare()
{
$listener = $this->getTranslatableListener();
$this->savedSkipOnLoad = $listener->isSkipOnLoad();
$listener->setSkipOnLoad(true);
parent::prepare();
}
/**
* {@inheritdoc}
*/
protected function cleanup()
{
parent::cleanup();
$listener = $this->getTranslatableListener();
$listener->setSkipOnLoad($this->savedSkipOnLoad !== null ? $this->savedSkipOnLoad : false);
}
/**
* Get the currently used TranslatableListener
*
* @throws \Gedmo\Exception\RuntimeException - if listener is not found
*
* @return TranslatableListener
*/
protected function getTranslatableListener()
{
$translatableListener = null;
foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) {
foreach ($listeners as $hash => $listener) {
if ($listener instanceof TranslatableListener) {
$translatableListener = $listener;
break;
}
}
if ($translatableListener) {
break;
}
}
if (is_null($translatableListener)) {
throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
}
return $translatableListener;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Gedmo\Translatable\Hydrator\ORM;
use Gedmo\Translatable\TranslatableListener;
use Doctrine\ORM\Internal\Hydration\SimpleObjectHydrator as BaseSimpleObjectHydrator;
/**
* If query uses TranslationQueryWalker and is hydrating
* objects - when it requires this custom object hydrator
* in order to skip onLoad event from triggering retranslation
* of the fields
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class SimpleObjectHydrator extends BaseSimpleObjectHydrator
{
/**
* State of skipOnLoad for listener between hydrations
*
* @see SimpleObjectHydrator::prepare()
* @see SimpleObjectHydrator::cleanup()
*
* @var bool
*/
private $savedSkipOnLoad;
/**
* {@inheritdoc}
*/
protected function prepare()
{
$listener = $this->getTranslatableListener();
$this->savedSkipOnLoad = $listener->isSkipOnLoad();
$listener->setSkipOnLoad(true);
parent::prepare();
}
/**
* {@inheritdoc}
*/
protected function cleanup()
{
parent::cleanup();
$listener = $this->getTranslatableListener();
$listener->setSkipOnLoad($this->savedSkipOnLoad !== null ? $this->savedSkipOnLoad : false);
}
/**
* Get the currently used TranslatableListener
*
* @throws \Gedmo\Exception\RuntimeException - if listener is not found
*
* @return TranslatableListener
*/
protected function getTranslatableListener()
{
$translatableListener = null;
foreach ($this->_em->getEventManager()->getListeners() as $event => $listeners) {
foreach ($listeners as $hash => $listener) {
if ($listener instanceof TranslatableListener) {
$translatableListener = $listener;
break;
}
}
if ($translatableListener) {
break;
}
}
if (is_null($translatableListener)) {
throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
}
return $translatableListener;
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Gedmo\Translatable\Mapping\Driver;
use Gedmo\Mapping\Driver\AbstractAnnotationDriver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is an annotation mapping driver for Translatable
* behavioral extension. Used for extraction of extended
* metadata from Annotations specifically for Translatable
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Annotation extends AbstractAnnotationDriver
{
/**
* Annotation to identity translation entity to be used for translation storage
*/
const ENTITY_CLASS = 'Gedmo\\Mapping\\Annotation\\TranslationEntity';
/**
* Annotation to identify field as translatable
*/
const TRANSLATABLE = 'Gedmo\\Mapping\\Annotation\\Translatable';
/**
* Annotation to identify field which can store used locale or language
* alias is LANGUAGE
*/
const LOCALE = 'Gedmo\\Mapping\\Annotation\\Locale';
/**
* Annotation to identify field which can store used locale or language
* alias is LOCALE
*/
const LANGUAGE = 'Gedmo\\Mapping\\Annotation\\Language';
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$class = $this->getMetaReflectionClass($meta);
// class annotations
if ($annot = $this->reader->getClassAnnotation($class, self::ENTITY_CLASS)) {
if (!$cl = $this->getRelatedClassName($meta, $annot->class)) {
throw new InvalidMappingException("Translation class: {$annot->class} does not exist.");
}
$config['translationClass'] = $cl;
}
// property annotations
foreach ($class->getProperties() as $property) {
if ($meta->isMappedSuperclass && !$property->isPrivate() ||
$meta->isInheritedField($property->name) ||
isset($meta->associationMappings[$property->name]['inherited'])
) {
continue;
}
// translatable property
if ($translatable = $this->reader->getPropertyAnnotation($property, self::TRANSLATABLE)) {
$field = $property->getName();
if (!$meta->hasField($field)) {
throw new InvalidMappingException("Unable to find translatable [{$field}] as mapped property in entity - {$meta->name}");
}
// fields cannot be overrided and throws mapping exception
$config['fields'][] = $field;
if (isset($translatable->fallback)) {
$config['fallback'][$field] = $translatable->fallback;
}
}
// locale property
if ($this->reader->getPropertyAnnotation($property, self::LOCALE)) {
$field = $property->getName();
if ($meta->hasField($field)) {
throw new InvalidMappingException("Locale field [{$field}] should not be mapped as column property in entity - {$meta->name}, since it makes no sense");
}
$config['locale'] = $field;
} elseif ($this->reader->getPropertyAnnotation($property, self::LANGUAGE)) {
$field = $property->getName();
if ($meta->hasField($field)) {
throw new InvalidMappingException("Language field [{$field}] should not be mapped as column property in entity - {$meta->name}, since it makes no sense");
}
$config['locale'] = $field;
}
}
// Embedded entity
if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) {
foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) {
if ($meta->isInheritedEmbeddedClass($propertyName)) {
continue;
}
$embeddedClass = new \ReflectionClass($embeddedClassInfo['class']);
foreach ($embeddedClass->getProperties() as $embeddedProperty) {
if ($translatable = $this->reader->getPropertyAnnotation($embeddedProperty, self::TRANSLATABLE)) {
$field = $propertyName . '.' . $embeddedProperty->getName();
$config['fields'][] = $field;
if (isset($translatable->fallback)) {
$config['fallback'][$field] = $translatable->fallback;
}
}
}
}
}
if (!$meta->isMappedSuperclass && $config) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->name}");
}
}
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Gedmo\Translatable\Mapping\Driver;
use Gedmo\Mapping\Driver\Xml as BaseXml;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a xml mapping driver for Translatable
* behavioral extension. Used for extraction of extended
* metadata from xml specifically for Translatable
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @author Miha Vrhovnik <miha.vrhovnik@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Xml extends BaseXml
{
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
/**
* @var \SimpleXmlElement $xml
*/
$xml = $this->_getMapping($meta->name);
$xmlDoctrine = $xml;
$xml = $xml->children(self::GEDMO_NAMESPACE_URI);
if (($xmlDoctrine->getName() == 'entity' || $xmlDoctrine->getName() == 'mapped-superclass')) {
if ($xml->count() && isset($xml->translation)) {
/**
* @var \SimpleXmlElement $data
*/
$data = $xml->translation;
if ($this->_isAttributeSet($data, 'locale')) {
$config['locale'] = $this->_getAttribute($data, 'locale');
} elseif ($this->_isAttributeSet($data, 'language')) {
$config['locale'] = $this->_getAttribute($data, 'language');
}
if ($this->_isAttributeSet($data, 'entity')) {
$entity = $this->_getAttribute($data, 'entity');
if (!$cl = $this->getRelatedClassName($meta, $entity)) {
throw new InvalidMappingException("Translation entity class: {$entity} does not exist.");
}
$config['translationClass'] = $cl;
}
}
}
if (property_exists($meta, 'embeddedClasses') && $meta->embeddedClasses) {
foreach ($meta->embeddedClasses as $propertyName => $embeddedClassInfo) {
if ($meta->isInheritedEmbeddedClass($propertyName)) {
continue;
}
$xmlEmbeddedClass = $this->_getMapping($embeddedClassInfo['class']);
$this->inspectElementsForTranslatableFields($xmlEmbeddedClass, $config, $propertyName);
}
}
if ($xmlDoctrine->{'attribute-overrides'}->count() > 0) {
foreach ($xmlDoctrine->{'attribute-overrides'}->{'attribute-override'} as $overrideMapping) {
$this->buildFieldConfiguration($this->_getAttribute($overrideMapping, 'name'), $overrideMapping->field, $config);
}
}
$this->inspectElementsForTranslatableFields($xmlDoctrine, $config);
if (!$meta->isMappedSuperclass && $config) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->name}");
}
}
}
private function inspectElementsForTranslatableFields(\SimpleXMLElement $xml, array &$config, $prefix = null)
{
if (!isset($xml->field)) {
return;
}
foreach ($xml->field as $mapping) {
$mappingDoctrine = $mapping;
$fieldName = $this->_getAttribute($mappingDoctrine, 'name');
if ($prefix !== null) {
$fieldName = $prefix . '.' . $fieldName;
}
$this->buildFieldConfiguration($fieldName, $mapping, $config);
}
}
private function buildFieldConfiguration($fieldName, \SimpleXMLElement $mapping, array &$config)
{
$mapping = $mapping->children(self::GEDMO_NAMESPACE_URI);
if ($mapping->count() > 0 && isset($mapping->translatable)) {
$config['fields'][] = $fieldName;
/** @var \SimpleXmlElement $data */
$data = $mapping->translatable;
if ($this->_isAttributeSet($data, 'fallback')) {
$config['fallback'][$fieldName] = 'true' == $this->_getAttribute($data, 'fallback') ? true : false;
}
}
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Gedmo\Translatable\Mapping\Driver;
use Gedmo\Mapping\Driver\File;
use Gedmo\Mapping\Driver;
use Gedmo\Exception\InvalidMappingException;
/**
* This is a yaml mapping driver for Translatable
* behavioral extension. Used for extraction of extended
* metadata from yaml specifically for Translatable
* extension.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class Yaml extends File implements Driver
{
/**
* File extension
* @var string
*/
protected $_extension = '.dcm.yml';
/**
* {@inheritDoc}
*/
public function readExtendedMetadata($meta, array &$config)
{
$mapping = $this->_getMapping($meta->name);
if (isset($mapping['gedmo'])) {
$classMapping = $mapping['gedmo'];
if (isset($classMapping['translation']['entity'])) {
$translationEntity = $classMapping['translation']['entity'];
if (!$cl = $this->getRelatedClassName($meta, $translationEntity)) {
throw new InvalidMappingException("Translation entity class: {$translationEntity} does not exist.");
}
$config['translationClass'] = $cl;
}
if (isset($classMapping['translation']['locale'])) {
$config['locale'] = $classMapping['translation']['locale'];
} elseif (isset($classMapping['translation']['language'])) {
$config['locale'] = $classMapping['translation']['language'];
}
}
if (isset($mapping['fields'])) {
foreach ($mapping['fields'] as $field => $fieldMapping) {
$this->buildFieldConfiguration($field, $fieldMapping, $config);
}
}
if (isset($mapping['attributeOverride'])) {
foreach ($mapping['attributeOverride'] as $field => $overrideMapping) {
$this->buildFieldConfiguration($field, $overrideMapping, $config);
}
}
if (!$meta->isMappedSuperclass && $config) {
if (is_array($meta->identifier) && count($meta->identifier) > 1) {
throw new InvalidMappingException("Translatable does not support composite identifiers in class - {$meta->name}");
}
}
}
/**
* {@inheritDoc}
*/
protected function _loadMappingFile($file)
{
return \Symfony\Component\Yaml\Yaml::parse(file_get_contents($file));
}
private function buildFieldConfiguration($field, $fieldMapping, array &$config)
{
if (is_array($fieldMapping) && isset($fieldMapping['gedmo'])) {
if (in_array('translatable', $fieldMapping['gedmo']) || isset($fieldMapping['gedmo']['translatable'])) {
// fields cannot be overrided and throws mapping exception
$config['fields'][] = $field;
if (isset($fieldMapping['gedmo']['translatable']['fallback'])) {
$config['fallback'][$field] = $fieldMapping['gedmo']['translatable']['fallback'];
}
}
}
}
}

View File

@@ -0,0 +1,205 @@
<?php
namespace Gedmo\Translatable\Mapping\Event\Adapter;
use Gedmo\Mapping\Event\Adapter\ODM as BaseAdapterODM;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
use Doctrine\ODM\MongoDB\Cursor;
use Gedmo\Translatable\Mapping\Event\TranslatableAdapter;
/**
* Doctrine event adapter for ODM adapted
* for Translatable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ODM extends BaseAdapterODM implements TranslatableAdapter
{
/**
* {@inheritDoc}
*/
public function usesPersonalTranslation($translationClassName)
{
return $this
->getObjectManager()
->getClassMetadata($translationClassName)
->getReflectionClass()
->isSubclassOf('Gedmo\Translatable\Document\MappedSuperclass\AbstractPersonalTranslation')
;
}
/**
* {@inheritDoc}
*/
public function getDefaultTranslationClass()
{
return 'Gedmo\\Translatable\\Document\\Translation';
}
/**
* {@inheritDoc}
*/
public function loadTranslations($object, $translationClass, $locale, $objectClass)
{
$dm = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $dm);
$result = array();
if ($this->usesPersonalTranslation($translationClass)) {
// first try to load it using collection
foreach ($wrapped->getMetadata()->fieldMappings as $mapping) {
$isRightCollection = isset($mapping['association'])
&& $mapping['association'] === ClassMetadataInfo::REFERENCE_MANY
&& $mapping['targetDocument'] === $translationClass
&& $mapping['mappedBy'] === 'object'
;
if ($isRightCollection) {
$collection = $wrapped->getPropertyValue($mapping['fieldName']);
foreach ($collection as $trans) {
if ($trans->getLocale() === $locale) {
$result[] = array(
'field' => $trans->getField(),
'content' => $trans->getContent(),
);
}
}
return $result;
}
}
$q = $dm
->createQueryBuilder($translationClass)
->field('object.$id')->equals($wrapped->getIdentifier())
->field('locale')->equals($locale)
->getQuery()
;
} else {
// load translated content for all translatable fields
// construct query
$q = $dm
->createQueryBuilder($translationClass)
->field('foreignKey')->equals($wrapped->getIdentifier())
->field('locale')->equals($locale)
->field('objectClass')->equals($objectClass)
->getQuery()
;
}
$q->setHydrate(false);
$result = $q->execute();
if ($result instanceof Cursor) {
$result = $result->toArray();
}
return $result;
}
/**
* {@inheritDoc}
*/
public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass)
{
$dm = $this->getObjectManager();
$qb = $dm
->createQueryBuilder($translationClass)
->field('locale')->equals($locale)
->field('field')->equals($field)
->limit(1)
;
if ($this->usesPersonalTranslation($translationClass)) {
$qb->field('object.$id')->equals($wrapped->getIdentifier());
} else {
$qb->field('foreignKey')->equals($wrapped->getIdentifier());
$qb->field('objectClass')->equals($objectClass);
}
$q = $qb->getQuery();
$result = $q->execute();
if ($result instanceof Cursor) {
$result = current($result->toArray());
}
return $result;
}
/**
* {@inheritDoc}
*/
public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass)
{
$dm = $this->getObjectManager();
$qb = $dm
->createQueryBuilder($transClass)
->remove()
;
if ($this->usesPersonalTranslation($transClass)) {
$qb->field('object.$id')->equals($wrapped->getIdentifier());
} else {
$qb->field('foreignKey')->equals($wrapped->getIdentifier());
$qb->field('objectClass')->equals($objectClass);
}
$q = $qb->getQuery();
return $q->execute();
}
/**
* {@inheritDoc}
*/
public function insertTranslationRecord($translation)
{
$dm = $this->getObjectManager();
$meta = $dm->getClassMetadata(get_class($translation));
$collection = $dm->getDocumentCollection($meta->name);
$data = array();
foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) {
if (!$meta->isIdentifier($fieldName)) {
$data[$meta->fieldMappings[$fieldName]['name']] = $reflProp->getValue($translation);
}
}
if (!$collection->insert($data)) {
throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record');
}
}
/**
* {@inheritDoc}
*/
public function getTranslationValue($object, $field, $value = false)
{
$dm = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $dm);
$meta = $wrapped->getMetadata();
$mapping = $meta->getFieldMapping($field);
$type = $this->getType($mapping['type']);
if ($value === false) {
$value = $wrapped->getPropertyValue($field);
}
return $type->convertToDatabaseValue($value);
}
/**
* {@inheritDoc}
*/
public function setTranslationValue($object, $field, $value)
{
$dm = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $dm);
$meta = $wrapped->getMetadata();
$mapping = $meta->getFieldMapping($field);
$type = $this->getType($mapping['type']);
$value = $type->convertToPHPValue($value);
$wrapped->setPropertyValue($field, $value);
}
private function getType($type)
{
// due to change in ODM beta 9
return class_exists('Doctrine\ODM\MongoDB\Types\Type') ? \Doctrine\ODM\MongoDB\Types\Type::getType($type)
: \Doctrine\ODM\MongoDB\Mapping\Types\Type::getType($type);
}
}

View File

@@ -0,0 +1,260 @@
<?php
namespace Gedmo\Translatable\Mapping\Event\Adapter;
use Doctrine\Common\Proxy\Proxy;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Gedmo\Mapping\Event\Adapter\ORM as BaseAdapterORM;
use Gedmo\Translatable\Mapping\Event\TranslatableAdapter;
use Gedmo\Tool\Wrapper\AbstractWrapper;
/**
* Doctrine event adapter for ORM adapted
* for Translatable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
final class ORM extends BaseAdapterORM implements TranslatableAdapter
{
/**
* {@inheritDoc}
*/
public function usesPersonalTranslation($translationClassName)
{
return $this
->getObjectManager()
->getClassMetadata($translationClassName)
->getReflectionClass()
->isSubclassOf('Gedmo\Translatable\Entity\MappedSuperclass\AbstractPersonalTranslation')
;
}
/**
* {@inheritDoc}
*/
public function getDefaultTranslationClass()
{
return 'Gedmo\\Translatable\\Entity\\Translation';
}
/**
* {@inheritDoc}
*/
public function loadTranslations($object, $translationClass, $locale, $objectClass)
{
$em = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $em);
$result = array();
if ($this->usesPersonalTranslation($translationClass)) {
// first try to load it using collection
$found = false;
foreach ($wrapped->getMetadata()->associationMappings as $assoc) {
$isRightCollection = $assoc['targetEntity'] === $translationClass
&& $assoc['mappedBy'] === 'object'
&& $assoc['type'] === ClassMetadataInfo::ONE_TO_MANY
;
if ($isRightCollection) {
$collection = $wrapped->getPropertyValue($assoc['fieldName']);
foreach ($collection as $trans) {
if ($trans->getLocale() === $locale) {
$result[] = array(
'field' => $trans->getField(),
'content' => $trans->getContent(),
);
}
}
$found = true;
break;
}
}
// if collection is not set, fetch it through relation
if (!$found) {
$dql = 'SELECT t.content, t.field FROM '.$translationClass.' t';
$dql .= ' WHERE t.locale = :locale';
$dql .= ' AND t.object = :object';
$q = $em->createQuery($dql);
$q->setParameters(compact('object', 'locale'));
$result = $q->getArrayResult();
}
} else {
// load translated content for all translatable fields
$objectId = $this->foreignKey($wrapped->getIdentifier(), $translationClass);
// construct query
$dql = 'SELECT t.content, t.field FROM '.$translationClass.' t';
$dql .= ' WHERE t.foreignKey = :objectId';
$dql .= ' AND t.locale = :locale';
$dql .= ' AND t.objectClass = :objectClass';
// fetch results
$q = $em->createQuery($dql);
$q->setParameters(compact('objectId', 'locale', 'objectClass'));
$result = $q->getArrayResult();
}
return $result;
}
/**
* Transforms foreigh key of translation to appropriate PHP value
* to prevent database level cast
*
* @param $key - foreign key value
* @param $className - translation class name
* @return transformed foreign key
*/
private function foreignKey($key, $className)
{
$em = $this->getObjectManager();
$meta = $em->getClassMetadata($className);
$type = Type::getType($meta->getTypeOfField('foreignKey'));
switch ($type->getName()) {
case Type::BIGINT:
case Type::INTEGER:
case Type::SMALLINT:
return intval($key);
default:
return (string)$key;
}
}
/**
* {@inheritDoc}
*/
public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass)
{
$em = $this->getObjectManager();
// first look in identityMap, will save one SELECT query
foreach ($em->getUnitOfWork()->getIdentityMap() as $className => $objects) {
if ($className === $translationClass) {
foreach ($objects as $trans) {
$isRequestedTranslation = !$trans instanceof Proxy
&& $trans->getLocale() === $locale
&& $trans->getField() === $field
;
if ($isRequestedTranslation) {
if ($this->usesPersonalTranslation($translationClass)) {
$isRequestedTranslation = $trans->getObject() === $wrapped->getObject();
} else {
$objectId = $this->foreignKey($wrapped->getIdentifier(), $translationClass);
$isRequestedTranslation = $trans->getForeignKey() === $objectId
&& $trans->getObjectClass() === $wrapped->getMetadata()->name
;
}
}
if ($isRequestedTranslation) {
return $trans;
}
}
}
}
$qb = $em->createQueryBuilder();
$qb->select('trans')
->from($translationClass, 'trans')
->where(
'trans.locale = :locale',
'trans.field = :field'
)
;
$qb->setParameters(compact('locale', 'field'));
if ($this->usesPersonalTranslation($translationClass)) {
$qb->andWhere('trans.object = :object');
if ($wrapped->getIdentifier()) {
$qb->setParameter('object', $wrapped->getObject());
} else {
$qb->setParameter('object', null);
}
} else {
$qb->andWhere('trans.foreignKey = :objectId');
$qb->andWhere('trans.objectClass = :objectClass');
$qb->setParameter('objectId', $this->foreignKey($wrapped->getIdentifier(), $translationClass));
$qb->setParameter('objectClass', $objectClass);
}
$q = $qb->getQuery();
$q->setMaxResults(1);
$result = $q->getResult();
if ($result) {
return array_shift($result);
}
return null;
}
/**
* {@inheritDoc}
*/
public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass)
{
$qb = $this
->getObjectManager()
->createQueryBuilder()
->delete($transClass, 'trans')
;
if ($this->usesPersonalTranslation($transClass)) {
$qb->where('trans.object = :object');
$qb->setParameter('object', $wrapped->getObject());
} else {
$qb->where(
'trans.foreignKey = :objectId',
'trans.objectClass = :class'
);
$qb->setParameter('objectId', $this->foreignKey($wrapped->getIdentifier(), $transClass));
$qb->setParameter('class', $objectClass);
}
return $qb->getQuery()->getSingleScalarResult();
}
/**
* {@inheritDoc}
*/
public function insertTranslationRecord($translation)
{
$em = $this->getObjectManager();
$meta = $em->getClassMetadata(get_class($translation));
$data = array();
foreach ($meta->getReflectionProperties() as $fieldName => $reflProp) {
if (!$meta->isIdentifier($fieldName)) {
$data[$meta->getColumnName($fieldName)] = $reflProp->getValue($translation);
}
}
$table = $meta->getTableName();
if (!$em->getConnection()->insert($table, $data)) {
throw new \Gedmo\Exception\RuntimeException('Failed to insert new Translation record');
}
}
/**
* {@inheritDoc}
*/
public function getTranslationValue($object, $field, $value = false)
{
$em = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $em);
$meta = $wrapped->getMetadata();
$type = Type::getType($meta->getTypeOfField($field));
if ($value === false) {
$value = $wrapped->getPropertyValue($field);
}
return $type->convertToDatabaseValue($value, $em->getConnection()->getDatabasePlatform());
}
/**
* {@inheritDoc}
*/
public function setTranslationValue($object, $field, $value)
{
$em = $this->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $em);
$meta = $wrapped->getMetadata();
$type = Type::getType($meta->getTypeOfField($field));
$value = $type->convertToPHPValue($value, $em->getConnection()->getDatabasePlatform());
$wrapped->setPropertyValue($field, $value);
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Gedmo\Translatable\Mapping\Event;
use Gedmo\Mapping\Event\AdapterInterface;
use Gedmo\Tool\Wrapper\AbstractWrapper;
/**
* Doctrine event adapter interface
* for Translatable behavior
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface TranslatableAdapter extends AdapterInterface
{
/**
* Checks if $translationClassName is a subclass
* of personal translation
*
* @param string $translationClassName
*
* @return boolean
*/
public function usesPersonalTranslation($translationClassName);
/**
* Get default LogEntry class used to store the logs
*
* @return string
*/
public function getDefaultTranslationClass();
/**
* Load the translations for a given object
*
* @param object $object
* @param string $translationClass
* @param string $locale
* @param string $objectClass
*
* @return array
*/
public function loadTranslations($object, $translationClass, $locale, $objectClass);
/**
* Search for existing translation record
*
* @param AbstractWrapper $wrapped
* @param string $locale
* @param string $field
* @param string $translationClass
* @param string $objectClass
*
* @return mixed - null if nothing is found, Translation otherwise
*/
public function findTranslation(AbstractWrapper $wrapped, $locale, $field, $translationClass, $objectClass);
/**
* Removes all associated translations for given object
*
* @param AbstractWrapper $wrapped
* @param string $transClass
* @param string $objectClass
*/
public function removeAssociatedTranslations(AbstractWrapper $wrapped, $transClass, $objectClass);
/**
* Inserts the translation record
*
* @param object $translation
*/
public function insertTranslationRecord($translation);
/**
* Get the transformed value for translation
* storage
*
* @param object $object
* @param string $field
* @param mixed $value
*
* @return mixed
*/
public function getTranslationValue($object, $field, $value = false);
/**
* Transform the value from database
* for translation
*
* @param object $object
* @param string $field
* @param mixed $value
*/
public function setTranslationValue($object, $field, $value);
}

View File

@@ -0,0 +1,472 @@
<?php
namespace Gedmo\Translatable\Query\TreeWalker;
use Doctrine\ORM\Mapping\ClassMetadata;
use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableEventAdapter;
use Gedmo\Translatable\TranslatableListener;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\AST\SelectStatement;
use Doctrine\ORM\Query\Exec\SingleSelectExecutor;
use Doctrine\ORM\Query\AST\RangeVariableDeclaration;
use Doctrine\ORM\Query\AST\Join;
use Doctrine\DBAL\Platforms\MySqlPlatform;
use Doctrine\DBAL\Platforms\PostgreSqlPlatform;
/**
* The translation sql output walker makes it possible
* to translate all query components during single query.
* It works with any select query, any hydration method.
*
* Behind the scenes, during the object hydration it forces
* custom hydrator in order to interact with TranslatableListener
* and skip postLoad event which would cause automatic retranslation
* of the fields.
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class TranslationWalker extends SqlWalker
{
/**
* Name for translation fallback hint
*
* @internal
*/
const HINT_TRANSLATION_FALLBACKS = '__gedmo.translatable.stored.fallbacks';
/**
* Customized object hydrator name
*
* @internal
*/
const HYDRATE_OBJECT_TRANSLATION = '__gedmo.translatable.object.hydrator';
/**
* Customized object hydrator name
*
* @internal
*/
const HYDRATE_SIMPLE_OBJECT_TRANSLATION = '__gedmo.translatable.simple_object.hydrator';
/**
* Stores all component references from select clause
*
* @var array
*/
private $translatedComponents = array();
/**
* DBAL database platform
*
* @var \Doctrine\DBAL\Platforms\AbstractPlatform
*/
private $platform;
/**
* DBAL database connection
*
* @var \Doctrine\DBAL\Connection
*/
private $conn;
/**
* List of aliases to replace with translation
* content reference
*
* @var array
*/
private $replacements = array();
/**
* List of joins for translated components in query
*
* @var array
*/
private $components = array();
/**
* {@inheritDoc}
*/
public function __construct($query, $parserResult, array $queryComponents)
{
parent::__construct($query, $parserResult, $queryComponents);
$this->conn = $this->getConnection();
$this->platform = $this->getConnection()->getDatabasePlatform();
$this->listener = $this->getTranslatableListener();
$this->extractTranslatedComponents($queryComponents);
}
/**
* {@inheritDoc}
*/
public function getExecutor($AST)
{
// If it's not a Select, the TreeWalker ought to skip it, and just return the parent.
// @see https://github.com/Atlantic18/DoctrineExtensions/issues/2013
if (!$AST instanceof SelectStatement) {
return parent::getExecutor($AST);
}
$this->prepareTranslatedComponents();
return new SingleSelectExecutor($AST, $this);
}
/**
* {@inheritDoc}
*/
public function walkSelectStatement(SelectStatement $AST)
{
$result = parent::walkSelectStatement($AST);
if (!count($this->translatedComponents)) {
return $result;
}
$hydrationMode = $this->getQuery()->getHydrationMode();
if ($hydrationMode === Query::HYDRATE_OBJECT) {
$this->getQuery()->setHydrationMode(self::HYDRATE_OBJECT_TRANSLATION);
$this->getEntityManager()->getConfiguration()->addCustomHydrationMode(
self::HYDRATE_OBJECT_TRANSLATION,
'Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator'
);
$this->getQuery()->setHint(Query::HINT_REFRESH, true);
} elseif ($hydrationMode === Query::HYDRATE_SIMPLEOBJECT) {
$this->getQuery()->setHydrationMode(self::HYDRATE_SIMPLE_OBJECT_TRANSLATION);
$this->getEntityManager()->getConfiguration()->addCustomHydrationMode(
self::HYDRATE_SIMPLE_OBJECT_TRANSLATION,
'Gedmo\\Translatable\\Hydrator\\ORM\\SimpleObjectHydrator'
);
$this->getQuery()->setHint(Query::HINT_REFRESH, true);
}
return $result;
}
/**
* {@inheritDoc}
*/
public function walkSelectClause($selectClause)
{
$result = parent::walkSelectClause($selectClause);
$result = $this->replace($this->replacements, $result);
return $result;
}
/**
* {@inheritDoc}
*/
public function walkFromClause($fromClause)
{
$result = parent::walkFromClause($fromClause);
$result .= $this->joinTranslations($fromClause);
return $result;
}
/**
* {@inheritDoc}
*/
public function walkWhereClause($whereClause)
{
$result = parent::walkWhereClause($whereClause);
return $this->replace($this->replacements, $result);
}
/**
* {@inheritDoc}
*/
public function walkHavingClause($havingClause)
{
$result = parent::walkHavingClause($havingClause);
return $this->replace($this->replacements, $result);
}
/**
* {@inheritDoc}
*/
public function walkOrderByClause($orderByClause)
{
$result = parent::walkOrderByClause($orderByClause);
return $this->replace($this->replacements, $result);
}
/**
* {@inheritDoc}
*/
public function walkSubselect($subselect)
{
$result = parent::walkSubselect($subselect);
return $result;
}
/**
* {@inheritDoc}
*/
public function walkSubselectFromClause($subselectFromClause)
{
$result = parent::walkSubselectFromClause($subselectFromClause);
$result .= $this->joinTranslations($subselectFromClause);
return $result;
}
/**
* {@inheritDoc}
*/
public function walkSimpleSelectClause($simpleSelectClause)
{
$result = parent::walkSimpleSelectClause($simpleSelectClause);
return $this->replace($this->replacements, $result);
}
/**
* {@inheritDoc}
*/
public function walkGroupByClause($groupByClause)
{
$result = parent::walkGroupByClause($groupByClause);
return $this->replace($this->replacements, $result);
}
/**
* Walks from clause, and creates translation joins
* for the translated components
*
* @param \Doctrine\ORM\Query\AST\FromClause $from
* @return string
*/
private function joinTranslations($from)
{
$result = '';
foreach ($from->identificationVariableDeclarations as $decl) {
if ($decl->rangeVariableDeclaration instanceof RangeVariableDeclaration) {
if (isset($this->components[$decl->rangeVariableDeclaration->aliasIdentificationVariable])) {
$result .= $this->components[$decl->rangeVariableDeclaration->aliasIdentificationVariable];
}
}
if (isset($decl->joinVariableDeclarations)) {
foreach ($decl->joinVariableDeclarations as $joinDecl) {
if ($joinDecl->join instanceof Join) {
if (isset($this->components[$joinDecl->join->aliasIdentificationVariable])) {
$result .= $this->components[$joinDecl->join->aliasIdentificationVariable];
}
}
}
} else {
// based on new changes
foreach ($decl->joins as $join) {
if ($join instanceof Join) {
if (isset($this->components[$join->joinAssociationDeclaration->aliasIdentificationVariable])) {
$result .= $this->components[$join->joinAssociationDeclaration->aliasIdentificationVariable];
}
}
}
}
}
return $result;
}
/**
* Creates a left join list for translations
* on used query components
*
* @todo: make it cleaner
* @return string
*/
private function prepareTranslatedComponents()
{
$q = $this->getQuery();
$locale = $q->getHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE);
if (!$locale) {
// use from listener
$locale = $this->listener->getListenerLocale();
}
$defaultLocale = $this->listener->getDefaultLocale();
if ($locale === $defaultLocale && !$this->listener->getPersistDefaultLocaleTranslation()) {
// Skip preparation as there's no need to translate anything
return;
}
$em = $this->getEntityManager();
$ea = new TranslatableEventAdapter();
$ea->setEntityManager($em);
$quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
$joinStrategy = $q->getHint(TranslatableListener::HINT_INNER_JOIN) ? 'INNER' : 'LEFT';
foreach ($this->translatedComponents as $dqlAlias => $comp) {
/** @var ClassMetadata $meta */
$meta = $comp['metadata'];
$config = $this->listener->getConfiguration($em, $meta->name);
$transClass = $this->listener->getTranslationClass($ea, $meta->name);
$transMeta = $em->getClassMetadata($transClass);
$transTable = $quoteStrategy->getTableName($transMeta, $this->platform);
foreach ($config['fields'] as $field) {
$compTblAlias = $this->walkIdentificationVariable($dqlAlias, $field);
$tblAlias = $this->getSQLTableAlias('trans'.$compTblAlias.$field);
$sql = " {$joinStrategy} JOIN ".$transTable.' '.$tblAlias;
$sql .= ' ON '.$tblAlias.'.'.$quoteStrategy->getColumnName('locale', $transMeta, $this->platform)
.' = '.$this->conn->quote($locale);
$sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('field', $transMeta, $this->platform)
.' = '.$this->conn->quote($field);
$identifier = $meta->getSingleIdentifierFieldName();
$idColName = $quoteStrategy->getColumnName($identifier, $meta, $this->platform);
if ($ea->usesPersonalTranslation($transClass)) {
$sql .= ' AND '.$tblAlias.'.'.$transMeta->getSingleAssociationJoinColumnName('object')
.' = '.$compTblAlias.'.'.$idColName;
} else {
$sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('objectClass', $transMeta, $this->platform)
.' = '.$this->conn->quote($config['useObjectClass']);
$mappingFK = $transMeta->getFieldMapping('foreignKey');
$mappingPK = $meta->getFieldMapping($identifier);
$fkColName = $this->getCastedForeignKey($compTblAlias.'.'.$idColName, $mappingFK['type'], $mappingPK['type']);
$sql .= ' AND '.$tblAlias.'.'.$quoteStrategy->getColumnName('foreignKey', $transMeta, $this->platform)
.' = '.$fkColName;
}
isset($this->components[$dqlAlias]) ? $this->components[$dqlAlias] .= $sql : $this->components[$dqlAlias] = $sql;
$originalField = $compTblAlias.'.'.$quoteStrategy->getColumnName($field, $meta, $this->platform);
$substituteField = $tblAlias.'.'.$quoteStrategy->getColumnName('content', $transMeta, $this->platform);
// Treat translation as original field type
$fieldMapping = $meta->getFieldMapping($field);
if ((($this->platform instanceof MySqlPlatform) &&
in_array($fieldMapping["type"], array("decimal"))) ||
(!($this->platform instanceof MySqlPlatform) &&
!in_array($fieldMapping["type"], array("datetime", "datetimetz", "date", "time")))) {
$type = Type::getType($fieldMapping["type"]);
$substituteField = 'CAST('.$substituteField.' AS '.$type->getSQLDeclaration($fieldMapping, $this->platform).')';
}
// Fallback to original if was asked for
if (($this->needsFallback() && (!isset($config['fallback'][$field]) || $config['fallback'][$field]))
|| (!$this->needsFallback() && isset($config['fallback'][$field]) && $config['fallback'][$field])
) {
$substituteField = 'COALESCE('.$substituteField.', '.$originalField.')';
}
$this->replacements[$originalField] = $substituteField;
}
}
}
/**
* Checks if translation fallbacks are needed
*
* @return boolean
*/
private function needsFallback()
{
$q = $this->getQuery();
$fallback = $q->getHint(TranslatableListener::HINT_FALLBACK);
if (false === $fallback) {
// non overrided
$fallback = $this->listener->getTranslationFallback();
}
// applies fallbacks to scalar hydration as well
return (bool) $fallback;
}
/**
* Search for translated components in the select clause
*
* @param array $queryComponents
*/
private function extractTranslatedComponents(array $queryComponents)
{
$em = $this->getEntityManager();
foreach ($queryComponents as $alias => $comp) {
if (!isset($comp['metadata'])) {
continue;
}
$meta = $comp['metadata'];
$config = $this->listener->getConfiguration($em, $meta->name);
if ($config && isset($config['fields'])) {
$this->translatedComponents[$alias] = $comp;
}
}
}
/**
* Get the currently used TranslatableListener
*
* @throws \Gedmo\Exception\RuntimeException - if listener is not found
*
* @return TranslatableListener
*/
private function getTranslatableListener()
{
$em = $this->getEntityManager();
foreach ($em->getEventManager()->getListeners() as $event => $listeners) {
foreach ($listeners as $hash => $listener) {
if ($listener instanceof TranslatableListener) {
return $listener;
}
}
}
throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
}
/**
* Replaces given sql $str with required
* results
*
* @param array $repl
* @param string $str
*
* @return string
*/
private function replace(array $repl, $str)
{
foreach ($repl as $target => $result) {
$str = preg_replace_callback('/(\s|\()('.$target.')(,?)(\s|\)|$)/smi', function ($m) use ($result) {
return $m[1].$result.$m[3].$m[4];
}, $str);
}
return $str;
}
/**
* Casts a foreign key if needed
* @NOTE: personal translations manages that for themselves.
*
* @param $component - a column with an alias to cast
* @param $typeFK - translation table foreign key type
* @param $typePK - primary key type which references translation table
* @return string - modified $component if needed
*/
private function getCastedForeignKey($component, $typeFK, $typePK)
{
// the keys are of same type
if ($typeFK === $typePK) {
return $component;
}
// try to look at postgres casting
if ($this->platform instanceof PostgreSqlPlatform) {
switch ($typeFK) {
case 'string':
case 'guid':
// need to cast to VARCHAR
$component = $component . '::VARCHAR';
break;
}
}
// @TODO may add the same thing for MySQL for performance to match index
return $component;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Gedmo\Translatable;
/**
* This interface is not necessary but can be implemented for
* Entities which in some cases needs to be identified as
* Translatable
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
interface Translatable
{
// use now annotations instead of predefined methods, this interface is not necessary
/**
* @gedmo:TranslationEntity
* to specify custom translation class use
* class annotation @gedmo:TranslationEntity(class="your\class")
*/
/**
* @gedmo:Translatable
* to mark the field as translatable,
* these fields will be translated
*/
/**
* @gedmo:Locale OR @gedmo:Language
* to mark the field as locale used to override global
* locale settings from TranslatableListener
*/
}

View File

@@ -0,0 +1,794 @@
<?php
namespace Gedmo\Translatable;
use Doctrine\Common\EventArgs;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ORM\ORMInvalidArgumentException;
use Gedmo\Tool\Wrapper\AbstractWrapper;
use Gedmo\Mapping\MappedEventSubscriber;
use Gedmo\Translatable\Mapping\Event\TranslatableAdapter;
/**
* The translation listener handles the generation and
* loading of translations for entities which implements
* the Translatable interface.
*
* This behavior can impact the performance of your application
* since it does an additional query for each field to translate.
*
* Nevertheless the annotation metadata is properly cached and
* it is not a big overhead to lookup all entity annotations since
* the caching is activated for metadata
*
* @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
class TranslatableListener extends MappedEventSubscriber
{
/**
* Query hint to override the fallback of translations
* integer 1 for true, 0 false
*/
const HINT_FALLBACK = 'gedmo.translatable.fallback';
/**
* Query hint to override the fallback locale
*/
const HINT_TRANSLATABLE_LOCALE = 'gedmo.translatable.locale';
/**
* Query hint to use inner join strategy for translations
*/
const HINT_INNER_JOIN = 'gedmo.translatable.inner_join.translations';
/**
* Locale which is set on this listener.
* If Entity being translated has locale defined it
* will override this one
*
* @var string
*/
protected $locale = 'en_US';
/**
* Default locale, this changes behavior
* to not update the original record field if locale
* which is used for updating is not default. This
* will load the default translation in other locales
* if record is not translated yet
*
* @var string
*/
private $defaultLocale = 'en_US';
/**
* If this is set to false, when if entity does
* not have a translation for requested locale
* it will show a blank value
*
* @var boolean
*/
private $translationFallback = false;
/**
* List of translations which do not have the foreign
* key generated yet - MySQL case. These translations
* will be updated with new keys on postPersist event
*
* @var array
*/
private $pendingTranslationInserts = array();
/**
* Currently in case if there is TranslationQueryWalker
* in charge. We need to skip issuing additional queries
* on load
*
* @var boolean
*/
private $skipOnLoad = false;
/**
* Tracks locale the objects currently translated in
*
* @var array
*/
private $translatedInLocale = array();
/**
* Whether or not, to persist default locale
* translation or keep it in original record
*
* @var boolean
*/
private $persistDefaultLocaleTranslation = false;
/**
* Tracks translation object for default locale
*
* @var array
*/
private $translationInDefaultLocale = array();
/**
* Specifies the list of events to listen
*
* @return array
*/
public function getSubscribedEvents()
{
return array(
'postLoad',
'postPersist',
'preFlush',
'onFlush',
'loadClassMetadata',
);
}
/**
* Set to skip or not onLoad event
*
* @param boolean $bool
*
* @return static
*/
public function setSkipOnLoad($bool)
{
$this->skipOnLoad = (bool) $bool;
return $this;
}
/**
* Whether or not, to persist default locale
* translation or keep it in original record
*
* @param boolean $bool
*
* @return static
*/
public function setPersistDefaultLocaleTranslation($bool)
{
$this->persistDefaultLocaleTranslation = (bool) $bool;
return $this;
}
/**
* Check if should persist default locale
* translation or keep it in original record
*
* @return boolean
*/
public function getPersistDefaultLocaleTranslation()
{
return (bool) $this->persistDefaultLocaleTranslation;
}
/**
* Add additional $translation for pending $oid object
* which is being inserted
*
* @param string $oid
* @param object $translation
*/
public function addPendingTranslationInsert($oid, $translation)
{
$this->pendingTranslationInserts[$oid][] = $translation;
}
/**
* Maps additional metadata
*
* @param EventArgs $eventArgs
*/
public function loadClassMetadata(EventArgs $eventArgs)
{
$ea = $this->getEventAdapter($eventArgs);
$this->loadMetadataForObjectClass($ea->getObjectManager(), $eventArgs->getClassMetadata());
}
/**
* Get the translation class to be used
* for the object $class
*
* @param TranslatableAdapter $ea
* @param string $class
*
* @return string
*/
public function getTranslationClass(TranslatableAdapter $ea, $class)
{
return isset(self::$configurations[$this->name][$class]['translationClass']) ?
self::$configurations[$this->name][$class]['translationClass'] :
$ea->getDefaultTranslationClass()
;
}
/**
* Enable or disable translation fallback
* to original record value
*
* @param boolean $bool
*
* @return static
*/
public function setTranslationFallback($bool)
{
$this->translationFallback = (bool) $bool;
return $this;
}
/**
* Weather or not is using the translation
* fallback to original record
*
* @return boolean
*/
public function getTranslationFallback()
{
return $this->translationFallback;
}
/**
* Set the locale to use for translation listener
*
* @param string $locale
*
* @return static
*/
public function setTranslatableLocale($locale)
{
$this->validateLocale($locale);
$this->locale = $locale;
return $this;
}
/**
* Sets the default locale, this changes behavior
* to not update the original record field if locale
* which is used for updating is not default
*
* @param string $locale
*
* @return static
*/
public function setDefaultLocale($locale)
{
$this->validateLocale($locale);
$this->defaultLocale = $locale;
return $this;
}
/**
* Gets the default locale
*
* @return string
*/
public function getDefaultLocale()
{
return $this->defaultLocale;
}
/**
* Get currently set global locale, used
* extensively during query execution
*
* @return string
*/
public function getListenerLocale()
{
return $this->locale;
}
/**
* Gets the locale to use for translation. Loads object
* defined locale first..
*
* @param object $object
* @param object $meta
* @param object $om
*
* @throws \Gedmo\Exception\RuntimeException - if language or locale property is not
* found in entity
* @return string
*/
public function getTranslatableLocale($object, $meta, $om = null)
{
$locale = $this->locale;
if (isset(self::$configurations[$this->name][$meta->name]['locale'])) {
/** @var \ReflectionClass $class */
$class = $meta->getReflectionClass();
$reflectionProperty = $class->getProperty(self::$configurations[$this->name][$meta->name]['locale']);
if (!$reflectionProperty) {
$column = self::$configurations[$this->name][$meta->name]['locale'];
throw new \Gedmo\Exception\RuntimeException("There is no locale or language property ({$column}) found on object: {$meta->name}");
}
$reflectionProperty->setAccessible(true);
$value = $reflectionProperty->getValue($object);
if (is_object($value) && method_exists($value, '__toString')) {
$value = (string) $value;
}
if ($this->isValidLocale($value)) {
$locale = $value;
}
} elseif ($om instanceof DocumentManager) {
list($mapping, $parentObject) = $om->getUnitOfWork()->getParentAssociation($object);
if ($parentObject != null) {
$parentMeta = $om->getClassMetadata(get_class($parentObject));
$locale = $this->getTranslatableLocale($parentObject, $parentMeta, $om);
}
}
return $locale;
}
/**
* Handle translation changes in default locale
*
* This has to be done in the preFlush because, when an entity has been loaded
* in a different locale, no changes will be detected.
*
* @param EventArgs $args
*/
public function preFlush(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
foreach ($this->translationInDefaultLocale as $oid => $fields) {
$trans = reset($fields);
if ($ea->usesPersonalTranslation(get_class($trans))) {
$entity = $trans->getObject();
} else {
$entity = $uow->tryGetById($trans->getForeignKey(), $trans->getObjectClass());
}
if (!$entity) {
continue;
}
try {
$uow->scheduleForUpdate($entity);
} catch (ORMInvalidArgumentException $e) {
foreach ($fields as $field => $trans) {
$this->removeTranslationInDefaultLocale($oid, $field);
}
}
}
}
/**
* Looks for translatable objects being inserted or updated
* for further processing
*
* @param EventArgs $args
*/
public function onFlush(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
// check all scheduled inserts for Translatable objects
foreach ($ea->getScheduledObjectInsertions($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
$config = $this->getConfiguration($om, $meta->name);
if (isset($config['fields'])) {
$this->handleTranslatableObjectUpdate($ea, $object, true);
}
}
// check all scheduled updates for Translatable entities
foreach ($ea->getScheduledObjectUpdates($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
$config = $this->getConfiguration($om, $meta->name);
if (isset($config['fields'])) {
$this->handleTranslatableObjectUpdate($ea, $object, false);
}
}
// check scheduled deletions for Translatable entities
foreach ($ea->getScheduledObjectDeletions($uow) as $object) {
$meta = $om->getClassMetadata(get_class($object));
$config = $this->getConfiguration($om, $meta->name);
if (isset($config['fields'])) {
$wrapped = AbstractWrapper::wrap($object, $om);
$transClass = $this->getTranslationClass($ea, $meta->name);
$ea->removeAssociatedTranslations($wrapped, $transClass, $config['useObjectClass']);
}
}
}
/**
* Checks for inserted object to update their translation
* foreign keys
*
* @param EventArgs $args
*/
public function postPersist(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
// check if entity is tracked by translatable and without foreign key
if ($this->getConfiguration($om, $meta->name) && count($this->pendingTranslationInserts)) {
$oid = spl_object_hash($object);
if (array_key_exists($oid, $this->pendingTranslationInserts)) {
// load the pending translations without key
$wrapped = AbstractWrapper::wrap($object, $om);
$objectId = $wrapped->getIdentifier();
$translationClass = $this->getTranslationClass($ea, get_class($object));
foreach ($this->pendingTranslationInserts[$oid] as $translation) {
if ($ea->usesPersonalTranslation($translationClass)) {
$translation->setObject($objectId);
} else {
$translation->setForeignKey($objectId);
}
$ea->insertTranslationRecord($translation);
}
unset($this->pendingTranslationInserts[$oid]);
}
}
}
/**
* After object is loaded, listener updates the translations
* by currently used locale
*
* @param EventArgs $args
*/
public function postLoad(EventArgs $args)
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
$config = $this->getConfiguration($om, $meta->name);
if (isset($config['fields'])) {
$locale = $this->getTranslatableLocale($object, $meta, $om);
$oid = spl_object_hash($object);
$this->translatedInLocale[$oid] = $locale;
}
if ($this->skipOnLoad) {
return;
}
if (isset($config['fields']) && ($locale !== $this->defaultLocale || $this->persistDefaultLocaleTranslation)) {
// fetch translations
$translationClass = $this->getTranslationClass($ea, $config['useObjectClass']);
$result = $ea->loadTranslations(
$object,
$translationClass,
$locale,
$config['useObjectClass']
);
// translate object's translatable properties
foreach ($config['fields'] as $field) {
$translated = null;
foreach ((array) $result as $entry) {
if ($entry['field'] == $field) {
$translated = isset($entry['content']) ? $entry['content'] : null;
break;
}
}
// update translation
if ($translated !== null
|| (!$this->translationFallback && (!isset($config['fallback'][$field]) || !$config['fallback'][$field]))
|| ($this->translationFallback && isset($config['fallback'][$field]) && !$config['fallback'][$field])
) {
$ea->setTranslationValue($object, $field, $translated);
// ensure clean changeset
$ea->setOriginalObjectProperty(
$om->getUnitOfWork(),
$oid,
$field,
$meta->getReflectionProperty($field)->getValue($object)
);
}
}
}
}
/**
* {@inheritDoc}
*/
protected function getNamespace()
{
return __NAMESPACE__;
}
/**
* Validates the given locale
*
* @param string $locale - locale to validate
*
* @throws \Gedmo\Exception\InvalidArgumentException if locale is not valid
*/
protected function validateLocale($locale)
{
if (!$this->isValidLocale($locale)) {
throw new \Gedmo\Exception\InvalidArgumentException('Locale or language cannot be empty and must be set through Listener or Entity');
}
}
/**
* Check if the given locale is valid
*
* @param string $locale - locale to check
*
* @return bool
*/
private function isValidlocale($locale)
{
return is_string($locale) && strlen($locale);
}
/**
* Creates the translation for object being flushed
*
* @param TranslatableAdapter $ea
* @param object $object
* @param boolean $isInsert
*
* @throws \UnexpectedValueException - if locale is not valid, or
* primary key is composite, missing or invalid
*/
private function handleTranslatableObjectUpdate(TranslatableAdapter $ea, $object, $isInsert)
{
$om = $ea->getObjectManager();
$wrapped = AbstractWrapper::wrap($object, $om);
$meta = $wrapped->getMetadata();
$config = $this->getConfiguration($om, $meta->name);
// no need cache, metadata is loaded only once in MetadataFactoryClass
$translationClass = $this->getTranslationClass($ea, $config['useObjectClass']);
$translationMetadata = $om->getClassMetadata($translationClass);
// check for the availability of the primary key
$objectId = $wrapped->getIdentifier();
// load the currently used locale
$locale = $this->getTranslatableLocale($object, $meta, $om);
$uow = $om->getUnitOfWork();
$oid = spl_object_hash($object);
$changeSet = $ea->getObjectChangeSet($uow, $object);
$translatableFields = $config['fields'];
foreach ($translatableFields as $field) {
$wasPersistedSeparetely = false;
$skip = isset($this->translatedInLocale[$oid]) && $locale === $this->translatedInLocale[$oid];
$skip = $skip && !isset($changeSet[$field]) && !$this->getTranslationInDefaultLocale($oid, $field);
if ($skip) {
continue; // locale is same and nothing changed
}
$translation = null;
foreach ($ea->getScheduledObjectInsertions($uow) as $trans) {
if ($locale !== $this->defaultLocale
&& get_class($trans) === $translationClass
&& $trans->getLocale() === $this->defaultLocale
&& $trans->getField() === $field
&& $this->belongsToObject($ea, $trans, $object)) {
$this->setTranslationInDefaultLocale($oid, $field, $trans);
break;
}
}
// lookup persisted translations
foreach ($ea->getScheduledObjectInsertions($uow) as $trans) {
if (get_class($trans) !== $translationClass
|| $trans->getLocale() !== $locale
|| $trans->getField() !== $field) {
continue;
}
if ($ea->usesPersonalTranslation($translationClass)) {
$wasPersistedSeparetely = $trans->getObject() === $object;
} else {
$wasPersistedSeparetely = $trans->getObjectClass() === $config['useObjectClass']
&& $trans->getForeignKey() === $objectId;
}
if ($wasPersistedSeparetely) {
$translation = $trans;
break;
}
}
// check if translation already is created
if (!$isInsert && !$translation) {
$translation = $ea->findTranslation(
$wrapped,
$locale,
$field,
$translationClass,
$config['useObjectClass']
);
}
// create new translation if translation not already created and locale is different from default locale, otherwise, we have the date in the original record
$persistNewTranslation = !$translation
&& ($locale !== $this->defaultLocale || $this->persistDefaultLocaleTranslation)
;
if ($persistNewTranslation) {
$translation = $translationMetadata->newInstance();
$translation->setLocale($locale);
$translation->setField($field);
if ($ea->usesPersonalTranslation($translationClass)) {
$translation->setObject($object);
} else {
$translation->setObjectClass($config['useObjectClass']);
$translation->setForeignKey($objectId);
}
}
if ($translation) {
// set the translated field, take value using reflection
$content = $ea->getTranslationValue($object, $field);
$translation->setContent($content);
// check if need to update in database
$transWrapper = AbstractWrapper::wrap($translation, $om);
if (((is_null($content) && !$isInsert) || is_bool($content) || is_int($content) || is_string($content) || !empty($content)) && ($isInsert || !$transWrapper->getIdentifier() || isset($changeSet[$field]))) {
if ($isInsert && !$objectId && !$ea->usesPersonalTranslation($translationClass)) {
// if we do not have the primary key yet available
// keep this translation in memory to insert it later with foreign key
$this->pendingTranslationInserts[spl_object_hash($object)][] = $translation;
} else {
// persist and compute change set for translation
if ($wasPersistedSeparetely) {
$ea->recomputeSingleObjectChangeset($uow, $translationMetadata, $translation);
} else {
$om->persist($translation);
$uow->computeChangeSet($translationMetadata, $translation);
}
}
}
}
if ($isInsert && $this->getTranslationInDefaultLocale($oid, $field) !== null) {
// We can't rely on object field value which is created in non-default locale.
// If we provide translation for default locale as well, the latter is considered to be trusted
// and object content should be overridden.
$wrapped->setPropertyValue($field, $this->getTranslationInDefaultLocale($oid, $field)->getContent());
$ea->recomputeSingleObjectChangeset($uow, $meta, $object);
$this->removeTranslationInDefaultLocale($oid, $field);
}
}
$this->translatedInLocale[$oid] = $locale;
// check if we have default translation and need to reset the translation
if (!$isInsert && strlen($this->defaultLocale)) {
$this->validateLocale($this->defaultLocale);
$modifiedChangeSet = $changeSet;
foreach ($changeSet as $field => $changes) {
if (in_array($field, $translatableFields)) {
if ($locale !== $this->defaultLocale) {
$ea->setOriginalObjectProperty($uow, $oid, $field, $changes[0]);
unset($modifiedChangeSet[$field]);
}
}
}
$ea->recomputeSingleObjectChangeset($uow, $meta, $object);
// cleanup current changeset only if working in a another locale different than de default one, otherwise the changeset will always be reverted
if ($locale !== $this->defaultLocale) {
$ea->clearObjectChangeSet($uow, $oid);
// recompute changeset only if there are changes other than reverted translations
if ($modifiedChangeSet || $this->hasTranslationsInDefaultLocale($oid)) {
foreach ($modifiedChangeSet as $field => $changes) {
$ea->setOriginalObjectProperty($uow, $oid, $field, $changes[0]);
}
foreach ($translatableFields as $field) {
if ($this->getTranslationInDefaultLocale($oid, $field) !== null) {
$wrapped->setPropertyValue($field, $this->getTranslationInDefaultLocale($oid, $field)->getContent());
$this->removeTranslationInDefaultLocale($oid, $field);
}
}
$ea->recomputeSingleObjectChangeset($uow, $meta, $object);
}
}
}
}
/**
* Sets translation object which represents translation in default language.
*
* @param string $oid hash of basic entity
* @param string $field field of basic entity
* @param mixed $trans Translation object
*/
public function setTranslationInDefaultLocale($oid, $field, $trans)
{
if (!isset($this->translationInDefaultLocale[$oid])) {
$this->translationInDefaultLocale[$oid] = array();
}
$this->translationInDefaultLocale[$oid][$field] = $trans;
}
/**
* @return boolean
*/
public function isSkipOnLoad()
{
return $this->skipOnLoad;
}
/**
* Removes translation object which represents translation in default language.
* This is for internal use only.
*
* @param string $oid hash of the basic entity
* @param string $field field of basic entity
*/
private function removeTranslationInDefaultLocale($oid, $field)
{
if (isset($this->translationInDefaultLocale[$oid])) {
if (isset($this->translationInDefaultLocale[$oid][$field])) {
unset($this->translationInDefaultLocale[$oid][$field]);
}
if (! $this->translationInDefaultLocale[$oid]) {
// We removed the final remaining elements from the
// translationInDefaultLocale[$oid] array, so we might as well
// completely remove the entry at $oid.
unset($this->translationInDefaultLocale[$oid]);
}
}
}
/**
* Gets translation object which represents translation in default language.
* This is for internal use only.
*
* @param string $oid hash of the basic entity
* @param string $field field of basic entity
*
* @return mixed Returns translation object if it exists or NULL otherwise
*/
private function getTranslationInDefaultLocale($oid, $field)
{
if (array_key_exists($oid, $this->translationInDefaultLocale)) {
if (array_key_exists($field, $this->translationInDefaultLocale[$oid])) {
$ret = $this->translationInDefaultLocale[$oid][$field];
} else {
$ret = null;
}
} else {
$ret = null;
}
return $ret;
}
/**
* Check if object has any translation object which represents translation in default language.
* This is for internal use only.
*
* @param string $oid hash of the basic entity
*
* @return bool
*/
public function hasTranslationsInDefaultLocale($oid)
{
return array_key_exists($oid, $this->translationInDefaultLocale);
}
/**
* Checks if the translation entity belongs to the object in question
*
* @param TranslatableAdapter $ea
* @param object $trans
* @param object $object
*
* @return boolean
*/
private function belongsToObject(TranslatableAdapter $ea, $trans, $object)
{
if ($ea->usesPersonalTranslation(get_class($trans))) {
return $trans->getObject() === $object;
}
return ($trans->getForeignKey() === $object->getId()
&& ($trans->getObjectClass() === get_class($object)));
}
}