Skip to content
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
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ parameters:
count: 1
path: src/Extension/Cryptography/Cipher/OpensslCipherKeyFactory.php

-
message: '#^Cannot access property \$nameToField on mixed\.$#'
identifier: property.nonObject
count: 1
path: src/Extension/Cryptography/LegacyCryptographyMetadataEnricher.php

-
message: '#^Method Patchlevel\\Hydrator\\Guesser\\BuiltInGuesser\:\:guess\(\) has parameter \$type with generic class Symfony\\Component\\TypeInfo\\Type\\ObjectType but does not specify its types\: T$#'
identifier: missingType.generics
Expand Down
5 changes: 5 additions & 0 deletions src/Extension/Cryptography/CryptographyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
public function __construct(
private readonly Cryptographer $cryptography,
private readonly PayloadCryptographer|null $legacyCryptographer = null,
private readonly bool $legacyMetadataMapping = false,
) {
}

Expand All @@ -22,6 +23,10 @@
$builder->addMetadataEnricher(new CryptographyMetadataEnricher(), 64);
$builder->addMiddleware(new CryptographyMiddleware($this->cryptography), 64);

if ($this->legacyMetadataMapping) {
$builder->addMetadataEnricher(new LegacyCryptographyMetadataEnricher(), 63);

Check warning on line 27 in src/Extension/Cryptography/CryptographyExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "IncrementInteger": @@ @@ $builder->addMiddleware(new CryptographyMiddleware($this->cryptography), 64); if ($this->legacyMetadataMapping) { - $builder->addMetadataEnricher(new LegacyCryptographyMetadataEnricher(), 63); + $builder->addMetadataEnricher(new LegacyCryptographyMetadataEnricher(), 64); } if ($this->legacyCryptographer === null) {

Check warning on line 27 in src/Extension/Cryptography/CryptographyExtension.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "DecrementInteger": @@ @@ $builder->addMiddleware(new CryptographyMiddleware($this->cryptography), 64); if ($this->legacyMetadataMapping) { - $builder->addMetadataEnricher(new LegacyCryptographyMetadataEnricher(), 63); + $builder->addMetadataEnricher(new LegacyCryptographyMetadataEnricher(), 62); } if ($this->legacyCryptographer === null) {
}

if ($this->legacyCryptographer === null) {
return;
}
Expand Down
82 changes: 82 additions & 0 deletions src/Extension/Cryptography/LegacyCryptographyMetadataEnricher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Cryptography;

use Patchlevel\Hydrator\Attribute\DataSubjectId;
use Patchlevel\Hydrator\Attribute\PersonalData;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Metadata\MetadataEnricher;
use ReflectionProperty;

use function array_key_exists;
use function in_array;

final class LegacyCryptographyMetadataEnricher implements MetadataEnricher
{
private const SUBJECT_ID = 'legacy';

public function enrich(ClassMetadata $classMetadata): void
{
/** @var array<string, string> $subjectIdMapping */
$subjectIdMapping = isset($classMetadata->extras[SubjectIdFieldMapping::class])
? $classMetadata->extras[SubjectIdFieldMapping::class]->nameToField
: [];

foreach ($classMetadata->properties as $property) {
$attributeReflectionList = $property->reflection->getAttributes(DataSubjectId::class);

if ($attributeReflectionList) {
if (array_key_exists(self::SUBJECT_ID, $subjectIdMapping)) {
throw new DuplicateSubjectIdIdentifier(
$classMetadata->className,
$classMetadata->propertyForField($subjectIdMapping[self::SUBJECT_ID])->propertyName,
$property->propertyName,
self::SUBJECT_ID,
);
}

$subjectIdMapping[self::SUBJECT_ID] = $property->fieldName;
}

$sensitiveDataInfo = $this->sensitiveDataInfo($property->reflection);

if (!$sensitiveDataInfo) {
continue;
}

if (in_array($property->fieldName, $subjectIdMapping, true)) {
throw new SubjectIdAndSensitiveDataConflict($classMetadata->className, $property->propertyName);
}

if (isset($property->extras[SensitiveDataInfo::class])) {
throw new PersonalDataAndSensitiveDataOnSameProperty($classMetadata->className, $property->propertyName);
}

$property->extras[SensitiveDataInfo::class] = $sensitiveDataInfo;
}

if ($subjectIdMapping === []) {
return;
}

$classMetadata->extras[SubjectIdFieldMapping::class] = new SubjectIdFieldMapping($subjectIdMapping);
}

private function sensitiveDataInfo(ReflectionProperty $reflectionProperty): SensitiveDataInfo|null
{
$attributeReflectionList = $reflectionProperty->getAttributes(PersonalData::class);

if ($attributeReflectionList === []) {
return null;
}

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

return new SensitiveDataInfo(
self::SUBJECT_ID,
$attribute->fallbackCallable !== null ? ($attribute->fallbackCallable)(...) : $attribute->fallback,
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Cryptography;

use Patchlevel\Hydrator\Metadata\MetadataException;
use RuntimeException;

use function sprintf;

/** @experimental */
final class PersonalDataAndSensitiveDataOnSameProperty extends RuntimeException implements MetadataException
{
/** @param class-string $class */
public function __construct(string $class, string $property)
{
parent::__construct(
sprintf(
'Personal data and sensitive data cannot be used on the same property %s::%s',
$class,
$property,
),
);
}
}
39 changes: 32 additions & 7 deletions src/StackHydratorBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,11 @@ public function setCache(CacheItemPoolInterface|CacheInterface|null $cache): sta

public function build(): StackHydrator
{
krsort($this->guessers);
krsort($this->metadataEnrichers);
krsort($this->middlewares);

$metadataFactory = new EnrichingMetadataFactory(
new AttributeMetadataFactory(
guesser: new ChainGuesser(array_merge(...$this->guessers)),
guesser: new ChainGuesser($this->guessers()),
),
array_merge(...$this->metadataEnrichers),
$this->metadataEnrichers(),
);

if ($this->cache instanceof CacheItemPoolInterface) {
Expand All @@ -102,8 +98,37 @@ public function build(): StackHydrator

return new StackHydrator(
$metadataFactory,
array_merge(...$this->middlewares),
$this->middlewares(),
$this->defaultLazy,
);
}

public function defaultLazy(): bool
{
return $this->defaultLazy;
}

/** @return list<Middleware> */
public function middlewares(): array
{
krsort($this->middlewares);

return array_merge(...$this->middlewares);
}

/** @return list<Guesser> */
public function guessers(): array
{
krsort($this->guessers);

return array_merge(...$this->guessers);
}

/** @return list<MetadataEnricher> */
public function metadataEnrichers(): array
{
krsort($this->metadataEnrichers);

return array_merge(...$this->metadataEnrichers);
}
}
71 changes: 71 additions & 0 deletions tests/Unit/Extension/Cryptography/CryptographyExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography;

use Patchlevel\Hydrator\Cryptography\PayloadCryptographer;
use Patchlevel\Hydrator\Extension\Cryptography\Cryptographer;
use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension;
use Patchlevel\Hydrator\Extension\Cryptography\CryptographyMetadataEnricher;
use Patchlevel\Hydrator\Extension\Cryptography\CryptographyMiddleware;
use Patchlevel\Hydrator\Extension\Cryptography\LegacyCryptographyDecryptMiddleware;
use Patchlevel\Hydrator\Extension\Cryptography\LegacyCryptographyMetadataEnricher;
use Patchlevel\Hydrator\StackHydratorBuilder;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(CryptographyExtension::class)]
final class CryptographyExtensionTest extends TestCase
{
public function testConfigureWithoutLegacyOptions(): void
{
$builder = new StackHydratorBuilder();
$cryptographer = $this->createMock(Cryptographer::class);

$extension = new CryptographyExtension($cryptographer);
$extension->configure($builder);

$middlewares = $builder->middlewares();
self::assertCount(1, $middlewares);
self::assertInstanceOf(CryptographyMiddleware::class, $middlewares[0]);

$metadataEnrichers = $builder->metadataEnrichers();
self::assertCount(1, $metadataEnrichers);
self::assertInstanceOf(CryptographyMetadataEnricher::class, $metadataEnrichers[0]);
}

public function testConfigureWithLegacyMetadataMapping(): void
{
$builder = new StackHydratorBuilder();
$cryptographer = $this->createMock(Cryptographer::class);

$extension = new CryptographyExtension($cryptographer, legacyMetadataMapping: true);
$extension->configure($builder);

$metadataEnrichers = $builder->metadataEnrichers();
self::assertCount(2, $metadataEnrichers);
self::assertInstanceOf(CryptographyMetadataEnricher::class, $metadataEnrichers[0]);
self::assertInstanceOf(LegacyCryptographyMetadataEnricher::class, $metadataEnrichers[1]);
}

public function testConfigureWithLegacyCryptographerAndMetadataMapping(): void
{
$builder = new StackHydratorBuilder();
$cryptographer = $this->createMock(Cryptographer::class);
$legacyCryptographer = $this->createMock(PayloadCryptographer::class);

$extension = new CryptographyExtension($cryptographer, $legacyCryptographer, true);
$extension->configure($builder);

$middlewares = $builder->middlewares();
self::assertCount(2, $middlewares);
self::assertInstanceOf(LegacyCryptographyDecryptMiddleware::class, $middlewares[0]);
self::assertInstanceOf(CryptographyMiddleware::class, $middlewares[1]);

$metadataEnrichers = $builder->metadataEnrichers();
self::assertCount(2, $metadataEnrichers);
self::assertInstanceOf(CryptographyMetadataEnricher::class, $metadataEnrichers[0]);
self::assertInstanceOf(LegacyCryptographyMetadataEnricher::class, $metadataEnrichers[1]);
}
}
Loading
Loading