diff --git a/.github/workflows/backward-compatibility-check.yml b/.github/workflows/backward-compatibility-check.yml index b964cbc..b9b37ec 100644 --- a/.github/workflows/backward-compatibility-check.yml +++ b/.github/workflows/backward-compatibility-check.yml @@ -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 }}" diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 4b578ad..8482083 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -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 }}" diff --git a/.github/workflows/coding-standard.yml b/.github/workflows/coding-standard.yml index 3cd229a..849c002 100644 --- a/.github/workflows/coding-standard.yml +++ b/.github/workflows/coding-standard.yml @@ -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 }}" diff --git a/.github/workflows/mutation-tests-diff.yml b/.github/workflows/mutation-tests-diff.yml index 2631879..50f6d76 100644 --- a/.github/workflows/mutation-tests-diff.yml +++ b/.github/workflows/mutation-tests-diff.yml @@ -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 }}" diff --git a/.github/workflows/mutation-tests.yml b/.github/workflows/mutation-tests.yml index 55ea546..e7a4ccc 100644 --- a/.github/workflows/mutation-tests.yml +++ b/.github/workflows/mutation-tests.yml @@ -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 }}" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index ac30151..fd9e68d 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -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 }}" diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 1183036..abfd6d3 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -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 }}" diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 4603e74..f85b281 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -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 }}" diff --git a/README.md b/README.md index dd90f4a..c3a8821 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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. @@ -155,14 +156,44 @@ use Patchlevel\Hydrator\Normalizer\DateTimeImmutableNormalizer; final class DTO { - #[ArrayNormalizer(new DateTimeImmutableNormalizer())] + /** + * @var list + */ + #[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, @@ -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. @@ -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. diff --git a/baseline.xml b/baseline.xml index 7869dec..f0ef8fa 100644 --- a/baseline.xml +++ b/baseline.xml @@ -1,5 +1,10 @@ - + + + + + + ivLength)]]> @@ -25,16 +30,58 @@ - - - + + + + + + + + + + + + personalDataFallbackCallable]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -42,10 +89,47 @@ + + + + + + + + + + + $this->extractProfileCreated($object), + $object instanceof Skill => $this->extractSkill($object), + default => throw new InvalidArgumentException('Unknown object type'), + }]]> + + + + + + + + + + + + + + + + + + + @@ -59,8 +143,53 @@ + + metadata(ProfileCreated::class)]]> + metadata(ProfileCreated::class)]]> + metadata(ProfileCreated::class)]]> + metadata(ProfileCreated::class)]]> + + + + + + + + + $normalizer]]]> + + + + + reflectionType($object, 'notAObject'))]]> + reflectionType($object, 'object'))]]> + reflectionType($object, 'objectNullable'))]]> + reflectionType($object, 'objectUnionNullable'))]]> + reflectionType($object, 'object'), + ProfileCreated::class, + )]]> + reflectionType($object, 'objectNullable'), + ProfileCreated::class, + )]]> + reflectionType($object, 'objectUnionNullable'), + ProfileCreated::class, + )]]> + reflectionType($object, 'object'), + ChildDto::class, + )]]> + reflectionType($object, 'intersection'))]]> + reflectionType($object, 'nullableString'))]]> + reflectionType($object, 'string'))]]> + reflectionType($object, 'union'))]]> + reflectionType($object, 'unionNullableString'))]]> + + diff --git a/composer.json b/composer.json index d9e0e1d..572be19 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/composer.lock b/composer.lock index ebf27f9..7ef1e2f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "88a6e4328de05fee58b52ac893a9927c", + "content-hash": "780c8d213d902449d9c8d03c4985c73e", "packages": [ { "name": "psr/cache", @@ -209,18 +209,85 @@ }, "time": "2021-10-29T13:26:27+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, { "name": "symfony/event-dispatcher", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1" + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/910c5db85a5356d0fea57680defec4e99eb9c8c1", - "reference": "910c5db85a5356d0fea57680defec4e99eb9c8c1", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", "shasum": "" }, "require": { @@ -271,7 +338,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.2.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" }, "funding": [ { @@ -287,20 +354,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2025-04-22T09:11:45+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f" + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f", - "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", "shasum": "" }, "require": { @@ -314,7 +381,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -347,7 +414,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" }, "funding": [ { @@ -363,28 +430,32 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/type-info", - "version": "v7.2.5", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "c4824a6b658294c828e609d3d8dbb4e87f6a375d" + "reference": "bc9af22e25796d98078f69c0749ab3a9d3454786" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/c4824a6b658294c828e609d3d8dbb4e87f6a375d", - "reference": "c4824a6b658294c828e609d3d8dbb4e87f6a375d", + "url": "https://api.github.com/repos/symfony/type-info/zipball/bc9af22e25796d98078f69c0749ab3a9d3454786", + "reference": "bc9af22e25796d98078f69c0749ab3a9d3454786", "shasum": "" }, "require": { "php": ">=8.2", - "psr/container": "^1.1|^2.0" + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" }, "require-dev": { - "phpstan/phpdoc-parser": "^1.0|^2.0" + "phpstan/phpdoc-parser": "^1.30|^2.0" }, "type": "library", "autoload": { @@ -422,7 +493,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v7.2.5" + "source": "https://github.com/symfony/type-info/tree/v7.3.0" }, "funding": [ { @@ -438,7 +509,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T09:03:36+00:00" + "time": "2025-03-30T12:17:06+00:00" } ], "packages-dev": [ @@ -2272,16 +2343,16 @@ }, { "name": "infection/infection", - "version": "0.29.12", + "version": "0.29.14", "source": { "type": "git", "url": "https://github.com/infection/infection.git", - "reference": "dfe9cf6e65545881c7d21343c494cc8a1fdbfb80" + "reference": "feea2a48a8aeedd3a4d2105167b41a46f0e568a3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/infection/infection/zipball/dfe9cf6e65545881c7d21343c494cc8a1fdbfb80", - "reference": "dfe9cf6e65545881c7d21343c494cc8a1fdbfb80", + "url": "https://api.github.com/repos/infection/infection/zipball/feea2a48a8aeedd3a4d2105167b41a46f0e568a3", + "reference": "feea2a48a8aeedd3a4d2105167b41a46f0e568a3", "shasum": "" }, "require": { @@ -2297,7 +2368,7 @@ "infection/extension-installer": "^0.1.0", "infection/include-interceptor": "^0.2.5", "infection/mutator": "^0.4", - "justinrainbow/json-schema": "^5.3", + "justinrainbow/json-schema": "^5.3 || ^6.0", "nikic/php-parser": "^5.3", "ondram/ci-detector": "^4.1.0", "php": "^8.2", @@ -2319,16 +2390,16 @@ "require-dev": { "ext-simplexml": "*", "fidry/makefile": "^1.0", - "helmich/phpunit-json-assert": "^3.0", - "phpstan/extension-installer": "^1.1.0", - "phpstan/phpstan": "^1.10.15", - "phpstan/phpstan-phpunit": "^1.0.0", - "phpstan/phpstan-strict-rules": "^1.1.0", - "phpstan/phpstan-webmozart-assert": "^1.0.2", - "phpunit/phpunit": "^10.5", - "rector/rector": "^1.0", - "sidz/phpstan-rules": "^0.4", - "symfony/yaml": "^6.4 || ^7.0" + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpstan/phpstan-webmozart-assert": "^2.0", + "phpunit/phpunit": "^11.5", + "rector/rector": "^2.0", + "sidz/phpstan-rules": "^0.5.1", + "symfony/yaml": "^6.4 || ^7.0", + "thecodingmachine/phpstan-safe-rule": "^1.4" }, "bin": [ "bin/infection" @@ -2384,7 +2455,7 @@ ], "support": { "issues": "https://github.com/infection/infection/issues", - "source": "https://github.com/infection/infection/tree/0.29.12" + "source": "https://github.com/infection/infection/tree/0.29.14" }, "funding": [ { @@ -2396,20 +2467,20 @@ "type": "open_collective" } ], - "time": "2025-02-17T18:25:11+00:00" + "time": "2025-03-02T18:49:12+00:00" }, { "name": "infection/mutator", - "version": "0.4.0", + "version": "0.4.1", "source": { "type": "git", "url": "https://github.com/infection/mutator.git", - "reference": "51d6d01a2357102030aee9d603063c4bad86b144" + "reference": "3c976d721b02b32f851ee4e15d553ef1e9186d1d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/infection/mutator/zipball/51d6d01a2357102030aee9d603063c4bad86b144", - "reference": "51d6d01a2357102030aee9d603063c4bad86b144", + "url": "https://api.github.com/repos/infection/mutator/zipball/3c976d721b02b32f851ee4e15d553ef1e9186d1d", + "reference": "3c976d721b02b32f851ee4e15d553ef1e9186d1d", "shasum": "" }, "require": { @@ -2437,7 +2508,7 @@ "description": "Mutator interface to implement custom mutators (mutation operators) for Infection", "support": { "issues": "https://github.com/infection/mutator/issues", - "source": "https://github.com/infection/mutator/tree/0.4.0" + "source": "https://github.com/infection/mutator/tree/0.4.1" }, "funding": [ { @@ -2449,34 +2520,44 @@ "type": "open_collective" } ], - "time": "2024-05-14T22:39:59+00:00" + "time": "2025-04-29T08:19:52+00:00" }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", + "version": "6.4.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "friendsofphp/php-cs-fixer": "3.3.0", "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -2505,16 +2586,16 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" }, - "time": "2024-07-06T21:00:26+00:00" + "time": "2025-06-03T18:27:04+00:00" }, { "name": "kelunik/certificate", @@ -2748,18 +2829,91 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.1", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "time": "2024-11-28T04:54:44+00:00" + }, { "name": "myclabs/deep-copy", - "version": "1.13.0", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414", - "reference": "024473a478be9df5fdaca2c793f2232fe788e414", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -2798,7 +2952,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -2806,7 +2960,7 @@ "type": "tidelift" } ], - "time": "2025-02-12T12:17:51+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "netresearch/jsonmapper", @@ -2861,16 +3015,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.4.0", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", - "reference": "447a020a1f875a434d62f2a401f53b82a396e494", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "shasum": "" }, "require": { @@ -2913,9 +3067,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" }, - "time": "2024-12-30T11:07:19+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "ondram/ci-detector", @@ -3541,16 +3695,16 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.12", + "version": "2.1.17", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "96dde49e967c0c22812bcfa7bda4ff82c09f3b0c" + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/96dde49e967c0c22812bcfa7bda4ff82c09f3b0c", - "reference": "96dde49e967c0c22812bcfa7bda4ff82c09f3b0c", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/89b5ef665716fa2a52ecd2633f21007a6a349053", + "reference": "89b5ef665716fa2a52ecd2633f21007a6a349053", "shasum": "" }, "require": { @@ -3595,7 +3749,7 @@ "type": "github" } ], - "time": "2025-04-16T13:19:18+00:00" + "time": "2025-05-21T20:55:28+00:00" }, { "name": "phpunit/php-code-coverage", @@ -3922,16 +4076,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.17", + "version": "11.5.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fd2e863a2995cdfd864fb514b5e0b28b09895b5c" + "reference": "4cd72faaa8f811e4cc63040cba167757660a5538" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fd2e863a2995cdfd864fb514b5e0b28b09895b5c", - "reference": "fd2e863a2995cdfd864fb514b5e0b28b09895b5c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4cd72faaa8f811e4cc63040cba167757660a5538", + "reference": "4cd72faaa8f811e4cc63040cba167757660a5538", "shasum": "" }, "require": { @@ -3941,7 +4095,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.0", + "myclabs/deep-copy": "^1.13.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", @@ -3954,7 +4108,7 @@ "sebastian/code-unit": "^3.0.3", "sebastian/comparator": "^6.3.1", "sebastian/diff": "^6.0.2", - "sebastian/environment": "^7.2.0", + "sebastian/environment": "^7.2.1", "sebastian/exporter": "^6.3.0", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", @@ -4003,7 +4157,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.17" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.22" }, "funding": [ { @@ -4014,12 +4168,20 @@ "url": "https://github.com/sebastianbergmann", "type": "github" }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], - "time": "2025-04-08T07:59:11+00:00" + "time": "2025-06-06T02:48:05+00:00" }, { "name": "psalm/plugin-phpunit", @@ -4311,33 +4473,33 @@ }, { "name": "roave/infection-static-analysis-plugin", - "version": "1.37.0", + "version": "1.38.0", "source": { "type": "git", "url": "https://github.com/Roave/infection-static-analysis-plugin.git", - "reference": "062af2a493b570346f6cbbae378e1d69bc4194bb" + "reference": "431c7bba21df58bf11186ec03501e2b47d321b2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/infection-static-analysis-plugin/zipball/062af2a493b570346f6cbbae378e1d69bc4194bb", - "reference": "062af2a493b570346f6cbbae378e1d69bc4194bb", + "url": "https://api.github.com/repos/Roave/infection-static-analysis-plugin/zipball/431c7bba21df58bf11186ec03501e2b47d321b2f", + "reference": "431c7bba21df58bf11186ec03501e2b47d321b2f", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", - "infection/infection": "0.29.12", + "infection/infection": "0.29.14", "php": "~8.2.0 || ~8.3.0 || ~8.4.0", - "sanmai/later": "^0.1.4", - "vimeo/psalm": "^6.8.8" + "sanmai/later": "^0.1.7", + "vimeo/psalm": "^6.12.0" }, "conflict": { "symfony/polyfill-php84": "<1.30.0" }, "require-dev": { - "azjezz/psl": "^3.2", - "doctrine/coding-standard": "^12.0.0", - "phpunit/phpunit": "^11.5.10", - "psalm/plugin-phpunit": "^0.19.2" + "azjezz/psl": "^3.3.0", + "doctrine/coding-standard": "^13.0.1", + "phpunit/phpunit": "^11.5.21", + "psalm/plugin-phpunit": "^0.19.5" }, "bin": [ "bin/roave-infection-static-analysis-plugin" @@ -4361,26 +4523,26 @@ "description": "Static analysis on top of mutation testing - prevents escaped mutants from being invalid according to static analysis", "support": { "issues": "https://github.com/Roave/infection-static-analysis-plugin/issues", - "source": "https://github.com/Roave/infection-static-analysis-plugin/tree/1.37.0" + "source": "https://github.com/Roave/infection-static-analysis-plugin/tree/1.38.0" }, - "time": "2025-02-27T18:02:45+00:00" + "time": "2025-06-04T01:32:53+00:00" }, { "name": "sanmai/later", - "version": "0.1.5", + "version": "0.1.7", "source": { "type": "git", "url": "https://github.com/sanmai/later.git", - "reference": "cf5164557d19930295892094996f049ea12ba14d" + "reference": "72a82d783864bca90412d8a26c1878f8981fee97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/later/zipball/cf5164557d19930295892094996f049ea12ba14d", - "reference": "cf5164557d19930295892094996f049ea12ba14d", + "url": "https://api.github.com/repos/sanmai/later/zipball/72a82d783864bca90412d8a26c1878f8981fee97", + "reference": "72a82d783864bca90412d8a26c1878f8981fee97", "shasum": "" }, "require": { - "php": ">=7.4" + "php": ">=8.2" }, "require-dev": { "ergebnis/composer-normalize": "^2.8", @@ -4419,7 +4581,7 @@ "description": "Later: deferred wrapper object", "support": { "issues": "https://github.com/sanmai/later/issues", - "source": "https://github.com/sanmai/later/tree/0.1.5" + "source": "https://github.com/sanmai/later/tree/0.1.7" }, "funding": [ { @@ -4427,24 +4589,24 @@ "type": "github" } ], - "time": "2024-12-06T02:36:26+00:00" + "time": "2025-05-11T01:48:00+00:00" }, { "name": "sanmai/pipeline", - "version": "6.12", + "version": "6.16", "source": { "type": "git", "url": "https://github.com/sanmai/pipeline.git", - "reference": "ad7dbc3f773eeafb90d5459522fbd8f188532e25" + "reference": "f32413630904f83b069a7fbdfab34267dbaecdb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/pipeline/zipball/ad7dbc3f773eeafb90d5459522fbd8f188532e25", - "reference": "ad7dbc3f773eeafb90d5459522fbd8f188532e25", + "url": "https://api.github.com/repos/sanmai/pipeline/zipball/f32413630904f83b069a7fbdfab34267dbaecdb6", + "reference": "f32413630904f83b069a7fbdfab34267dbaecdb6", "shasum": "" }, "require": { - "php": "^7.4 || ^8.0" + "php": ">=8.2" }, "require-dev": { "ergebnis/composer-normalize": "^2.8", @@ -4453,8 +4615,8 @@ "league/pipeline": "^0.3 || ^1.0", "phan/phan": ">=1.1", "php-coveralls/php-coveralls": "^2.4.1", - "phpstan/phpstan": ">=0.10", - "phpunit/phpunit": ">=9.4", + "phpstan/phpstan": ">=0.10 <2", + "phpunit/phpunit": ">=9.4 <12", "vimeo/psalm": ">=2" }, "type": "library", @@ -4484,7 +4646,7 @@ "description": "General-purpose collections pipeline", "support": { "issues": "https://github.com/sanmai/pipeline/issues", - "source": "https://github.com/sanmai/pipeline/tree/6.12" + "source": "https://github.com/sanmai/pipeline/tree/6.16" }, "funding": [ { @@ -4492,7 +4654,7 @@ "type": "github" } ], - "time": "2024-10-17T02:22:57+00:00" + "time": "2025-06-02T06:36:22+00:00" }, { "name": "sebastian/cli-parser", @@ -4871,23 +5033,23 @@ }, { "name": "sebastian/environment", - "version": "7.2.0", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", - "reference": "855f3ae0ab316bbafe1ba4e16e9f3c078d24a0c5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^11.0" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -4923,15 +5085,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", "security": "https://github.com/sebastianbergmann/environment/security/policy", - "source": "https://github.com/sebastianbergmann/environment/tree/7.2.0" + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" } ], - "time": "2024-07-03T04:54:44+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", @@ -5486,32 +5660,32 @@ }, { "name": "slevomat/coding-standard", - "version": "8.17.0", + "version": "8.18.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "ace04a4e2e20c9bc26ad14d6c4c737cde6056ec0" + "reference": "06b18b3f64979ab31d27c37021838439f3ed5919" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/ace04a4e2e20c9bc26ad14d6c4c737cde6056ec0", - "reference": "ace04a4e2e20c9bc26ad14d6c4c737cde6056ec0", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/06b18b3f64979ab31d27c37021838439f3ed5919", + "reference": "06b18b3f64979ab31d27c37021838439f3ed5919", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7 || ^1.0", "php": "^7.4 || ^8.0", "phpstan/phpdoc-parser": "^2.1.0", - "squizlabs/php_codesniffer": "^3.12.1" + "squizlabs/php_codesniffer": "^3.13.0" }, "require-dev": { "phing/phing": "3.0.1", "php-parallel-lint/php-parallel-lint": "1.4.0", - "phpstan/phpstan": "2.1.11", - "phpstan/phpstan-deprecation-rules": "2.0.1", + "phpstan/phpstan": "2.1.17", + "phpstan/phpstan-deprecation-rules": "2.0.3", "phpstan/phpstan-phpunit": "2.0.6", "phpstan/phpstan-strict-rules": "2.0.4", - "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.17|12.1.2" + "phpunit/phpunit": "9.6.8|10.5.45|11.4.4|11.5.21|12.1.3" }, "type": "phpcodesniffer-standard", "extra": { @@ -5535,7 +5709,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.17.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.18.1" }, "funding": [ { @@ -5547,7 +5721,7 @@ "type": "tidelift" } ], - "time": "2025-04-10T06:06:16+00:00" + "time": "2025-05-22T14:32:30+00:00" }, { "name": "spatie/array-to-xml", @@ -5619,16 +5793,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.12.2", + "version": "3.13.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa" + "reference": "65ff2489553b83b4597e89c3b8b721487011d186" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", - "reference": "6d4cf6032d4b718f168c90a96e36c7d0eaacb2aa", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", + "reference": "65ff2489553b83b4597e89c3b8b721487011d186", "shasum": "" }, "require": { @@ -5699,7 +5873,7 @@ "type": "thanks_dev" } ], - "time": "2025-04-13T04:10:18+00:00" + "time": "2025-05-11T03:36:00+00:00" }, { "name": "staabm/side-effects-detector", @@ -5755,23 +5929,24 @@ }, { "name": "symfony/console", - "version": "v7.2.5", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "e51498ea18570c062e7df29d05a7003585b19b88" + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/e51498ea18570c062e7df29d05a7003585b19b88", - "reference": "e51498ea18570c062e7df29d05a7003585b19b88", + "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^6.4|^7.0" + "symfony/string": "^7.2" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -5828,7 +6003,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.2.5" + "source": "https://github.com/symfony/console/tree/v7.3.0" }, "funding": [ { @@ -5844,78 +6019,11 @@ "type": "tidelift" } ], - "time": "2025-03-12T08:11:12+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v3.5.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.5-dev" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-05-24T10:34:04+00:00" }, { "name": "symfony/filesystem", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -5961,7 +6069,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.2.0" + "source": "https://github.com/symfony/filesystem/tree/v7.3.0" }, "funding": [ { @@ -5981,16 +6089,16 @@ }, { "name": "symfony/finder", - "version": "v7.2.2", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb" + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/87a71856f2f56e4100373e92529eed3171695cfb", - "reference": "87a71856f2f56e4100373e92529eed3171695cfb", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", "shasum": "" }, "require": { @@ -6025,7 +6133,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.2.2" + "source": "https://github.com/symfony/finder/tree/v7.3.0" }, "funding": [ { @@ -6041,20 +6149,20 @@ "type": "tidelift" } ], - "time": "2024-12-30T19:00:17+00:00" + "time": "2024-12-30T19:00:26+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50" + "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/7da8fbac9dcfef75ffc212235d76b2754ce0cf50", - "reference": "7da8fbac9dcfef75ffc212235d76b2754ce0cf50", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/afb9a8038025e5dbc657378bfab9198d75f10fca", + "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca", "shasum": "" }, "require": { @@ -6092,7 +6200,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.2.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.3.0" }, "funding": [ { @@ -6108,11 +6216,11 @@ "type": "tidelift" } ], - "time": "2024-11-20T11:17:29+00:00" + "time": "2025-04-04T13:12:05+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -6171,7 +6279,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -6191,7 +6299,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -6249,7 +6357,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -6269,7 +6377,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -6330,7 +6438,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -6350,19 +6458,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -6410,7 +6519,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -6426,20 +6535,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd" + "reference": "000df7860439609837bbe28670b0be15783b7fbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/e5493eb51311ab0b1cc2243416613f06ed8f18bd", - "reference": "e5493eb51311ab0b1cc2243416613f06ed8f18bd", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", "shasum": "" }, "require": { @@ -6486,7 +6595,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" }, "funding": [ { @@ -6502,20 +6611,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T12:04:04+00:00" + "time": "2025-02-20T12:04:08+00:00" }, { "name": "symfony/process", - "version": "v7.2.5", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d" + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/87b7c93e57df9d8e39a093d32587702380ff045d", - "reference": "87b7c93e57df9d8e39a093d32587702380ff045d", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", "shasum": "" }, "require": { @@ -6547,7 +6656,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.2.5" + "source": "https://github.com/symfony/process/tree/v7.3.0" }, "funding": [ { @@ -6563,20 +6672,20 @@ "type": "tidelift" } ], - "time": "2025-03-13T12:21:46+00:00" + "time": "2025-04-17T09:11:12+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -6594,7 +6703,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.5-dev" + "dev-main": "3.6-dev" } }, "autoload": { @@ -6630,7 +6739,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -6646,20 +6755,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v7.2.0", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82" + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/446e0d146f991dde3e73f45f2c97a9faad773c82", - "reference": "446e0d146f991dde3e73f45f2c97a9faad773c82", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", "shasum": "" }, "require": { @@ -6717,7 +6826,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.2.0" + "source": "https://github.com/symfony/string/tree/v7.3.0" }, "funding": [ { @@ -6733,24 +6842,25 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:26+00:00" + "time": "2025-04-20T20:19:01+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.2.3", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a" + "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a", - "reference": "82b478c69745d8878eb60f9a049a4d584996f73a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", + "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", "shasum": "" }, "require": { "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "conflict": { @@ -6800,7 +6910,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.2.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.0" }, "funding": [ { @@ -6816,20 +6926,20 @@ "type": "tidelift" } ], - "time": "2025-01-17T11:39:41+00:00" + "time": "2025-04-27T18:39:23+00:00" }, { "name": "thecodingmachine/safe", - "version": "v3.1.0", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "e14ac96126e6c19ea9d1f4029abb51487f4cf2cf" + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/e14ac96126e6c19ea9d1f4029abb51487f4cf2cf", - "reference": "e14ac96126e6c19ea9d1f4029abb51487f4cf2cf", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/2cdd579eeaa2e78e51c7509b50cc9fb89a956236", + "reference": "2cdd579eeaa2e78e51c7509b50cc9fb89a956236", "shasum": "" }, "require": { @@ -6939,7 +7049,7 @@ "description": "PHP core functions that throw exceptions instead of returning FALSE on error", "support": { "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v3.1.0" + "source": "https://github.com/thecodingmachine/safe/tree/v3.3.0" }, "funding": [ { @@ -6955,7 +7065,7 @@ "type": "github" } ], - "time": "2025-04-12T06:41:26+00:00" + "time": "2025-05-14T06:15:44+00:00" }, { "name": "theseer/tokenizer", @@ -7009,16 +7119,16 @@ }, { "name": "vimeo/psalm", - "version": "6.10.0", + "version": "6.12.0", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "9c0add4eb88d4b169ac04acb7c679918cbb9c252" + "reference": "cf420941d061a57050b6c468ef2c778faf40aee2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/9c0add4eb88d4b169ac04acb7c679918cbb9c252", - "reference": "9c0add4eb88d4b169ac04acb7c679918cbb9c252", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/cf420941d061a57050b6c468ef2c778faf40aee2", + "reference": "cf420941d061a57050b6c468ef2c778faf40aee2", "shasum": "" }, "require": { @@ -7123,7 +7233,7 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2025-03-31T10:12:50+00:00" + "time": "2025-05-28T12:52:06+00:00" }, { "name": "webmozart/assert", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0cdb2bb..32f26cc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -63,7 +63,7 @@ parameters: - message: '#^Parameter \#1 \$class of method Patchlevel\\Hydrator\\Metadata\\AttributeMetadataFactory\:\:findNormalizerOnClass\(\) expects class\-string, string given\.$#' identifier: argument.type - count: 2 + count: 3 path: src/Metadata/AttributeMetadataFactory.php - diff --git a/src/Attribute/Lazy.php b/src/Attribute/Lazy.php new file mode 100644 index 0000000..1407229 --- /dev/null +++ b/src/Attribute/Lazy.php @@ -0,0 +1,16 @@ +getSubjectIdField($reflectionClass), $this->getPostHydrateCallbacks($reflectionClass), $this->getPreExtractCallbacks($reflectionClass), + $this->getLazy($reflectionClass), ); $parentMetadataClass = $reflectionClass->getParentClass(); @@ -212,6 +216,18 @@ private function getPreExtractCallbacks(ReflectionClass $reflection): array return $methods; } + /** @param ReflectionClass $reflection */ + private function getLazy(ReflectionClass $reflection): bool|null + { + $attributeReflectionList = $reflection->getAttributes(Lazy::class); + + if ($attributeReflectionList === []) { + return null; + } + + return $attributeReflectionList[0]->newInstance()->enabled; + } + private function getFieldName(ReflectionProperty $reflectionProperty): string { $attributeReflectionList = $reflectionProperty->getAttributes(NormalizedName::class); @@ -271,6 +287,7 @@ private function mergeMetadata(ClassMetadata $parent, ClassMetadata $child): Cla $parentDataSubjectIdField ?? $childDataSubjectIdField, array_merge($parent->postHydrateCallbacks(), $child->postHydrateCallbacks()), array_merge($parent->preExtractCallbacks(), $child->preExtractCallbacks()), + $child->lazy() ?? $parent->lazy(), ); } @@ -384,6 +401,42 @@ private function inferNormalizerByType(Type $type): Normalizer|null return $this->guesser->guess($type); } + if ($type instanceof ArrayShapeType) { + $shape = $type->getShape(); + + $normalizers = []; + + foreach ($shape as $field => $fieldInfo) { + $valueType = $fieldInfo['type']; + + if ($valueType instanceof NullableType) { + $valueType = $valueType->getWrappedType(); + } + + $normalizer = null; + + if ($valueType instanceof ObjectType) { + $normalizer = $this->findNormalizerOnClass($valueType->getClassName()); + } + + if ($normalizer === null) { + $normalizer = $this->inferNormalizerByType($valueType); + } + + if ($normalizer === null) { + continue; + } + + $normalizers[$field] = $normalizer; + } + + if ($normalizers === []) { + return null; + } + + return new ArrayShapeNormalizer($normalizers); + } + if ($type instanceof CollectionType) { $valueType = $type->getCollectionValueType(); diff --git a/src/Metadata/ClassMetadata.php b/src/Metadata/ClassMetadata.php index 89ba44c..c3ead0a 100644 --- a/src/Metadata/ClassMetadata.php +++ b/src/Metadata/ClassMetadata.php @@ -13,6 +13,7 @@ * dataSubjectIdField: string|null, * postHydrateCallbacks: list, * preExtractCallbacks: list, + * lazy: bool|null, * } * @template T of object = object */ @@ -30,6 +31,7 @@ public function __construct( private readonly string|null $dataSubjectIdField = null, private readonly array $postHydrateCallbacks = [], private readonly array $preExtractCallbacks = [], + private readonly bool|null $lazy = null, ) { } @@ -63,6 +65,11 @@ public function preExtractCallbacks(): array return $this->preExtractCallbacks; } + public function lazy(): bool|null + { + return $this->lazy; + } + public function dataSubjectIdField(): string|null { return $this->dataSubjectIdField; @@ -94,6 +101,7 @@ public function __serialize(): array 'dataSubjectIdField' => $this->dataSubjectIdField, 'postHydrateCallbacks' => $this->postHydrateCallbacks, 'preExtractCallbacks' => $this->preExtractCallbacks, + 'lazy' => $this->lazy, ]; } @@ -105,5 +113,6 @@ public function __unserialize(array $data): void $this->dataSubjectIdField = $data['dataSubjectIdField']; $this->postHydrateCallbacks = $data['postHydrateCallbacks']; $this->preExtractCallbacks = $data['preExtractCallbacks']; + $this->lazy = $data['lazy']; } } diff --git a/src/MetadataHydrator.php b/src/MetadataHydrator.php index 2e00b28..e1a37a7 100644 --- a/src/MetadataHydrator.php +++ b/src/MetadataHydrator.php @@ -16,6 +16,7 @@ use Patchlevel\Hydrator\Metadata\ClassNotFound; use Patchlevel\Hydrator\Metadata\MetadataFactory; use Patchlevel\Hydrator\Normalizer\HydratorAwareNormalizer; +use ReflectionClass; use ReflectionParameter; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -27,6 +28,8 @@ use function is_object; use function spl_object_id; +use const PHP_VERSION_ID; + final class MetadataHydrator implements Hydrator { /** @var array */ @@ -36,6 +39,7 @@ public function __construct( private readonly MetadataFactory $metadataFactory = new AttributeMetadataFactory(), PayloadCryptographer|null $cryptographer = null, private EventDispatcherInterface|null $eventDispatcher = null, + private readonly bool $defaultLazy = false, ) { if (!$cryptographer) { return; @@ -66,6 +70,33 @@ public function hydrate(string $class, array $data): object throw new ClassNotSupported($class, $e); } + if (PHP_VERSION_ID < 80400) { + return $this->doHydrate($metadata, $data); + } + + $lazy = $metadata->lazy() ?? $this->defaultLazy; + + if (!$lazy) { + return $this->doHydrate($metadata, $data); + } + + return (new ReflectionClass($class))->newLazyProxy( + function () use ($metadata, $data): object { + return $this->doHydrate($metadata, $data); + }, + ); + } + + /** + * @param ClassMetadata $metadata + * @param array $data + * + * @return T + * + * @template T of object + */ + private function doHydrate(ClassMetadata $metadata, array $data): object + { if ($this->eventDispatcher) { $data = $this->eventDispatcher->dispatch(new PreHydrate($data, $metadata))->data; } @@ -110,7 +141,7 @@ public function hydrate(string $class, array $data): object $value = $normalizer->denormalize($value); } catch (Throwable $e) { throw new DenormalizationFailure( - $class, + $metadata->className(), $propertyMetadata->propertyName(), $normalizer::class, $e, @@ -122,7 +153,7 @@ public function hydrate(string $class, array $data): object $propertyMetadata->setValue($object, $value); } catch (TypeError $e) { throw new TypeMismatch( - $class, + $metadata->className(), $propertyMetadata->propertyName(), $e, ); @@ -234,6 +265,7 @@ private function promotedConstructorParametersWithDefaultValue(ClassMetadata $me public static function create( iterable $guessers = [], EventDispatcherInterface|null $eventDispatcher = null, + bool $defaultLazy = false, ): self { $guesser = new BuiltInGuesser(); @@ -250,6 +282,7 @@ public static function create( ), null, $eventDispatcher, + $defaultLazy, ); } } diff --git a/src/Normalizer/ArrayShapeNormalizer.php b/src/Normalizer/ArrayShapeNormalizer.php new file mode 100644 index 0000000..64c10eb --- /dev/null +++ b/src/Normalizer/ArrayShapeNormalizer.php @@ -0,0 +1,107 @@ + $normalizerMap */ + public function __construct( + private readonly array $normalizerMap, + ) { + } + + /** @return array|null */ + public function normalize(mixed $value): array|null + { + if ($value === null) { + return null; + } + + if (!is_array($value)) { + throw InvalidArgument::withWrongType('array|null', $value); + } + + $result = []; + + foreach ($this->normalizerMap as $field => $normalizer) { + if (!isset($value[$field])) { + continue; + } + + $result[$field] = $normalizer->normalize($value[$field]); + } + + return $result; + } + + /** @return array|null */ + public function denormalize(mixed $value): array|null + { + if ($value === null) { + return null; + } + + if (!is_array($value)) { + throw InvalidArgument::withWrongType('array|null', $value); + } + + $result = []; + + foreach ($this->normalizerMap as $field => $normalizer) { + if (!isset($value[$field])) { + continue; + } + + $result[$field] = $normalizer->denormalize($value[$field]); + } + + return $result; + } + + public function setHydrator(Hydrator $hydrator): void + { + foreach ($this->normalizerMap as $normalizer) { + if (!$normalizer instanceof HydratorAwareNormalizer) { + continue; + } + + $normalizer->setHydrator($hydrator); + } + } + + public function handleType(Type|null $type): void + { + if ($type === null) { + return; + } + + if ($type instanceof NullableType) { + $type = $type->getWrappedType(); + } + + if (!$type instanceof ArrayShapeType) { + return; + } + + $shape = $type->getShape(); + + foreach ($this->normalizerMap as $field => $normalizer) { + if (!$normalizer instanceof TypeAwareNormalizer) { + continue; + } + + $normalizer->handleType($shape[$field]['type']); + } + } +} diff --git a/tests/Benchmark/HydratorBench.php b/tests/Benchmark/HydratorBench.php index 7c0c35b..529b980 100644 --- a/tests/Benchmark/HydratorBench.php +++ b/tests/Benchmark/HydratorBench.php @@ -66,7 +66,7 @@ public function benchExtract1Object(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(3)] public function benchHydrate1000Objects(): void { for ($i = 0; $i < 1_000; $i++) { @@ -81,7 +81,7 @@ public function benchHydrate1000Objects(): void } } - #[Bench\Revs(5)] + #[Bench\Revs(3)] public function benchExtract1000Objects(): void { $object = new ProfileCreated( @@ -98,7 +98,7 @@ public function benchExtract1000Objects(): void } } - #[Bench\Revs(5)] + #[Bench\Revs(3)] public function benchHydrate1000000Objects(): void { for ($i = 0; $i < 1_000_000; $i++) { @@ -113,7 +113,7 @@ public function benchHydrate1000000Objects(): void } } - #[Bench\Revs(5)] + #[Bench\Revs(3)] public function benchExtract1000000Objects(): void { $object = new ProfileCreated( diff --git a/tests/Benchmark/HydratorWithCryptographyBench.php b/tests/Benchmark/HydratorWithCryptographyBench.php index 5b8ebb2..da3ab2b 100644 --- a/tests/Benchmark/HydratorWithCryptographyBench.php +++ b/tests/Benchmark/HydratorWithCryptographyBench.php @@ -84,7 +84,7 @@ public function benchExtract1Object(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(3)] public function benchHydrate1000Objects(): void { for ($i = 0; $i < 1_000; $i++) { @@ -102,7 +102,7 @@ public function benchHydrate1000Objects(): void } } - #[Bench\Revs(5)] + #[Bench\Revs(3)] public function benchExtract1000Objects(): void { $object = new ProfileCreated( @@ -119,7 +119,7 @@ public function benchExtract1000Objects(): void } } - #[Bench\Revs(5)] + #[Bench\Revs(3)] public function benchHydrate1000000Objects(): void { for ($i = 0; $i < 1_000_000; $i++) { @@ -137,7 +137,7 @@ public function benchHydrate1000000Objects(): void } } - #[Bench\Revs(5)] + #[Bench\Revs(3)] public function benchExtract1000000Objects(): void { $object = new ProfileCreated( diff --git a/tests/Benchmark/HydratorWithLazyBench.php b/tests/Benchmark/HydratorWithLazyBench.php new file mode 100644 index 0000000..fe9c98d --- /dev/null +++ b/tests/Benchmark/HydratorWithLazyBench.php @@ -0,0 +1,130 @@ +hydrator = MetadataHydrator::create(defaultLazy: true); + } + + public function setUp(): void + { + $object = $this->hydrator->hydrate( + ProfileCreated::class, + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ); + + $this->hydrator->extract($object); + } + + #[Bench\Revs(5)] + public function benchHydrate1Object(): void + { + $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + + #[Bench\Revs(5)] + public function benchHydrate1ObjectTriggerInit(): void + { + $object = $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + + $name = $object->name; + } + + #[Bench\Revs(3)] + public function benchHydrate1000Objects(): void + { + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + } + + #[Bench\Revs(3)] + public function benchHydrate1000ObjectsTriggerInit(): void + { + for ($i = 0; $i < 1_000; $i++) { + $object = $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + + $name = $object->name; + } + } + + #[Bench\Revs(3)] + public function benchHydrate1000000Objects(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + } + + #[Bench\Revs(3)] + public function benchHydrate1000000ObjectsTriggerInit(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $object = $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + + $object = $object->name; + } + } +} diff --git a/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php b/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php index e6cb424..4a8310c 100644 --- a/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php +++ b/tests/Unit/Fixture/InferNormalizerWithIterablesDto.php @@ -13,6 +13,7 @@ final class InferNormalizerWithIterablesDto * @param array $hashMap * @param array> $nested * @param array{foo: string, bar: int, baz: list}|null $jsonArray + * @param array{status: Status, other: iterable}|null $shapeArray */ public function __construct( public array $defaultArray = [], @@ -21,6 +22,7 @@ public function __construct( public array $hashMap = [], public iterable $nested = [], public array|null $jsonArray = null, + public array|null $shapeArray = null, ) { } } diff --git a/tests/Unit/Fixture/LazyProfileCreated.php b/tests/Unit/Fixture/LazyProfileCreated.php new file mode 100644 index 0000000..18a148d --- /dev/null +++ b/tests/Unit/Fixture/LazyProfileCreated.php @@ -0,0 +1,19 @@ +preExtractCallbacks()); self::assertCount(0, $metadata->postHydrateCallbacks()); } + + public function testNoLazy(): void + { + $object = new class { + }; + + $metadataFactory = new AttributeMetadataFactory(); + $metadata = $metadataFactory->metadata($object::class); + + self::assertNull($metadata->lazy()); + } + + public function testLazy(): void + { + $object = new #[Lazy] + class { + }; + + $metadataFactory = new AttributeMetadataFactory(); + $metadata = $metadataFactory->metadata($object::class); + + self::assertTrue($metadata->lazy()); + } } diff --git a/tests/Unit/MetadataHydratorTest.php b/tests/Unit/MetadataHydratorTest.php index df9800c..c5efaa9 100644 --- a/tests/Unit/MetadataHydratorTest.php +++ b/tests/Unit/MetadataHydratorTest.php @@ -29,6 +29,7 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto; +use Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated; use Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto; use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated; @@ -40,7 +41,9 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\StatusWithNormalizer; use Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer; use Patchlevel\Hydrator\TypeMismatch; +use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\TestCase; +use ReflectionClass; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\TypeInfo\Type\ObjectType; @@ -476,6 +479,10 @@ public function testHydrateWithInferNormalizerWitIterables(): void 'bar' => 15, 'baz' => ['test'], ], + [ + 'status' => Status::Draft, + 'other' => [Status::Draft], + ], ); $event = $this->hydrator->hydrate( @@ -487,6 +494,7 @@ public function testHydrateWithInferNormalizerWitIterables(): void 'hashMap' => ['foo' => 'draft', 'bar' => 'draft'], 'nested' => ['foo' => ['draft'], 'bar' => ['draft']], 'jsonArray' => ['foo' => 'php', 'bar' => 15, 'baz' => ['test']], + 'shapeArray' => ['status' => 'draft', 'other' => ['draft']], ], ); @@ -512,6 +520,59 @@ public function testHydrateWithHooks(): void self::assertEquals(false, $object->preExtractCalled); } + #[RequiresPhp('>=8.4')] + public function testLazyHydrate(): void + { + $event = $this->hydrator->hydrate( + LazyProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + $expected = new LazyProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + self::assertInstanceOf(LazyProfileCreated::class, $event); + + $reflection = new ReflectionClass(LazyProfileCreated::class); + + self::assertTrue($reflection->isUninitializedLazyObject($event)); + + $reflection->initializeLazyObject($event); + + self::assertEquals($expected, $event); + } + + #[RequiresPhp('<8.4')] + public function testLazyNotSupported(): void + { + $event = $this->hydrator->hydrate( + LazyProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + $expected = new LazyProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + self::assertEquals($expected, $event); + } + + #[RequiresPhp('>=8.4')] + public function testLazyExtract(): void + { + $event = $this->hydrator->hydrate( + LazyProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + $data = $this->hydrator->extract($event); + + self::assertEquals(['profileId' => '1', 'email' => 'info@patchlevel.de'], $data); + } + public function testCreate(): void { $eventDispatcher = $this->createMock(EventDispatcherInterface::class); diff --git a/tests/Unit/Normalizer/ArrayShapeNormalizerTest.php b/tests/Unit/Normalizer/ArrayShapeNormalizerTest.php new file mode 100644 index 0000000..79dc159 --- /dev/null +++ b/tests/Unit/Normalizer/ArrayShapeNormalizerTest.php @@ -0,0 +1,99 @@ +createMock(Normalizer::class); + + $normalizer = new ArrayShapeNormalizer(['foo' => $innerNormalizer]); + $this->assertEquals(null, $normalizer->normalize(null)); + } + + public function testDenormalizeWithNull(): void + { + $innerNormalizer = $this->createMock(Normalizer::class); + + $normalizer = new ArrayShapeNormalizer(['foo' => $innerNormalizer]); + $this->assertEquals(null, $normalizer->denormalize(null)); + } + + public function testNormalizeWithInvalidArgument(): void + { + $this->expectException(InvalidArgument::class); + + $innerNormalizer = $this->createMock(Normalizer::class); + + $normalizer = new ArrayShapeNormalizer(['foo' => $innerNormalizer]); + $normalizer->normalize('foo'); + } + + public function testDenormalizeWithInvalidArgument(): void + { + $this->expectException(InvalidArgument::class); + + $innerNormalizer = $this->createMock(Normalizer::class); + + $normalizer = new ArrayShapeNormalizer(['foo' => $innerNormalizer]); + $normalizer->denormalize('foo'); + } + + public function testNormalizeWithValue(): void + { + $innerNormalizer = new class implements Normalizer { + public function normalize(mixed $value): string + { + return (string)$value; + } + + public function denormalize(mixed $value): int + { + return (int)$value; + } + }; + + $normalizer = new ArrayShapeNormalizer(['foo' => $innerNormalizer]); + $this->assertEquals(['foo' => '1'], $normalizer->normalize(['foo' => 1])); + } + + public function testDenormalizeWithValue(): void + { + $innerNormalizer = new class implements Normalizer { + public function normalize(mixed $value): string + { + return (string)$value; + } + + public function denormalize(mixed $value): int + { + return (int)$value; + } + }; + + $normalizer = new ArrayShapeNormalizer(['foo' => $innerNormalizer]); + $this->assertEquals(['foo' => 1], $normalizer->denormalize(['foo' => '1'])); + } + + public function testPassHydrator(): void + { + $hydrator = $this->createMock(Hydrator::class); + $normalizer = $this->createMockForIntersectionOfInterfaces([Normalizer::class, HydratorAwareNormalizer::class]); + $normalizer->expects($this->once())->method('setHydrator')->with($hydrator); + + $normalizer = new ArrayShapeNormalizer(['foo' => $normalizer]); + $normalizer->setHydrator($hydrator); + } +}