Failed Conditions
Pull Request — master (#6743)
by Grégoire
18:17 queued 12:33
created

SchemaValidator::validateAssociation()   F

Complexity

Conditions 45
Paths > 20000

Size

Total Lines 215
Code Lines 125

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 96
CRAP Score 68.3129

Importance

Changes 0
Metric Value
cc 45
eloc 125
nc 49921
nop 2
dl 0
loc 215
ccs 96
cts 124
cp 0.7742
crap 68.3129
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Tools;
6
7
use Doctrine\ORM\EntityManagerInterface;
8
use Doctrine\ORM\Mapping\AssociationMetadata;
9
use Doctrine\ORM\Mapping\ClassMetadata;
10
use Doctrine\ORM\Mapping\FieldMetadata;
11
use Doctrine\ORM\Mapping\JoinColumnMetadata;
12
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
13
use Doctrine\ORM\Mapping\ManyToOneAssociationMetadata;
14
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
15
use Doctrine\ORM\Mapping\OneToOneAssociationMetadata;
16
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
17
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
18
19
/**
20
 * Performs strict validation of the mapping schema
21
 */
22
class SchemaValidator
23
{
24
    /**
25
     * @var EntityManagerInterface
26
     */
27
    private $em;
28
29 49
    public function __construct(EntityManagerInterface $em)
30
    {
31 49
        $this->em = $em;
32 49
    }
33
34
    /**
35
     * Checks the internal consistency of all mapping files.
36
     *
37
     * There are several checks that can't be done at runtime or are too expensive, which can be verified
38
     * with this command. For example:
39
     *
40
     * 1. Check if a relation with "mappedBy" is actually connected to that specified field.
41
     * 2. Check if "mappedBy" and "inversedBy" are consistent to each other.
42
     * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns.
43
     *
44
     * @return string[]
45
     */
46 6
    public function validateMapping()
47
    {
48 6
        $errors  = [];
49 6
        $cmf     = $this->em->getMetadataFactory();
50 6
        $classes = $cmf->getAllMetadata();
51
52 6
        foreach ($classes as $class) {
53 6
            $ce = $this->validateClass($class);
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\Schem...idator::validateClass(). ( Ignorable by Annotation )

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

53
            $ce = $this->validateClass(/** @scrutinizer ignore-type */ $class);
Loading history...
54
55 6
            if ($ce) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $ce of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
56 6
                $errors[$class->getClassName()] = $ce;
0 ignored issues
show
Bug introduced by
The method getClassName() 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

56
                $errors[$class->/** @scrutinizer ignore-call */ getClassName()] = $ce;

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...
57
            }
58
        }
59
60 6
        return $errors;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $errors returns an array which contains values of type string[] which are incompatible with the documented value type string.
Loading history...
61
    }
62
63
    /**
64
     * Validates a single class of the current.
65
     *
66
     * @return string[]
67
     */
68 49
    public function validateClass(ClassMetadata $class)
69
    {
70 49
        $ce = [];
71
72 49
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $association) {
73 49
            if (! ($association instanceof AssociationMetadata)) {
74 48
                continue;
75
            }
76
77 46
            $ce = array_merge($ce, $this->validateAssociation($class, $association));
78
        }
79
80 49
        foreach ($class->getSubClasses() as $subClass) {
81 13
            if (! in_array($class->getClassName(), class_parents($subClass), true)) {
82
                $message = "According to the discriminator map class, '%s' has to be a child of '%s', but these entities are not related through inheritance.";
83
84 13
                $ce[] = sprintf($message, $subClass, $class->getClassName());
85
            }
86
        }
87
88 49
        return $ce;
89
    }
90
91
    /**
92
     * @return string[]
93
     */
94 46
    private function validateAssociation(ClassMetadata $class, AssociationMetadata $association)
95
    {
96 46
        $metadataFactory = $this->em->getMetadataFactory();
97 46
        $fieldName       = $association->getName();
98 46
        $targetEntity    = $association->getTargetEntity();
99
100 46
        if (! class_exists($targetEntity) || $metadataFactory->isTransient($targetEntity)) {
101
            $message = "The target entity '%s' specified on %s#%s is unknown or not an entity.";
102
103
            return [sprintf($message, $targetEntity, $class->getClassName(), $fieldName)];
104
        }
105
106 46
        $mappedBy   = $association->getMappedBy();
107 46
        $inversedBy = $association->getInversedBy();
108
109 46
        $ce = [];
110
111 46
        if ($mappedBy && $inversedBy) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mappedBy of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $inversedBy of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
112
            $message = 'The association %s#%s cannot be defined as both inverse and owning.';
113
114
            $ce[] = sprintf($message, $class, $fieldName);
115
        }
116
117
        /** @var ClassMetadata $targetMetadata */
118 46
        $targetMetadata    = $metadataFactory->getMetadataFor($targetEntity);
119 46
        $containsForeignId = array_filter($targetMetadata->identifier, function ($identifier) use ($targetMetadata) {
120 46
            $targetProperty = $targetMetadata->getProperty($identifier);
121
122 46
            return $targetProperty instanceof AssociationMetadata;
123 46
        });
124
125 46
        if ($association->isPrimaryKey() && count($containsForeignId)) {
126 1
            $message = "Cannot map association %s#%s as identifier, because the target entity '%s' also maps an association as identifier.";
127
128 1
            $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity);
129
        }
130
131 46
        if ($mappedBy) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $mappedBy of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
132
            /** @var AssociationMetadata $targetAssociation */
133 40
            $targetAssociation = $targetMetadata->getProperty($mappedBy);
134
135 40
            if (! $targetAssociation) {
0 ignored issues
show
introduced by
The condition ! $targetAssociation can never be false.
Loading history...
136
                $message = 'The association %s#%s refers to the owning side property %s#%s which does not exist.';
137
138
                $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy);
139 40
            } elseif ($targetAssociation instanceof FieldMetadata) {
140
                $message = 'The association %s#%s refers to the owning side property %s#%s which is not defined as association, but as field.';
141
142
                $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy);
143 40
            } elseif ($targetAssociation->getInversedBy() === null) {
144
                $message = 'The property %s#%s is on the inverse side of a bi-directional relationship, but the '
145 1
                    . "specified mappedBy association on the target-entity %s#%s does not contain the required 'inversedBy=\"%s\"' attribute.";
146
147 1
                $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy, $fieldName);
148 39
            } elseif ($targetAssociation->getInversedBy() !== $fieldName) {
149
                $message = 'The mapping between %s#%s and %s#%s are inconsistent with each other.';
150
151
                $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy);
152
            }
153
        }
154
155 46
        if ($inversedBy) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $inversedBy of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
156
            /** @var AssociationMetadata $targetAssociation */
157 37
            $targetAssociation = $targetMetadata->getProperty($inversedBy);
158
159 37
            if (! $targetAssociation) {
0 ignored issues
show
introduced by
The condition ! $targetAssociation can never be false.
Loading history...
160
                $message = 'The association %s#%s refers to the inverse side property %s#%s which does not exist.';
161
162
                $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $inversedBy);
163 37
            } elseif ($targetAssociation instanceof FieldMetadata) {
164
                $message = 'The association %s#%s refers to the inverse side property %s#%s which is not defined as association, but as field.';
165
166
                $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $inversedBy);
167 37
            } elseif ($targetAssociation->getMappedBy() === null) {
168
                $message = 'The property %s#%s is on the owning side of a bi-directional relationship, but the '
169
                    . "specified mappedBy association on the target-entity %s#%s does not contain the required 'inversedBy=\"%s\"' attribute.";
170
171
                $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy, $fieldName);
172 37
            } elseif ($targetAssociation->getMappedBy() !== $fieldName) {
173
                $message = 'The mapping between %s#%s and %s#%s are inconsistent with each other.';
174
175
                $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $inversedBy);
176
            }
177
178
            // Verify inverse side/owning side match each other
179 37
            if ($targetAssociation) {
0 ignored issues
show
introduced by
The condition $targetAssociation can never be true.
Loading history...
180 37
                if ($association instanceof OneToOneAssociationMetadata && ! $targetAssociation instanceof OneToOneAssociationMetadata) {
181
                    $message = 'If association %s#%s is one-to-one, then the inversed side %s#%s has to be one-to-one as well.';
182
183
                    $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetMetadata->getClassName(), $inversedBy);
184 37
                } elseif ($association instanceof ManyToOneAssociationMetadata && ! $targetAssociation instanceof OneToManyAssociationMetadata) {
185
                    $message = 'If association %s#%s is many-to-one, then the inversed side %s#%s has to be one-to-many.';
186
187
                    $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetMetadata->getClassName(), $inversedBy);
188 37
                } elseif ($association instanceof ManyToManyAssociationMetadata && ! $targetAssociation instanceof ManyToManyAssociationMetadata) {
189
                    $message = 'If association %s#%s is many-to-many, then the inversed side %s#%s has to be many-to-many as well.';
190
191
                    $ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetMetadata->getClassName(), $inversedBy);
192
                }
193
            }
194
        }
195
196 46
        if ($association->isOwningSide()) {
197 42
            if ($association instanceof ManyToManyAssociationMetadata) {
198 22
                $classIdentifierColumns  = array_keys($class->getIdentifierColumns($this->em));
199 22
                $targetIdentifierColumns = array_keys($targetMetadata->getIdentifierColumns($this->em));
200 22
                $joinTable               = $association->getJoinTable();
201
202 22
                foreach ($joinTable->getJoinColumns() as $joinColumn) {
203 22
                    if (! in_array($joinColumn->getReferencedColumnName(), $classIdentifierColumns, true)) {
204
                        $message = "The referenced column name '%s' has to be a primary key column on the target entity class '%s'.";
205
206
                        $ce[] = sprintf($message, $joinColumn->getReferencedColumnName(), $class->getClassName());
207 22
                        break;
208
                    }
209
                }
210
211 22
                foreach ($joinTable->getInverseJoinColumns() as $inverseJoinColumn) {
212 22
                    if (! in_array($inverseJoinColumn->getReferencedColumnName(), $targetIdentifierColumns, true)) {
213
                        $message = "The referenced column name '%s' has to be a primary key column on the target entity class '%s'.";
214
215
                        $ce[] = sprintf($message, $joinColumn->getReferencedColumnName(), $targetMetadata->getClassName());
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $joinColumn seems to be defined by a foreach iteration on line 202. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
216 22
                        break;
217
                    }
218
                }
219
220 22
                if (count($targetIdentifierColumns) !== count($joinTable->getInverseJoinColumns())) {
221 1
                    $columnNames = array_map(
222 1
                        function (JoinColumnMetadata $joinColumn) {
223 1
                            return $joinColumn->getReferencedColumnName();
224 1
                        },
225 1
                        $joinTable->getInverseJoinColumns()
226
                    );
227
228 1
                    $columnString = implode("', '", array_diff($targetIdentifierColumns, $columnNames));
229
                    $message      = "The inverse join columns of the many-to-many table '%s' have to contain to ALL "
230 1
                        . "identifier columns of the target entity '%s', however '%s' are missing.";
231
232 1
                    $ce[] = sprintf($message, $joinTable->getName(), $targetMetadata->getClassName(), $columnString);
233
                }
234
235 22
                if (count($classIdentifierColumns) !== count($joinTable->getJoinColumns())) {
236 1
                    $columnNames = array_map(
237 1
                        function (JoinColumnMetadata $joinColumn) {
238 1
                            return $joinColumn->getReferencedColumnName();
239 1
                        },
240 1
                        $joinTable->getJoinColumns()
241
                    );
242
243 1
                    $columnString = implode("', '", array_diff($classIdentifierColumns, $columnNames));
244
                    $message      = "The join columns of the many-to-many table '%s' have to contain to ALL "
245 1
                        . "identifier columns of the source entity '%s', however '%s' are missing.";
246
247 22
                    $ce[] = sprintf($message, $joinTable->getName(), $class->getClassName(), $columnString);
248
                }
249 38
            } elseif ($association instanceof ToOneAssociationMetadata) {
250 38
                $identifierColumns = array_keys($targetMetadata->getIdentifierColumns($this->em));
251 38
                $joinColumns       = $association->getJoinColumns();
252
253 38
                foreach ($joinColumns as $joinColumn) {
254 38
                    if (! in_array($joinColumn->getReferencedColumnName(), $identifierColumns, true)) {
255 2
                        $message = "The referenced column name '%s' has to be a primary key column on the target entity class '%s'.";
256
257 38
                        $ce[] = sprintf($message, $joinColumn->getReferencedColumnName(), $targetMetadata->getClassName());
258
                    }
259
                }
260
261 38
                if (count($identifierColumns) !== count($joinColumns)) {
262 1
                    $ids = [];
263
264 1
                    foreach ($joinColumns as $joinColumn) {
265 1
                        $ids[] = $joinColumn->getColumnName();
266
                    }
267
268 1
                    $columnString = implode("', '", array_diff($identifierColumns, $ids));
269
                    $message      = "The join columns of the association '%s' have to match to ALL "
270 1
                        . "identifier columns of the target entity '%s', however '%s' are missing.";
271
272 1
                    $ce[] = sprintf($message, $fieldName, $targetMetadata->getClassName(), $columnString);
273
                }
274
            }
275
        }
276
277 46
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
278 11
            foreach ($association->getOrderBy() as $orderField => $orientation) {
279 11
                $targetProperty = $targetMetadata->getProperty($orderField);
280
281 11
                if ($targetProperty instanceof FieldMetadata) {
282 9
                    continue;
283
                }
284
285 3
                if (! ($targetProperty instanceof AssociationMetadata)) {
286 1
                    $message = "The association %s#%s is ordered by a property '%s' that is non-existing field on the target entity '%s'.";
287
288 1
                    $ce[] = sprintf($message, $class->getClassName(), $fieldName, $orderField, $targetMetadata->getClassName());
289 1
                    continue;
290
                }
291
292 2
                if ($targetProperty instanceof ToManyAssociationMetadata) {
293 1
                    $message = "The association %s#%s is ordered by a property '%s' on '%s' that is a collection-valued association.";
294
295 1
                    $ce[] = sprintf($message, $class->getClassName(), $fieldName, $orderField, $targetMetadata->getClassName());
296 1
                    continue;
297
                }
298
299 2
                if ($targetProperty instanceof AssociationMetadata && ! $targetProperty->isOwningSide()) {
300 1
                    $message = "The association %s#%s is ordered by a property '%s' on '%s' that is the inverse side of an association.";
301
302 1
                    $ce[] = sprintf($message, $class->getClassName(), $fieldName, $orderField, $targetMetadata->getClassName());
303 2
                    continue;
304
                }
305
            }
306
        }
307
308 46
        return $ce;
309
    }
310
311
    /**
312
     * Checks if the Database Schema is in sync with the current metadata state.
313
     *
314
     * @return bool
315
     */
316
    public function schemaInSyncWithMetadata()
317
    {
318
        $schemaTool  = new SchemaTool($this->em);
319
        $allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
320
321
        return count($schemaTool->getUpdateSchemaSql($allMetadata, true)) === 0;
322
    }
323
}
324