SchemaValidator.php 12KB

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