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

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