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
|
|
|
use function array_diff; |
19
|
|
|
use function array_filter; |
20
|
|
|
use function array_keys; |
21
|
|
|
use function array_map; |
22
|
|
|
use function array_merge; |
23
|
|
|
use function class_exists; |
24
|
|
|
use function class_parents; |
25
|
|
|
use function count; |
26
|
|
|
use function implode; |
27
|
|
|
use function in_array; |
28
|
|
|
use function sprintf; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Performs strict validation of the mapping schema |
32
|
|
|
*/ |
33
|
|
|
class SchemaValidator |
34
|
|
|
{ |
35
|
|
|
/** @var EntityManagerInterface */ |
36
|
|
|
private $em; |
37
|
|
|
|
38
|
50 |
|
public function __construct(EntityManagerInterface $em) |
39
|
|
|
{ |
40
|
50 |
|
$this->em = $em; |
41
|
50 |
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Checks the internal consistency of all mapping files. |
45
|
|
|
* |
46
|
|
|
* There are several checks that can't be done at runtime or are too expensive, which can be verified |
47
|
|
|
* with this command. For example: |
48
|
|
|
* |
49
|
|
|
* 1. Check if a relation with "mappedBy" is actually connected to that specified field. |
50
|
|
|
* 2. Check if "mappedBy" and "inversedBy" are consistent to each other. |
51
|
|
|
* 3. Check if "referencedColumnName" attributes are really pointing to primary key columns. |
52
|
|
|
* |
53
|
|
|
* @return string[] |
54
|
|
|
*/ |
55
|
6 |
|
public function validateMapping() |
56
|
|
|
{ |
57
|
6 |
|
$errors = []; |
58
|
6 |
|
$cmf = $this->em->getMetadataFactory(); |
59
|
6 |
|
$classes = $cmf->getAllMetadata(); |
60
|
|
|
|
61
|
6 |
|
foreach ($classes as $class) { |
62
|
6 |
|
$ce = $this->validateClass($class); |
63
|
|
|
|
64
|
6 |
|
if ($ce) { |
|
|
|
|
65
|
|
|
$errors[$class->getClassName()] = $ce; |
|
|
|
|
66
|
|
|
} |
67
|
|
|
} |
68
|
|
|
|
69
|
6 |
|
return $errors; |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Validates a single class of the current. |
74
|
|
|
* |
75
|
|
|
* @return string[] |
76
|
|
|
*/ |
77
|
50 |
|
public function validateClass(ClassMetadata $class) |
78
|
|
|
{ |
79
|
50 |
|
$ce = []; |
80
|
|
|
|
81
|
50 |
|
foreach ($class->getPropertiesIterator() as $fieldName => $association) { |
82
|
50 |
|
if (! ($association instanceof AssociationMetadata)) { |
83
|
49 |
|
continue; |
84
|
|
|
} |
85
|
|
|
|
86
|
47 |
|
$ce = array_merge($ce, $this->validateAssociation($class, $association)); |
87
|
|
|
} |
88
|
|
|
|
89
|
50 |
|
foreach ($class->getSubClasses() as $subClass) { |
90
|
13 |
|
if (! in_array($class->getClassName(), class_parents($subClass), true)) { |
91
|
|
|
$message = "According to the discriminator map class, '%s' has to be a child of '%s', but these entities are not related through inheritance."; |
92
|
|
|
|
93
|
|
|
$ce[] = sprintf($message, $subClass, $class->getClassName()); |
94
|
|
|
} |
95
|
|
|
} |
96
|
|
|
|
97
|
50 |
|
return $ce; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* @return string[] |
102
|
|
|
*/ |
103
|
47 |
|
private function validateAssociation(ClassMetadata $class, AssociationMetadata $association) |
104
|
|
|
{ |
105
|
47 |
|
$metadataFactory = $this->em->getMetadataFactory(); |
106
|
47 |
|
$fieldName = $association->getName(); |
107
|
47 |
|
$targetEntity = $association->getTargetEntity(); |
108
|
|
|
|
109
|
47 |
|
if (! class_exists($targetEntity) || $metadataFactory->isTransient($targetEntity)) { |
110
|
|
|
$message = "The target entity '%s' specified on %s#%s is unknown or not an entity."; |
111
|
|
|
|
112
|
|
|
return [sprintf($message, $targetEntity, $class->getClassName(), $fieldName)]; |
113
|
|
|
} |
114
|
|
|
|
115
|
47 |
|
$mappedBy = $association->getMappedBy(); |
116
|
47 |
|
$inversedBy = $association->getInversedBy(); |
117
|
|
|
|
118
|
47 |
|
$ce = []; |
119
|
|
|
|
120
|
47 |
|
if ($mappedBy && $inversedBy) { |
121
|
|
|
$message = 'The association %s#%s cannot be defined as both inverse and owning.'; |
122
|
|
|
|
123
|
|
|
$ce[] = sprintf($message, $class, $fieldName); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** @var ClassMetadata $targetMetadata */ |
127
|
47 |
|
$targetMetadata = $metadataFactory->getMetadataFor($targetEntity); |
128
|
|
|
$containsForeignId = array_filter($targetMetadata->identifier, static function ($identifier) use ($targetMetadata) { |
129
|
47 |
|
$targetProperty = $targetMetadata->getProperty($identifier); |
130
|
|
|
|
131
|
47 |
|
return $targetProperty instanceof AssociationMetadata; |
132
|
47 |
|
}); |
133
|
|
|
|
134
|
47 |
|
if ($association->isPrimaryKey() && count($containsForeignId)) { |
135
|
1 |
|
$message = "Cannot map association %s#%s as identifier, because the target entity '%s' also maps an association as identifier."; |
136
|
|
|
|
137
|
1 |
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity); |
138
|
|
|
} |
139
|
|
|
|
140
|
47 |
|
if ($mappedBy) { |
141
|
|
|
/** @var AssociationMetadata $targetAssociation */ |
142
|
40 |
|
$targetAssociation = $targetMetadata->getProperty($mappedBy); |
143
|
|
|
|
144
|
40 |
|
if (! $targetAssociation) { |
|
|
|
|
145
|
|
|
$message = 'The association %s#%s refers to the owning side property %s#%s which does not exist.'; |
146
|
|
|
|
147
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy); |
148
|
40 |
|
} elseif ($targetAssociation instanceof FieldMetadata) { |
|
|
|
|
149
|
|
|
$message = 'The association %s#%s refers to the owning side property %s#%s which is not defined as association, but as field.'; |
150
|
|
|
|
151
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy); |
152
|
40 |
|
} elseif ($targetAssociation->getInversedBy() === null) { |
153
|
|
|
$message = 'The property %s#%s is on the inverse side of a bi-directional relationship, but the ' |
154
|
1 |
|
. "specified mappedBy association on the target-entity %s#%s does not contain the required 'inversedBy=\"%s\"' attribute."; |
155
|
|
|
|
156
|
1 |
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy, $fieldName); |
157
|
39 |
|
} elseif ($targetAssociation->getInversedBy() !== $fieldName) { |
158
|
|
|
$message = 'The mapping between %s#%s and %s#%s are inconsistent with each other.'; |
159
|
|
|
|
160
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy); |
161
|
|
|
} |
162
|
|
|
} |
163
|
|
|
|
164
|
47 |
|
if ($inversedBy) { |
165
|
|
|
/** @var AssociationMetadata $targetAssociation */ |
166
|
37 |
|
$targetAssociation = $targetMetadata->getProperty($inversedBy); |
167
|
|
|
|
168
|
37 |
|
if (! $targetAssociation) { |
|
|
|
|
169
|
|
|
$message = 'The association %s#%s refers to the inverse side property %s#%s which does not exist.'; |
170
|
|
|
|
171
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $inversedBy); |
172
|
37 |
|
} elseif ($targetAssociation instanceof FieldMetadata) { |
|
|
|
|
173
|
|
|
$message = 'The association %s#%s refers to the inverse side property %s#%s which is not defined as association, but as field.'; |
174
|
|
|
|
175
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $inversedBy); |
176
|
37 |
|
} elseif ($targetAssociation->getMappedBy() === null) { |
177
|
|
|
$message = 'The property %s#%s is on the owning side of a bi-directional relationship, but the ' |
178
|
|
|
. "specified mappedBy association on the target-entity %s#%s does not contain the required 'inversedBy=\"%s\"' attribute."; |
179
|
|
|
|
180
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $mappedBy, $fieldName); |
181
|
37 |
|
} elseif ($targetAssociation->getMappedBy() !== $fieldName) { |
182
|
|
|
$message = 'The mapping between %s#%s and %s#%s are inconsistent with each other.'; |
183
|
|
|
|
184
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetEntity, $inversedBy); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
// Verify inverse side/owning side match each other |
188
|
37 |
|
if ($targetAssociation) { |
|
|
|
|
189
|
37 |
|
if ($association instanceof OneToOneAssociationMetadata && ! $targetAssociation instanceof OneToOneAssociationMetadata) { |
190
|
|
|
$message = 'If association %s#%s is one-to-one, then the inversed side %s#%s has to be one-to-one as well.'; |
191
|
|
|
|
192
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetMetadata->getClassName(), $inversedBy); |
193
|
37 |
|
} elseif ($association instanceof ManyToOneAssociationMetadata && ! $targetAssociation instanceof OneToManyAssociationMetadata) { |
194
|
|
|
$message = 'If association %s#%s is many-to-one, then the inversed side %s#%s has to be one-to-many.'; |
195
|
|
|
|
196
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetMetadata->getClassName(), $inversedBy); |
197
|
37 |
|
} elseif ($association instanceof ManyToManyAssociationMetadata && ! $targetAssociation instanceof ManyToManyAssociationMetadata) { |
198
|
|
|
$message = 'If association %s#%s is many-to-many, then the inversed side %s#%s has to be many-to-many as well.'; |
199
|
|
|
|
200
|
|
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $targetMetadata->getClassName(), $inversedBy); |
201
|
|
|
} |
202
|
|
|
} |
203
|
|
|
} |
204
|
|
|
|
205
|
47 |
|
if ($association->isOwningSide()) { |
206
|
43 |
|
if ($association instanceof ManyToManyAssociationMetadata) { |
207
|
23 |
|
$classIdentifierColumns = array_keys($class->getIdentifierColumns($this->em)); |
208
|
23 |
|
$targetIdentifierColumns = array_keys($targetMetadata->getIdentifierColumns($this->em)); |
209
|
23 |
|
$joinTable = $association->getJoinTable(); |
210
|
|
|
|
211
|
23 |
|
foreach ($joinTable->getJoinColumns() as $joinColumn) { |
212
|
23 |
|
if (! in_array($joinColumn->getReferencedColumnName(), $classIdentifierColumns, 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(), $class->getClassName()); |
216
|
|
|
break; |
217
|
|
|
} |
218
|
|
|
} |
219
|
|
|
|
220
|
23 |
|
foreach ($joinTable->getInverseJoinColumns() as $inverseJoinColumn) { |
221
|
23 |
|
if (! in_array($inverseJoinColumn->getReferencedColumnName(), $targetIdentifierColumns, true)) { |
222
|
1 |
|
$message = "The referenced column name '%s' has to be a primary key column on the target entity class '%s'."; |
223
|
|
|
|
224
|
1 |
|
$ce[] = sprintf($message, $inverseJoinColumn->getReferencedColumnName(), $targetMetadata->getClassName()); |
225
|
1 |
|
break; |
226
|
|
|
} |
227
|
|
|
} |
228
|
|
|
|
229
|
23 |
|
if (count($targetIdentifierColumns) !== count($joinTable->getInverseJoinColumns())) { |
230
|
1 |
|
$columnNames = array_map( |
231
|
|
|
static function (JoinColumnMetadata $joinColumn) { |
232
|
1 |
|
return $joinColumn->getReferencedColumnName(); |
233
|
1 |
|
}, |
234
|
1 |
|
$joinTable->getInverseJoinColumns() |
235
|
|
|
); |
236
|
|
|
|
237
|
1 |
|
$columnString = implode("', '", array_diff($targetIdentifierColumns, $columnNames)); |
238
|
|
|
$message = "The inverse join columns of the many-to-many table '%s' have to contain to ALL " |
239
|
1 |
|
. "identifier columns of the target entity '%s', however '%s' are missing."; |
240
|
|
|
|
241
|
1 |
|
$ce[] = sprintf($message, $joinTable->getName(), $targetMetadata->getClassName(), $columnString); |
242
|
|
|
} |
243
|
|
|
|
244
|
23 |
|
if (count($classIdentifierColumns) !== count($joinTable->getJoinColumns())) { |
245
|
1 |
|
$columnNames = array_map( |
246
|
|
|
static function (JoinColumnMetadata $joinColumn) { |
247
|
1 |
|
return $joinColumn->getReferencedColumnName(); |
248
|
1 |
|
}, |
249
|
1 |
|
$joinTable->getJoinColumns() |
250
|
|
|
); |
251
|
|
|
|
252
|
1 |
|
$columnString = implode("', '", array_diff($classIdentifierColumns, $columnNames)); |
253
|
|
|
$message = "The join columns of the many-to-many table '%s' have to contain to ALL " |
254
|
1 |
|
. "identifier columns of the source entity '%s', however '%s' are missing."; |
255
|
|
|
|
256
|
23 |
|
$ce[] = sprintf($message, $joinTable->getName(), $class->getClassName(), $columnString); |
257
|
|
|
} |
258
|
38 |
|
} elseif ($association instanceof ToOneAssociationMetadata) { |
259
|
38 |
|
$identifierColumns = array_keys($targetMetadata->getIdentifierColumns($this->em)); |
260
|
38 |
|
$joinColumns = $association->getJoinColumns(); |
261
|
|
|
|
262
|
38 |
|
foreach ($joinColumns as $joinColumn) { |
263
|
38 |
|
if (! in_array($joinColumn->getReferencedColumnName(), $identifierColumns, true)) { |
264
|
2 |
|
$message = "The referenced column name '%s' has to be a primary key column on the target entity class '%s'."; |
265
|
|
|
|
266
|
2 |
|
$ce[] = sprintf($message, $joinColumn->getReferencedColumnName(), $targetMetadata->getClassName()); |
267
|
|
|
} |
268
|
|
|
} |
269
|
|
|
|
270
|
38 |
|
if (count($identifierColumns) !== count($joinColumns)) { |
271
|
1 |
|
$ids = []; |
272
|
|
|
|
273
|
1 |
|
foreach ($joinColumns as $joinColumn) { |
274
|
1 |
|
$ids[] = $joinColumn->getColumnName(); |
275
|
|
|
} |
276
|
|
|
|
277
|
1 |
|
$columnString = implode("', '", array_diff($identifierColumns, $ids)); |
278
|
|
|
$message = "The join columns of the association '%s' have to match to ALL " |
279
|
1 |
|
. "identifier columns of the target entity '%s', however '%s' are missing."; |
280
|
|
|
|
281
|
1 |
|
$ce[] = sprintf($message, $fieldName, $targetMetadata->getClassName(), $columnString); |
282
|
|
|
} |
283
|
|
|
} |
284
|
|
|
} |
285
|
|
|
|
286
|
47 |
|
if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) { |
287
|
11 |
|
foreach ($association->getOrderBy() as $orderField => $orientation) { |
288
|
11 |
|
$targetProperty = $targetMetadata->getProperty($orderField); |
289
|
|
|
|
290
|
11 |
|
if ($targetProperty instanceof FieldMetadata) { |
291
|
9 |
|
continue; |
292
|
|
|
} |
293
|
|
|
|
294
|
3 |
|
if (! ($targetProperty instanceof AssociationMetadata)) { |
295
|
1 |
|
$message = "The association %s#%s is ordered by a property '%s' that is non-existing field on the target entity '%s'."; |
296
|
|
|
|
297
|
1 |
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $orderField, $targetMetadata->getClassName()); |
298
|
1 |
|
continue; |
299
|
|
|
} |
300
|
|
|
|
301
|
2 |
|
if ($targetProperty instanceof ToManyAssociationMetadata) { |
302
|
1 |
|
$message = "The association %s#%s is ordered by a property '%s' on '%s' that is a collection-valued association."; |
303
|
|
|
|
304
|
1 |
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $orderField, $targetMetadata->getClassName()); |
305
|
1 |
|
continue; |
306
|
|
|
} |
307
|
|
|
|
308
|
2 |
|
if ($targetProperty instanceof AssociationMetadata && ! $targetProperty->isOwningSide()) { |
309
|
1 |
|
$message = "The association %s#%s is ordered by a property '%s' on '%s' that is the inverse side of an association."; |
310
|
|
|
|
311
|
1 |
|
$ce[] = sprintf($message, $class->getClassName(), $fieldName, $orderField, $targetMetadata->getClassName()); |
312
|
1 |
|
continue; |
313
|
|
|
} |
314
|
|
|
} |
315
|
|
|
} |
316
|
|
|
|
317
|
47 |
|
return $ce; |
318
|
|
|
} |
319
|
|
|
|
320
|
|
|
/** |
321
|
|
|
* Checks if the Database Schema is in sync with the current metadata state. |
322
|
|
|
* |
323
|
|
|
* @return bool |
324
|
|
|
*/ |
325
|
|
|
public function schemaInSyncWithMetadata() |
326
|
|
|
{ |
327
|
|
|
$schemaTool = new SchemaTool($this->em); |
328
|
|
|
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); |
329
|
|
|
|
330
|
|
|
return count($schemaTool->getUpdateSchemaSql($allMetadata, true)) === 0; |
331
|
|
|
} |
332
|
|
|
} |
333
|
|
|
|
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.