vendor/doctrine/orm/lib/Doctrine/ORM/Tools/SchemaValidator.php line 86

Open in your IDE?
  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 MIT license. For more information, see
  17.  * <http://www.doctrine-project.org>.
  18.  */
  19. namespace Doctrine\ORM\Tools;
  20. use Doctrine\DBAL\Types\Type;
  21. use Doctrine\ORM\EntityManagerInterface;
  22. use Doctrine\ORM\Mapping\ClassMetadataInfo;
  23. use function array_diff;
  24. use function array_key_exists;
  25. use function array_search;
  26. use function array_values;
  27. use function class_exists;
  28. use function class_parents;
  29. use function count;
  30. use function implode;
  31. use function in_array;
  32. /**
  33.  * Performs strict validation of the mapping schema
  34.  *
  35.  * @link        www.doctrine-project.com
  36.  */
  37. class SchemaValidator
  38. {
  39.     /** @var EntityManagerInterface */
  40.     private $em;
  41.     public function __construct(EntityManagerInterface $em)
  42.     {
  43.         $this->em $em;
  44.     }
  45.     /**
  46.      * Checks the internal consistency of all mapping files.
  47.      *
  48.      * There are several checks that can't be done at runtime or are too expensive, which can be verified
  49.      * with this command. For example:
  50.      *
  51.      * 1. Check if a relation with "mappedBy" is actually connected to that specified field.
  52.      * 2. Check if "mappedBy" and "inversedBy" are consistent to each other.
  53.      * 3. Check if "referencedColumnName" attributes are really pointing to primary key columns.
  54.      *
  55.      * @psalm-return array<string, list<string>>
  56.      */
  57.     public function validateMapping()
  58.     {
  59.         $errors  = [];
  60.         $cmf     $this->em->getMetadataFactory();
  61.         $classes $cmf->getAllMetadata();
  62.         foreach ($classes as $class) {
  63.             $ce $this->validateClass($class);
  64.             if ($ce) {
  65.                 $errors[$class->name] = $ce;
  66.             }
  67.         }
  68.         return $errors;
  69.     }
  70.     /**
  71.      * Validates a single class of the current.
  72.      *
  73.      * @return string[]
  74.      * @psalm-return list<string>
  75.      */
  76.     public function validateClass(ClassMetadataInfo $class)
  77.     {
  78.         $ce  = [];
  79.         $cmf $this->em->getMetadataFactory();
  80.         foreach ($class->fieldMappings as $fieldName => $mapping) {
  81.             if (! Type::hasType($mapping['type'])) {
  82.                 $ce[] = "The field '" $class->name '#' $fieldName "' uses a non-existent type '" $mapping['type'] . "'.";
  83.             }
  84.         }
  85.         if ($class->isEmbeddedClass && count($class->associationMappings) > 0) {
  86.             $ce[] = "Embeddable '" $class->name "' does not support associations";
  87.             return $ce;
  88.         }
  89.         foreach ($class->associationMappings as $fieldName => $assoc) {
  90.             if (! class_exists($assoc['targetEntity']) || $cmf->isTransient($assoc['targetEntity'])) {
  91.                 $ce[] = "The target entity '" $assoc['targetEntity'] . "' specified on " $class->name '#' $fieldName ' is unknown or not an entity.';
  92.                 return $ce;
  93.             }
  94.             if ($assoc['mappedBy'] && $assoc['inversedBy']) {
  95.                 $ce[] = 'The association ' $class '#' $fieldName ' cannot be defined as both inverse and owning.';
  96.             }
  97.             $targetMetadata $cmf->getMetadataFor($assoc['targetEntity']);
  98.             if (isset($assoc['id']) && $targetMetadata->containsForeignIdentifier) {
  99.                 $ce[] = "Cannot map association '" $class->name '#' $fieldName ' as identifier, because ' .
  100.                         "the target entity '" $targetMetadata->name "' also maps an association as identifier.";
  101.             }
  102.             if ($assoc['mappedBy']) {
  103.                 if ($targetMetadata->hasField($assoc['mappedBy'])) {
  104.                     $ce[] = 'The association ' $class->name '#' $fieldName ' refers to the owning side ' .
  105.                             'field ' $assoc['targetEntity'] . '#' $assoc['mappedBy'] . ' which is not defined as association, but as field.';
  106.                 }
  107.                 if (! $targetMetadata->hasAssociation($assoc['mappedBy'])) {
  108.                     $ce[] = 'The association ' $class->name '#' $fieldName ' refers to the owning side ' .
  109.                             'field ' $assoc['targetEntity'] . '#' $assoc['mappedBy'] . ' which does not exist.';
  110.                 } elseif ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] === null) {
  111.                     $ce[] = 'The field ' $class->name '#' $fieldName ' is on the inverse side of a ' .
  112.                             'bi-directional relationship, but the specified mappedBy association on the target-entity ' .
  113.                             $assoc['targetEntity'] . '#' $assoc['mappedBy'] . ' does not contain the required ' .
  114.                             "'inversedBy=\"" $fieldName "\"' attribute.";
  115.                 } elseif ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] !== $fieldName) {
  116.                     $ce[] = 'The mappings ' $class->name '#' $fieldName ' and ' .
  117.                             $assoc['targetEntity'] . '#' $assoc['mappedBy'] . ' are ' .
  118.                             'inconsistent with each other.';
  119.                 }
  120.             }
  121.             if ($assoc['inversedBy']) {
  122.                 if ($targetMetadata->hasField($assoc['inversedBy'])) {
  123.                     $ce[] = 'The association ' $class->name '#' $fieldName ' refers to the inverse side ' .
  124.                             'field ' $assoc['targetEntity'] . '#' $assoc['inversedBy'] . ' which is not defined as association.';
  125.                 }
  126.                 if (! $targetMetadata->hasAssociation($assoc['inversedBy'])) {
  127.                     $ce[] = 'The association ' $class->name '#' $fieldName ' refers to the inverse side ' .
  128.                             'field ' $assoc['targetEntity'] . '#' $assoc['inversedBy'] . ' which does not exist.';
  129.                 } elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] === null) {
  130.                     $ce[] = 'The field ' $class->name '#' $fieldName ' is on the owning side of a ' .
  131.                             'bi-directional relationship, but the specified mappedBy association on the target-entity ' .
  132.                             $assoc['targetEntity'] . '#' $assoc['mappedBy'] . ' does not contain the required ' .
  133.                             "'inversedBy' attribute.";
  134.                 } elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] !== $fieldName) {
  135.                     $ce[] = 'The mappings ' $class->name '#' $fieldName ' and ' .
  136.                             $assoc['targetEntity'] . '#' $assoc['inversedBy'] . ' are ' .
  137.                             'inconsistent with each other.';
  138.                 }
  139.                 // Verify inverse side/owning side match each other
  140.                 if (array_key_exists($assoc['inversedBy'], $targetMetadata->associationMappings)) {
  141.                     $targetAssoc $targetMetadata->associationMappings[$assoc['inversedBy']];
  142.                     if ($assoc['type'] === ClassMetadataInfo::ONE_TO_ONE && $targetAssoc['type'] !== ClassMetadataInfo::ONE_TO_ONE) {
  143.                         $ce[] = 'If association ' $class->name '#' $fieldName ' is one-to-one, then the inversed ' .
  144.                                 'side ' $targetMetadata->name '#' $assoc['inversedBy'] . ' has to be one-to-one as well.';
  145.                     } elseif ($assoc['type'] === ClassMetadataInfo::MANY_TO_ONE && $targetAssoc['type'] !== ClassMetadataInfo::ONE_TO_MANY) {
  146.                         $ce[] = 'If association ' $class->name '#' $fieldName ' is many-to-one, then the inversed ' .
  147.                                 'side ' $targetMetadata->name '#' $assoc['inversedBy'] . ' has to be one-to-many.';
  148.                     } elseif ($assoc['type'] === ClassMetadataInfo::MANY_TO_MANY && $targetAssoc['type'] !== ClassMetadataInfo::MANY_TO_MANY) {
  149.                         $ce[] = 'If association ' $class->name '#' $fieldName ' is many-to-many, then the inversed ' .
  150.                                 'side ' $targetMetadata->name '#' $assoc['inversedBy'] . ' has to be many-to-many as well.';
  151.                     }
  152.                 }
  153.             }
  154.             if ($assoc['isOwningSide']) {
  155.                 if ($assoc['type'] === ClassMetadataInfo::MANY_TO_MANY) {
  156.                     $identifierColumns $class->getIdentifierColumnNames();
  157.                     foreach ($assoc['joinTable']['joinColumns'] as $joinColumn) {
  158.                         if (! in_array($joinColumn['referencedColumnName'], $identifierColumns)) {
  159.                             $ce[] = "The referenced column name '" $joinColumn['referencedColumnName'] . "' " .
  160.                                 "has to be a primary key column on the target entity class '" $class->name "'.";
  161.                             break;
  162.                         }
  163.                     }
  164.                     $identifierColumns $targetMetadata->getIdentifierColumnNames();
  165.                     foreach ($assoc['joinTable']['inverseJoinColumns'] as $inverseJoinColumn) {
  166.                         if (! in_array($inverseJoinColumn['referencedColumnName'], $identifierColumns)) {
  167.                             $ce[] = "The referenced column name '" $inverseJoinColumn['referencedColumnName'] . "' " .
  168.                                 "has to be a primary key column on the target entity class '" $targetMetadata->name "'.";
  169.                             break;
  170.                         }
  171.                     }
  172.                     if (count($targetMetadata->getIdentifierColumnNames()) !== count($assoc['joinTable']['inverseJoinColumns'])) {
  173.                         $ce[] = "The inverse join columns of the many-to-many table '" $assoc['joinTable']['name'] . "' " .
  174.                                 "have to contain to ALL identifier columns of the target entity '" $targetMetadata->name "', " .
  175.                                 "however '" implode(', 'array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc['relationToTargetKeyColumns']))) .
  176.                                 "' are missing.";
  177.                     }
  178.                     if (count($class->getIdentifierColumnNames()) !== count($assoc['joinTable']['joinColumns'])) {
  179.                         $ce[] = "The join columns of the many-to-many table '" $assoc['joinTable']['name'] . "' " .
  180.                                 "have to contain to ALL identifier columns of the source entity '" $class->name "', " .
  181.                                 "however '" implode(', 'array_diff($class->getIdentifierColumnNames(), array_values($assoc['relationToSourceKeyColumns']))) .
  182.                                 "' are missing.";
  183.                     }
  184.                 } elseif ($assoc['type'] & ClassMetadataInfo::TO_ONE) {
  185.                     $identifierColumns $targetMetadata->getIdentifierColumnNames();
  186.                     foreach ($assoc['joinColumns'] as $joinColumn) {
  187.                         if (! in_array($joinColumn['referencedColumnName'], $identifierColumns)) {
  188.                             $ce[] = "The referenced column name '" $joinColumn['referencedColumnName'] . "' " .
  189.                                     "has to be a primary key column on the target entity class '" $targetMetadata->name "'.";
  190.                         }
  191.                     }
  192.                     if (count($identifierColumns) !== count($assoc['joinColumns'])) {
  193.                         $ids = [];
  194.                         foreach ($assoc['joinColumns'] as $joinColumn) {
  195.                             $ids[] = $joinColumn['name'];
  196.                         }
  197.                         $ce[] = "The join columns of the association '" $assoc['fieldName'] . "' " .
  198.                                 "have to match to ALL identifier columns of the target entity '" $targetMetadata->name "', " .
  199.                                 "however '" implode(', 'array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) .
  200.                                 "' are missing.";
  201.                     }
  202.                 }
  203.             }
  204.             if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) {
  205.                 foreach ($assoc['orderBy'] as $orderField => $orientation) {
  206.                     if (! $targetMetadata->hasField($orderField) && ! $targetMetadata->hasAssociation($orderField)) {
  207.                         $ce[] = 'The association ' $class->name '#' $fieldName ' is ordered by a foreign field ' .
  208.                                 $orderField ' that is not a field on the target entity ' $targetMetadata->name '.';
  209.                         continue;
  210.                     }
  211.                     if ($targetMetadata->isCollectionValuedAssociation($orderField)) {
  212.                         $ce[] = 'The association ' $class->name '#' $fieldName ' is ordered by a field ' .
  213.                                 $orderField ' on ' $targetMetadata->name ' that is a collection-valued association.';
  214.                         continue;
  215.                     }
  216.                     if ($targetMetadata->isAssociationInverseSide($orderField)) {
  217.                         $ce[] = 'The association ' $class->name '#' $fieldName ' is ordered by a field ' .
  218.                                 $orderField ' on ' $targetMetadata->name ' that is the inverse side of an association.';
  219.                         continue;
  220.                     }
  221.                 }
  222.             }
  223.         }
  224.         if (! $class->isInheritanceTypeNone() && ! $class->isRootEntity() && ! $class->isMappedSuperclass && array_search($class->name$class->discriminatorMap) === false) {
  225.             $ce[] = "Entity class '" $class->name "' is part of inheritance hierarchy, but is " .
  226.                 "not mapped in the root entity '" $class->rootEntityName "' discriminator map. " .
  227.                 'All subclasses must be listed in the discriminator map.';
  228.         }
  229.         foreach ($class->subClasses as $subClass) {
  230.             if (! in_array($class->nameclass_parents($subClass))) {
  231.                 $ce[] = "According to the discriminator map class '" $subClass "' has to be a child " .
  232.                         "of '" $class->name "' but these entities are not related through inheritance.";
  233.             }
  234.         }
  235.         return $ce;
  236.     }
  237.     /**
  238.      * Checks if the Database Schema is in sync with the current metadata state.
  239.      *
  240.      * @return bool
  241.      */
  242.     public function schemaInSyncWithMetadata()
  243.     {
  244.         $schemaTool = new SchemaTool($this->em);
  245.         $allMetadata $this->em->getMetadataFactory()->getAllMetadata();
  246.         return count($schemaTool->getUpdateSchemaSql($allMetadatatrue)) === 0;
  247.     }
  248. }