/** * @group DDC-117 */ public function testIndexByMetadataColumn() { $this->_rsm->addEntityResult('Doctrine\\Tests\\Models\\Legacy\\LegacyUser', 'u'); $this->_rsm->addJoinedEntityResult('Doctrine\\Tests\\Models\\Legacy', 'lu', 'u', '_references'); $this->_rsm->addMetaResult('lu', '_source', '_source', true); $this->_rsm->addMetaResult('lu', '_target', '_target', true); $this->_rsm->addIndexBy('lu', '_source'); $this->assertTrue($this->_rsm->hasIndexBy('lu')); }
/** * Walks down a FromClause AST node, thereby generating the appropriate SQL. * * @return string The SQL. */ public function walkFromClause($fromClause) { $sql = ' FROM '; $identificationVarDecls = $fromClause->identificationVariableDeclarations; $firstIdentificationVarDecl = $identificationVarDecls[0]; $rangeDecl = $firstIdentificationVarDecl->rangeVariableDeclaration; $dqlAlias = $rangeDecl->aliasIdentificationVariable; $this->_currentRootAlias = $dqlAlias; $class = $this->_em->getClassMetadata($rangeDecl->abstractSchemaName); $sql .= $class->getQuotedTableName($this->_platform) . ' ' . $this->getSqlTableAlias($class->table['name'], $dqlAlias); if ($class->isInheritanceTypeJoined()) { $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias); } foreach ($firstIdentificationVarDecl->joinVariableDeclarations as $joinVarDecl) { $sql .= $this->walkJoinVariableDeclaration($joinVarDecl); } if ($firstIdentificationVarDecl->indexBy) { $this->_rsm->addIndexBy($firstIdentificationVarDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, $firstIdentificationVarDecl->indexBy->simpleStateFieldPathExpression->parts[0]); } return $this->_platform->appendLockHint($sql, $this->_query->getHint(Query::HINT_LOCK_MODE)); }
/** * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. * * @param AST\JoinAssociationDeclaration $joinAssociationDeclaration * @param int $joinType * * @return string * * @throws QueryException */ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER) { $sql = ''; $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression; $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; $indexBy = $joinAssociationDeclaration->indexBy; $relation = $this->queryComponents[$joinedDqlAlias]['relation']; $targetClass = $this->em->getClassMetadata($relation['targetEntity']); $sourceClass = $this->em->getClassMetadata($relation['sourceEntity']); $targetTableName = $targetClass->getQuotedTableName($this->platform); $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable); // Ensure we got the owning side, since it has all mapping info $assoc = !$relation['isOwningSide'] ? $targetClass->associationMappings[$relation['mappedBy']] : $relation; if ($this->query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->query->getHint(self::HINT_DISTINCT) || isset($this->selectedClasses[$joinedDqlAlias]))) { if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) { throw QueryException::iterateWithFetchJoinNotAllowed($assoc); } } // This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot // be the owning side and previously we ensured that $assoc is always the owning side of the associations. // The owning side is necessary at this point because only it contains the JoinColumn information. switch (true) { case $assoc['type'] & ClassMetadata::TO_ONE: $conditions = array(); foreach ($assoc['joinColumns'] as $joinColumn) { $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); if ($relation['isOwningSide']) { $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; continue; } $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn; } // Apply remaining inheritance restrictions $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); if ($discrSql) { $conditions[] = $discrSql; } // Apply the filters $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); if ($filterExpr) { $conditions[] = $filterExpr; } $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ' . implode(' AND ', $conditions); break; case $assoc['type'] == ClassMetadata::MANY_TO_MANY: // Join relation table $joinTable = $assoc['joinTable']; $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias); $joinTableName = $sourceClass->getQuotedJoinTableName($assoc, $this->platform); $conditions = array(); $relationColumns = $relation['isOwningSide'] ? $assoc['joinTable']['joinColumns'] : $assoc['joinTable']['inverseJoinColumns']; foreach ($relationColumns as $joinColumn) { $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; } $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions); // Join target table $sql .= $joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN '; $conditions = array(); $relationColumns = $relation['isOwningSide'] ? $assoc['joinTable']['inverseJoinColumns'] : $assoc['joinTable']['joinColumns']; foreach ($relationColumns as $joinColumn) { $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; } // Apply remaining inheritance restrictions $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); if ($discrSql) { $conditions[] = $discrSql; } // Apply the filters $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); if ($filterExpr) { $conditions[] = $filterExpr; } $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ' . implode(' AND ', $conditions); break; } // FIXME: these should either be nested or all forced to be left joins (DDC-XXX) if ($targetClass->isInheritanceTypeJoined()) { $sql .= $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); } // Apply the indexes if ($indexBy) { // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. $this->rsm->addIndexBy($indexBy->simpleStateFieldPathExpression->identificationVariable, $indexBy->simpleStateFieldPathExpression->field); } else { if (isset($relation['indexBy'])) { $this->rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']); } } return $sql; }
/** * Walks down a JoinVariableDeclaration AST node and creates the corresponding SQL. * * @param JoinVariableDeclaration $joinVarDecl * @return string The SQL. */ public function walkJoinVariableDeclaration($joinVarDecl) { $join = $joinVarDecl->join; $joinType = $join->joinType; if ($joinVarDecl->indexBy) { // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. $this->_rsm->addIndexBy($joinVarDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, $joinVarDecl->indexBy->simpleStateFieldPathExpression->field); } if ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) { $sql = ' LEFT JOIN '; } else { $sql = ' INNER JOIN '; } $joinAssocPathExpr = $join->joinAssociationPathExpression; $joinedDqlAlias = $join->aliasIdentificationVariable; $relation = $this->_queryComponents[$joinedDqlAlias]['relation']; $targetClass = $this->_em->getClassMetadata($relation['targetEntity']); $sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']); $targetTableName = $targetClass->getQuotedTableName($this->_platform); $targetTableAlias = $this->getSqlTableAlias($targetClass->table['name'], $joinedDqlAlias); $sourceTableAlias = $this->getSqlTableAlias($sourceClass->table['name'], $joinAssocPathExpr->identificationVariable); // Ensure we got the owning side, since it has all mapping info $assoc = !$relation['isOwningSide'] ? $targetClass->associationMappings[$relation['mappedBy']] : $relation; if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true) { if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) { throw QueryException::iterateWithFetchJoinNotAllowed($assoc); } } if ($assoc['type'] & ClassMetadata::TO_ONE) { $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; $first = true; foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) { if (!$first) { $sql .= ' AND '; } else { $first = false; } if ($relation['isOwningSide']) { $quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform); $sql .= $sourceTableAlias . '.' . $sourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; } else { $quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform); $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; } } } else { if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { // Join relation table $joinTable = $assoc['joinTable']; $joinTableAlias = $this->getSqlTableAlias($joinTable['name'], $joinedDqlAlias); $sql .= $sourceClass->getQuotedJoinTableName($assoc, $this->_platform) . ' ' . $joinTableAlias . ' ON '; $first = true; if ($relation['isOwningSide']) { foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { if (!$first) { $sql .= ' AND '; } else { $first = false; } $sql .= $sourceTableAlias . '.' . $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform) . ' = ' . $joinTableAlias . '.' . $relationColumn; } } else { foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { if (!$first) { $sql .= ' AND '; } else { $first = false; } $sql .= $sourceTableAlias . '.' . $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform) . ' = ' . $joinTableAlias . '.' . $relationColumn; } } // Join target table $sql .= $joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN '; $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON '; $first = true; if ($relation['isOwningSide']) { foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) { if (!$first) { $sql .= ' AND '; } else { $first = false; } $sql .= $targetTableAlias . '.' . $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform) . ' = ' . $joinTableAlias . '.' . $relationColumn; } } else { foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) { if (!$first) { $sql .= ' AND '; } else { $first = false; } $sql .= $targetTableAlias . '.' . $targetClass->getQuotedColumnName($targetClass->fieldNames[$sourceColumn], $this->_platform) . ' = ' . $joinTableAlias . '.' . $relationColumn; } } } } // Handle WITH clause if (($condExpr = $join->conditionalExpression) !== null) { // Phase 2 AST optimization: Skip processment of ConditionalExpression // if only one ConditionalTerm is defined $sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')'; } $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); if ($discrSql) { $sql .= ' AND ' . $discrSql; } // FIXME: these should either be nested or all forced to be left joins (DDC-XXX) if ($targetClass->isInheritanceTypeJoined()) { $sql .= $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); } return $sql; }
/** * Gets the SQL fragment with the list of columns to select when querying for * an entity in this persister. * * Subclasses should override this method to alter or change the select column * list SQL fragment. Note that in the implementation of BasicEntityPersister * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}. * Subclasses may or may not do the same. * * @return string The SQL fragment. */ protected function getSelectColumnsSQL() { if ($this->selectColumnListSql !== null) { return $this->selectColumnListSql; } $columnList = array(); $this->rsm = new Query\ResultSetMapping(); $this->rsm->addEntityResult($this->class->name, 'r'); // r for root // Add regular columns to select list foreach ($this->class->fieldNames as $field) { $columnList[] = $this->getSelectColumnSQL($field, $this->class); } $this->selectJoinSql = ''; $eagerAliasCounter = 0; foreach ($this->class->associationMappings as $assocField => $assoc) { $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class); if ($assocColumnSQL) { $columnList[] = $assocColumnSQL; } $isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && !$assoc['isOwningSide']; $isAssocFromOneEager = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER; if (!($isAssocFromOneEager || $isAssocToOneInverseSide)) { continue; } $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']); if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) { continue; // now this is why you shouldn't use inheritance } $assocAlias = 'e' . $eagerAliasCounter++; $this->rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField); foreach ($eagerEntity->fieldNames as $field) { $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias); } foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) { $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL($eagerAssocField, $eagerAssoc, $eagerEntity, $assocAlias); if ($eagerAssocColumnSQL) { $columnList[] = $eagerAssocColumnSQL; } } $association = $assoc; $joinCondition = array(); if (isset($assoc['indexBy'])) { $this->rsm->addIndexBy($assocAlias, $assoc['indexBy']); } if (!$assoc['isOwningSide']) { $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']); $association = $eagerEntity->getAssociationMapping($assoc['mappedBy']); } $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias); $joinTableName = $this->quoteStrategy->getTableName($eagerEntity, $this->platform); if ($assoc['isOwningSide']) { $tableAlias = $this->getSQLTableAlias($association['targetEntity'], $assocAlias); $this->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association['joinColumns']); foreach ($association['joinColumns'] as $joinColumn) { $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity']) . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol; } // Add filter SQL if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) { $joinCondition[] = $filterSql; } } else { $this->selectJoinSql .= ' LEFT JOIN'; foreach ($association['joinColumns'] as $joinColumn) { $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = ' . $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol; } } $this->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON '; $this->selectJoinSql .= implode(' AND ', $joinCondition); } $this->selectColumnListSql = implode(', ', $columnList); return $this->selectColumnListSql; }
/** * select u.id, u.status, upper(u.name) nameUpper from User u index by u.id * join u.phonenumbers p indexby p.phonenumber * = * select u.id, u.status, upper(u.name) as p__0 from USERS u * INNER JOIN PHONENUMBERS p ON u.id = p.user_id */ public function testNewHydrationMixedQueryFetchJoinCustomIndex() { $rsm = new ResultSetMapping(); $rsm->addEntityResult('Doctrine\\Tests\\Models\\CMS\\CmsUser', 'u'); $rsm->addJoinedEntityResult('Doctrine\\Tests\\Models\\CMS\\CmsPhonenumber', 'p', 'u', $this->_em->getClassMetadata('Doctrine\\Tests\\Models\\CMS\\CmsUser')->getAssociationMapping('phonenumbers')); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); $rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); $rsm->addIndexBy('u', 'id'); $rsm->addIndexBy('p', 'phonenumber'); // Faked result set $resultSet = array(array('u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42'), array('u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43'), array('u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91')); $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ArrayHydrator($this->_em); $result = $hydrator->hydrateAll($stmt, $rsm); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); // test the scalar values $this->assertEquals('ROMANB', $result[0]['nameUpper']); $this->assertEquals('JWAGE', $result[1]['nameUpper']); // first user => 2 phonenumbers. notice the custom indexing by user id $this->assertEquals(2, count($result[0]['1']['phonenumbers'])); // second user => 1 phonenumber. notice the custom indexing by user id $this->assertEquals(1, count($result[1]['2']['phonenumbers'])); // test the custom indexing of the phonenumbers $this->assertTrue(isset($result[0]['1']['phonenumbers']['42'])); $this->assertTrue(isset($result[0]['1']['phonenumbers']['43'])); $this->assertTrue(isset($result[1]['2']['phonenumbers']['91'])); }
/** * SELECT PARTIAL u.{id, status}, UPPER(u.name) AS nameUpper * FROM Doctrine\Tests\Models\CMS\CmsUser u * INDEX BY u.id * * @group DDC-1385 * @dataProvider provideDataForUserEntityResult */ public function testIndexByAndMixedResult($userEntityKey) { $rsm = new ResultSetMapping(); $rsm->addEntityResult('Doctrine\\Tests\\Models\\CMS\\CmsUser', 'u', $userEntityKey ?: null); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); $rsm->addIndexBy('u', 'id'); // Faked result set $resultSet = array(array('u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB'), array('u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE')); $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); $this->assertTrue(isset($result[1])); $this->assertEquals(1, $result[1][$userEntityKey]->id); $this->assertTrue(isset($result[2])); $this->assertEquals(2, $result[2][$userEntityKey]->id); }
/** * select u.id, u.status, upper(u.name) nameUpper from User u index by u.id * join u.phonenumbers p indexby p.phonenumber * = * select u.id, u.status, upper(u.name) as p__0 from USERS u * INNER JOIN PHONENUMBERS p ON u.id = p.user_id */ public function testMixedQueryFetchJoinCustomIndex() { $rsm = new ResultSetMapping(); $rsm->addEntityResult('Doctrine\\Tests\\Models\\CMS\\CmsUser', 'u'); $rsm->addJoinedEntityResult('Doctrine\\Tests\\Models\\CMS\\CmsPhonenumber', 'p', 'u', 'phonenumbers'); $rsm->addFieldResult('u', 'u__id', 'id'); $rsm->addFieldResult('u', 'u__status', 'status'); $rsm->addScalarResult('sclr0', 'nameUpper'); $rsm->addFieldResult('p', 'p__phonenumber', 'phonenumber'); $rsm->addIndexBy('u', 'id'); $rsm->addIndexBy('p', 'phonenumber'); // Faked result set $resultSet = array(array('u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '42'), array('u__id' => '1', 'u__status' => 'developer', 'sclr0' => 'ROMANB', 'p__phonenumber' => '43'), array('u__id' => '2', 'u__status' => 'developer', 'sclr0' => 'JWAGE', 'p__phonenumber' => '91')); $stmt = new HydratorMockStatement($resultSet); $hydrator = new \Doctrine\ORM\Internal\Hydration\ObjectHydrator($this->_em); $result = $hydrator->hydrateAll($stmt, $rsm, array(Query::HINT_FORCE_PARTIAL_LOAD => true)); $this->assertEquals(2, count($result)); $this->assertTrue(is_array($result)); $this->assertTrue(is_array($result[0])); $this->assertTrue(is_array($result[1])); // test the scalar values $this->assertEquals('ROMANB', $result[0]['nameUpper']); $this->assertEquals('JWAGE', $result[1]['nameUpper']); $this->assertTrue($result[0]['1'] instanceof \Doctrine\Tests\Models\CMS\CmsUser); $this->assertTrue($result[1]['2'] instanceof \Doctrine\Tests\Models\CMS\CmsUser); $this->assertTrue($result[0]['1']->phonenumbers instanceof \Doctrine\ORM\PersistentCollection); // first user => 2 phonenumbers. notice the custom indexing by user id $this->assertEquals(2, count($result[0]['1']->phonenumbers)); // second user => 1 phonenumber. notice the custom indexing by user id $this->assertEquals(1, count($result[1]['2']->phonenumbers)); // test the custom indexing of the phonenumbers $this->assertTrue(isset($result[0]['1']->phonenumbers['42'])); $this->assertTrue(isset($result[0]['1']->phonenumbers['43'])); $this->assertTrue(isset($result[1]['2']->phonenumbers['91'])); }