From 11f82bd41f021560bcbb4d611c51d2c4b71715fb Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Mon, 31 Oct 2011 20:49:28 +0100 Subject: [PATCH] DDC-1439 - Fix validate mapping some more --- lib/Doctrine/ORM/Tools/SchemaValidator.php | 306 +++++++++--------- .../Tests/ORM/Tools/SchemaValidatorTest.php | 84 +++++ 2 files changed, 245 insertions(+), 145 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index a7f8e3a1c..2ddd0bf66 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -50,7 +50,7 @@ class SchemaValidator } /** - * Checks the internal consistency of mapping files. + * Checks the internal consistency of all mapping files. * * There are several checks that can't be done at runtime or are too expensive, which can be verified * with this command. For example: @@ -69,150 +69,7 @@ class SchemaValidator $classes = $cmf->getAllMetadata(); foreach ($classes AS $class) { - $ce = array(); - /* @var $class ClassMetadata */ - foreach ($class->associationMappings AS $fieldName => $assoc) { - if (!$cmf->hasMetadataFor($assoc['targetEntity'])) { - $ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown.'; - } - - if ($assoc['mappedBy'] && $assoc['inversedBy']) { - $ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning."; - } - - $targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']); - - /* @var $assoc AssociationMapping */ - if ($assoc['mappedBy']) { - if ($targetMetadata->hasField($assoc['mappedBy'])) { - $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association."; - } - if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) { - $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which does not exist."; - } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] == null) { - $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ". - "bi-directional relationship, but the specified mappedBy association on the target-entity ". - $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". - "'inversedBy' attribute."; - } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] != $fieldName) { - $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . - $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " are ". - "incosistent with each other."; - } - } - - if ($assoc['inversedBy']) { - if ($targetMetadata->hasField($assoc['inversedBy'])) { - $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which is not defined as association."; - } - if (!$targetMetadata->hasAssociation($assoc['inversedBy'])) { - $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which does not exist."; - } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] == null) { - $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the owning side of a ". - "bi-directional relationship, but the specified mappedBy association on the target-entity ". - $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". - "'inversedBy' attribute."; - } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] != $fieldName) { - $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . - $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " are ". - "incosistent with each other."; - } - } - - if ($assoc['isOwningSide']) { - if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY) { - foreach ($assoc['joinTable']['joinColumns'] AS $joinColumn) { - if (!isset($class->fieldNames[$joinColumn['referencedColumnName']])) { - $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . - "have a corresponding field with this column name on the class '" . $class->name . "'."; - break; - } - - $fieldName = $class->fieldNames[$joinColumn['referencedColumnName']]; - if (!in_array($fieldName, $class->identifier)) { - $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . - "has to be a primary key column."; - } - } - foreach ($assoc['joinTable']['inverseJoinColumns'] AS $inverseJoinColumn) { - $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); - if (!isset($targetClass->fieldNames[$inverseJoinColumn['referencedColumnName']])) { - $ce[] = "The inverse referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' does not " . - "have a corresponding field with this column name on the class '" . $targetClass->name . "'."; - break; - } - - $fieldName = $targetClass->fieldNames[$inverseJoinColumn['referencedColumnName']]; - if (!in_array($fieldName, $targetClass->identifier)) { - $ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " . - "has to be a primary key column."; - } - } - - if (count($targetClass->identifier) != count($assoc['joinTable']['inverseJoinColumns'])) { - $ce[] = "The inverse join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . - "have to match to ALL identifier columns of the target entity '". $targetClass->name . "'"; - } - - if (count($class->identifier) != count($assoc['joinTable']['joinColumns'])) { - $ce[] = "The join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . - "have to match to ALL identifier columns of the source entity '". $class->name . "'"; - } - - } else if ($assoc['type'] & ClassMetadataInfo::TO_ONE) { - foreach ($assoc['joinColumns'] AS $joinColumn) { - $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); - if (!isset($targetClass->fieldNames[$joinColumn['referencedColumnName']])) { - $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . - "have a corresponding field with this column name on the class '" . $targetClass->name . "'."; - break; - } - - $fieldName = $targetClass->fieldNames[$joinColumn['referencedColumnName']]; - if (!in_array($fieldName, $targetClass->identifier)) { - $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . - "has to be a primary key column."; - } - } - - if (count($class->identifier) != count($assoc['joinColumns'])) { - $ce[] = "The join columns of the association '" . $assoc['fieldName'] . "' " . - "have to match to ALL identifier columns of the source entity '". $class->name . "'"; - } - } - } - - if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) { - $targetClass = $cmf->getMetadataFor($assoc['targetEntity']); - foreach ($assoc['orderBy'] AS $orderField => $orientation) { - if (!$targetClass->hasField($orderField)) { - $ce[] = "The association " . $class->name."#".$fieldName." is ordered by a foreign field " . - $orderField . " that is not a field on the target entity " . $targetClass->name; - } - } - } - } - - foreach ($class->reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $publicAttr) { - if ($publicAttr->isStatic()) { - continue; - } - $ce[] = "Field '".$publicAttr->getName()."' in class '".$class->name."' must be private ". - "or protected. Public fields may break lazy-loading."; - } - - foreach ($class->subClasses AS $subClass) { - if (!in_array($class->name, class_parents($subClass))) { - $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child ". - "of '" . $class->name . "' but these entities are not related through inheritance."; - } - } - - if ($ce) { + if ($ce = $this->validateClass($class)) { $errors[$class->name] = $ce; } } @@ -220,6 +77,165 @@ class SchemaValidator return $errors; } + /** + * Validate a single class of the current + * + * @param ClassMetadataInfo $class + * @return array + */ + public function validateClass(ClassMetadataInfo $class) + { + $ce = array(); + $cmf = $this->em->getMetadataFactory(); + + foreach ($class->associationMappings AS $fieldName => $assoc) { + if (!$cmf->hasMetadataFor($assoc['targetEntity'])) { + $ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown.'; + return $ce; + } + + if ($assoc['mappedBy'] && $assoc['inversedBy']) { + $ce[] = "The association " . $class . "#" . $fieldName . " cannot be defined as both inverse and owning."; + } + + $targetMetadata = $cmf->getMetadataFor($assoc['targetEntity']); + + /* @var $assoc AssociationMapping */ + if ($assoc['mappedBy']) { + if ($targetMetadata->hasField($assoc['mappedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association."; + } + if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which does not exist."; + } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] == null) { + $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the inverse side of a ". + "bi-directional relationship, but the specified mappedBy association on the target-entity ". + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". + "'inversedBy' attribute."; + } else if ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] != $fieldName) { + $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " are ". + "incosistent with each other."; + } + } + + if ($assoc['inversedBy']) { + if ($targetMetadata->hasField($assoc['inversedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which is not defined as association."; + } + if (!$targetMetadata->hasAssociation($assoc['inversedBy'])) { + $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the inverse side ". + "field " . $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " which does not exist."; + } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] == null) { + $ce[] = "The field " . $class->name . "#" . $fieldName . " is on the owning side of a ". + "bi-directional relationship, but the specified mappedBy association on the target-entity ". + $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " does not contain the required ". + "'inversedBy' attribute."; + } else if ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] != $fieldName) { + $ce[] = "The mappings " . $class->name . "#" . $fieldName . " and " . + $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " are ". + "incosistent with each other."; + } + } + + if ($assoc['isOwningSide']) { + if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY) { + foreach ($assoc['joinTable']['joinColumns'] AS $joinColumn) { + if (!isset($class->fieldNames[$joinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $class->name . "'."; + break; + } + + $fieldName = $class->fieldNames[$joinColumn['referencedColumnName']]; + if (!in_array($fieldName, $class->identifier)) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + foreach ($assoc['joinTable']['inverseJoinColumns'] AS $inverseJoinColumn) { + if (!isset($targetMetadata->fieldNames[$inverseJoinColumn['referencedColumnName']])) { + $ce[] = "The inverse referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $targetMetadata->name . "'."; + break; + } + + $fieldName = $targetMetadata->fieldNames[$inverseJoinColumn['referencedColumnName']]; + if (!in_array($fieldName, $targetMetadata->identifier)) { + $ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + + if (count($targetMetadata->getIdentifierColumnNames()) != count($assoc['joinTable']['inverseJoinColumns'])) { + $ce[] = "The inverse join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . + "have to contain to ALL identifier columns of the target entity '". $targetMetadata->name . "', " . + "however '" . implode(", ", array_diff($targetMetadata->getIdentifierColumnNames(), $assoc['relationToTargetKeyColumns'])) . + "' are missing."; + } + + if (count($class->getIdentifierColumnNames()) != count($assoc['joinTable']['joinColumns'])) { + $ce[] = "The join columns of the many-to-many table '" . $assoc['joinTable']['name'] . "' " . + "have to contain to ALL identifier columns of the source entity '". $class->name . "', " . + "however '" . implode(", ", array_diff($class->getIdentifierColumnNames(), $assoc['relationToSourceKeyColumns'])) . + "' are missing."; + } + + } else if ($assoc['type'] & ClassMetadataInfo::TO_ONE) { + foreach ($assoc['joinColumns'] AS $joinColumn) { + if (!isset($targetMetadata->fieldNames[$joinColumn['referencedColumnName']])) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' does not " . + "have a corresponding field with this column name on the class '" . $targetMetadata->name . "'."; + break; + } + + $fieldName = $targetMetadata->fieldNames[$joinColumn['referencedColumnName']]; + if (!in_array($fieldName, $targetMetadata->identifier)) { + $ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " . + "has to be a primary key column."; + } + } + + if (count($class->getIdentifierColumnNames()) != count($assoc['joinColumns'])) { + $ce[] = "The join columns of the association '" . $assoc['fieldName'] . "' " . + "have to match to ALL identifier columns of the source entity '". $class->name . "', " . + "however '" . implode(", ", array_diff($class->getIdentifierColumnNames(), $assoc['joinColumns'])) . + "' are missing."; + } + } + } + + if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) { + foreach ($assoc['orderBy'] AS $orderField => $orientation) { + if (!$targetMetadata->hasField($orderField)) { + $ce[] = "The association " . $class->name."#".$fieldName." is ordered by a foreign field " . + $orderField . " that is not a field on the target entity " . $targetMetadata->name; + } + } + } + } + + foreach ($class->reflClass->getProperties(\ReflectionProperty::IS_PUBLIC) as $publicAttr) { + if ($publicAttr->isStatic()) { + continue; + } + $ce[] = "Field '".$publicAttr->getName()."' in class '".$class->name."' must be private ". + "or protected. Public fields may break lazy-loading."; + } + + foreach ($class->subClasses AS $subClass) { + if (!in_array($class->name, class_parents($subClass))) { + $ce[] = "According to the discriminator map class '" . $subClass . "' has to be a child ". + "of '" . $class->name . "' but these entities are not related through inheritance."; + } + } + + return $ce; + } + /** * Check if the Database Schema is in sync with the current metadata state. * diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php index 64bb03f36..0fc3bb636 100644 --- a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php @@ -71,4 +71,88 @@ class SchemaValidatorTest extends \Doctrine\Tests\OrmTestCase )); $this->validator->validateMapping(); } + + /** + * @group DDC-1439 + */ + public function testInvalidManyToManyJoinColumnSchema() + { + $class1 = $this->em->getClassMetadata(__NAMESPACE__ . '\InvalidEntity1'); + $class2 = $this->em->getClassMetadata(__NAMESPACE__ . '\InvalidEntity2'); + + $ce = $this->validator->validateClass($class1); + + $this->assertEquals( + array( + "The inverse join columns of the many-to-many table 'Entity1Entity2' have to contain to ALL identifier columns of the target entity 'Doctrine\Tests\ORM\Tools\InvalidEntity2', however 'key4' are missing.", + "The join columns of the many-to-many table 'Entity1Entity2' have to contain to ALL identifier columns of the source entity 'Doctrine\Tests\ORM\Tools\InvalidEntity1', however 'key2' are missing." + ), + $ce + ); + } + + /** + * @group DDC-1439 + */ + public function testInvalidToOneJoinColumnSchema() + { + $class1 = $this->em->getClassMetadata(__NAMESPACE__ . '\InvalidEntity1'); + $class2 = $this->em->getClassMetadata(__NAMESPACE__ . '\InvalidEntity2'); + + $ce = $this->validator->validateClass($class2); + + $this->assertEquals( + array( + "The referenced column name 'id' does not have a corresponding field with this column name on the class 'Doctrine\Tests\ORM\Tools\InvalidEntity1'.", + "The join columns of the association 'assoc' have to match to ALL identifier columns of the source entity 'Doctrine\Tests\ORM\Tools\InvalidEntity2', however 'key3, key4' are missing." + ), + $ce + ); + } +} + +/** + * @Entity + */ +class InvalidEntity1 +{ + /** + * @Id @Column + */ + protected $key1; + /** + * @Id @Column + */ + protected $key2; + /** + * @ManyToMany (targetEntity="InvalidEntity2") + * @JoinTable (name="Entity1Entity2", + * joinColumns={@JoinColumn(name="key1", referencedColumnName="key1")}, + * inverseJoinColumns={@JoinColumn(name="key3", referencedColumnName="key3")} + * ) + */ + protected $entity2; +} + +/** + * @Entity + */ +class InvalidEntity2 +{ + /** + * @Id @Column + * @GeneratedValue(strategy="AUTO") + */ + protected $key3; + + /** + * @Id @Column + * @GeneratedValue(strategy="AUTO") + */ + protected $key4; + + /** + * @ManyToOne(targetEntity="InvalidEntity1") + */ + protected $assoc; } \ No newline at end of file