Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Add ODM embedded-like functionality #419

Closed
wants to merge 25 commits into from

2 participants

Derek J. Lambert Don't Add Me To Your Organization a.k.a The Travis Bot
Derek J. Lambert

This PR adds ODM embedded-like functionality to the ORM.

Including the new @MappedAssociation annotation on a field having a one-to-one association adds a discriminator column to the table for storing the class name of a "mapped" entity.

This allows a class or mapped superclass with a one-to-one identifying association to be extended by additional entities without requiring any code changes (as is required with the discriminator map when using inheritance).

I apologize if this is the incorrect way to submit a feature request. Currently just the annotation driver has been updated, I wanted to get feedback before continuing with the remaining drivers. Models and tests are included.

added some commits August 07, 2012
Derek J. Lambert New MappedAssociation annotation. 9d487c7
Derek J. Lambert Annotation driver support for MappedAssocation annotation. 0e55b8a
Derek J. Lambert Added array to hold mapped associations. 8be7fd9
Derek J. Lambert Include mapped associations in sleep and reflection wakeup. 34925e9
Derek J. Lambert Added method addMappedAssociation to validate and add mapped associat…
…ion to class.
b5032bd
Derek J. Lambert Added mapped association exceptions. ca160d9
Derek J. Lambert Added utility functions to check if class has mapped associations and…
… getter.
b165068
Derek J. Lambert Added method addMappedAssociationDiscriminatorColumnDefinitions to ad…
…d mapped association descriminator columns to the table.
457c439
Derek J. Lambert Add mapped association discriminator columns to the schema. f15895c
Derek J. Lambert Include mapped association in change detection. be0c83b
Derek J. Lambert Skip association if mapped association discriminator column is empty. 27ec173
Derek J. Lambert Populate mapped association entity. 3a4e012
Derek J. Lambert Added method getMappedAssociationDiscriminatorColumnName to get mappe…
…d association discriminator column name.
dd315f4
Derek J. Lambert Null 'field' value for mapped association discriminator columns to sk…
…ip PHP value conversion in hydrateRowData.
b4d24e7
Derek J. Lambert Include the mapped association discriminator column value in the upda…
…te data.
19c2951
Derek J. Lambert Added method _getSelectMappedAssociationDiscriminatorColumnSQL to get…
… mapped association discriminator column SELECT SQL.
d042db4
Derek J. Lambert Add mapped association descriminator columns to select list. If mappe…
…d association is a mapped superclass add to array to skip association.
02c4f08
Derek J. Lambert Move getClassMetaData call earlier and refactor variable name. 3e4a601
Derek J. Lambert Check if association is in the skip array. 72112fa
Derek J. Lambert If mapped association don't INSERT non-existent column. e314497
Derek J. Lambert Add mapped association discriminator columns to the INSERT list. a18a07d
Derek J. Lambert Models for mapped association testing. 66bf0c3
Derek J. Lambert Add new models to model sets and teardown. 96971b9
Derek J. Lambert Tests for mapped associations. 9602bbf
Derek J. Lambert Remove extra whitespace and add newline at EOF. e7fa8a3
Don't Add Me To Your Organization a.k.a The Travis Bot

This pull request passes (merged e7fa8a3 into 72ce9a7).

Derek J. Lambert

With support for dynamically generating the discriminator map added in #221 I'm able to accomplish similar results using class table inheritance.

Derek J. Lambert djlambert closed this August 30, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 25 unique commits by 1 author.

Aug 07, 2012
Derek J. Lambert New MappedAssociation annotation. 9d487c7
Derek J. Lambert Annotation driver support for MappedAssocation annotation. 0e55b8a
Derek J. Lambert Added array to hold mapped associations. 8be7fd9
Derek J. Lambert Include mapped associations in sleep and reflection wakeup. 34925e9
Derek J. Lambert Added method addMappedAssociation to validate and add mapped associat…
…ion to class.
b5032bd
Derek J. Lambert Added mapped association exceptions. ca160d9
Derek J. Lambert Added utility functions to check if class has mapped associations and…
… getter.
b165068
Derek J. Lambert Added method addMappedAssociationDiscriminatorColumnDefinitions to ad…
…d mapped association descriminator columns to the table.
457c439
Derek J. Lambert Add mapped association discriminator columns to the schema. f15895c
Derek J. Lambert Include mapped association in change detection. be0c83b
Derek J. Lambert Skip association if mapped association discriminator column is empty. 27ec173
Derek J. Lambert Populate mapped association entity. 3a4e012
Derek J. Lambert Added method getMappedAssociationDiscriminatorColumnName to get mappe…
…d association discriminator column name.
dd315f4
Derek J. Lambert Null 'field' value for mapped association discriminator columns to sk…
…ip PHP value conversion in hydrateRowData.
b4d24e7
Derek J. Lambert Include the mapped association discriminator column value in the upda…
…te data.
19c2951
Derek J. Lambert Added method _getSelectMappedAssociationDiscriminatorColumnSQL to get…
… mapped association discriminator column SELECT SQL.
d042db4
Derek J. Lambert Add mapped association descriminator columns to select list. If mappe…
…d association is a mapped superclass add to array to skip association.
02c4f08
Derek J. Lambert Move getClassMetaData call earlier and refactor variable name. 3e4a601
Derek J. Lambert Check if association is in the skip array. 72112fa
Derek J. Lambert If mapped association don't INSERT non-existent column. e314497
Derek J. Lambert Add mapped association discriminator columns to the INSERT list. a18a07d
Derek J. Lambert Models for mapped association testing. 66bf0c3
Derek J. Lambert Add new models to model sets and teardown. 96971b9
Derek J. Lambert Tests for mapped associations. 9602bbf
Derek J. Lambert Remove extra whitespace and add newline at EOF. e7fa8a3
This page is out of date. Refresh to see the latest.
4  lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php
@@ -158,10 +158,12 @@ protected function hydrateColumnInfo($entityName, $column)
158 158
                     return null;
159 159
                 }
160 160
 
  161
+                $field = isset($class->mappedAssociationMappings[$this->_rsm->fieldMappings[$column]]) ? null : true;
  162
+
161 163
                 return array(
162 164
                     'class' => $class,
163 165
                     'name'  => $this->_rsm->fieldMappings[$column],
164  
-                    'field' => true,
  166
+                    'field' => $field,
165 167
                 );
166 168
 
167 169
             case (isset($this->_rsm->relationMap[$column])):
74  lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -374,6 +374,13 @@ class ClassMetadataInfo implements ClassMetadata
374 374
     public $columnNames = array();
375 375
 
376 376
     /**
  377
+     * READ-ONLY: The mapped associations of the class.
  378
+     *
  379
+     * @var array
  380
+     */
  381
+    public $mappedAssociations = array();
  382
+
  383
+    /**
377 384
      * READ-ONLY: The discriminator value of this class.
378 385
      *
379 386
      * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
@@ -816,6 +823,10 @@ public function __sleep()
816 823
             $serialized[] = "customGeneratorDefinition";
817 824
         }
818 825
 
  826
+        if ($this->mappedAssociations) {
  827
+            $serialized[] = "mappedAssociations";
  828
+        }
  829
+
819 830
         return $serialized;
820 831
     }
821 832
 
@@ -854,6 +865,10 @@ public function wakeupReflection($reflService)
854 865
                 ? $reflService->getAccessibleProperty($mapping['declared'], $field)
855 866
                 : $reflService->getAccessibleProperty($this->name, $field);
856 867
         }
  868
+
  869
+        foreach ($this->mappedAssociations as $field => $mapping) {
  870
+            $this->reflFields[$field] = $reflService->getAccessibleProperty($this->name, $field);
  871
+        }
857 872
     }
858 873
 
859 874
     /**
@@ -2084,6 +2099,45 @@ public function addInheritedFieldMapping(array $fieldMapping)
2084 2099
     }
2085 2100
 
2086 2101
     /**
  2102
+     * Add a mapped association to the class
  2103
+     *
  2104
+     * @param array $mapping
  2105
+     *
  2106
+     * @throws MappingException
  2107
+     */
  2108
+    public function addMappedAssociation(array $mapping)
  2109
+    {
  2110
+        $fieldMapping = &$mapping['fieldMapping'];
  2111
+
  2112
+        if (!isset($fieldMapping['columnName'])) {
  2113
+            $fieldMapping['fieldName'] = $mapping['name'] . 'Class';
  2114
+            $fieldMapping['columnName'] = $this->namingStrategy->propertyToColumnName($fieldMapping['fieldName']);
  2115
+        } else {
  2116
+            $fieldMapping['fieldName'] = $fieldMapping['columnName'];
  2117
+        }
  2118
+
  2119
+        if ($fieldMapping['columnName'][0] === '`') {
  2120
+            $fieldMapping['columnName']  = trim($fieldMapping['columnName'], '`');
  2121
+            $fieldMapping['quoted']      = true;
  2122
+        }
  2123
+
  2124
+        $fieldMapping['type'] = 'string';
  2125
+
  2126
+        if (isset($this->fieldNames[$fieldMapping['columnName']])) {
  2127
+            throw MappingException::duplicateColumnName($this->name, $fieldMapping['columnName']);
  2128
+        }
  2129
+
  2130
+        if (!isset($this->associationMappings[$mapping['name']])) {
  2131
+            throw MappingException::associationRequiredForMappedAssociation($this->name, $fieldMapping['columnName']);
  2132
+        } elseif ($this->associationMappings[$mapping['name']]['type'] != self::ONE_TO_ONE || $this->associationMappings[$mapping['name']]['isOwningSide']) {
  2133
+            throw MappingException::unsupportedAssociationForMappedAssociation($this->name, $fieldMapping['columnName']);
  2134
+        }
  2135
+
  2136
+        $this->mappedAssociationMappings[$fieldMapping['columnName']] = $mapping['name'];
  2137
+        $this->mappedAssociations[$mapping['name']] = $mapping;
  2138
+    }
  2139
+
  2140
+    /**
2087 2141
      * INTERNAL:
2088 2142
      * Adds a named query to this class.
2089 2143
      *
@@ -2468,6 +2522,26 @@ public function hasSqlResultSetMapping($name)
2468 2522
     }
2469 2523
 
2470 2524
     /**
  2525
+     * Checks whether the class has any mapped associations.
  2526
+     *
  2527
+     * @return array
  2528
+     */
  2529
+    public function hasMappedAssociations()
  2530
+    {
  2531
+        return $this->mappedAssociations;
  2532
+    }
  2533
+
  2534
+    /**
  2535
+     * Return array of all mapped associations of the class.
  2536
+     *
  2537
+     * @return array
  2538
+     */
  2539
+    public function getMappedAssociations()
  2540
+    {
  2541
+        return $this->mappedAssociations;
  2542
+    }
  2543
+
  2544
+    /**
2471 2545
      * {@inheritDoc}
2472 2546
      */
2473 2547
     public function hasAssociation($fieldName)
14  lib/Doctrine/ORM/Mapping/DefaultQuoteStrategy.php
@@ -43,9 +43,19 @@ public function getColumnName($fieldName, ClassMetadata $class, AbstractPlatform
43 43
     /**
44 44
      * {@inheritdoc}
45 45
      */
  46
+    public function getMappedAssociationDiscriminatorColumnName(array $assoc, ClassMetadata $class, AbstractPlatform $platform)
  47
+    {
  48
+        return isset($assoc['fieldMapping']['quoted'])
  49
+            ? $platform->quoteIdentifier($assoc['fieldMapping']['columnName'])
  50
+            : $assoc['fieldMapping']['columnName'];
  51
+    }
  52
+
  53
+    /**
  54
+     * {@inheritdoc}
  55
+     */
46 56
     public function getTableName(ClassMetadata $class, AbstractPlatform $platform)
47 57
     {
48  
-        return isset($class->table['quoted']) 
  58
+        return isset($class->table['quoted'])
49 59
             ? $platform->quoteIdentifier($class->table['name'])
50 60
             : $class->table['name'];
51 61
     }
@@ -137,4 +147,4 @@ public function getColumnAlias($columnName, $counter, AbstractPlatform $platform
137 147
         return $platform->getSQLResultCasing($columnName);
138 148
     }
139 149
 
140  
-}
  150
+}
12  lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
@@ -400,6 +400,18 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
400 400
 
401 401
                 $metadata->mapManyToMany($mapping);
402 402
             }
  403
+
  404
+            // Evaluate MappedAssociation annotation
  405
+            $mappedAssociation = array();
  406
+            if ($mappedAssocAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\MappedAssociation')) {
  407
+                $mappedAssociation['name'] = $property->getName();
  408
+                $mappedAssociation['fieldMapping'] = array (
  409
+                    'columnName' => $mappedAssocAnnot->discriminatorColumn,
  410
+                    'length'     => $mappedAssocAnnot->length,
  411
+                    'nullable'   => $mappedAssocAnnot->nullable,
  412
+                );
  413
+                $metadata->addMappedAssociation($mappedAssociation);
  414
+            }
403 415
         }
404 416
 
405 417
         // Evaluate AssociationOverrides annotation
1  lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php
@@ -64,3 +64,4 @@
64 64
 require_once __DIR__.'/../AssociationOverrides.php';
65 65
 require_once __DIR__.'/../AttributeOverride.php';
66 66
 require_once __DIR__.'/../AttributeOverrides.php';
  67
+require_once __DIR__.'/../MappedAssociation.php';
36  lib/Doctrine/ORM/Mapping/MappedAssociation.php
... ...
@@ -0,0 +1,36 @@
  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
+
  20
+namespace Doctrine\ORM\Mapping;
  21
+
  22
+/**
  23
+ * @Annotation
  24
+ * @Target("PROPERTY")
  25
+ */
  26
+final class MappedAssociation implements Annotation
  27
+{
  28
+    /** @var string */
  29
+    public $discriminatorColumn;
  30
+
  31
+    /** @var integer */
  32
+    public $length = 255;
  33
+
  34
+    /** @var boolean */
  35
+    public $nullable = true;
  36
+}
22  lib/Doctrine/ORM/Mapping/MappingException.php
@@ -438,4 +438,26 @@ public static function invalidCascadeOption(array $cascades, $className, $proper
438 438
             $cascades
439 439
         ));
440 440
     }
  441
+
  442
+    /**
  443
+     * @param string $className
  444
+     * @param string $field
  445
+     *
  446
+     * @return self
  447
+     */
  448
+    public static function associationRequiredForMappedAssociation($className, $field)
  449
+    {
  450
+        return new self("One-to-one association required on '$className#$field' for mapped association.");
  451
+    }
  452
+
  453
+    /**
  454
+     * @param string $className
  455
+     * @param string $field
  456
+     *
  457
+     * @return self
  458
+     */
  459
+    public static function unsupportedAssociationForMappedAssociation($className, $field)
  460
+    {
  461
+        return new self("Invalid association type on '$className#$field' for mapped association. Only one-to-one associations are supported with the mapped entity reusing the primary key.");
  462
+    }
441 463
 }
13  lib/Doctrine/ORM/Mapping/QuoteStrategy.php
@@ -109,4 +109,15 @@ function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platfo
109 109
      */
110 110
     function getColumnAlias($columnName, $counter, AbstractPlatform $platform, ClassMetadata $class = null);
111 111
 
112  
-}
  112
+    /**
  113
+     * Gets the (possibly quoted) mapped association discriminator column name
  114
+     *
  115
+     * @param array            $assoc
  116
+     * @param ClassMetadata    $class
  117
+     * @param AbstractPlatform $platform
  118
+     *
  119
+     * @return string
  120
+     */
  121
+    function getMappedAssociationDiscriminatorColumnName(array $assoc, ClassMetadata $class, AbstractPlatform $platform);
  122
+
  123
+}
83  lib/Doctrine/ORM/Persisters/BasicEntityPersister.php
@@ -563,6 +563,13 @@ protected function _prepareUpdateData($entity)
563 563
 
564 564
             $newVal = $change[1];
565 565
 
  566
+            if (isset($this->_class->mappedAssociations[$field])) {
  567
+                $fieldMapping = $this->_class->mappedAssociations[$field]['fieldMapping'];
  568
+                $columnName = $fieldMapping['columnName'];
  569
+                $this->_columnTypes[$columnName] = $fieldMapping['type'];
  570
+                $result[$this->getOwningTable($field)][$columnName] = $newVal ? get_class($newVal) : $newVal;
  571
+            }
  572
+
566 573
             if (isset($this->_class->associationMappings[$field])) {
567 574
                 $assoc = $this->_class->associationMappings[$field];
568 575
 
@@ -1096,10 +1103,34 @@ protected function _getSelectColumnListSQL()
1096 1103
             $columnList .= $this->_getSelectColumnSQL($field, $this->_class);
1097 1104
         }
1098 1105
 
  1106
+        $skipMapped = array();
  1107
+
  1108
+        // Add mapped association discriminator columns to select list
  1109
+        foreach ($this->_class->mappedAssociations as $mappedAssoc => $assoc) {
  1110
+            if ($columnList) {
  1111
+                $columnList .= ', ';
  1112
+            }
  1113
+
  1114
+            $columnList .= $this->_getSelectMappedAssociationDiscriminatorColumnSQL($assoc, $this->_class);
  1115
+
  1116
+            if (isset($this->_class->associationMappings[$mappedAssoc])) {
  1117
+                $targetMetadata = $this->_em->getClassMetadata($this->_class->associationMappings[$mappedAssoc]['targetEntity']);
  1118
+                if ($targetMetadata->isMappedSuperclass) {
  1119
+                    $skipMapped[$mappedAssoc] = true;
  1120
+                }
  1121
+            }
  1122
+        }
  1123
+
1099 1124
         $this->_selectJoinSql = '';
1100 1125
         $eagerAliasCounter = 0;
1101 1126
 
1102 1127
         foreach ($this->_class->associationMappings as $assocField => $assoc) {
  1128
+            $targetEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
  1129
+
  1130
+            if (isset($skipMapped[$assocField])) {
  1131
+                continue;
  1132
+            }
  1133
+
1103 1134
             $assocColumnSQL = $this->_getSelectColumnAssociationSQL($assocField, $assoc, $this->_class);
1104 1135
 
1105 1136
             if ($assocColumnSQL) {
@@ -1109,23 +1140,21 @@ protected function _getSelectColumnListSQL()
1109 1140
             }
1110 1141
 
1111 1142
             if ($assoc['type'] & ClassMetadata::TO_ONE && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || !$assoc['isOwningSide'])) {
1112  
-                $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
1113  
-
1114  
-                if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) {
  1143
+                if ($targetEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) {
1115 1144
                     continue; // now this is why you shouldn't use inheritance
1116 1145
                 }
1117 1146
 
1118 1147
                 $assocAlias = 'e' . ($eagerAliasCounter++);
1119 1148
                 $this->_rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
1120 1149
 
1121  
-                foreach ($eagerEntity->fieldNames as $field) {
  1150
+                foreach ($targetEntity->fieldNames as $field) {
1122 1151
                     if ($columnList) $columnList .= ', ';
1123 1152
 
1124  
-                    $columnList .= $this->_getSelectColumnSQL($field, $eagerEntity, $assocAlias);
  1153
+                    $columnList .= $this->_getSelectColumnSQL($field, $targetEntity, $assocAlias);
1125 1154
                 }
1126 1155
 
1127  
-                foreach ($eagerEntity->associationMappings as $assoc2Field => $assoc2) {
1128  
-                    $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $eagerEntity, $assocAlias);
  1156
+                foreach ($targetEntity->associationMappings as $assoc2Field => $assoc2) {
  1157
+                    $assoc2ColumnSQL = $this->_getSelectColumnAssociationSQL($assoc2Field, $assoc2, $targetEntity, $assocAlias);
1129 1158
 
1130 1159
                     if ($assoc2ColumnSQL) {
1131 1160
                         if ($columnList) $columnList .= ', ';
@@ -1136,7 +1165,7 @@ protected function _getSelectColumnListSQL()
1136 1165
 
1137 1166
                 if ($assoc['isOwningSide']) {
1138 1167
                     $this->_selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($assoc['joinColumns']);
1139  
-                    $this->_selectJoinSql .= ' ' . $this->quoteStrategy->getTableName($eagerEntity, $this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON ';
  1168
+                    $this->_selectJoinSql .= ' ' . $this->quoteStrategy->getTableName($targetEntity, $this->_platform) . ' ' . $this->_getSQLTableAlias($targetEntity->name, $assocAlias) .' ON ';
1140 1169
 
1141 1170
                     $tableAlias = $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias);
1142 1171
                     foreach ($assoc['joinColumns'] as $joinColumn) {
@@ -1152,16 +1181,16 @@ protected function _getSelectColumnListSQL()
1152 1181
                     }
1153 1182
 
1154 1183
                     // Add filter SQL
1155  
-                    if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) {
  1184
+                    if ($filterSql = $this->generateFilterConditionSQL($targetEntity, $tableAlias)) {
1156 1185
                         $this->_selectJoinSql .= ' AND ' . $filterSql;
1157 1186
                     }
1158 1187
                 } else {
1159  
-                    $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
1160  
-                    $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
  1188
+                    $targetEntity = $this->_em->getClassMetadata($assoc['targetEntity']);
  1189
+                    $owningAssoc = $targetEntity->getAssociationMapping($assoc['mappedBy']);
1161 1190
 
1162 1191
                     $this->_selectJoinSql .= ' LEFT JOIN';
1163  
-                    $this->_selectJoinSql .= ' ' . $this->quoteStrategy->getTableName($eagerEntity, $this->_platform) . ' '
1164  
-                                           . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) . ' ON ';
  1192
+                    $this->_selectJoinSql .= ' ' . $this->quoteStrategy->getTableName($targetEntity, $this->_platform) . ' '
  1193
+                                           . $this->_getSQLTableAlias($targetEntity->name, $assocAlias) . ' ON ';
1165 1194
 
1166 1195
                     foreach ($owningAssoc['sourceToTargetKeyColumns'] as $sourceCol => $targetCol) {
1167 1196
                         if ( ! $first) {
@@ -1303,6 +1332,11 @@ protected function _getInsertColumnList()
1303 1332
                 continue;
1304 1333
             }
1305 1334
 
  1335
+            if (isset($this->_class->mappedAssociations[$name])) {
  1336
+                $columns[] = $columnName = $this->quoteStrategy->getMappedAssociationDiscriminatorColumnName($this->_class->mappedAssociations[$name], $this->_class, $this->_platform);
  1337
+                $this->_columnTypes[$columnName] = $this->_class->mappedAssociations[$name]['fieldMapping']['type'];
  1338
+            }
  1339
+
1306 1340
             if (isset($this->_class->associationMappings[$name])) {
1307 1341
                 $assoc = $this->_class->associationMappings[$name];
1308 1342
                 if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
@@ -1310,7 +1344,7 @@ protected function _getInsertColumnList()
1310 1344
                         $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform);
1311 1345
                     }
1312 1346
                 }
1313  
-            } else if ($this->_class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $this->_class->identifier[0] != $name) {
  1347
+            } else if (($this->_class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $this->_class->identifier[0] != $name) && !isset($this->_class->mappedAssociations[$name])) {
1314 1348
                 $columns[] = $this->quoteStrategy->getColumnName($name, $this->_class, $this->_platform);
1315 1349
                 $this->_columnTypes[$name] = $this->_class->fieldMappings[$name]['type'];
1316 1350
             }
@@ -1344,6 +1378,27 @@ protected function _getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r
1344 1378
     }
1345 1379
 
1346 1380
     /**
  1381
+     * Gets the SQL snippet of a qualified mapped association discriminator column name for the given
  1382
+     * mapped association field name.
  1383
+     *
  1384
+     * @param array         $assoc The mapped association.
  1385
+     * @param ClassMetadata $class The class that declares this field. The table this class is mapped to must own the column for the given field.
  1386
+     * @param string        $alias Table alias
  1387
+     *
  1388
+     * @return string
  1389
+     */
  1390
+    protected function _getSelectMappedAssociationDiscriminatorColumnSQL(array $assoc, ClassMetadata $class, $alias = 'r')
  1391
+    {
  1392
+        $sql = $this->_getSQLTableAlias($class->name, $alias == 'r' ? '' : $alias)
  1393
+            . '.' . $this->quoteStrategy->getMappedAssociationDiscriminatorColumnName($assoc, $class, $this->_platform);
  1394
+        $columnAlias = $this->getSQLColumnAlias($assoc['fieldMapping']['columnName']);
  1395
+
  1396
+        $this->_rsm->addFieldResult($alias, $columnAlias, $assoc['fieldMapping']['columnName']);
  1397
+
  1398
+        return $sql . ' AS ' . $columnAlias;
  1399
+    }
  1400
+
  1401
+    /**
1347 1402
      * Gets the SQL table alias for the given class name.
1348 1403
      *
1349 1404
      * @param string $className
25  lib/Doctrine/ORM/Tools/SchemaTool.php
@@ -33,7 +33,7 @@
33 33
  * The SchemaTool is a tool to create/drop/update database schemas based on
34 34
  * <tt>ClassMetadata</tt> class descriptors.
35 35
  *
36  
- * 
  36
+ *
37 37
  * @link    www.doctrine-project.org
38 38
  * @since   2.0
39 39
  * @author  Guilherme Blanco <guilhermeblanco@hotmail.com>
@@ -213,6 +213,10 @@ public function getSchemaFromMetadata(array $classes)
213 213
                 $this->_gatherRelationsSql($class, $table, $schema);
214 214
             }
215 215
 
  216
+            if ($class->hasMappedAssociations()) {
  217
+                $this->addMappedAssociationDiscriminatorColumnDefinitions($class, $table);
  218
+            }
  219
+
216 220
             $pkColumns = array();
217 221
             foreach ($class->identifier as $identifierField) {
218 222
                 if (isset($class->fieldMappings[$identifierField])) {
@@ -308,6 +312,25 @@ private function addDiscriminatorColumnDefinition($class, $table)
308 312
     }
309 313
 
310 314
     /**
  315
+     * Get a portable column definition as required by the DBAL for the mapped association
  316
+     * discriminator columns of a class.
  317
+     *
  318
+     * @param ClassMetadata $class
  319
+     * @param Table         $table
  320
+     */
  321
+    private function addMappedAssociationDiscriminatorColumnDefinitions($class, $table)
  322
+    {
  323
+        foreach ($class->getMappedAssociations() as $mappedAssociation) {
  324
+            $fieldMapping = $mappedAssociation['fieldMapping'];
  325
+            $options = array(
  326
+                'length' => $fieldMapping['length'],
  327
+                'notnull' => !$fieldMapping['nullable'],
  328
+            );
  329
+            $table->addColumn($fieldMapping['columnName'], $fieldMapping['type'], $options);
  330
+        }
  331
+    }
  332
+
  333
+    /**
311 334
      * Gathers the column definitions as required by the DBAL of all field mappings
312 335
      * found in the given class.
313 336
      *
15  lib/Doctrine/ORM/UnitOfWork.php
@@ -539,7 +539,7 @@ public function computeChangeSet(ClassMetadata $class, $entity)
539 539
             $changeSet = array();
540 540
 
541 541
             foreach ($actualData as $propName => $actualValue) {
542  
-                if ( ! isset($class->associationMappings[$propName])) {
  542
+                if ( ! isset($class->associationMappings[$propName]) || isset($class->mappedAssociations[$propName])) {
543 543
                     $changeSet[$propName] = array(null, $actualValue);
544 544
 
545 545
                     continue;
@@ -575,8 +575,8 @@ public function computeChangeSet(ClassMetadata $class, $entity)
575 575
                     continue;
576 576
                 }
577 577
 
578  
-                // if regular field
579  
-                if ( ! isset($class->associationMappings[$propName])) {
  578
+                // if regular field or mapped association
  579
+                if ( ! isset($class->associationMappings[$propName]) || isset($class->mappedAssociations[$propName])) {
580 580
                     if ($isChangeTrackingNotify) {
581 581
                         continue;
582 582
                     }
@@ -2407,6 +2407,10 @@ public function createEntity($className, array $data, &$hints = array())
2407 2407
                 continue;
2408 2408
             }
2409 2409
 
  2410
+            if (isset($class->mappedAssociations[$field]) && ($assoc['targetEntity'] = $data[$class->mappedAssociations[$field]['fieldMapping']['columnName']]) == null) {
  2411
+                continue;
  2412
+            }
  2413
+
2410 2414
             $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
2411 2415
 
2412 2416
             switch (true) {
@@ -2478,6 +2482,11 @@ public function createEntity($className, array $data, &$hints = array())
2478 2482
 
2479 2483
                         default:
2480 2484
                             switch (true) {
  2485
+                                // Populate mapped associations
  2486
+                                case (isset($class->mappedAssociations[$field])):
  2487
+                                    $newValue = $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity, $associatedId);
  2488
+                                    break;
  2489
+
2481 2490
                                 // We are negating the condition here. Other cases will assume it is valid!
2482 2491
                                 case ($hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER):
2483 2492
                                     $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $associatedId);
56  tests/Doctrine/Tests/Models/MappedAssociation/AbstractContent.php
... ...
@@ -0,0 +1,56 @@
  1
+<?php
  2
+namespace Doctrine\Tests\Models\MappedAssociation;
  3
+
  4
+/**
  5
+ * @MappedSuperclass
  6
+ */
  7
+class AbstractContent
  8
+{
  9
+    /**
  10
+     * @Id
  11
+     * @OneToOne(targetEntity="FileFolder", inversedBy="content")
  12
+     * @JoinColumn(name="file_folder_id")
  13
+     *
  14
+     * @var FileFolder $fileFolder
  15
+     */
  16
+    private $fileFolder;
  17
+
  18
+    /**
  19
+     * @Column(type="string", length=128)
  20
+     *
  21
+     * @var string $description
  22
+     */
  23
+    private $description;
  24
+
  25
+    /**
  26
+     * @param string $description
  27
+     */
  28
+    public function setDescription($description)
  29
+    {
  30
+        $this->description = $description;
  31
+    }
  32
+
  33
+    /**
  34
+     * @return string
  35
+     */
  36
+    public function getDescription()
  37
+    {
  38
+        return $this->description;
  39
+    }
  40
+
  41
+    /**
  42
+     * @return FileFolder
  43
+     */
  44
+    public function getFileFolder()
  45
+    {
  46
+        return $this->fileFolder;
  47
+    }
  48
+
  49
+    /**
  50
+     * @param FileFolder $fileFolder
  51
+     */
  52
+    public function setFileFolder(FileFolder $fileFolder)
  53
+    {
  54
+        $this->fileFolder = $fileFolder;
  55
+    }
  56
+}
93  tests/Doctrine/Tests/Models/MappedAssociation/FileFolder.php
... ...
@@ -0,0 +1,93 @@
  1
+<?php
  2
+namespace Doctrine\Tests\Models\MappedAssociation;
  3
+
  4
+/**
  5
+ * @Entity
  6
+ * @Table(name="file_folder")
  7
+ * @NamedNativeQueries({
  8
+ *      @NamedNativeQuery(
  9
+ *          name                = "get-class-by-id",
  10
+ *          resultSetMapping    = "get-class",
  11
+ *          query               = "SELECT content_class from file_folder WHERE id = ?"
  12
+ *      )
  13
+ * })
  14
+ *
  15
+ * @SqlResultSetMappings({
  16
+ *      @SqlResultSetMapping(
  17
+ *          name    = "get-class",
  18
+ *          columns = {
  19
+ *              @ColumnResult(
  20
+ *                  name = "content_class"
  21
+ *              )
  22
+ *          }
  23
+ *      )
  24
+ * })
  25
+ *
  26
+ */
  27
+class FileFolder
  28
+{
  29
+    /**
  30
+     * @Id
  31
+     * @GeneratedValue
  32
+     * @Column(type="integer")
  33
+     *
  34
+     * @var int $id
  35
+     */
  36
+    private $id;
  37
+
  38
+    /**
  39
+     * @Column(type="string", length=128)
  40
+     *
  41
+     * @var string $title
  42
+     */
  43
+    private $title;
  44
+
  45
+    /**
  46
+     * @OneToOne(targetEntity="AbstractContent", mappedBy="fileFolder", cascade={"all"}, orphanRemoval=true)
  47
+     * @MappedAssociation(discriminatorColumn="content_class")
  48
+     *
  49
+     * @var AbstractContent $content
  50
+     */
  51
+    private $content;
  52
+
  53
+    /**
  54
+     * @return int
  55
+     */
  56
+    public function getId()
  57
+    {
  58
+        return $this->id;
  59
+    }
  60
+
  61
+    /**
  62
+     * @param string $title
  63
+     */
  64
+    public function setTitle($title)
  65
+    {
  66
+        $this->title = $title;
  67
+    }
  68
+
  69
+    /**
  70
+     * @return string
  71
+     */
  72
+    public function getTitle()
  73
+    {
  74
+        return $this->title;
  75
+    }
  76
+
  77
+    /**
  78
+     * @param AbstractContent $content
  79
+     */
  80
+    public function setContent(AbstractContent $content)
  81
+    {
  82
+        $content->setFileFolder($this);
  83
+        $this->content = $content;
  84
+    }
  85
+
  86
+    /**
  87
+     * @return AbstractContent
  88
+     */
  89
+    public function getContent()
  90
+    {
  91
+        return $this->content;
  92
+    }
  93
+}
11  tests/Doctrine/Tests/Models/MappedAssociation/Paper.php
... ...
@@ -0,0 +1,11 @@
  1
+<?php
  2
+namespace Doctrine\Tests\Models\MappedAssociation;
  3
+
  4
+/**
  5
+ * @Entity
  6
+ * @Table(name="paper")
  7
+ */
  8
+class Paper extends AbstractContent
  9
+{
  10
+
  11
+}
11  tests/Doctrine/Tests/Models/MappedAssociation/Photo.php
... ...
@@ -0,0 +1,11 @@
  1
+<?php
  2
+namespace Doctrine\Tests\Models\MappedAssociation;
  3
+
  4
+/**
  5
+ * @Entity
  6
+ * @Table(name="photo")
  7
+ */
  8
+class Photo extends AbstractContent
  9
+{
  10
+
  11
+}
150  tests/Doctrine/Tests/ORM/Functional/MappedAssociationTest.php
... ...
@@ -0,0 +1,150 @@
  1
+<?php
  2
+
  3
+namespace Doctrine\Tests\ORM\Functional;
  4
+
  5
+use Doctrine\Tests\Models\MappedAssociation\FileFolder;
  6
+use Doctrine\Tests\Models\MappedAssociation\Paper;
  7
+use Doctrine\Tests\Models\MappedAssociation\Photo;
  8
+
  9
+require_once __DIR__ . '/../../TestInit.php';
  10
+
  11
+/**
  12
+ * Mapped association tests
  13
+ *
  14
+ * @group Doctrine.MappedAssociation
  15
+ */
  16
+class MappedAssociationTest extends \Doctrine\Tests\OrmFunctionalTestCase
  17
+{
  18
+    const FILEFOLDER         = 'Doctrine\Tests\Models\MappedAssociation\FileFolder';
  19
+    const PAPER              = 'Doctrine\Tests\Models\MappedAssociation\Paper';
  20
+    const PHOTO              = 'Doctrine\Tests\Models\MappedAssociation\Photo';
  21
+
  22
+    protected function setUp()
  23
+    {
  24
+        $this->useModelSet('mappedassociation');
  25
+        parent::setUp();
  26
+    }
  27
+
  28
+    /**
  29
+     * Test mapped association with mapped entity having primary key as a foreign key.
  30
+     */
  31
+    public function testMappedAssociation()
  32
+    {
  33
+        /**
  34
+         * Create file folder 0
  35
+         */
  36
+        $fileFolder0 = new FileFolder();
  37
+        $fileFolder0->setTitle('Folder 0');
  38
+        $this->_em->persist($fileFolder0);
  39
+        $this->_em->flush();
  40
+        $id0 = $fileFolder0->getId();
  41
+
  42
+        /**
  43
+         * Create file folder 1
  44
+         */
  45
+        $fileFolder1 = new FileFolder();
  46
+        $fileFolder1->setTitle('Folder 1');
  47
+        $this->_em->persist($fileFolder1);
  48
+        $this->_em->flush();
  49
+        $id1 = $fileFolder1->getId();
  50
+
  51
+        $content1 = new Paper;
  52
+        $content1->setDescription('Expense report');
  53
+        $fileFolder1->setContent($content1);
  54
+        $this->_em->flush();
  55
+
  56
+        /**
  57
+         * Create file folder 2
  58
+         */
  59
+        $fileFolder2 = new FileFolder();
  60
+        $fileFolder2->setTitle('Folder 2');
  61
+        $this->_em->persist($fileFolder2);
  62
+        $this->_em->flush();
  63
+        $id2 = $fileFolder2->getId();
  64
+
  65
+        $content2 = new Photo;
  66
+        $content2->setDescription('Family photo');
  67
+        $fileFolder2->setContent($content2);
  68
+        $this->_em->flush();
  69
+
  70
+        /**
  71
+         * Clear entity manager for tests
  72
+         */
  73
+        $this->_em->clear();
  74
+
  75
+        /**
  76
+         * Check discriminator column was set properly
  77
+         */
  78
+        $fileFolderRepository = $this->_em->getRepository($this::FILEFOLDER);
  79
+        $query = $fileFolderRepository->createNativeNamedQuery('get-class-by-id');
  80
+
  81
+        $results = $query->setParameter(1, $id0)
  82
+            ->getResult();
  83
+        $this->assertEquals($results[0]['content_class'], null);
  84
+
  85
+        $results = $query->setParameter(1, $id1)
  86
+            ->getResult();
  87
+        $this->assertEquals($results[0]['content_class'], get_class($content1));
  88
+
  89
+        $results = $query->setParameter(1, $id2)
  90
+            ->getResult();
  91
+        $this->assertEquals($results[0]['content_class'], get_class($content2));
  92
+
  93
+        /**
  94
+         * Check can get container from mapped association
  95
+         */
  96
+        $paperRepository = $this->_em->getRepository($this::PAPER);
  97
+        $queryPaper1 = $paperRepository->find($id1);
  98
+        $this->assertEquals($content1->getFileFolder()->getTitle(), $queryPaper1->getFileFolder()->getTitle());
  99
+
  100
+        $photoRepository = $this->_em->getRepository($this::PHOTO);
  101
+        $queryPhoto2 = $photoRepository->find($id2);
  102
+        $this->assertEquals($content2->getFileFolder()->getTitle(), $queryPhoto2->getFileFolder()->getTitle());
  103
+
  104
+        /**
  105
+         * Clear the EM so we're not comparing proxies (or set join eager)
  106
+         */
  107
+        $this->_em->clear();
  108
+
  109
+        /**
  110
+         * Check container entity retrieval
  111
+         */
  112
+        $queryFileFolder0 = $fileFolderRepository->find($id0);
  113
+        $this->assertEquals($fileFolder0, $queryFileFolder0);
  114
+
  115
+        $queryFileFolder1 = $fileFolderRepository->find($id1);
  116
+        $this->assertEquals($fileFolder1, $queryFileFolder1);
  117
+
  118
+        $queryFileFolder2 = $fileFolderRepository->find($id2);
  119
+        $this->assertEquals($fileFolder2, $queryFileFolder2);
  120
+
  121
+        /**
  122
+         * Remove container entities and clear EM
  123
+         */
  124
+        $this->_em->remove($queryFileFolder0);
  125
+        $this->_em->remove($queryFileFolder1);
  126
+        $this->_em->remove($queryFileFolder2);
  127
+        $this->_em->flush();
  128
+        $this->_em->clear();
  129
+
  130
+        /**
  131
+         * Check containers and mapped associations removed
  132
+         */
  133
+        $queryFileFolder0 = $fileFolderRepository->find($id0);
  134
+        $this->assertEquals(null, $queryFileFolder0);
  135
+
  136
+        $queryFileFolder1 = $fileFolderRepository->find($id1);
  137
+        $this->assertEquals(null, $queryFileFolder1);
  138
+
  139
+        $queryFileFolder2 = $fileFolderRepository->find($id2);
  140
+        $this->assertEquals(null, $queryFileFolder2);
  141
+
  142
+        $fileFolderRepository = $this->_em->getRepository($this::PAPER);
  143
+        $results = $fileFolderRepository->findAll();
  144
+        $this->assertEmpty($results);
  145
+
  146
+        $fileFolderRepository = $this->_em->getRepository($this::PHOTO);
  147
+        $results = $fileFolderRepository->findAll();
  148
+        $this->assertEmpty($results);
  149
+    }
  150
+}
12  tests/Doctrine/Tests/OrmFunctionalTestCase.php
@@ -124,6 +124,12 @@
124 124
             'Doctrine\Tests\Models\CustomType\CustomTypeParent',
125 125
             'Doctrine\Tests\Models\CustomType\CustomTypeUpperCase',
126 126
         ),
  127
+        'mappedassociation' => array(
  128
+            'Doctrine\Tests\Models\MappedAssociation\FileFolder',
  129
+            'Doctrine\Tests\Models\MappedAssociation\AbstractContent',
  130
+            'Doctrine\Tests\Models\MappedAssociation\Paper',
  131
+            'Doctrine\Tests\Models\MappedAssociation\Photo',
  132
+        ),
127 133
     );
128 134
 
129 135
     protected function useModelSet($setName)
@@ -239,6 +245,12 @@ protected function tearDown()
239 245
             $conn->executeUpdate('DELETE FROM customtype_uppercases');
240 246
         }
241 247
 
  248
+        if (isset($this->_usedModelSets['mappedassociation'])) {
  249
+            $conn->executeUpdate('DELETE FROM paper');
  250
+            $conn->executeUpdate('DELETE FROM photo');
  251
+            $conn->executeUpdate('DELETE FROM file_folder');
  252
+        }
  253
+
242 254
         $this->_em->clear();
243 255
     }
244 256
 
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.