Skip to content

Commit

Permalink
Merge pull request #485 from doctrine/fix/set-strategy-defect
Browse files Browse the repository at this point in the history
fixed prepareQueryElement() "set" strategy issue
  • Loading branch information
jmikola committed Apr 1, 2013
2 parents c8e9a6e + 59fcd66 commit c1a90e7
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 111 deletions.
2 changes: 1 addition & 1 deletion lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ public function getReflectionClass()
*/
public function isIdentifier($fieldName)
{
return $this->identifier === $fieldName ? true : false;
return $this->identifier === $fieldName;
}

/**
Expand Down
266 changes: 156 additions & 110 deletions lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,7 @@ public function refresh($id, $document)
public function load($criteria, $document = null, array $hints = array(), $lockMode = 0, array $sort = array())
{
$criteria = $this->prepareQuery($criteria);

$cursor = $this->collection->find($criteria)->limit(1);
if ($sort) {
$cursor->sort($sort);
Expand Down Expand Up @@ -777,13 +778,14 @@ public function prepareSort($sort)
/**
* Prepare a mongodb field name and convert the PHP property names to MongoDB field names.
*
* @param string $key
* @return string $preparedKey
* @param string $fieldName
* @return string
*/
public function prepareFieldName($key)
public function prepareFieldName($fieldName)
{
$this->prepareQueryElement($key, null, null, false);
return $key;
list($fieldName) = $this->prepareQueryElement($fieldName, null, null, false);

return $fieldName;
}

/**
Expand Down Expand Up @@ -812,7 +814,8 @@ public function prepareQuery($query = array())
if (isset($key[0]) && $key[0] === $this->cmd && is_array($value)) {
$newQuery[$key] = $this->prepareSubQuery($value);
} else {
$newQuery[$key] = $this->prepareQueryElement($key, $value, null, true);
list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
$newQuery[$key] = $value;
}
}
$newQuery = $this->convertTypes($newQuery);
Expand All @@ -834,7 +837,8 @@ public function prepareNewObj($newObj)
if (isset($key[0]) && $key[0] === $this->cmd && is_array($value)) {
$prepared[$key] = $this->prepareSubQuery($value);
} else {
$prepared[$key] = $this->prepareQueryElement($key, $value, null, true);
list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
$prepared[$key] = $value;
}
}
$prepared = $this->convertTypes($prepared);
Expand All @@ -857,7 +861,8 @@ private function prepareSubQuery($query)
if (isset($key[0]) && $key[0] === $this->cmd && is_array($value)) {
$newQuery[$key] = $this->prepareSubQuery($value);
} else {
$newQuery[$key] = $this->prepareQueryElement($key, $value, null, true);
list($key, $value) = $this->prepareQueryElement($key, $value, null, true);
$newQuery[$key] = $value;
}
}
$newQuery = $this->convertTypes($newQuery);
Expand All @@ -884,132 +889,173 @@ private function convertTypes(array $query)
}

/**
* Prepares a query value and converts the php value to the database value if it is an identifier.
* Prepares a query value and converts the PHP value to the database value
* if it is an identifier.
*
* It also handles converting $fieldName to the database name if they are different.
*
* @param string $fieldName
* @param string $value
* @param ClassMetadata $metadata Defaults to $this->class
* @param bool $prepareValue Whether or not to prepare the value
* @return mixed $value
* @param mixed $value
* @param ClassMetadata $class Defaults to $this->class
* @param boolean $prepareValue Whether or not to prepare the value
* @return array Prepared field name and value
*/
private function prepareQueryElement(&$fieldName, $value = null, $metadata = null, $prepareValue = true)
private function prepareQueryElement($fieldName, $value = null, $class = null, $prepareValue = true)
{
$metadata = ($metadata === null) ? $this->class : $metadata;
$class = isset($class) ? $class : $this->class;

// Process "association.fieldName"
if (strpos($fieldName, '.') !== false) {
$e = explode('.', $fieldName);
// @todo Consider inlining calls to ClassMetadataInfo methods

if (!isset($metadata->fieldMappings[$e[0]])) {
return $value;
}
// Process all non-identifier fields by translating field names
if ($class->hasField($fieldName) && ! $class->isIdentifier($fieldName)) {
$mapping = $class->fieldMappings[$fieldName];
$fieldName = $mapping['name'];

$mapping = $metadata->fieldMappings[$e[0]];
$e[0] = $mapping['name'];
$fieldName = $e[0] . '.' .$e[1];
if ($e[1] != '$') {
$fieldName = $e[0] . '.' .$e[1];
$objectProperty = $e[1];
$objectPropertyPrefix = '';
$fieldHasCollectionItemPointer = false;
} else {
$fieldName = $e[0] . '.' .$e[1] . '.' .$e[2];
$objectProperty = $e[2];
$objectPropertyPrefix = $e[1] . '.';
$fieldHasCollectionItemPointer = true;
if ( ! $prepareValue || empty($mapping['reference']) || empty($mapping['simple'])) {
return array($fieldName, $value);
}

if (isset($mapping['targetDocument'])) {
// Additional preparation for one or more simple reference values
if ( ! is_array($value)) {
$targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
if ($targetClass->hasField($objectProperty)) {
if ($targetClass->identifier === $objectProperty) {
$targetMapping = $targetClass->getFieldMapping($objectProperty);
$objectProperty = $targetMapping['name'];
if (isset($mapping['reference']) && $mapping['reference']) {
$fieldName = $mapping['simple'] ? $e[0] . '.' .$objectPropertyPrefix . $objectProperty : $e[0] . '.$id';
} else {
$fieldName = $e[0] . '.' .$objectPropertyPrefix . $objectProperty;
}
if ($prepareValue === true) {
if (is_array($value)) {
$keys = array_keys($value);
if (isset($keys[0][0]) && $keys[0][0] === $this->cmd) {
foreach ($value[$keys[0]] as $k => $v) {
$value[$keys[0]][$k] = $targetClass->getDatabaseIdentifierValue($v);
}
} else {
foreach ($value as $k => $v) {
$value[$k] = $targetClass->getDatabaseIdentifierValue($v);
}
}
} else {
$value = $targetClass->getDatabaseIdentifierValue($value);
}
}

} else {
$targetMapping = $targetClass->getFieldMapping($objectProperty);
$objectProperty = $targetMapping['name'];
$fieldName = $e[0] . '.' . $objectPropertyPrefix . $objectProperty;
return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
}

if (count($e) > 2 + $fieldHasCollectionItemPointer ? 1 : 0) {
if ($fieldHasCollectionItemPointer) {
unset($e[2]);
}
unset($e[0], $e[1]);
$key = implode('.', $e);

if (isset($targetMapping['targetDocument'])) {
$nextTargetClass = $this->dm->getClassMetadata($targetMapping['targetDocument']);
$value = $this->prepareQueryElement($key, $value, $nextTargetClass, $prepareValue);
} else {
$value = $this->prepareQueryElement($key, $value, null, $prepareValue);
}
return array($fieldName, $this->prepareSubQuery($value));
}

$fieldName .= '.' . $key;
}
}
}
} elseif ($mapping['type'] === 'hash') {
$fieldName = implode('.', $e);
// Process identifier fields
if (($class->hasField($fieldName) && $class->isIdentifier($fieldName)) || $fieldName === '_id') {
$fieldName = '_id';

if ( ! $prepareValue) {
return array($fieldName, $value);
}

// Process all non identifier fields
// We only change the field names here to the mongodb field name used for persistence
} elseif ($metadata->hasField($fieldName) && ! $metadata->isIdentifier($fieldName)) {
$mapping = $metadata->fieldMappings[$fieldName];
$fieldName = $mapping['name'];
if ( ! is_array($value)) {
return array($fieldName, $class->getDatabaseIdentifierValue($value));
}

if ($prepareValue === true && isset($mapping['reference']) && isset($mapping['simple']) && $mapping['simple']) {
if (is_array($value)) {
$value = $this->prepareSubQuery($value);
foreach ($value as $k => $v) {
// Process query operators (e.g. "$in")
if (isset($k[0]) && $k[0] === $this->cmd && is_array($v)) {
foreach ($v as $k2 => $v2) {
$value[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
}
} else {
$targetClass = $this->dm->getClassMetadata($mapping['targetDocument']);
$value = $targetClass->getDatabaseIdentifierValue($value);
$value[$k] = $class->getDatabaseIdentifierValue($v);
}
}

// Process identifier
} elseif (($metadata->hasField($fieldName) && $metadata->isIdentifier($fieldName)) || $fieldName === '_id') {
$fieldName = '_id';
if ($prepareValue === true) {
if (is_array($value)) {
foreach ($value as $k => $v) {
if ($k[0] === '$' && is_array($v)) {
foreach ($v as $k2 => $v2) {
$value[$k][$k2] = $metadata->getDatabaseIdentifierValue($v2);
}
} else {
$value[$k] = $metadata->getDatabaseIdentifierValue($v);
}
}
} else {
$value = $metadata->getDatabaseIdentifierValue($value);
return array($fieldName, $value);
}

// No processing for unmapped, non-identifier, non-dotted field names
if (strpos($fieldName, '.') === false) {
return array($fieldName, $value);
}

/* Process "fieldName.objectProperty" queries (on arrays or objects).
*
* We can limit parsing here, since at most three segments are
* significant: "fieldName.objectProperty" with an optional index or key
* for collections stored as either BSON arrays or objects.
*/
$e = explode('.', $fieldName, 4);

// No further processing for unmapped fields
if ( ! isset($class->fieldMappings[$e[0]])) {
return array($fieldName, $value);
}

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

// Hash fields will not be prepared beyond the field name
if ($mapping['type'] === 'hash') {
$fieldName = implode('.', $e);

return array($fieldName, $value);
}

if ($mapping['strategy'] === 'set' && isset($e[2])) {
$objectProperty = $e[2];
$objectPropertyPrefix = $e[1] . '.';
$nextObjectProperty = implode('.', array_slice($e, 3));
} elseif ($e[1] != '$') {
$fieldName = $e[0] . '.' .$e[1];
$objectProperty = $e[1];
$objectPropertyPrefix = '';
$nextObjectProperty = implode('.', array_slice($e, 2));
} else {
$fieldName = $e[0] . '.' .$e[1] . '.' .$e[2];
$objectProperty = $e[2];
$objectPropertyPrefix = $e[1] . '.';
$nextObjectProperty = implode('.', array_slice($e, 3));
}

// No further processing for fields without a targetDocument mapping
if ( ! isset($mapping['targetDocument'])) {
return array($fieldName, $value);
}

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

// No further processing for unmapped targetDocument fields
if ( ! $targetClass->hasField($objectProperty)) {
return array($fieldName, $value);
}

$targetMapping = $targetClass->getFieldMapping($objectProperty);
$objectPropertyIsId = $targetClass->isIdentifier($objectProperty);

// Prepare DBRef identifiers or the mapped field's property path
$fieldName = ($objectPropertyIsId && !empty($mapping['reference']) && empty($mapping['simple']))
? $e[0] . '.$id'
: $e[0] . '.' . $objectPropertyPrefix . $targetMapping['name'];

// Process targetDocument identifier fields
if ($objectPropertyIsId) {
if ( ! $prepareValue) {
return array($fieldName, $value);
}

if ( ! is_array($value)) {
return array($fieldName, $targetClass->getDatabaseIdentifierValue($value));
}

$keys = array_keys($value);

if (isset($keys[0][0]) && $keys[0][0] === $this->cmd) {
foreach ($value[$keys[0]] as $k => $v) {
$value[$keys[0]][$k] = $targetClass->getDatabaseIdentifierValue($v);
}
} else {
foreach ($value as $k => $v) {
$value[$k] = $targetClass->getDatabaseIdentifierValue($v);
}
}

return array($fieldName, $value);
}

/* The property path may include a third field segment, excluding the
* collection item pointer. If present, this next object property must
* be processed recursively.
*/
if ($nextObjectProperty) {
// Respect the targetDocument's class metadata when recursing
$nextTargetClass = isset($targetMapping['targetDocument'])
? $this->dm->getClassMetadata($targetMapping['targetDocument'])
: null;

list($key, $value) = $this->prepareQueryElement($nextObjectProperty, $value, $nextTargetClass, $prepareValue);

$fieldName .= '.' . $key;
}
return $value;

return array($fieldName, $value);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,22 @@ public function testNestedEmbedManySetStrategy()
$this->assertEquals($comment3->comment, $check['comments']['first']['comments']['second']['comment']);
$this->assertFalse(isset($check['comments']['second']));
}

public function testFindBySetStrategyKey()
{
$post = new CollectionPersisterPost('postA');
$commentA = new CollectionPersisterComment('commentA', 'userA');
$commentAB = new CollectionPersisterComment('commentAA', 'userB');

$post->comments['a'] = $commentA;
$commentA->comments['b'] = $commentAB;

$this->dm->persist($post);
$this->dm->flush();

$this->assertSame($post, $this->dm->getRepository(get_class($post))->findOneBy(array('comments.a.by' => 'userA')));
$this->assertSame($post, $this->dm->getRepository(get_class($post))->findOneBy(array('comments.a.comments.b.by' => 'userB')));
}
}

/** @ODM\Document(collection="user_collection_persister_test") */
Expand Down

0 comments on commit c1a90e7

Please sign in to comment.