Skip to content
Browse files

[MODM-105] Adding much improved reference support. Added mappedBy and…

… inversedBy for better performing references instead of always storing references on both sides.
  • Loading branch information...
1 parent 3bf5d44 commit d1bffbd6bb7b7b690e25f1314f1bf6b69f1e41a6 @jwage jwage committed
View
36 lib/Doctrine/ODM/MongoDB/Hydrator/HydratorFactory.php
@@ -203,7 +203,7 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName,
$mapping['fieldName'],
Type::getType($mapping['type'])->closureToPHP()
);
- } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE) {
+ } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
$code .= sprintf(<<<EOF
/** @ReferenceOne */
@@ -222,6 +222,40 @@ private function generateHydratorClass(ClassMetadata $class, $hydratorClassName,
$mapping['name'],
$mapping['fieldName']
);
+ } elseif ($mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isInverseSide']) {
+ if ($mapping['repositoryMethod']) {
+ $code .= sprintf(<<<EOF
+
+ \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
+ \$return = \$this->dm->getRepository(\$className)->%3\$s();
+ \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
+ \$hydratedData['%2\$s'] = \$return;
+
+EOF
+ ,
+ $mapping['name'],
+ $mapping['fieldName'],
+ $mapping['repositoryMethod']
+ );
+ } else {
+ $code .= sprintf(<<<EOF
+
+ \$className = \$this->class->fieldMappings['%2\$s']['targetDocument'];
+ \$criteria = array_merge(
+ array(\$this->class->fieldMappings['%2\$s']['mappedBy'] . '.\$id' => \$data['_id']),
+ isset(\$this->class->fieldMappings['%2\$s']['criteria']) ? \$this->class->fieldMappings['%2\$s']['criteria'] : array()
+ );
+ \$sort = isset(\$this->class->fieldMappings['%2\$s']['sort']) ? \$this->class->fieldMappings['%2\$s']['sort'] : array();
+ \$return = \$this->unitOfWork->getDocumentPersister(\$className)->load(\$criteria, null, array(), 0, \$sort);
+ \$this->class->reflFields['%2\$s']->setValue(\$document, \$return);
+ \$hydratedData['%2\$s'] = \$return;
+
+EOF
+ ,
+ $mapping['name'],
+ $mapping['fieldName']
+ );
+ }
} elseif ($mapping['association'] === ClassMetadata::REFERENCE_MANY || $mapping['association'] === ClassMetadata::EMBED_MANY) {
$code .= sprintf(<<<EOF
View
12 lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php
@@ -974,6 +974,18 @@ public function mapField(array $mapping)
if (isset($mapping['lock'])) {
$this->setLockMapping($mapping);
}
+ $mapping['isOwningSide'] = true;
+ $mapping['isInverseSide'] = false;
+ if (isset($mapping['reference'])) {
+ if (isset($mapping['inversedBy']) && $mapping['inversedBy']) {
+ $mapping['isOwningSide'] = true;
+ $mapping['isInverseSide'] = false;
+ }
+ if (isset($mapping['mappedBy']) && $mapping['mappedBy']) {
+ $mapping['isInverseSide'] = true;
+ $mapping['isOwningSide'] = false;
+ }
+ }
$this->fieldMappings[$mapping['fieldName']] = $mapping;
return $mapping;
}
View
14 lib/Doctrine/ODM/MongoDB/Mapping/Driver/DoctrineAnnotations.php
@@ -176,6 +176,13 @@ class Field extends Annotation
public $discriminatorField;
public $discriminatorMap;
public $cascade;
+ public $inversedBy;
+ public $mappedBy;
+ public $repositoryMethod;
+ public $sort = array();
+ public $criteria = array();
+ public $limit;
+ public $skip;
}
final class ReferenceMany extends Field
{
@@ -185,6 +192,13 @@ class Field extends Annotation
public $discriminatorField;
public $discriminatorMap;
public $cascade;
+ public $inversedBy;
+ public $mappedBy;
+ public $repositoryMethod;
+ public $sort = array();
+ public $criteria = array();
+ public $limit;
+ public $skip;
public $strategy = 'pushAll'; // pushAll, set
}
class NotSaved extends Field {}
View
13 lib/Doctrine/ODM/MongoDB/PersistentCollection.php
@@ -156,7 +156,7 @@ private function changed()
{
if ( ! $this->isDirty) {
$this->isDirty = true;
- if ($this->dm && $this->mapping !== null && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()) {
+ if ($this->dm && $this->mapping !== null && $this->mapping['isOwningSide'] && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()) {
$this->uow->scheduleForDirtyCheck($this->owner);
}
}
@@ -408,6 +408,9 @@ public function getValues()
*/
public function count()
{
+ if ($this->mapping['isInverseSide']) {
+ $this->initialize();
+ }
return count($this->mongoData) + $this->coll->count();
}
@@ -508,9 +511,11 @@ public function clear()
}
$this->mongoData = array();
$this->coll->clear();
- $this->changed();
- $this->uow->scheduleCollectionDeletion($this);
- $this->takeSnapshot();
+ if ($this->mapping['isOwningSide']) {
+ $this->changed();
+ $this->uow->scheduleCollectionDeletion($this);
+ $this->takeSnapshot();
+ }
}
/**
View
8 lib/Doctrine/ODM/MongoDB/Persisters/CollectionPersister.php
@@ -88,6 +88,10 @@ public function __construct(DocumentManager $dm, PersistenceBuilder $pb, UnitOfW
*/
public function delete(PersistentCollection $coll, array $options)
{
+ $mapping = $coll->getMapping();
+ if ($mapping['isInverseSide']) {
+ return; // ignore inverse side
+ }
list($propertyPath, $parent) = $this->getPathAndParent($coll);
$query = array($this->cmd . 'unset' => array($propertyPath => true));
$this->executeQuery($parent, $query, $options);
@@ -101,6 +105,10 @@ public function delete(PersistentCollection $coll, array $options)
*/
public function update(PersistentCollection $coll, array $options)
{
+ $mapping = $coll->getMapping();
+ if ($mapping['isInverseSide']) {
+ return; // ignore inverse side
+ }
$this->deleteRows($coll, $options);
$this->insertRows($coll, $options);
}
View
97 lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
@@ -148,6 +148,16 @@ public function __construct(PersistenceBuilder $pb, DocumentManager $dm, EventMa
$this->collection = $dm->getDocumentCollection($class->name);
}
+ public function getInserts()
+ {
+ return $this->queuedInserts;
+ }
+
+ public function isQueuedForInsert($document)
+ {
+ return isset($this->queuedInserts[spl_object_hash($document)]) ? true : false;
+ }
+
/**
* Adds a document to the queued insertions.
* The document remains queued until {@link executeInserts} is invoked.
@@ -324,13 +334,18 @@ public function refresh($id, $document)
* a new document is created.
* @param array $hints Hints for document creation.
* @param int $lockMode
+ * @param array $sort
* @return object The loaded and managed document instance or NULL if the document can not be found.
* @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
*/
- public function load($criteria, $document = null, array $hints = array(), $lockMode = 0)
+ public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = array())
{
$criteria = $this->prepareQuery($criteria);
- $result = $this->collection->findOne($criteria);
+ $cursor = $this->collection->find($criteria)->limit(1);
+ if ($sort) {
+ $cursor->sort($sort);
+ }
+ $result = $cursor->getSingleResult();
if ($this->class->isLockable) {
$lockMapping = $this->class->fieldMappings[$this->class->lockField];
@@ -457,7 +472,16 @@ public function loadCollection(PersistentCollection $collection)
break;
case ClassMetadata::REFERENCE_MANY:
- $this->loadReferenceManyCollection($collection);
+ $mapping = $collection->getMapping();
+ if ($mapping['repositoryMethod']) {
+ $this->loadReferenceManyWithRepositoryMethod($collection);
+ } else {
+ if ($mapping['isOwningSide']) {
+ $this->loadReferenceManyCollectionOwningSide($collection);
+ } else {
+ $this->loadReferenceManyCollectionInverseSide($collection);
+ }
+ }
break;
}
}
@@ -481,7 +505,7 @@ private function loadEmbedManyCollection(PersistentCollection $collection)
}
}
- private function loadReferenceManyCollection(PersistentCollection $collection)
+ private function loadReferenceManyCollectionOwningSide(PersistentCollection $collection)
{
$mapping = $collection->getMapping();
$cmd = $this->cmd;
@@ -502,8 +526,21 @@ private function loadReferenceManyCollection(PersistentCollection $collection)
foreach ($groupedIds as $className => $ids) {
$class = $this->dm->getClassMetadata($className);
$mongoCollection = $this->dm->getDocumentCollection($className);
- $data = $mongoCollection->find(array('_id' => array($cmd . 'in' => $ids)));
- foreach ($data as $documentData) {
+ $criteria = array_merge(
+ array('_id' => array($cmd . 'in' => $ids)),
+ $mapping['criteria']
+ );
+ $cursor = $mongoCollection->find($criteria);
+ if ($mapping['sort']) {
+ $cursor->sort($mapping['sort']);
+ }
+ if ($mapping['limit']) {
+ $cursor->limit($mapping['limit']);
+ }
+ if ($mapping['skip']) {
+ $cursor->skip($mapping['skip']);
+ }
+ foreach ($cursor as $documentData) {
$document = $this->uow->getById((string) $documentData['_id'], $class->rootDocumentName);
$data = $this->hydratorFactory->hydrate($document, $documentData);
$this->uow->setOriginalDocumentData($document, $data);
@@ -511,6 +548,54 @@ private function loadReferenceManyCollection(PersistentCollection $collection)
}
}
+ private function loadReferenceManyCollectionInverseSide(PersistentCollection $collection)
+ {
+ $mapping = $collection->getMapping();
+ $owner = $collection->getOwner();
+ $class = $this->dm->getClassMetadata($mapping['targetDocument']);
+ $mongoCollection = $this->dm->getDocumentCollection($mapping['targetDocument']);
+ $criteria = array_merge(
+ array($mapping['mappedBy'].'.'.$this->cmd.'id' => $class->getIdentifierObject($owner)),
+ $mapping['criteria']
+ );
+ $cursor = $mongoCollection->find($criteria);
+ if ($mapping['sort']) {
+ $cursor->sort($mapping['sort']);
+ }
+ if ($mapping['limit']) {
+ $cursor->limit($mapping['limit']);
+ }
+ if ($mapping['skip']) {
+ $cursor->skip($mapping['skip']);
+ }
+ foreach ($cursor as $documentData) {
+ $document = $this->dm->getReference($class->name, (string) $documentData['_id']);
@tecbot
tecbot added a note

the discriminator map for inverse side, doesn't work.

@jwage Doctrine member
jwage added a note

If you think about it, the inverse side discriminator map cannot work. How could it? we no longer store references on the inverse side if you specify mappedBy

@tecbot
tecbot added a note

i mean the discriminator map for the class and this is stored in the document, but the returned document from doctrine is only the document wich declare the discriminator map, because it use only the targetDocument option.

@jwage Doctrine member
jwage added a note

But like I said, it is impossible to discriminate which class to use because we do not store a reference in the database for the inverse side, so we have no where to store the discriminator value, which is used to lookup what class to instantiate. If you want to use inverse and owning side you cannot use the discriminator map on the inverse side.

@tecbot
tecbot added a note

I think you dont understand me.

When i load a document with find and the class has a discriminator map. e.g:

@mongodb:Document()
@mongodb:InheritanceType("SINGLE_COLLECTION")
@mongodb:DiscriminatorMap({
    "foo" = "Bar",
    "bar" = "Foo"
})
class Foo
{
     // ....
}

Then doctrine return the right class Bar or Foo for a id.

But wenn the id loaded with the loadReferenceManyCollectionInverseSide function, then is the class only Foo.

@jwage Doctrine member
jwage added a note

I understand now. I thought you meant the discriminator map on the reference. This is why test cases are important! :)

@tecbot
tecbot added a note

hehe thats right ;).

@jwage Doctrine member
jwage added a note

8bb7d01

I think this should take care of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ if ($document instanceof Proxy && ! $document->__isInitialized__) {
+ $data = $this->hydratorFactory->hydrate($document, $documentData);
+ $this->uow->setOriginalDocumentData($document, $data);
+ }
+ $collection->add($document);
+ }
+ }
+
+ private function loadReferenceManyWithRepositoryMethod(PersistentCollection $collection)
+ {
+ $mapping = $collection->getMapping();
+ $cursor = $this->dm->getRepository($mapping['targetDocument'])->$mapping['repositoryMethod']();
+ if ($mapping['sort']) {
+ $cursor->sort($mapping['sort']);
+ }
+ if ($mapping['limit']) {
+ $cursor->limit($mapping['limit']);
+ }
+ if ($mapping['skip']) {
+ $cursor->skip($mapping['skip']);
+ }
+ foreach ($cursor as $document) {
+ $collection->add($document);
+ }
+ }
+
/**
* Prepares a query array by converting the portable Doctrine types to the types mongodb expects.
*
View
15 lib/Doctrine/ODM/MongoDB/Persisters/PersistenceBuilder.php
@@ -109,8 +109,13 @@ public function prepareInsertData($document)
// @ReferenceOne
} elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
+ if ($mapping['isInverseSide']) {
+ continue;
+ }
+
$oid = spl_object_hash($new);
- if (isset($this->queuedInserts[$oid]) || $this->uow->isScheduledForInsert($new)) {
+
+ if ($this->isScheduledForInsert($new)) {
// The associated document $new is not yet persisted, so we must
// set $new = null, in order to insert a null value and schedule an
// extra update on the UnitOfWork.
@@ -230,7 +235,7 @@ public function prepareUpdateData($document)
}
// @ReferenceOne
- } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE) {
+ } elseif (isset($mapping['association']) && $mapping['association'] === ClassMetadata::REFERENCE_ONE && $mapping['isOwningSide']) {
if (isset($new) || $mapping['nullable'] === true) {
$updateData[$this->cmd . 'set'][$mapping['name']] = $this->prepareReferencedDocumentValue($mapping, $new);
} else {
@@ -323,4 +328,10 @@ public function prepareEmbeddedDocumentValue(array $embeddedMapping, $embeddedDo
}
return $embeddedDocumentValue;
}
+
+ private function isScheduledForInsert($document)
+ {
+ return $this->uow->isScheduledForInsert($document)
+ || $this->uow->getDocumentPersister(get_class($document))->isQueuedForInsert($document);
+ }
}
View
61 lib/Doctrine/ODM/MongoDB/UnitOfWork.php
@@ -563,11 +563,14 @@ public function computeChangeSet(ClassMetadata $class, $document)
$this->scheduleOrphanRemoval($orgValue);
}
$changeSet[$propName] = array($orgValue, $actualValue);
- } else if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $orgValue !== $actualValue) {
+ } else if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['type'] === 'one' && $class->fieldMappings[$propName]['isOwningSide'] && $orgValue !== $actualValue) {
$changeSet[$propName] = array($orgValue, $actualValue);
} else if ($isChangeTrackingNotify) {
continue;
} else if (isset($class->fieldMappings[$propName]['type']) && $class->fieldMappings[$propName]['type'] === 'many' && $orgValue !== $actualValue) {
+ if (isset($class->fieldMappings[$propName]['reference']) && $class->fieldMappings[$propName]['isInverseSide']) {
+ continue; // ignore inverse side
+ }
$changeSet[$propName] = array($orgValue, $actualValue);
if ($orgValue instanceof PersistentCollection) {
$this->collectionDeletions[] = $orgValue;
@@ -661,7 +664,7 @@ public function computeChangeSets()
*/
private function computeAssociationChanges($parentDocument, $mapping, $value)
{
- if ($value instanceof PersistentCollection && $value->isDirty()) {
+ if ($value instanceof PersistentCollection && $value->isDirty() && $mapping['isOwningSide']) {
$owner = $value->getOwner();
$className = get_class($owner);
$class = $this->dm->getClassMetadata($className);
@@ -1106,6 +1109,58 @@ private function getCommitOrder(array $documentChangeSet = null)
return $calc->getCommitOrder();
}
+/*
+private function getCommitOrder(array $entityChangeSet = null)
+{
+ if ($entityChangeSet === null) {
+ $entityChangeSet = array_merge(
+ $this->entityInsertions,
+ $this->entityUpdates,
+ $this->entityDeletions
+ );
+ }
+
+ $calc = $this->getCommitOrderCalculator();
+
+ // See if there are any new classes in the changeset, that are not in the
+ // commit order graph yet (dont have a node).
+ $newNodes = array();
+ foreach ($entityChangeSet as $oid => $entity) {
+ $className = get_class($entity);
+ if ( ! $calc->hasClass($className)) {
+ $class = $this->em->getClassMetadata($className);
+ $calc->addClass($class);
+ $newNodes[] = $class;
+ }
+ }
+
+ // Calculate dependencies for new nodes
+ foreach ($newNodes as $class) {
+ foreach ($class->associationMappings as $assoc) {
+ if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
+ $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
+ if ( ! $calc->hasClass($targetClass->name)) {
+ $calc->addClass($targetClass);
+ }
+ $calc->addDependency($targetClass, $class);
+ // If the target class has mapped subclasses,
+ // these share the same dependency.
+ if ($targetClass->subClasses) {
+ foreach ($targetClass->subClasses as $subClassName) {
+ $targetSubClass = $this->em->getClassMetadata($subClassName);
+ if ( ! $calc->hasClass($subClassName)) {
+ $calc->addClass($targetSubClass);
+ }
+ $calc->addDependency($targetSubClass, $class);
+ }
+ }
+ }
+ }
+ }
+
+ return $calc->getCommitOrder();
+}
+*/
/**
* Add dependencies recursively through embedded documents. Embedded documents
* may have references to other documents so those need to be saved first.
@@ -1116,7 +1171,7 @@ private function getCommitOrder(array $documentChangeSet = null)
private function addDependencies(ClassMetadata $class, $calc)
{
foreach ($class->fieldMappings as $mapping) {
- if (isset($mapping['reference']) && isset($mapping['targetDocument'])) {
+ if ($mapping['isOwningSide'] && isset($mapping['reference']) && isset($mapping['targetDocument'])) {
$targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
if ( ! $calc->hasClass($targetClass->name)) {
$calc->addClass($targetClass);
View
2 tests/Doctrine/ODM/MongoDB/Tests/BaseTest.php
@@ -40,7 +40,7 @@ public function setUp()
$reader = new AnnotationReader();
$reader->setDefaultAnnotationNamespace('Doctrine\ODM\MongoDB\Mapping\\');
- $this->annotationDriver = new AnnotationDriver($reader, __DIR__ . '/Documents');
+ $this->annotationDriver = new AnnotationDriver($reader, __DIR__ . '/../../../../Documents');
$config->setMetadataDriverImpl($this->annotationDriver);
$conn = new Connection(null, array(), $config);
View
259 tests/Doctrine/ODM/MongoDB/Tests/Functional/OwningAndInverseReferencesTest.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace Doctrine\ODM\MongoDB\Tests\Functional;
+
+use DateTime,
+ Doctrine\ODM\MongoDB\PersistentCollection;
+
+class OwningAndInverseReferencedTest extends \Doctrine\ODM\MongoDB\Tests\BaseTest
+{
+ public function testOneToOne()
+ {
+ // cart stores reference to customer
+ // customer does not store reference to cart
+ // customer has to load cart by querying for
+ // db.cart.findOne({ 'customer.$id' : customer.id })
+
+ // if inversedBy then isOwningSide
+ // if mappedBy then isInverseSide
+
+ $customer = new \Documents\Customer;
+ $customer->name = 'Jon Wage';
+ $customer->cart = new \Documents\Cart;
+ $customer->cart->numItems = 5;
+ $customer->cart->customer = $customer;
+ $customer->cartTest = 'test';
+ $this->dm->persist($customer);
+ $this->dm->persist($customer->cart);
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $customer = $this->dm->getRepository('Documents\Customer')->find($customer->id);
+ $this->assertInstanceOf('Documents\Cart', $customer->cart);
+ $this->assertEquals($customer->cart->id, $customer->cart->id);
+
+ $check = $this->dm->getDocumentCollection(get_class($customer))->findOne();
+ $this->assertTrue(isset($check['cart']));
+ $this->assertEquals('test', $check['cart']);
+
+ $customer->cart = null;
+ $customer->cartTest = 'ok';
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $check = $this->dm->getDocumentCollection(get_class($customer))->findOne();
+ $this->assertTrue(isset($check['cart']));
+ $this->assertEquals('ok', $check['cart']);
+
+ $customer = $this->dm->getRepository('Documents\Customer')->find($customer->id);
+ $this->assertInstanceOf('Documents\Cart', $customer->cart);
+ $this->assertEquals('ok', $customer->cartTest);
+ }
+
+ public function testOneToManyBiDirectional()
+ {
+ $product = new \Documents\Product('Book');
+ $product->addFeature(new \Documents\Feature('Pages'));
+ $product->addFeature(new \Documents\Feature('Cover'));
+ $this->dm->persist($product);
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $check = $this->dm->getDocumentCollection(get_class($product))->findOne();
+ $this->assertFalse(isset($check['tags']));
+
+ $check = $this->dm->getDocumentCollection('Documents\Feature')->findOne();
+ $this->assertTrue(isset($check['product']));
+
+ $product = $this->dm->createQueryBuilder(get_class($product))
+ ->getQuery()
+ ->getSingleResult();
+ $features = $product->features;
+ $this->assertEquals(2, count($features));
+ $this->assertEquals('Pages', $features[0]->name);
+ $this->assertEquals('Cover', $features[1]->name);
+ }
+
+ public function testOneToManySelfReferencing()
+ {
+ $node = new \Documents\BrowseNode('Root');
+ $node->addChild(new \Documents\BrowseNode('Child 1'));
+ $node->addChild(new \Documents\BrowseNode('Child 2'));
+
+ $this->dm->persist($node);
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $check = $this->dm->getDocumentCollection(get_class($node))->findOne(array('parent' => array('$exists' => false)));
+ $this->assertNotNull($check);
+ $this->assertFalse(isset($check['children']));
+
+ $root = $this->dm->createQueryBuilder(get_class($node))
+ ->field('children')->exists(false)
+ ->getQuery()
+ ->getSingleResult();
+ $this->assertInstanceOf('Documents\BrowseNode', $root);
+ $this->assertEquals(2, count($root->children));
+
+ unset($root->children[0]);
+ $this->dm->flush();
+
+ $this->assertEquals(1, count($root->children));
+
+ $this->dm->refresh($root);
+ $this->assertEquals(2, count($root->children));
+ }
+
+ public function testManyToMany()
+ {
+ $baseballTag = new \Documents\Tag('baseball');
+ $blogPost = new \Documents\BlogPost();
+ $blogPost->name = 'Test';
+ $blogPost->addTag($baseballTag);
+
+ $this->dm->persist($blogPost);
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $check = $this->dm->getDocumentCollection(get_class($blogPost))->findOne();
+ $this->assertEquals(1, count($check['tags']));
+
+ $check = $this->dm->getDocumentCollection('Documents\Tag')->findOne();
+ $this->assertFalse(isset($check['blogPosts']));
+
+ $blogPost = $this->dm->createQueryBuilder('Documents\BlogPost')
+ ->getQuery()
+ ->getSingleResult();
+ $this->assertEquals(1, count($blogPost->tags));
+
+ $this->dm->clear();
+
+ $tag = $this->dm->createQueryBuilder('Documents\Tag')
+ ->getQuery()
+ ->getSingleResult();
+ $this->assertEquals('baseball', $tag->name);
+ $this->assertEquals(1, $tag->blogPosts->count());
+ $this->assertEquals('Test', $tag->blogPosts[0]->name);
+ }
+
+ public function testManyToManySelfReferencing()
+ {
+ $jwage = new \Documents\FriendUser('jwage');
+ $fabpot = new \Documents\FriendUser('fabpot');
+ $fabpot->addFriend($jwage);
+ $romanb = new \Documents\FriendUser('romanb');
+ $romanb->addFriend($jwage);
+ $jwage->addFriend($fabpot);
+ $jwage->addFriend($romanb);
+
+ $this->dm->persist($jwage);
+ $this->dm->persist($fabpot);
+ $this->dm->persist($romanb);
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $check = $this->dm->createQueryBuilder('Documents\FriendUser')
+ ->field('name')->equals('fabpot')
+ ->hydrate(false)
+ ->getQuery()
+ ->getSingleResult();
+ $this->assertFalse(isset($check['friendsWithMe']));
+
+ $user = $this->dm->createQueryBuilder('Documents\FriendUser')
+ ->field('name')->equals('fabpot')
+ ->getQuery()
+ ->getSingleResult();
+
+ $this->assertEquals(1, count($user->friendsWithMe));
+ $this->assertEquals('jwage', $user->friendsWithMe[0]->name);
+
+ $this->dm->clear();
+
+ $user = $this->dm->createQueryBuilder('Documents\FriendUser')
+ ->field('name')->equals('romanb')
+ ->getQuery()
+ ->getSingleResult();
+
+ $this->assertEquals(1, count($user->friendsWithMe));
+ $this->assertEquals('jwage', $user->friendsWithMe[0]->name);
+
+ $this->dm->clear();
+
+ $user = $this->dm->createQueryBuilder('Documents\FriendUser')
+ ->field('name')->equals('jwage')
+ ->getQuery()
+ ->getSingleResult();
+
+ $this->assertEquals(2, count($user->myFriends));
+ $this->assertEquals('fabpot', $user->myFriends[0]->name);
+ $this->assertEquals('romanb', $user->myFriends[1]->name);
+
+ $this->assertEquals(2, count($user->friendsWithMe));
+ $this->assertEquals('fabpot', $user->friendsWithMe[0]->name);
+ $this->assertEquals('romanb', $user->friendsWithMe[1]->name);
+
+ $this->dm->clear();
+ }
+
+ public function testSortLimitAndSkipReferences()
+ {
+ $date1 = new DateTime();
+ $date1->setTimestamp(strtotime('-20 seconds'));
+
+ $date2 = new DateTime();
+ $date2->setTimestamp(strtotime('-10 seconds'));
+
+ $blogPost = new \Documents\BlogPost('Test');
+ $blogPost->addComment(new \Documents\Comment('Comment 1', $date1));
+ $blogPost->addComment(new \Documents\Comment('Comment 2', $date2));
+
+ $this->dm->persist($blogPost);
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $blogPost = $this->dm->createQueryBuilder('Documents\BlogPost')
+ ->getQuery()
+ ->getSingleResult();
+ $this->assertEquals('Comment 1', $blogPost->comments[0]->text);
+ $this->assertEquals('Comment 2', $blogPost->comments[1]->text);
+ $this->assertEquals('Test', $blogPost->comments[0]->parent->name);
+ $this->assertEquals('Test', $blogPost->comments[1]->parent->name);
+
+ $this->dm->clear();
+
+ $comment = $this->dm->createQueryBuilder('Documents\Comment')
+ ->getQuery()
+ ->getSingleResult();
+ $this->assertEquals('Test', $comment->parent->getName());
+
+ $this->dm->clear();
+
+ $blogPost = $this->dm->createQueryBuilder('Documents\BlogPost')
+ ->getQuery()
+ ->getSingleResult();
+ $this->assertEquals('Comment 1', $blogPost->firstComment->getText());
+ $this->assertEquals('Comment 2', $blogPost->latestComment->getText());
+ $this->assertEquals(2, count($blogPost->last5Comments));
+
+ $this->assertEquals('Comment 2', $blogPost->last5Comments[0]->getText());
+ $this->assertEquals('Comment 1', $blogPost->last5Comments[1]->getText());
+
+ $this->dm->clear();
+
+ $blogPost = $this->dm->createQueryBuilder('Documents\BlogPost')
+ ->getQuery()
+ ->getSingleResult();
+
+ $blogPost->addComment(new \Documents\Comment('Comment 3 by admin', $date1, true));
+ $blogPost->addComment(new \Documents\Comment('Comment 4 by admin', $date2, true));
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $blogPost = $this->dm->createQueryBuilder('Documents\BlogPost')
+ ->getQuery()
+ ->getSingleResult();
+ $this->assertEquals(2, count($blogPost->adminComments));
+ $this->assertEquals('Comment 4 by admin', $blogPost->adminComments[0]->getText());
+ $this->assertEquals('Comment 3 by admin', $blogPost->adminComments[1]->getText());
+ }
+}
View
33 tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferenceRepositoryMethodTest.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace Doctrine\ODM\MongoDB\Tests\Functional;
+
+use DateTime,
+ Doctrine\ODM\MongoDB\PersistentCollection;
+
+class ReferenceRepositoryMethodTest extends \Doctrine\ODM\MongoDB\Tests\BaseTest
+{
+ public function testOneToOne()
+ {
+ $date1 = new DateTime();
+ $date1->setTimestamp(strtotime('-20 seconds'));
+
+ $date2 = new DateTime();
+ $date2->setTimestamp(strtotime('-10 seconds'));
+
+ $blogPost = new \Documents\BlogPost('Test');
+ $blogPost->addComment(new \Documents\Comment('Comment 1', $date1));
+ $blogPost->addComment(new \Documents\Comment('Comment 2', $date2));
+ $this->dm->persist($blogPost);
+ $this->dm->flush();
+ $this->dm->clear();
+
+ $blogPost = $this->dm->createQueryBuilder('Documents\BlogPost')
+ ->getQuery()
+ ->getSingleResult();
+
+ $this->assertEquals('Comment 2', $blogPost->repoComment->getText());
+ $this->assertEquals('Comment 1', $blogPost->repoComments[0]->getText());
+ $this->assertEquals('Comment 2', $blogPost->repoComments[1]->getText());
+ }
+}
View
12 tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php
@@ -3,6 +3,7 @@
namespace Doctrine\ODM\MongoDB\Tests\Mapping;
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
+use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo;
use Doctrine\ODM\MongoDB\Events;
class ClassMetadataTest extends \Doctrine\ODM\MongoDB\Tests\BaseTest
@@ -49,6 +50,17 @@ public function testClassMetadataInstanceSerialization()
$this->assertEquals('Documents\Bar', $mapping['targetDocument']);
}
+ public function testOwningSideAndInverseSide()
+ {
+ $cm = new ClassMetadataInfo('Documents\User');
+ $cm->mapManyReference(array('fieldName' => 'articles', 'inversedBy' => 'user'));
+ $this->assertTrue($cm->fieldMappings['articles']['isOwningSide']);
+
+ $cm = new ClassMetadataInfo('Documents\Article');
+ $cm->mapOneReference(array('fieldName' => 'user', 'mappedBy' => 'articles'));
+ $this->assertTrue($cm->fieldMappings['user']['isInverseSide']);
+ }
+
public function testFieldIsNullable()
{
$cm = new ClassMetadata('Documents\CmsUser');
View
20 tests/Doctrine/ODM/MongoDB/Tests/Mapping/Driver/AbstractDriverTest.php
@@ -41,6 +41,8 @@ public function testDriver()
'isCascadePersist' => false,
'isCascadeRefresh' => false,
'isCascadeRemove' => false,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false
), $classMetadata->fieldMappings['id']);
@@ -54,6 +56,8 @@ public function testDriver()
'isCascadePersist' => false,
'isCascadeRefresh' => false,
'isCascadeRemove' => false,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false
), $classMetadata->fieldMappings['username']);
@@ -67,6 +71,8 @@ public function testDriver()
'isCascadePersist' => false,
'isCascadeRefresh' => false,
'isCascadeRemove' => false,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false
), $classMetadata->fieldMappings['createdAt']);
@@ -80,6 +86,8 @@ public function testDriver()
'isCascadePersist' => false,
'isCascadeRefresh' => false,
'isCascadeRemove' => false,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false,
'strategy' => 'pushAll',
), $classMetadata->fieldMappings['tags']);
@@ -97,6 +105,8 @@ public function testDriver()
'isCascadePersist' => false,
'isCascadeRefresh' => false,
'isCascadeRemove' => false,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false,
'strategy' => 'pushAll',
), $classMetadata->fieldMappings['address']);
@@ -114,6 +124,8 @@ public function testDriver()
'isCascadePersist' => false,
'isCascadeRefresh' => false,
'isCascadeRemove' => false,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false,
'strategy' => 'pushAll',
), $classMetadata->fieldMappings['phonenumbers']);
@@ -131,6 +143,8 @@ public function testDriver()
'isCascadePersist' => true,
'isCascadeRefresh' => true,
'isCascadeRemove' => true,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false,
'strategy' => 'pushAll',
), $classMetadata->fieldMappings['profile']);
@@ -148,6 +162,8 @@ public function testDriver()
'isCascadePersist' => true,
'isCascadeRefresh' => true,
'isCascadeRemove' => true,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false,
'strategy' => 'pushAll',
), $classMetadata->fieldMappings['account']);
@@ -165,6 +181,8 @@ public function testDriver()
'isCascadePersist' => true,
'isCascadeRefresh' => true,
'isCascadeRemove' => true,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false,
'strategy' => 'pushAll',
), $classMetadata->fieldMappings['groups']);
@@ -189,6 +207,8 @@ public function testDriver()
'isCascadePersist' => false,
'isCascadeRefresh' => false,
'isCascadeRemove' => false,
+ 'isInverseSide' => false,
+ 'isOwningSide' => true,
'nullable' => false,
), $classMetadata->fieldMappings['name']);
}
View
6 tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php
@@ -43,7 +43,7 @@ public function testEnsureIndexes(array $classes)
public function getIndexedClasses()
{
return array(
- array(array('Documents\CmsArticle', 'Documents\CmsAddress', 'Documents\CmsComment', 'Documents\CmsProduct'))
+ array(array('Documents\Comment', 'Documents\CmsArticle', 'Documents\CmsAddress', 'Documents\CmsComment', 'Documents\CmsProduct'))
);
}
@@ -270,7 +270,7 @@ protected function getDocumentManager()
protected function getConnection()
{
- return $this->getMock('Doctrine\MongoDB\Connection', array('selectDB'), array(), '', false, false);
+ return $this->getMock('Doctrine\MongoDB\Connection', array('selectDatabase'), array(), '', false, false);
}
protected function getMetadataFactory()
@@ -287,7 +287,7 @@ protected function getClassMetadata($className)
protected function getDocumentCollection()
{
- return $this->getMock('Doctrine\MongoDB\Collection', array('ensureIndex', 'deleteIndex', 'deleteIndexes'), array(), '', false, false);
+ return $this->getMock('Doctrine\MongoDB\Collection', array('ensureIndex', 'deleteIndex', 'deleteIndexes', 'selectCollection'), array(), '', false, false);
}
protected function getDocumentDatabase($className)
View
59 tests/Documents/BlogPost.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Documents;
+
+/** @Document */
+class BlogPost
+{
+ /** @Id */
+ public $id;
+
+ /** @String */
+ public $name;
+
+ /** @ReferenceMany(targetDocument="Tag", inversedBy="blogPosts", cascade={"all"}) */
+ public $tags = array();
+
+ /** @ReferenceMany(targetDocument="Comment", mappedBy="parent", cascade={"all"}) */
+ public $comments = array();
+
+ /** @ReferenceOne(targetDocument="Comment", mappedBy="parent", sort={"date"="asc"}) */
+ public $firstComment;
+
+ /** @ReferenceOne(targetDocument="Comment", mappedBy="parent", sort={"date"="desc"}) */
+ public $latestComment;
+
+ /** @ReferenceMany(targetDocument="Comment", mappedBy="parent", sort={"date"="desc"}, limit=5) */
+ public $last5Comments = array();
+
+ /** @ReferenceMany(targetDocument="Comment", mappedBy="parent", criteria={"isByAdmin"=true}, sort={"date"="desc"}) */
+ public $adminComments = array();
+
+ /** @ReferenceOne(targetDocument="Comment", mappedBy="parent", repositoryMethod="findOneComment") */
+ public $repoComment;
+
+ /** @ReferenceMany(targetDocument="Comment", mappedBy="parent", repositoryMethod="findManyComments") */
+ public $repoComments;
+
+ public function __construct($name = null)
+ {
+ $this->name = $name;
+ }
+
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ public function addTag(Tag $tag)
+ {
+ $tag->addBlogPost($this); // synchronously updating inverse side
+ $this->tags[] = $tag;
+ }
+
+ public function addComment(Comment $comment)
+ {
+ $comment->parent = $this;
+ $this->comments[] = $comment;
+ }
+}
View
35 tests/Documents/BrowseNode.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Documents;
+
+/** @Document */
+class BrowseNode
+{
+ /** @Id */
+ public $id;
+
+ /** @String */
+ public $name;
+
+ /**
+ * @ReferenceOne(targetDocument="BrowseNode", inversedBy="children", cascade={"all"})
+ */
+ public $parent;
+
+ /**
+ * @ReferenceMany(targetDocument="BrowseNode", mappedBy="parent", cascade={"all"})
+ */
+ public $children;
+
+ public function __construct($name = null)
+ {
+ $this->name = $name;
+ $this->children = new \Doctrine\Common\Collections\ArrayCollection();
+ }
+
+ public function addChild(BrowseNode $child)
+ {
+ $child->parent = $this;
+ $this->children[] = $child;
+ }
+}
View
18 tests/Documents/Cart.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace Documents;
+
+/** @Document */
+class Cart
+{
+ /** @Id */
+ public $id;
+
+ /** @Int */
+ public $numItems = 0;
+
+ /**
+ * @ReferenceOne(targetDocument="Customer", inversedBy="cart")
+ */
+ public $customer;
+}
View
37 tests/Documents/Comment.php
@@ -0,0 +1,37 @@
+<?php
+
+namespace Documents;
+
+use DateTime;
+
+/** @Document(repositoryClass="Documents\CommentRepository") */
+class Comment
+{
+ /** @Id */
+ public $id;
+
+ /** @String */
+ public $text;
+
+ /** @ReferenceOne(targetDocument="BlogPost", inversedBy="comments", cascade={"all"}) */
+ public $parent;
+
+ /** @Date @Index(order="1") */
+ public $date;
+
+ /** @Boolean */
+ public $isByAdmin = false;
+
+ public function __construct($text, DateTime $date, $isByAdmin = false)
+ {
+ $this->text = $text;
+ $this->date = $date;
+ $this->isByAdmin = $isByAdmin;
+ }
+
+ public function getText()
+ {
+ return $this->text;
+ }
+
+}
View
21 tests/Documents/CommentRepository.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Documents;
+
+use Doctrine\ODM\MongoDB\DocumentRepository;
+
+class CommentRepository extends DocumentRepository
+{
+ public function findOneComment()
+ {
+ return $this->findBy(array())
+ ->sort(array('date' => 'desc'))
+ ->limit(1)
+ ->getSingleResult();
+ }
+
+ public function findManyComments()
+ {
+ return $this->findBy(array());
+ }
+}
View
21 tests/Documents/Customer.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Documents;
+
+/** @Document */
+class Customer
+{
+ /** @Id */
+ public $id;
+
+ /** @String */
+ public $name;
+
+ /** @String(name="cart") */
+ public $cartTest;
+
+ /**
+ * @ReferenceOne(targetDocument="Cart", mappedBy="customer")
+ */
+ public $cart;
+}
View
23 tests/Documents/Feature.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Documents;
+
+/** @Document */
+class Feature
+{
+ /** @Id */
+ public $id;
+
+ /** @String */
+ public $name;
+
+ /**
+ * @ReferenceOne(targetDocument="Product", inversedBy="features", cascade={"all"})
+ */
+ public $product;
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ }
+}
View
36 tests/Documents/FriendUser.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Documents;
+
+/** @Document */
+class FriendUser
+{
+ /** @Id */
+ public $id;
+
+ /** @String */
+ public $name;
+
+ /**
+ * @ReferenceMany(targetDocument="FriendUser", mappedBy="myFriends", cascade={"all"})
+ */
+ public $friendsWithMe;
+
+ /**
+ * @ReferenceMany(targetDocument="FriendUser", inversedBy="friendsWithMe", cascade={"all"})
+ */
+ public $myFriends;
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ $this->friendsWithMe = new \Doctrine\Common\Collections\ArrayCollection();
+ $this->myFriends = new \Doctrine\Common\Collections\ArrayCollection();
+ }
+
+ public function addFriend(FriendUser $user)
+ {
+ $user->friendsWithMe[] = $this;
+ $this->myFriends[] = $user;
+ }
+}
View
30 tests/Documents/Product.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Documents;
+
+/** @Document */
+class Product
+{
+ /** @Id */
+ public $id;
+
+ /** @String */
+ public $name;
+
+ /**
+ * @ReferenceMany(targetDocument="Feature", mappedBy="product", cascade={"all"})
+ */
+ public $features;
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ $this->features = new \Doctrine\Common\Collections\ArrayCollection();
+ }
+
+ public function addFeature(Feature $feature)
+ {
+ $feature->product = $this;
+ $this->features[] = $feature;
+ }
+}
View
26 tests/Documents/Tag.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Documents;
+
+/** @Document */
+class Tag
+{
+ /** @Id */
+ public $id;
+
+ /** @String */
+ public $name;
+
+ /** @ReferenceMany(targetDocument="BlogPost", mappedBy="tags") */
+ public $blogPosts;
+
+ public function __construct($name)
+ {
+ $this->name = $name;
+ }
+
+ public function addBlogPost(BlogPost $blogPost)
+ {
+ $this->blogPosts[] = $blogPost;
+ }
+}

0 comments on commit d1bffbd

Please sign in to comment.
Something went wrong with that request. Please try again.