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

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\ORM\EntityManagerInterface;
  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/mit-license.php MIT
  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 EntityManagerInterface
  38.      */
  39.     private $em;
  40.     /**
  41.      * @param EntityManagerInterface $em
  42.      */
  43.     public function __construct(EntityManagerInterface $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.      *
  57.      * @return array
  58.      */
  59.     public function validateMapping()
  60.     {
  61.         $errors = [];
  62.         $cmf $this->em->getMetadataFactory();
  63.         $classes $cmf->getAllMetadata();
  64.         foreach ($classes as $class) {
  65.             if ($ce $this->validateClass($class)) {
  66.                 $errors[$class->name] = $ce;
  67.             }
  68.         }
  69.         return $errors;
  70.     }
  71.     /**
  72.      * Validates a single class of the current.
  73.      *
  74.      * @param ClassMetadataInfo $class
  75.      *
  76.      * @return array
  77.      */
  78.     public function validateClass(ClassMetadataInfo $class)
  79.     {
  80.         $ce = [];
  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-existent 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.             if ($assoc['mappedBy']) {
  101.                 if ($targetMetadata->hasField($assoc['mappedBy'])) {
  102.                     $ce[] = "The association " $class->name "#" $fieldName " refers to the owning side ".
  103.                             "field " $assoc['targetEntity'] . "#" $assoc['mappedBy'] . " which is not defined as association, but as field.";
  104.                 }
  105.                 if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) {
  106.                     $ce[] = "The association " $class->name "#" $fieldName " refers to the owning side ".
  107.                             "field " $assoc['targetEntity'] . "#" $assoc['mappedBy'] . " which does not exist.";
  108.                 } elseif ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] == null) {
  109.                     $ce[] = "The field " $class->name "#" $fieldName " is on the inverse side of a ".
  110.                             "bi-directional relationship, but the specified mappedBy association on the target-entity ".
  111.                             $assoc['targetEntity'] . "#" $assoc['mappedBy'] . " does not contain the required ".
  112.                             "'inversedBy=\"" $fieldName "\"' attribute.";
  113.                 } elseif ($targetMetadata->associationMappings[$assoc['mappedBy']]['inversedBy'] != $fieldName) {
  114.                     $ce[] = "The mappings " $class->name "#" $fieldName " and " .
  115.                             $assoc['targetEntity'] . "#" $assoc['mappedBy'] . " are ".
  116.                             "inconsistent with each other.";
  117.                 }
  118.             }
  119.             if ($assoc['inversedBy']) {
  120.                 if ($targetMetadata->hasField($assoc['inversedBy'])) {
  121.                     $ce[] = "The association " $class->name "#" $fieldName " refers to the inverse side ".
  122.                             "field " $assoc['targetEntity'] . "#" $assoc['inversedBy'] . " which is not defined as association.";
  123.                 }
  124.                 if (!$targetMetadata->hasAssociation($assoc['inversedBy'])) {
  125.                     $ce[] = "The association " $class->name "#" $fieldName " refers to the inverse side ".
  126.                             "field " $assoc['targetEntity'] . "#" $assoc['inversedBy'] . " which does not exist.";
  127.                 } elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] == null) {
  128.                     $ce[] = "The field " $class->name "#" $fieldName " is on the owning side of a ".
  129.                             "bi-directional relationship, but the specified mappedBy association on the target-entity ".
  130.                             $assoc['targetEntity'] . "#" $assoc['mappedBy'] . " does not contain the required ".
  131.                             "'inversedBy' attribute.";
  132.                 } elseif ($targetMetadata->associationMappings[$assoc['inversedBy']]['mappedBy'] != $fieldName) {
  133.                     $ce[] = "The mappings " $class->name "#" $fieldName " and " .
  134.                             $assoc['targetEntity'] . "#" $assoc['inversedBy'] . " are ".
  135.                             "inconsistent with each other.";
  136.                 }
  137.                 // Verify inverse side/owning side match each other
  138.                 if (array_key_exists($assoc['inversedBy'], $targetMetadata->associationMappings)) {
  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.                     } elseif ($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.                     } elseif ($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.             }
  152.             if ($assoc['isOwningSide']) {
  153.                 if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY) {
  154.                     $identifierColumns $class->getIdentifierColumnNames();
  155.                     foreach ($assoc['joinTable']['joinColumns'] as $joinColumn) {
  156.                         if (!in_array($joinColumn['referencedColumnName'], $identifierColumns)) {
  157.                             $ce[] = "The referenced column name '" $joinColumn['referencedColumnName'] . "' " .
  158.                                 "has to be a primary key column on the target entity class '".$class->name."'.";
  159.                             break;
  160.                         }
  161.                     }
  162.                     $identifierColumns $targetMetadata->getIdentifierColumnNames();
  163.                     foreach ($assoc['joinTable']['inverseJoinColumns'] as $inverseJoinColumn) {
  164.                         if (! in_array($inverseJoinColumn['referencedColumnName'], $identifierColumns)) {
  165.                             $ce[] = "The referenced column name '" $inverseJoinColumn['referencedColumnName'] . "' " .
  166.                                 "has to be a primary key column on the target entity class '" .$targetMetadata->name "'.";
  167.                             break;
  168.                         }
  169.                     }
  170.                     if (count($targetMetadata->getIdentifierColumnNames()) != count($assoc['joinTable']['inverseJoinColumns'])) {
  171.                         $ce[] = "The inverse join columns of the many-to-many table '" $assoc['joinTable']['name'] . "' " .
  172.                                 "have to contain to ALL identifier columns of the target entity '"$targetMetadata->name "', " .
  173.                                 "however '" implode(", "array_diff($targetMetadata->getIdentifierColumnNames(), array_values($assoc['relationToTargetKeyColumns']))) .
  174.                                 "' are missing.";
  175.                     }
  176.                     if (count($class->getIdentifierColumnNames()) != count($assoc['joinTable']['joinColumns'])) {
  177.                         $ce[] = "The join columns of the many-to-many table '" $assoc['joinTable']['name'] . "' " .
  178.                                 "have to contain to ALL identifier columns of the source entity '"$class->name "', " .
  179.                                 "however '" implode(", "array_diff($class->getIdentifierColumnNames(), array_values($assoc['relationToSourceKeyColumns']))) .
  180.                                 "' are missing.";
  181.                     }
  182.                 } elseif ($assoc['type'] & ClassMetadataInfo::TO_ONE) {
  183.                     $identifierColumns $targetMetadata->getIdentifierColumnNames();
  184.                     foreach ($assoc['joinColumns'] as $joinColumn) {
  185.                         if (!in_array($joinColumn['referencedColumnName'], $identifierColumns)) {
  186.                             $ce[] = "The referenced column name '" $joinColumn['referencedColumnName'] . "' " .
  187.                                     "has to be a primary key column on the target entity class '".$targetMetadata->name."'.";
  188.                         }
  189.                     }
  190.                     if (count($identifierColumns) != count($assoc['joinColumns'])) {
  191.                         $ids = [];
  192.                         foreach ($assoc['joinColumns'] as $joinColumn) {
  193.                             $ids[] = $joinColumn['name'];
  194.                         }
  195.                         $ce[] = "The join columns of the association '" $assoc['fieldName'] . "' " .
  196.                                 "have to match to ALL identifier columns of the target entity '"$targetMetadata->name "', " .
  197.                                 "however '" implode(", "array_diff($targetMetadata->getIdentifierColumnNames(), $ids)) .
  198.                                 "' are missing.";
  199.                     }
  200.                 }
  201.             }
  202.             if (isset($assoc['orderBy']) && $assoc['orderBy'] !== null) {
  203.                 foreach ($assoc['orderBy'] as $orderField => $orientation) {
  204.                     if (!$targetMetadata->hasField($orderField) && !$targetMetadata->hasAssociation($orderField)) {
  205.                         $ce[] = "The association " $class->name."#".$fieldName." is ordered by a foreign field " .
  206.                                 $orderField " that is not a field on the target entity " $targetMetadata->name ".";
  207.                         continue;
  208.                     }
  209.                     if ($targetMetadata->isCollectionValuedAssociation($orderField)) {
  210.                         $ce[] = "The association " $class->name."#".$fieldName." is ordered by a field " .
  211.                                 $orderField " on " $targetMetadata->name " that is a collection-valued association.";
  212.                         continue;
  213.                     }
  214.                     if ($targetMetadata->isAssociationInverseSide($orderField)) {
  215.                         $ce[] = "The association " $class->name."#".$fieldName." is ordered by a field " .
  216.                                 $orderField " on " $targetMetadata->name " that is the inverse side of an association.";
  217.                         continue;
  218.                     }
  219.                 }
  220.             }
  221.         }
  222.         foreach ($class->subClasses as $subClass) {
  223.             if (!in_array($class->nameclass_parents($subClass))) {
  224.                 $ce[] = "According to the discriminator map class '" $subClass "' has to be a child ".
  225.                         "of '" $class->name "' but these entities are not related through inheritance.";
  226.             }
  227.         }
  228.         return $ce;
  229.     }
  230.     /**
  231.      * Checks if the Database Schema is in sync with the current metadata state.
  232.      *
  233.      * @return bool
  234.      */
  235.     public function schemaInSyncWithMetadata()
  236.     {
  237.         $schemaTool = new SchemaTool($this->em);
  238.         $allMetadata $this->em->getMetadataFactory()->getAllMetadata();
  239.         return count($schemaTool->getUpdateSchemaSql($allMetadatatrue)) == 0;
  240.     }
  241. }