ResultSetMappingBuilder.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <?php
  2. /*
  3. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14. *
  15. * This software consists of voluntary contributions made by many individuals
  16. * and is licensed under the MIT license. For more information, see
  17. * <http://www.doctrine-project.org>.
  18. */
  19. namespace Doctrine\ORM\Query;
  20. use Doctrine\ORM\EntityManagerInterface;
  21. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  22. use Doctrine\ORM\Mapping\MappingException;
  23. use Doctrine\ORM\Utility\PersisterHelper;
  24. use InvalidArgumentException;
  25. use function explode;
  26. use function in_array;
  27. use function sprintf;
  28. use function strpos;
  29. use function strtolower;
  30. /**
  31. * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields.
  32. */
  33. class ResultSetMappingBuilder extends ResultSetMapping
  34. {
  35. /**
  36. * Picking this rename mode will register entity columns as is,
  37. * as they are in the database. This can cause clashes when multiple
  38. * entities are fetched that have columns with the same name.
  39. */
  40. public const COLUMN_RENAMING_NONE = 1;
  41. /**
  42. * Picking custom renaming allows the user to define the renaming
  43. * of specific columns with a rename array that contains column names as
  44. * keys and result alias as values.
  45. */
  46. public const COLUMN_RENAMING_CUSTOM = 2;
  47. /**
  48. * Incremental renaming uses a result set mapping internal counter to add a
  49. * number to each column result, leading to uniqueness. This only works if
  50. * you use {@see generateSelectClause()} to generate the SELECT clause for
  51. * you.
  52. */
  53. public const COLUMN_RENAMING_INCREMENT = 3;
  54. /** @var int */
  55. private $sqlCounter = 0;
  56. /** @var EntityManagerInterface */
  57. private $em;
  58. /**
  59. * Default column renaming mode.
  60. *
  61. * @var int
  62. */
  63. private $defaultRenameMode;
  64. /**
  65. * @param int $defaultRenameMode
  66. */
  67. public function __construct(EntityManagerInterface $em, $defaultRenameMode = self::COLUMN_RENAMING_NONE)
  68. {
  69. $this->em = $em;
  70. $this->defaultRenameMode = $defaultRenameMode;
  71. }
  72. /**
  73. * Adds a root entity and all of its fields to the result set.
  74. *
  75. * @param string $class The class name of the root entity.
  76. * @param string $alias The unique alias to use for the root entity.
  77. * @param int|null $renameMode One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM).
  78. *
  79. * @return void
  80. *
  81. * @psalm-param array<string, string> $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
  82. */
  83. public function addRootEntityFromClassMetadata($class, $alias, $renamedColumns = [], $renameMode = null)
  84. {
  85. $renameMode = $renameMode ?: $this->defaultRenameMode;
  86. $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns);
  87. $this->addEntityResult($class, $alias);
  88. $this->addAllClassFields($class, $alias, $columnAliasMap);
  89. }
  90. /**
  91. * Adds a joined entity and all of its fields to the result set.
  92. *
  93. * @param string $class The class name of the joined entity.
  94. * @param string $alias The unique alias to use for the joined entity.
  95. * @param string $parentAlias The alias of the entity result that is the parent of this joined result.
  96. * @param string $relation The association field that connects the parent entity result
  97. * with the joined entity result.
  98. * @param int|null $renameMode One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM).
  99. *
  100. * @return void
  101. *
  102. * @psalm-param array<string, string> $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
  103. */
  104. public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $renamedColumns = [], $renameMode = null)
  105. {
  106. $renameMode = $renameMode ?: $this->defaultRenameMode;
  107. $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns);
  108. $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation);
  109. $this->addAllClassFields($class, $alias, $columnAliasMap);
  110. }
  111. /**
  112. * Adds all fields of the given class to the result set mapping (columns and meta fields).
  113. *
  114. * @param string $class
  115. * @param string $alias
  116. *
  117. * @return void
  118. *
  119. * @throws InvalidArgumentException
  120. *
  121. * @psalm-param array<string, string> $columnAliasMap
  122. */
  123. protected function addAllClassFields($class, $alias, $columnAliasMap = [])
  124. {
  125. $classMetadata = $this->em->getClassMetadata($class);
  126. $platform = $this->em->getConnection()->getDatabasePlatform();
  127. if (! $this->isInheritanceSupported($classMetadata)) {
  128. throw new InvalidArgumentException('ResultSetMapping builder does not currently support your inheritance scheme.');
  129. }
  130. foreach ($classMetadata->getColumnNames() as $columnName) {
  131. $propertyName = $classMetadata->getFieldName($columnName);
  132. $columnAlias = $platform->getSQLResultCasing($columnAliasMap[$columnName]);
  133. if (isset($this->fieldMappings[$columnAlias])) {
  134. throw new InvalidArgumentException(sprintf(
  135. "The column '%s' conflicts with another column in the mapper.",
  136. $columnName
  137. ));
  138. }
  139. $this->addFieldResult($alias, $columnAlias, $propertyName);
  140. }
  141. foreach ($classMetadata->associationMappings as $associationMapping) {
  142. if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
  143. $targetClass = $this->em->getClassMetadata($associationMapping['targetEntity']);
  144. $isIdentifier = isset($associationMapping['id']) && $associationMapping['id'] === true;
  145. foreach ($associationMapping['joinColumns'] as $joinColumn) {
  146. $columnName = $joinColumn['name'];
  147. $columnAlias = $platform->getSQLResultCasing($columnAliasMap[$columnName]);
  148. $columnType = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
  149. if (isset($this->metaMappings[$columnAlias])) {
  150. throw new InvalidArgumentException(sprintf(
  151. "The column '%s' conflicts with another column in the mapper.",
  152. $columnAlias
  153. ));
  154. }
  155. $this->addMetaResult($alias, $columnAlias, $columnName, $isIdentifier, $columnType);
  156. }
  157. }
  158. }
  159. }
  160. private function isInheritanceSupported(ClassMetadataInfo $classMetadata): bool
  161. {
  162. if (
  163. $classMetadata->isInheritanceTypeSingleTable()
  164. && in_array($classMetadata->name, $classMetadata->discriminatorMap, true)
  165. ) {
  166. return true;
  167. }
  168. return ! ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined());
  169. }
  170. /**
  171. * Gets column alias for a given column.
  172. *
  173. * @param string $columnName
  174. * @param int $mode
  175. *
  176. * @return string
  177. *
  178. * @psalm-param array<string, string> $customRenameColumns
  179. */
  180. private function getColumnAlias($columnName, $mode, array $customRenameColumns)
  181. {
  182. switch ($mode) {
  183. case self::COLUMN_RENAMING_INCREMENT:
  184. return $columnName . $this->sqlCounter++;
  185. case self::COLUMN_RENAMING_CUSTOM:
  186. return $customRenameColumns[$columnName] ?? $columnName;
  187. case self::COLUMN_RENAMING_NONE:
  188. return $columnName;
  189. }
  190. }
  191. /**
  192. * Retrieves a class columns and join columns aliases that are used in the SELECT clause.
  193. *
  194. * This depends on the renaming mode selected by the user.
  195. *
  196. * @param string $className
  197. * @param int $mode
  198. *
  199. * @return string[]
  200. *
  201. * @psalm-param array<string, string> $customRenameColumns
  202. * @psalm-return array<array-key, string>
  203. */
  204. private function getColumnAliasMap($className, $mode, array $customRenameColumns)
  205. {
  206. if ($customRenameColumns) { // for BC with 2.2-2.3 API
  207. $mode = self::COLUMN_RENAMING_CUSTOM;
  208. }
  209. $columnAlias = [];
  210. $class = $this->em->getClassMetadata($className);
  211. foreach ($class->getColumnNames() as $columnName) {
  212. $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns);
  213. }
  214. foreach ($class->associationMappings as $associationMapping) {
  215. if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
  216. foreach ($associationMapping['joinColumns'] as $joinColumn) {
  217. $columnName = $joinColumn['name'];
  218. $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns);
  219. }
  220. }
  221. }
  222. return $columnAlias;
  223. }
  224. /**
  225. * Adds the mappings of the results of native SQL queries to the result set.
  226. *
  227. * @param mixed[] $queryMapping
  228. *
  229. * @return ResultSetMappingBuilder
  230. */
  231. public function addNamedNativeQueryMapping(ClassMetadataInfo $class, array $queryMapping)
  232. {
  233. if (isset($queryMapping['resultClass'])) {
  234. return $this->addNamedNativeQueryResultClassMapping($class, $queryMapping['resultClass']);
  235. }
  236. return $this->addNamedNativeQueryResultSetMapping($class, $queryMapping['resultSetMapping']);
  237. }
  238. /**
  239. * Adds the class mapping of the results of native SQL queries to the result set.
  240. *
  241. * @param string $resultClassName
  242. *
  243. * @return self
  244. */
  245. public function addNamedNativeQueryResultClassMapping(ClassMetadataInfo $class, $resultClassName)
  246. {
  247. $classMetadata = $this->em->getClassMetadata($resultClassName);
  248. $shortName = $classMetadata->reflClass->getShortName();
  249. $alias = strtolower($shortName[0]) . '0';
  250. $this->addEntityResult($class->name, $alias);
  251. if ($classMetadata->discriminatorColumn) {
  252. $discrColumn = $classMetadata->discriminatorColumn;
  253. $this->setDiscriminatorColumn($alias, $discrColumn['name']);
  254. $this->addMetaResult($alias, $discrColumn['name'], $discrColumn['fieldName'], false, $discrColumn['type']);
  255. }
  256. foreach ($classMetadata->getColumnNames() as $key => $columnName) {
  257. $propertyName = $classMetadata->getFieldName($columnName);
  258. $this->addFieldResult($alias, $columnName, $propertyName);
  259. }
  260. foreach ($classMetadata->associationMappings as $associationMapping) {
  261. if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
  262. $targetClass = $this->em->getClassMetadata($associationMapping['targetEntity']);
  263. foreach ($associationMapping['joinColumns'] as $joinColumn) {
  264. $columnName = $joinColumn['name'];
  265. $columnType = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
  266. $this->addMetaResult($alias, $columnName, $columnName, $classMetadata->isIdentifier($columnName), $columnType);
  267. }
  268. }
  269. }
  270. return $this;
  271. }
  272. /**
  273. * Adds the result set mapping of the results of native SQL queries to the result set.
  274. *
  275. * @param string $resultSetMappingName
  276. *
  277. * @return self
  278. */
  279. public function addNamedNativeQueryResultSetMapping(ClassMetadataInfo $class, $resultSetMappingName)
  280. {
  281. $counter = 0;
  282. $resultMapping = $class->getSqlResultSetMapping($resultSetMappingName);
  283. $rootShortName = $class->reflClass->getShortName();
  284. $rootAlias = strtolower($rootShortName[0]) . $counter;
  285. if (isset($resultMapping['entities'])) {
  286. foreach ($resultMapping['entities'] as $key => $entityMapping) {
  287. $classMetadata = $this->em->getClassMetadata($entityMapping['entityClass']);
  288. if ($class->reflClass->name === $classMetadata->reflClass->name) {
  289. $this->addEntityResult($classMetadata->name, $rootAlias);
  290. $this->addNamedNativeQueryEntityResultMapping($classMetadata, $entityMapping, $rootAlias);
  291. } else {
  292. $shortName = $classMetadata->reflClass->getShortName();
  293. $joinAlias = strtolower($shortName[0]) . ++$counter;
  294. $associations = $class->getAssociationsByTargetClass($classMetadata->name);
  295. $this->addNamedNativeQueryEntityResultMapping($classMetadata, $entityMapping, $joinAlias);
  296. foreach ($associations as $relation => $mapping) {
  297. $this->addJoinedEntityResult($mapping['targetEntity'], $joinAlias, $rootAlias, $relation);
  298. }
  299. }
  300. }
  301. }
  302. if (isset($resultMapping['columns'])) {
  303. foreach ($resultMapping['columns'] as $entityMapping) {
  304. $type = isset($class->fieldNames[$entityMapping['name']])
  305. ? PersisterHelper::getTypeOfColumn($entityMapping['name'], $class, $this->em)
  306. : 'string';
  307. $this->addScalarResult($entityMapping['name'], $entityMapping['name'], $type);
  308. }
  309. }
  310. return $this;
  311. }
  312. /**
  313. * Adds the entity result mapping of the results of native SQL queries to the result set.
  314. *
  315. * @param mixed[] $entityMapping
  316. * @param string $alias
  317. *
  318. * @return self
  319. *
  320. * @throws MappingException
  321. * @throws InvalidArgumentException
  322. */
  323. public function addNamedNativeQueryEntityResultMapping(ClassMetadataInfo $classMetadata, array $entityMapping, $alias)
  324. {
  325. if (isset($entityMapping['discriminatorColumn']) && $entityMapping['discriminatorColumn']) {
  326. $discriminatorColumn = $entityMapping['discriminatorColumn'];
  327. $discriminatorType = $classMetadata->discriminatorColumn['type'];
  328. $this->setDiscriminatorColumn($alias, $discriminatorColumn);
  329. $this->addMetaResult($alias, $discriminatorColumn, $discriminatorColumn, false, $discriminatorType);
  330. }
  331. if (isset($entityMapping['fields']) && ! empty($entityMapping['fields'])) {
  332. foreach ($entityMapping['fields'] as $field) {
  333. $fieldName = $field['name'];
  334. $relation = null;
  335. if (strpos($fieldName, '.') !== false) {
  336. [$relation, $fieldName] = explode('.', $fieldName);
  337. }
  338. if (isset($classMetadata->associationMappings[$relation])) {
  339. if ($relation) {
  340. $associationMapping = $classMetadata->associationMappings[$relation];
  341. $joinAlias = $alias . $relation;
  342. $parentAlias = $alias;
  343. $this->addJoinedEntityResult($associationMapping['targetEntity'], $joinAlias, $parentAlias, $relation);
  344. $this->addFieldResult($joinAlias, $field['column'], $fieldName);
  345. } else {
  346. $this->addFieldResult($alias, $field['column'], $fieldName, $classMetadata->name);
  347. }
  348. } else {
  349. if (! isset($classMetadata->fieldMappings[$fieldName])) {
  350. throw new InvalidArgumentException("Entity '" . $classMetadata->name . "' has no field '" . $fieldName . "'. ");
  351. }
  352. $this->addFieldResult($alias, $field['column'], $fieldName, $classMetadata->name);
  353. }
  354. }
  355. } else {
  356. foreach ($classMetadata->getColumnNames() as $columnName) {
  357. $propertyName = $classMetadata->getFieldName($columnName);
  358. $this->addFieldResult($alias, $columnName, $propertyName);
  359. }
  360. }
  361. return $this;
  362. }
  363. /**
  364. * Generates the Select clause from this ResultSetMappingBuilder.
  365. *
  366. * Works only for all the entity results. The select parts for scalar
  367. * expressions have to be written manually.
  368. *
  369. * @return string
  370. *
  371. * @psalm-param array<string, string> $tableAliases
  372. */
  373. public function generateSelectClause($tableAliases = [])
  374. {
  375. $sql = '';
  376. foreach ($this->columnOwnerMap as $columnName => $dqlAlias) {
  377. $tableAlias = $tableAliases[$dqlAlias] ?? $dqlAlias;
  378. if ($sql) {
  379. $sql .= ', ';
  380. }
  381. $sql .= $tableAlias . '.';
  382. if (isset($this->fieldMappings[$columnName])) {
  383. $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]);
  384. $sql .= $class->fieldMappings[$this->fieldMappings[$columnName]]['columnName'];
  385. } elseif (isset($this->metaMappings[$columnName])) {
  386. $sql .= $this->metaMappings[$columnName];
  387. } elseif (isset($this->discriminatorColumns[$dqlAlias])) {
  388. $sql .= $this->discriminatorColumns[$dqlAlias];
  389. }
  390. $sql .= ' AS ' . $columnName;
  391. }
  392. return $sql;
  393. }
  394. /**
  395. * @return string
  396. */
  397. public function __toString()
  398. {
  399. return $this->generateSelectClause([]);
  400. }
  401. }