TranslationWalker.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. <?php
  2. namespace Gedmo\Translatable\Query\TreeWalker;
  3. use Gedmo\Translatable\Mapping\Event\Adapter\ORM as TranslatableEventAdapter;
  4. use Gedmo\Translatable\TranslationListener;
  5. use Doctrine\ORM\Query;
  6. use Doctrine\ORM\Query\SqlWalker;
  7. use Doctrine\ORM\Query\TreeWalkerAdapter;
  8. use Doctrine\ORM\Query\AST\SelectStatement;
  9. use Doctrine\ORM\Query\Exec\SingleSelectExecutor;
  10. use Doctrine\ORM\Query\AST\RangeVariableDeclaration;
  11. use Doctrine\ORM\Query\AST\Join;
  12. /**
  13. * The translation sql output walker makes it possible
  14. * to translate all query components during single query.
  15. * It works with any select query, any hydration method.
  16. *
  17. * Behind the scenes, during the object hydration it forces
  18. * custom hydrator in order to interact with TranslationListener
  19. * and skip postLoad event which would couse automatic retranslation
  20. * of the fields.
  21. *
  22. * @author Gediminas Morkevicius <gediminas.morkevicius@gmail.com>
  23. * @package Gedmo.Translatable.Query.TreeWalker
  24. * @subpackage TranslationWalker
  25. * @link http://www.gediminasm.org
  26. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  27. */
  28. class TranslationWalker extends SqlWalker
  29. {
  30. /**
  31. * Name for translation fallback hint
  32. *
  33. * @internal
  34. */
  35. const HINT_TRANSLATION_FALLBACKS = '__gedmo.translatable.stored.fallbacks';
  36. /**
  37. * Customized object hydrator name
  38. *
  39. * @internal
  40. */
  41. const HYDRATE_OBJECT_TRANSLATION = '__gedmo.translatable.object.hydrator';
  42. /**
  43. * Customized object hydrator name
  44. *
  45. * @internal
  46. */
  47. const HYDRATE_SIMPLE_OBJECT_TRANSLATION = '__gedmo.translatable.simple_object.hydrator';
  48. /**
  49. * Stores all component references from select clause
  50. *
  51. * @var array
  52. */
  53. private $translatedComponents = array();
  54. /**
  55. * DBAL database platform
  56. *
  57. * @var Doctrine\DBAL\Platforms\AbstractPlatform
  58. */
  59. private $platform;
  60. /**
  61. * DBAL database connection
  62. *
  63. * @var Doctrine\DBAL\Connection
  64. */
  65. private $conn;
  66. /**
  67. * List of aliases to replace with translation
  68. * content reference
  69. *
  70. * @var array
  71. */
  72. private $replacements = array();
  73. /**
  74. * List of joins for translated components in query
  75. *
  76. * @var array
  77. */
  78. private $components = array();
  79. /**
  80. * {@inheritDoc}
  81. */
  82. public function __construct($query, $parserResult, array $queryComponents)
  83. {
  84. parent::__construct($query, $parserResult, $queryComponents);
  85. $this->conn = $this->getConnection();
  86. $this->platform = $this->getConnection()->getDatabasePlatform();
  87. $this->listener = $this->getTranslationListener();
  88. $this->extractTranslatedComponents($queryComponents);
  89. }
  90. /**
  91. * {@inheritDoc}
  92. */
  93. public function getExecutor($AST)
  94. {
  95. if (!$AST instanceof SelectStatement) {
  96. throw new \Gedmo\Exception\UnexpectedValueException('Translation walker should be used only on select statement');
  97. }
  98. $this->prepareTranslatedComponents();
  99. return new SingleSelectExecutor($AST, $this);
  100. }
  101. /**
  102. * {@inheritDoc}
  103. */
  104. public function walkSelectStatement(SelectStatement $AST)
  105. {
  106. $result = parent::walkSelectStatement($AST);
  107. if (!count($this->translatedComponents)) {
  108. return $result;
  109. }
  110. $hydrationMode = $this->getQuery()->getHydrationMode();
  111. if ($hydrationMode === Query::HYDRATE_OBJECT) {
  112. $this->getQuery()->setHydrationMode(self::HYDRATE_OBJECT_TRANSLATION);
  113. $this->getEntityManager()->getConfiguration()->addCustomHydrationMode(
  114. self::HYDRATE_OBJECT_TRANSLATION,
  115. 'Gedmo\\Translatable\\Hydrator\\ORM\\ObjectHydrator'
  116. );
  117. $this->getQuery()->setHint(Query::HINT_REFRESH, true);
  118. } elseif ($hydrationMode === Query::HYDRATE_SIMPLEOBJECT) {
  119. $this->getQuery()->setHydrationMode(self::HYDRATE_SIMPLE_OBJECT_TRANSLATION);
  120. $this->getEntityManager()->getConfiguration()->addCustomHydrationMode(
  121. self::HYDRATE_SIMPLE_OBJECT_TRANSLATION,
  122. 'Gedmo\\Translatable\\Hydrator\\ORM\\SimpleObjectHydrator'
  123. );
  124. $this->getQuery()->setHint(Query::HINT_REFRESH, true);
  125. }
  126. return $result;
  127. }
  128. /**
  129. * {@inheritDoc}
  130. */
  131. public function walkSelectClause($selectClause)
  132. {
  133. $result = parent::walkSelectClause($selectClause);
  134. $result = $this->replace($this->replacements, $result);
  135. return $result;
  136. }
  137. /**
  138. * {@inheritDoc}
  139. */
  140. public function walkFromClause($fromClause)
  141. {
  142. $result = parent::walkFromClause($fromClause);
  143. $result .= $this->joinTranslations($fromClause);
  144. return $result;
  145. }
  146. /**
  147. * {@inheritDoc}
  148. */
  149. public function walkWhereClause($whereClause)
  150. {
  151. $result = parent::walkWhereClause($whereClause);
  152. return $this->replace($this->replacements, $result);
  153. }
  154. /**
  155. * {@inheritDoc}
  156. */
  157. public function walkHavingClause($havingClause)
  158. {
  159. $result = parent::walkHavingClause($havingClause);
  160. return $this->replace($this->replacements, $result);
  161. }
  162. /**
  163. * {@inheritDoc}
  164. */
  165. public function walkOrderByClause($orderByClause)
  166. {
  167. $result = parent::walkOrderByClause($orderByClause);
  168. return $this->replace($this->replacements, $result);
  169. }
  170. /**
  171. * {@inheritDoc}
  172. */
  173. public function walkSubselect($subselect)
  174. {
  175. $result = parent::walkSubselect($subselect);
  176. return $result;
  177. }
  178. /**
  179. * {@inheritDoc}
  180. */
  181. public function walkSubselectFromClause($subselectFromClause)
  182. {
  183. $result = parent::walkSubselectFromClause($subselectFromClause);
  184. $result .= $this->joinTranslations($subselectFromClause);
  185. return $result;
  186. }
  187. /**
  188. * {@inheritDoc}
  189. */
  190. public function walkSimpleSelectClause($simpleSelectClause)
  191. {
  192. $result = parent::walkSimpleSelectClause($simpleSelectClause);
  193. return $this->replace($this->replacements, $result);
  194. }
  195. /**
  196. * Walks from clause, and creates translation joins
  197. * for the translated components
  198. *
  199. * @param Doctrine\ORM\Query\AST\FromClause $from
  200. * @return string
  201. */
  202. private function joinTranslations($from)
  203. {
  204. $result = '';
  205. foreach ($from->identificationVariableDeclarations as $decl) {
  206. if ($decl->rangeVariableDeclaration instanceof RangeVariableDeclaration) {
  207. if (isset($this->components[$decl->rangeVariableDeclaration->aliasIdentificationVariable])) {
  208. $result .= $this->components[$decl->rangeVariableDeclaration->aliasIdentificationVariable];
  209. }
  210. }
  211. foreach ($decl->joinVariableDeclarations as $joinDecl) {
  212. if ($joinDecl->join instanceof Join) {
  213. if (isset($this->components[$joinDecl->join->aliasIdentificationVariable])) {
  214. $result .= $this->components[$joinDecl->join->aliasIdentificationVariable];
  215. }
  216. }
  217. }
  218. }
  219. return $result;
  220. }
  221. /**
  222. * Creates a left join list for translations
  223. * on used query components
  224. *
  225. * @todo: make it cleaner
  226. * @return string
  227. */
  228. private function prepareTranslatedComponents()
  229. {
  230. $em = $this->getEntityManager();
  231. $q = $this->getQuery();
  232. $ea = new TranslatableEventAdapter;
  233. $locale = $q->getHint(TranslationListener::HINT_TRANSLATABLE_LOCALE);
  234. if (!$locale) {
  235. // use from listener
  236. $locale = $this->listener->getListenerLocale();
  237. }
  238. $defaultLocale = $this->listener->getDefaultLocale();
  239. $joinStrategy = $q->getHint(TranslationListener::HINT_INNER_JOIN) ? 'INNER' : 'LEFT';
  240. foreach ($this->translatedComponents as $dqlAlias => $comp) {
  241. $meta = $comp['metadata'];
  242. $config = $this->listener->getConfiguration($em, $meta->name);
  243. $transClass = $this->listener->getTranslationClass($ea, $meta->name);
  244. $transMeta = $em->getClassMetadata($transClass);
  245. $transTable = $transMeta->getQuotedTableName($this->platform);
  246. if ($locale !== $defaultLocale) {
  247. foreach ($config['fields'] as $field) {
  248. $compTableName = $meta->getQuotedTableName($this->platform);
  249. $compTblAlias = $this->getSQLTableAlias($compTableName, $dqlAlias);
  250. $tblAlias = $this->getSQLTableAlias('trans'.$compTblAlias.$field);
  251. $sql = " {$joinStrategy} JOIN ".$transTable.' '.$tblAlias;
  252. $sql .= ' ON '.$tblAlias.'.'.$transMeta->getQuotedColumnName('locale', $this->platform)
  253. .' = '.$this->conn->quote($locale);
  254. $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('objectClass', $this->platform)
  255. .' = '.$this->conn->quote($meta->name);
  256. $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('field', $this->platform)
  257. .' = '.$this->conn->quote($field);
  258. $identifier = $meta->getSingleIdentifierFieldName();
  259. $colName = $meta->getQuotedColumnName($identifier, $this->platform);
  260. $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('foreignKey', $this->platform)
  261. .' = '.$compTblAlias.'.'.$colName;
  262. isset($this->components[$dqlAlias]) ? $this->components[$dqlAlias] .= $sql : $this->components[$dqlAlias] = $sql;
  263. if ($this->needsFallback()) {
  264. // COALESCE with the original record columns
  265. $this->replacements[$compTblAlias.'.'.$meta->getQuotedColumnName($field, $this->platform)]
  266. = 'COALESCE('.$tblAlias.'.'.$transMeta->getQuotedColumnName('content', $this->platform)
  267. .', '.$compTblAlias.'.'.$meta->getQuotedColumnName($field, $this->platform).')'
  268. ;
  269. } else {
  270. $this->replacements[$compTblAlias.'.'.$meta->getQuotedColumnName($field, $this->platform)]
  271. = $tblAlias.'.'.$transMeta->getQuotedColumnName('content', $this->platform)
  272. ;
  273. }
  274. }
  275. }
  276. }
  277. }
  278. /**
  279. * Checks if translation fallbacks are needed
  280. *
  281. * @return boolean
  282. */
  283. private function needsFallback()
  284. {
  285. $q = $this->getQuery();
  286. $fallback = $q->getHint(TranslationListener::HINT_FALLBACK);
  287. if (false === $fallback) {
  288. // non overrided
  289. $fallback = $this->listener->getTranslationFallback();
  290. }
  291. return (bool)$fallback
  292. && $q->getHydrationMode() !== Query::HYDRATE_SCALAR
  293. && $q->getHydrationMode() !== Query::HYDRATE_SINGLE_SCALAR;
  294. }
  295. /**
  296. * Search for translated components in the select clause
  297. *
  298. * @param array $queryComponents
  299. * @return void
  300. */
  301. private function extractTranslatedComponents(array $queryComponents)
  302. {
  303. $em = $this->getEntityManager();
  304. foreach ($queryComponents as $alias => $comp) {
  305. if (!isset($comp['metadata'])) {
  306. continue;
  307. }
  308. $meta = $comp['metadata'];
  309. $config = $this->listener->getConfiguration($em, $meta->name);
  310. if ($config && isset($config['fields'])) {
  311. $this->translatedComponents[$alias] = $comp;
  312. }
  313. }
  314. }
  315. /**
  316. * Get the currently used TranslationListener
  317. *
  318. * @throws \Gedmo\Exception\RuntimeException - if listener is not found
  319. * @return TranslationListener
  320. */
  321. private function getTranslationListener()
  322. {
  323. $translationListener = null;
  324. $em = $this->getEntityManager();
  325. foreach ($em->getEventManager()->getListeners() as $event => $listeners) {
  326. foreach ($listeners as $hash => $listener) {
  327. if ($listener instanceof TranslationListener) {
  328. $translationListener = $listener;
  329. break;
  330. }
  331. }
  332. if ($translationListener) {
  333. break;
  334. }
  335. }
  336. if (is_null($translationListener)) {
  337. throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
  338. }
  339. return $translationListener;
  340. }
  341. /**
  342. * Replaces given sql $str with required
  343. * results
  344. *
  345. * @param array $repl
  346. * @param string $str
  347. * @return string
  348. */
  349. private function replace(array $repl, $str)
  350. {
  351. foreach ($repl as $target => $result) {
  352. $str = preg_replace_callback('/(\s|\()('.$target.')(\s|\))/smi', function($m) use ($result) {
  353. return $m[1].$result.$m[3];
  354. }, $str);
  355. }
  356. return $str;
  357. }
  358. }