TranslationWalker.php 13 KB

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