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 20namespace Doctrine\ORM\Query; 21 22use Doctrine\ORM\EntityManagerInterface; 23use Doctrine\ORM\Mapping\ClassMetadataInfo; 24 25/** 26 * A ResultSetMappingBuilder uses the EntityManager to automatically populate entity fields. 27 * 28 * @author Michael Ridgway <mcridgway@gmail.com> 29 * @since 2.1 30 */ 31class ResultSetMappingBuilder extends ResultSetMapping 32{ 33 /** 34 * Picking this rename mode will register entity columns as is, 35 * as they are in the database. This can cause clashes when multiple 36 * entities are fetched that have columns with the same name. 37 * 38 * @var int 39 */ 40 const COLUMN_RENAMING_NONE = 1; 41 42 /** 43 * Picking custom renaming allows the user to define the renaming 44 * of specific columns with a rename array that contains column names as 45 * keys and result alias as values. 46 * 47 * @var int 48 */ 49 const COLUMN_RENAMING_CUSTOM = 2; 50 51 /** 52 * Incremental renaming uses a result set mapping internal counter to add a 53 * number to each column result, leading to uniqueness. This only works if 54 * you use {@see generateSelectClause()} to generate the SELECT clause for 55 * you. 56 * 57 * @var int 58 */ 59 const COLUMN_RENAMING_INCREMENT = 3; 60 61 /** 62 * @var int 63 */ 64 private $sqlCounter = 0; 65 66 /** 67 * @var EntityManagerInterface 68 */ 69 private $em; 70 71 /** 72 * Default column renaming mode. 73 * 74 * @var int 75 */ 76 private $defaultRenameMode; 77 78 /** 79 * @param EntityManagerInterface $em 80 * @param integer $defaultRenameMode 81 */ 82 public function __construct(EntityManagerInterface $em, $defaultRenameMode = self::COLUMN_RENAMING_NONE) 83 { 84 $this->em = $em; 85 $this->defaultRenameMode = $defaultRenameMode; 86 } 87 88 /** 89 * Adds a root entity and all of its fields to the result set. 90 * 91 * @param string $class The class name of the root entity. 92 * @param string $alias The unique alias to use for the root entity. 93 * @param array $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName). 94 * @param int|null $renameMode One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM). 95 * 96 * @return void 97 */ 98 public function addRootEntityFromClassMetadata($class, $alias, $renamedColumns = array(), $renameMode = null) 99 { 100 $renameMode = $renameMode ?: $this->defaultRenameMode; 101 $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); 102 103 $this->addEntityResult($class, $alias); 104 $this->addAllClassFields($class, $alias, $columnAliasMap); 105 } 106 107 /** 108 * Adds a joined entity and all of its fields to the result set. 109 * 110 * @param string $class The class name of the joined entity. 111 * @param string $alias The unique alias to use for the joined entity. 112 * @param string $parentAlias The alias of the entity result that is the parent of this joined result. 113 * @param string $relation The association field that connects the parent entity result 114 * with the joined entity result. 115 * @param array $renamedColumns Columns that have been renamed (tableColumnName => queryColumnName). 116 * @param int|null $renameMode One of the COLUMN_RENAMING_* constants or array for BC reasons (CUSTOM). 117 * 118 * @return void 119 */ 120 public function addJoinedEntityFromClassMetadata($class, $alias, $parentAlias, $relation, $renamedColumns = array(), $renameMode = null) 121 { 122 $renameMode = $renameMode ?: $this->defaultRenameMode; 123 $columnAliasMap = $this->getColumnAliasMap($class, $renameMode, $renamedColumns); 124 125 $this->addJoinedEntityResult($class, $alias, $parentAlias, $relation); 126 $this->addAllClassFields($class, $alias, $columnAliasMap); 127 } 128 129 /** 130 * Adds all fields of the given class to the result set mapping (columns and meta fields). 131 * 132 * @param string $class 133 * @param string $alias 134 * @param array $columnAliasMap 135 * 136 * @return void 137 * 138 * @throws \InvalidArgumentException 139 */ 140 protected function addAllClassFields($class, $alias, $columnAliasMap = array()) 141 { 142 $classMetadata = $this->em->getClassMetadata($class); 143 $platform = $this->em->getConnection()->getDatabasePlatform(); 144 145 if ( ! $this->isInheritanceSupported($classMetadata)) { 146 throw new \InvalidArgumentException('ResultSetMapping builder does not currently support your inheritance scheme.'); 147 } 148 149 150 foreach ($classMetadata->getColumnNames() as $columnName) { 151 $propertyName = $classMetadata->getFieldName($columnName); 152 $columnAlias = $platform->getSQLResultCasing($columnAliasMap[$columnName]); 153 154 if (isset($this->fieldMappings[$columnAlias])) { 155 throw new \InvalidArgumentException("The column '$columnName' conflicts with another column in the mapper."); 156 } 157 158 $this->addFieldResult($alias, $columnAlias, $propertyName); 159 } 160 161 foreach ($classMetadata->associationMappings as $associationMapping) { 162 if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) { 163 foreach ($associationMapping['joinColumns'] as $joinColumn) { 164 $columnName = $joinColumn['name']; 165 $columnAlias = $platform->getSQLResultCasing($columnAliasMap[$columnName]); 166 167 if (isset($this->metaMappings[$columnAlias])) { 168 throw new \InvalidArgumentException("The column '$columnAlias' conflicts with another column in the mapper."); 169 } 170 171 $this->addMetaResult( 172 $alias, 173 $columnAlias, 174 $columnName, 175 (isset($associationMapping['id']) && $associationMapping['id'] === true) 176 ); 177 } 178 } 179 } 180 } 181 182 private function isInheritanceSupported(ClassMetadataInfo $classMetadata) 183 { 184 if ($classMetadata->isInheritanceTypeSingleTable() 185 && in_array($classMetadata->name, $classMetadata->discriminatorMap, true)) { 186 return true; 187 } 188 189 return ! ($classMetadata->isInheritanceTypeSingleTable() || $classMetadata->isInheritanceTypeJoined()); 190 } 191 192 /** 193 * Gets column alias for a given column. 194 * 195 * @param string $columnName 196 * @param int $mode 197 * @param array $customRenameColumns 198 * 199 * @return string 200 */ 201 private function getColumnAlias($columnName, $mode, array $customRenameColumns) 202 { 203 switch ($mode) { 204 case self::COLUMN_RENAMING_INCREMENT: 205 return $columnName . $this->sqlCounter++; 206 207 case self::COLUMN_RENAMING_CUSTOM: 208 return isset($customRenameColumns[$columnName]) 209 ? $customRenameColumns[$columnName] : $columnName; 210 211 case self::COLUMN_RENAMING_NONE: 212 return $columnName; 213 214 } 215 } 216 217 /** 218 * Retrieves a class columns and join columns aliases that are used in the SELECT clause. 219 * 220 * This depends on the renaming mode selected by the user. 221 * 222 * @param string $className 223 * @param int $mode 224 * @param array $customRenameColumns 225 * 226 * @return array 227 */ 228 private function getColumnAliasMap($className, $mode, array $customRenameColumns) 229 { 230 if ($customRenameColumns) { // for BC with 2.2-2.3 API 231 $mode = self::COLUMN_RENAMING_CUSTOM; 232 } 233 234 $columnAlias = array(); 235 $class = $this->em->getClassMetadata($className); 236 237 foreach ($class->getColumnNames() as $columnName) { 238 $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); 239 } 240 241 foreach ($class->associationMappings as $associationMapping) { 242 if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) { 243 foreach ($associationMapping['joinColumns'] as $joinColumn) { 244 $columnName = $joinColumn['name']; 245 $columnAlias[$columnName] = $this->getColumnAlias($columnName, $mode, $customRenameColumns); 246 } 247 } 248 } 249 250 return $columnAlias; 251 } 252 253 /** 254 * Adds the mappings of the results of native SQL queries to the result set. 255 * 256 * @param ClassMetadataInfo $class 257 * @param array $queryMapping 258 * 259 * @return ResultSetMappingBuilder 260 */ 261 public function addNamedNativeQueryMapping(ClassMetadataInfo $class, array $queryMapping) 262 { 263 if (isset($queryMapping['resultClass'])) { 264 return $this->addNamedNativeQueryResultClassMapping($class, $queryMapping['resultClass']); 265 } 266 267 return $this->addNamedNativeQueryResultSetMapping($class, $queryMapping['resultSetMapping']); 268 } 269 270 /** 271 * Adds the class mapping of the results of native SQL queries to the result set. 272 * 273 * @param ClassMetadataInfo $class 274 * @param string $resultClassName 275 * 276 * @return ResultSetMappingBuilder 277 */ 278 public function addNamedNativeQueryResultClassMapping(ClassMetadataInfo $class, $resultClassName) 279 { 280 $classMetadata = $this->em->getClassMetadata($resultClassName); 281 $shortName = $classMetadata->reflClass->getShortName(); 282 $alias = strtolower($shortName[0]).'0'; 283 284 $this->addEntityResult($class->name, $alias); 285 286 if ($classMetadata->discriminatorColumn) { 287 $discriminatorColumn = $classMetadata->discriminatorColumn; 288 $this->setDiscriminatorColumn($alias, $discriminatorColumn['name']); 289 $this->addMetaResult($alias, $discriminatorColumn['name'], $discriminatorColumn['fieldName']); 290 } 291 292 foreach ($classMetadata->getColumnNames() as $key => $columnName) { 293 $propertyName = $classMetadata->getFieldName($columnName); 294 $this->addFieldResult($alias, $columnName, $propertyName); 295 } 296 297 foreach ($classMetadata->associationMappings as $associationMapping) { 298 if ($associationMapping['isOwningSide'] && $associationMapping['type'] & ClassMetadataInfo::TO_ONE) { 299 foreach ($associationMapping['joinColumns'] as $joinColumn) { 300 $columnName = $joinColumn['name']; 301 $this->addMetaResult($alias, $columnName, $columnName, $classMetadata->isIdentifier($columnName)); 302 } 303 } 304 } 305 306 return $this; 307 } 308 309 /** 310 * Adds the result set mapping of the results of native SQL queries to the result set. 311 * 312 * @param ClassMetadataInfo $class 313 * @param string $resultSetMappingName 314 * 315 * @return ResultSetMappingBuilder 316 */ 317 public function addNamedNativeQueryResultSetMapping(ClassMetadataInfo $class, $resultSetMappingName) 318 { 319 $counter = 0; 320 $resultMapping = $class->getSqlResultSetMapping($resultSetMappingName); 321 $rooShortName = $class->reflClass->getShortName(); 322 $rootAlias = strtolower($rooShortName[0]) . $counter; 323 324 325 if (isset($resultMapping['entities'])) { 326 foreach ($resultMapping['entities'] as $key => $entityMapping) { 327 $classMetadata = $this->em->getClassMetadata($entityMapping['entityClass']); 328 329 if ($class->reflClass->name == $classMetadata->reflClass->name) { 330 $this->addEntityResult($classMetadata->name, $rootAlias); 331 $this->addNamedNativeQueryEntityResultMapping($classMetadata, $entityMapping, $rootAlias); 332 } else { 333 $shortName = $classMetadata->reflClass->getShortName(); 334 $joinAlias = strtolower($shortName[0]) . ++ $counter; 335 $associations = $class->getAssociationsByTargetClass($classMetadata->name); 336 337 foreach ($associations as $relation => $mapping) { 338 $this->addJoinedEntityResult($mapping['targetEntity'], $joinAlias, $rootAlias, $relation); 339 $this->addNamedNativeQueryEntityResultMapping($classMetadata, $entityMapping, $joinAlias); 340 } 341 } 342 343 } 344 } 345 346 if (isset($resultMapping['columns'])) { 347 foreach ($resultMapping['columns'] as $entityMapping) { 348 $this->addScalarResult($entityMapping['name'], $entityMapping['name']); 349 } 350 } 351 352 return $this; 353 } 354 355 /** 356 * Adds the entity result mapping of the results of native SQL queries to the result set. 357 * 358 * @param ClassMetadataInfo $classMetadata 359 * @param array $entityMapping 360 * @param string $alias 361 * 362 * @return ResultSetMappingBuilder 363 * 364 * @throws \InvalidArgumentException 365 */ 366 public function addNamedNativeQueryEntityResultMapping(ClassMetadataInfo $classMetadata, array $entityMapping, $alias) 367 { 368 if (isset($entityMapping['discriminatorColumn']) && $entityMapping['discriminatorColumn']) { 369 $discriminatorColumn = $entityMapping['discriminatorColumn']; 370 $this->setDiscriminatorColumn($alias, $discriminatorColumn); 371 $this->addMetaResult($alias, $discriminatorColumn, $discriminatorColumn); 372 } 373 374 if (isset($entityMapping['fields']) && !empty($entityMapping['fields'])) { 375 foreach ($entityMapping['fields'] as $field) { 376 $fieldName = $field['name']; 377 $relation = null; 378 379 if(strpos($fieldName, '.')){ 380 list($relation, $fieldName) = explode('.', $fieldName); 381 } 382 383 if (isset($classMetadata->associationMappings[$relation])) { 384 if($relation) { 385 $associationMapping = $classMetadata->associationMappings[$relation]; 386 $joinAlias = $alias.$relation; 387 $parentAlias = $alias; 388 389 $this->addJoinedEntityResult($associationMapping['targetEntity'], $joinAlias, $parentAlias, $relation); 390 $this->addFieldResult($joinAlias, $field['column'], $fieldName); 391 }else { 392 $this->addFieldResult($alias, $field['column'], $fieldName, $classMetadata->name); 393 } 394 } else { 395 if(!isset($classMetadata->fieldMappings[$fieldName])) { 396 throw new \InvalidArgumentException("Entity '".$classMetadata->name."' has no field '".$fieldName."'. "); 397 } 398 $this->addFieldResult($alias, $field['column'], $fieldName, $classMetadata->name); 399 } 400 } 401 402 } else { 403 foreach ($classMetadata->getColumnNames() as $columnName) { 404 $propertyName = $classMetadata->getFieldName($columnName); 405 $this->addFieldResult($alias, $columnName, $propertyName); 406 } 407 } 408 409 return $this; 410 } 411 412 /** 413 * Generates the Select clause from this ResultSetMappingBuilder. 414 * 415 * Works only for all the entity results. The select parts for scalar 416 * expressions have to be written manually. 417 * 418 * @param array $tableAliases 419 * 420 * @return string 421 */ 422 public function generateSelectClause($tableAliases = array()) 423 { 424 $sql = ""; 425 426 foreach ($this->columnOwnerMap as $columnName => $dqlAlias) { 427 $tableAlias = isset($tableAliases[$dqlAlias]) 428 ? $tableAliases[$dqlAlias] : $dqlAlias; 429 430 if ($sql) { 431 $sql .= ", "; 432 } 433 434 $sql .= $tableAlias . "."; 435 436 if (isset($this->fieldMappings[$columnName])) { 437 $class = $this->em->getClassMetadata($this->declaringClasses[$columnName]); 438 $sql .= $class->fieldMappings[$this->fieldMappings[$columnName]]['columnName']; 439 } else if (isset($this->metaMappings[$columnName])) { 440 $sql .= $this->metaMappings[$columnName]; 441 } else if (isset($this->discriminatorColumns[$dqlAlias])) { 442 $sql .= $this->discriminatorColumns[$dqlAlias]; 443 } 444 445 $sql .= " AS " . $columnName; 446 } 447 448 return $sql; 449 } 450 451 /** 452 * @return string 453 */ 454 public function __toString() 455 { 456 return $this->generateSelectClause(array()); 457 } 458} 459