Skip to content

Added conversion from PHP values to DB values during query preparation #2104

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

Merged
merged 14 commits into from
Feb 4, 2020
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
62 changes: 58 additions & 4 deletions lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -1021,15 +1021,56 @@ public function prepareQueryOrNewObj(array $query, bool $isNewObj = false) : arr

$preparedQueryElements = $this->prepareQueryElement((string) $key, $value, null, true, $isNewObj);
foreach ($preparedQueryElements as [$preparedKey, $preparedValue]) {
$preparedQuery[$preparedKey] = is_array($preparedValue)
? array_map('\Doctrine\ODM\MongoDB\Types\Type::convertPHPToDatabaseValue', $preparedValue)
: Type::convertPHPToDatabaseValue($preparedValue);
$preparedValue = Type::convertPHPToDatabaseValue($preparedValue);
if ($this->class->hasField($key)) {
$preparedValue = $this->convertToDatabaseValue($key, $preparedValue);
}
$preparedQuery[$preparedKey] = $preparedValue;
}
}

return $preparedQuery;
}

/**
* Converts a single value to its database representation based on the mapping type
*
* @param mixed $value
*
* @return mixed
*/
private function convertToDatabaseValue(string $fieldName, $value)
{
$mapping = $this->class->fieldMappings[$fieldName];
$typeName = $mapping['type'];

if (is_array($value)) {
foreach ($value as $k => $v) {
$value[$k] = $this->convertToDatabaseValue($fieldName, $v);
}

return $value;
}

if (! empty($mapping['reference']) || ! empty($mapping['embedded'])) {
return $value;
}

if (! Type::hasType($typeName)) {
throw new InvalidArgumentException(
sprintf('Mapping type "%s" does not exist', $typeName)
);
}
if (in_array($typeName, ['collection', 'hash'])) {
return $value;
}

$type = Type::getType($typeName);
$value = $type->convertToDatabaseValue($value);

return $value;
}

/**
* Prepares a query value and converts the PHP value to the database value
* if it is an identifier.
Expand Down Expand Up @@ -1244,6 +1285,15 @@ private function prepareQueryExpression(array $expression, ClassMetadata $class)
// Process query operators whose argument arrays need type conversion
if (in_array($k, ['$in', '$nin', '$all']) && is_array($v)) {
foreach ($v as $k2 => $v2) {
if ($v2 instanceof $class->name) {
// If a value in a query is a target document, e.g. ['referenceField' => $targetDocument],
// retreive id from target document and convert this id using it's type
$expression[$k][$k2] = $class->getDatabaseIdentifierValue($class->getIdentifierValue($v2));

continue;
}
// Otherwise if a value in a query is already id, e.g. ['referenceField' => $targetDocumentId],
// just convert id to it's database representation using it's type
$expression[$k][$k2] = $class->getDatabaseIdentifierValue($v2);
}
continue;
Expand All @@ -1255,7 +1305,11 @@ private function prepareQueryExpression(array $expression, ClassMetadata $class)
continue;
}

$expression[$k] = $class->getDatabaseIdentifierValue($v);
if ($v instanceof $class->name) {
$expression[$k] = $class->getDatabaseIdentifierValue($class->getIdentifierValue($v));
} else {
$expression[$k] = $class->getDatabaseIdentifierValue($v);
}
}

return $expression;
Expand Down
225 changes: 225 additions & 0 deletions tests/Doctrine/ODM/MongoDB/Tests/Functional/DocumentPersisterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@

namespace Doctrine\ODM\MongoDB\Tests\Functional;

use Closure;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\LockException;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use Doctrine\ODM\MongoDB\Persisters\DocumentPersister;
use Doctrine\ODM\MongoDB\Tests\BaseTest;
use Doctrine\ODM\MongoDB\Types\ClosureToPHP;
use Doctrine\ODM\MongoDB\Types\Type;
use Generator;
use InvalidArgumentException;
use MongoDB\BSON\ObjectId;
use MongoDB\Collection;
use ReflectionProperty;
use function get_class;
use function gettype;
use function is_object;
use function sprintf;

class DocumentPersisterTest extends BaseTest
{
Expand Down Expand Up @@ -176,6 +186,40 @@ public function testPrepareQueryOrNewObjWithHashIdAndInOperators($hashId)
$this->assertEquals($expected, $documentPersister->prepareQueryOrNewObj($value));
}

/**
* @dataProvider queryProviderForCustomTypeId
*/
public function testPrepareQueryOrNewObjWithCustomTypedId(array $expected, array $query)
{
$class = DocumentPersisterTestDocumentWithCustomId::class;
$documentPersister = $this->uow->getDocumentPersister($class);

Type::registerType('DocumentPersisterCustomId', DocumentPersisterCustomIdType::class);

$this->assertEquals(
$expected,
$documentPersister->prepareQueryOrNewObj($query)
);
}

/**
* @dataProvider queryProviderForDocumentWithReferenceToDocumentWithCustomTypedId
*/
public function testPrepareQueryOrNewObjWithReferenceToDocumentWithCustomTypedId(Closure $getTestCase)
{
Type::registerType('DocumentPersisterCustomId', DocumentPersisterCustomIdType::class);

$class = DocumentPersisterTestDocumentWithReferenceToDocumentWithCustomId::class;
$documentPersister = $this->uow->getDocumentPersister($class);

['query' => $query, 'expected' => $expected] = $getTestCase($this->dm);

$this->assertEquals(
$expected,
$documentPersister->prepareQueryOrNewObj($query)
);
}

public function provideHashIdentifiers()
{
return [
Expand Down Expand Up @@ -223,6 +267,84 @@ public function testPrepareQueryOrNewObjWithSimpleReferenceToTargetDocumentWithN
$this->assertEquals($expected, $documentPersister->prepareQueryOrNewObj($value));
}

public static function queryProviderForCustomTypeId() : Generator
{
$objectIdString = (string) new ObjectId();
$objectIdString2 = (string) new ObjectId();

$customId = DocumentPersisterCustomTypedId::fromString($objectIdString);
$customId2 = DocumentPersisterCustomTypedId::fromString($objectIdString2);

yield 'Direct comparison' => [
'expected' => ['_id' => new ObjectId($objectIdString)],
'query' => ['id' => $customId],
];

yield 'Operator with single value' => [
'expected' => ['_id' => ['$ne' => new ObjectId($objectIdString)]],
'query' => ['id' => ['$ne' => $customId]],
];

yield 'Operator with multiple values' => [
'expected' => ['_id' => ['$in' => [new ObjectId($objectIdString), new ObjectId($objectIdString2)]]],
'query' => ['id' => ['$in' => [$customId, $customId2]]],
];
}

public static function queryProviderForDocumentWithReferenceToDocumentWithCustomTypedId() : Generator
{
$getReference = static function (DocumentManager $dm) : DocumentPersisterTestDocumentWithCustomId {
$objectIdString = (string) new ObjectId();
$customId = DocumentPersisterCustomTypedId::fromString($objectIdString);

return $dm->getReference(
DocumentPersisterTestDocumentWithCustomId::class,
$customId
);
};

yield 'Direct comparison' => [
static function (DocumentManager $dm) use ($getReference) : array {
$ref = $getReference($dm);

return [
'query' => ['documentWithCustomId' => $ref],
'expected' => ['documentWithCustomId' => new ObjectId($ref->getId()->toString())],
];
},
];

yield 'Operator with single value' => [
static function (DocumentManager $dm) use ($getReference) : array {
$ref = $getReference($dm);

return [
'query' => ['documentWithCustomId' => ['$ne' => $ref]],
'expected' => ['documentWithCustomId' => ['$ne' => new ObjectId($ref->getId()->toString())]],
];
},
];

yield 'Operator with multiple values' => [
static function (DocumentManager $dm) use ($getReference) : array {
$ref1 = $getReference($dm);
$ref2 = $getReference($dm);

return [
'query' => ['documentWithCustomId' => ['$in' => [$ref1, $ref2]]],
'expected' => [
'documentWithCustomId' => [
'$in' => [
new ObjectId($ref1->getId()->toString()),
new ObjectId($ref2->getId()->toString()),
],
],
],
];
},
];
}

/**
* @dataProvider provideHashIdentifiers
*/
Expand Down Expand Up @@ -706,3 +828,106 @@ class DocumentPersisterWriteConcernAcknowledged
/** @ODM\Id */
public $id;
}

final class DocumentPersisterCustomTypedId
{
private $value;

private function __construct(string $value)
{
$this->value = $value;
}

public function toString() : string
{
return $this->value;
}

public static function fromString(string $value) : self
{
return new static($value);
}

public static function generate() : self
{
return new static((string) (new ObjectId()));
}
}

final class DocumentPersisterCustomIdType extends Type
{
use ClosureToPHP;

public function convertToDatabaseValue($value)
{
if ($value instanceof ObjectId) {
return $value;
}
if ($value instanceof DocumentPersisterCustomTypedId) {
return new ObjectId($value->toString());
}

throw self::createException($value);
}

public function convertToPHPValue($value)
{
if ($value instanceof DocumentPersisterCustomTypedId) {
return $value;
}
if ($value instanceof ObjectId) {
return DocumentPersisterCustomTypedId::fromString((string) $value);
}

throw self::createException($value);
}

private static function createException($value) : InvalidArgumentException
{
return new InvalidArgumentException(
sprintf(
'Expected "%s" or "%s", got "%s"',
DocumentPersisterCustomTypedId::class,
ObjectId::class,
is_object($value) ? get_class($value) : gettype($value)
)
);
}
}

/** @ODM\Document() */
class DocumentPersisterTestDocumentWithCustomId
{
/** @ODM\Id(strategy="NONE", type="DocumentPersisterCustomId") */
private $id;

public function __construct(DocumentPersisterCustomTypedId $id)
{
$this->id = $id;
}

public function getId() : DocumentPersisterCustomTypedId
{
return $this->id;
}
}

/** @ODM\Document() */
class DocumentPersisterTestDocumentWithReferenceToDocumentWithCustomId
{
/** @ODM\Id() */
private $id;

/** @ODM\ReferenceOne(targetDocument=DocumentPersisterTestDocumentWithCustomId::class, storeAs="id") */
private $documentWithCustomId;

public function __construct(DocumentPersisterTestDocumentWithCustomId $documentWithCustomId)
{
$this->documentWithCustomId = $documentWithCustomId;
}

public function getId() : DocumentPersisterCustomTypedId
{
return $this->id;
}
}