Passed
Push — master ( b9880b...e1bb9e )
by Guilherme
09:04
created

addJoinedEntityFromClassMetadata()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 2
nop 6
dl 0
loc 12
ccs 4
cts 4
cp 1
crap 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Query;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use Doctrine\ORM\Mapping\ClassMetadata;
9
use Doctrine\ORM\Mapping\FieldMetadata;
10
use Doctrine\ORM\Mapping\InheritanceType;
11
use Doctrine\ORM\Mapping\JoinColumnMetadata;
12
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
13
use Doctrine\ORM\Utility\PersisterHelper;
14
use InvalidArgumentException;
15
use function in_array;
16
use function sprintf;
17
18
/**
19
 * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields.
20
 */
21
class ResultSetMappingBuilder extends ResultSetMapping
22
{
23
    /**
24
     * Picking this rename mode will register entity columns as is,
25
     * as they are in the database. This can cause clashes when multiple
26
     * entities are fetched that have columns with the same name.
27
     */
28
    public const COLUMN_RENAMING_NONE = 1;
29
30
    /**
31
     * Picking custom renaming allows the user to define the renaming
32
     * of specific columns with a rename array that contains column names as
33
     * keys and result alias as values.
34
     */
35
    public const COLUMN_RENAMING_CUSTOM = 2;
36
37
    /**
38
     * Incremental renaming uses a result set mapping internal counter to add a
39
     * number to each column result, leading to uniqueness. This only works if
40
     * you use {@see generateSelectClause()} to generate the SELECT clause for
41
     * you.
42
     */
43
    public const COLUMN_RENAMING_INCREMENT = 3;
44
45
    /** @var int */
46
    private $sqlCounter = 0;
47
48
    /** @var EntityManagerInterface */
49
    private $em;
50
51
    /**
52
     * Default column renaming mode.
53
     *
54
     * @var int
55
     */
56
    private $defaultRenameMode;
57
58
    /**
59
     * @param int $defaultRenameMode
60
     */
61 52
    public function __construct(EntityManagerInterface $em, $defaultRenameMode = self::COLUMN_RENAMING_NONE)
62
    {
63 52
        $this->em                = $em;
64 52
        $this->defaultRenameMode = $defaultRenameMode;
65 52
    }
66
67
    /**
68
     * Adds a root entity and all of its fields to the result set.
69
     *
70
     * @param string   $class          The class name of the root entity.
71
     * @param string   $alias          The unique alias to use for the root entity.
72
     * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
73
     * @param int|null $renameMode     One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM).
74
     */
75 43
    public function addRootEntityFromClassMetadata(
76
        string $class,
77
        string $alias,
78
        array $renamedColumns = [],
79
        ?int $renameMode = null
80
    ) {
81 43
        $renameMode = $renameMode ?: (empty($renamedColumns) ? $this->defaultRenameMode : self::COLUMN_RENAMING_CUSTOM);
82
83 43
        $this->addEntityResult($class, $alias);
84 43
        $this->addAllClassFields($class, $alias, $renamedColumns, $renameMode);
85 42
    }
86
87
    /**
88
     * Adds a joined entity and all of its fields to the result set.
89
     *
90
     * @param string   $class          The class name of the joined entity.
91
     * @param string   $alias          The unique alias to use for the joined entity.
92
     * @param string   $parentAlias    The alias of the entity result that is the parent of this joined result.
93
     * @param string   $relation       The association field that connects the parent entity result
94
     *                                 with the joined entity result.
95
     * @param string[] $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName).
96
     * @param int|null $renameMode     One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM).
97
     */
98 11
    public function addJoinedEntityFromClassMetadata(
99
        string $class,
100
        string $alias,
101
        string $parentAlias,
102
        string $relation,
103
        array $renamedColumns = [],
104
        ?int $renameMode = null
105
    ) {
106 11
        $renameMode = $renameMode ?: (empty($renamedColumns) ? $this->defaultRenameMode : self::COLUMN_RENAMING_CUSTOM);
107
108 11
        $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation);
109 11
        $this->addAllClassFields($class, $alias, $renamedColumns, $renameMode);
110 10
    }
111
112
    /**
113
     * Adds all fields of the given class to the result set mapping (columns and meta fields).
114
     *
115
     * @param string[] $customRenameColumns
116
     *
117
     * @throws InvalidArgumentException
118
     */
119 43
    protected function addAllClassFields(string $class, string $alias, array $customRenameColumns, int $renameMode) : void
120
    {
121
        /** @var ClassMetadata $classMetadata */
122 43
        $classMetadata = $this->em->getClassMetadata($class);
123 43
        $platform      = $this->em->getConnection()->getDatabasePlatform();
124
125 43
        if (! $this->isInheritanceSupported($classMetadata)) {
126 1
            throw new InvalidArgumentException(
127 1
                'ResultSetMapping builder does not currently support your inheritance scheme.'
128
            );
129
        }
130
131 42
        foreach ($classMetadata->getDeclaredPropertiesIterator() as $property) {
132
            switch (true) {
133 42
                case $property instanceof FieldMetadata:
134 42
                    $columnName  = $property->getColumnName();
135 42
                    $columnAlias = $platform->getSQLResultCasing(
136 42
                        $this->getColumnAlias($columnName, $renameMode, $customRenameColumns)
137
                    );
138
139 42
                    if (isset($this->fieldMappings[$columnAlias])) {
140 1
                        throw new InvalidArgumentException(
141 1
                            sprintf("The column '%s' conflicts with another column in the mapper.", $columnName)
142
                        );
143
                    }
144
145 42
                    $this->addFieldResult($alias, $columnAlias, $property->getName());
146 42
                    break;
147
148 32
                case $property instanceof ToOneAssociationMetadata && $property->isOwningSide():
149 26
                    $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
0 ignored issues
show
Bug introduced by
The method getTargetEntity() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\AssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

149
                    $targetClass = $this->em->getClassMetadata($property->/** @scrutinizer ignore-call */ getTargetEntity());
Loading history...
150
151 26
                    foreach ($property->getJoinColumns() as $joinColumn) {
0 ignored issues
show
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

151
                    foreach ($property->/** @scrutinizer ignore-call */ getJoinColumns() as $joinColumn) {
Loading history...
152
                        /** @var JoinColumnMetadata $joinColumn */
153 26
                        $columnName           = $joinColumn->getColumnName();
154 26
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
155 26
                        $columnAlias          = $platform->getSQLResultCasing(
156 26
                            $this->getColumnAlias($columnName, $renameMode, $customRenameColumns)
157
                        );
158
159 26
                        if (isset($this->metaMappings[$columnAlias])) {
160
                            throw new InvalidArgumentException(
161
                                sprintf("The column '%s' conflicts with another column in the mapper.", $columnName)
162
                            );
163
                        }
164
165 26
                        if (! $joinColumn->getType()) {
166 2
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
167
                        }
168
169 26
                        $this->addMetaResult($alias, $columnAlias, $columnName, $property->isPrimaryKey(), $joinColumn->getType());
170
                    }
171 26
                    break;
172
            }
173
        }
174 42
    }
175
176
    /**
177
     * Checks if inheritance if supported.
178
     *
179
     * @return bool
180
     */
181 43
    private function isInheritanceSupported(ClassMetadata $metadata)
182
    {
183 43
        if ($metadata->inheritanceType === InheritanceType::SINGLE_TABLE
184 43
            && in_array($metadata->getClassName(), $metadata->discriminatorMap, true)) {
185 3
            return true;
186
        }
187
188 41
        return ! in_array($metadata->inheritanceType, [InheritanceType::SINGLE_TABLE, InheritanceType::JOINED], true);
189
    }
190
191
    /**
192
     * Gets column alias for a given column.
193
     *
194
     * @param string   $columnName
195
     * @param int      $mode
196
     * @param string[] $customRenameColumns
197
     *
198
     * @return string
199
     */
200 42
    private function getColumnAlias($columnName, $mode, array $customRenameColumns)
201
    {
202
        switch ($mode) {
203 42
            case self::COLUMN_RENAMING_INCREMENT:
204 3
                return $columnName . $this->sqlCounter++;
205 39
            case self::COLUMN_RENAMING_CUSTOM:
206 10
                return $customRenameColumns[$columnName] ?? $columnName;
207 38
            case self::COLUMN_RENAMING_NONE:
208
            default:
209 38
                return $columnName;
210
        }
211
    }
212
213
    /**
214
     * Generates the Select clause from this ResultSetMappingBuilder.
215
     *
216
     * Works only for all the entity results. The select parts for scalar
217
     * expressions have to be written manually.
218
     *
219
     * @param string[] $tableAliases
220
     *
221
     * @return string
222
     */
223 18
    public function generateSelectClause($tableAliases = [])
224
    {
225 18
        $sql = '';
226
227 18
        foreach ($this->columnOwnerMap as $columnName => $dqlAlias) {
228 18
            $tableAlias = $tableAliases[$dqlAlias] ?? $dqlAlias;
229
230 18
            if ($sql) {
231 18
                $sql .= ', ';
232
            }
233
234 18
            $sql .= $tableAlias . '.';
235
236 18
            if (isset($this->fieldMappings[$columnName])) {
237 18
                $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]);
238 18
                $field = $this->fieldMappings[$columnName];
239 18
                $sql  .= $class->getProperty($field)->getColumnName();
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

239
                $sql  .= $class->/** @scrutinizer ignore-call */ getProperty($field)->getColumnName();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
240 12
            } elseif (isset($this->metaMappings[$columnName])) {
241 11
                $sql .= $this->metaMappings[$columnName];
242 1
            } elseif (isset($this->discriminatorColumns[$dqlAlias])) {
243 1
                $sql .= $this->discriminatorColumns[$dqlAlias];
244
            }
245
246 18
            $sql .= ' AS ' . $columnName;
247
        }
248
249 18
        return $sql;
250
    }
251
252
    /**
253
     * @return string
254
     */
255 1
    public function __toString()
256
    {
257 1
        return $this->generateSelectClause([]);
258
    }
259
}
260