123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419 |
- <?php
- namespace Gedmo\Translatable\Query\TreeWalker;
- use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableEventAdapter;
- use Gedmo\Translatable\TranslatableListener;
- 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;
- use Doctrine\ORM\Query\AST\RangeVariableDeclaration;
- use Doctrine\ORM\Query\AST\Join;
- /**
- * 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 couse 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 (!$AST instanceof SelectStatement) {
- throw new \Gedmo\Exception\UnexpectedValueException('Translation walker should be used only on select statement');
- }
- $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);
- $joinStrategy = $q->getHint(TranslatableListener::HINT_INNER_JOIN) ? 'INNER' : 'LEFT';
- foreach ($this->translatedComponents as $dqlAlias => $comp) {
- $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) {
- $compTblAlias = $this->walkIdentificationVariable($dqlAlias, $field);
- $tblAlias = $this->getSQLTableAlias('trans'.$compTblAlias.$field);
- $sql = " {$joinStrategy} JOIN ".$transTable.' '.$tblAlias;
- $sql .= ' ON '.$tblAlias.'.'.$transMeta->getQuotedColumnName('locale', $this->platform)
- .' = '.$this->conn->quote($locale);
- $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('field', $this->platform)
- .' = '.$this->conn->quote($field);
- $identifier = $meta->getSingleIdentifierFieldName();
- $idColName = $meta->getQuotedColumnName($identifier, $this->platform);
- if ($ea->usesPersonalTranslation($transClass)) {
- $sql .= ' AND '.$tblAlias.'.'.$transMeta->getSingleAssociationJoinColumnName('object')
- .' = '.$compTblAlias.'.'.$idColName;
- } else {
- $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('objectClass', $this->platform)
- .' = '.$this->conn->quote($meta->name);
- $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('foreignKey', $this->platform)
- .' = '.$compTblAlias.'.'.$idColName;
- }
- isset($this->components[$dqlAlias]) ? $this->components[$dqlAlias] .= $sql : $this->components[$dqlAlias] = $sql;
- $originalField = $compTblAlias.'.'.$meta->getQuotedColumnName($field, $this->platform);
- $substituteField = $tblAlias . '.' . $transMeta->getQuotedColumnName('content', $this->platform);
- // If original field is integer - treat translation as integer (for ORDER BY, WHERE, etc)
- $fieldMapping = $meta->getFieldMapping($field);
- if (in_array($fieldMapping["type"], array("integer", "bigint", "tinyint", "int"))) {
- $substituteField = 'CAST(' . $substituteField . ' AS SIGNED)';
- }
- // 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();
- }
- return (bool)$fallback
- && $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) {
- 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()
- {
- $translatableListener = null;
- $em = $this->getEntityManager();
- foreach ($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;
- }
- /**
- * 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;
- }
- }
|