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: 3 additions & 3 deletions .github/workflows/backward-compatibility-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ jobs:
fetch-depth: 0

- name: "Install PHP"
uses: "shivammathur/setup-php@2.36.0"
uses: "shivammathur/setup-php@2.37.0"
with:
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}
working-directory: 'tools'
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

steps:
- name: "Install PHP"
uses: "shivammathur/setup-php@2.36.0"
uses: "shivammathur/setup-php@2.37.0"
with:
coverage: "none"
php-version: "${{ matrix.php-version }}"
Expand All @@ -33,7 +33,7 @@ jobs:
with:
ref: ${{ github.base_ref }}

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}

Expand All @@ -45,7 +45,7 @@ jobs:
with:
clean: false

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/coding-standard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ jobs:
uses: actions/checkout@v6

- name: "Install PHP"
uses: "shivammathur/setup-php@2.36.0"
uses: "shivammathur/setup-php@2.37.0"
with:
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/mutation-tests-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ jobs:
fetch-depth: 0

- name: "Install PHP"
uses: "shivammathur/setup-php@2.36.0"
uses: "shivammathur/setup-php@2.37.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/mutation-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ jobs:
uses: actions/checkout@v6

- name: "Install PHP"
uses: "shivammathur/setup-php@2.36.0"
uses: "shivammathur/setup-php@2.37.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,12 @@ jobs:
uses: actions/checkout@v6

- name: "Install PHP"
uses: "shivammathur/setup-php@2.36.0"
uses: "shivammathur/setup-php@2.37.0"
with:
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,13 @@ jobs:
uses: actions/checkout@v6

- name: "Install PHP"
uses: "shivammathur/setup-php@2.36.0"
uses: "shivammathur/setup-php@2.37.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
ini-values: memory_limit=-1

- uses: ramsey/composer-install@3.2.0
- uses: ramsey/composer-install@4.0.0
with:
dependency-versions: ${{ matrix.dependencies }}
composer-options: ${{ matrix.composer-options }}
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,24 @@ $oldEvent == $event // true
> [!WARNING]
> It is important to know that the constructor is not called!

#### Object to populate

If you want to hydrate an object that already exists, you can specify the object to populate.
This is useful if you want to update an existing object.

```php
$dto = new Dto();

$event = $hydrator->hydrate(
$dto::class,
[
'name' => 'patchlevel',
], [
MetadataHydrator::OBJECT_TO_POPULATE => $dto,
],
);
```

### Normalizer

For more complex structures, i.e. non-scalar data types, we use normalizers.
Expand Down
30 changes: 30 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,36 @@ parameters:
count: 3
path: src/Metadata/AttributeMetadataFactory.php

-
message: '#^Method Patchlevel\\Hydrator\\MetadataHydrator\:\:doHydrate\(\) should return T of object but returns mixed\.$#'
identifier: return.type
count: 1
path: src/MetadataHydrator.php

-
message: '#^Parameter \#1 \$object of method Patchlevel\\Hydrator\\Metadata\\CallbackMetadata\:\:invoke\(\) expects object, mixed given\.$#'
identifier: argument.type
count: 1
path: src/MetadataHydrator.php

-
message: '#^Parameter \#1 \$object of method Patchlevel\\Hydrator\\Metadata\\PropertyMetadata\:\:setValue\(\) expects object, mixed given\.$#'
identifier: argument.type
count: 2
path: src/MetadataHydrator.php

-
message: '#^Method Patchlevel\\Hydrator\\Middleware\\TransformMiddleware\:\:hydrate\(\) should return T of object but returns mixed\.$#'
identifier: return.type
count: 1
path: src/Middleware/TransformMiddleware.php

-
message: '#^Parameter \#1 \$object of method Patchlevel\\Hydrator\\Metadata\\PropertyMetadata\:\:setValue\(\) expects object, mixed given\.$#'
identifier: argument.type
count: 2
path: src/Middleware/TransformMiddleware.php

-
message: '#^Property Patchlevel\\Hydrator\\Normalizer\\EnumNormalizer\:\:\$enum \(class\-string\<BackedEnum\>\|null\) does not accept string\.$#'
identifier: assign.propertyType
Expand Down
8 changes: 8 additions & 0 deletions src/Extension/Cryptography/CryptographyExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Patchlevel\Hydrator\Extension\Cryptography;

use Patchlevel\Hydrator\Cryptography\PayloadCryptographer;
use Patchlevel\Hydrator\Extension;
use Patchlevel\Hydrator\StackHydratorBuilder;

Expand All @@ -12,12 +13,19 @@ final class CryptographyExtension implements Extension
{
public function __construct(
private readonly Cryptographer $cryptography,
private readonly PayloadCryptographer|null $legacyCryptographer = null,
) {
}

public function configure(StackHydratorBuilder $builder): void
{
$builder->addMetadataEnricher(new CryptographyMetadataEnricher(), 64);
$builder->addMiddleware(new CryptographyMiddleware($this->cryptography), 64);

if ($this->legacyCryptographer === null) {
return;
}

$builder->addMiddleware(new LegacyCryptographyDecryptMiddleware($this->legacyCryptographer), 65);
}
}
6 changes: 6 additions & 0 deletions src/Extension/Cryptography/CryptographyMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public function hydrate(ClassMetadata $metadata, array $data, array $context, St
{
$context[SubjectIds::class] = $subjectIds = $this->resolveSubjectIds($metadata, $data, $context);

if ($context[LegacyCryptographyDecryptMiddleware::class] ?? false) {
unset($context[LegacyCryptographyDecryptMiddleware::class]);

return $stack->next()->hydrate($metadata, $data, $context, $stack);
}

foreach ($metadata->properties as $propertyMetadata) {
$info = $propertyMetadata->extras[SensitiveDataInfo::class] ?? null;

Expand Down
53 changes: 53 additions & 0 deletions src/Extension/Cryptography/LegacyCryptographyDecryptMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Cryptography;

use Patchlevel\Hydrator\Cryptography\PayloadCryptographer;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\Stack;

/** @experimental */
final readonly class LegacyCryptographyDecryptMiddleware implements Middleware
{
public function __construct(
private PayloadCryptographer $payloadCryptographer,
) {
}

/**
* @param ClassMetadata<T> $metadata
* @param array<string, mixed> $data
* @param array<string, mixed> $context
*
* @return T
*
* @template T of object
*/
public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object
{
$processedData = $this->payloadCryptographer->decrypt($metadata, $data);

if ($processedData !== $data) {
$context[self::class] = true;
}

return $stack->next()->hydrate($metadata, $processedData, $context, $stack);
}

/**
* @param ClassMetadata<T> $metadata
* @param T $object
* @param array<string, mixed> $context
*
* @return array<string, mixed>
*
* @template T of object
*/
public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array
{
return $stack->next()->extract($metadata, $object, $context, $stack);
}
}
73 changes: 73 additions & 0 deletions src/Extension/Cryptography/Store/Psr16CacheStoreDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Extension\Cryptography\Store;

use DateInterval;
use Patchlevel\Hydrator\Extension\Cryptography\Cipher\CipherKey;
use Psr\SimpleCache\CacheInterface;

final readonly class Psr16CacheStoreDecorator implements CipherKeyStore
{
public function __construct(
private CipherKeyStore $cipherKeyStore,
private CacheInterface $cache,
private DateInterval|int|null $ttl = null,
) {
}

public function currentKeyFor(string $subjectId): CipherKey
{
$key = 'subjectId:' . $subjectId;
$entry = $this->cache->get($key);

if ($entry instanceof CipherKey) {
return $entry;
}

$entry = $this->cipherKeyStore->currentKeyFor($subjectId);

$this->cache->set($key, $entry, $this->ttl);

return $entry;
}

public function get(string $id): CipherKey
{
$key = 'id:' . $id;
$entry = $this->cache->get($key);

if ($entry instanceof CipherKey) {
return $entry;
}

$entry = $this->cipherKeyStore->get($id);

$this->cache->set($key, $entry, $this->ttl);

return $entry;
}

public function store(CipherKey $key): void
{
$this->cipherKeyStore->store($key);

$this->cache->set('id:' . $key->id, $key, $this->ttl);
$this->cache->set('subjectId:' . $key->subjectId, $key, $this->ttl);
}

public function remove(string $id): void
{
$this->cipherKeyStore->remove($id);

$this->cache->delete('id:' . $id);
}

public function removeWithSubjectId(string $subjectId): void
{
$this->cipherKeyStore->removeWithSubjectId($subjectId);

$this->cache->delete('subjectId:' . $subjectId);
}
}
Loading
Loading