Failed Conditions
CANCELLED  
Pull Request — master (#7095)
by Benjamin
10:13
created

ResultSetMappingBuilder::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
144 42
                    $columnName  = $property->getColumnName();
145 42
                    $columnAlias = $platform->getSQLResultCasing(
146 42
                        $this->getColumnAlias($columnName, $renameMode, $customRenameColumns)
147
                    );
148
149 42
                    if (isset($this->fieldMappings[$columnAlias])) {
150 1
                        throw new \InvalidArgumentException(
151 1
                            sprintf("The column '%s' conflicts with another column in the mapper.", $columnName)
152
                        );
153
                    }
154
155 42
                    $this->addFieldResult($alias, $columnAlias, $property->getName());
156 42
                    break;
157
158 32
                case ($property instanceof ToOneAssociationMetadata && $property->isOwningSide()):
159 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

159
                    $targetClass = $this->em->getClassMetadata($property->/** @scrutinizer ignore-call */ getTargetEntity());
Loading history...
160
161 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

161
                    foreach ($property->/** @scrutinizer ignore-call */ getJoinColumns() as $joinColumn) {
Loading history...
162
                        /** @var JoinColumnMetadata $joinColumn */
163 26
                        $columnName           = $joinColumn->getColumnName();
164 26
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
165 26
                        $columnAlias          = $platform->getSQLResultCasing(
166 26
                            $this->getColumnAlias($columnName, $renameMode, $customRenameColumns)
167
                        );
168
169 26
                        if (isset($this->metaMappings[$columnAlias])) {
170
                            throw new \InvalidArgumentException(
171
                                sprintf("The column '%s' conflicts with another column in the mapper.", $columnName)
172
                            );
173
                        }
174
175 26
                        if (! $joinColumn->getType()) {
176 2
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

176
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
177
                        }
178
179 26
                        $this->addMetaResult($alias, $columnAlias, $columnName, $property->isPrimaryKey(), $joinColumn->getType());
180
                    }
181 42
                    break;
182
            }
183
        }
184 42
    }
185
186
    /**
187
     * Checks if inheritance if supported.
188
     *
189
     * @return bool
190
     */
191 43
    private function isInheritanceSupported(ClassMetadata $metadata)
192
    {
193 43
        if ($metadata->inheritanceType === InheritanceType::SINGLE_TABLE
194 43
            && in_array($metadata->getClassName(), $metadata->discriminatorMap, true)) {
195 3
            return true;
196
        }
197
198 41
        return ! in_array($metadata->inheritanceType, [InheritanceType::SINGLE_TABLE, InheritanceType::JOINED], true);
199
    }
200
201
    /**
202
     * Gets column alias for a given column.
203
     *
204
     * @param string   $columnName
205
     * @param int      $mode
206
     * @param string[] $customRenameColumns
207
     *
208
     * @return string
209
     */
210 42
    private function getColumnAlias($columnName, $mode, array $customRenameColumns)
211
    {
212
        switch ($mode) {
213 42
            case self::COLUMN_RENAMING_INCREMENT:
214 3
                return $columnName . $this->sqlCounter++;
215
216 39
            case self::COLUMN_RENAMING_CUSTOM:
217 10
                return $customRenameColumns[$columnName] ?? $columnName;
218
219 38
            case self::COLUMN_RENAMING_NONE:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
220 38
                return $columnName;
221
        }
222
    }
223
224
    /**
225
     * Generates the Select clause from this ResultSetMappingBuilder.
226
     *
227
     * Works only for all the entity results. The select parts for scalar
228
     * expressions have to be written manually.
229
     *
230
     * @param string[] $tableAliases
231
     *
232
     * @return string
233
     */
234 18
    public function generateSelectClause($tableAliases = [])
235
    {
236 18
        $sql = '';
237
238 18
        foreach ($this->columnOwnerMap as $columnName => $dqlAlias) {
239 18
            $tableAlias = $tableAliases[$dqlAlias] ?? $dqlAlias;
240
241 18
            if ($sql) {
242 18
                $sql .= ', ';
243
            }
244
245 18
            $sql .= $tableAlias . '.';
246
247 18
            if (isset($this->fieldMappings[$columnName])) {
248 18
                $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]);
249 18
                $field = $this->fieldMappings[$columnName];
250 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

250
                $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...
251 12
            } elseif (isset($this->metaMappings[$columnName])) {
252 11
                $sql .= $this->metaMappings[$columnName];
253 1
            } elseif (isset($this->discriminatorColumns[$dqlAlias])) {
254 1
                $sql .= $this->discriminatorColumns[$dqlAlias];
255
            }
256
257 18
            $sql .= ' AS ' . $columnName;
258
        }
259
260 18
        return $sql;
261
    }
262
263
    /**
264
     * @return string
265
     */
266 1
    public function __toString()
267
    {
268 1
        return $this->generateSelectClause([]);
269
    }
270
}
271