TranslationWalker.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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. $q = $this->getQuery();
  231. $locale = $q->getHint(TranslationListener::HINT_TRANSLATABLE_LOCALE);
  232. if (!$locale) {
  233. // use from listener
  234. $locale = $this->listener->getListenerLocale();
  235. }
  236. $defaultLocale = $this->listener->getDefaultLocale();
  237. if ($locale === $defaultLocale) {
  238. // Skip preparation as there's no need to translate anything
  239. return;
  240. }
  241. $em = $this->getEntityManager();
  242. $ea = new TranslatableEventAdapter;
  243. $joinStrategy = $q->getHint(TranslationListener::HINT_INNER_JOIN) ? 'INNER' : 'LEFT';
  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. foreach ($config['fields'] as $field) {
  251. $compTableName = $meta->getQuotedTableName($this->platform);
  252. $compTblAlias = $this->getSQLTableAlias($compTableName, $dqlAlias);
  253. $tblAlias = $this->getSQLTableAlias('trans'.$compTblAlias.$field);
  254. $sql = " {$joinStrategy} JOIN ".$transTable.' '.$tblAlias;
  255. $sql .= ' ON '.$tblAlias.'.'.$transMeta->getQuotedColumnName('locale', $this->platform)
  256. .' = '.$this->conn->quote($locale);
  257. $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('objectClass', $this->platform)
  258. .' = '.$this->conn->quote($meta->name);
  259. $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('field', $this->platform)
  260. .' = '.$this->conn->quote($field);
  261. $identifier = $meta->getSingleIdentifierFieldName();
  262. $colName = $meta->getQuotedColumnName($identifier, $this->platform);
  263. $sql .= ' AND '.$tblAlias.'.'.$transMeta->getQuotedColumnName('foreignKey', $this->platform)
  264. .' = '.$compTblAlias.'.'.$colName;
  265. isset($this->components[$dqlAlias]) ? $this->components[$dqlAlias] .= $sql : $this->components[$dqlAlias] = $sql;
  266. $originalField = $compTblAlias.'.'.$meta->getQuotedColumnName($field, $this->platform);
  267. $substituteField = $tblAlias . '.' . $transMeta->getQuotedColumnName('content', $this->platform);
  268. // If original field is integer - treat translation as integer (for ORDER BY, WHERE, etc)
  269. $fieldMapping = $meta->getFieldMapping($field);
  270. if (in_array($fieldMapping["type"], array("integer", "bigint", "tinyint", "int"))) {
  271. $substituteField = 'CAST(' . $substituteField . ' AS SIGNED)';
  272. }
  273. // Fallback to original if was asked for
  274. if ($this->needsFallback()) {
  275. $substituteField = 'COALESCE('.$substituteField.', '.$originalField.')';
  276. }
  277. $this->replacements[$originalField] = $substituteField;
  278. }
  279. }
  280. }
  281. /**
  282. * Checks if translation fallbacks are needed
  283. *
  284. * @return boolean
  285. */
  286. private function needsFallback()
  287. {
  288. $q = $this->getQuery();
  289. $fallback = $q->getHint(TranslationListener::HINT_FALLBACK);
  290. if (false === $fallback) {
  291. // non overrided
  292. $fallback = $this->listener->getTranslationFallback();
  293. }
  294. return (bool)$fallback
  295. && $q->getHydrationMode() !== Query::HYDRATE_SCALAR
  296. && $q->getHydrationMode() !== Query::HYDRATE_SINGLE_SCALAR;
  297. }
  298. /**
  299. * Search for translated components in the select clause
  300. *
  301. * @param array $queryComponents
  302. * @return void
  303. */
  304. private function extractTranslatedComponents(array $queryComponents)
  305. {
  306. $em = $this->getEntityManager();
  307. foreach ($queryComponents as $alias => $comp) {
  308. if (!isset($comp['metadata'])) {
  309. continue;
  310. }
  311. $meta = $comp['metadata'];
  312. $config = $this->listener->getConfiguration($em, $meta->name);
  313. if ($config && isset($config['fields'])) {
  314. $this->translatedComponents[$alias] = $comp;
  315. }
  316. }
  317. }
  318. /**
  319. * Get the currently used TranslationListener
  320. *
  321. * @throws \Gedmo\Exception\RuntimeException - if listener is not found
  322. * @return TranslationListener
  323. */
  324. private function getTranslationListener()
  325. {
  326. $translationListener = null;
  327. $em = $this->getEntityManager();
  328. foreach ($em->getEventManager()->getListeners() as $event => $listeners) {
  329. foreach ($listeners as $hash => $listener) {
  330. if ($listener instanceof TranslationListener) {
  331. $translationListener = $listener;
  332. break;
  333. }
  334. }
  335. if ($translationListener) {
  336. break;
  337. }
  338. }
  339. if (is_null($translationListener)) {
  340. throw new \Gedmo\Exception\RuntimeException('The translation listener could not be found');
  341. }
  342. return $translationListener;
  343. }
  344. /**
  345. * Replaces given sql $str with required
  346. * results
  347. *
  348. * @param array $repl
  349. * @param string $str
  350. * @return string
  351. */
  352. private function replace(array $repl, $str)
  353. {
  354. foreach ($repl as $target => $result) {
  355. $str = preg_replace_callback('/(\s|\()('.$target.')(\s|\))/smi', function($m) use ($result) {
  356. return $m[1].$result.$m[3];
  357. }, $str);
  358. }
  359. return $str;
  360. }
  361. }