[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.
This commit is contained in:
parent
85d1707a2b
commit
68436fee75
@ -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>
|
||||
|
||||
|
@ -1115,7 +1115,7 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
$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 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
}
|
||||
}
|
||||
|
||||
$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']));
|
||||
|
@ -412,6 +412,7 @@ class AnnotationDriver implements Driver
|
||||
$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')) {
|
||||
|
@ -402,6 +402,10 @@ class XmlDriver extends AbstractFileDriver
|
||||
$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'})) {
|
||||
|
@ -451,6 +451,10 @@ class YamlDriver extends AbstractFileDriver
|
||||
$mapping['indexBy'] = $manyToManyElement['indexBy'];
|
||||
}
|
||||
|
||||
if (isset($manyToManyElement['orphanRemoval'])) {
|
||||
$mapping['orphanRemoval'] = (bool)$manyToManyElement['orphanRemoval'];
|
||||
}
|
||||
|
||||
$metadata->mapManyToMany($mapping);
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,8 @@ final class ManyToMany implements Annotation
|
||||
public $cascade;
|
||||
/** @var string */
|
||||
public $fetch = 'LAZY';
|
||||
/** @var boolean */
|
||||
public $orphanRemoval = false;
|
||||
/** @var string */
|
||||
public $indexBy;
|
||||
}
|
||||
|
@ -388,7 +388,7 @@ final class PersistentCollection implements Collection
|
||||
$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 @@ final class PersistentCollection implements Collection
|
||||
$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 @@ final class PersistentCollection implements Collection
|
||||
|
||||
$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();
|
||||
|
103
tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1654Test.php
Normal file
103
tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1654Test.php
Normal file
@ -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;
|
||||
}
|
@ -377,6 +377,7 @@ class ClassMetadataBuilderTest extends \Doctrine\Tests\OrmTestCase
|
||||
array(
|
||||
'user_id' => 'id',
|
||||
),
|
||||
'orphanRemoval' => false,
|
||||
),
|
||||
), $this->cm->associationMappings);
|
||||
}
|
||||
|
@ -38,6 +38,12 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
|
||||
/** 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 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
|
||||
$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.
|
||||
|
Loading…
Reference in New Issue
Block a user