Skip to content

Commit 4f34c70

Browse files
committed
Create encryption field map
1 parent 9eedf0d commit 4f34c70

File tree

8 files changed

+128
-15
lines changed

8 files changed

+128
-15
lines changed

lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Encrypt.php

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,34 @@
66

77
use Attribute;
88
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
9+
use InvalidArgumentException;
910
use MongoDB\BSON\Type;
1011
use MongoDB\Driver\ClientEncryption;
1112

13+
use function in_array;
14+
1215
/**
13-
* Defines an index on a field
16+
* Defines an encrypted field mapping.
17+
*
18+
* @see https://www.mongodb.com/docs/manual/core/queryable-encryption/fundamentals/encrypt-and-query/#configure-encrypted-fields-for-optimal-search-and-storage
1419
*
1520
* @Annotation
1621
* @NamedArgumentConstructor
1722
*/
1823
#[Attribute(Attribute::TARGET_PROPERTY)]
19-
final class Encrypt
24+
final class Encrypt implements Annotation
2025
{
26+
public const QUERY_TYPE_EQUALITY = ClientEncryption::QUERY_TYPE_EQUALITY;
27+
public const QUERY_TYPE_RANGE = ClientEncryption::QUERY_TYPE_RANGE;
28+
2129
/**
22-
* @link https://www.mongodb.com/docs/manual/core/queryable-encryption/fundamentals/encrypt-and-query/#configure-encrypted-fields-for-optimal-search-and-storage
23-
*
24-
* @param ClientEncryption::QUERY_TYPE_* $queryType
25-
* @param int<1, 4>|null $sparsity
26-
* @param positive-int|null $prevision
27-
* @param positive-int|null $trimFactor
28-
* @param positive-int|null $contention
30+
* @param self::QUERY_TYPE_*|null $queryType Set the query type for the field, null if not queryable.
31+
* @param int<1, 4>|null $sparsity
32+
* @param positive-int|null $prevision
33+
* @param positive-int|null $trimFactor
34+
* @param positive-int|null $contention
2935
*/
3036
public function __construct(
31-
public ?string $bsonType = null, // Should be extracted from the field type
3237
public ?string $queryType = null,
3338
public string|int|Type|null $min = null,
3439
public string|int|Type|null $max = null,
@@ -37,5 +42,8 @@ public function __construct(
3742
public ?int $trimFactor = null,
3843
public ?int $contention = null,
3944
) {
45+
if ($this->queryType && ! in_array($this->queryType, [self::QUERY_TYPE_EQUALITY, self::QUERY_TYPE_RANGE], true)) {
46+
throw new InvalidArgumentException('Invalid query type');
47+
}
4048
}
4149
}

lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
* order?: int|string,
108108
* background?: bool,
109109
* enumType?: class-string<BackedEnum>,
110+
* encrypt?: array{queryType?: ?string, min?: mixed, max?: mixed, sparsity?: int<1, 4>, prevision?: int, trimFactor?: int, contention?: int}
110111
* }
111112
* @phpstan-type FieldMapping array{
112113
* type: string,
@@ -153,6 +154,7 @@
153154
* alsoLoadFields?: list<string>,
154155
* enumType?: class-string<BackedEnum>,
155156
* storeEmptyArray?: bool,
157+
* encrypt?: array{queryType?: ?string, min?: mixed, max?: mixed, sparsity?: int<1, 4>, prevision?: int, trimFactor?: int, contention?: int},
156158
* }
157159
* @phpstan-type AssociationFieldMapping array{
158160
* type?: string,

lib/Doctrine/ODM/MongoDB/Mapping/Driver/AttributeDriver.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
use function trigger_deprecation;
3333

3434
/**
35-
* The AtttributeDriver reads the mapping metadata from attributes.
35+
* The AttributeDriver reads the mapping metadata from attributes.
3636
*/
3737
class AttributeDriver implements MappingDriver
3838
{
@@ -264,6 +264,8 @@ public function loadMetadataForClass($className, PersistenceClassMetadata $metad
264264
$mapping['version'] = true;
265265
} elseif ($propertyAttribute instanceof ODM\Lock) {
266266
$mapping['lock'] = true;
267+
} elseif ($propertyAttribute instanceof ODM\Encrypt) {
268+
$mapping['encrypt'] = (array) $propertyAttribute;
267269
}
268270
}
269271

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ODM\MongoDB\Utility;
6+
7+
use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactoryInterface;
8+
use Generator;
9+
10+
use function array_filter;
11+
use function iterator_to_array;
12+
13+
final class EncryptionFieldMap
14+
{
15+
public function __construct(private ClassMetadataFactoryInterface $classMetadataFactory)
16+
{
17+
}
18+
19+
/**
20+
* Generate the encryption field map from the class metadata.
21+
*
22+
* @param class-string $className
23+
*/
24+
public function getEncryptionFieldMap(string $className): array
25+
{
26+
return iterator_to_array($this->createEncryptionFieldMap($className));
27+
}
28+
29+
private function createEncryptionFieldMap(string $className, string $path = ''): Generator
30+
{
31+
$classMetadata = $this->classMetadataFactory->getMetadataFor($className);
32+
foreach ($classMetadata->fieldMappings as $mapping) {
33+
// Add fields recursively
34+
if ($mapping['embedded'] ?? false) {
35+
yield from $this->createEncryptionFieldMap($mapping['targetDocument'], $path . $mapping['name'] . '.');
36+
}
37+
38+
if (! isset($mapping['encrypt'])) {
39+
continue;
40+
}
41+
42+
$field = [
43+
'name' => $path . $mapping['name'],
44+
'type' => match ($mapping['type']) {
45+
'one' => 'object',
46+
'many' => 'array',
47+
default => $mapping['type'],
48+
},
49+
// @todo allow setting a keyId in #[Encrypt] attribute
50+
'keyId' => null, // Generate the key automatically
51+
];
52+
53+
// When queryType is null, the field is not queryable
54+
if (isset($mapping['encrypt']['queryType'])) {
55+
$field['queries'] = array_filter($mapping['encrypt'], static fn ($v) => $v !== null);
56+
}
57+
58+
yield $field;
59+
}
60+
}
61+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ODM\MongoDB\Tests;
6+
7+
use Doctrine\ODM\MongoDB\Utility\EncryptionFieldMap;
8+
use Documents\Encryption\Patient;
9+
10+
class EncryptionTest extends BaseTestCase
11+
{
12+
public function testMetadataIsEncrypted(): void
13+
{
14+
$factory = new EncryptionFieldMap($this->dm->getMetadataFactory());
15+
$encryptedFieldsMap = $factory->getEncryptionFieldMap(Patient::class);
16+
17+
$expected = [
18+
[
19+
'name' => 'patientRecord.ssn',
20+
'type' => 'string',
21+
'keyId' => null,
22+
'queries' => ['queryType' => 'equality'],
23+
],
24+
[
25+
'name' => 'patientRecord.billing',
26+
'type' => 'object',
27+
'keyId' => null,
28+
],
29+
[
30+
'name' => 'patientRecord.billingAmount',
31+
'type' => 'int',
32+
'keyId' => null,
33+
'queries' => ['queryType' => 'range', 'min' => 100, 'max' => 2000, 'sparsity' => 1, 'trimFactor' => 4],
34+
],
35+
];
36+
37+
self::assertSame($expected, $encryptedFieldsMap);
38+
}
39+
}

tests/Documents/Encrypted/Patient.php renamed to tests/Documents/Encryption/Patient.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Documents\Encrypted;
5+
namespace Documents\Encryption;
66

77
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
88

tests/Documents/Encrypted/PatientBilling.php renamed to tests/Documents/Encryption/PatientBilling.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Documents\Encrypted;
5+
namespace Documents\Encryption;
66

77
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
88
use Doctrine\ODM\MongoDB\Mapping\Annotations\EmbeddedDocument;

tests/Documents/Encrypted/PatientRecord.php renamed to tests/Documents/Encryption/PatientRecord.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Documents\Encrypted;
5+
namespace Documents\Encryption;
66

77
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
88
use Doctrine\ODM\MongoDB\Mapping\Annotations\EmbeddedDocument;
@@ -14,13 +14,14 @@ class PatientRecord
1414
public ?string $id;
1515

1616
#[ODM\Field]
17-
#[ODM\Encrypt]
17+
#[ODM\Encrypt(queryType: ODM\Encrypt::QUERY_TYPE_EQUALITY)]
1818
public string $ssn;
1919

2020
#[ODM\EmbedOne(targetDocument: PatientBilling::class)]
2121
#[ODM\Encrypt]
2222
public PatientBilling $billing;
2323

2424
#[ODM\Field]
25+
#[ODM\Encrypt(queryType: ODM\Encrypt::QUERY_TYPE_RANGE, sparsity: 1, trimFactor: 4, min: 100, max: 2000)]
2526
public int $billingAmount;
2627
}

0 commit comments

Comments
 (0)