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
20namespace Doctrine\ORM\Query;
21
22use Doctrine\ORM\EntityManagerInterface;
23use Doctrine\ORM\Mapping\ClassMetadataInfo;
24
25/**
26 * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields.
27 *
28 * @author Michael Ridgway <mcridgway@gmail.com>
29 * @since 2.1
30 */
31class ResultSetMappingBuilder extends ResultSetMapping
32{
33    /**
34     * Picking this rename mode will register entity columns as is,
35     * as they are in the database. This can cause clashes when multiple
36     * entities are fetched that have columns with the same name.
37     *
38     * @var int
39     */
40    const COLUMN_RENAMING_NONE = 1;
41
42    /**
43     * Picking custom renaming allows the user to define the renaming
44     * of specific columns with a rename array that contains column names as
45     * keys and result alias as values.
46     *
47     * @var int
48     */
49    const COLUMN_RENAMING_CUSTOM = 2;
50
51    /**
52     * Incremental renaming uses a result set mapping internal counter to add a
53     * number to each column result, leading to uniqueness. This only works if
54     * you use {@see generateSelectClause()} to generate the SELECT clause for
55     * you.
56     *
57     * @var int
58     */
59    const COLUMN_RENAMING_INCREMENT = 3;
60
61    /**
62     * @var int
63     */
64    private $sqlCounter = 0;
65
66    /**
67     * @var EntityManagerInterface
68     */
69    private $em;
70
71    /**
72     * Default column renaming mode.
73     *
74     * @var int
75     */
76    private $defaultRenameMode;
77
78    /**
79     * @param EntityManagerInterface $em
80     * @param integer                $defaultRenameMode
81     */
82    public function __construct(EntityManagerInterface $em, $defaultRenameMode = self::COLUMN_RENAMING_NONE)
83    {
84        $this->em                = $em;
85        $this->defaultRenameMode = $defaultRenameMode;
86    }
87
88    /**
89     * Adds a root entity and all of its fields to the result set.
90     *
91     * @param string   $class          The class name of the root entity.
92     * @param string   $alias          The unique alias to use for the root entity.
93     * @param array    $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
94     * @param int|null $renameMode     One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM).
95     *
96     * @return void
97     */
98    public function addRootEntityFromClassMetadata($class, $alias, $renamedColumns = array(), $renameMode = null)
99    {
100        $renameMode     = $renameMode ?: $this->defaultRenameMode;
101        $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns);
102
103        $this->addEntityResult($class, $alias);
104        $this->addAllClassFields($class, $alias, $columnAliasMap);
105    }
106
107    /**
108     * Adds a joined entity and all of its fields to the result set.
109     *
110     * @param string   $class          The class name of the joined entity.
111     * @param string   $alias          The unique alias to use for the joined entity.
112     * @param string   $parentAlias    The alias of the entity result that is the parent of this joined result.
113     * @param string   $relation       The association field that connects the parent entity result
114     *                                 with the joined entity result.
115     * @param array    $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
116     * @param int|null $renameMode     One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM).
117     *
118     * @return void
119     */
120    public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $renamedColumns = array(), $renameMode = null)
121    {
122        $renameMode     = $renameMode ?: $this->defaultRenameMode;
123        $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns);
124
125        $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation);
126        $this->addAllClassFields($class, $alias, $columnAliasMap);
127    }
128
129    /**
130     * Adds all fields of the given class to the result set mapping (columns and meta fields).
131     *
132     * @param string $class
133     * @param string $alias
134     * @param array  $columnAliasMap
135     *
136     * @return void
137     *
138     * @throws \InvalidArgumentException
139     */
140    protected function addAllClassFields($class, $alias, $columnAliasMap = array())
141    {
142        $classMetadata = $this->em->getClassMetadata($class);
143        $platform      = $this->em->getConnection()->getDatabasePlatform();
144
145        if ( ! $this->isInheritanceSupported($classMetadata)) {
146            throw new \InvalidArgumentException('ResultSetMapping builder does not currently support your inheritance scheme.');
147        }
148
149
150        foreach ($classMetadata->getColumnNames() as $columnName) {
151            $propertyName = $classMetadata->getFieldName($columnName);
152            $columnAlias  = $platform->getSQLResultCasing($columnAliasMap[$columnName]);
153
154            if (isset($this->fieldMappings[$columnAlias])) {
155                throw new \InvalidArgumentException("The column '$columnName' conflicts with another column in the mapper.");
156            }
157
158            $this->addFieldResult($alias, $columnAlias, $propertyName);
159        }
160
161        foreach ($classMetadata->associationMappings as $associationMapping) {
162            if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
163                foreach ($associationMapping['joinColumns'] as $joinColumn) {
164                    $columnName  = $joinColumn['name'];
165                    $columnAlias = $platform->getSQLResultCasing($columnAliasMap[$columnName]);
166
167                    if (isset($this->metaMappings[$columnAlias])) {
168                        throw new \InvalidArgumentException("The column '$columnAlias' conflicts with another column in the mapper.");
169                    }
170
171                    $this->addMetaResult(
172                        $alias,
173                        $columnAlias,
174                        $columnName,
175                        (isset($associationMapping['id']) && $associationMapping['id'] === true)
176                    );
177                }
178            }
179        }
180    }
181
182    private function isInheritanceSupported(ClassMetadataInfo $classMetadata)
183    {
184        if ($classMetadata->isInheritanceTypeSingleTable()
185            && in_array($classMetadata->name, $classMetadata->discriminatorMap, true)) {
186            return true;
187        }
188
189        return ! ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined());
190    }
191
192    /**
193     * Gets column alias for a given column.
194     *
195     * @param string $columnName
196     * @param int    $mode
197     * @param array  $customRenameColumns
198     *
199     * @return string
200     */
201    private function getColumnAlias($columnName, $mode, array $customRenameColumns)
202    {
203        switch ($mode) {
204            case self::COLUMN_RENAMING_INCREMENT:
205                return $columnName . $this->sqlCounter++;
206
207            case self::COLUMN_RENAMING_CUSTOM:
208                return isset($customRenameColumns[$columnName])
209                    ? $customRenameColumns[$columnName] : $columnName;
210
211            case self::COLUMN_RENAMING_NONE:
212                return $columnName;
213
214        }
215    }
216
217    /**
218     * Retrieves a class columns and join columns aliases that are used in the SELECT clause.
219     *
220     * This depends on the renaming mode selected by the user.
221     *
222     * @param string $className
223     * @param int    $mode
224     * @param array  $customRenameColumns
225     *
226     * @return array
227     */
228    private function getColumnAliasMap($className, $mode, array $customRenameColumns)
229    {
230        if ($customRenameColumns) { // for BC with 2.2-2.3 API
231            $mode = self::COLUMN_RENAMING_CUSTOM;
232        }
233
234        $columnAlias = array();
235        $class       = $this->em->getClassMetadata($className);
236
237        foreach ($class->getColumnNames() as $columnName) {
238            $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns);
239        }
240
241        foreach ($class->associationMappings as $associationMapping) {
242            if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
243                foreach ($associationMapping['joinColumns'] as $joinColumn) {
244                    $columnName = $joinColumn['name'];
245                    $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns);
246                }
247            }
248        }
249
250        return $columnAlias;
251    }
252
253    /**
254     * Adds the mappings of the results of native SQL queries to the result set.
255     *
256     * @param ClassMetadataInfo $class
257     * @param array             $queryMapping
258     *
259     * @return ResultSetMappingBuilder
260     */
261    public function addNamedNativeQueryMapping(ClassMetadataInfo $class, array $queryMapping)
262    {
263        if (isset($queryMapping['resultClass'])) {
264            return $this->addNamedNativeQueryResultClassMapping($class, $queryMapping['resultClass']);
265        }
266
267        return $this->addNamedNativeQueryResultSetMapping($class, $queryMapping['resultSetMapping']);
268    }
269
270    /**
271     * Adds the class mapping of the results of native SQL queries to the result set.
272     *
273     * @param ClassMetadataInfo $class
274     * @param string            $resultClassName
275     *
276     * @return  ResultSetMappingBuilder
277     */
278    public function addNamedNativeQueryResultClassMapping(ClassMetadataInfo $class, $resultClassName)
279    {
280        $classMetadata  = $this->em->getClassMetadata($resultClassName);
281        $shortName      = $classMetadata->reflClass->getShortName();
282        $alias          = strtolower($shortName[0]).'0';
283
284        $this->addEntityResult($class->name, $alias);
285
286        if ($classMetadata->discriminatorColumn) {
287            $discriminatorColumn = $classMetadata->discriminatorColumn;
288            $this->setDiscriminatorColumn($alias, $discriminatorColumn['name']);
289            $this->addMetaResult($alias, $discriminatorColumn['name'], $discriminatorColumn['fieldName']);
290        }
291
292        foreach ($classMetadata->getColumnNames() as $key => $columnName) {
293            $propertyName   = $classMetadata->getFieldName($columnName);
294            $this->addFieldResult($alias, $columnName, $propertyName);
295        }
296
297        foreach ($classMetadata->associationMappings as $associationMapping) {
298            if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) {
299                foreach ($associationMapping['joinColumns'] as $joinColumn) {
300                    $columnName = $joinColumn['name'];
301                    $this->addMetaResult($alias, $columnName, $columnName, $classMetadata->isIdentifier($columnName));
302                }
303            }
304        }
305
306        return $this;
307    }
308
309    /**
310     * Adds the result set mapping of the results of native SQL queries to the result set.
311     *
312     * @param ClassMetadataInfo $class
313     * @param string            $resultSetMappingName
314     *
315     * @return ResultSetMappingBuilder
316     */
317    public function addNamedNativeQueryResultSetMapping(ClassMetadataInfo $class, $resultSetMappingName)
318    {
319        $counter        = 0;
320        $resultMapping  = $class->getSqlResultSetMapping($resultSetMappingName);
321        $rooShortName   = $class->reflClass->getShortName();
322        $rootAlias      = strtolower($rooShortName[0]) . $counter;
323
324
325        if (isset($resultMapping['entities'])) {
326            foreach ($resultMapping['entities'] as $key => $entityMapping) {
327                $classMetadata  = $this->em->getClassMetadata($entityMapping['entityClass']);
328
329                if ($class->reflClass->name == $classMetadata->reflClass->name) {
330                    $this->addEntityResult($classMetadata->name, $rootAlias);
331                    $this->addNamedNativeQueryEntityResultMapping($classMetadata, $entityMapping, $rootAlias);
332                } else {
333                    $shortName      = $classMetadata->reflClass->getShortName();
334                    $joinAlias      = strtolower($shortName[0]) . ++ $counter;
335                    $associations   = $class->getAssociationsByTargetClass($classMetadata->name);
336
337                    foreach ($associations as $relation => $mapping) {
338                        $this->addJoinedEntityResult($mapping['targetEntity'], $joinAlias, $rootAlias, $relation);
339                        $this->addNamedNativeQueryEntityResultMapping($classMetadata, $entityMapping, $joinAlias);
340                    }
341                }
342
343            }
344        }
345
346        if (isset($resultMapping['columns'])) {
347            foreach ($resultMapping['columns'] as $entityMapping) {
348                $this->addScalarResult($entityMapping['name'], $entityMapping['name']);
349            }
350        }
351
352        return $this;
353    }
354
355    /**
356     * Adds the entity result mapping of the results of native SQL queries to the result set.
357     *
358     * @param ClassMetadataInfo $classMetadata
359     * @param array             $entityMapping
360     * @param string            $alias
361     *
362     * @return ResultSetMappingBuilder
363     *
364     * @throws \InvalidArgumentException
365     */
366    public function addNamedNativeQueryEntityResultMapping(ClassMetadataInfo $classMetadata, array $entityMapping, $alias)
367    {
368        if (isset($entityMapping['discriminatorColumn']) && $entityMapping['discriminatorColumn']) {
369            $discriminatorColumn = $entityMapping['discriminatorColumn'];
370            $this->setDiscriminatorColumn($alias, $discriminatorColumn);
371            $this->addMetaResult($alias, $discriminatorColumn, $discriminatorColumn);
372        }
373
374        if (isset($entityMapping['fields']) && !empty($entityMapping['fields'])) {
375            foreach ($entityMapping['fields'] as $field) {
376                $fieldName = $field['name'];
377                $relation  = null;
378
379                if(strpos($fieldName, '.')){
380                    list($relation, $fieldName) = explode('.', $fieldName);
381                }
382
383                if (isset($classMetadata->associationMappings[$relation])) {
384                    if($relation) {
385                        $associationMapping = $classMetadata->associationMappings[$relation];
386                        $joinAlias          = $alias.$relation;
387                        $parentAlias        = $alias;
388
389                        $this->addJoinedEntityResult($associationMapping['targetEntity'], $joinAlias, $parentAlias, $relation);
390                        $this->addFieldResult($joinAlias, $field['column'], $fieldName);
391                    }else {
392                        $this->addFieldResult($alias, $field['column'], $fieldName, $classMetadata->name);
393                    }
394                } else {
395                    if(!isset($classMetadata->fieldMappings[$fieldName])) {
396                        throw new \InvalidArgumentException("Entity '".$classMetadata->name."' has no field '".$fieldName."'. ");
397                    }
398                    $this->addFieldResult($alias, $field['column'], $fieldName, $classMetadata->name);
399                }
400            }
401
402        } else {
403            foreach ($classMetadata->getColumnNames() as $columnName) {
404                $propertyName   = $classMetadata->getFieldName($columnName);
405                $this->addFieldResult($alias, $columnName, $propertyName);
406            }
407        }
408
409        return $this;
410    }
411
412    /**
413     * Generates the Select clause from this ResultSetMappingBuilder.
414     *
415     * Works only for all the entity results. The select parts for scalar
416     * expressions have to be written manually.
417     *
418     * @param array $tableAliases
419     *
420     * @return string
421     */
422    public function generateSelectClause($tableAliases = array())
423    {
424        $sql = "";
425
426        foreach ($this->columnOwnerMap as $columnName => $dqlAlias) {
427            $tableAlias = isset($tableAliases[$dqlAlias])
428                ? $tableAliases[$dqlAlias] : $dqlAlias;
429
430            if ($sql) {
431                $sql .= ", ";
432            }
433
434            $sql .= $tableAlias . ".";
435
436            if (isset($this->fieldMappings[$columnName])) {
437                $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]);
438                $sql  .= $class->fieldMappings[$this->fieldMappings[$columnName]]['columnName'];
439            } else if (isset($this->metaMappings[$columnName])) {
440                $sql .= $this->metaMappings[$columnName];
441            } else if (isset($this->discriminatorColumns[$dqlAlias])) {
442                $sql .= $this->discriminatorColumns[$dqlAlias];
443            }
444
445            $sql .= " AS " . $columnName;
446        }
447
448        return $sql;
449    }
450
451    /**
452     * @return string
453     */
454    public function __toString()
455    {
456        return $this->generateSelectClause(array());
457    }
458}
459