TranslationWalker.php 13 KB

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