Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions docs/en/reference/basic-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ Here is a quick overview of the built-in mapping types:
- ``hash``
- ``id``
- ``int``
- ``int64``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Contrary to the PR description, int64 might have uses beyond encryption such as satisfying a general schema or improving compatibility in cross-language apps (other drivers might not play nice with interchangeable integer types).

Copy link
Member Author

@GromNaN GromNaN Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, the new type can be used independently from encryption.

Note that the int type will encode to Int64 when the value exceeds 32 bits. That would be a programmer issue. I could add an Int32 type to throw an error when the value does not fit in 32 bits.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the int type will encode to Int64 when the value exceeds 32 bits.

That's legacy behavior for ODM and the expected behavior for the server (e.g. incrementing an int32 BSON type server-side could yield an int64), so I'm not worried about it. And so long as ODM continues to accept int64 BSON types for int mappings I think we're OK.

I could add an Int32 type to throw an error when the value does not fit in 32 bits.

This could certainly be added down the line. I'd probably wait until it was requested, though.

- ``key``
- ``object_id``
- ``raw``
Expand Down
10 changes: 10 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -2386,6 +2386,16 @@ public function mapField(array $mapping): array
$mapping['nullable'] = false;
}

if (isset($mapping['encrypt']['queryType'])) {
// The encrypted range query options min and max must be converted to the database type
$type = Type::getType($mapping['type']);
foreach (['min', 'max'] as $option) {
if (isset($mapping['encrypt'][$option])) {
$mapping['encrypt'][$option] = $type->convertToDatabaseValue($mapping['encrypt'][$option]);
}
}
}

if (
isset($mapping['reference'])
&& isset($mapping['storeAs'])
Expand Down
3 changes: 1 addition & 2 deletions lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
use Doctrine\ODM\MongoDB\Mapping\MappingException;
use Doctrine\ODM\MongoDB\Mapping\TimeSeries\Granularity;
use Doctrine\ODM\MongoDB\Types\Type;
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
use Doctrine\Persistence\Mapping\Driver\FileDriver;
use DOMDocument;
Expand Down Expand Up @@ -941,7 +940,7 @@ private function addEncryptionMapping(SimpleXMLElement $encrypt, string $type):
foreach ($encrypt->attributes() as $key => $value) {
$encryptMapping[$key] = match ($key) {
'queryType' => EncryptQuery::from((string) $value),
'min', 'max' => Type::getType($type)->convertToDatabaseValue((string) $value),
'min', 'max' => (string) $value,
'sparsity', 'precision', 'trimFactor', 'contention' => (int) $value,
};
}
Expand Down
23 changes: 23 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/Int64Type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace Doctrine\ODM\MongoDB\Types;

use MongoDB\BSON\Int64;

/**
* The Int64 type (long)
*/
class Int64Type extends IntType implements Incrementable, Versionable
{
public function convertToDatabaseValue($value)
{
return $value !== null ? new Int64($value) : null;
}

public function closureToMongo(): string
{
return '$return = new \MongoDB\BSON\Int64($value);';
}
}
2 changes: 2 additions & 0 deletions lib/Doctrine/ODM/MongoDB/Types/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ abstract class Type
public const CUSTOMID = 'custom_id';
public const BOOL = 'bool';
public const INT = 'int';
public const INT64 = 'int64';
public const FLOAT = 'float';
public const STRING = 'string';
public const DATE = 'date';
Expand Down Expand Up @@ -66,6 +67,7 @@ abstract class Type
self::BOOLEAN => Types\BooleanType::class,
self::INT => Types\IntType::class,
self::INTEGER => Types\IntType::class,
self::INT64 => Types\Int64Type::class,
self::FLOAT => Types\FloatType::class,
self::STRING => Types\StringType::class,
self::DATE => Types\DateType::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ private function createEncryptedFieldsMapForClass(
ClassMetadata::ONE, Type::HASH => 'object',
ClassMetadata::MANY, Type::COLLECTION => 'array',
Type::INT, Type::INTEGER => 'int',
Type::INT64 => 'long',
Type::FLOAT => 'double',
Type::DECIMAL128 => 'decimal',
Type::DATE, Type::DATE_IMMUTABLE => 'date',
Expand Down
14 changes: 7 additions & 7 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1999,43 +1999,43 @@ parameters:
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php

-
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:199\:\:__construct\(\) has parameter \$classNames with no value type specified in iterable type array\.$#'
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:212\:\:__construct\(\) has parameter \$classNames with no value type specified in iterable type array\.$#'
identifier: missingType.iterableValue
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php

-
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:199\:\:doLoadMetadata\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:212\:\:doLoadMetadata\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php

-
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:199\:\:doLoadMetadata\(\) has parameter \$parent with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:212\:\:doLoadMetadata\(\) has parameter \$parent with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php

-
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:199\:\:getAllMetadata\(\) return type with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata does not specify its types\: T$#'
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:212\:\:getAllMetadata\(\) return type with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php

-
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:199\:\:initializeReflection\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:212\:\:initializeReflection\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php

-
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:199\:\:isEntity\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:212\:\:isEntity\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php

-
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:199\:\:wakeupReflection\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
message: '#^Method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory@anonymous/tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest\.php\:212\:\:wakeupReflection\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#'
identifier: missingType.generics
count: 1
path: tests/Doctrine/ODM/MongoDB/Tests/Tools/EncryptedFieldsMapGeneratorTest.php
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Documents\Phonenumber;
use Documents\Profile;
use MongoDB\BSON\Decimal128;
use MongoDB\BSON\Int64;
use MongoDB\BSON\UTCDateTime;
use PHPUnit\Framework\TestCase;
use TestDocuments\EmbeddedDocument;
Expand All @@ -26,6 +27,8 @@
use TestDocuments\QueryResultDocument;
use TestDocuments\User;

use const PHP_INT_MAX;

abstract class AbstractDriverTestCase extends TestCase
{
protected MappingDriver|null $driver;
Expand Down Expand Up @@ -573,6 +576,12 @@ public function testEncryptQueryRangeTypes(): void
'max' => 10,
], $classMetadata->fieldMappings['intField']['encrypt']);

self::assertEquals([
'queryType' => EncryptQuery::Range,
'min' => new Int64(5),
'max' => new Int64(PHP_INT_MAX - 5),
], $classMetadata->fieldMappings['int64Field']['encrypt']);

self::assertEquals([
'queryType' => EncryptQuery::Range,
'min' => 5.5,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<field name="intField" type="int">
<encrypt queryType="range" min="5" max="10"/>
</field>
<field name="int64Field" type="int64">
<encrypt queryType="range" min="5" max="9223372036854775802"/>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it even necessary to use PHP_INT_MAX for this example? Since Int64 types allow comparisons with integers, the assertion in AbstractDriverTestCase doesn't actually check that the value is converted to an Int64 object. It may make more sense to use relaxed numeric comparisons for the assertEquals and then also assert that the value is an instance of Int64. In that case you can do without the large values here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a type assertion in TypeTest.

My idea for using a low and a high value for the min and max was to test that they are both converted to Int64. I could add a functional test for QE.

</field>
<field name="floatField" type="float">
<encrypt queryType="range" min="5.5" max="10.5"/>
</field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@
use Documents\Encryption\PatientRecord;
use Documents\Encryption\RangeTypes;
use MongoDB\BSON\Decimal128;
use MongoDB\BSON\Int64;
use MongoDB\BSON\UTCDateTime;

use function array_map;

use const PHP_INT_MAX;

class EncryptedFieldsMapGeneratorTest extends BaseTestCase
{
public function testGetEncryptionFieldsMapForClass(): void
Expand Down Expand Up @@ -90,6 +93,16 @@ public function testVariousRangeTypes(): void
'keyId' => null,
'queries' => ['queryType' => 'range', 'min' => 5, 'max' => 10],
],
[
'path' => 'int64Field',
'bsonType' => 'long',
'keyId' => null,
'queries' => [
'queryType' => 'range',
'min' => new Int64(5),
'max' => new Int64(PHP_INT_MAX - 5),
],
],
[
'path' => 'floatField',
'bsonType' => 'double',
Expand Down
63 changes: 36 additions & 27 deletions tests/Doctrine/ODM/MongoDB/Tests/Types/TypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
use Doctrine\ODM\MongoDB\Types\Type;
use MongoDB\BSON\Binary;
use MongoDB\BSON\Decimal128;
use MongoDB\BSON\Int64;
use MongoDB\BSON\MaxKey;
use MongoDB\BSON\MinKey;
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\Timestamp;
use MongoDB\BSON\UTCDateTime;
Expand All @@ -24,40 +27,46 @@

class TypeTest extends BaseTestCase
{
/** @param mixed $test */
#[DataProvider('provideTypes')]
public function testConversion(Type $type, $test): void
public function testConversion(string $typeName, mixed $phpValue, mixed $bsonValue = null): void
{
self::assertEquals($test, $type->convertToPHPValue($type->convertToDatabaseValue($test)));
$bsonValue ??= $phpValue;
$type = Type::getType($typeName);

self::assertEquals($phpValue, $type->convertToPHPValue($bsonValue));
self::assertEquals($bsonValue, $type->convertToDatabaseValue($phpValue));
}

public static function provideTypes(): array
{
$array = ['foo' => 'bar'];

return [
'id' => [Type::getType(Type::ID), '507f1f77bcf86cd799439011'],
'intId' => [Type::getType(Type::INTID), 1],
'customId' => [Type::getType(Type::CUSTOMID), (object) ['foo' => 'bar']],
'bool' => [Type::getType(Type::BOOL), true],
'boolean' => [Type::getType(Type::BOOLEAN), false],
'int' => [Type::getType(Type::INT), 69],
'integer' => [Type::getType(Type::INTEGER), 42],
'float' => [Type::getType(Type::FLOAT), 3.14],
'string' => [Type::getType(Type::STRING), 'ohai'],
'minKey' => [Type::getType(Type::KEY), 0],
'maxKey' => [Type::getType(Type::KEY), 1],
'timestamp' => [Type::getType(Type::TIMESTAMP), time()],
'binData' => [Type::getType(Type::BINDATA), 'foobarbaz'],
'binDataFunc' => [Type::getType(Type::BINDATAFUNC), 'foobarbaz'],
'binDataByteArray' => [Type::getType(Type::BINDATABYTEARRAY), 'foobarbaz'],
'binDataUuid' => [Type::getType(Type::BINDATAUUID), 'testtesttesttest'],
'binDataUuidRFC4122' => [Type::getType(Type::BINDATAUUIDRFC4122), str_repeat('a', 16)],
'binDataMD5' => [Type::getType(Type::BINDATAMD5), md5('ODM')],
'binDataCustom' => [Type::getType(Type::BINDATACUSTOM), 'foobarbaz'],
'hash' => [Type::getType(Type::HASH), ['foo' => 'bar']],
'collection' => [Type::getType(Type::COLLECTION), ['foo', 'bar']],
'objectId' => [Type::getType(Type::OBJECTID), '507f1f77bcf86cd799439011'],
'raw' => [Type::getType(Type::RAW), (object) ['foo' => 'bar']],
'decimal128' => [Type::getType(Type::DECIMAL128), '4.20'],
'id' => [Type::ID, '507f1f77bcf86cd799439011', new ObjectId('507f1f77bcf86cd799439011')],
'intId' => [Type::INTID, 1],
'customId' => [Type::CUSTOMID, (object) ['foo' => 'bar']],
'bool' => [Type::BOOL, true],
'boolean' => [Type::BOOLEAN, false],
'int' => [Type::INT, 69],
'integer' => [Type::INTEGER, 42],
'int64' => [Type::INT64, 9223372036854775807, new Int64(9223372036854775807)],
'float' => [Type::FLOAT, 3.14],
'string' => [Type::STRING, 'ohai'],
'minKey' => [Type::KEY, 0, new MinKey()],
'maxKey' => [Type::KEY, 1, new MaxKey()],
'timestamp' => [Type::TIMESTAMP, $t = time(), new Timestamp(0, $t)],
'binData' => [Type::BINDATA, 'foobarbaz'],
'binDataFunc' => [Type::BINDATAFUNC, 'foobarbaz'],
'binDataByteArray' => [Type::BINDATABYTEARRAY, 'foobarbaz'],
'binDataUuid' => [Type::BINDATAUUID, 'testtesttesttest'],
'binDataUuidRFC4122' => [Type::BINDATAUUIDRFC4122, str_repeat('a', 16)],
'binDataMD5' => [Type::BINDATAMD5, md5('ODM')],
'binDataCustom' => [Type::BINDATACUSTOM, 'foobarbaz'],
'hash' => [Type::HASH, ['foo' => 'bar'], (object) ['foo' => 'bar']],
'collection' => [Type::COLLECTION, ['foo', 'bar']],
'objectId' => [Type::OBJECTID, '507f1f77bcf86cd799439011'],
'raw' => [Type::RAW, (object) ['foo' => 'bar']],
'decimal128' => [Type::DECIMAL128, '4.20'],
];
}

Expand Down
6 changes: 6 additions & 0 deletions tests/Documents/Encryption/RangeTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
use Doctrine\ODM\MongoDB\Types\Type;
use MongoDB\BSON\Decimal128;

use const PHP_INT_MAX;

/**
* Test all supported types for range encrypted queries.
*
Expand All @@ -28,6 +30,10 @@ class RangeTypes
#[Encrypt(EncryptQuery::Range, min: 5, max: 10)]
public int $intField;

#[Field(type: Type::INT64)]
#[Encrypt(EncryptQuery::Range, min: 5, max: PHP_INT_MAX - 5)]
public int $int64Field;

#[Field(type: Type::FLOAT)]
#[Encrypt(EncryptQuery::Range, min: 5.5, max: 10.5, precision: 1)]
public float $floatField;
Expand Down