Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

[DDC-1654] Add support for orphanRemoval on ManyToMany associations. …

…This only makes sense when ManyToMany is used as uni-directional OneToMany association with join table. The join column has a unique constraint on it to enforce this on the DB level, but we dont validate that this actually happens. Foreign Key constraints help prevent issues and notify developers early if they use it wrong.
  • Loading branch information...
commit 68436fee757a0b069429d3b496dabb6d9480afe1 1 parent 85d1707
@beberlei beberlei authored
View
1  doctrine-mapping.xsd
@@ -371,6 +371,7 @@
<xs:attribute name="index-by" type="xs:NMTOKEN" />
<xs:attribute name="inversed-by" type="xs:NMTOKEN" />
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />
+ <xs:attribute name="orphan-removal" type="xs:boolean" default="false" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
View
4 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -1115,7 +1115,7 @@ protected function _validateAndCompleteAssociationMapping(array $mapping)
$mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\');
}
- if ( ($mapping['type'] & (self::MANY_TO_ONE|self::MANY_TO_MANY)) > 0 &&
+ if ( ($mapping['type'] & self::MANY_TO_ONE) > 0 &&
isset($mapping['orphanRemoval']) &&
$mapping['orphanRemoval'] == true) {
@@ -1335,6 +1335,8 @@ protected function _validateAndCompleteManyToManyMapping(array $mapping)
}
}
+ $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) ? (bool) $mapping['orphanRemoval'] : false;
+
if (isset($mapping['orderBy'])) {
if ( ! is_array($mapping['orderBy'])) {
throw new \InvalidArgumentException("'orderBy' is expected to be an array, not ".gettype($mapping['orderBy']));
View
1  lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
@@ -412,6 +412,7 @@ public function loadMetadataForClass($className, ClassMetadataInfo $metadata)
$mapping['inversedBy'] = $manyToManyAnnot->inversedBy;
$mapping['cascade'] = $manyToManyAnnot->cascade;
$mapping['indexBy'] = $manyToManyAnnot->indexBy;
+ $mapping['orphanRemoval'] = $manyToManyAnnot->orphanRemoval;
$mapping['fetch'] = $this->getFetchMode($className, $manyToManyAnnot->fetch);
if ($orderByAnnot = $this->_reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) {
View
4 lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
@@ -402,6 +402,10 @@ public function loadMetadataForClass($className, ClassMetadataInfo $metadata)
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string)$manyToManyElement['fetch']);
}
+ if (isset($manyToManyElement['orphan-removal'])) {
+ $mapping['orphanRemoval'] = (bool)$manyToManyElement['orphan-removal'];
+ }
+
if (isset($manyToManyElement['mapped-by'])) {
$mapping['mappedBy'] = (string)$manyToManyElement['mapped-by'];
} else if (isset($manyToManyElement->{'join-table'})) {
View
4 lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
@@ -451,6 +451,10 @@ public function loadMetadataForClass($className, ClassMetadataInfo $metadata)
$mapping['indexBy'] = $manyToManyElement['indexBy'];
}
+ if (isset($manyToManyElement['orphanRemoval'])) {
+ $mapping['orphanRemoval'] = (bool)$manyToManyElement['orphanRemoval'];
+ }
+
$metadata->mapManyToMany($mapping);
}
}
View
2  lib/Doctrine/ORM/Mapping/ManyToMany.php
@@ -35,6 +35,8 @@
public $cascade;
/** @var string */
public $fetch = 'LAZY';
+ /** @var boolean */
+ public $orphanRemoval = false;
/** @var string */
public $indexBy;
}
View
6 lib/Doctrine/ORM/PersistentCollection.php
@@ -388,7 +388,7 @@ public function remove($key)
$this->changed();
if ($this->association !== null &&
- $this->association['type'] == ClassMetadata::ONE_TO_MANY &&
+ $this->association['type'] & ClassMetadata::TO_MANY &&
$this->association['orphanRemoval']) {
$this->em->getUnitOfWork()->scheduleOrphanRemoval($removed);
}
@@ -426,7 +426,7 @@ public function removeElement($element)
$this->changed();
if ($this->association !== null &&
- $this->association['type'] === ClassMetadata::ONE_TO_MANY &&
+ $this->association['type'] & ClassMetadata::TO_MANY &&
$this->association['orphanRemoval']) {
$this->em->getUnitOfWork()->scheduleOrphanRemoval($element);
}
@@ -631,7 +631,7 @@ public function clear()
$uow = $this->em->getUnitOfWork();
- if ($this->association['type'] === ClassMetadata::ONE_TO_MANY && $this->association['orphanRemoval']) {
+ if ($this->association['type'] & ClassMetadata::TO_MANY && $this->association['orphanRemoval']) {
// we need to initialize here, as orphan removal acts like implicit cascadeRemove,
// hence for event listeners we need the objects in memory.
$this->initialize();
View
103 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1654Test.php
@@ -0,0 +1,103 @@
+<?php
+
+namespace Doctrine\Tests\ORM\Functional\Ticket;
+
+/**
+ * @group DDC-1654
+ */
+class DDC1654Test extends \Doctrine\Tests\OrmFunctionalTestCase
+{
+ public function setUp()
+ {
+ parent::setUp();
+ $this->setUpEntitySchema(array(
+ __NAMESPACE__ . '\\DDC1654Post',
+ __NAMESPACE__ . '\\DDC1654Comment',
+ ));
+ }
+
+ public function testManyToManyRemoveFromCollectionOrphanRemoval()
+ {
+ $post = new DDC1654Post();
+ $post->comments[] = new DDC1654Comment();
+ $post->comments[] = new DDC1654Comment();
+
+ $this->_em->persist($post);
+ $this->_em->flush();
+
+ $post->comments->remove(0);
+ $post->comments->remove(1);
+
+ $this->_em->flush();
+ $this->_em->clear();
+
+ $comments = $this->_em->getRepository(__NAMESPACE__ . '\\DDC1654Comment')->findAll();
+ $this->assertEquals(0, count($comments));
+ }
+
+ public function testManyToManyRemoveElementFromCollectionOrphanRemoval()
+ {
+ $post = new DDC1654Post();
+ $post->comments[] = new DDC1654Comment();
+ $post->comments[] = new DDC1654Comment();
+
+ $this->_em->persist($post);
+ $this->_em->flush();
+
+ $post->comments->removeElement($post->comments[0]);
+ $post->comments->removeElement($post->comments[1]);
+
+ $this->_em->flush();
+ $this->_em->clear();
+
+ $comments = $this->_em->getRepository(__NAMESPACE__ . '\\DDC1654Comment')->findAll();
+ $this->assertEquals(0, count($comments));
+ }
+
+ public function testManyToManyClearCollectionOrphanRemoval()
+ {
+ $post = new DDC1654Post();
+ $post->comments[] = new DDC1654Comment();
+ $post->comments[] = new DDC1654Comment();
+
+ $this->_em->persist($post);
+ $this->_em->flush();
+
+ $post->comments->clear();
+
+ $this->_em->flush();
+ $this->_em->clear();
+
+ $comments = $this->_em->getRepository(__NAMESPACE__ . '\\DDC1654Comment')->findAll();
+ $this->assertEquals(0, count($comments));
+
+ }
+}
+
+/**
+ * @Entity
+ */
+class DDC1654Post
+{
+ /**
+ * @Id @Column(type="integer") @GeneratedValue
+ */
+ public $id;
+
+ /**
+ * @ManyToMany(targetEntity="DDC1654Comment", orphanRemoval=true,
+ * cascade={"persist"})
+ */
+ public $comments = array();
+}
+
+/**
+ * @Entity
+ */
+class DDC1654Comment
+{
+ /**
+ * @Id @Column(type="integer") @GeneratedValue
+ */
+ public $id;
+}
View
1  tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php
@@ -377,6 +377,7 @@ public function testCreateManyToMany()
array(
'user_id' => 'id',
),
+ 'orphanRemoval' => false,
),
), $this->cm->associationMappings);
}
View
25 tests/Doctrine/Tests/OrmFunctionalTestCase.php
@@ -38,6 +38,12 @@
/** Whether the database schema has already been created. */
protected static $_tablesCreated = array();
+ /**
+ * Array of entity class name to their tables that were created.
+ * @var array
+ */
+ protected static $_entityTablesCreated = array();
+
/** List of model sets and their classes. */
protected static $_modelSets = array(
'cms' => array(
@@ -235,6 +241,25 @@ protected function tearDown()
$this->_em->clear();
}
+ protected function setUpEntitySchema(array $classNames)
+ {
+ if ($this->_em === null) {
+ throw new \RuntimeException("EntityManager not set, you have to call parent::setUp() before invoking this method.");
+ }
+
+ $classes = array();
+ foreach ($classNames as $className) {
+ if ( ! isset(static::$_entityTablesCreated[$className])) {
+ static::$_entityTablesCreated[$className] = true;
+ $classes[] = $this->_em->getClassMetadata($className);
+ }
+ }
+
+ if ($classes) {
+ $this->_schemaTool->createSchema($classes);
+ }
+ }
+
/**
* Creates a connection to the test database, if there is none yet, and
* creates the necessary tables.
Please sign in to comment.
Something went wrong with that request. Please try again.