|
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 LGPL. For more information, see |
|
17 * <http://www.doctrine-project.org>. |
|
18 */ |
|
19 |
|
20 namespace Doctrine\ORM\Tools; |
|
21 |
|
22 use Doctrine\ORM\EntityManager; |
|
23 use Doctrine\ORM\Mapping\ClassMetadataInfo; |
|
24 |
|
25 /** |
|
26 * Performs strict validation of the mapping schema |
|
27 * |
|
28 * @license http://www.opensource.org/licenses/lgpl-license.php LGPL |
|
29 * @link www.doctrine-project.com |
|
30 * @since 1.0 |
|
31 * @version $Revision$ |
|
32 * @author Benjamin Eberlei <kontakt@beberlei.de> |
|
33 * @author Guilherme Blanco <guilhermeblanco@hotmail.com> |
|
34 * @author Jonathan Wage <jonwage@gmail.com> |
|
35 * @author Roman Borschel <roman@code-factory.org> |
|
36 */ |
|
37 class SchemaValidator |
|
38 { |
|
39 /** |
|
40 * @var EntityManager |
|
41 */ |
|
42 private $em; |
|
43 |
|
44 /** |
|
45 * @param EntityManager $em |
|
46 */ |
|
47 public function __construct(EntityManager $em) |
|
48 { |
|
49 $this->em = $em; |
|
50 } |
|
51 |
|
52 /** |
|
53 * Checks the internal consistency of mapping files. |
|
54 * |
|
55 * There are several checks that can't be done at runtime or are too expensive, which can be verified |
|
56 * with this command. For example: |
|
57 * |
|
58 * 1. Check if a relation with "mappedBy" is actually connected to that specified field. |
|
59 * 2. Check if "mappedBy" and "inversedBy" are consistent to each other. |
|
60 * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns. |
|
61 * 4. Check if there are public properties that might cause problems with lazy loading. |
|
62 * |
|
63 * @return array |
|
64 */ |
|
65 public function validateMapping() |
|
66 { |
|
67 $errors = array(); |
|
68 $cmf = $this->em->getMetadataFactory(); |
|
69 $classes = $cmf->getAllMetadata(); |
|
70 |
|
71 foreach ($classes AS $class) { |
|
72 $ce = array(); |
|
73 /* @var $class ClassMetadata */ |
|
74 foreach ($class->associationMappings AS $fieldName => $assoc) { |
|
75 if (!$cmf->hasMetadataFor($assoc['targetEntity'])) { |
|
76 $ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown.'; |
|
77 } |
|
78 |
|
79 if ($assoc['mappedBy'] && $assoc['inversedBy']) { |
|
80 $ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning."; |
|
81 } |
|
82 |
|
83 $targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']); |
|
84 |
|
85 /* @var $assoc AssociationMapping */ |
|
86 if ($assoc['mappedBy']) { |
|
87 if ($targetMetadata->hasField($assoc['mappedBy'])) { |
|
88 $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". |
|
89 "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association."; |
|
90 } |
|
91 if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) { |
|
92 $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". |
|
93 "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which does not exist."; |
|
94 } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] == null) { |
|
95 $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ". |
|
96 "bi-directional relationship, but the specified mappedBy association on the target-entity ". |
|
97 $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". |
|
98 "'inversedBy' attribute."; |
|
99 } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] != $fieldName) { |
|
100 $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . |
|
101 $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " are ". |
|
102 "incosistent with each other."; |
|
103 } |
|
104 } |
|
105 |
|
106 if ($assoc['inversedBy']) { |
|
107 if ($targetMetadata->hasField($assoc['inversedBy'])) { |
|
108 $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". |
|
109 "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which is not defined as association."; |
|
110 } |
|
111 if (!$targetMetadata->hasAssociation($assoc['inversedBy'])) { |
|
112 $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". |
|
113 "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which does not exist."; |
|
114 } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] == null) { |
|
115 $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the owning side of a ". |
|
116 "bi-directional relationship, but the specified mappedBy association on the target-entity ". |
|
117 $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". |
|
118 "'inversedBy' attribute."; |
|
119 } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] != $fieldName) { |
|
120 $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . |
|
121 $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " are ". |
|
122 "incosistent with each other."; |
|
123 } |
|
124 } |
|
125 |
|
126 if ($assoc['isOwningSide']) { |
|
127 if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY) { |
|
128 foreach ($assoc['joinTable']['joinColumns'] AS $joinColumn) { |
|
129 if (!isset($class->fieldNames[$joinColumn['referencedColumnName']])) { |
|
130 $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . |
|
131 "have a corresponding field with this column name on the class '" . $class->name . "'."; |
|
132 break; |
|
133 } |
|
134 |
|
135 $fieldName = $class->fieldNames[$joinColumn['referencedColumnName']]; |
|
136 if (!in_array($fieldName, $class->identifier)) { |
|
137 $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . |
|
138 "has to be a primary key column."; |
|
139 } |
|
140 } |
|
141 foreach ($assoc['joinTable']['inverseJoinColumns'] AS $inverseJoinColumn) { |
|
142 $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); |
|
143 if (!isset($targetClass->fieldNames[$inverseJoinColumn['referencedColumnName']])) { |
|
144 $ce[] = "The inverse referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' does not " . |
|
145 "have a corresponding field with this column name on the class '" . $targetClass->name . "'."; |
|
146 break; |
|
147 } |
|
148 |
|
149 $fieldName = $targetClass->fieldNames[$inverseJoinColumn['referencedColumnName']]; |
|
150 if (!in_array($fieldName, $targetClass->identifier)) { |
|
151 $ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " . |
|
152 "has to be a primary key column."; |
|
153 } |
|
154 } |
|
155 } else if ($assoc['type'] & ClassMetadataInfo::TO_ONE) { |
|
156 foreach ($assoc['joinColumns'] AS $joinColumn) { |
|
157 $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); |
|
158 if (!isset($targetClass->fieldNames[$joinColumn['referencedColumnName']])) { |
|
159 $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . |
|
160 "have a corresponding field with this column name on the class '" . $targetClass->name . "'."; |
|
161 break; |
|
162 } |
|
163 |
|
164 $fieldName = $targetClass->fieldNames[$joinColumn['referencedColumnName']]; |
|
165 if (!in_array($fieldName, $targetClass->identifier)) { |
|
166 $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . |
|
167 "has to be a primary key column."; |
|
168 } |
|
169 } |
|
170 } |
|
171 } |
|
172 |
|
173 if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) { |
|
174 $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); |
|
175 foreach ($assoc['orderBy'] AS $orderField => $orientation) { |
|
176 if (!$targetClass->hasField($orderField)) { |
|
177 $ce[] = "The association " . $class->name."#".$fieldName." is ordered by a foreign field " . |
|
178 $orderField . " that is not a field on the target entity " . $targetClass->name; |
|
179 } |
|
180 } |
|
181 } |
|
182 } |
|
183 |
|
184 foreach ($class->reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $publicAttr) { |
|
185 if ($publicAttr->isStatic()) { |
|
186 continue; |
|
187 } |
|
188 $ce[] = "Field '".$publicAttr->getName()."' in class '".$class->name."' must be private ". |
|
189 "or protected. Public fields may break lazy-loading."; |
|
190 } |
|
191 |
|
192 foreach ($class->subClasses AS $subClass) { |
|
193 if (!in_array($class->name, class_parents($subClass))) { |
|
194 $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child ". |
|
195 "of '" . $class->name . "' but these entities are not related through inheritance."; |
|
196 } |
|
197 } |
|
198 |
|
199 if ($ce) { |
|
200 $errors[$class->name] = $ce; |
|
201 } |
|
202 } |
|
203 |
|
204 return $errors; |
|
205 } |
|
206 |
|
207 /** |
|
208 * Check if the Database Schema is in sync with the current metadata state. |
|
209 * |
|
210 * @return bool |
|
211 */ |
|
212 public function schemaInSyncWithMetadata() |
|
213 { |
|
214 $schemaTool = new SchemaTool($this->em); |
|
215 |
|
216 $allMetadata = $this->em->getMetadataFactory()->getAllMetadata(); |
|
217 return (count($schemaTool->getUpdateSchemaSql($allMetadata, false)) == 0); |
|
218 } |
|
219 } |