Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Embedded priming fix for Issue #624, with Tests #970

Merged
merged 5 commits into from
Mar 28, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion lib/Doctrine/ODM/MongoDB/Query/ReferencePrimer.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ public function __construct(DocumentManager $dm, UnitOfWork $uow)
*/
public function primeReferences(ClassMetadata $class, $documents, $fieldName, array $hints = array(), $primer = null)
{
$mapping = $class->getFieldMapping($fieldName);
$data = $this->parseDotSyntaxForPrimer($fieldName, $class, $documents);
$mapping = $data['mapping'];
$fieldName = $data['fieldName'];
$class = $data['class'];
$documents = $data['documents'];

/* Inverse-side references would need to be populated before we can
* collect references to be primed. This is not supported.
Expand All @@ -131,6 +135,7 @@ public function primeReferences(ClassMetadata $class, $documents, $fieldName, ar
$primer = $primer ?: $this->defaultPrimer;
$groupedIds = array();

/* @var $document PersistentCollection */
foreach ($documents as $document) {
$fieldValue = $class->getFieldValue($document, $fieldName);

Expand All @@ -156,6 +161,84 @@ public function primeReferences(ClassMetadata $class, $documents, $fieldName, ar
}
}

/**
* If you are priming references inside an embedded document you'll need to parse the dot syntax.
* This method will traverse through embedded documents to find the reference to prime.
* However this method will not traverse through multiple layers of references.
* I.e. you can prime this: myDocument.embeddedDocument.embeddedDocuments.embeddedDocuments.referencedDocument(s)
* ... but you cannot prime this: myDocument.embeddedDocument.referencedDocuments.referencedDocument(s)
* This addresses Issue #624.
*
* @param string $fieldName
* @param ClassMetadata $class
* @param array|\Traversable $documents
* @param array $mapping
* @return array
*/
private function parseDotSyntaxForPrimer($fieldName, $class, $documents, $mapping = null)
{
// Recursion passthrough:
if ($mapping != null) {
return array('fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping);
}

// Gather mapping data:
$e = explode('.', $fieldName);

if ( ! isset($class->fieldMappings[$e[0]])) {
throw new \InvalidArgumentException(sprintf('Field %s cannot be further parsed for priming because it is unmapped.', $fieldName));
}

$mapping = $class->fieldMappings[$e[0]];
$e[0] = $mapping['name'];

// Case of embedded document(s) to recurse through:
if ( ! isset($mapping['reference'])) {
if (empty($mapping['embedded'])) {
throw new \InvalidArgumentException(sprintf('Field "%s" of fieldName "%s" is not an embedded document, therefore no children can be primed. Aborting. This feature does not support traversing nested referenced documents at this time.', $e[0], $fieldName));
}

if ( ! isset($mapping['targetDocument'])) {
throw new \InvalidArgumentException(sprintf('No target document class has been specified for this embedded document. However, targetDocument mapping must be specified in order for prime to work on fieldName "%s" for mapping of field "%s".', $fieldName, $mapping['fieldName']));
}

$childDocuments = array();

foreach ($documents as $document) {
$fieldValue = $class->getFieldValue($document, $e[0]);

if ($fieldValue instanceof PersistentCollection) {
foreach ($fieldValue as $elemDocument) {
array_push($childDocuments, $elemDocument);
}
} else {
array_push($childDocuments,$fieldValue);
}
}

array_shift($e);

$childClass = $this->dm->getClassMetadata($mapping['targetDocument']);

if ( ! $childClass->hasField($e[0])) {
throw new \InvalidArgumentException(sprintf('Field to prime must exist in embedded target document. Reference fieldName "%s" for mapping of target document class "%s".', $fieldName, $mapping['targetDocument']));
}

$childFieldName = implode('.',$e);

return $this->parseDotSyntaxForPrimer($childFieldName, $childClass, $childDocuments);
}

// Case of reference(s) to prime:
if ($mapping['reference']) {
if (count($e) > 1) {
throw new \InvalidArgumentException(sprintf('Cannot prime more than one layer deep but field "%s" is a reference and has children in fieldName "%s".', $e[0], $fieldName));
}

return array('fieldName' => $fieldName, 'class' => $class, 'documents' => $documents, 'mapping' => $mapping);
}
}

/**
* Adds identifiers from a PersistentCollection to $groupedIds.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@
use Documents\Functional\FavoritesUser;
use Documents\Group;
use Documents\GuestServer;
use Documents\Phonenumber;
use Documents\Project;
use Documents\SimpleReferenceUser;
use Documents\User;
use Documents\Ecommerce\ConfigurableProduct;
use Documents\Ecommerce\Currency;
use Documents\Ecommerce\Money;
use Documents\Ecommerce\Option;
use Documents\Ecommerce\StockItem;

class ReferencePrimerTest extends \Doctrine\ODM\MongoDB\Tests\BaseTest
{
Expand Down Expand Up @@ -250,4 +256,78 @@ public function testPrimeReferencesInFindAndModifyResult()
$this->assertInstanceOf('Documents\Group', $group);
}
}

public function testPrimeEmbeddedReferenceOneLevelDeep()
{
$user1 = new User();
$user2 = new User();
$phone = new Phonenumber('555-GET-THIS',$user2);

$user1->addPhonenumber($phone);
$user1->setUsername('SomeName');

$this->dm->persist($user1);
$this->dm->persist($user2);
$this->dm->flush();

$this->dm->clear();

$qb = $this->dm->createQueryBuilder('Documents\User')
->field('username')->equals('SomeName')
->field('phonenumbers.lastCalledBy')->prime(true);

$user = $qb->getQuery()->getSingleResult();
$phonenumbers = $user->getPhonenumbers();

$this->assertCount(1, $phonenumbers);

$phonenumber = $phonenumbers->current();

$this->assertNotInstanceOf('Doctrine\ODM\MongoDB\Proxy\Proxy', $phonenumber);
$this->assertInstanceOf('Documents\Phonenumber', $phonenumber);
}

public function testPrimeEmbeddedReferenceTwoLevelsDeep()
{
$product = new ConfigurableProduct('Bundle');

$product->addOption(
new Option('Lens1', new Money(75.00, new Currency('USD',1)),
new StockItem('Filter1', new Money(50.00, new Currency('USD',1)), 1))
);
$product->addOption(
new Option('Lens2', new Money(120.00, new Currency('USD',1)),
new StockItem('Filter2', new Money(100.00, new Currency('USD',1)), 1))
);
$product->addOption(
new Option('Lens3', new Money(180.00, new Currency('USD',1)),
new StockItem('Filter3', new Money(0.01, new Currency('USD',1)), 1))
);

$this->dm->persist($product);
$this->dm->flush();
$this->dm->clear();

$qb = $this->dm->createQueryBuilder('Documents\Ecommerce\ConfigurableProduct')
->field('name')->equals('Bundle')
->field('options.money.currency')->prime(true);

/** @var Query $query */
$query = $qb->getQuery();

/** @var ConfigurableProduct $product */
$product = $query->getSingleResult();

/** @var Option $option */
$option = $product->getOption('Lens2');

/** @var Money $money */
$money = $option->getPrice(true);

/** @var Currency $currency */
$currency = $money->getCurrency();

$this->assertInstanceOf('Doctrine\ODM\MongoDB\Proxy\Proxy', $currency);
$this->assertTrue($currency->__isInitialized());
}
}