SchemaValidator.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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. use Doctrine\DBAL\Types\Type;
  23. /**
  24. * Performs strict validation of the mapping schema
  25. *
  26. * @license http://www.opensource.org/licenses/lgpl-license.php LGPL
  27. * @link www.doctrine-project.com
  28. * @since 1.0
  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 all 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. if ($ce = $this->validateClass($class)) {
  67. $errors[$class->name] = $ce;
  68. }
  69. }
  70. return $errors;
  71. }
  72. /**
  73. * Validate a single class of the current
  74. *
  75. * @param ClassMetadataInfo $class
  76. * @return array
  77. */
  78. public function validateClass(ClassMetadataInfo $class)
  79. {
  80. $ce = array();
  81. $cmf = $this->em->getMetadataFactory();
  82. foreach ($class->fieldMappings as $fieldName => $mapping) {
  83. if (!Type::hasType($mapping['type'])) {
  84. $ce[] = "The field '" . $class->name . "#" . $fieldName."' uses a non-existant type '" . $mapping['type'] . "'.";
  85. }
  86. }
  87. foreach ($class->associationMappings AS $fieldName => $assoc) {
  88. if (!class_exists($assoc['targetEntity']) || $cmf->isTransient($assoc['targetEntity'])) {
  89. $ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown or not an entity.';
  90. return $ce;
  91. }
  92. if ($assoc['mappedBy'] && $assoc['inversedBy']) {
  93. $ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning.";
  94. }
  95. $targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']);
  96. if (isset($assoc['id']) && $targetMetadata->containsForeignIdentifier) {
  97. $ce[] = "Cannot map association '" . $class->name. "#". $fieldName ." as identifier, because " .
  98. "the target entity '". $targetMetadata->name . "' also maps an association as identifier.";
  99. }
  100. /* @var $assoc AssociationMapping */
  101. if ($assoc['mappedBy']) {
  102. if ($targetMetadata->hasField($assoc['mappedBy'])) {
  103. $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ".
  104. "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association.";
  105. }
  106. if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) {
  107. $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ".
  108. "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which does not exist.";
  109. } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] == null) {
  110. $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ".
  111. "bi-directional relationship, but the specified mappedBy association on the target-entity ".
  112. $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ".
  113. "'inversedBy=".$fieldName."' attribute.";
  114. } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] != $fieldName) {
  115. $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " .
  116. $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " are ".
  117. "incosistent with each other.";
  118. }
  119. }
  120. if ($assoc['inversedBy']) {
  121. if ($targetMetadata->hasField($assoc['inversedBy'])) {
  122. $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ".
  123. "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which is not defined as association.";
  124. }
  125. if (!$targetMetadata->hasAssociation($assoc['inversedBy'])) {
  126. $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ".
  127. "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which does not exist.";
  128. } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] == null) {
  129. $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the owning side of a ".
  130. "bi-directional relationship, but the specified mappedBy association on the target-entity ".
  131. $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ".
  132. "'inversedBy' attribute.";
  133. } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] != $fieldName) {
  134. $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " .
  135. $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " are ".
  136. "incosistent with each other.";
  137. }
  138. // Verify inverse side/owning side match each other
  139. $targetAssoc = $targetMetadata->associationMappings[$assoc['inversedBy']];
  140. if ($assoc['type'] == ClassMetadataInfo::ONE_TO_ONE && $targetAssoc['type'] !== ClassMetadataInfo::ONE_TO_ONE){
  141. $ce[] = "If association " . $class->name . "#" . $fieldName . " is one-to-one, then the inversed " .
  142. "side " . $targetMetadata->name . "#" . $assoc['inversedBy'] . " has to be one-to-one as well.";
  143. } else if ($assoc['type'] == ClassMetadataInfo::MANY_TO_ONE && $targetAssoc['type'] !== ClassMetadataInfo::ONE_TO_MANY){
  144. $ce[] = "If association " . $class->name . "#" . $fieldName . " is many-to-one, then the inversed " .
  145. "side " . $targetMetadata->name . "#" . $assoc['inversedBy'] . " has to be one-to-many.";
  146. } else if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY && $targetAssoc['type'] !== ClassMetadataInfo::MANY_TO_MANY){
  147. $ce[] = "If association " . $class->name . "#" . $fieldName . " is many-to-many, then the inversed " .
  148. "side " . $targetMetadata->name . "#" . $assoc['inversedBy'] . " has to be many-to-many as well.";
  149. }
  150. }
  151. if ($assoc['isOwningSide']) {
  152. if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY) {
  153. $identifierColumns = $class->getIdentifierColumnNames();
  154. foreach ($assoc['joinTable']['joinColumns'] AS $joinColumn) {
  155. if (!in_array($joinColumn['referencedColumnName'], $identifierColumns)) {
  156. $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " .
  157. "has to be a primary key column on the target entity class '".$class->name."'.";
  158. break;
  159. }
  160. }
  161. $identifierColumns = $targetMetadata->getIdentifierColumnNames();
  162. foreach ($assoc['joinTable']['inverseJoinColumns'] AS $inverseJoinColumn) {
  163. if (!in_array($inverseJoinColumn['referencedColumnName'], $identifierColumns)) {
  164. $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " .
  165. "has to be a primary key column on the target entity class '".$targetMetadata->name."'.";
  166. break;
  167. }
  168. }
  169. if (count($targetMetadata->getIdentifierColumnNames()) != count($assoc['joinTable']['inverseJoinColumns'])) {
  170. $ce[] = "The inverse join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " .
  171. "have to contain to ALL identifier columns of the target entity '". $targetMetadata->name . "', " .
  172. "however '" . implode(", ", array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc['relationToTargetKeyColumns']))) .
  173. "' are missing.";
  174. }
  175. if (count($class->getIdentifierColumnNames()) != count($assoc['joinTable']['joinColumns'])) {
  176. $ce[] = "The join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " .
  177. "have to contain to ALL identifier columns of the source entity '". $class->name . "', " .
  178. "however '" . implode(", ", array_diff($class->getIdentifierColumnNames(), array_values($assoc['relationToSourceKeyColumns']))) .
  179. "' are missing.";
  180. }
  181. } else if ($assoc['type'] & ClassMetadataInfo::TO_ONE) {
  182. $identifierColumns = $targetMetadata->getIdentifierColumnNames();
  183. foreach ($assoc['joinColumns'] AS $joinColumn) {
  184. if (!in_array($joinColumn['referencedColumnName'], $identifierColumns)) {
  185. $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " .
  186. "has to be a primary key column on the target entity class '".$targetMetadata->name."'.";
  187. }
  188. }
  189. if (count($identifierColumns) != count($assoc['joinColumns'])) {
  190. $ids = array();
  191. foreach ($assoc['joinColumns'] AS $joinColumn) {
  192. $ids[] = $joinColumn['name'];
  193. }
  194. $ce[] = "The join columns of the association '" . $assoc['fieldName'] . "' " .
  195. "have to match to ALL identifier columns of the target entity '". $class->name . "', " .
  196. "however '" . implode(", ", array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) .
  197. "' are missing.";
  198. }
  199. }
  200. }
  201. if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) {
  202. foreach ($assoc['orderBy'] AS $orderField => $orientation) {
  203. if (!$targetMetadata->hasField($orderField)) {
  204. $ce[] = "The association " . $class->name."#".$fieldName." is ordered by a foreign field " .
  205. $orderField . " that is not a field on the target entity " . $targetMetadata->name;
  206. }
  207. }
  208. }
  209. }
  210. foreach ($class->reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $publicAttr) {
  211. if ($publicAttr->isStatic()) {
  212. continue;
  213. }
  214. $ce[] = "Field '".$publicAttr->getName()."' in class '".$class->name."' must be private ".
  215. "or protected. Public fields may break lazy-loading.";
  216. }
  217. foreach ($class->subClasses AS $subClass) {
  218. if (!in_array($class->name, class_parents($subClass))) {
  219. $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child ".
  220. "of '" . $class->name . "' but these entities are not related through inheritance.";
  221. }
  222. }
  223. return $ce;
  224. }
  225. /**
  226. * Check if the Database Schema is in sync with the current metadata state.
  227. *
  228. * @return bool
  229. */
  230. public function schemaInSyncWithMetadata()
  231. {
  232. $schemaTool = new SchemaTool($this->em);
  233. $allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
  234. return (count($schemaTool->getUpdateSchemaSql($allMetadata, true)) == 0);
  235. }
  236. }