vendor/doctrine/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php
changeset 0 7f95f8617b0b
equal deleted inserted replaced
-1:000000000000 0:7f95f8617b0b
       
     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 LGPL. For more information, see
       
    17  * <http://www.doctrine-project.org>.
       
    18  */
       
    19 
       
    20 namespace Doctrine\ORM\Persisters;
       
    21 
       
    22 use Doctrine\ORM\ORMException,
       
    23     Doctrine\ORM\Mapping\ClassMetadata,
       
    24     Doctrine\DBAL\LockMode,
       
    25     Doctrine\ORM\Query\ResultSetMapping;
       
    26 
       
    27 /**
       
    28  * The joined subclass persister maps a single entity instance to several tables in the
       
    29  * database as it is defined by the <tt>Class Table Inheritance</tt> strategy.
       
    30  *
       
    31  * @author Roman Borschel <roman@code-factory.org>
       
    32  * @author Benjamin Eberlei <kontakt@beberlei.de>
       
    33  * @since 2.0
       
    34  * @see http://martinfowler.com/eaaCatalog/classTableInheritance.html
       
    35  */
       
    36 class JoinedSubclassPersister extends AbstractEntityInheritancePersister
       
    37 {
       
    38     /**
       
    39      * Map that maps column names to the table names that own them.
       
    40      * This is mainly a temporary cache, used during a single request.
       
    41      *
       
    42      * @var array
       
    43      */
       
    44     private $_owningTableMap = array();
       
    45 
       
    46     /**
       
    47      * Map of table to quoted table names.
       
    48      * 
       
    49      * @var array
       
    50      */
       
    51     private $_quotedTableMap = array();
       
    52 
       
    53     /**
       
    54      * {@inheritdoc}
       
    55      */
       
    56     protected function _getDiscriminatorColumnTableName()
       
    57     {
       
    58         if ($this->_class->name == $this->_class->rootEntityName) {
       
    59             return $this->_class->table['name'];
       
    60         } else {
       
    61             return $this->_em->getClassMetadata($this->_class->rootEntityName)->table['name'];
       
    62         }
       
    63     }
       
    64 
       
    65     /**
       
    66      * This function finds the ClassMetadata instance in an inheritance hierarchy
       
    67      * that is responsible for enabling versioning.
       
    68      *
       
    69      * @return Doctrine\ORM\Mapping\ClassMetadata
       
    70      */
       
    71     private function _getVersionedClassMetadata()
       
    72     {
       
    73         if (isset($this->_class->fieldMappings[$this->_class->versionField]['inherited'])) {
       
    74             $definingClassName = $this->_class->fieldMappings[$this->_class->versionField]['inherited'];
       
    75             return $this->_em->getClassMetadata($definingClassName);
       
    76         }
       
    77         return $this->_class;
       
    78     }
       
    79 
       
    80     /**
       
    81      * Gets the name of the table that owns the column the given field is mapped to.
       
    82      *
       
    83      * @param string $fieldName
       
    84      * @return string
       
    85      * @override
       
    86      */
       
    87     public function getOwningTable($fieldName)
       
    88     {
       
    89         if (!isset($this->_owningTableMap[$fieldName])) {
       
    90             if (isset($this->_class->associationMappings[$fieldName]['inherited'])) {
       
    91                 $cm = $this->_em->getClassMetadata($this->_class->associationMappings[$fieldName]['inherited']);
       
    92             } else if (isset($this->_class->fieldMappings[$fieldName]['inherited'])) {
       
    93                 $cm = $this->_em->getClassMetadata($this->_class->fieldMappings[$fieldName]['inherited']);
       
    94             } else {
       
    95                 $cm = $this->_class;
       
    96             }
       
    97             $this->_owningTableMap[$fieldName] = $cm->table['name'];
       
    98             $this->_quotedTableMap[$cm->table['name']] = $cm->getQuotedTableName($this->_platform);
       
    99         }
       
   100 
       
   101         return $this->_owningTableMap[$fieldName];
       
   102     }
       
   103 
       
   104     /**
       
   105      * {@inheritdoc}
       
   106      */
       
   107     public function executeInserts()
       
   108     {
       
   109         if ( ! $this->_queuedInserts) {
       
   110             return;
       
   111         }
       
   112 
       
   113         $postInsertIds = array();
       
   114         $idGen = $this->_class->idGenerator;
       
   115         $isPostInsertId = $idGen->isPostInsertGenerator();
       
   116 
       
   117         // Prepare statement for the root table
       
   118         $rootClass = $this->_class->name == $this->_class->rootEntityName ?
       
   119                 $this->_class : $this->_em->getClassMetadata($this->_class->rootEntityName);
       
   120         $rootPersister = $this->_em->getUnitOfWork()->getEntityPersister($rootClass->name);
       
   121         $rootTableName = $rootClass->table['name'];
       
   122         $rootTableStmt = $this->_conn->prepare($rootPersister->_getInsertSQL());
       
   123 
       
   124         // Prepare statements for sub tables.
       
   125         $subTableStmts = array();
       
   126         if ($rootClass !== $this->_class) {
       
   127             $subTableStmts[$this->_class->table['name']] = $this->_conn->prepare($this->_getInsertSQL());
       
   128         }
       
   129         foreach ($this->_class->parentClasses as $parentClassName) {
       
   130             $parentClass = $this->_em->getClassMetadata($parentClassName);
       
   131             $parentTableName = $parentClass->table['name'];
       
   132             if ($parentClass !== $rootClass) {
       
   133                 $parentPersister = $this->_em->getUnitOfWork()->getEntityPersister($parentClassName);
       
   134                 $subTableStmts[$parentTableName] = $this->_conn->prepare($parentPersister->_getInsertSQL());
       
   135             }
       
   136         }
       
   137 
       
   138         // Execute all inserts. For each entity:
       
   139         // 1) Insert on root table
       
   140         // 2) Insert on sub tables
       
   141         foreach ($this->_queuedInserts as $entity) {
       
   142             $insertData = $this->_prepareInsertData($entity);
       
   143 
       
   144             // Execute insert on root table
       
   145             $paramIndex = 1;
       
   146             foreach ($insertData[$rootTableName] as $columnName => $value) {
       
   147                 $rootTableStmt->bindValue($paramIndex++, $value, $this->_columnTypes[$columnName]);
       
   148             }
       
   149             $rootTableStmt->execute();
       
   150 
       
   151             if ($isPostInsertId) {
       
   152                 $id = $idGen->generate($this->_em, $entity);
       
   153                 $postInsertIds[$id] = $entity;
       
   154             } else {
       
   155                 $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
       
   156             }
       
   157 
       
   158             // Execute inserts on subtables.
       
   159             // The order doesn't matter because all child tables link to the root table via FK.
       
   160             foreach ($subTableStmts as $tableName => $stmt) {
       
   161                 $data = isset($insertData[$tableName]) ? $insertData[$tableName] : array();
       
   162                 $paramIndex = 1;
       
   163                 foreach ((array) $id as $idVal) {
       
   164                     $stmt->bindValue($paramIndex++, $idVal);
       
   165                 }
       
   166                 foreach ($data as $columnName => $value) {
       
   167                     $stmt->bindValue($paramIndex++, $value, $this->_columnTypes[$columnName]);
       
   168                 }
       
   169                 $stmt->execute();
       
   170             }
       
   171         }
       
   172 
       
   173         $rootTableStmt->closeCursor();
       
   174         foreach ($subTableStmts as $stmt) {
       
   175             $stmt->closeCursor();
       
   176         }
       
   177 
       
   178         if ($this->_class->isVersioned) {
       
   179             $this->assignDefaultVersionValue($entity, $id);
       
   180         }
       
   181 
       
   182         $this->_queuedInserts = array();
       
   183 
       
   184         return $postInsertIds;
       
   185     }
       
   186 
       
   187     /**
       
   188      * {@inheritdoc}
       
   189      */
       
   190     public function update($entity)
       
   191     {
       
   192         $updateData = $this->_prepareUpdateData($entity);
       
   193 
       
   194         if ($isVersioned = $this->_class->isVersioned) {
       
   195             $versionedClass = $this->_getVersionedClassMetadata();
       
   196             $versionedTable = $versionedClass->table['name'];
       
   197         }
       
   198 
       
   199         if ($updateData) {
       
   200             foreach ($updateData as $tableName => $data) {
       
   201                 $this->_updateTable($entity, $this->_quotedTableMap[$tableName], $data, $isVersioned && $versionedTable == $tableName);
       
   202             }
       
   203             // Make sure the table with the version column is updated even if no columns on that
       
   204             // table were affected.
       
   205             if ($isVersioned && ! isset($updateData[$versionedTable])) {
       
   206                 $this->_updateTable($entity, $versionedClass->getQuotedTableName($this->_platform), array(), true);
       
   207 
       
   208                 $id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
       
   209                 $this->assignDefaultVersionValue($entity, $id);
       
   210             }
       
   211         }
       
   212     }
       
   213 
       
   214     /**
       
   215      * {@inheritdoc}
       
   216      */
       
   217     public function delete($entity)
       
   218     {
       
   219         $identifier = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
       
   220         $this->deleteJoinTableRecords($identifier);
       
   221 
       
   222         $id = array_combine($this->_class->getIdentifierColumnNames(), $identifier);
       
   223 
       
   224         // If the database platform supports FKs, just
       
   225         // delete the row from the root table. Cascades do the rest.
       
   226         if ($this->_platform->supportsForeignKeyConstraints()) {
       
   227             $this->_conn->delete($this->_em->getClassMetadata($this->_class->rootEntityName)
       
   228                     ->getQuotedTableName($this->_platform), $id);
       
   229         } else {
       
   230             // Delete from all tables individually, starting from this class' table up to the root table.
       
   231             $this->_conn->delete($this->_class->getQuotedTableName($this->_platform), $id);
       
   232             foreach ($this->_class->parentClasses as $parentClass) {
       
   233                 $this->_conn->delete($this->_em->getClassMetadata($parentClass)->getQuotedTableName($this->_platform), $id);
       
   234             }
       
   235         }
       
   236     }
       
   237 
       
   238     /**
       
   239      * {@inheritdoc}
       
   240      */
       
   241     protected function _getSelectEntitiesSQL(array $criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null)
       
   242     {
       
   243         $idColumns = $this->_class->getIdentifierColumnNames();
       
   244         $baseTableAlias = $this->_getSQLTableAlias($this->_class->name);
       
   245 
       
   246         // Create the column list fragment only once
       
   247         if ($this->_selectColumnListSql === null) {
       
   248             
       
   249             $this->_rsm = new ResultSetMapping();
       
   250             $this->_rsm->addEntityResult($this->_class->name, 'r');
       
   251             
       
   252             // Add regular columns
       
   253             $columnList = '';
       
   254             foreach ($this->_class->fieldMappings as $fieldName => $mapping) {
       
   255                 if ($columnList != '') $columnList .= ', ';
       
   256                 $columnList .= $this->_getSelectColumnSQL($fieldName,
       
   257                         isset($mapping['inherited']) ?
       
   258                         $this->_em->getClassMetadata($mapping['inherited']) :
       
   259                         $this->_class);
       
   260             }
       
   261 
       
   262             // Add foreign key columns
       
   263             foreach ($this->_class->associationMappings as $assoc2) {
       
   264                 if ($assoc2['isOwningSide'] && $assoc2['type'] & ClassMetadata::TO_ONE) {
       
   265                     $tableAlias = isset($assoc2['inherited']) ?
       
   266                             $this->_getSQLTableAlias($assoc2['inherited'])
       
   267                             : $baseTableAlias;
       
   268                     foreach ($assoc2['targetToSourceKeyColumns'] as $srcColumn) {
       
   269                         if ($columnList != '') $columnList .= ', ';
       
   270                         $columnList .= $this->getSelectJoinColumnSQL($tableAlias, $srcColumn,
       
   271                             isset($assoc2['inherited']) ? $assoc2['inherited'] : $this->_class->name
       
   272                         );
       
   273                     }
       
   274                 }
       
   275             }
       
   276 
       
   277             // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#_processSQLResult).
       
   278             $discrColumn = $this->_class->discriminatorColumn['name'];
       
   279             if ($this->_class->rootEntityName == $this->_class->name) {
       
   280                 $columnList .= ", $baseTableAlias.$discrColumn";
       
   281             } else {
       
   282                 $columnList .= ', ' . $this->_getSQLTableAlias($this->_class->rootEntityName)
       
   283                         . ".$discrColumn";
       
   284             }
       
   285 
       
   286             $resultColumnName = $this->_platform->getSQLResultCasing($discrColumn);
       
   287             $this->_rsm->setDiscriminatorColumn('r', $resultColumnName);
       
   288             $this->_rsm->addMetaResult('r', $resultColumnName, $discrColumn);
       
   289         }
       
   290 
       
   291         // INNER JOIN parent tables
       
   292         $joinSql = '';
       
   293         foreach ($this->_class->parentClasses as $parentClassName) {
       
   294             $parentClass = $this->_em->getClassMetadata($parentClassName);
       
   295             $tableAlias = $this->_getSQLTableAlias($parentClassName);
       
   296             $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON ';
       
   297             $first = true;
       
   298             foreach ($idColumns as $idColumn) {
       
   299                 if ($first) $first = false; else $joinSql .= ' AND ';
       
   300                 $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
       
   301             }
       
   302         }
       
   303 
       
   304         // OUTER JOIN sub tables
       
   305         foreach ($this->_class->subClasses as $subClassName) {
       
   306             $subClass = $this->_em->getClassMetadata($subClassName);
       
   307             $tableAlias = $this->_getSQLTableAlias($subClassName);
       
   308 
       
   309             if ($this->_selectColumnListSql === null) {
       
   310                 // Add subclass columns
       
   311                 foreach ($subClass->fieldMappings as $fieldName => $mapping) {
       
   312                     if (isset($mapping['inherited'])) {
       
   313                         continue;
       
   314                     }
       
   315                     $columnList .= ', ' . $this->_getSelectColumnSQL($fieldName, $subClass);
       
   316                 }
       
   317 
       
   318                 // Add join columns (foreign keys)
       
   319                 foreach ($subClass->associationMappings as $assoc2) {
       
   320                     if ($assoc2['isOwningSide'] && $assoc2['type'] & ClassMetadata::TO_ONE
       
   321                             && ! isset($assoc2['inherited'])) {
       
   322                         foreach ($assoc2['targetToSourceKeyColumns'] as $srcColumn) {
       
   323                             if ($columnList != '') $columnList .= ', ';
       
   324                             $columnList .= $this->getSelectJoinColumnSQL($tableAlias, $srcColumn,
       
   325                                 isset($assoc2['inherited']) ? $assoc2['inherited'] : $subClass->name
       
   326                             );
       
   327                         }
       
   328                     }
       
   329                 }
       
   330             }
       
   331 
       
   332             // Add LEFT JOIN
       
   333             $joinSql .= ' LEFT JOIN ' . $subClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON ';
       
   334             $first = true;
       
   335             foreach ($idColumns as $idColumn) {
       
   336                 if ($first) $first = false; else $joinSql .= ' AND ';
       
   337                 $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
       
   338             }
       
   339         }
       
   340 
       
   341         $joinSql .= $assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY ?
       
   342                 $this->_getSelectManyToManyJoinSQL($assoc) : '';
       
   343 
       
   344         $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc);
       
   345 
       
   346         $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy;
       
   347         $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $baseTableAlias) : '';
       
   348 
       
   349         if ($this->_selectColumnListSql === null) {
       
   350             $this->_selectColumnListSql = $columnList;
       
   351         }
       
   352 
       
   353         $lockSql = '';
       
   354         if ($lockMode == LockMode::PESSIMISTIC_READ) {
       
   355             $lockSql = ' ' . $this->_platform->getReadLockSql();
       
   356         } else if ($lockMode == LockMode::PESSIMISTIC_WRITE) {
       
   357             $lockSql = ' ' . $this->_platform->getWriteLockSql();
       
   358         }
       
   359 
       
   360         return $this->_platform->modifyLimitQuery('SELECT ' . $this->_selectColumnListSql
       
   361                 . ' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias
       
   362                 . $joinSql
       
   363                 . ($conditionSql != '' ? ' WHERE ' . $conditionSql : '') . $orderBySql, $limit, $offset)
       
   364                 . $lockSql;
       
   365     }
       
   366 
       
   367     /**
       
   368      * Get the FROM and optionally JOIN conditions to lock the entity managed by this persister.
       
   369      *
       
   370      * @return string
       
   371      */
       
   372     public function getLockTablesSql()
       
   373     {
       
   374         $idColumns = $this->_class->getIdentifierColumnNames();
       
   375         $baseTableAlias = $this->_getSQLTableAlias($this->_class->name);
       
   376 
       
   377         // INNER JOIN parent tables
       
   378         $joinSql = '';
       
   379         foreach ($this->_class->parentClasses as $parentClassName) {
       
   380             $parentClass = $this->_em->getClassMetadata($parentClassName);
       
   381             $tableAlias = $this->_getSQLTableAlias($parentClassName);
       
   382             $joinSql .= ' INNER JOIN ' . $parentClass->getQuotedTableName($this->_platform) . ' ' . $tableAlias . ' ON ';
       
   383             $first = true;
       
   384             foreach ($idColumns as $idColumn) {
       
   385                 if ($first) $first = false; else $joinSql .= ' AND ';
       
   386                 $joinSql .= $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
       
   387             }
       
   388         }
       
   389 
       
   390         return 'FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' . $baseTableAlias . $joinSql;
       
   391     }
       
   392     
       
   393     /* Ensure this method is never called. This persister overrides _getSelectEntitiesSQL directly. */
       
   394     protected function _getSelectColumnListSQL()
       
   395     {
       
   396         throw new \BadMethodCallException("Illegal invocation of ".__METHOD__.".");
       
   397     }
       
   398     
       
   399     /** {@inheritdoc} */
       
   400     protected function _getInsertColumnList()
       
   401     {
       
   402         // Identifier columns must always come first in the column list of subclasses.
       
   403         $columns = $this->_class->parentClasses ? $this->_class->getIdentifierColumnNames() : array();
       
   404 
       
   405         foreach ($this->_class->reflFields as $name => $field) {
       
   406             if (isset($this->_class->fieldMappings[$name]['inherited']) && ! isset($this->_class->fieldMappings[$name]['id'])
       
   407                     || isset($this->_class->associationMappings[$name]['inherited'])
       
   408                     || ($this->_class->isVersioned && $this->_class->versionField == $name)) {
       
   409                 continue;
       
   410             }
       
   411 
       
   412             if (isset($this->_class->associationMappings[$name])) {
       
   413                 $assoc = $this->_class->associationMappings[$name];
       
   414                 if ($assoc['type'] & ClassMetadata::TO_ONE && $assoc['isOwningSide']) {
       
   415                     foreach ($assoc['targetToSourceKeyColumns'] as $sourceCol) {
       
   416                         $columns[] = $sourceCol;
       
   417                     }
       
   418                 }
       
   419             } else if ($this->_class->name != $this->_class->rootEntityName ||
       
   420                     ! $this->_class->isIdGeneratorIdentity() || $this->_class->identifier[0] != $name) {
       
   421                 $columns[] = $this->_class->getQuotedColumnName($name, $this->_platform);
       
   422             }
       
   423         }
       
   424 
       
   425         // Add discriminator column if it is the topmost class.
       
   426         if ($this->_class->name == $this->_class->rootEntityName) {
       
   427             $columns[] = $this->_class->discriminatorColumn['name'];
       
   428         }
       
   429 
       
   430         return $columns;
       
   431     }
       
   432 
       
   433     /**
       
   434      * {@inheritdoc}
       
   435      */
       
   436     protected function assignDefaultVersionValue($entity, $id)
       
   437     {
       
   438         $value = $this->fetchVersionValue($this->_getVersionedClassMetadata(), $id);
       
   439         $this->_class->setFieldValue($entity, $this->_class->versionField, $value);
       
   440     }
       
   441 }