Skip to content

Commit 6b073cc

Browse files
committed
Enable multiple subject id's by adding the possibilty to add an identifier to them.
With this, you can now specify for every personal data which subject id should be used by providing the corresponding identifier.
1 parent d9846e8 commit 6b073cc

13 files changed

Lines changed: 285 additions & 115 deletions

baseline.xml

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,6 @@
3030
</MixedAssignment>
3131
</file>
3232
<file src="src/Metadata/AttributeMetadataFactory.php">
33-
<InvalidReturnStatement>
34-
<code><![CDATA[[false, null]]]></code>
35-
</InvalidReturnStatement>
36-
<InvalidReturnType>
37-
<code><![CDATA[array{bool, mixed, (callable(string, mixed):mixed)|null}]]></code>
38-
</InvalidReturnType>
3933
<PossiblyNullReference>
4034
<code><![CDATA[guess]]></code>
4135
</PossiblyNullReference>
@@ -82,23 +76,6 @@
8276
<code><![CDATA[$value]]></code>
8377
</MixedArgumentTypeCoercion>
8478
</file>
85-
<file src="tests/Benchmark/tideways.php">
86-
<ClassMustBeFinal>
87-
<code><![CDATA[CompiledMetadataHydrator]]></code>
88-
</ClassMustBeFinal>
89-
<MixedMethodCall>
90-
<code><![CDATA[normalize]]></code>
91-
<code><![CDATA[normalize]]></code>
92-
</MixedMethodCall>
93-
<MixedReturnTypeCoercion>
94-
<code><![CDATA[array]]></code>
95-
<code><![CDATA[match (true) {
96-
$object instanceof ProfileCreated => $this->extractProfileCreated($object),
97-
$object instanceof Skill => $this->extractSkill($object),
98-
default => throw new InvalidArgumentException('Unknown object type'),
99-
}]]></code>
100-
</MixedReturnTypeCoercion>
101-
</file>
10279
<file src="tests/Unit/Cryptography/Cipher/OpensslCipherTest.php">
10380
<MixedAssignment>
10481
<code><![CDATA[$return]]></code>

src/Attribute/DataSubjectId.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@
99
#[Attribute(Attribute::TARGET_PROPERTY)]
1010
final class DataSubjectId
1111
{
12+
public function __construct(
13+
public readonly string $identifier = 'default',
14+
) {
15+
}
1216
}

src/Attribute/PersonalData.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ final class PersonalData
1616
public function __construct(
1717
public readonly mixed $fallback = null,
1818
callable|null $fallbackCallable = null,
19+
public readonly string $identifier = 'default',
1920
) {
2021
$this->fallbackCallable = $fallbackCallable;
2122

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Hydrator\Cryptography;
6+
7+
use RuntimeException;
8+
9+
use function sprintf;
10+
11+
final class NotPersonalData extends RuntimeException
12+
{
13+
/** @param class-string $class */
14+
public function __construct(string $class, string $fieldName)
15+
{
16+
parent::__construct(sprintf('Trying to get subject id for %s::%s which is not marked as personal data.', $class, $fieldName));
17+
}
18+
}

src/Cryptography/PersonalDataPayloadCryptographer.php

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -36,24 +36,20 @@ public function __construct(
3636
*/
3737
public function encrypt(ClassMetadata $metadata, array $data): array
3838
{
39-
$subjectId = $this->subjectId($metadata, $data);
40-
41-
if ($subjectId === null) {
42-
return $data;
43-
}
44-
45-
try {
46-
$cipherKey = $this->cipherKeyStore->get($subjectId);
47-
} catch (CipherKeyNotExists) {
48-
$cipherKey = ($this->cipherKeyFactory)();
49-
$this->cipherKeyStore->store($subjectId, $cipherKey);
50-
}
51-
5239
foreach ($metadata->properties() as $propertyMetadata) {
5340
if (!$propertyMetadata->isPersonalData()) {
5441
continue;
5542
}
5643

44+
$subjectId = $this->subjectId($propertyMetadata, $metadata, $data);
45+
46+
try {
47+
$cipherKey = $this->cipherKeyStore->get($subjectId);
48+
} catch (CipherKeyNotExists) {
49+
$cipherKey = ($this->cipherKeyFactory)();
50+
$this->cipherKeyStore->store($subjectId, $cipherKey);
51+
}
52+
5753
$targetFieldName = $this->useEncryptedFieldName
5854
? $propertyMetadata->encryptedFieldName()
5955
: $propertyMetadata->fieldName();
@@ -80,23 +76,19 @@ public function encrypt(ClassMetadata $metadata, array $data): array
8076
*/
8177
public function decrypt(ClassMetadata $metadata, array $data): array
8278
{
83-
$subjectId = $this->subjectId($metadata, $data);
84-
85-
if ($subjectId === null) {
86-
return $data;
87-
}
88-
89-
try {
90-
$cipherKey = $this->cipherKeyStore->get($subjectId);
91-
} catch (CipherKeyNotExists) {
92-
$cipherKey = null;
93-
}
94-
9579
foreach ($metadata->properties() as $propertyMetadata) {
9680
if (!$propertyMetadata->isPersonalData()) {
9781
continue;
9882
}
9983

84+
$subjectId = $this->subjectId($propertyMetadata, $metadata, $data);
85+
86+
try {
87+
$cipherKey = $this->cipherKeyStore->get($subjectId);
88+
} catch (CipherKeyNotExists) {
89+
$cipherKey = null;
90+
}
91+
10092
if ($this->useEncryptedFieldName && array_key_exists($propertyMetadata->encryptedFieldName(), $data)) {
10193
$rawData = $data[$propertyMetadata->encryptedFieldName()];
10294
unset($data[$propertyMetadata->encryptedFieldName()]);
@@ -125,14 +117,20 @@ public function decrypt(ClassMetadata $metadata, array $data): array
125117
}
126118

127119
/** @param array<string, mixed> $data */
128-
private function subjectId(ClassMetadata $metadata, array $data): string|null
120+
private function subjectId(PropertyMetadata $propertyMetadata, ClassMetadata $metadata, array $data): string
129121
{
130-
$fieldName = $metadata->dataSubjectIdField();
122+
if (!$propertyMetadata->isPersonalData()) {
123+
throw new NotPersonalData($metadata->className(), $propertyMetadata->propertyName());
124+
}
131125

132-
if ($fieldName === null) {
133-
return null;
126+
$personalDataIdentifier = $propertyMetadata->personalDataIdentifier();
127+
128+
if (!$metadata->hasSubjectIdIdentifier($personalDataIdentifier)) {
129+
throw new MissingSubjectId($metadata->className(), $propertyMetadata->propertyName());
134130
}
135131

132+
$fieldName = $metadata->getSubjectIdFieldName($personalDataIdentifier);
133+
136134
if (!array_key_exists($fieldName, $data)) {
137135
throw new MissingSubjectId($metadata->className(), $fieldName);
138136
}

src/Metadata/AttributeMetadataFactory.php

Lines changed: 34 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ private function getClassMetadata(ReflectionClass $reflectionClass): ClassMetada
9999
$metadata = new ClassMetadata(
100100
$reflectionClass,
101101
$this->getPropertyMetadataList($reflectionClass),
102-
$this->getSubjectIdField($reflectionClass),
103102
$this->getPostHydrateCallbacks($reflectionClass),
104103
$this->getPreExtractCallbacks($reflectionClass),
105104
$this->getLazy($reflectionClass),
@@ -156,6 +155,7 @@ private function getPropertyMetadataList(ReflectionClass $reflectionClass): arra
156155
$reflectionProperty,
157156
$fieldName,
158157
$this->getNormalizer($reflectionProperty),
158+
$this->getSubjectId($reflectionProperty),
159159
...$this->getPersonalData($reflectionProperty),
160160
);
161161
}
@@ -270,82 +270,73 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla
270270
$properties[$property->fieldName()] = $property;
271271
}
272272

273-
$parentDataSubjectIdField = $parent->dataSubjectIdField();
274-
$childDataSubjectIdField = $child->dataSubjectIdField();
275-
276-
if ($parentDataSubjectIdField !== null && $childDataSubjectIdField !== null) {
277-
$parentProperty = $parent->propertyForField($parentDataSubjectIdField);
278-
$childProperty = $child->propertyForField($childDataSubjectIdField);
279-
280-
throw new MultipleDataSubjectId($parentProperty->propertyName(), $childProperty->propertyName());
281-
}
282-
283-
return new ClassMetadata(
273+
$mergedClassMetadata = new ClassMetadata(
284274
$parent->reflection(),
285275
array_values($properties),
286-
$parentDataSubjectIdField ?? $childDataSubjectIdField,
287276
array_merge($parent->postHydrateCallbacks(), $child->postHydrateCallbacks()),
288277
array_merge($parent->preExtractCallbacks(), $child->preExtractCallbacks()),
289278
$child->lazy() ?? $parent->lazy(),
290279
);
291-
}
292280

293-
/** @param ReflectionClass<object> $reflectionClass */
294-
private function getSubjectIdField(ReflectionClass $reflectionClass): string|null
295-
{
296-
$property = null;
281+
$this->validate($mergedClassMetadata);
297282

298-
foreach ($reflectionClass->getProperties() as $reflectionProperty) {
299-
$attributeReflectionList = $reflectionProperty->getAttributes(DataSubjectId::class);
300-
301-
if (!$attributeReflectionList) {
302-
continue;
303-
}
304-
305-
if ($property !== null) {
306-
throw new MultipleDataSubjectId($property->getName(), $reflectionProperty->getName());
307-
}
283+
return $mergedClassMetadata;
284+
}
308285

309-
$property = $reflectionProperty;
310-
}
286+
private function getSubjectId(ReflectionProperty $reflectionProperty): string|null
287+
{
288+
$attributeReflectionList = $reflectionProperty->getAttributes(DataSubjectId::class);
311289

312-
if ($property === null) {
290+
if (!$attributeReflectionList) {
313291
return null;
314292
}
315293

316-
return $this->getFieldName($property);
294+
return $attributeReflectionList[0]->newInstance()->identifier;
317295
}
318296

319-
/** @return array{bool, mixed, (callable(string, mixed):mixed)|null} */
297+
/** @return array{string|null, mixed, (callable(string, mixed):mixed)|null} */
320298
private function getPersonalData(ReflectionProperty $reflectionProperty): array
321299
{
322300
$attributeReflectionList = $reflectionProperty->getAttributes(PersonalData::class);
323301

324302
if ($attributeReflectionList === []) {
325-
return [false, null];
303+
return [null, null, null];
326304
}
327305

328306
$attribute = $attributeReflectionList[0]->newInstance();
329307

330-
return [true, $attribute->fallback, $attribute->fallbackCallable];
308+
return [$attribute->identifier, $attribute->fallback, $attribute->fallbackCallable];
331309
}
332310

333311
private function validate(ClassMetadata $metadata): void
334312
{
335-
$hasPersonalData = false;
313+
$subjectIds = [];
336314

337315
foreach ($metadata->properties() as $property) {
338-
if ($property->isPersonalData()) {
339-
$hasPersonalData = true;
316+
if ($property->isPersonalData() && $property->isSubjectId()) {
317+
throw new SubjectIdAndPersonalDataConflict($metadata->className(), $property->propertyName());
340318
}
341319

342-
if ($property->isPersonalData() && $metadata->dataSubjectIdField() === $property->fieldName()) {
343-
throw new SubjectIdAndPersonalDataConflict($metadata->className(), $property->propertyName());
320+
if ($property->isPersonalData() && !$metadata->hasSubjectIdIdentifier($property->personalDataIdentifier())) {
321+
throw new MissingDataSubjectId($metadata->className());
322+
}
323+
324+
if (!$property->isSubjectId()) {
325+
continue;
326+
}
327+
328+
$subjectIdIdentifier = $property->subjectIdIdentifier();
329+
330+
if (array_key_exists($subjectIdIdentifier, $subjectIds)) {
331+
throw new DuplicateSubjectIdIdentifier(
332+
$metadata->className(),
333+
$subjectIds[$subjectIdIdentifier],
334+
$property->propertyName(),
335+
$subjectIdIdentifier,
336+
);
344337
}
345-
}
346338

347-
if ($hasPersonalData && $metadata->dataSubjectIdField() === null) {
348-
throw new MissingDataSubjectId($metadata->className());
339+
$subjectIds[$subjectIdIdentifier] = $property->propertyName();
349340
}
350341
}
351342

src/Metadata/ClassMetadata.php

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
namespace Patchlevel\Hydrator\Metadata;
66

77
use ReflectionClass;
8+
use RuntimeException;
89

910
/**
1011
* @psalm-type serialized array{
1112
* className: class-string,
1213
* properties: list<PropertyMetadata>,
13-
* dataSubjectIdField: string|null,
1414
* postHydrateCallbacks: list<CallbackMetadata>,
1515
* preExtractCallbacks: list<CallbackMetadata>,
1616
* lazy: bool|null,
@@ -28,7 +28,6 @@ final class ClassMetadata
2828
public function __construct(
2929
private readonly ReflectionClass $reflection,
3030
private readonly array $properties = [],
31-
private readonly string|null $dataSubjectIdField = null,
3231
private readonly array $postHydrateCallbacks = [],
3332
private readonly array $preExtractCallbacks = [],
3433
private readonly bool|null $lazy = null,
@@ -70,11 +69,6 @@ public function lazy(): bool|null
7069
return $this->lazy;
7170
}
7271

73-
public function dataSubjectIdField(): string|null
74-
{
75-
return $this->dataSubjectIdField;
76-
}
77-
7872
public function propertyForField(string $name): PropertyMetadata
7973
{
8074
foreach ($this->properties as $property) {
@@ -86,6 +80,28 @@ public function propertyForField(string $name): PropertyMetadata
8680
throw PropertyMetadataNotFound::withName($name);
8781
}
8882

83+
public function hasSubjectIdIdentifier(string $subjectIdIdentifier): bool
84+
{
85+
foreach ($this->properties as $property) {
86+
if ($property->subjectIdIdentifier() === $subjectIdIdentifier) {
87+
return true;
88+
}
89+
}
90+
91+
return false;
92+
}
93+
94+
public function getSubjectIdFieldName(string $subjectIdIdentifier): string
95+
{
96+
foreach ($this->properties as $property) {
97+
if ($property->subjectIdIdentifier() === $subjectIdIdentifier) {
98+
return $property->fieldName();
99+
}
100+
}
101+
102+
throw new RuntimeException('No subject id');
103+
}
104+
89105
/** @return T */
90106
public function newInstance(): object
91107
{
@@ -98,7 +114,6 @@ public function __serialize(): array
98114
return [
99115
'className' => $this->reflection->getName(),
100116
'properties' => $this->properties,
101-
'dataSubjectIdField' => $this->dataSubjectIdField,
102117
'postHydrateCallbacks' => $this->postHydrateCallbacks,
103118
'preExtractCallbacks' => $this->preExtractCallbacks,
104119
'lazy' => $this->lazy,
@@ -110,7 +125,6 @@ public function __unserialize(array $data): void
110125
{
111126
$this->reflection = new ReflectionClass($data['className']);
112127
$this->properties = $data['properties'];
113-
$this->dataSubjectIdField = $data['dataSubjectIdField'];
114128
$this->postHydrateCallbacks = $data['postHydrateCallbacks'];
115129
$this->preExtractCallbacks = $data['preExtractCallbacks'];
116130
$this->lazy = $data['lazy'];

0 commit comments

Comments
 (0)