diff --git a/lib/Doctrine/ORM/Mapping/AssociationOverride.php b/lib/Doctrine/ORM/Mapping/AssociationOverride.php new file mode 100644 index 000000000..cd149760b --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/AssociationOverride.php @@ -0,0 +1,56 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * This annotation is used to override association mapping of property for an entity relationship. + * + * @author Fabio B. Silva + * @since 2.2 + * + * @Annotation + * @Target({"PROPERTY","ANNOTATION"}) + */ +final class AssociationOverride implements Annotation +{ + + /** + * The name of the relationship property whose mapping is being overridden + * + * @var string + */ + public $name; + + /** + * The join column that is being mapped to the persistent attribute. + * + * @var array<\Doctrine\ORM\Mapping\JoinColumn> + */ + public $joinColumns; + + + /** + * The join table that maps the relationship. + * + * @var \Doctrine\ORM\Mapping\JoinTable + */ + public $joinTable; + +} diff --git a/lib/Doctrine/ORM/Mapping/AssociationOverrides.php b/lib/Doctrine/ORM/Mapping/AssociationOverrides.php new file mode 100644 index 000000000..6cc0d9a41 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/AssociationOverrides.php @@ -0,0 +1,41 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * This annotation is used to override association mappings of relationship properties. + * + * @author Fabio B. Silva + * @since 2.2 + * + * @Annotation + * @Target("CLASS") + */ +final class AssociationOverrides implements Annotation +{ + + /** + * Mapping overrides of relationship properties + * + * @var array<\Doctrine\ORM\Mapping\AssociationOverride> + */ + public $value; + +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 6a89131fe..eae6083f8 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -23,7 +23,8 @@ use Doctrine\Common\Cache\ArrayCache, Doctrine\Common\Annotations\AnnotationReader, Doctrine\Common\Annotations\AnnotationRegistry, Doctrine\ORM\Mapping\ClassMetadataInfo, - Doctrine\ORM\Mapping\MappingException; + Doctrine\ORM\Mapping\MappingException, + Doctrine\ORM\Mapping\JoinColumn; /** * The AnnotationDriver reads the mapping metadata from docblock annotations. @@ -275,6 +276,43 @@ class AnnotationDriver implements Driver } } + $associationOverrides = array(); + // Evaluate AssociationOverrides annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\AssociationOverrides'])) { + $associationOverridesAnnot = $classAnnotations['Doctrine\ORM\Mapping\AssociationOverrides']; + + foreach ($associationOverridesAnnot->value as $associationOverride) { + // Check for JoinColummn/JoinColumns annotations + if ($associationOverride->joinColumns) { + $joinColumns = array(); + foreach ($associationOverride->joinColumns as $joinColumn) { + $joinColumns[] = $this->joinColumnToArray($joinColumn); + } + $associationOverrides[$associationOverride->name]['joinColumns'] = $joinColumns; + } + + // Check for JoinTable annotations + if ($associationOverride->joinTable) { + $joinTable = null; + $joinTableAnnot = $associationOverride->joinTable; + $joinTable = array( + 'name' => $joinTableAnnot->name, + 'schema' => $joinTableAnnot->schema + ); + + foreach ($joinTableAnnot->joinColumns as $joinColumn) { + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + foreach ($joinTableAnnot->inverseJoinColumns as $joinColumn) { + $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + $associationOverrides[$associationOverride->name]['joinTable'] = $joinTable; + } + } + } + // Evaluate InheritanceType annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\InheritanceType'])) { $inheritanceTypeAnnot = $classAnnotations['Doctrine\ORM\Mapping\InheritanceType']; @@ -325,25 +363,13 @@ class AnnotationDriver implements Driver // Check for JoinColummn/JoinColumns annotations $joinColumns = array(); - if ($joinColumnAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumn')) { - $joinColumns[] = array( - 'name' => $joinColumnAnnot->name, - 'referencedColumnName' => $joinColumnAnnot->referencedColumnName, - 'unique' => $joinColumnAnnot->unique, - 'nullable' => $joinColumnAnnot->nullable, - 'onDelete' => $joinColumnAnnot->onDelete, - 'columnDefinition' => $joinColumnAnnot->columnDefinition, - ); + if (isset($associationOverrides[$property->name]['joinColumns'])) { + $joinColumns = $associationOverrides[$property->name]['joinColumns']; + } else if ($joinColumnAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumn')) { + $joinColumns[] = $this->joinColumnToArray($joinColumnAnnot); } else if ($joinColumnsAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumns')) { foreach ($joinColumnsAnnot->value as $joinColumn) { - $joinColumns[] = array( - 'name' => $joinColumn->name, - 'referencedColumnName' => $joinColumn->referencedColumnName, - 'unique' => $joinColumn->unique, - 'nullable' => $joinColumn->nullable, - 'onDelete' => $joinColumn->onDelete, - 'columnDefinition' => $joinColumn->columnDefinition, - ); + $joinColumns[] = $this->joinColumnToArray($joinColumn); } } @@ -440,32 +466,20 @@ class AnnotationDriver implements Driver } else if ($manyToManyAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToMany')) { $joinTable = array(); - if ($joinTableAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinTable')) { + if (isset($associationOverrides[$property->name]['joinTable'])) { + $joinTable = $associationOverrides[$property->name]['joinTable']; + } else if ($joinTableAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinTable')) { $joinTable = array( 'name' => $joinTableAnnot->name, 'schema' => $joinTableAnnot->schema ); foreach ($joinTableAnnot->joinColumns as $joinColumn) { - $joinTable['joinColumns'][] = array( - 'name' => $joinColumn->name, - 'referencedColumnName' => $joinColumn->referencedColumnName, - 'unique' => $joinColumn->unique, - 'nullable' => $joinColumn->nullable, - 'onDelete' => $joinColumn->onDelete, - 'columnDefinition' => $joinColumn->columnDefinition, - ); + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); } foreach ($joinTableAnnot->inverseJoinColumns as $joinColumn) { - $joinTable['inverseJoinColumns'][] = array( - 'name' => $joinColumn->name, - 'referencedColumnName' => $joinColumn->referencedColumnName, - 'unique' => $joinColumn->unique, - 'nullable' => $joinColumn->nullable, - 'onDelete' => $joinColumn->onDelete, - 'columnDefinition' => $joinColumn->columnDefinition, - ); + $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); } } @@ -635,6 +649,25 @@ class AnnotationDriver implements Driver return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode); } + + /** + * Parse the given JoinColumn as array + * + * @param JoinColumn $joinColumn + * @return array + */ + private function joinColumnToArray(JoinColumn $joinColumn) + { + return array( + 'name' => $joinColumn->name, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'columnDefinition' => $joinColumn->columnDefinition, + 'referencedColumnName' => $joinColumn->referencedColumnName, + ); + } + /** * Factory method for the Annotation Driver * diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 46fa1551b..cb911e522 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -59,4 +59,6 @@ require_once __DIR__.'/../EntityResult.php'; require_once __DIR__.'/../NamedNativeQuery.php'; require_once __DIR__.'/../NamedNativeQueries.php'; require_once __DIR__.'/../SqlResultSetMapping.php'; -require_once __DIR__.'/../SqlResultSetMappings.php'; \ No newline at end of file +require_once __DIR__.'/../SqlResultSetMappings.php'; +require_once __DIR__.'/../AssociationOverride.php'; +require_once __DIR__.'/../AssociationOverrides.php'; \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/JoinTable.php b/lib/Doctrine/ORM/Mapping/JoinTable.php index 9ff9d4511..31625ed8f 100644 --- a/lib/Doctrine/ORM/Mapping/JoinTable.php +++ b/lib/Doctrine/ORM/Mapping/JoinTable.php @@ -21,7 +21,7 @@ namespace Doctrine\ORM\Mapping; /** * @Annotation - * @Target("PROPERTY") + * @Target({"PROPERTY","ANNOTATION"}) */ final class JoinTable implements Annotation { diff --git a/tests/Doctrine/Tests/Models/DDC964/DDC964Address.php b/tests/Doctrine/Tests/Models/DDC964/DDC964Address.php new file mode 100644 index 000000000..f5edaf857 --- /dev/null +++ b/tests/Doctrine/Tests/Models/DDC964/DDC964Address.php @@ -0,0 +1,123 @@ +zip = $zip; + $this->country = $country; + $this->city = $city; + $this->street = $street; + } + + /** + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getCountry() + { + return $this->country; + } + + /** + * @param string $country + */ + public function setCountry($country) + { + $this->country = $country; + } + + /** + * @return string + */ + public function getZip() + { + return $this->zip; + } + + /** + * @param string $zip + */ + public function setZip($zip) + { + $this->zip = $zip; + } + + /** + * @return string + */ + public function getCity() + { + return $this->city; + } + + /** + * @param string $city + */ + public function setCity($city) + { + $this->city = $city; + } + + /** + * @return string + */ + public function getStreet() + { + return $this->street; + } + + /** + * @param string $street + */ + public function setStreet($street) + { + $this->street = $street; + } + +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/DDC964/DDC964Admin.php b/tests/Doctrine/Tests/Models/DDC964/DDC964Admin.php new file mode 100644 index 000000000..509297cd9 --- /dev/null +++ b/tests/Doctrine/Tests/Models/DDC964/DDC964Admin.php @@ -0,0 +1,27 @@ +name = $name; + $this->users = new ArrayCollection(); + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param DDC964User $user + */ + public function addUser(DDC964User $user) + { + $this->users[] = $user; + } + + /** + * @return ArrayCollection + */ + public function getUsers() + { + return $this->users; + } + +} + diff --git a/tests/Doctrine/Tests/Models/DDC964/DDC964Guest.php b/tests/Doctrine/Tests/Models/DDC964/DDC964Guest.php new file mode 100644 index 000000000..8cd2c6446 --- /dev/null +++ b/tests/Doctrine/Tests/Models/DDC964/DDC964Guest.php @@ -0,0 +1,14 @@ +name = $name; + $this->groups = new ArrayCollection; + } + + /** + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @param DDC964Group $group + */ + public function addGroup(DDC964Group $group) + { + $this->groups->add($group); + $group->addUser($this); + } + + /** + * @return ArrayCollection + */ + public function getGroups() + { + return $this->groups; + } + + /** + * @return DDC964Address + */ + public function getAddress() + { + return $this->address; + } + + /** + * @param DDC964Address $address + */ + public function setAddress(DDC964Address $address) + { + $this->address = $address; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC964Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC964Test.php new file mode 100644 index 000000000..b250d97f7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC964Test.php @@ -0,0 +1,193 @@ +_em->getClassMetadata('Doctrine\Tests\Models\DDC964\DDC964Admin'); + $guestMetadata = $this->_em->getClassMetadata('Doctrine\Tests\Models\DDC964\DDC964Guest'); + + + // assert groups association mappings + $this->assertArrayHasKey('groups', $guestMetadata->associationMappings); + $this->assertArrayHasKey('groups', $adminMetadata->associationMappings); + + $guestGroups = $guestMetadata->associationMappings['groups']; + $adminGroups = $adminMetadata->associationMappings['groups']; + + $this->assertEquals('ddc964_users_groups', $guestGroups['joinTable']['name']); + $this->assertEquals('user_id', $guestGroups['joinTable']['joinColumns'][0]['name']); + $this->assertEquals('group_id', $guestGroups['joinTable']['inverseJoinColumns'][0]['name']); + + $this->assertEquals(array('user_id'=>'id'), $guestGroups['relationToSourceKeyColumns']); + $this->assertEquals(array('group_id'=>'id'), $guestGroups['relationToTargetKeyColumns']); + $this->assertEquals(array('user_id','group_id'), $guestGroups['joinTableColumns']); + + + $this->assertEquals('ddc964_users_admingroups', $adminGroups['joinTable']['name']); + $this->assertEquals('adminuser_id', $adminGroups['joinTable']['joinColumns'][0]['name']); + $this->assertEquals('admingroup_id', $adminGroups['joinTable']['inverseJoinColumns'][0]['name']); + + $this->assertEquals(array('adminuser_id'=>'id'), $adminGroups['relationToSourceKeyColumns']); + $this->assertEquals(array('admingroup_id'=>'id'), $adminGroups['relationToTargetKeyColumns']); + $this->assertEquals(array('adminuser_id','admingroup_id'), $adminGroups['joinTableColumns']); + + + // assert address association mappings + $this->assertArrayHasKey('address', $guestMetadata->associationMappings); + $this->assertArrayHasKey('address', $adminMetadata->associationMappings); + + $guestAddress = $guestMetadata->associationMappings['address']; + $adminAddress = $adminMetadata->associationMappings['address']; + + $this->assertEquals('address_id', $guestAddress['joinColumns'][0]['name']); + $this->assertEquals(array('address_id'=>'id'), $guestAddress['sourceToTargetKeyColumns']); + $this->assertEquals(array('address_id'=>'address_id'), $guestAddress['joinColumnFieldNames']); + $this->assertEquals(array('id'=>'address_id'), $guestAddress['targetToSourceKeyColumns']); + + + $this->assertEquals('adminaddress_id', $adminAddress['joinColumns'][0]['name']); + $this->assertEquals(array('adminaddress_id'=>'id'), $adminAddress['sourceToTargetKeyColumns']); + $this->assertEquals(array('adminaddress_id'=>'adminaddress_id'), $adminAddress['joinColumnFieldNames']); + $this->assertEquals(array('id'=>'adminaddress_id'), $adminAddress['targetToSourceKeyColumns']); + } + + public function testShouldCreateAndRetrieveOverriddenAssociation() + { + list($admin1,$admin2, $guest1,$guest2) = $this->loadFixtures(); + + $this->_em->clear(); + + $this->assertNotNull($admin1->getId()); + $this->assertNotNull($admin2->getId()); + + $this->assertNotNull($guest1->getId()); + $this->assertNotNull($guest1->getId()); + + + $adminCount = $this->_em + ->createQuery('SELECT COUNT(a) FROM ' . self::NS . '\DDC964Admin a') + ->getSingleScalarResult(); + + $guestCount = $this->_em + ->createQuery('SELECT COUNT(g) FROM ' . self::NS . '\DDC964Guest g') + ->getSingleScalarResult(); + + + $this->assertEquals(2, $adminCount); + $this->assertEquals(2, $guestCount); + + + $admin1 = $this->_em->find(self::NS . '\DDC964Admin', $admin1->getId()); + $admin2 = $this->_em->find(self::NS . '\DDC964Admin', $admin2->getId()); + + $guest1 = $this->_em->find(self::NS . '\DDC964Guest', $guest1->getId()); + $guest2 = $this->_em->find(self::NS . '\DDC964Guest', $guest2->getId()); + + + $this->assertUser($admin1, self::NS . '\DDC964Admin', '11111-111', 2); + $this->assertUser($admin2, self::NS . '\DDC964Admin', '22222-222', 2); + + $this->assertUser($guest1, self::NS . '\DDC964Guest', '33333-333', 2); + $this->assertUser($guest2, self::NS . '\DDC964Guest', '44444-444', 1); + } + + + /** + * @param DDC964User $user + * @param string $addressZip + * @param integer $groups + */ + private function assertUser(DDC964User $user, $className, $addressZip, $groups) + { + $this->assertInstanceOf($className, $user); + $this->assertInstanceOf(self::NS . '\DDC964User', $user); + $this->assertInstanceOf(self::NS . '\DDC964Address', $user->getAddress()); + $this->assertEquals($addressZip, $user->getAddress()->getZip()); + $this->assertEquals($groups, $user->getGroups()->count()); + } + + private function createSchemaDDC964() + { + try { + + $this->_schemaTool->createSchema(array( + $this->_em->getClassMetadata(self::NS . '\DDC964Address'), + $this->_em->getClassMetadata(self::NS . '\DDC964Group'), + $this->_em->getClassMetadata(self::NS . '\DDC964Guest'), + $this->_em->getClassMetadata(self::NS . '\DDC964Admin'), + )); + } catch (\Exception $exc) { + + } + } + + /** + * @return array + */ + private function loadFixtures() + { + $this->createSchemaDDC964(); + + $group1 = new DDC964Group('Foo Admin Group'); + $group2 = new DDC964Group('Bar Admin Group'); + $group3 = new DDC964Group('Foo Guest Group'); + $group4 = new DDC964Group('Bar Guest Group'); + + $this->_em->persist($group1); + $this->_em->persist($group2); + $this->_em->persist($group3); + $this->_em->persist($group4); + + $this->_em->flush(); + + + $admin1 = new DDC964Admin('Admin 1'); + $admin2 = new DDC964Admin('Admin 2'); + $guest1 = new DDC964Guest('Guest 1'); + $guest2 = new DDC964Guest('Guest 2'); + + + $admin1->setAddress(new DDC964Address('11111-111', 'Some Country', 'Some City', 'Some Street')); + $admin2->setAddress(new DDC964Address('22222-222', 'Some Country', 'Some City', 'Some Street')); + $guest1->setAddress(new DDC964Address('33333-333', 'Some Country', 'Some City', 'Some Street')); + $guest2->setAddress(new DDC964Address('44444-444', 'Some Country', 'Some City', 'Some Street')); + + + $admin1->addGroup($group1); + $admin1->addGroup($group2); + $admin2->addGroup($group1); + $admin2->addGroup($group2); + + $guest1->addGroup($group3); + $guest1->addGroup($group4); + $guest2->addGroup($group2); + + $this->_em->persist($admin1); + $this->_em->persist($admin2); + $this->_em->persist($guest1); + $this->_em->persist($guest2); + + $this->_em->flush(); + + return array($admin1,$admin2, $guest1,$guest2); + } + +} \ No newline at end of file