Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Merge pull request doctrine#96 from bakura10/automate-collection
Automate collection merging
  • Loading branch information
Ocramius committed Oct 9, 2012
2 parents 185aa3b + af017a4 commit 57ce7c0
Show file tree
Hide file tree
Showing 3 changed files with 257 additions and 18 deletions.
18 changes: 13 additions & 5 deletions src/DoctrineModule/Stdlib/Hydrator/DoctrineObject.php
Expand Up @@ -24,6 +24,7 @@
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use DoctrineModule\Util\CollectionUtils;
use Zend\Stdlib\Hydrator\HydratorInterface;
use Zend\Stdlib\Hydrator\ClassMethods as ClassMethodsHydrator;

Expand Down Expand Up @@ -136,6 +137,13 @@ public function hydrate(array $data, $object)
$value = $this->toOne($value, $target);
} elseif ($this->metadata->isCollectionValuedAssociation($field)) {
$value = $this->toMany($value, $target);

// Automatically merge collections using helper utility
$propertyRefl = $this->metadata->getReflectionClass()->getProperty($field);
$propertyRefl->setAccessible(true);

$previousValue = $propertyRefl->getValue($object);
$value = CollectionUtils::intersectUnion($previousValue, $value);
}
}
}
Expand All @@ -144,8 +152,8 @@ public function hydrate(array $data, $object)
}

/**
* @param mixed $valueOrObject
* @param string $target
* @param mixed $valueOrObject
* @param string $target
* @return object
*/
protected function toOne($valueOrObject, $target)
Expand All @@ -158,9 +166,9 @@ protected function toOne($valueOrObject, $target)
}

/**
* @param mixed $valueOrObject
* @param string $target
* @return array
* @param mixed $valueOrObject
* @param string $target
* @return ArrayCollection
*/
protected function toMany($valueOrObject, $target)
{
Expand Down
12 changes: 9 additions & 3 deletions src/DoctrineModule/Util/CollectionUtils.php
Expand Up @@ -36,14 +36,20 @@ class CollectionUtils
* This function performs a kind of "intersection union" operation, and is useful especially when dealing
* with dynamic forms. For instance, if a collection contains existing elements and a form remove one of those
* elements, this function will return a Collection that contains all the elements from $collection1, minus ones
* that are not present in $collection2
* that are not present in $collection2. This is used internally in the DoctrineModule hydrator, so that the
* work is done for you automatically
*
* @param Collection $collection1
* @param Collection $collection2
* @param Collection $collection1
* @param Collection $collection2
* @return Collection
*/
public static function intersectUnion(Collection $collection1, Collection $collection2)
{
// Don't make the work both
if ($collection1 === $collection2) {
return $collection1;
}

$toRemove = array();

foreach ($collection1 as $key1 => $value1) {
Expand Down
245 changes: 235 additions & 10 deletions tests/DoctrineModuleTest/Stdlib/Hydrator/DoctrineObjectTest.php
Expand Up @@ -4,6 +4,7 @@

use stdClass;
use PHPUnit_Framework_TestCase as BaseTestCase;
use Doctrine\Common\Collections\ArrayCollection;
use DoctrineModule\Stdlib\Hydrator\DoctrineObject as DoctrineObjectHydrator;
use Zend\Stdlib\Hydrator\ObjectProperty as ObjectPropertyHydrator;

Expand Down Expand Up @@ -101,6 +102,14 @@ public function testCanHydrateOneToManyEntity()
)
);

$reflClass = $this->getMock('\ReflectionClass',
array(),
array('Doctrine\Common\Collections\ArrayCollection'));

$reflProperty = $this->getMock('\ReflProperty',
array('setAccessible', 'getValue')
);

$this->metadata->expects($this->exactly(1))
->method('getTypeOfField')
->with($this->equalTo('categories'))
Expand All @@ -126,10 +135,19 @@ public function testCanHydrateOneToManyEntity()
->with($this->equalTo('categories'))
->will($this->returnValue(true));

$categories = array();
$categories[] = new stdClass();
$categories[] = new stdClass();
$categories[] = new stdClass();
$this->metadata->expects($this->exactly(1))
->method('getReflectionClass')
->will($this->returnValue($reflClass));

$reflClass->expects($this->exactly(1))
->method('getProperty')
->with($this->equalTo('categories'))
->will($this->returnValue($reflProperty));

$reflProperty->expects($this->exactly(1))
->method('getValue')
->withAnyParameters()
->will($this->returnValue(new ArrayCollection($data['categories'])));

$this->objectManager->expects($this->exactly(3))
->method('find')
Expand Down Expand Up @@ -215,7 +233,6 @@ public function testHydrateCanFindSingleRelatedObjectByNonScalarIdentifier()
->with($this->equalTo('review'))
->will($this->returnValue(true));


$this->objectManager->expects($this->exactly(1))
->method('find')
->with('Review', $reviewReference)
Expand Down Expand Up @@ -245,6 +262,14 @@ public function testHydrateCanFindMultipleRelatedObjectByNonScalarIdentifier()
),
);

$reflClass = $this->getMock('\ReflectionClass',
array(),
array('Doctrine\Common\Collections\ArrayCollection'));

$reflProperty = $this->getMock('\ReflProperty',
array('setAccessible', 'getValue')
);

$this->metadata->expects($this->exactly(1))
->method('getTypeOfField')
->with($this->equalTo('reviews'))
Expand Down Expand Up @@ -275,11 +300,26 @@ public function testHydrateCanFindMultipleRelatedObjectByNonScalarIdentifier()
->with('Review', $reviewReference)
->will($this->returnValue($review));

$this->metadata->expects($this->exactly(1))
->method('getReflectionClass')
->will($this->returnValue($reflClass));

$reflClass->expects($this->exactly(1))
->method('getProperty')
->with($this->equalTo('reviews'))
->will($this->returnValue($reflProperty));

$reflProperty->expects($this->exactly(1))
->method('getValue')
->withAnyParameters()
->will($this->returnValue(new ArrayCollection($data['reviews'])));

$object = $this->hydrator->hydrate($data, new stdClass());
$this->assertCount(3, $object->reviews);
$this->assertSame($review, $object->reviews[0]);
$this->assertSame($review, $object->reviews[1]);
$this->assertSame($review, $object->reviews[2]);

foreach ($object->reviews as $review) {
$this->assertSame($review, $review);
}
}

public function testHydrateCanHandleSingleRelatedObject()
Expand Down Expand Up @@ -330,6 +370,14 @@ public function testHydrateCanHandleMultipleRelatedObjects()
),
);

$reflClass = $this->getMock('\ReflectionClass',
array(),
array('Doctrine\Common\Collections\ArrayCollection'));

$reflProperty = $this->getMock('\ReflProperty',
array('setAccessible', 'getValue')
);

$this->metadata->expects($this->exactly(1))
->method('getTypeOfField')
->with($this->equalTo('reviews'))
Expand All @@ -355,6 +403,20 @@ public function testHydrateCanHandleMultipleRelatedObjects()
->with($this->equalTo('reviews'))
->will($this->returnValue(true));

$this->metadata->expects($this->exactly(1))
->method('getReflectionClass')
->will($this->returnValue($reflClass));

$reflClass->expects($this->exactly(1))
->method('getProperty')
->with($this->equalTo('reviews'))
->will($this->returnValue($reflProperty));

$reflProperty->expects($this->exactly(1))
->method('getValue')
->withAnyParameters()
->will($this->returnValue(new ArrayCollection($data['reviews'])));

$object = $this->hydrator->hydrate($data, new stdClass());
$this->assertCount(3, $object->reviews);
$this->assertSame($review, $object->reviews[0]);
Expand All @@ -379,6 +441,14 @@ public function testAlwaysRetrieveArrayCollectionForToManyRelationships()
),
);

$reflClass = $this->getMock('\ReflectionClass',
array(),
array('Doctrine\Common\Collections\ArrayCollection'));

$reflProperty = $this->getMock('\ReflProperty',
array('setAccessible', 'getValue')
);

$this->metadata->expects($this->exactly(1))
->method('getTypeOfField')
->with($this->equalTo('reviews'))
Expand All @@ -404,6 +474,20 @@ public function testAlwaysRetrieveArrayCollectionForToManyRelationships()
->with($this->equalTo('reviews'))
->will($this->returnValue(true));

$this->metadata->expects($this->exactly(1))
->method('getReflectionClass')
->will($this->returnValue($reflClass));

$reflClass->expects($this->exactly(1))
->method('getProperty')
->with($this->equalTo('reviews'))
->will($this->returnValue($reflProperty));

$reflProperty->expects($this->exactly(1))
->method('getValue')
->withAnyParameters()
->will($this->returnValue(new ArrayCollection($data['reviews'])));

$this->objectManager->expects($this->exactly(3))
->method('find')
->with('Review', $reviewReference)
Expand All @@ -419,8 +503,6 @@ public function testReturnObjectIfArrayContainIdentifierValues()
$reviewReference = new stdClass();
$reviewReference->uuid = '5678';

$review = new stdClass();

$reviewWithId = new stdClass();
$reviewWithId->id = 5;

Expand Down Expand Up @@ -453,4 +535,147 @@ public function testReturnObjectIfArrayContainIdentifierValues()
$this->assertEquals('5', $object->id);
$this->assertEquals('Michaël Gallego', $object->reviewer);
}

/**
* This data set contains the data that is added to an existing collection. The original collection is always
* the same, that is to say :
* 'categories' => [0 => 'foo', 1 => 'bar', 2 => 'french']
*
* @return array
*/
public function intersectionUnionProvider()
{
$first = new stdClass();
$first->value = 'foo';
$second = new stdClass();
$second->value = 'bar';
$third = new stdClass();
$third->value = 'italian';
$fourth = new stdClass();
$fourth->value = 'umbrella';

return array(
// Same count, but different values
array(
// new collection
array(
'categories' => array(
$first, $second, $third
),
),

// expected merge
array(
'categories' => array(
$first, $second, $third
)
)
),

// Fewer count
array(
// new collection
array(
'categories' => array(
$first, $second
),
),

// expected merge
array(
'categories' => array(
$first, $second
)
)
),

// More count (new elements)
array(
// new collection
array(
'categories' => array(
$first, $second, $third, $fourth
),
),

// expected merge
array(
'categories' => array(
$first, $second, $third, $fourth
)
)
),
);
}

/**
* @dataProvider intersectionUnionProvider
*/
public function testAutomaticallyIntersectUnionCollections(array $data, array $expected)
{
$reflClass = $this->getMock('\ReflectionClass',
array(),
array('Doctrine\Common\Collections\ArrayCollection'));

$reflProperty = $this->getMock('\ReflProperty',
array('setAccessible', 'getValue')
);

$this->metadata->expects($this->exactly(1))
->method('getTypeOfField')
->with($this->equalTo('categories'))
->will($this->returnValue('array'));

$this->metadata->expects($this->exactly(1))
->method('hasAssociation')
->with($this->equalTo('categories'))
->will($this->returnValue(true));

$this->metadata->expects($this->exactly(1))
->method('getAssociationTargetClass')
->with($this->equalTo('categories'))
->will($this->returnValue('stdClass'));

$this->metadata->expects($this->exactly(1))
->method('isSingleValuedAssociation')
->with($this->equalTo('categories'))
->will($this->returnValue(false));

$this->metadata->expects($this->exactly(1))
->method('isCollectionValuedAssociation')
->with($this->equalTo('categories'))
->will($this->returnValue(true));

$this->metadata->expects($this->exactly(1))
->method('getReflectionClass')
->will($this->returnValue($reflClass));

$reflClass->expects($this->exactly(1))
->method('getProperty')
->with($this->equalTo('categories'))
->will($this->returnValue($reflProperty));

$reflProperty->expects($this->exactly(1))
->method('getValue')
->withAnyParameters()
->will($this->returnValue(new ArrayCollection($data)));

// Set an object with pre-defined values (we have to create stdClass as element so that elements are passed
// by reference and not as value, so that we can emulate normal behaviour)
$first = new stdClass();
$first->value = 'foo';
$second = new stdClass();
$second->value = 'bar';
$third = new stdClass();
$third->value = 'french';

$existingObject = new stdClass();
$existingObject->categories = array(
$first, $second, $first
);

$object = $this->hydrator->hydrate($data, $existingObject);
$this->assertEquals(count($expected['categories']), count($object->categories));
$this->assertEquals($expected['categories'], $object->categories->toArray());
}
}

0 comments on commit 57ce7c0

Please sign in to comment.