1
0
mirror of synced 2024-12-13 14:56:01 +03:00

Merge branch DDC-117 into master

This commit is contained in:
Benjamin Eberlei 2011-01-02 12:01:05 +01:00
commit 78d4277e4b
26 changed files with 1553 additions and 85 deletions

View File

@ -187,8 +187,9 @@
<xs:element name="sequence-generator" type="orm:sequence-generator" minOccurs="0" maxOccurs="1" />
</xs:sequence>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="xs:NMTOKEN" />
<xs:attribute name="column" type="xs:NMTOKEN" />
<xs:attribute name="association-key" type="xs:boolean" default="false" />
</xs:complexType>
<xs:complexType name="sequence-generator">

View File

@ -49,7 +49,12 @@ class AssignedGenerator extends AbstractIdGenerator
foreach ($idFields as $idField) {
$value = $class->getReflectionProperty($idField)->getValue($entity);
if (isset($value)) {
$identifier[$idField] = $value;
if (is_object($value)) {
// NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
$identifier[$idField] = current($em->getUnitOfWork()->getEntityIdentifier($value));
} else {
$identifier[$idField] = $value;
}
} else {
throw ORMException::entityMissingAssignedId($entity);
}
@ -58,7 +63,12 @@ class AssignedGenerator extends AbstractIdGenerator
$idField = $class->identifier[0];
$value = $class->reflFields[$idField]->getValue($entity);
if (isset($value)) {
$identifier[$idField] = $value;
if (is_object($value)) {
// NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
$identifier[$idField] = current($em->getUnitOfWork()->getEntityIdentifier($value));
} else {
$identifier[$idField] = $value;
}
} else {
throw ORMException::entityMissingAssignedId($entity);
}

View File

@ -190,9 +190,11 @@ abstract class AbstractHydrator
continue;
} else {
// Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
$fieldName = $this->_rsm->metaMappings[$key];
$cache[$key]['isMetaColumn'] = true;
$cache[$key]['fieldName'] = $this->_rsm->metaMappings[$key];
$cache[$key]['fieldName'] = $fieldName;
$cache[$key]['dqlAlias'] = $this->_rsm->columnOwnerMap[$key];
$cache[$key]['isIdentifier'] = $classMetadata->isIdentifier($fieldName);
}
}
@ -203,15 +205,15 @@ abstract class AbstractHydrator
$dqlAlias = $cache[$key]['dqlAlias'];
if ($cache[$key]['isIdentifier']) {
$id[$dqlAlias] .= '|' . $value;
}
if (isset($cache[$key]['isMetaColumn'])) {
$rowData[$dqlAlias][$cache[$key]['fieldName']] = $value;
continue;
}
if ($cache[$key]['isIdentifier']) {
$id[$dqlAlias] .= '|' . $value;
}
$rowData[$dqlAlias][$cache[$key]['fieldName']] = $cache[$key]['type']->convertToPHPValue($value, $this->_platform);
if ( ! isset($nonemptyComponents[$dqlAlias]) && $value !== null) {

View File

@ -317,6 +317,10 @@ class ClassMetadata extends ClassMetadataInfo
$serialized[] = 'isMappedSuperclass';
}
if ($this->containsForeignIdentifier) {
$serialized[] = 'containsForeignIdentifier';
}
if ($this->isVersioned) {
$serialized[] = 'isVersioned';
$serialized[] = 'versionField';

View File

@ -255,7 +255,7 @@ class ClassMetadataInfo
* - <b>scale</b> (integer, optional, schema-only)
* The scale of a decimal column. Only valid if the column type is decimal.
*
* - <b>unique (string, optional, schema-only)</b>
[* - <b>'unique'] (string, optional, schema-only)</b>
* Whether a unique constraint should be generated for the column.
*
* @var array
@ -392,6 +392,15 @@ class ClassMetadataInfo
*/
public $isIdentifierComposite = false;
/**
* READ-ONLY: Flag indicating wheather the identifier/primary key contains at least one foreign key association.
*
* This flag is necessary because some code blocks require special treatment of this cases.
*
* @var boolean
*/
public $containsForeignIdentifier = false;
/**
* READ-ONLY: The ID generator used for generating IDs for this class.
*
@ -710,6 +719,29 @@ class ClassMetadataInfo
$mapping['targetEntity'] = $this->namespace . '\\' . $mapping['targetEntity'];
}
// Complete id mapping
if (isset($mapping['id']) && $mapping['id'] === true) {
if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'] == true) {
throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->name, $mapping['fieldName']);
}
if ( ! in_array($mapping['fieldName'], $this->identifier)) {
if (count($mapping['joinColumns']) >= 2) {
throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
$mapping['targetEntity'], $this->name, $mapping['fieldName']
);
}
$this->identifier[] = $mapping['fieldName'];
$this->containsForeignIdentifier = true;
}
// Check for composite key
if ( ! $this->isIdentifierComposite && count($this->identifier) > 1) {
$this->isIdentifierComposite = true;
}
}
// Mandatory attributes for both sides
// Mandatory: fieldName, targetEntity
if ( ! isset($mapping['fieldName'])) {
throw MappingException::missingFieldName();
@ -729,6 +761,10 @@ class ClassMetadataInfo
} else {
$mapping['isOwningSide'] = false;
}
if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & ClassMetadata::TO_MANY) {
throw MappingException::illegalToManyIdentifierAssoaction($this->name, $mapping['fieldName']);
}
// Fetch mode. Default fetch mode to LAZY, if not set.
if ( ! isset($mapping['fetch'])) {
@ -779,9 +815,15 @@ class ClassMetadataInfo
'referencedColumnName' => 'id'
));
}
$uniqueContraintColumns = array();
foreach ($mapping['joinColumns'] as $key => &$joinColumn) {
if ($mapping['type'] === self::ONE_TO_ONE) {
$joinColumn['unique'] = true;
if (count($mapping['joinColumns']) == 1) {
$joinColumn['unique'] = true;
} else {
$uniqueContraintColumns[] = $joinColumn['name'];
}
}
if (empty($joinColumn['name'])) {
$joinColumn['name'] = $mapping['fieldName'] . '_id';
@ -793,6 +835,16 @@ class ClassMetadataInfo
$mapping['joinColumnFieldNames'][$joinColumn['name']] = isset($joinColumn['fieldName'])
? $joinColumn['fieldName'] : $joinColumn['name'];
}
if ($uniqueContraintColumns) {
if (!$this->table) {
throw new \RuntimeException("ClassMetadataInfo::setTable() has to be called before defining a one to one relationship.");
}
$this->table['uniqueConstraints'][$mapping['fieldName']."_uniq"] = array(
'columns' => $uniqueContraintColumns
);
}
$mapping['targetToSourceKeyColumns'] = array_flip($mapping['sourceToTargetKeyColumns']);
}
@ -800,6 +852,10 @@ class ClassMetadataInfo
$mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) ?
(bool) $mapping['orphanRemoval'] : false;
if (isset($mapping['id']) && $mapping['id'] === true && !$mapping['isOwningSide']) {
throw MappingException::illegalInverseIdentifierAssocation($this->name, $mapping['fieldName']);
}
return $mapping;
}
@ -990,11 +1046,19 @@ class ClassMetadataInfo
if ($this->isIdentifierComposite) {
$columnNames = array();
foreach ($this->identifier as $idField) {
$columnNames[] = $this->fieldMappings[$idField]['columnName'];
if (isset($this->associationMappings[$idField])) {
// no composite pk as fk entity assumption:
$columnNames[] = $this->associationMappings[$idField]['joinColumns'][0]['name'];
} else {
$columnNames[] = $this->fieldMappings[$idField]['columnName'];
}
}
return $columnNames;
} else {
} else if(isset($this->fieldMappings[$this->identifier[0]])) {
return array($this->fieldMappings[$this->identifier[0]]['columnName']);
} else {
// no composite pk as fk entity assumption:
return array($this->associationMappings[$this->identifier[0]]['joinColumns'][0]['name']);
}
}
@ -1530,6 +1594,74 @@ class ClassMetadataInfo
! ($this->associationMappings[$fieldName]['type'] & self::TO_ONE);
}
/**
* Is this an association that only has a single join column?
*
* @param string $fieldName
* @return bool
*/
public function isAssociationWithSingleJoinColumn($fieldName)
{
return (
isset($this->associationMappings[$fieldName]) &&
isset($this->associationMappings[$fieldName]['joinColumns'][0]) &&
!isset($this->associationMappings[$fieldName]['joinColumns'][1])
);
}
/**
* Return the single association join column (if any).
*
* @param string $fieldName
* @return string
*/
public function getSingleAssociationJoinColumnName($fieldName)
{
if (!$this->isAssociationWithSingleJoinColumn($fieldName)) {
throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
}
return $this->associationMappings[$fieldName]['joinColumns'][0]['name'];
}
/**
* Return the single association referenced join column name (if any).
*
* @param string $fieldName
* @return string
*/
public function getSingleAssociationReferencedJoinColumnName($fieldName)
{
if (!$this->isAssociationWithSingleJoinColumn($fieldName)) {
throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
}
return $this->associationMappings[$fieldName]['joinColumns'][0]['referencedColumnName'];
}
/**
* Used to retrieve a fieldname for either field or association from a given column,
*
* This method is used in foreign-key as primary-key contexts.
*
* @param string $columnName
* @return string
*/
public function getFieldForColumn($columnName)
{
if (isset($this->fieldNames[$columnName])) {
return $this->fieldNames[$columnName];
} else {
foreach ($this->associationMappings AS $assocName => $mapping) {
if ($this->isAssociationWithSingleJoinColumn($assocName) &&
$this->associationMappings[$assocName]['joinColumns'][0]['name'] == $columnName) {
return $assocName;
}
}
throw MappingException::noFieldNameFoundForColumn($this->name, $columnName);
}
}
/**
* Sets the ID generator used to generate IDs for instances of this class.
*
@ -1601,4 +1733,4 @@ class ClassMetadataInfo
{
$this->versionField = $versionField;
}
}
}

View File

@ -288,6 +288,10 @@ class AnnotationDriver implements Driver
throw MappingException::tableIdGeneratorNotImplemented($className);
}
} else if ($oneToOneAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToOne')) {
if ($idAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) {
$mapping['id'] = true;
}
$mapping['targetEntity'] = $oneToOneAnnot->targetEntity;
$mapping['joinColumns'] = $joinColumns;
$mapping['mappedBy'] = $oneToOneAnnot->mappedBy;
@ -309,6 +313,10 @@ class AnnotationDriver implements Driver
$metadata->mapOneToMany($mapping);
} else if ($manyToOneAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToOne')) {
if ($idAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) {
$mapping['id'] = true;
}
$mapping['joinColumns'] = $joinColumns;
$mapping['cascade'] = $manyToOneAnnot->cascade;
$mapping['inversedBy'] = $manyToOneAnnot->inversedBy;

View File

@ -194,7 +194,13 @@ class XmlDriver extends AbstractFileDriver
}
// Evaluate <id ...> mappings
$associationIds = array();
foreach ($xmlRoot->id as $idElement) {
if ((bool)$idElement['association-key'] == true) {
$associationIds[(string)$idElement['fieldName']] = true;
continue;
}
$mapping = array(
'id' => true,
'fieldName' => (string)$idElement['name'],
@ -235,6 +241,10 @@ class XmlDriver extends AbstractFileDriver
'targetEntity' => (string)$oneToOneElement['target-entity']
);
if (isset($associationIds[$mapping['fieldName']])) {
$mapping['id'] = true;
}
if (isset($oneToOneElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string)$oneToOneElement['fetch']);
}
@ -311,6 +321,10 @@ class XmlDriver extends AbstractFileDriver
'targetEntity' => (string)$manyToOneElement['target-entity']
);
if (isset($associationIds[$mapping['fieldName']])) {
$mapping['id'] = true;
}
if (isset($manyToOneElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string)$manyToOneElement['fetch']);
}

View File

@ -135,9 +135,15 @@ class YamlDriver extends AbstractFileDriver
}
}
$associationIds = array();
if (isset($element['id'])) {
// Evaluate identifier settings
foreach ($element['id'] as $name => $idElement) {
if (isset($idElement['associationKey']) && $idElement['associationKey'] == true) {
$associationIds[$name] = true;
continue;
}
if (!isset($idElement['type'])) {
throw MappingException::propertyTypeIsRequired($className, $name);
}
@ -234,6 +240,10 @@ class YamlDriver extends AbstractFileDriver
'targetEntity' => $oneToOneElement['targetEntity']
);
if (isset($associationIds[$mapping['fieldName']])) {
$mapping['id'] = true;
}
if (isset($oneToOneElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $oneToOneElement['fetch']);
}
@ -303,6 +313,10 @@ class YamlDriver extends AbstractFileDriver
'targetEntity' => $manyToOneElement['targetEntity']
);
if (isset($associationIds[$mapping['fieldName']])) {
$mapping['id'] = true;
}
if (isset($manyToOneElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $manyToOneElement['fetch']);
}

View File

@ -231,4 +231,43 @@ class MappingException extends \Doctrine\ORM\ORMException
{
return new self("It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass '".$className."#".$field."'.");
}
/**
* @param string $className
* @param string $targetEntity
* @param string $targetField
* @return self
*/
public static function cannotMapCompositePrimaryKeyEntitiesAsForeignId($className, $targetEntity, $targetField)
{
return new self("It is not possible to map entity '".$className."' with a composite primary key ".
"as part of the primary key of another entity '".$targetEntity."#".$targetField."'.");
}
public static function noSingleAssociationJoinColumnFound($className, $field)
{
return new self("'$className#$field' is not an association with a single join column.");
}
public static function noFieldNameFoundForColumn($className, $column)
{
return new self("Cannot find a field on '$className' that is mapped to column '$column'. Either the ".
"field does not exist or an association exists but it has multiple join columns.");
}
public static function illegalOrphanRemovalOnIdentifierAssociation($className, $field)
{
return new self("The orphan removal option is not allowed on an association that is ".
"part of the identifier in '$className#$field'.");
}
public static function illegalInverseIdentifierAssocation($className, $field)
{
return new self("An inverse association is not allowed to be identifier in '$className#$field'.");
}
public static function illegalToManyIdentifierAssoaction($className, $field)
{
return new self("Many-to-many or one-to-many associations are not allowed to be identifier in '$className#$field'.");
}
}

View File

@ -313,9 +313,16 @@ class BasicEntityPersister
$where = array();
$id = $this->_em->getUnitOfWork()->getEntityIdentifier($entity);
foreach ($this->_class->identifier as $idField) {
$where[] = $this->_class->getQuotedColumnName($idField, $this->_platform);
$params[] = $id[$idField];
$types[] = $this->_class->fieldMappings[$idField]['type'];
if (isset($this->_class->associationMappings[$idField])) {
$targetMapping = $this->_em->getClassMetadata($this->_class->associationMappings[$idField]['targetEntity']);
$where[] = $this->_class->associationMappings[$idField]['joinColumns'][0]['name'];
$params[] = $id[$idField];
$types[] = $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
} else {
$where[] = $this->_class->getQuotedColumnName($idField, $this->_platform);
$params[] = $id[$idField];
$types[] = $this->_class->fieldMappings[$idField]['type'];
}
}
if ($versioned) {
@ -466,6 +473,8 @@ class BasicEntityPersister
foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) {
if ($newVal === null) {
$result[$owningTable][$sourceColumn] = null;
} else if ($targetClass->containsForeignIdentifier) {
$result[$owningTable][$sourceColumn] = $newValId[$targetClass->getFieldForColumn($targetColumn)];
} else {
$result[$owningTable][$sourceColumn] = $newValId[$targetClass->fieldNames[$targetColumn]];
}
@ -716,13 +725,21 @@ class BasicEntityPersister
* @param PersistentCollection $coll The collection to fill.
*/
public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
{
{
$criteria = array();
$sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
$joinTableConditions = array();
if ($assoc['isOwningSide']) {
foreach ($assoc['relationToSourceKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) {
if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
if ($sourceClass->containsForeignIdentifier) {
$field = $sourceClass->getFieldForColumn($sourceKeyColumn);
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
if (isset($sourceClass->associationMappings[$field])) {
$value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
$value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]];
}
$criteria[$relationKeyColumn] = $value;
} else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
$criteria[$relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
} else {
throw MappingException::joinColumnMustPointToMappedField(
@ -734,7 +751,15 @@ class BasicEntityPersister
$owningAssoc = $this->_em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']];
// TRICKY: since the association is inverted source and target are flipped
foreach ($owningAssoc['relationToTargetKeyColumns'] as $relationKeyColumn => $sourceKeyColumn) {
if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
if ($sourceClass->containsForeignIdentifier) {
$field = $sourceClass->getFieldForColumn($sourceKeyColumn);
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
if (isset($sourceClass->associationMappings[$field])) {
$value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
$value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]];
}
$criteria[$relationKeyColumn] = $value;
} else if (isset($sourceClass->fieldNames[$sourceKeyColumn])) {
$criteria[$relationKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
} else {
throw MappingException::joinColumnMustPointToMappedField(
@ -910,7 +935,22 @@ class BasicEntityPersister
$columnList .= $this->_getSelectColumnSQL($field, $this->_class);
}
$this->_selectColumnListSql = $columnList . $this->_getSelectJoinColumnsSQL($this->_class);
foreach ($this->_class->associationMappings as $assoc) {
if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) {
if ($columnList) $columnList .= ', ';
$columnAlias = $srcColumn . $this->_sqlAliasCounter++;
$columnList .= $this->_getSQLTableAlias($this->_class->name) . ".$srcColumn AS $columnAlias";
$resultColumnName = $this->_platform->getSQLResultCasing($columnAlias);
if ( ! isset($this->_resultColumnNames[$resultColumnName])) {
$this->_resultColumnNames[$resultColumnName] = $srcColumn;
}
}
}
}
$this->_selectColumnListSql = $columnList;
return $this->_selectColumnListSql;
}
@ -937,8 +977,15 @@ class BasicEntityPersister
$joinSql = '';
foreach ($joinClauses as $joinTableColumn => $sourceColumn) {
if ($joinSql != '') $joinSql .= ' AND ';
if ($this->_class->containsForeignIdentifier && !isset($this->_class->fieldNames[$sourceColumn])) {
$quotedColumn = $sourceColumn; // join columns cannot be quoted
} else {
$quotedColumn = $this->_class->getQuotedColumnName($this->_class->fieldNames[$sourceColumn], $this->_platform);
}
$joinSql .= $this->_getSQLTableAlias($this->_class->name) .
'.' . $this->_class->getQuotedColumnName($this->_class->fieldNames[$sourceColumn], $this->_platform) . ' = '
'.' . $quotedColumn . ' = '
. $joinTableName . '.' . $joinTableColumn;
}
@ -1023,33 +1070,6 @@ class BasicEntityPersister
return "$sql AS $columnAlias";
}
/**
* Gets the SQL snippet for all join columns of the given class that are to be
* placed in an SQL SELECT statement.
*
* @param $class
* @return string
* @todo Not reused... inline?
*/
private function _getSelectJoinColumnsSQL(ClassMetadata $class)
{
$sql = '';
foreach ($class->associationMappings as $assoc) {
if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
foreach ($assoc['targetToSourceKeyColumns'] as $srcColumn) {
$columnAlias = $srcColumn . $this->_sqlAliasCounter++;
$sql .= ', ' . $this->_getSQLTableAlias($this->_class->name) . ".$srcColumn AS $columnAlias";
$resultColumnName = $this->_platform->getSQLResultCasing($columnAlias);
if ( ! isset($this->_resultColumnNames[$resultColumnName])) {
$this->_resultColumnNames[$resultColumnName] = $srcColumn;
}
}
}
}
return $sql;
}
/**
* Gets the SQL table alias for the given class name.
*
@ -1137,7 +1157,6 @@ class BasicEntityPersister
} else {
$conditionSql .= $this->_getSQLTableAlias($this->_class->name) . '.';
}
$conditionSql .= $this->_class->associationMappings[$field]['joinColumns'][0]['name'];
} else if ($assoc !== null) {
@ -1169,7 +1188,17 @@ class BasicEntityPersister
$owningAssoc = $this->_class->associationMappings[$assoc['mappedBy']];
$sourceClass = $this->_em->getClassMetadata($assoc['sourceEntity']);
foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
$criteria[$targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
if ($sourceClass->containsForeignIdentifier) {
$field = $sourceClass->getFieldForColumn($sourceKeyColumn);
$value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
if (isset($sourceClass->associationMappings[$field])) {
$value = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
$value = $value[$this->_em->getClassMetadata($assoc['targetEntity'])->identifier[0]];
}
$criteria[$targetKeyColumn] = $value;
} else {
$criteria[$targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
}
}
$sql = $this->_getSelectEntitiesSQL($criteria, $assoc);

View File

@ -117,13 +117,21 @@ class ManyToManyPersister extends AbstractCollectionPersister
foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
if (isset($mapping['relationToSourceKeyColumns'][$joinTableColumn])) {
if ($isComposite) {
$params[] = $identifier1[$class1->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]];
if ($class1->containsForeignIdentifier) {
$params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])];
} else {
$params[] = $identifier1[$class1->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]];
}
} else {
$params[] = array_pop($identifier1);
}
} else {
if ($isComposite) {
$params[] = $identifier2[$class2->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]];
if ($class2->containsForeignIdentifier) {
$params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])];
} else {
$params[] = $identifier2[$class2->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]];
}
} else {
$params[] = array_pop($identifier2);
}

View File

@ -738,7 +738,11 @@ class SqlWalker implements TreeWalker
}
}
// 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.
if ($assoc['type'] & ClassMetadata::TO_ONE) {
$sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ';
$first = true;
@ -746,15 +750,19 @@ class SqlWalker implements TreeWalker
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($relation['isOwningSide']) {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform);
$sql .= $sourceTableAlias . '.' . $sourceColumn
. ' = '
. $targetTableAlias . '.' . $quotedTargetColumn;
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$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;
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn;
}
}
} else if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) {
@ -768,17 +776,25 @@ class SqlWalker implements TreeWalker
foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
$sql .= $sourceTableAlias . '.' . $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform)
. ' = '
. $joinTableAlias . '.' . $relationColumn;
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$sourceColumn])) {
$quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $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;
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
}
@ -792,17 +808,25 @@ class SqlWalker implements TreeWalker
foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
$sql .= $targetTableAlias . '.' . $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform)
. ' = '
. $joinTableAlias . '.' . $relationColumn;
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $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;
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$sourceColumn])) {
$quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$sourceColumn], $this->_platform);
}
$sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
}
}
@ -970,6 +994,29 @@ class SqlWalker implements TreeWalker
$this->_rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name);
}
if ($class->containsForeignIdentifier) {
// Add double entry for association identifier columns to simplify hydrator code
foreach ($class->identifier AS $idField) {
if (isset($class->associationMappings[$idField])) {
if (isset($mapping['inherited'])) {
$tableName = $this->_em->getClassMetadata($mapping['inherited'])->table['name'];
} else {
$tableName = $class->table['name'];
}
if ($beginning) $beginning = false; else $sql .= ', ';
$joinColumnName = $class->associationMappings[$idField]['joinColumns'][0]['name'];
$sqlTableAlias = $this->getSqlTableAlias($tableName, $dqlAlias);
$columnAlias = $this->getSqlColumnAlias($joinColumnName);
$sql .= $sqlTableAlias . '.' . $joinColumnName . ' AS ' . $columnAlias;
$columnAlias = $this->_platform->getSQLResultCasing($columnAlias);
$this->_rsm->addMetaResult($dqlAlias, $columnAlias, $idField);
}
}
}
// Add any additional fields of subclasses (excluding inherited fields)
// 1) on Single Table Inheritance: always, since its marginal overhead
// 2) on Class Table Inheritance only if partial objects are disallowed,

View File

@ -199,6 +199,22 @@ class SchemaTool
$this->_gatherRelationsSql($class, $table, $schema);
}
$pkColumns = array();
foreach ($class->identifier AS $identifierField) {
if (isset($class->fieldMappings[$identifierField])) {
$pkColumns[] = $class->getQuotedColumnName($identifierField, $this->_platform);
} else if (isset($class->associationMappings[$identifierField])) {
/* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
$assoc = $class->associationMappings[$identifierField];
foreach ($assoc['joinColumns'] AS $joinColumn) {
$pkColumns[] = $joinColumn['name'];
}
}
}
if (!$table->hasIndex('primary')) {
$table->setPrimaryKey($pkColumns);
}
if (isset($class->table['indexes'])) {
foreach ($class->table['indexes'] AS $indexName => $indexData) {
$table->addIndex($indexData['columns'], $indexName);
@ -285,10 +301,11 @@ class SchemaTool
$pkColumns[] = $class->getQuotedColumnName($mapping['fieldName'], $this->_platform);
}
}
// For now, this is a hack required for single table inheritence, since this method is called
// twice by single table inheritence relations
if(!$table->hasIndex('primary')) {
$table->setPrimaryKey($pkColumns);
//$table->setPrimaryKey($pkColumns);
}
return $columns;
@ -409,13 +426,47 @@ class SchemaTool
}
}
/**
* Get the class metadata that is responsible for the definition of the referenced column name.
*
* Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
* not a simple field, go through all identifier field names that are associations recursivly and
* find that referenced column name.
*
* TODO: Is there any way to make this code more pleasing?
*
* @param ClassMetadata $class
* @param string $referencedColumnName
* @return array(ClassMetadata, referencedFieldName)
*/
private function getDefiningClass($class, $referencedColumnName)
{
$referencedFieldName = $class->getFieldName($referencedColumnName);
if ($class->hasField($referencedFieldName)) {
return array($class, $referencedFieldName);
} else if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) {
// it seems to be an entity as foreign key
foreach ($class->getIdentifierFieldNames() AS $fieldName) {
if ($class->hasAssociation($fieldName) && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
return $this->getDefiningClass(
$this->_em->getClassMetadata($class->associationMappings[$fieldName]['targetEntity']),
$class->getSingleAssociationReferencedJoinColumnName($fieldName)
);
}
}
}
return null;
}
/**
* Gather columns and fk constraints that are required for one part of relationship.
*
* @param array $joinColumns
* @param \Doctrine\DBAL\Schema\Table $theJoinTable
* @param ClassMetadata $class
* @param \Doctrine\ORM\Mapping\AssociationMapping $mapping
* @param array $mapping
* @param array $primaryKeyColumns
* @param array $uniqueConstraints
*/
@ -424,12 +475,13 @@ class SchemaTool
$localColumns = array();
$foreignColumns = array();
$fkOptions = array();
$foreignTableName = $class->getTableName();
foreach ($joinColumns as $joinColumn) {
$columnName = $joinColumn['name'];
$referencedFieldName = $class->getFieldName($joinColumn['referencedColumnName']);
list($definingClass, $referencedFieldName) = $this->getDefiningClass($class, $joinColumn['referencedColumnName']);
if ( ! $class->hasField($referencedFieldName)) {
if (!$definingClass) {
throw new \Doctrine\ORM\ORMException(
"Column name `".$joinColumn['referencedColumnName']."` referenced for relation from ".
$mapping['sourceEntity'] . " towards ". $mapping['targetEntity'] . " does not exist."
@ -445,7 +497,7 @@ class SchemaTool
// It might exist already if the foreign key is mapped into a regular
// property as well.
$fieldMapping = $class->getFieldMapping($referencedFieldName);
$fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
$columnDef = null;
if (isset($joinColumn['columnDefinition'])) {
@ -464,9 +516,7 @@ class SchemaTool
$columnOptions['precision'] = $fieldMapping['precision'];
}
$theJoinTable->addColumn(
$columnName, $class->getTypeOfColumn($joinColumn['referencedColumnName']), $columnOptions
);
$theJoinTable->addColumn($columnName, $fieldMapping['type'], $columnOptions);
}
if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
@ -483,7 +533,7 @@ class SchemaTool
}
$theJoinTable->addUnnamedForeignKeyConstraint(
$class->getTableName(), $localColumns, $foreignColumns, $fkOptions
$foreignTableName, $localColumns, $foreignColumns, $fkOptions
);
}

View File

@ -1837,11 +1837,19 @@ class UnitOfWork implements PropertyChangedListener
if ($class->isIdentifierComposite) {
$id = array();
foreach ($class->identifier as $fieldName) {
$id[$fieldName] = $data[$fieldName];
if (isset($class->associationMappings[$fieldName])) {
$id[$fieldName] = $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']];
} else {
$id[$fieldName] = $data[$fieldName];
}
}
$idHash = implode(' ', $id);
} else {
$idHash = $data[$class->identifier[0]];
if (isset($class->associationMappings[$class->identifier[0]])) {
$idHash = $data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']];
} else {
$idHash = $data[$class->identifier[0]];
}
$id = array($class->identifier[0] => $idHash);
}
@ -1894,7 +1902,11 @@ class UnitOfWork implements PropertyChangedListener
foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) {
$joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null;
if ($joinColumnValue !== null) {
$associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
if ($targetClass->containsForeignIdentifier) {
$associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue;
} else {
$associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
}
}
}
if ( ! $associatedId) {

View File

@ -0,0 +1,65 @@
<?php
namespace Doctrine\Tests\Models\DDC117;
/**
* @Entity
*/
class DDC117ApproveChanges
{
/**
* @Id @Column(type="integer") @GeneratedValue
*/
private $id;
/**
* @ManyToOne(targetEntity="DDC117ArticleDetails")
* @JoinColumn(name="details_id", referencedColumnName="article_id")
*/
private $articleDetails;
/**
* @ManyToOne(targetEntity="DDC117Reference")
* @JoinColumns({
* @JoinColumn(name="source_id", referencedColumnName="source_id"),
* @JoinColumn(name="target_id", referencedColumnName="target_id")
* })
*/
private $reference;
/**
* @ManyToOne(targetEntity="DDC117Translation")
* @JoinColumns({
* @JoinColumn(name="trans_article_id", referencedColumnName="article_id"),
* @JoinColumn(name="trans_language", referencedColumnName="language")
* })
*/
private $translation;
public function __construct($details, $reference, $translation)
{
$this->articleDetails = $details;
$this->reference = $reference;
$this->translation = $translation;
}
public function getId()
{
return $this->id;
}
public function getArticleDetails()
{
return $this->articleDetails;
}
public function getReference()
{
return $this->reference;
}
public function getTranslation()
{
return $this->translation;
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Doctrine\Tests\Models\DDC117;
/**
* @Entity
*/
class DDC117Article
{
/** @Id @Column(type="integer", name="article_id") @GeneratedValue */
private $id;
/** @Column */
private $title;
/**
* @OneToMany(targetEntity="DDC117Reference", mappedBy="source", cascade={"remove"})
*/
private $references;
/**
* @OneToOne(targetEntity="DDC117ArticleDetails", mappedBy="article", cascade={"persist", "remove"})
*/
private $details;
/**
* @OneToMany(targetEntity="DDC117Translation", mappedBy="article", cascade={"persist", "remove"})
*/
private $translations;
public function __construct($title)
{
$this->title = $title;
$this->references = new \Doctrine\Common\Collections\ArrayCollection();
$this->translations = new \Doctrine\Common\Collections\ArrayCollection();
}
public function setDetails($details)
{
$this->details = $details;
}
public function id()
{
return $this->id;
}
public function addReference($reference)
{
$this->references[] = $reference;
}
public function references()
{
return $this->references;
}
public function addTranslation($language, $title)
{
$this->translations[] = new DDC117Translation($this, $language, $title);
}
public function getText()
{
return $this->details->getText();
}
public function getDetails()
{
return $this->details;
}
public function resetText()
{
$this->details = null;
}
public function getTranslations()
{
return $this->translations;
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace Doctrine\Tests\Models\DDC117;
/**
* @Entity
*/
class DDC117ArticleDetails
{
/**
* @Id
* @OneToOne(targetEntity="DDC117Article", inversedBy="details")
* @JoinColumn(name="article_id", referencedColumnName="article_id")
*/
private $article;
/**
* @Column(type="text")
*/
private $text;
public function __construct($article, $text)
{
$this->article = $article;
$article->setDetails($this);
$this->update($text);
}
public function update($text)
{
$this->text = $text;
}
public function getText()
{
return $this->text;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Doctrine\Tests\Models\DDC117;
/**
* @Entity
*/
class DDC117Editor
{
/**
* @Id @Column(type="integer") @GeneratedValue
*/
public $id;
/**
* @Column(type="string")
*/
public $name;
/**
* @ManyToMany(targetEntity="DDC117Translation", inversedBy="reviewedByEditors")
* @JoinTable(
* inverseJoinColumns={
* @JoinColumn(name="article_id", referencedColumnName="article_id"),
* @JoinColumn(name="language", referencedColumnName="language")
* },
* joinColumns={
* @JoinColumn(name="editor_id", referencedColumnName="id")
* }
* )
*/
public $reviewingTranslations;
/**
* @ManyToOne(targetEntity="DDC117Translation", inversedBy="lastTranslatedBy")
* @JoinColumns({
* @JoinColumn(name="lt_article_id", referencedColumnName="article_id"),
* @JoinColumn(name="lt_language", referencedColumnName="language")
* })
*/
public $lastTranslation;
public function __construct($name = "")
{
$this->name = $name;
$this->reviewingTranslations = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addLastTranslation(DDC117Translation $t)
{
$this->lastTranslation = $t;
$t->lastTranslatedBy[] = $this;
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Doctrine\Tests\Models\DDC117;
/**
* @Entity
*/
class DDC117Reference
{
/**
* @Id
* @ManyToOne(targetEntity="DDC117Article", inversedBy="references")
* @JoinColumn(name="source_id", referencedColumnName="article_id")
*/
private $source;
/**
* @Id
* @ManyToOne(targetEntity="DDC117Article", inversedBy="references")
* @JoinColumn(name="target_id", referencedColumnName="article_id")
*/
private $target;
/**
* @column(type="string")
*/
private $description;
/**
* @column(type="datetime")
*/
private $created;
public function __construct($source, $target, $description)
{
$source->addReference($this);
$target->addReference($this);
$this->source = $source;
$this->target = $target;
$this->description = $description;
$this->created = new \DateTime("now");
}
public function source()
{
return $this->source;
}
public function target()
{
return $this->target;
}
public function setDescription($desc)
{
$this->description = $desc;
}
public function getDescription()
{
return $this->description;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Doctrine\Tests\Models\DDC117;
/**
* @Entity
*/
class DDC117Translation
{
/**
* @Id
* @ManyToOne(targetEntity="DDC117Article")
* @JoinColumn(name="article_id", referencedColumnName="article_id")
*/
private $article;
/**
* @Id @column(type="string")
*/
private $language;
/**
* @column(type="string")
*/
private $title;
/**
* @ManyToMany(targetEntity="DDC117Editor", mappedBy="reviewingTranslations")
*/
public $reviewedByEditors;
/**
* @OneToMany(targetEntity="DDC117Editor", mappedBy="lastTranslation")
*/
public $lastTranslatedBy;
public function __construct($article, $language, $title)
{
$this->article = $article;
$this->language = $language;
$this->title = $title;
$this->reviewedByEditors = new \Doctrine\Common\Collections\ArrayCollection();
$this->lastTranslatedBy = new \Doctrine\Common\Collections\ArrayCollection();
}
public function getArticleId()
{
return $this->article->id();
}
public function getLanguage()
{
return $this->language;
}
public function getLastTranslatedBy()
{
return $this->lastTranslatedBy;
}
public function getReviewedByEditors()
{
return $this->reviewedByEditors;
}
}

View File

@ -0,0 +1,410 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Tests\Models\DDC117\DDC117ArticleDetails;
use Doctrine\Tests\Models\DDC117\DDC117Article;
use Doctrine\Tests\Models\DDC117\DDC117Reference;
use Doctrine\Tests\Models\DDC117\DDC117Translation;
use Doctrine\Tests\Models\DDC117\DDC117ApproveChanges;
use Doctrine\Tests\Models\DDC117\DDC117Editor;
require_once __DIR__ . '/../../../TestInit.php';
class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase
{
private $article1;
private $article2;
private $reference;
private $translation;
private $articleDetails;
protected function setUp() {
$this->useModelSet('ddc117');
parent::setUp();
$this->article1 = new DDC117Article("Foo");
$this->article2 = new DDC117Article("Bar");
$this->_em->persist($this->article1);
$this->_em->persist($this->article2);
$this->_em->flush();
$this->reference = new DDC117Reference($this->article1, $this->article2, "Test-Description");
$this->_em->persist($this->reference);
$this->translation = new DDC117Translation($this->article1, "en", "Bar");
$this->_em->persist($this->translation);
$this->articleDetails = new DDC117ArticleDetails($this->article1, "Very long text");
$this->_em->persist($this->articleDetails);
$this->_em->flush();
$this->_em->clear();
}
/**
* @group DDC-117
*/
public function testAssociationOnlyCompositeKey()
{
$idCriteria = array('source' => $this->article1->id(), 'target' => $this->article2->id());
$mapRef = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria);
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Reference", $mapRef);
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Article", $mapRef->target());
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Article", $mapRef->source());
$this->assertSame($mapRef, $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria));
$this->_em->clear();
$dql = "SELECT r, s FROM "."Doctrine\Tests\Models\DDC117\DDC117Reference r JOIN r.source s WHERE r.source = ?1";
$dqlRef = $this->_em->createQuery($dql)->setParameter(1, 1)->getSingleResult();
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Reference", $mapRef);
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Article", $mapRef->target());
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Article", $mapRef->source());
$this->assertSame($dqlRef, $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria));
$this->_em->clear();
$dql = "SELECT r, s FROM "."Doctrine\Tests\Models\DDC117\DDC117Reference r JOIN r.source s WHERE s.title = ?1";
$dqlRef = $this->_em->createQuery($dql)->setParameter(1, 'Foo')->getSingleResult();
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Reference", $dqlRef);
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Article", $dqlRef->target());
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Article", $dqlRef->source());
$this->assertSame($dqlRef, $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria));
$dql = "SELECT r, s FROM "."Doctrine\Tests\Models\DDC117\DDC117Reference r JOIN r.source s WHERE s.title = ?1";
$dqlRef = $this->_em->createQuery($dql)->setParameter(1, 'Foo')->getSingleResult();
$this->_em->contains($dqlRef);
}
/**
* @group DDC-117
*/
public function testUpdateAssocationEntity()
{
$idCriteria = array('source' => $this->article1->id(), 'target' => $this->article2->id());
$mapRef = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria);
$this->assertNotNull($mapRef);
$mapRef->setDescription("New Description!!");
$this->_em->flush();
$this->_em->clear();
$mapRef = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria);
$this->assertEquals('New Description!!', $mapRef->getDescription());
}
/**
* @group DDC-117
*/
public function testFetchDql()
{
$dql = "SELECT r, s FROM "."Doctrine\Tests\Models\DDC117\DDC117Reference r JOIN r.source s WHERE s.title = ?1";
$refs = $this->_em->createQuery($dql)->setParameter(1, 'Foo')->getResult();
$this->assertTrue(count($refs) > 0, "Has to contain at least one Reference.");
foreach ($refs AS $ref) {
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Reference", $ref, "Contains only Reference instances.");
$this->assertTrue($this->_em->contains($ref), "Contains Reference in the IdentityMap.");
}
}
/**
* @group DDC-117
*/
public function testRemoveCompositeElement()
{
$idCriteria = array('source' => $this->article1->id(), 'target' => $this->article2->id());
$refRep = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria);
$this->_em->remove($refRep);
$this->_em->flush();
$this->_em->clear();
$this->assertNull($this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria));
}
/**
* @group DDC-117
*/
public function testDqlRemoveCompositeElement()
{
$idCriteria = array('source' => $this->article1->id(), 'target' => $this->article2->id());
$dql = "DELETE "."Doctrine\Tests\Models\DDC117\DDC117Reference r WHERE r.source = ?1 AND r.target = ?2";
$this->_em->createQuery($dql)
->setParameter(1, $this->article1->id())
->setParameter(2, $this->article2->id())
->execute();
$this->assertNull($this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria));
}
/**
* @group DDC-117
*/
public function testInverseSideAccess()
{
$this->article1 = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Article", $this->article1->id());
$this->assertEquals(1, count($this->article1->references()));
foreach ($this->article1->references() AS $this->reference) {
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Reference", $this->reference);
$this->assertSame($this->article1, $this->reference->source());
}
$this->_em->clear();
$dql = 'SELECT a, r FROM '. 'Doctrine\Tests\Models\DDC117\DDC117Article a INNER JOIN a.references r WHERE a.id = ?1';
$articleDql = $this->_em->createQuery($dql)
->setParameter(1, $this->article1->id())
->getSingleResult();
$this->assertEquals(1, count($this->article1->references()));
foreach ($this->article1->references() AS $this->reference) {
$this->assertType("Doctrine\Tests\Models\DDC117\DDC117Reference", $this->reference);
$this->assertSame($this->article1, $this->reference->source());
}
}
/**
* @group DDC-117
*/
public function testMixedCompositeKey()
{
$idCriteria = array('article' => $this->article1->id(), 'language' => 'en');
$this->translation = $this->_em->find('Doctrine\Tests\Models\DDC117\DDC117Translation', $idCriteria);
$this->assertType('Doctrine\Tests\Models\DDC117\DDC117Translation', $this->translation);
$this->assertSame($this->translation, $this->_em->find('Doctrine\Tests\Models\DDC117\DDC117Translation', $idCriteria));
$this->_em->clear();
$dql = 'SELECT t, a FROM ' . 'Doctrine\Tests\Models\DDC117\DDC117Translation t JOIN t.article a WHERE t.article = ?1 AND t.language = ?2';
$dqlTrans = $this->_em->createQuery($dql)
->setParameter(1, $this->article1->id())
->setParameter(2, 'en')
->getSingleResult();
$this->assertType('Doctrine\Tests\Models\DDC117\DDC117Translation', $this->translation);
}
/**
* @group DDC-117
*/
public function testMixedCompositeKeyViolateUniqueness()
{
$this->article1 = $this->_em->find('Doctrine\Tests\Models\DDC117\DDC117Article', $this->article1->id());
$this->article1->addTranslation('en', 'Bar');
$this->article1->addTranslation('en', 'Baz');
$this->setExpectedException('Exception');
$this->_em->flush();
}
/**
* @group DDC-117
*/
public function testOneToOneForeignObjectId()
{
$this->article1 = new DDC117Article("Foo");
$this->_em->persist($this->article1);
$this->_em->flush();
$this->articleDetails = new DDC117ArticleDetails($this->article1, "Very long text");
$this->_em->persist($this->articleDetails);
$this->_em->flush();
$this->articleDetails->update("not so very long text!");
$this->_em->flush();
$this->_em->clear();
/* @var $article DDC117Article */
$article = $this->_em->find(get_class($this->article1), $this->article1->id());
$this->assertEquals('not so very long text!', $article->getText());
}
/**
* @group DDC-117
*/
public function testOneToOneCascadeRemove()
{
$article = $this->_em->find(get_class($this->article1), $this->article1->id());
$this->_em->remove($article);
$this->_em->flush();
$this->assertFalse($this->_em->contains($article->getDetails()));
}
/**
* @group DDC-117
*/
public function testOneToOneCascadePersist()
{
if (!$this->_em->getConnection()->getDatabasePlatform()->prefersSequences()) {
$this->markTestSkipped('Test only works with databases that prefer sequences as ID strategy.');
}
$this->article1 = new DDC117Article("Foo");
$this->articleDetails = new DDC117ArticleDetails($this->article1, "Very long text");
$this->_em->persist($this->article1);
$this->_em->flush();
}
/**
* @group DDC-117
*/
public function testReferencesToForeignKeyEntities()
{
$idCriteria = array('source' => $this->article1->id(), 'target' => $this->article2->id());
$reference = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Reference", $idCriteria);
$idCriteria = array('article' => $this->article1->id(), 'language' => 'en');
$translation = $this->_em->find('Doctrine\Tests\Models\DDC117\DDC117Translation', $idCriteria);
$approveChanges = new DDC117ApproveChanges($reference->source()->getDetails(), $reference, $translation);
$this->_em->persist($approveChanges);
$this->_em->flush();
$this->_em->clear();
$approveChanges = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117ApproveChanges", $approveChanges->getId());
$this->assertType('Doctrine\Tests\Models\DDC117\DDC117ArticleDetails', $approveChanges->getArticleDetails());
$this->assertType('Doctrine\Tests\Models\DDC117\DDC117Reference', $approveChanges->getReference());
$this->assertType('Doctrine\Tests\Models\DDC117\DDC117Translation', $approveChanges->getTranslation());
}
/**
* @group DDC-117
*/
public function testLoadOneToManyCollectionOfForeignKeyEntities()
{
/* @var $article DDC117Article */
$article = $this->_em->find(get_class($this->article1), $this->article1->id());
$translations = $article->getTranslations();
$this->assertFalse($translations->isInitialized());
$this->assertContainsOnly('Doctrine\Tests\Models\DDC117\DDC117Translation', $translations);
$this->assertTrue($translations->isInitialized());
}
/**
* @group DDC-117
*/
public function testLoadManyToManyCollectionOfForeignKeyEntities()
{
$editor = $this->loadEditorFixture();
$this->assertFalse($editor->reviewingTranslations->isInitialized());
$this->assertContainsOnly("Doctrine\Tests\Models\DDC117\DDC117Translation", $editor->reviewingTranslations);
$this->assertTrue($editor->reviewingTranslations->isInitialized());
$this->_em->clear();
$dql = "SELECT e, t FROM Doctrine\Tests\Models\DDC117\DDC117Editor e JOIN e.reviewingTranslations t WHERE e.id = ?1";
$editor = $this->_em->createQuery($dql)->setParameter(1, $editor->id)->getSingleResult();
$this->assertTrue($editor->reviewingTranslations->isInitialized());
$this->assertContainsOnly("Doctrine\Tests\Models\DDC117\DDC117Translation", $editor->reviewingTranslations);
}
/**
* @group DDC-117
*/
public function testClearManyToManyCollectionOfForeignKeyEntities()
{
$editor = $this->loadEditorFixture();
$this->assertEquals(3, count($editor->reviewingTranslations));
$editor->reviewingTranslations->clear();
$this->_em->flush();
$this->_em->clear();
$editor = $this->_em->find(get_class($editor), $editor->id);
$this->assertEquals(0, count($editor->reviewingTranslations));
}
/**
* @group DDC-117
*/
public function testLoadInverseManyToManyCollection()
{
$editor = $this->loadEditorFixture();
$this->assertType('Doctrine\Tests\Models\DDC117\DDC117Translation', $editor->reviewingTranslations[0]);
$reviewedBy = $editor->reviewingTranslations[0]->getReviewedByEditors();
$this->assertEquals(1, count($reviewedBy));
$this->assertSame($editor, $reviewedBy[0]);
$this->_em->clear();
$dql = "SELECT t, e FROM Doctrine\Tests\Models\DDC117\DDC117Translation t ".
"JOIN t.reviewedByEditors e WHERE t.article = ?1 AND t.language = ?2";
$trans = $this->_em->createQuery($dql)
->setParameter(1, $this->translation->getArticleId())
->setParameter(2, $this->translation->getLanguage())
->getSingleResult();
$this->assertType('Doctrine\Tests\Models\DDC117\DDC117Translation', $trans);
$this->assertContainsOnly('Doctrine\Tests\Models\DDC117\DDC117Editor', $trans->reviewedByEditors);
$this->assertEquals(1, count($trans->reviewedByEditors));
}
/**
* @group DDC-117
*/
public function testLoadOneToManyOfSourceEntityWithAssociationIdentifier()
{
$editor = $this->loadEditorFixture();
$editor->addLastTranslation($editor->reviewingTranslations[0]);
$this->_em->flush();
$this->_em->clear();
$editor = $this->_em->find(get_class($editor), $editor->id);
$lastTranslatedBy = $editor->reviewingTranslations[0]->getLastTranslatedBy();
$lastTranslatedBy->count();
$this->assertEquals(1, count($lastTranslatedBy));
}
/**
* @return DDC117Editor
*/
private function loadEditorFixture()
{
$editor = new DDC117Editor("beberlei");
/* @var $article1 DDC117Article */
$article1 = $this->_em->find(get_class($this->article1), $this->article1->id());
foreach ($article1->getTranslations() AS $translation) {
$editor->reviewingTranslations[] = $translation;
}
/* @var $article2 DDC117Article */
$article2 = $this->_em->find(get_class($this->article2), $this->article2->id());
$article2->addTranslation("de", "Vanille-Krapferl"); // omnomnom
$article2->addTranslation("fr", "Sorry can't speak french!");
foreach ($article2->getTranslations() AS $translation) {
$this->_em->persist($translation); // otherwise persisting the editor won't work, reachability!
$editor->reviewingTranslations[] = $translation;
}
$this->_em->persist($editor);
$this->_em->flush();
$this->_em->clear();
return $this->_em->find(get_class($editor), $editor->id);
}
}

View File

@ -0,0 +1,215 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
require_once __DIR__ . '/../../../TestInit.php';
class DDC881Test extends \Doctrine\Tests\OrmFunctionalTestCase
{
protected function setUp()
{
parent::setUp();
try {
$this->_schemaTool->createSchema(array(
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC881User'),
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC881Phonenumber'),
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC881Phonecall'),
));
} catch (\Exception $e) {
}
}
/**
* @group DDC-117
* @group DDC-881
*/
public function testIssue()
{
/* Create two test users: albert and alfons */
$albert = new DDC881User;
$albert->setName("albert");
$this->_em->persist($albert);
$alfons = new DDC881User;
$alfons->setName("alfons");
$this->_em->persist($alfons);
$this->_em->flush();
/* Assign two phone numbers to each user */
$phoneAlbert1 = new DDC881PhoneNumber();
$phoneAlbert1->setUser($albert);
$phoneAlbert1->setId(1);
$phoneAlbert1->setPhoneNumber("albert home: 012345");
$this->_em->persist($phoneAlbert1);
$phoneAlbert2 = new DDC881PhoneNumber();
$phoneAlbert2->setUser($albert);
$phoneAlbert2->setId(2);
$phoneAlbert2->setPhoneNumber("albert mobile: 67890");
$this->_em->persist($phoneAlbert2);
$phoneAlfons1 = new DDC881PhoneNumber();
$phoneAlfons1->setId(1);
$phoneAlfons1->setUser($alfons);
$phoneAlfons1->setPhoneNumber("alfons home: 012345");
$this->_em->persist($phoneAlfons1);
$phoneAlfons2 = new DDC881PhoneNumber();
$phoneAlfons2->setId(2);
$phoneAlfons2->setUser($alfons);
$phoneAlfons2->setPhoneNumber("alfons mobile: 67890");
$this->_em->persist($phoneAlfons2);
/* We call alfons and albert once on their mobile numbers */
$call1 = new DDC881PhoneCall();
$call1->setPhoneNumber($phoneAlfons2);
$this->_em->persist($call1);
$call2 = new DDC881PhoneCall();
$call2->setPhoneNumber($phoneAlbert2);
$this->_em->persist($call2);
$this->_em->flush();
$this->_em->clear();
// fetch-join that foreign-key/primary-key entity association
$dql = "SELECT c, p FROM " . __NAMESPACE__ . "\DDC881PhoneCall c JOIN c.phonenumber p";
$calls = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($calls));
$this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $calls[0]->getPhoneNumber());
$this->assertNotInstanceOf('Doctrine\ORM\Proxy\Proxy', $calls[1]->getPhoneNumber());
$dql = "SELECT p, c FROM " . __NAMESPACE__ . "\DDC881PhoneNumber p JOIN p.calls c";
$numbers = $this->_em->createQuery($dql)->getResult();
$this->assertEquals(2, count($numbers));
$this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $numbers[0]->getCalls());
$this->assertTrue($numbers[0]->getCalls()->isInitialized());
}
}
/**
* @Entity
*/
class DDC881User
{
/**
* @Id
* @Column(type="integer")
* @GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @Column(type="string")
*/
private $name;
/**
* @OneToMany(targetEntity="DDC881PhoneNumber",mappedBy="id")
*/
private $phoneNumbers;
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
}
/**
* @Entity
*/
class DDC881PhoneNumber
{
/**
* @Id
* @Column(type="integer")
*/
private $id;
/**
* @Id
* @ManyToOne(targetEntity="DDC881User",cascade={"all"})
*/
private $user;
/**
* @Column(type="string")
*/
private $phonenumber;
/**
* @OneToMany(targetEntity="DDC881PhoneCall", mappedBy="phonenumber")
*/
private $calls;
public function __construct()
{
$this->calls = new \Doctrine\Common\Collections\ArrayCollection();
}
public function setId($id)
{
$this->id = $id;
}
public function setUser(DDC881User $user)
{
$this->user = $user;
}
public function setPhoneNumber($phoneNumber)
{
$this->phonenumber = $phoneNumber;
}
public function getCalls()
{
return $this->calls;
}
}
/**
* @Entity
*/
class DDC881PhoneCall
{
/**
* @Id
* @Column(type="integer")
* @GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ManyToOne(targetEntity="DDC881PhoneNumber", inversedBy="calls", cascade={"all"})
* @JoinColumns({
* @JoinColumn(name="phonenumber_id", referencedColumnName="id"),
* @JoinColumn(name="user_id", referencedColumnName="user_id")
* })
*/
private $phonenumber;
/**
* @Column(type="string",nullable=true)
*/
private $callDate;
public function setPhoneNumber(DDC881PhoneNumber $phoneNumber)
{
$this->phonenumber = $phoneNumber;
}
public function getPhoneNumber()
{
return $this->phonenumber;
}
}

View File

@ -312,4 +312,71 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase
$this->assertEquals('doctrineglobal_article_cmsuser', $cm->associationMappings['author']['joinTable']['name']);
}
}
/**
* @group DDC-117
*/
public function testMapIdentifierAssociation()
{
$cm = new ClassMetadata('Doctrine\Tests\Models\DDC117\DDC117ArticleDetails');
$cm->mapOneToOne(array(
'fieldName' => 'article',
'id' => true,
'targetEntity' => 'Doctrine\Tests\Models\DDC117\DDC117Article',
'joinColumns' => array(),
));
$this->assertTrue($cm->containsForeignIdentifier, "Identifier Association should set 'containsForeignIdentifier' boolean flag.");
$this->assertEquals(array("article"), $cm->identifier);
}
/**
* @group DDC-117
*/
public function testOrphanRemovalIdentifierAssociation()
{
$cm = new ClassMetadata('Doctrine\Tests\Models\DDC117\DDC117ArticleDetails');
$this->setExpectedException('Doctrine\ORM\Mapping\MappingException', 'The orphan removal option is not allowed on an association that');
$cm->mapOneToOne(array(
'fieldName' => 'article',
'id' => true,
'targetEntity' => 'Doctrine\Tests\Models\DDC117\DDC117Article',
'orphanRemoval' => true,
'joinColumns' => array(),
));
}
/**
* @group DDC-117
*/
public function testInverseIdentifierAssocation()
{
$cm = new ClassMetadata('Doctrine\Tests\Models\DDC117\DDC117ArticleDetails');
$this->setExpectedException('Doctrine\ORM\Mapping\MappingException', 'An inverse association is not allowed to be identifier in');
$cm->mapOneToOne(array(
'fieldName' => 'article',
'id' => true,
'mappedBy' => 'details', // INVERSE!
'targetEntity' => 'Doctrine\Tests\Models\DDC117\DDC117Article',
'joinColumns' => array(),
));
}
/**
* @group DDC-117
*/
public function testIdentifierAssocationManyToMany()
{
$cm = new ClassMetadata('Doctrine\Tests\Models\DDC117\DDC117ArticleDetails');
$this->setExpectedException('Doctrine\ORM\Mapping\MappingException', 'Many-to-many or one-to-many associations are not allowed to be identifier in');
$cm->mapManyToMany(array(
'fieldName' => 'article',
'id' => true,
'targetEntity' => 'Doctrine\Tests\Models\DDC117\DDC117Article',
'joinColumns' => array(),
));
}
}

View File

@ -495,6 +495,22 @@ class LanguageRecognitionTest extends \Doctrine\Tests\OrmTestCase
{
$this->assertInvalidDQL('SELECT g FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN u.groups g');
}
/**
* @group DDC-117
*/
public function testSizeOfForeignKeyOneToManyPrimaryKeyEntity()
{
$this->assertValidDQL("SELECT a, t FROM Doctrine\Tests\Models\DDC117\DDC117Article a JOIN a.translations t WHERE SIZE(a.translations) > 0");
}
/**
* @group DDC-117
*/
public function testSizeOfForeignKeyManyToManyPrimaryKeyEntity()
{
$this->assertValidDQL("SELECT e, t FROM Doctrine\Tests\Models\DDC117\DDC117Editor e JOIN e.reviewingTranslations t WHERE SIZE(e.reviewingTranslations) > 0");
}
}
/** @Entity */

View File

@ -91,6 +91,14 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
'Doctrine\Tests\Models\DirectoryTree\File',
'Doctrine\Tests\Models\DirectoryTree\Directory',
),
'ddc117' => array(
'Doctrine\Tests\Models\DDC117\DDC117Article',
'Doctrine\Tests\Models\DDC117\DDC117Reference',
'Doctrine\Tests\Models\DDC117\DDC117Translation',
'Doctrine\Tests\Models\DDC117\DDC117ArticleDetails',
'Doctrine\Tests\Models\DDC117\DDC117ApproveChanges',
'Doctrine\Tests\Models\DDC117\DDC117Editor',
),
);
protected function useModelSet($setName)
@ -173,6 +181,16 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
$conn->executeUpdate('DELETE FROM Directory WHERE parentDirectory_id IS NOT NULL');
$conn->executeUpdate('DELETE FROM Directory');
}
if (isset($this->_usedModelSets['ddc117'])) {
return;
$conn->executeUpdate('DELETE FROM ddc117editor_ddc117translation');
$conn->executeUpdate('DELETE FROM DDC117Editor');
$conn->executeUpdate('DELETE FROM DDC117ApproveChanges');
$conn->executeUpdate('DELETE FROM DDC117Reference');
$conn->executeUpdate('DELETE FROM DDC117ArticleDetails');
$conn->executeUpdate('DELETE FROM DDC117Translation');
$conn->executeUpdate('DELETE FROM DDC117Article');
}
$this->_em->clear();
}

View File

@ -93,6 +93,10 @@ class TestUtil
'driver' => 'pdo_sqlite',
'memory' => true
);
if (isset($GLOBALS['db_path'])) {
$params['path'] = $GLOBALS['db_path'];
unlink($GLOBALS['db_path']);
}
$conn = \Doctrine\DBAL\DriverManager::getConnection($params);
}