vendor/doctrine/orm/lib/Doctrine/ORM/Tools/SchemaTool.php line 879

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\Schema\AbstractAsset;
  21. use Doctrine\DBAL\Schema\Comparator;
  22. use Doctrine\DBAL\Schema\Index;
  23. use Doctrine\DBAL\Schema\Schema;
  24. use Doctrine\DBAL\Schema\Table;
  25. use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
  26. use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets;
  27. use Doctrine\ORM\EntityManagerInterface;
  28. use Doctrine\ORM\Mapping\ClassMetadata;
  29. use Doctrine\ORM\ORMException;
  30. use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
  31. use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
  32. /**
  33.  * The SchemaTool is a tool to create/drop/update database schemas based on
  34.  * <tt>ClassMetadata</tt> class descriptors.
  35.  *
  36.  * @link    www.doctrine-project.org
  37.  * @since   2.0
  38.  * @author  Guilherme Blanco <guilhermeblanco@hotmail.com>
  39.  * @author  Jonathan Wage <jonwage@gmail.com>
  40.  * @author  Roman Borschel <roman@code-factory.org>
  41.  * @author  Benjamin Eberlei <kontakt@beberlei.de>
  42.  * @author  Stefano Rodriguez <stefano.rodriguez@fubles.com>
  43.  */
  44. class SchemaTool
  45. {
  46.     private const KNOWN_COLUMN_OPTIONS = ['comment''unsigned''fixed''default'];
  47.     /**
  48.      * @var \Doctrine\ORM\EntityManagerInterface
  49.      */
  50.     private $em;
  51.     /**
  52.      * @var \Doctrine\DBAL\Platforms\AbstractPlatform
  53.      */
  54.     private $platform;
  55.     /**
  56.      * The quote strategy.
  57.      *
  58.      * @var \Doctrine\ORM\Mapping\QuoteStrategy
  59.      */
  60.     private $quoteStrategy;
  61.     /**
  62.      * Initializes a new SchemaTool instance that uses the connection of the
  63.      * provided EntityManager.
  64.      *
  65.      * @param \Doctrine\ORM\EntityManagerInterface $em
  66.      */
  67.     public function __construct(EntityManagerInterface $em)
  68.     {
  69.         $this->em               $em;
  70.         $this->platform         $em->getConnection()->getDatabasePlatform();
  71.         $this->quoteStrategy    $em->getConfiguration()->getQuoteStrategy();
  72.     }
  73.     /**
  74.      * Creates the database schema for the given array of ClassMetadata instances.
  75.      *
  76.      * @param array $classes
  77.      *
  78.      * @return void
  79.      *
  80.      * @throws ToolsException
  81.      */
  82.     public function createSchema(array $classes)
  83.     {
  84.         $createSchemaSql $this->getCreateSchemaSql($classes);
  85.         $conn $this->em->getConnection();
  86.         foreach ($createSchemaSql as $sql) {
  87.             try {
  88.                 $conn->executeQuery($sql);
  89.             } catch (\Throwable $e) {
  90.                 throw ToolsException::schemaToolFailure($sql$e);
  91.             }
  92.         }
  93.     }
  94.     /**
  95.      * Gets the list of DDL statements that are required to create the database schema for
  96.      * the given list of ClassMetadata instances.
  97.      *
  98.      * @param array $classes
  99.      *
  100.      * @return array The SQL statements needed to create the schema for the classes.
  101.      */
  102.     public function getCreateSchemaSql(array $classes)
  103.     {
  104.         $schema $this->getSchemaFromMetadata($classes);
  105.         return $schema->toSql($this->platform);
  106.     }
  107.     /**
  108.      * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
  109.      *
  110.      * @param ClassMetadata $class
  111.      * @param array         $processedClasses
  112.      *
  113.      * @return bool
  114.      */
  115.     private function processingNotRequired($class, array $processedClasses)
  116.     {
  117.         return (
  118.             isset($processedClasses[$class->name]) ||
  119.             $class->isMappedSuperclass ||
  120.             $class->isEmbeddedClass ||
  121.             ($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName)
  122.         );
  123.     }
  124.     /**
  125.      * Creates a Schema instance from a given set of metadata classes.
  126.      *
  127.      * @param array $classes
  128.      *
  129.      * @return Schema
  130.      *
  131.      * @throws \Doctrine\ORM\ORMException
  132.      */
  133.     public function getSchemaFromMetadata(array $classes)
  134.     {
  135.         // Reminder for processed classes, used for hierarchies
  136.         $processedClasses       = [];
  137.         $eventManager           $this->em->getEventManager();
  138.         $schemaManager          $this->em->getConnection()->getSchemaManager();
  139.         $metadataSchemaConfig   $schemaManager->createSchemaConfig();
  140.         $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
  141.         $schema = new Schema([], [], $metadataSchemaConfig);
  142.         $addedFks = [];
  143.         $blacklistedFks = [];
  144.         foreach ($classes as $class) {
  145.             /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
  146.             if ($this->processingNotRequired($class$processedClasses)) {
  147.                 continue;
  148.             }
  149.             $table $schema->createTable($this->quoteStrategy->getTableName($class$this->platform));
  150.             if ($class->isInheritanceTypeSingleTable()) {
  151.                 $this->gatherColumns($class$table);
  152.                 $this->gatherRelationsSql($class$table$schema$addedFks$blacklistedFks);
  153.                 // Add the discriminator column
  154.                 $this->addDiscriminatorColumnDefinition($class$table);
  155.                 // Aggregate all the information from all classes in the hierarchy
  156.                 foreach ($class->parentClasses as $parentClassName) {
  157.                     // Parent class information is already contained in this class
  158.                     $processedClasses[$parentClassName] = true;
  159.                 }
  160.                 foreach ($class->subClasses as $subClassName) {
  161.                     $subClass $this->em->getClassMetadata($subClassName);
  162.                     $this->gatherColumns($subClass$table);
  163.                     $this->gatherRelationsSql($subClass$table$schema$addedFks$blacklistedFks);
  164.                     $processedClasses[$subClassName] = true;
  165.                 }
  166.             } elseif ($class->isInheritanceTypeJoined()) {
  167.                 // Add all non-inherited fields as columns
  168.                 foreach ($class->fieldMappings as $fieldName => $mapping) {
  169.                     if ( ! isset($mapping['inherited'])) {
  170.                         $this->gatherColumn($class$mapping$table);
  171.                     }
  172.                 }
  173.                 $this->gatherRelationsSql($class$table$schema$addedFks$blacklistedFks);
  174.                 // Add the discriminator column only to the root table
  175.                 if ($class->name == $class->rootEntityName) {
  176.                     $this->addDiscriminatorColumnDefinition($class$table);
  177.                 } else {
  178.                     // Add an ID FK column to child tables
  179.                     $pkColumns           = [];
  180.                     $inheritedKeyColumns = [];
  181.                     foreach ($class->identifier as $identifierField) {
  182.                         if (isset($class->fieldMappings[$identifierField]['inherited'])) {
  183.                             $idMapping $class->fieldMappings[$identifierField];
  184.                             $this->gatherColumn($class$idMapping$table);
  185.                             $columnName $this->quoteStrategy->getColumnName(
  186.                                 $identifierField,
  187.                                 $class,
  188.                                 $this->platform
  189.                             );
  190.                             // TODO: This seems rather hackish, can we optimize it?
  191.                             $table->getColumn($columnName)->setAutoincrement(false);
  192.                             $pkColumns[] = $columnName;
  193.                             $inheritedKeyColumns[] = $columnName;
  194.                             continue;
  195.                         }
  196.                         if (isset($class->associationMappings[$identifierField]['inherited'])) {
  197.                             $idMapping $class->associationMappings[$identifierField];
  198.                             $targetEntity current(
  199.                                 array_filter(
  200.                                     $classes,
  201.                                     function (ClassMetadata $class) use ($idMapping) : bool {
  202.                                         return $class->name === $idMapping['targetEntity'];
  203.                                     }
  204.                                 )
  205.                             );
  206.                             foreach ($idMapping['joinColumns'] as $joinColumn) {
  207.                                 if (isset($targetEntity->fieldMappings[$joinColumn['referencedColumnName']])) {
  208.                                     $columnName $this->quoteStrategy->getJoinColumnName(
  209.                                         $joinColumn,
  210.                                         $class,
  211.                                         $this->platform
  212.                                     );
  213.                                     $pkColumns[]           = $columnName;
  214.                                     $inheritedKeyColumns[] = $columnName;
  215.                                 }
  216.                             }
  217.                         }
  218.                     }
  219.                     if ( ! empty($inheritedKeyColumns)) {
  220.                         // Add a FK constraint on the ID column
  221.                         $table->addForeignKeyConstraint(
  222.                             $this->quoteStrategy->getTableName(
  223.                                 $this->em->getClassMetadata($class->rootEntityName),
  224.                                 $this->platform
  225.                             ),
  226.                             $inheritedKeyColumns,
  227.                             $inheritedKeyColumns,
  228.                             ['onDelete' => 'CASCADE']
  229.                         );
  230.                     }
  231.                     if ( ! empty($pkColumns)) {
  232.                         $table->setPrimaryKey($pkColumns);
  233.                     }
  234.                 }
  235.             } elseif ($class->isInheritanceTypeTablePerClass()) {
  236.                 throw ORMException::notSupported();
  237.             } else {
  238.                 $this->gatherColumns($class$table);
  239.                 $this->gatherRelationsSql($class$table$schema$addedFks$blacklistedFks);
  240.             }
  241.             $pkColumns = [];
  242.             foreach ($class->identifier as $identifierField) {
  243.                 if (isset($class->fieldMappings[$identifierField])) {
  244.                     $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField$class$this->platform);
  245.                 } elseif (isset($class->associationMappings[$identifierField])) {
  246.                     /* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
  247.                     $assoc $class->associationMappings[$identifierField];
  248.                     foreach ($assoc['joinColumns'] as $joinColumn) {
  249.                         $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn$class$this->platform);
  250.                     }
  251.                 }
  252.             }
  253.             if ( ! $table->hasIndex('primary')) {
  254.                 $table->setPrimaryKey($pkColumns);
  255.             }
  256.             // there can be unique indexes automatically created for join column
  257.             // if join column is also primary key we should keep only primary key on this column
  258.             // so, remove indexes overruled by primary key
  259.             $primaryKey $table->getIndex('primary');
  260.             foreach ($table->getIndexes() as $idxKey => $existingIndex) {
  261.                 if ($primaryKey->overrules($existingIndex)) {
  262.                     $table->dropIndex($idxKey);
  263.                 }
  264.             }
  265.             if (isset($class->table['indexes'])) {
  266.                 foreach ($class->table['indexes'] as $indexName => $indexData) {
  267.                     if ( ! isset($indexData['flags'])) {
  268.                         $indexData['flags'] = [];
  269.                     }
  270.                     $table->addIndex($indexData['columns'], is_numeric($indexName) ? null $indexName, (array) $indexData['flags'], $indexData['options'] ?? []);
  271.                 }
  272.             }
  273.             if (isset($class->table['uniqueConstraints'])) {
  274.                 foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
  275.                     $uniqIndex = new Index($indexName$indexData['columns'], truefalse, [], $indexData['options'] ?? []);
  276.                     foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
  277.                         if ($tableIndex->isFullfilledBy($uniqIndex)) {
  278.                             $table->dropIndex($tableIndexName);
  279.                             break;
  280.                         }
  281.                     }
  282.                     $table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null $indexName$indexData['options'] ?? []);
  283.                 }
  284.             }
  285.             if (isset($class->table['options'])) {
  286.                 foreach ($class->table['options'] as $key => $val) {
  287.                     $table->addOption($key$val);
  288.                 }
  289.             }
  290.             $processedClasses[$class->name] = true;
  291.             if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) {
  292.                 $seqDef     $class->sequenceGeneratorDefinition;
  293.                 $quotedName $this->quoteStrategy->getSequenceName($seqDef$class$this->platform);
  294.                 if ( ! $schema->hasSequence($quotedName)) {
  295.                     $schema->createSequence(
  296.                         $quotedName,
  297.                         $seqDef['allocationSize'],
  298.                         $seqDef['initialValue']
  299.                     );
  300.                 }
  301.             }
  302.             if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
  303.                 $eventManager->dispatchEvent(
  304.                     ToolEvents::postGenerateSchemaTable,
  305.                     new GenerateSchemaTableEventArgs($class$schema$table)
  306.                 );
  307.             }
  308.         }
  309.         if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
  310.             $schema->visit(new RemoveNamespacedAssets());
  311.         }
  312.         if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
  313.             $eventManager->dispatchEvent(
  314.                 ToolEvents::postGenerateSchema,
  315.                 new GenerateSchemaEventArgs($this->em$schema)
  316.             );
  317.         }
  318.         return $schema;
  319.     }
  320.     /**
  321.      * Gets a portable column definition as required by the DBAL for the discriminator
  322.      * column of a class.
  323.      *
  324.      * @param ClassMetadata $class
  325.      * @param Table         $table
  326.      *
  327.      * @return void
  328.      */
  329.     private function addDiscriminatorColumnDefinition($classTable $table)
  330.     {
  331.         $discrColumn $class->discriminatorColumn;
  332.         if ( ! isset($discrColumn['type']) ||
  333.             (strtolower($discrColumn['type']) == 'string' && ! isset($discrColumn['length']))
  334.         ) {
  335.             $discrColumn['type'] = 'string';
  336.             $discrColumn['length'] = 255;
  337.         }
  338.         $options = [
  339.             'length'    => $discrColumn['length'] ?? null,
  340.             'notnull'   => true
  341.         ];
  342.         if (isset($discrColumn['columnDefinition'])) {
  343.             $options['columnDefinition'] = $discrColumn['columnDefinition'];
  344.         }
  345.         $table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
  346.     }
  347.     /**
  348.      * Gathers the column definitions as required by the DBAL of all field mappings
  349.      * found in the given class.
  350.      *
  351.      * @param ClassMetadata $class
  352.      * @param Table         $table
  353.      *
  354.      * @return void
  355.      */
  356.     private function gatherColumns($classTable $table)
  357.     {
  358.         $pkColumns = [];
  359.         foreach ($class->fieldMappings as $mapping) {
  360.             if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
  361.                 continue;
  362.             }
  363.             $this->gatherColumn($class$mapping$table);
  364.             if ($class->isIdentifier($mapping['fieldName'])) {
  365.                 $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class$this->platform);
  366.             }
  367.         }
  368.     }
  369.     /**
  370.      * Creates a column definition as required by the DBAL from an ORM field mapping definition.
  371.      *
  372.      * @param ClassMetadata $class   The class that owns the field mapping.
  373.      * @param array         $mapping The field mapping.
  374.      * @param Table         $table
  375.      *
  376.      * @return void
  377.      */
  378.     private function gatherColumn($class, array $mappingTable $table)
  379.     {
  380.         $columnName $this->quoteStrategy->getColumnName($mapping['fieldName'], $class$this->platform);
  381.         $columnType $mapping['type'];
  382.         $options = [];
  383.         $options['length'] = $mapping['length'] ?? null;
  384.         $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
  385.         if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) {
  386.             $options['notnull'] = false;
  387.         }
  388.         $options['platformOptions'] = [];
  389.         $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
  390.         if (strtolower($columnType) === 'string' && null === $options['length']) {
  391.             $options['length'] = 255;
  392.         }
  393.         if (isset($mapping['precision'])) {
  394.             $options['precision'] = $mapping['precision'];
  395.         }
  396.         if (isset($mapping['scale'])) {
  397.             $options['scale'] = $mapping['scale'];
  398.         }
  399.         if (isset($mapping['default'])) {
  400.             $options['default'] = $mapping['default'];
  401.         }
  402.         if (isset($mapping['columnDefinition'])) {
  403.             $options['columnDefinition'] = $mapping['columnDefinition'];
  404.         }
  405.         // the 'default' option can be overwritten here
  406.         $options $this->gatherColumnOptions($mapping) + $options;
  407.         if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
  408.             $options['autoincrement'] = true;
  409.         }
  410.         if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
  411.             $options['autoincrement'] = false;
  412.         }
  413.         if ($table->hasColumn($columnName)) {
  414.             // required in some inheritance scenarios
  415.             $table->changeColumn($columnName$options);
  416.         } else {
  417.             $table->addColumn($columnName$columnType$options);
  418.         }
  419.         $isUnique $mapping['unique'] ?? false;
  420.         if ($isUnique) {
  421.             $table->addUniqueIndex([$columnName]);
  422.         }
  423.     }
  424.     /**
  425.      * Gathers the SQL for properly setting up the relations of the given class.
  426.      * This includes the SQL for foreign key constraints and join tables.
  427.      *
  428.      * @param ClassMetadata $class
  429.      * @param Table         $table
  430.      * @param Schema        $schema
  431.      * @param array         $addedFks
  432.      * @param array         $blacklistedFks
  433.      *
  434.      * @return void
  435.      *
  436.      * @throws \Doctrine\ORM\ORMException
  437.      */
  438.     private function gatherRelationsSql($class$table$schema, &$addedFks, &$blacklistedFks)
  439.     {
  440.         foreach ($class->associationMappings as $id => $mapping) {
  441.             if (isset($mapping['inherited']) && ! \in_array($id$class->identifiertrue)) {
  442.                 continue;
  443.             }
  444.             $foreignClass $this->em->getClassMetadata($mapping['targetEntity']);
  445.             if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
  446.                 $primaryKeyColumns = []; // PK is unnecessary for this relation-type
  447.                 $this->gatherRelationJoinColumns(
  448.                     $mapping['joinColumns'],
  449.                     $table,
  450.                     $foreignClass,
  451.                     $mapping,
  452.                     $primaryKeyColumns,
  453.                     $addedFks,
  454.                     $blacklistedFks
  455.                 );
  456.             } elseif ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) {
  457.                 //... create join table, one-many through join table supported later
  458.                 throw ORMException::notSupported();
  459.             } elseif ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
  460.                 // create join table
  461.                 $joinTable $mapping['joinTable'];
  462.                 $theJoinTable $schema->createTable(
  463.                     $this->quoteStrategy->getJoinTableName($mapping$foreignClass$this->platform)
  464.                 );
  465.                 $primaryKeyColumns = [];
  466.                 // Build first FK constraint (relation table => source table)
  467.                 $this->gatherRelationJoinColumns(
  468.                     $joinTable['joinColumns'],
  469.                     $theJoinTable,
  470.                     $class,
  471.                     $mapping,
  472.                     $primaryKeyColumns,
  473.                     $addedFks,
  474.                     $blacklistedFks
  475.                 );
  476.                 // Build second FK constraint (relation table => target table)
  477.                 $this->gatherRelationJoinColumns(
  478.                     $joinTable['inverseJoinColumns'],
  479.                     $theJoinTable,
  480.                     $foreignClass,
  481.                     $mapping,
  482.                     $primaryKeyColumns,
  483.                     $addedFks,
  484.                     $blacklistedFks
  485.                 );
  486.                 $theJoinTable->setPrimaryKey($primaryKeyColumns);
  487.             }
  488.         }
  489.     }
  490.     /**
  491.      * Gets the class metadata that is responsible for the definition of the referenced column name.
  492.      *
  493.      * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
  494.      * not a simple field, go through all identifier field names that are associations recursively and
  495.      * find that referenced column name.
  496.      *
  497.      * TODO: Is there any way to make this code more pleasing?
  498.      *
  499.      * @param ClassMetadata $class
  500.      * @param string        $referencedColumnName
  501.      *
  502.      * @return array (ClassMetadata, referencedFieldName)
  503.      */
  504.     private function getDefiningClass($class$referencedColumnName)
  505.     {
  506.         $referencedFieldName $class->getFieldName($referencedColumnName);
  507.         if ($class->hasField($referencedFieldName)) {
  508.             return [$class$referencedFieldName];
  509.         }
  510.         if (in_array($referencedColumnName$class->getIdentifierColumnNames())) {
  511.             // it seems to be an entity as foreign key
  512.             foreach ($class->getIdentifierFieldNames() as $fieldName) {
  513.                 if ($class->hasAssociation($fieldName)
  514.                     && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
  515.                     return $this->getDefiningClass(
  516.                         $this->em->getClassMetadata($class->associationMappings[$fieldName]['targetEntity']),
  517.                         $class->getSingleAssociationReferencedJoinColumnName($fieldName)
  518.                     );
  519.                 }
  520.             }
  521.         }
  522.         return null;
  523.     }
  524.     /**
  525.      * Gathers columns and fk constraints that are required for one part of relationship.
  526.      *
  527.      * @param array         $joinColumns
  528.      * @param Table         $theJoinTable
  529.      * @param ClassMetadata $class
  530.      * @param array         $mapping
  531.      * @param array         $primaryKeyColumns
  532.      * @param array         $addedFks
  533.      * @param array         $blacklistedFks
  534.      *
  535.      * @return void
  536.      *
  537.      * @throws \Doctrine\ORM\ORMException
  538.      */
  539.     private function gatherRelationJoinColumns(
  540.         $joinColumns,
  541.         $theJoinTable,
  542.         $class,
  543.         $mapping,
  544.         &$primaryKeyColumns,
  545.         &$addedFks,
  546.         &$blacklistedFks
  547.     )
  548.     {
  549.         $localColumns       = [];
  550.         $foreignColumns     = [];
  551.         $fkOptions          = [];
  552.         $foreignTableName   $this->quoteStrategy->getTableName($class$this->platform);
  553.         $uniqueConstraints  = [];
  554.         foreach ($joinColumns as $joinColumn) {
  555.             list($definingClass$referencedFieldName) = $this->getDefiningClass(
  556.                 $class,
  557.                 $joinColumn['referencedColumnName']
  558.             );
  559.             if ( ! $definingClass) {
  560.                 throw new \Doctrine\ORM\ORMException(
  561.                     'Column name `' $joinColumn['referencedColumnName'] . '` referenced for relation from '
  562.                     $mapping['sourceEntity'] . ' towards ' $mapping['targetEntity'] . ' does not exist.'
  563.                 );
  564.             }
  565.             $quotedColumnName    $this->quoteStrategy->getJoinColumnName($joinColumn$class$this->platform);
  566.             $quotedRefColumnName $this->quoteStrategy->getReferencedJoinColumnName(
  567.                 $joinColumn,
  568.                 $class,
  569.                 $this->platform
  570.             );
  571.             $primaryKeyColumns[] = $quotedColumnName;
  572.             $localColumns[]      = $quotedColumnName;
  573.             $foreignColumns[]    = $quotedRefColumnName;
  574.             if ( ! $theJoinTable->hasColumn($quotedColumnName)) {
  575.                 // Only add the column to the table if it does not exist already.
  576.                 // It might exist already if the foreign key is mapped into a regular
  577.                 // property as well.
  578.                 $fieldMapping $definingClass->getFieldMapping($referencedFieldName);
  579.                 $columnDef null;
  580.                 if (isset($joinColumn['columnDefinition'])) {
  581.                     $columnDef $joinColumn['columnDefinition'];
  582.                 } elseif (isset($fieldMapping['columnDefinition'])) {
  583.                     $columnDef $fieldMapping['columnDefinition'];
  584.                 }
  585.                 $columnOptions = ['notnull' => false'columnDefinition' => $columnDef];
  586.                 if (isset($joinColumn['nullable'])) {
  587.                     $columnOptions['notnull'] = ! $joinColumn['nullable'];
  588.                 }
  589.                 $columnOptions $columnOptions $this->gatherColumnOptions($fieldMapping);
  590.                 if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
  591.                     $columnOptions['length'] = $fieldMapping['length'];
  592.                 } elseif ($fieldMapping['type'] == "decimal") {
  593.                     $columnOptions['scale'] = $fieldMapping['scale'];
  594.                     $columnOptions['precision'] = $fieldMapping['precision'];
  595.                 }
  596.                 $theJoinTable->addColumn($quotedColumnName$fieldMapping['type'], $columnOptions);
  597.             }
  598.             if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
  599.                 $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
  600.             }
  601.             if (isset($joinColumn['onDelete'])) {
  602.                 $fkOptions['onDelete'] = $joinColumn['onDelete'];
  603.             }
  604.         }
  605.         // Prefer unique constraints over implicit simple indexes created for foreign keys.
  606.         // Also avoids index duplication.
  607.         foreach ($uniqueConstraints as $indexName => $unique) {
  608.             $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null $indexName);
  609.         }
  610.         $compositeName $theJoinTable->getName().'.'.implode(''$localColumns);
  611.         if (isset($addedFks[$compositeName])
  612.             && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
  613.             || count(array_diff($foreignColumns$addedFks[$compositeName]['foreignColumns'])))
  614.         ) {
  615.             foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
  616.                 if (=== count(array_diff($key->getLocalColumns(), $localColumns))
  617.                     && (($key->getForeignTableName() != $foreignTableName)
  618.                     || count(array_diff($key->getForeignColumns(), $foreignColumns)))
  619.                 ) {
  620.                     $theJoinTable->removeForeignKey($fkName);
  621.                     break;
  622.                 }
  623.             }
  624.             $blacklistedFks[$compositeName] = true;
  625.         } elseif ( ! isset($blacklistedFks[$compositeName])) {
  626.             $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName'foreignColumns' => $foreignColumns];
  627.             $theJoinTable->addUnnamedForeignKeyConstraint(
  628.                 $foreignTableName,
  629.                 $localColumns,
  630.                 $foreignColumns,
  631.                 $fkOptions
  632.             );
  633.         }
  634.     }
  635.     /**
  636.      * @param mixed[] $mapping
  637.      *
  638.      * @return mixed[]
  639.      */
  640.     private function gatherColumnOptions(array $mapping) : array
  641.     {
  642.         if (! isset($mapping['options'])) {
  643.             return [];
  644.         }
  645.         $options array_intersect_key($mapping['options'], array_flip(self::KNOWN_COLUMN_OPTIONS));
  646.         $options['customSchemaOptions'] = array_diff_key($mapping['options'], $options);
  647.         return $options;
  648.     }
  649.     /**
  650.      * Drops the database schema for the given classes.
  651.      *
  652.      * In any way when an exception is thrown it is suppressed since drop was
  653.      * issued for all classes of the schema and some probably just don't exist.
  654.      *
  655.      * @param array $classes
  656.      *
  657.      * @return void
  658.      */
  659.     public function dropSchema(array $classes)
  660.     {
  661.         $dropSchemaSql $this->getDropSchemaSQL($classes);
  662.         $conn $this->em->getConnection();
  663.         foreach ($dropSchemaSql as $sql) {
  664.             try {
  665.                 $conn->executeQuery($sql);
  666.             } catch (\Throwable $e) {
  667.                 // ignored
  668.             }
  669.         }
  670.     }
  671.     /**
  672.      * Drops all elements in the database of the current connection.
  673.      *
  674.      * @return void
  675.      */
  676.     public function dropDatabase()
  677.     {
  678.         $dropSchemaSql $this->getDropDatabaseSQL();
  679.         $conn $this->em->getConnection();
  680.         foreach ($dropSchemaSql as $sql) {
  681.             $conn->executeQuery($sql);
  682.         }
  683.     }
  684.     /**
  685.      * Gets the SQL needed to drop the database schema for the connections database.
  686.      *
  687.      * @return array
  688.      */
  689.     public function getDropDatabaseSQL()
  690.     {
  691.         $sm $this->em->getConnection()->getSchemaManager();
  692.         $schema $sm->createSchema();
  693.         $visitor = new DropSchemaSqlCollector($this->platform);
  694.         $schema->visit($visitor);
  695.         return $visitor->getQueries();
  696.     }
  697.     /**
  698.      * Gets SQL to drop the tables defined by the passed classes.
  699.      *
  700.      * @param array $classes
  701.      *
  702.      * @return array
  703.      */
  704.     public function getDropSchemaSQL(array $classes)
  705.     {
  706.         $visitor = new DropSchemaSqlCollector($this->platform);
  707.         $schema $this->getSchemaFromMetadata($classes);
  708.         $sm $this->em->getConnection()->getSchemaManager();
  709.         $fullSchema $sm->createSchema();
  710.         foreach ($fullSchema->getTables() as $table) {
  711.             if ( ! $schema->hasTable($table->getName())) {
  712.                 foreach ($table->getForeignKeys() as $foreignKey) {
  713.                     /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
  714.                     if ($schema->hasTable($foreignKey->getForeignTableName())) {
  715.                         $visitor->acceptForeignKey($table$foreignKey);
  716.                     }
  717.                 }
  718.             } else {
  719.                 $visitor->acceptTable($table);
  720.                 foreach ($table->getForeignKeys() as $foreignKey) {
  721.                     $visitor->acceptForeignKey($table$foreignKey);
  722.                 }
  723.             }
  724.         }
  725.         if ($this->platform->supportsSequences()) {
  726.             foreach ($schema->getSequences() as $sequence) {
  727.                 $visitor->acceptSequence($sequence);
  728.             }
  729.             foreach ($schema->getTables() as $table) {
  730.                 /* @var $sequence Table */
  731.                 if ($table->hasPrimaryKey()) {
  732.                     $columns $table->getPrimaryKey()->getColumns();
  733.                     if (count($columns) == 1) {
  734.                         $checkSequence $table->getName() . '_' $columns[0] . '_seq';
  735.                         if ($fullSchema->hasSequence($checkSequence)) {
  736.                             $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
  737.                         }
  738.                     }
  739.                 }
  740.             }
  741.         }
  742.         return $visitor->getQueries();
  743.     }
  744.     /**
  745.      * Updates the database schema of the given classes by comparing the ClassMetadata
  746.      * instances to the current database schema that is inspected.
  747.      *
  748.      * @param array   $classes
  749.      * @param boolean $saveMode If TRUE, only performs a partial update
  750.      *                          without dropping assets which are scheduled for deletion.
  751.      *
  752.      * @return void
  753.      */
  754.     public function updateSchema(array $classes$saveMode false)
  755.     {
  756.         $updateSchemaSql $this->getUpdateSchemaSql($classes$saveMode);
  757.         $conn $this->em->getConnection();
  758.         foreach ($updateSchemaSql as $sql) {
  759.             $conn->executeQuery($sql);
  760.         }
  761.     }
  762.     /**
  763.      * Gets the sequence of SQL statements that need to be performed in order
  764.      * to bring the given class mappings in-synch with the relational schema.
  765.      *
  766.      * @param array   $classes  The classes to consider.
  767.      * @param boolean $saveMode If TRUE, only generates SQL for a partial update
  768.      *                          that does not include SQL for dropping assets which are scheduled for deletion.
  769.      *
  770.      * @return array The sequence of SQL statements.
  771.      */
  772.     public function getUpdateSchemaSql(array $classes$saveMode false)
  773.     {
  774.         $toSchema $this->getSchemaFromMetadata($classes);
  775.         $fromSchema $this->createSchemaForComparison($toSchema);
  776.         $comparator = new Comparator();
  777.         $schemaDiff $comparator->compare($fromSchema$toSchema);
  778.         if ($saveMode) {
  779.             return $schemaDiff->toSaveSql($this->platform);
  780.         }
  781.         return $schemaDiff->toSql($this->platform);
  782.     }
  783.     /**
  784.      * Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison.
  785.      */
  786.     private function createSchemaForComparison(Schema $toSchema) : Schema
  787.     {
  788.         $connection    $this->em->getConnection();
  789.         $schemaManager $connection->getSchemaManager();
  790.         // backup schema assets filter
  791.         $config         $connection->getConfiguration();
  792.         $previousFilter $config->getSchemaAssetsFilter();
  793.         if ($previousFilter === null) {
  794.             return $schemaManager->createSchema();
  795.         }
  796.         // whitelist assets we already know about in $toSchema, use the existing filter otherwise
  797.         $config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter$toSchema) : bool {
  798.             $assetName $asset instanceof AbstractAsset $asset->getName() : $asset;
  799.             return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset);
  800.         });
  801.         try {
  802.             return $schemaManager->createSchema();
  803.         } finally {
  804.             // restore schema assets filter
  805.             $config->setSchemaAssetsFilter($previousFilter);
  806.         }
  807.     }
  808. }