123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- <?php
- namespace Gedmo\Translatable\Query\TreeWalker;
- use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableEventAdapter;
- use Gedmo\Translatable\TranslationListener;
- use Doctrine\ORM\Query;
- use Doctrine\ORM\Query\SqlWalker;
- use Doctrine\ORM\Query\TreeWalkerAdapter;
- use Doctrine\ORM\Query\AST\SelectStatement;
- use Doctrine\ORM\Query\Exec\SingleSelectExecutor;
- /**
- * 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 TranslationListener
- * and skip postLoad event which would couse automatic retranslation
- * of the fields.
- *
- * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
- * @package Gedmo.Translatable.Query.TreeWalker
- * @subpackage TranslationWalker
- * @link http://www.gediminasm.org
- * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
- */
- class TranslationWalker extends SqlWalker
- {
- /**
- * Name for translation fallback hint
- */
- const HINT_TRANSLATION_FALLBACKS = 'translation_fallbacks';
- /**
- * Name for translation listener hint
- */
- const HINT_TRANSLATION_LISTENER = 'translation_listener';
- /**
- * Customized object hydrator name
- */
- const HYDRATE_OBJECT_TRANSLATION = 'object_translation_hydrator';
- /**
- * Customized object hydrator name
- */
- const HYDRATE_ARRAY_TRANSLATION = 'array_translation_hydrator';
- /**
- * Customized object hydrator name
- */
- const HYDRATE_SIMPLE_OBJECT_TRANSLATION = 'simple_object_translation_hydrator';
- /**
- * Stores all component references from select clause
- *
- * @var array
- */
- private $translatedComponents = array();
- /**
- * Current TranslationListener instance used
- * in EntityManager
- *
- * @var TranslationListener
- */
- private $listener;
- /**
- * 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();
- /**
- * Translation joins on current query
- * components
- *
- * @var array
- */
- private $translationJoinSql = array();
- /**
- * Processed subselect number
- *
- * @var integer
- */
- private $processedNestingLevel = 0;
- /**
- * {@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->getTranslationListener();
- $this->extractTranslatedComponents($queryComponents);
- }
- /**
- * {@inheritDoc}
- */
- public function getExecutor($AST)
- {
- if (!$AST instanceof SelectStatement) {
- throw new \Gedmo\Exception\UnexpectedValueException('Translation walker should be used only on select statement');
- }
- $this->translationJoinSql = $this->getTranslationJoinsSql();
- 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 ($this->needsFallback()) {
- // in case if fallback is used and hydration is array, it needs custom hydrator
- if ($hydrationMode === Query::HYDRATE_ARRAY) {
- $this->getQuery()->setHydrationMode(self::HYDRATE_ARRAY_TRANSLATION);
- $this->getEntityManager()->getConfiguration()->addCustomHydrationMode(
- self::HYDRATE_ARRAY_TRANSLATION,
- 'Gedmo\\Translatable\\Hydrator\\ORM\\ArrayHydrator'
- );
- }
- }
- $this->getQuery()->setHint(self::HINT_TRANSLATION_LISTENER, $this->listener);
- 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);
- $fallbackSql = '';
- if ($this->needsFallback() && count($this->translatedComponents)) {
- $fallbackAliases = array();
- foreach ($this->replacements as $dqlAlias => $trans) {
- if (preg_match("/{$dqlAlias} AS ([^\s]+)/smi", $result, $m)) {
- list($tblAlias, $colName) = explode('.', $dqlAlias);
- $fallback = $this->getSQLColumnAlias($colName.'_fallback');
- $fallbackSql .= ', '.$dqlAlias.' AS '.$fallback;
- $fallbackAliases[$fallback] = rtrim($m[1], ',');
- }
- }
- $this->getQuery()->setHint(self::HINT_TRANSLATION_FALLBACKS, $fallbackAliases);
- }
- $result = str_replace(
- array_keys($this->replacements),
- array_values($this->replacements),
- $result
- );
- $result .= $fallbackSql;
- return $result;
- }
- /**
- * {@inheritDoc}
- */
- public function walkFromClause($fromClause)
- {
- $result = parent::walkFromClause($fromClause);
- $result .= $this->translationJoinSql[0];
- return $result;
- }
- /**
- * {@inheritDoc}
- */
- public function walkWhereClause($whereClause)
- {
- $result = parent::walkWhereClause($whereClause);
- return str_replace(
- array_keys($this->replacements),
- array_values($this->replacements),
- $result
- );
- }
- /**
- * {@inheritDoc}
- */
- public function walkHavingClause($havingClause)
- {
- $result = parent::walkHavingClause($havingClause);
- return str_replace(
- array_keys($this->replacements),
- array_values($this->replacements),
- $result
- );
- }
- /**
- * {@inheritDoc}
- */
- public function walkOrderByClause($orderByClause)
- {
- $result = parent::walkOrderByClause($orderByClause);
- return str_replace(
- array_keys($this->replacements),
- array_values($this->replacements),
- $result
- );
- }
- /**
- * {@inheritDoc}
- */
- public function walkSubselect($subselect)
- {
- $this->processedNestingLevel++;
- $result = parent::walkSubselect($subselect);
- return $result;
- }
- /**
- * {@inheritDoc}
- */
- public function walkSubselectFromClause($subselectFromClause)
- {
- $result = parent::walkSubselectFromClause($subselectFromClause);
- if (isset($this->translationJoinSql[$this->processedNestingLevel])) {
- $result .= $this->translationJoinSql[$this->processedNestingLevel];
- }
- return $result;
- }
- /**
- * {@inheritDoc}
- */
- public function walkSimpleSelectClause($simpleSelectClause)
- {
- $result = parent::walkSimpleSelectClause($simpleSelectClause);
- return str_replace(
- array_keys($this->replacements),
- array_values($this->replacements),
- $result
- );
- }
- /**
- * Creates a left join list for translations
- * on used query components
- *
- * @todo: make it cleaner
- * @return string
- */
- private function getTranslationJoinsSql()
- {
- $result = array();
- $em = $this->getEntityManager();
- $ea = new TranslatableEventAdapter;
- $locale = $this->listener->getListenerLocale();
- foreach ($this->translatedComponents as $dqlAlias => $comp) {
- if (!isset($result[$comp['nestingLevel']])) {
- $result[$comp['nestingLevel']] = '';
- }
- $meta = $comp['metadata'];
- $config = $this->listener->getConfiguration($em, $meta->name);
- $transClass = $this->listener->getTranslationClass($ea, $meta->name);
- $transMeta = $em->getClassMetadata($transClass);
- $transTable = $transMeta->getQuotedTableName($this->platform);
- foreach ($config['fields'] as $field) {
- $compTableName = $meta->getQuotedTableName($this->platform);
- $compTblAlias = $this->getSQLTableAlias($compTableName, $dqlAlias);
- $tblAlias = $this->getSQLTableAlias('trans'.$compTblAlias.$field);
- $sql = ' LEFT JOIN '.$transTable.' '.$tblAlias;
- $sql .= ' ON '.$tblAlias.'.'.$transMeta->getQuotedColumnName('locale', $this->platform)
- .' = '.$this->conn->quote($locale);
- $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('objectClass', $this->platform)
- .' = '.$this->conn->quote($meta->name);
- $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('field', $this->platform)
- .' = '.$this->conn->quote($field);
- $identifier = $meta->getSingleIdentifierFieldName();
- $colName = $meta->getQuotedColumnName($identifier, $this->platform);
- $colAlias = $this->getSQLColumnAlias($colName);
- $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('foreignKey', $this->platform)
- .' = '.$compTblAlias.'.'.$colName;
- $result[$comp['nestingLevel']] .= $sql;
- $this->replacements[$compTblAlias.'.'.$meta->getQuotedColumnName($field, $this->platform)]
- = $tblAlias.'.'.$transMeta->getQuotedColumnName('content', $this->platform);
- }
- }
- return $result;
- }
- /**
- * Checks if translation fallbacks are needed
- *
- * @return boolean
- */
- private function needsFallback()
- {
- $q = $this->getQuery();
- return $this->listener->getTranslationFallback()
- && $q->getHydrationMode() !== Query::HYDRATE_SCALAR
- && $q->getHydrationMode() !== Query::HYDRATE_SINGLE_SCALAR;
- }
- /**
- * Search for translated components in the select clause
- *
- * @param array $queryComponents
- * @return void
- */
- private function extractTranslatedComponents(array $queryComponents)
- {
- $em = $this->getEntityManager();
- foreach ($queryComponents as $alias => $comp) {
- $meta = $comp['metadata'];
- $config = $this->listener->getConfiguration($em, $meta->name);
- if ($config && isset($config['fields'])) {
- $this->translatedComponents[$alias] = $comp;
- }
- }
- }
- /**
- * Get the currently used TranslationListener
- *
- * @throws \Gedmo\Exception\RuntimeException - if listener is not found
- * @return TranslationListener
- */
- private function getTranslationListener()
- {
- $translationListener = null;
- $em = $this->getEntityManager();
- foreach ($em->getEventManager()->getListeners() as $event => $listeners) {
- foreach ($listeners as $hash => $listener) {
- if ($listener instanceof TranslationListener) {
- $translationListener = $listener;
- break;
- }
- }
- if ($translationListener) {
- break;
- }
- }
- if (is_null($translationListener)) {
- throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
- }
- return $translationListener;
- }
- }
|