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
2 changes: 1 addition & 1 deletion .github/workflows/backward-compatibility-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
fetch-depth: 0

- name: "Install PHP"
uses: "shivammathur/setup-php@2.33.0"
uses: "shivammathur/setup-php@2.34.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
Expand Down
2 changes: 1 addition & 1 deletion .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.33.0"
uses: "shivammathur/setup-php@2.34.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coding-standard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v4

- name: "Install PHP"
uses: "shivammathur/setup-php@2.33.0"
uses: "shivammathur/setup-php@2.34.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mutation-tests-diff.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
fetch-depth: 0

- name: "Install PHP"
uses: "shivammathur/setup-php@2.33.0"
uses: "shivammathur/setup-php@2.34.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/mutation-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v4

- name: "Install PHP"
uses: "shivammathur/setup-php@2.33.0"
uses: "shivammathur/setup-php@2.34.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/phpstan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v4

- name: "Install PHP"
uses: "shivammathur/setup-php@2.33.0"
uses: "shivammathur/setup-php@2.34.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/psalm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v4

- name: "Install PHP"
uses: "shivammathur/setup-php@2.33.0"
uses: "shivammathur/setup-php@2.34.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
uses: actions/checkout@v4

- name: "Install PHP"
uses: "shivammathur/setup-php@2.33.0"
uses: "shivammathur/setup-php@2.34.0"
with:
coverage: "pcov"
php-version: "${{ matrix.php-version }}"
Expand Down
68 changes: 63 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The library is a core component of [patchlevel/event-sourcing](ttps://github.com
where it powers the storage and retrieval of thousands of objects.

Hydration is handled through normalizers, especially for complex data types.
The system can automatically determine the appropriate normalizer based on the data type and PHPStan/Psalm annotations.
The system can automatically determine the appropriate normalizer based on the data type and annotations.

In most cases, no manual configuration is needed.
And if customization is required, it can be done easily using attributes.
Expand Down Expand Up @@ -137,9 +137,10 @@ For this purpose, normalizers of this order are determined:

1) Does the class property have a normalizer as an attribute? Use this.
2) The data type of the property is determined.
1) If it is a collection, use the ArrayNormalizer (recursive).
2) If it is an object, then look for a normalizer as attribute on the class or interfaces and use this.
3) If it is an object, then guess the normalizer based on the object. Fallback to the object normalizer.
1) If it is an array shape, use the ArrayShapeNormalizer (recursive).
2) If it is a collection, use the ArrayNormalizer (recursive).
3) If it is an object, then look for a normalizer as attribute on the class or interfaces and use this.
4) If it is an object, then guess the normalizer based on the object. Fallback to the object normalizer.

The normalizer is only determined once because it is cached in the metadata.
Below you will find the list of all normalizers and how to set them manually or explicitly.
Expand All @@ -155,14 +156,44 @@ use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;

final class DTO
{
#[ArrayNormalizer(new DateTimeImmutableNormalizer())]
/**
* @var list<DateTimeImmutable>
*/
#[ArrayNormalizer]
public array $dates;

#[ArrayNormalizer(new DateTimeImmutableNormalizer())]
public array $explicitDates;
}
```

> [!NOTE]
> The keys from the arrays are taken over here.

#### ArrayShape

If you have an array with a specific shape, you can use the `ArrayShapeNormalizer`.

```php
use Patchlevel\Hydrator\Normalizer\ArrayShapeNormalizer;
use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer;

final class DTO
{
/**
* @var array{
* date: DateTimeImmutable,
* otherField: string
* }
*/
#[ArrayShapeNormalizer]
public array $meta;

#[ArrayShapeNormalizer(['date' => new DateTimeImmutableNormalizer()])]
public array $explicitMeta;
}
```

#### DateTimeImmutable

With the `DateTimeImmutable` Normalizer, as the name suggests,
Expand Down Expand Up @@ -442,6 +473,29 @@ readonly class ProfileCreated
}
```

### Lazy

Since PHP 8.4, it's been possible to lazy-hydrate objects.
That is, the actual hydration process occurs when the object is accessed.
You can define for each class whether you want it to be lazy by using the `Lazy` attribute.

```php
use Patchlevel\Hydrator\Attribute\Lazy;

#[Lazy]
readonly class ProfileCreated
{
public function __construct(
public string $id,
public string $name,
) {
}
}
```

> [!NOTE]
> If you are using a PHP version older than 8.4, the attribute will be ignored.

### Hooks

Sometimes you need to do something before extract or after hydrate process.
Expand Down Expand Up @@ -592,6 +646,10 @@ final class ProfileCreated
}
```

> [!TIP]
> Cryptography is very expensive in terms of performance,
> you can combine it with lazy to improve performance and only decrypt when you actually access the object.

#### Configure Cryptography

Here we show you how to configure the cryptography.
Expand Down
137 changes: 133 additions & 4 deletions baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="6.9.1@81c8a77c0793d450fee40265cfe68891df11d505">
<files psalm-version="6.12.0@cf420941d061a57050b6c468ef2c778faf40aee2">
<file src="src/Attribute/PersonalData.php">
<MixedPropertyTypeCoercion>
<code><![CDATA[$fallbackCallable]]></code>
</MixedPropertyTypeCoercion>
</file>
<file src="src/Cryptography/Cipher/OpensslCipherKeyFactory.php">
<ArgumentTypeCoercion>
<code><![CDATA[openssl_random_pseudo_bytes($this->ivLength)]]></code>
Expand All @@ -25,27 +30,106 @@
</MixedAssignment>
</file>
<file src="src/Metadata/AttributeMetadataFactory.php">
<MixedAssignment>
<code><![CDATA[$personalDataFallback]]></code>
</MixedAssignment>
<InvalidReturnStatement>
<code><![CDATA[[false, null]]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code><![CDATA[array{bool, mixed, (callable(string, mixed):mixed)|null}]]></code>
</InvalidReturnType>
<PossiblyNullReference>
<code><![CDATA[guess]]></code>
</PossiblyNullReference>
</file>
<file src="src/Metadata/ClassMetadata.php">
<InvalidPropertyAssignmentValue>
<code><![CDATA[new ReflectionClass($data['className'])]]></code>
</InvalidPropertyAssignmentValue>
</file>
<file src="src/Metadata/PropertyMetadata.php">
<RiskyTruthyFalsyComparison>
<code><![CDATA[$this->personalDataFallbackCallable]]></code>
</RiskyTruthyFalsyComparison>
</file>
<file src="src/MetadataHydrator.php">
<InvalidOperand>
<code><![CDATA[$guessers]]></code>
</InvalidOperand>
<PossiblyInvalidArgument>
<code><![CDATA[[
...$guessers,
$guesser,
]]]></code>
</PossiblyInvalidArgument>
</file>
<file src="src/Normalizer/ArrayShapeNormalizer.php">
<MixedAssignment>
<code><![CDATA[$result[$field]]]></code>
<code><![CDATA[$result[$field]]]></code>
</MixedAssignment>
</file>
<file src="src/Normalizer/EnumNormalizer.php">
<DeprecatedClass>
<code><![CDATA[ReflectionTypeUtil::classStringInstanceOf($reflectionType, BackedEnum::class)]]></code>
</DeprecatedClass>
<DeprecatedInterface>
<code><![CDATA[EnumNormalizer]]></code>
</DeprecatedInterface>
</file>
<file src="src/Normalizer/ObjectNormalizer.php">
<DeprecatedClass>
<code><![CDATA[ReflectionTypeUtil::classString($reflectionType)]]></code>
</DeprecatedClass>
<DeprecatedInterface>
<code><![CDATA[ObjectNormalizer]]></code>
</DeprecatedInterface>
<MixedArgument>
<code><![CDATA[$value]]></code>
</MixedArgument>
<MixedArgumentTypeCoercion>
<code><![CDATA[$value]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="tests/Benchmark/tideways.php">
<ClassMustBeFinal>
<code><![CDATA[CompiledMetadataHydrator]]></code>
</ClassMustBeFinal>
<MixedMethodCall>
<code><![CDATA[normalize]]></code>
<code><![CDATA[normalize]]></code>
</MixedMethodCall>
<MixedReturnTypeCoercion>
<code><![CDATA[array]]></code>
<code><![CDATA[match (true) {
$object instanceof ProfileCreated => $this->extractProfileCreated($object),
$object instanceof Skill => $this->extractSkill($object),
default => throw new InvalidArgumentException('Unknown object type'),
}]]></code>
</MixedReturnTypeCoercion>
</file>
<file src="tests/Unit/Cryptography/Cipher/OpensslCipherTest.php">
<MixedAssignment>
<code><![CDATA[$return]]></code>
</MixedAssignment>
<MixedReturnStatement>
<code><![CDATA[Generator]]></code>
</MixedReturnStatement>
</file>
<file src="tests/Unit/Cryptography/CryptographySubscriberTest.php">
<InvalidArgument>
<code><![CDATA[$metadata]]></code>
<code><![CDATA[$metadata]]></code>
</InvalidArgument>
</file>
<file src="tests/Unit/Fixture/IdNormalizer.php">
<DeprecatedClass>
<code><![CDATA[ReflectionTypeUtil::classStringInstanceOf(
$reflectionType,
Id::class,
)]]></code>
</DeprecatedClass>
<DeprecatedInterface>
<code><![CDATA[IdNormalizer]]></code>
</DeprecatedInterface>
</file>
<file src="tests/Unit/Metadata/AttributeMetadataFactoryTest.php">
<ArgumentTypeCoercion>
Expand All @@ -59,8 +143,53 @@
<ArgumentTypeCoercion>
<code><![CDATA['Unknown']]></code>
</ArgumentTypeCoercion>
<InvalidArgument>
<code><![CDATA[$metadataFactory->metadata(ProfileCreated::class)]]></code>
<code><![CDATA[$metadataFactory->metadata(ProfileCreated::class)]]></code>
<code><![CDATA[$metadataFactory->metadata(ProfileCreated::class)]]></code>
<code><![CDATA[$metadataFactory->metadata(ProfileCreated::class)]]></code>
</InvalidArgument>
<UndefinedClass>
<code><![CDATA['Unknown']]></code>
</UndefinedClass>
</file>
<file src="tests/Unit/Normalizer/ArrayNormalizerTest.php">
<InvalidArgument>
<code><![CDATA[$normalizer]]></code>
</InvalidArgument>
</file>
<file src="tests/Unit/Normalizer/ArrayShapeNormalizerTest.php">
<InvalidArgument>
<code><![CDATA[['foo' => $normalizer]]]></code>
</InvalidArgument>
</file>
<file src="tests/Unit/Normalizer/ReflectionTypeUtilTest.php">
<DeprecatedClass>
<code><![CDATA[ReflectionTypeUtil::classString($this->reflectionType($object, 'notAObject'))]]></code>
<code><![CDATA[ReflectionTypeUtil::classString($this->reflectionType($object, 'object'))]]></code>
<code><![CDATA[ReflectionTypeUtil::classString($this->reflectionType($object, 'objectNullable'))]]></code>
<code><![CDATA[ReflectionTypeUtil::classString($this->reflectionType($object, 'objectUnionNullable'))]]></code>
<code><![CDATA[ReflectionTypeUtil::classStringInstanceOf(
$this->reflectionType($object, 'object'),
ProfileCreated::class,
)]]></code>
<code><![CDATA[ReflectionTypeUtil::classStringInstanceOf(
$this->reflectionType($object, 'objectNullable'),
ProfileCreated::class,
)]]></code>
<code><![CDATA[ReflectionTypeUtil::classStringInstanceOf(
$this->reflectionType($object, 'objectUnionNullable'),
ProfileCreated::class,
)]]></code>
<code><![CDATA[ReflectionTypeUtil::classStringInstanceOf(
$this->reflectionType($object, 'object'),
ChildDto::class,
)]]></code>
<code><![CDATA[ReflectionTypeUtil::type($this->reflectionType($object, 'intersection'))]]></code>
<code><![CDATA[ReflectionTypeUtil::type($this->reflectionType($object, 'nullableString'))]]></code>
<code><![CDATA[ReflectionTypeUtil::type($this->reflectionType($object, 'string'))]]></code>
<code><![CDATA[ReflectionTypeUtil::type($this->reflectionType($object, 'union'))]]></code>
<code><![CDATA[ReflectionTypeUtil::type($this->reflectionType($object, 'unionNullableString'))]]></code>
</DeprecatedClass>
</file>
</files>
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"psr/cache": "^2.0.0 || ^3.0.0",
"psr/simple-cache": "^2.0.0 || ^3.0.0",
"symfony/event-dispatcher": "^5.4.29 || ^6.4.0 || ^7.0.0",
"symfony/type-info": "^7.2.4"
"symfony/type-info": "^7.3.0"
},
"require-dev": {
"infection/infection": "^0.29.10",
Expand Down
Loading
Loading