diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 23ef92b0..4a280557 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -26,7 +26,7 @@ jobs: with: coverage: "none" php-version: "${{ matrix.php-version }}" - ini-values: memory_limit=-1, opcache.enable_cli=1, opcache.jit=tracing, opcache.jit_buffer_size=64M + ini-values: memory_limit=-1 - name: "Checkout base" uses: actions/checkout@v7 @@ -38,7 +38,7 @@ jobs: dependency-versions: ${{ matrix.dependencies }} - name: "phpbench on base" - run: "vendor/bin/phpbench run tests/Benchmark --progress=none --report=default --tag=base" + run: "vendor/bin/phpbench run tests/Benchmark --progress=none --tag=base" - name: "Checkout" uses: actions/checkout@v7 @@ -50,18 +50,35 @@ jobs: dependency-versions: ${{ matrix.dependencies }} - name: "phpbench diff" + id: bench + continue-on-error: true run: "vendor/bin/phpbench run tests/Benchmark --progress=none --report=diff --ref=base > bench.txt" - - name: "Get Bench Result" - id: phpbench - run: | - echo 'BENCH_RESULT<> $GITHUB_ENV - cat bench.txt >> $GITHUB_ENV - echo 'EOF' >> $GITHUB_ENV - - uses: actions/github-script@v9 with: script: | + const fs = require('fs'); + + // the diff report prints one section per benchmark class: a title line, a console table, a blank line. + // The console table becomes a markdown table by dropping the outer borders and turning the border + // below the header into the separator row. + const sections = fs.readFileSync('bench.txt', 'utf8').trim().split(/\n\s*\n/).map((section) => { + const [title, , header, separator, ...rows] = section.trim().split('\n'); + const table = [header, separator.replace(/\+/g, '|'), ...rows.filter((row) => !row.startsWith('+'))]; + + return `### ${title.replace(/Bench$/, '')}\n\n${table.join('\n')}`; + }).join('\n\n'); + + const failed = ${{ steps.bench.outcome == 'failure' }}; + const commentBody = `Hello :wave: + + Here are the most recent benchmark results, compared against \`${{ github.base_ref }}\`. Time is the mode over the iterations in the \`its\` column, with the relative standard deviation in brackets. + + ${sections} + + ${failed ? ':warning: At least one benchmark got slower than the tolerance in phpbench.json allows.' : ''} + This comment gets updated every time a new commit comes in.`; + // Get the existing comments. const {data: comments} = await github.rest.issues.listComments({ owner: context.repo.owner, @@ -69,27 +86,8 @@ jobs: issue_number: context.payload.number, }) - // Find any comment already made by the bot. - const botComment = comments.find(comment => comment.user.id === 41898282) - const commentBody = ` - - Hello :wave: - -
- here is the most recent benchmark result: - -

- - \`\`\` - ${{ env.BENCH_RESULT }} - \`\`\` - -

-
- - This comment gets update everytime a new commit comes in! - - `; + // Find any comment already made by the bot. + const botComment = comments.find(comment => comment.user.id === 41898282) if (!context.payload.pull_request.head.repo.full_name.startsWith('patchlevel/')) { console.log('Not attempting to write comment on PR from fork'); @@ -110,3 +108,7 @@ jobs: }) } } + + - name: "Fail on regression" + if: steps.bench.outcome == 'failure' + run: "exit 1" diff --git a/.gitignore b/.gitignore index a8264bba..3e62e239 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ infection.log infection.html .phpbench/ docs_php/ +var +stub diff --git a/Makefile b/Makefile index faf64b56..9c351c11 100644 --- a/Makefile +++ b/Makefile @@ -34,15 +34,18 @@ static: phpstan cs ## run stat test: phpunit ## run tests -.PHONY: benchmark -benchmark: vendor ## run benchmarks - vendor/bin/phpbench run tests/Benchmark --report=default +.PHONY: snapshot +snapshot: vendor ## regenerate the snapshot of the generated middleware + UPDATE_SNAPSHOTS=1 vendor/bin/phpunit --no-coverage --filter testGeneratedCodeMatchesSnapshot -.PHONY: benchmark-diff-test -benchmark-diff-test: vendor ## run benchmarks - vendor/bin/phpbench run tests/Benchmark --revs=1 --report=default --progress=none --tag=base - vendor/bin/phpbench run tests/Benchmark --revs=1 --report=diff --progress=none --ref=base +.PHONY: benchmark +benchmark: vendor ## run all benchmarks + vendor/bin/phpbench run --report=diff +.PHONY: benchmark-diff +benchmark-diff: vendor ## run the benchmarks twice and compare the second run against the first, like the CI does + vendor/bin/phpbench run --progress=none --tag=base + vendor/bin/phpbench run --progress=none --report=diff --ref=base .PHONY: docs docs: docs-extract-php docs-php-lint docs-phpcs docs-inject-php ## check and format docs code diff --git a/composer.lock b/composer.lock index 28ace722..cb32eb05 100644 --- a/composer.lock +++ b/composer.lock @@ -161,16 +161,16 @@ }, { "name": "symfony/type-info", - "version": "v8.1.0", + "version": "v8.1.5", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7" + "reference": "ceb48db5b38d6a48640c414be0c69d53980ae5c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7", - "reference": "9f24df8a79781b9b9f030fea7dfd2f3bd1e7e7e7", + "url": "https://api.github.com/repos/symfony/type-info/zipball/ceb48db5b38d6a48640c414be0c69d53980ae5c5", + "reference": "ceb48db5b38d6a48640c414be0c69d53980ae5c5", "shasum": "" }, "require": { @@ -219,7 +219,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v8.1.0" + "source": "https://github.com/symfony/type-info/tree/v8.1.5" }, "funding": [ { @@ -239,7 +239,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-08-21T17:47:34+00:00" } ], "packages-dev": [ @@ -723,27 +723,25 @@ }, { "name": "doctrine/lexer", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + "reference": "e96fe45e92a54233726014a7cc7340abf29bb14c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", - "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e96fe45e92a54233726014a7cc7340abf29bb14c", + "reference": "e96fe45e92a54233726014a7cc7340abf29bb14c", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "doctrine/coding-standard": "^12", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^10.5", - "psalm/plugin-phpunit": "^0.18.3", - "vimeo/psalm": "^5.21" + "doctrine/coding-standard": "^14", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10.5.58 || ^12.5.4" }, "type": "library", "autoload": { @@ -780,7 +778,7 @@ ], "support": { "issues": "https://github.com/doctrine/lexer/issues", - "source": "https://github.com/doctrine/lexer/tree/3.0.1" + "source": "https://github.com/doctrine/lexer/tree/3.0.2" }, "funding": [ { @@ -796,7 +794,7 @@ "type": "tidelift" } ], - "time": "2024-02-05T11:56:58+00:00" + "time": "2026-06-14T20:44:06+00:00" }, { "name": "fidry/cpu-core-counter", @@ -1231,19 +1229,20 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.10.0", + "version": "6.13.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" + "reference": "30de5e9b3fa04253af272a7d932a38cd026c311b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", - "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/30de5e9b3fa04253af272a7d932a38cd026c311b", + "reference": "30de5e9b3fa04253af272a7d932a38cd026c311b", "shasum": "" }, "require": { + "ext-filter": "*", "ext-json": "*", "marc-mabe/php-enum": "^4.4", "php": "^7.2 || ^8.0" @@ -1276,20 +1275,9 @@ ], "authors": [ { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - }, - { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" + "name": "Danny van der Sluijs", + "email": "danny.vandersluijs@icloud.com", + "role": "Maintainer" } ], "description": "A library to validate a json schema.", @@ -1300,22 +1288,22 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.13.0" }, - "time": "2026-06-16T20:50:26+00:00" + "time": "2026-09-23T19:46:42+00:00" }, { "name": "league/commonmark", - "version": "2.10.1", + "version": "2.10.3", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "9d489ab67a02960fd8ffe624d93f751daf95439e" + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/9d489ab67a02960fd8ffe624d93f751daf95439e", - "reference": "9d489ab67a02960fd8ffe624d93f751daf95439e", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6efbd9c472b91db0a3350fcd601c8332c2382e1f", + "reference": "6efbd9c472b91db0a3350fcd601c8332c2382e1f", "shasum": "" }, "require": { @@ -1409,7 +1397,7 @@ "type": "tidelift" } ], - "time": "2026-09-07T13:44:26+00:00" + "time": "2026-09-21T13:07:34+00:00" }, { "name": "league/config", @@ -1568,20 +1556,20 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.4", + "version": "1.14.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", "shasum": "" }, "require": { - "php": "^7.1 || ^8.0" + "php": "^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", @@ -1616,15 +1604,15 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" }, "funding": [ { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" + "url": "https://github.com/mnapoli", + "type": "github" } ], - "time": "2025-08-01T08:46:24+00:00" + "time": "2026-08-11T10:17:44+00:00" }, { "name": "nette/schema", @@ -1786,16 +1774,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.8.0", + "version": "v5.9.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", - "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9e33da9553fe7786f0962b35f4e4ecf01be89def", + "reference": "9e33da9553fe7786f0962b35f4e4ecf01be89def", "shasum": "" }, "require": { @@ -1837,9 +1825,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.9.0" }, - "time": "2026-07-04T14:30:18+00:00" + "time": "2026-09-13T18:51:52+00:00" }, { "name": "ondram/ci-detector", @@ -2094,16 +2082,16 @@ }, { "name": "phpat/phpat", - "version": "0.12.4", + "version": "0.12.5", "source": { "type": "git", "url": "https://github.com/carlosas/phpat.git", - "reference": "5319264270c335f548451209bb0f32b55aa59924" + "reference": "408d16fdd893e1efc4a3c4a851f407204c69781a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/carlosas/phpat/zipball/5319264270c335f548451209bb0f32b55aa59924", - "reference": "5319264270c335f548451209bb0f32b55aa59924", + "url": "https://api.github.com/repos/carlosas/phpat/zipball/408d16fdd893e1efc4a3c4a851f407204c69781a", + "reference": "408d16fdd893e1efc4a3c4a851f407204c69781a", "shasum": "" }, "require": { @@ -2145,9 +2133,9 @@ "description": "PHP Architecture Tester", "support": { "issues": "https://github.com/carlosas/phpat/issues", - "source": "https://github.com/carlosas/phpat/tree/0.12.4" + "source": "https://github.com/carlosas/phpat/tree/0.12.5" }, - "time": "2026-03-17T16:47:43+00:00" + "time": "2026-09-15T18:11:37+00:00" }, { "name": "phpbench/container", @@ -2301,16 +2289,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.3", + "version": "2.3.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/148cefffaf0233e4c08cc13db8a195a56dd6dfe9", + "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9", "shasum": "" }, "require": { @@ -2342,17 +2330,17 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.5" }, - "time": "2026-07-08T07:01:06+00:00" + "time": "2026-08-31T16:05:28+00:00" }, { "name": "phpstan/phpstan", - "version": "2.2.7", + "version": "2.2.16", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921", - "reference": "692db47b9dddb0487934e5236e77d48594aef921", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92", + "reference": "46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92", "shasum": "" }, "require": { @@ -2408,7 +2396,7 @@ "type": "github" } ], - "time": "2026-07-29T17:39:32+00:00" + "time": "2026-09-25T09:31:51+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -3059,22 +3047,22 @@ }, { "name": "sanmai/di-container", - "version": "0.1.17", + "version": "0.1.23", "source": { "type": "git", "url": "https://github.com/sanmai/di-container.git", - "reference": "a901c4a8778c9212ef4d66607525281af2f787bd" + "reference": "8cf59c091f33297389d0a5a27ea0d688df15c376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/di-container/zipball/a901c4a8778c9212ef4d66607525281af2f787bd", - "reference": "a901c4a8778c9212ef4d66607525281af2f787bd", + "url": "https://api.github.com/repos/sanmai/di-container/zipball/8cf59c091f33297389d0a5a27ea0d688df15c376", + "reference": "8cf59c091f33297389d0a5a27ea0d688df15c376", "shasum": "" }, "require": { "php": ">=8.2", "psr/container": "^1.1.2 || ^2.0", - "sanmai/pipeline": "^6.17 || ^7.0" + "sanmai/pipeline": "^7.10" }, "require-dev": { "ergebnis/composer-normalize": "^2.8", @@ -3126,7 +3114,7 @@ ], "support": { "issues": "https://github.com/sanmai/di-container/issues", - "source": "https://github.com/sanmai/di-container/tree/0.1.17" + "source": "https://github.com/sanmai/di-container/tree/0.1.23" }, "funding": [ { @@ -3134,7 +3122,7 @@ "type": "github" } ], - "time": "2026-06-01T08:52:14+00:00" + "time": "2026-08-11T00:58:41+00:00" }, { "name": "sanmai/duoclock", @@ -4448,16 +4436,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.5", + "version": "3.13.6", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", - "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/4c378e1a528ea066890fc2397cbdd2f94eb2fc91", + "reference": "4c378e1a528ea066890fc2397cbdd2f94eb2fc91", "shasum": "" }, "require": { @@ -4523,7 +4511,7 @@ "type": "thanks_dev" } ], - "time": "2025-11-04T16:30:35+00:00" + "time": "2026-08-06T00:17:32+00:00" }, { "name": "staabm/side-effects-detector", @@ -4758,16 +4746,16 @@ }, { "name": "symfony/console", - "version": "v8.1.2", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15" + "reference": "29afb89f4e941f68a6e90f28e3f52ff1f6793a7d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/535e18a1b8925f6c01a55b171d157ab66c2ace15", - "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15", + "url": "https://api.github.com/repos/symfony/console/zipball/29afb89f4e941f68a6e90f28e3f52ff1f6793a7d", + "reference": "29afb89f4e941f68a6e90f28e3f52ff1f6793a7d", "shasum": "" }, "require": { @@ -4834,7 +4822,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.1.2" + "source": "https://github.com/symfony/console/tree/v8.1.7" }, "funding": [ { @@ -4854,7 +4842,7 @@ "type": "tidelift" } ], - "time": "2026-07-27T13:58:19+00:00" + "time": "2026-09-13T10:55:57+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4929,16 +4917,16 @@ }, { "name": "symfony/filesystem", - "version": "v8.1.2", + "version": "v8.1.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "17856b7a222664a26a5ea1cb06ee0721c2438217" + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/17856b7a222664a26a5ea1cb06ee0721c2438217", - "reference": "17856b7a222664a26a5ea1cb06ee0721c2438217", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/7599ebb855fede59413ddb7f15198d67bfcae7ba", + "reference": "7599ebb855fede59413ddb7f15198d67bfcae7ba", "shasum": "" }, "require": { @@ -4976,7 +4964,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.1.2" + "source": "https://github.com/symfony/filesystem/tree/v8.1.6" }, "funding": [ { @@ -4996,20 +4984,20 @@ "type": "tidelift" } ], - "time": "2026-07-22T15:42:13+00:00" + "time": "2026-08-23T10:06:25+00:00" }, { "name": "symfony/finder", - "version": "v8.1.1", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "e2989e762c70f9490fa3a00a0ac0fae5aa97a531" + "reference": "4fbe46a3eb64abf8a57f0364075b91f4a233e062" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/e2989e762c70f9490fa3a00a0ac0fae5aa97a531", - "reference": "e2989e762c70f9490fa3a00a0ac0fae5aa97a531", + "url": "https://api.github.com/repos/symfony/finder/zipball/4fbe46a3eb64abf8a57f0364075b91f4a233e062", + "reference": "4fbe46a3eb64abf8a57f0364075b91f4a233e062", "shasum": "" }, "require": { @@ -5044,7 +5032,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.1.1" + "source": "https://github.com/symfony/finder/tree/v8.1.7" }, "funding": [ { @@ -5064,7 +5052,7 @@ "type": "tidelift" } ], - "time": "2026-06-27T09:05:56+00:00" + "time": "2026-09-10T19:14:39+00:00" }, { "name": "symfony/options-resolver", @@ -5391,16 +5379,16 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.38.0", + "version": "v1.42.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", "shasum": "" }, "require": { @@ -5452,7 +5440,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" }, "funding": [ { @@ -5472,7 +5460,7 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:48:31+00:00" + "time": "2026-08-07T06:33:24+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -5725,16 +5713,16 @@ }, { "name": "symfony/process", - "version": "v8.1.0", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5" + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", - "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "url": "https://api.github.com/repos/symfony/process/zipball/10823b09358e690df4ff943e24e8492bb1019fc3", + "reference": "10823b09358e690df4ff943e24e8492bb1019fc3", "shasum": "" }, "require": { @@ -5766,7 +5754,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.1.0" + "source": "https://github.com/symfony/process/tree/v8.1.7" }, "funding": [ { @@ -5786,7 +5774,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-09-02T12:40:29+00:00" }, { "name": "symfony/service-contracts", @@ -5877,16 +5865,16 @@ }, { "name": "symfony/string", - "version": "v8.1.2", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", - "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "url": "https://api.github.com/repos/symfony/string/zipball/d950140b5f56f31901e5b7a0c04ffc3a3deb943c", + "reference": "d950140b5f56f31901e5b7a0c04ffc3a3deb943c", "shasum": "" }, "require": { @@ -5943,7 +5931,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.1.2" + "source": "https://github.com/symfony/string/tree/v8.1.7" }, "funding": [ { @@ -5963,20 +5951,20 @@ "type": "tidelift" } ], - "time": "2026-07-28T07:35:25+00:00" + "time": "2026-09-11T14:51:16+00:00" }, { "name": "symfony/var-dumper", - "version": "v8.1.2", + "version": "v8.1.7", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "865103cf742a039f34645b971fc3ace308d6c167" + "reference": "741a8c4c948b0bb9bd3653daacd3644c73c40ea5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/865103cf742a039f34645b971fc3ace308d6c167", - "reference": "865103cf742a039f34645b971fc3ace308d6c167", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/741a8c4c948b0bb9bd3653daacd3644c73c40ea5", + "reference": "741a8c4c948b0bb9bd3653daacd3644c73c40ea5", "shasum": "" }, "require": { @@ -6030,7 +6018,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.1.2" + "source": "https://github.com/symfony/var-dumper/tree/v8.1.7" }, "funding": [ { @@ -6050,7 +6038,7 @@ "type": "tidelift" } ], - "time": "2026-07-22T15:42:13+00:00" + "time": "2026-09-03T16:02:27+00:00" }, { "name": "symfony/var-exporter", diff --git a/docs/extensions.md b/docs/extensions.md index 9cd0f7f1..1690d21c 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -23,7 +23,7 @@ $hydrator = (new StackHydratorBuilder()) ``` ## Built-in extensions -The library ships with four extensions out of the box: +The library ships with five extensions out of the box: | Extension | Purpose | | --- | --- | @@ -31,6 +31,7 @@ The library ships with four extensions out of the box: | `LifecycleExtension` | [Lifecycle hooks](lifecycle-hooks.md), run code before and after the extract and hydrate process. | | `CryptographyExtension` | [Cryptography](cryptography.md), encrypt and decrypt sensitive data with crypto-shredding. | | `UpcastExtension` | [Upcasting](upcasting.md), reshape outdated stored data while it is hydrated. | +| `GeneratedMiddlewareExtension` | [Generated code](generated-code.md), generated mapping code for a fixed set of classes on top of the `CoreExtension`. | ## Middleware @@ -175,4 +176,5 @@ final class AuditExtension implements Extension * [How to run code before extract and after hydrate](lifecycle-hooks.md) * [How to encrypt sensitive data](cryptography.md) * [How to reshape outdated stored data](upcasting.md) +* [How to speed up hydration with generated code](generated-code.md) * [How to cache the metadata](caching.md) diff --git a/docs/generated-code.md b/docs/generated-code.md new file mode 100644 index 00000000..f8ce15da --- /dev/null +++ b/docs/generated-code.md @@ -0,0 +1,103 @@ +# Generated Code + +The `TransformMiddleware` maps arrays to objects with reflection: it iterates +over the metadata of a class, sets each property through a `ReflectionProperty` +and calls the [normalizers](normalizer.md) dynamically. This is fast enough for +most applications, but when you hydrate millions of objects the reflection +overhead adds up. The `GeneratedMiddlewareExtension` generates plain PHP code +for a fixed set of classes, which is then used instead of the reflection based +mapping for exactly these classes. + +## Enable the extension + +Register the `GeneratedMiddlewareExtension` in addition to the `CoreExtension`. +It needs a writable directory for the generated file and the list of classes +for which code should be generated. Classes referenced by these classes, such +as nested value objects, are discovered automatically. + +```php +use Patchlevel\Hydrator\CoreExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; +use Patchlevel\Hydrator\StackHydratorBuilder; + +$hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedMiddlewareExtension( + cachePath: __DIR__ . '/var/cache/hydrator', + classes: [ + ProfileCreated::class, + NameChanged::class, + ], + )) + ->build(); +``` +:::note +The builder returns a `GeneratedHydrator` instead of the plain `StackHydrator`. +It is a `StackHydrator` in every respect, but for the generated classes it +calls the generated code directly and skips the middleware stack entirely, as +long as no other middleware has to run for the class (see +[Nested objects](#nested-objects) for how that is decided). Everything else +takes the regular path. +::: + +:::success +The generated code behaves exactly like the `TransformMiddleware`: objects are +created without calling the constructor, missing fields keep their promoted +default value, private and readonly properties are supported and the same +exceptions are thrown. You can switch between both without changing your +classes. +::: + +The generated middleware runs right before the `TransformMiddleware` of the +`CoreExtension`, regardless of the registration order. Classes which are not +part of the list are passed on and handled by the `TransformMiddleware` as +usual. + +## Caching + +The generated middleware is written to the cache path once and reused on +subsequent requests, so the generation only happens on the first run. The file +name is derived from the configuration, changing the class list or the options +results in a new file. In development you can regenerate the code on every +build with the `debug` flag. + +```php +use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; + +$extension = new GeneratedMiddlewareExtension( + cachePath: __DIR__ . '/var/cache/hydrator', + classes: [ProfileCreated::class], + debug: true, +); +``` +:::warning +The generated code is tied to the [metadata](caching.md) of your classes. If a +field is renamed or a normalizer is added after the code was generated, the +middleware throws an `OutdatedGeneratedMiddleware` exception. Clear the cache +path when you deploy, like you do for other generated files. +::: + +## Nested objects + +The biggest speedup comes from inlining nested objects: instead of going through +the whole middleware stack again for every nested object, the generated code +hydrates and extracts them directly. Inlining a class would hide it from the +other middlewares, so it only happens when every other middleware in the stack +is a `SkippableMiddleware` which [skips](extensions.md) that class. The decision +is made per class and per direction: with the +[cryptography](cryptography.md) extension for example, value objects without +sensitive data are inlined while classes with sensitive data still go through +the stack. + +:::note +Nested classes with a class level normalizer or [lazy](lazy.md) classes are +never inlined, they always take the regular path through the hydrator. The +generated middleware is itself skippable: classes it has no code for go +straight to the `TransformMiddleware`. +::: + +## Learn more + +* [How to build the hydrator](hydrator.md) +* [How to cache the metadata](caching.md) +* [How to write your own extension](extensions.md) diff --git a/docs/hydrator.md b/docs/hydrator.md index 8cf62e48..0211ad18 100644 --- a/docs/hydrator.md +++ b/docs/hydrator.md @@ -183,3 +183,4 @@ value does not fit the property type. * [How normalizers convert complex types](normalizer.md) * [How to hydrate objects lazily](lazy.md) * [How to hook into the hydration process](extensions.md) +* [How to speed up hydration with generated code](generated-code.md) diff --git a/docs/project.json b/docs/project.json index 5fae563a..c7103e75 100644 --- a/docs/project.json +++ b/docs/project.json @@ -18,7 +18,8 @@ "subEntries": [ { "title": "Lifecycle Hooks", "file": "lifecycle-hooks.md" }, { "title": "Cryptography", "file": "cryptography.md" }, - { "title": "Upcasting", "file": "upcasting.md" } + { "title": "Upcasting", "file": "upcasting.md" }, + { "title": "Generated Code", "file": "generated-code.md" } ] }, { diff --git a/phpbench.json b/phpbench.json index 729b39d4..60c604cd 100644 --- a/phpbench.json +++ b/phpbench.json @@ -1,54 +1,37 @@ { - "$schema":"./vendor/phpbench/phpbench/phpbench.schema.json", + "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", "runner.bootstrap": "vendor/autoload.php", + "runner.path": "tests/Benchmark", "runner.file_pattern": "*Bench.php", + "runner.iterations": 5, + "runner.php_config": { + "memory_limit": "-1", + "opcache.enable_cli": 1, + "opcache.jit_buffer_size": 0, + "xdebug.mode": "off" + }, + "runner.assert": "mode(variant.time.avg) <= mode(baseline.time.avg) +/- 25%", "report.generators": { "diff": { - "generator": "component", - "partition": ["benchmark_name"], - "components": [ - { - "component": "section", - "title": "{{ first(frame[\"benchmark_name\"]) }}", - "components": [ - { - "component": "table_aggregate", - "partition": ["subject_name", "variant_name"], - "groups": - { - "time (kde mode)": - { - "cols": ["time"] - }, - "memory": - { - "cols": ["memory"] - } - }, - "row": - { - "subject": "first(partition[\"subject_name\"]) ~ \" (\" ~ first(partition[\"variant_name\"]) ~ \")\"", - "time": - { - "type": "expand", - "partition": "suite_tag", - "cols": - { - "Tag: {{ key }}": "mode(partition[\"result_time_avg\"]) as time ~ ' (' ~ rstdev(partition['result_time_avg']) ~ ')'" - } - }, - "memory": - { - "type": "expand", - "partition": "suite_tag", - "cols": - { - "Tag: {{ key }} ": "mode(partition[\"result_mem_peak\"]) as memory" - } - } - } - }] - }] + "generator": "expression", + "break": [ + "benchmark" + ], + "cols": [ + "subject", + "revs", + "its", + "memory", + "base", + "current", + "diff" + ], + "expressions": { + "memory": "if(first(has_baseline), (first(result_mem_peak) as memory) ~ ' ' ~ percent_diff(first(baseline_mem_peak), first(result_mem_peak)), first(result_mem_peak) as memory)", + "base": "if(first(has_baseline), mode(baseline_time_avg) as time ~ ' (' ~ rstdev(baseline_time_avg) ~ ')', '')", + "current": "mode(result_time_avg) as time ~ ' (' ~ rstdev(result_time_avg) ~ ')'", + "diff": "if(first(has_baseline), percent_diff(mode(baseline_time_avg), mode(result_time_avg), rstdev(result_time_avg)), '')" + } } } -} \ No newline at end of file +} diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 6998ee63..75142e1f 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -10,6 +10,8 @@ src tests + + tests/Unit/Extension/Generated/Snapshot/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 81e9ded9..7bb272f1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -8,6 +8,8 @@ parameters: paths: - src - tests + excludePaths: + - tests/Unit/Extension/Generated/Snapshot reportUnmatchedIgnoredErrors: false services: diff --git a/src/Extension/Generated/ClassNotGeneratable.php b/src/Extension/Generated/ClassNotGeneratable.php new file mode 100644 index 00000000..bff6fb12 --- /dev/null +++ b/src/Extension/Generated/ClassNotGeneratable.php @@ -0,0 +1,18 @@ + $properties + * @param bool $scopedHydrate hydrate code must run in the scope of the class + * @param bool $scopedExtract extract code must run in the scope of the class + */ + public function __construct( + public readonly int $index, + public readonly ClassMetadata $metadata, + public readonly array $properties, + public readonly bool $scopedHydrate, + public readonly bool $scopedExtract, + ) { + } + + /** @return class-string */ + public function className(): string + { + return $this->metadata->className; + } + + /** Classes without normalizers can never be part of a circular reference. */ + public function leaf(): bool + { + foreach ($this->properties as $property) { + if ($property->kind !== ValueKind::Raw) { + return false; + } + } + + return true; + } +} diff --git a/src/Extension/Generated/ClassPlanner.php b/src/Extension/Generated/ClassPlanner.php new file mode 100644 index 00000000..888e8d17 --- /dev/null +++ b/src/Extension/Generated/ClassPlanner.php @@ -0,0 +1,304 @@ + class => index, null for classes which can not be part of the middleware */ + private array $index = []; + + /** @var array */ + private array $classes = []; + + private int $nextIndex = 0; + private int $normalizerSlots = 0; + private int $flagSlots = 0; + private int $defaultSlots = 0; + + public function __construct( + private readonly MetadataFactory $metadataFactory, + ) { + } + + /** + * @param class-string $class + * + * @throws ClassNotGeneratable + */ + public function add(string $class): void + { + $class = ltrim($class, '\\'); + + try { + $metadata = $this->metadataFactory->metadata($class); + } catch (ClassNotFound) { + throw new ClassNotGeneratable($class, 'class not found'); + } + + if ($metadata->normalizer !== null) { + // handled by the hydrator itself, never reaches a middleware + return; + } + + if (!self::generatable($metadata)) { + throw new ClassNotGeneratable($class, 'the class is internal, abstract or an interface'); + } + + $this->register($metadata); + } + + /** @return list ordered by index */ + public function plans(): array + { + ksort($this->classes); + + return array_values($this->classes); + } + + /** @return int index of the registered class */ + private function register(ClassMetadata $metadata): int + { + $class = $metadata->className; + $index = $this->index[$class] ?? null; + + if ($index !== null) { + return $index; + } + + $index = $this->nextIndex++; + // reserve the index before planning the properties: nested classes may reference this class again + $this->index[$class] = $index; + + $properties = []; + + foreach ($metadata->properties as $property) { + $properties[] = $this->plan($metadata, $property); + } + + [$scopedHydrate, $scopedExtract] = self::scoped($metadata, $properties); + + $this->classes[$index] = new ClassPlan($index, $metadata, $properties, $scopedHydrate, $scopedExtract); + + return $index; + } + + private function plan(ClassMetadata $metadata, PropertyMetadata $property): PropertyPlan + { + $reflection = $property->reflection; + $declaringClass = $reflection->getDeclaringClass()->getName(); + $inherited = $declaringClass !== $metadata->className; + + $kind = ValueKind::Raw; + $slot = null; + $inner = null; + $flag = null; + $nested = null; + $default = null; + $defaultSlot = null; + + $normalizer = $property->normalizer; + + if ($normalizer !== null) { + $kind = ValueKind::Normalizer; + $slot = $this->normalizerSlots++; + $nested = $this->nestedClass($normalizer); + + if ($nested !== null) { + $kind = ValueKind::NestedObject; + } elseif ($normalizer instanceof ArrayNormalizer) { + $nested = $this->nestedClass($normalizer->innerNormalizer()); + + if ($nested !== null) { + $kind = ValueKind::NestedArray; + $inner = $this->normalizerSlots++; + } + } + + if ($nested !== null) { + $flag = $this->flagSlots++; + } + } + + if ($reflection->isPromoted()) { + $parameter = $metadata->promotedConstructorDefaults()[$property->propertyName] ?? null; + + if ($parameter !== null) { + // scalar defaults are inlined, everything else is resolved via reflection at runtime + $default = self::literal($parameter->getDefaultValue()); + + if ($default === null) { + $defaultSlot = $this->defaultSlots++; + } + } + } + + return new PropertyPlan( + name: $property->propertyName, + field: $property->fieldName, + kind: $kind, + slot: $slot, + inner: $inner, + flag: $flag, + nested: $nested, + default: $default, + defaultSlot: $defaultSlot, + hydrateScope: $inherited && ($reflection->isPrivate() || $reflection->isReadOnly() || self::privateSet($reflection)) ? $declaringClass : null, + extractScope: $inherited && $reflection->isPrivate() ? $declaringClass : null, + ); + } + + /** @return int|null the index of the nested class if the normalizer is an object normalizer for an inlinable class */ + private function nestedClass(Normalizer $normalizer): int|null + { + if (!$normalizer instanceof ObjectNormalizer) { + return null; + } + + try { + $class = $normalizer->className(); + } catch (InvalidType) { + return null; + } + + return $this->nestedIndex($class); + } + + /** + * Nested classes can only be inlined if they would end up in this middleware anyway. + * + * @param class-string $class + */ + private function nestedIndex(string $class): int|null + { + if (array_key_exists($class, $this->index)) { + return $this->index[$class]; + } + + try { + $metadata = $this->metadataFactory->metadata($class); + } catch (ClassNotFound) { + return $this->index[$class] = null; + } + + if ($metadata->normalizer !== null || $metadata->lazy === true || !self::generatable($metadata)) { + return $this->index[$class] = null; + } + + return $this->register($metadata); + } + + /** + * Code runs in the scope of the class (a bound closure) only if needed, otherwise plain methods are used. + * + * @param list $properties + * + * @return array{bool, bool} [hydrate needs the class scope, extract needs the class scope] + */ + private static function scoped(ClassMetadata $metadata, array $properties): array + { + $hydrate = false; + $extract = false; + + foreach ($properties as $property) { + if ($property->hydrateScope !== null || $property->extractScope !== null) { + // handled by helper closures bound to the declaring class + continue; + } + + $reflection = $metadata->properties[$property->name]->reflection; + + if (!$reflection->isPublic()) { + $hydrate = true; + $extract = true; + } elseif (self::restrictedSet($reflection)) { + $hydrate = true; + } + } + + return [$hydrate, $extract]; + } + + /** Constructor visibility does not matter, the generated code runs in the scope of the class. */ + private static function generatable(ClassMetadata $metadata): bool + { + $reflection = $metadata->reflection; + + return !$reflection->isInternal() && !$reflection->isAbstract() && !$reflection->isInterface() && !$reflection->isEnum(); + } + + /** Readonly and asymmetric visibility restrict writes to the class scope. */ + private static function restrictedSet(ReflectionProperty $reflection): bool + { + return $reflection->isReadOnly() || self::privateSet($reflection) || (PHP_VERSION_ID >= 80400 && $reflection->isProtectedSet()); + } + + private static function privateSet(ReflectionProperty $reflection): bool + { + return PHP_VERSION_ID >= 80400 && $reflection->isPrivateSet(); + } + + /** @return string|null php expression for the value, null if it can not be expressed as a literal */ + private static function literal(mixed $value): string|null + { + if ($value === null || is_bool($value) || is_int($value) || is_float($value) || is_string($value)) { + return var_export($value, true); + } + + if ($value instanceof UnitEnum) { + return sprintf('\\%s::%s', $value::class, $value->name); + } + + if (is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $literal = self::literal($item); + + if ($literal === null) { + return null; + } + + $items[] = sprintf('%s => %s', var_export($key, true), $literal); + } + + return '[' . implode(', ', $items) . ']'; + } + + return null; + } +} diff --git a/src/Extension/Generated/CodeEmitter.php b/src/Extension/Generated/CodeEmitter.php new file mode 100644 index 00000000..8e51b6ca --- /dev/null +++ b/src/Extension/Generated/CodeEmitter.php @@ -0,0 +1,523 @@ + property declarations of the generated class */ + private array $properties = []; + + /** @var list init methods, one per class */ + private array $inits = []; + + /** @var list hydrate/extract methods which do not need the scope of a class */ + private array $methods = []; + + private int $helperSlots = 0; + + /** @param list $classes indexed by {@see ClassPlan::$index} */ + public function __construct( + private readonly array $classes, + ) { + } + + public function emit(string $namespace, string $className): string + { + $constructor = []; + $compiledHydrateCases = []; + $compiledExtractCases = []; + $skipCases = []; + $hydrateCases = []; + $extractCases = []; + + foreach ($this->classes as $plan) { + $class = $plan->className(); + $index = $plan->index; + + // until the class is initialized, the closures point to a stub which initializes the class first + $constructor[] = sprintf( + '$this->h%1$d = function (array $data, array $context, object|null $object = null): object { $this->init%1$d(); return ($this->h%1$d)($data, $context, $object); };', + $index, + ); + $constructor[] = sprintf( + '$this->e%1$d = function (object $object, array $context): array { $this->init%1$d(); return ($this->e%1$d)($object, $context); };', + $index, + ); + + $hydrateCall = $plan->scopedHydrate ? sprintf('($this->h%d)', $index) : sprintf('$this->hydrate%d', $index); + $extractCall = $plan->scopedExtract ? sprintf('$this->e%d', $index) : sprintf('$this->extract%d(...)', $index); + $compiledHydrateCases[] = Templates::render(Templates::COMPILED_CASE, ['class' => $class, 'index' => (string)$index, 'closure' => sprintf('$this->fastHydrate%d(...)', $index)]); + $compiledExtractCases[] = Templates::render(Templates::COMPILED_CASE, ['class' => $class, 'index' => (string)$index, 'closure' => $extractCall]); + $this->methods[] = Templates::render(Templates::FAST_HYDRATE, ['index' => (string)$index, 'call' => $hydrateCall]); + + $skipCases[] = sprintf('\\%s::class,', $class); + $hydrateCases[] = $plan->scopedHydrate + ? sprintf('\\%s::class => ($this->h%d)($data, $populate, $object),', $class, $index) + : sprintf('\\%1$s::class => $this->ready%2$d ? $this->hydrate%2$d($data, $populate, $object) : ($this->h%2$d)($data, $populate, $object),', $class, $index); + $extractCases[] = $plan->scopedExtract + ? sprintf('\\%s::class => ($this->e%d)($object, $context),', $class, $index) + : sprintf('\\%1$s::class => $this->ready%2$d ? $this->extract%2$d($object, $context) : ($this->e%2$d)($object, $context),', $class, $index); + + $this->emitClass($plan); + } + + return Templates::render(Templates::MIDDLEWARE, [ + 'namespace' => $namespace, + 'className' => $className, + 'version' => (string)MiddlewareGenerator::VERSION, + 'properties' => implode("\n", $this->properties), + 'constructor' => implode("\n", $constructor), + 'skipCases' => implode("\n", $skipCases), + 'compiledHydrateCases' => implode("\n", $compiledHydrateCases), + 'compiledExtractCases' => implode("\n", $compiledExtractCases), + 'hydrateCases' => implode("\n", $hydrateCases), + 'extractCases' => implode("\n", $extractCases), + 'inits' => implode("\n\n", $this->inits), + 'methods' => implode("\n\n", $this->methods), + ]); + } + + /** Emits the properties, the init method and the hydrate/extract code of one class. */ + private function emitClass(ClassPlan $plan): void + { + $class = $plan->className(); + $index = $plan->index; + + $this->declareProperties($plan); + + $init = [sprintf('if ($this->ready%1$d) { return; }', $index), sprintf('$this->ready%d = true;', $index)]; + $expected = []; + $nestedClasses = []; + + foreach ($plan->properties as $property) { + $expected[] = sprintf( + '%s => [%s, %s]', + var_export($property->name, true), + var_export($property->field, true), + $property->kind === ValueKind::Raw ? 'false' : 'true', + ); + } + + $init[] = sprintf('$metadata = $this->metadata(\\%s::class, [%s]);', $class, implode(', ', $expected)); + $init[] = sprintf('$this->r%d = $metadata->reflection;', $index); + + foreach ($plan->properties as $property) { + if ($property->defaultSlot !== null) { + $init[] = sprintf('$this->d%d = $this->promotedDefault($metadata, %s);', $property->defaultSlot, var_export($property->name, true)); + } + + if ($property->slot === null) { + continue; + } + + $init[] = sprintf('$this->n%d = $this->normalizer($metadata, %s);', $property->slot, var_export($property->name, true)); + + if ($property->nested === null) { + continue; + } + + $nested = $this->nested($property)->className(); + $nestedClasses[$property->nested] = true; + + $check = $property->kind === ValueKind::NestedObject ? 'inlineObject' : 'inlineArray'; + $init[] = sprintf('$this->ih%d = $this->%s($this->n%d, \\%s::class, Skip::Hydrate);', $property->flag, $check, $property->slot, $nested); + $init[] = sprintf('$this->ie%d = $this->%s($this->n%d, \\%s::class, Skip::Extract);', $property->flag, $check, $property->slot, $nested); + + if ($property->kind !== ValueKind::NestedArray) { + continue; + } + + $init[] = sprintf('if ($this->ih%1$d || $this->ie%1$d) { $this->n%2$d = $this->n%3$d->innerNormalizer(); }', $property->flag, $property->inner, $property->slot); + } + + [$hydrate, $hydrateHelpers] = $this->hydrateBody($plan); + [$extract, $extractHelpers] = $this->extractBody($plan); + + foreach ($hydrateHelpers as $helper) { + $init[] = $helper; + } + + $init[] = $this->wrap($plan, 'h', 'hydrate', 'array $data, array $context, object|null $object = null', 'object', $hydrate, $plan->scopedHydrate); + + foreach ($extractHelpers as $helper) { + $init[] = $helper; + } + + $init[] = $this->wrap($plan, 'e', 'extract', 'object $object, array $context', 'array', $extract, $plan->scopedExtract); + + foreach (array_keys($nestedClasses) as $nested) { + $init[] = sprintf('$this->init%d();', $nested); + } + + if (!$plan->leaf()) { + $init[] = sprintf('$this->tracked%d = $this->recursive%d();', $index, $index); + $this->properties[] = sprintf('public bool $tracked%d = true;', $index); + $this->methods[] = $this->recursiveMethod($plan); + } + + $this->inits[] = Templates::render(Templates::INIT, [ + 'index' => (string)$index, + 'body' => implode("\n", $init), + ]); + } + + private function declareProperties(ClassPlan $plan): void + { + foreach ($plan->properties as $property) { + if ($property->slot !== null) { + $this->properties[] = sprintf('public Normalizer $n%d;', $property->slot); + } + + if ($property->inner !== null) { + $this->properties[] = sprintf('public ObjectNormalizer $n%d;', $property->inner); + } + + if ($property->flag !== null) { + $this->properties[] = sprintf('public bool $ih%d = false;', $property->flag); + $this->properties[] = sprintf('public bool $ie%d = false;', $property->flag); + } + + if ($property->defaultSlot === null) { + continue; + } + + $this->properties[] = sprintf('public ReflectionParameter $d%d;', $property->defaultSlot); + } + + $index = $plan->index; + + $this->properties[] = sprintf('public bool $ready%d = false;', $index); + $this->properties[] = sprintf('public bool|null $recursive%d = null;', $index); + $this->properties[] = sprintf('public Closure $h%d;', $index); + $this->properties[] = sprintf('public Closure $e%d;', $index); + $this->properties[] = sprintf('public ReflectionClass $r%d;', $index); + } + + /** + * Circular references are only possible through normalizers which call the hydrator again. A class whose + * normalizers can not reach the class itself again does not need the (expensive) call stack tracking. + * Unknown paths and cycles are conservatively treated as recursive. + */ + private function recursiveMethod(ClassPlan $plan): string + { + $checks = []; + + foreach ($plan->properties as $property) { + if ($property->kind === ValueKind::Raw) { + continue; + } + + if ($property->nested === null) { + $checks[] = sprintf('$this->n%d instanceof HydratorAwareNormalizer', $property->slot); + + continue; + } + + $nested = $this->nested($property); + $recursive = $nested->leaf() ? 'false' : sprintf('$this->recursive%d()', $nested->index); + $checks[] = sprintf('($this->ie%d ? %s : $this->n%d instanceof HydratorAwareNormalizer)', $property->flag, $recursive, $property->slot); + } + + return Templates::render(Templates::RECURSIVE, [ + 'index' => (string)$plan->index, + 'checks' => implode("\n || ", $checks), + ]); + } + + /** + * Emits the body either as closure bound to the class scope (assigned in init) or as a plain method. + * + * @return string init code which assigns the closure + */ + private function wrap(ClassPlan $plan, string $slot, string $name, string $signature, string $return, string $body, bool $scoped): string + { + $index = $plan->index; + + if ($scoped) { + return Templates::render(Templates::CLOSURE, [ + 'slot' => $slot . $index, + 'signature' => $signature, + 'return' => $return, + 'body' => $body, + 'class' => $plan->className(), + ]); + } + + $this->methods[] = Templates::render(Templates::METHOD, [ + 'name' => $name . $index, + 'signature' => $signature, + 'return' => $return, + 'body' => $body, + ]); + + return sprintf('$this->%s%d = $this->%s%d(...);', $slot, $index, $name, $index); + } + + /** + * Generates the hydrate code, properties which need another scope are assigned in a helper closure. + * + * @return array{string, list} body and init code of the helper closures + */ + private function hydrateBody(ClassPlan $plan): array + { + $main = []; + $scoped = []; + + foreach ($plan->properties as $property) { + if ($property->hydrateScope !== null) { + $scoped[$property->hydrateScope][] = $this->assignment($plan, $property); + + continue; + } + + $main[] = $this->assignment($plan, $property); + } + + $helpers = []; + + foreach ($scoped as $scope => $code) { + $slot = $this->helperSlots++; + $this->properties[] = sprintf('public Closure $hp%d;', $slot); + $helpers[] = Templates::render(Templates::HYDRATE_HELPER, [ + 'slot' => (string)$slot, + 'scope' => $scope, + 'body' => implode("\n", $code), + ]); + $main[] = sprintf('($this->hp%d)($object, $data, $context);', $slot); + } + + $body = [sprintf('$object ??= $this->r%d->newInstanceWithoutConstructor();', $plan->index)]; + $body[] = implode("\n\n", $main); + $body[] = 'return $object;'; + + return [ + implode("\n\n", array_filter($body, static fn (string $code): bool => $code !== '')), + $helpers, + ]; + } + + private function assignment(ClassPlan $plan, PropertyPlan $property): string + { + $template = $property->hasDefault() ? Templates::ASSIGN_DEFAULT : Templates::ASSIGN; + + return Templates::render($template, [ + 'field' => var_export($property->field, true), + 'name' => $property->name, + 'property' => var_export($property->name, true), + 'class' => $plan->className(), + 'value' => $this->denormalize($plan, $property, '$value'), + 'default' => $property->default ?? sprintf('$this->d%d->getDefaultValue()', $property->defaultSlot), + ]); + } + + /** Code which denormalizes the field into the given variable. */ + private function denormalize(ClassPlan $plan, PropertyPlan $property, string $variable): string + { + $field = var_export($property->field, true); + + if ($property->kind === ValueKind::Raw) { + return sprintf('%s = $data[%s];', $variable, $field); + } + + $inner = match ($property->kind) { + ValueKind::Normalizer => sprintf('%s = $this->n%d->denormalize($data[%s], $context);', $variable, $property->slot, $field), + ValueKind::NestedObject => Templates::render(Templates::DENORMALIZE_OBJECT, [ + 'variable' => $variable, + 'field' => $field, + 'flag' => (string)$property->flag, + 'slot' => (string)$property->slot, + 'nested' => $this->nestedHydrateCall($property), + ]), + ValueKind::NestedArray => Templates::render(Templates::DENORMALIZE_ARRAY, [ + 'variable' => $variable, + 'field' => $field, + 'flag' => (string)$property->flag, + 'slot' => (string)$property->slot, + 'inner' => (string)$property->inner, + 'nested' => $this->nestedHydrateCall($property), + ]), + }; + + return Templates::render(Templates::DENORMALIZE, [ + 'inner' => $inner, + 'class' => $plan->className(), + 'property' => var_export($property->name, true), + 'slot' => (string)$property->slot, + ]); + } + + /** + * Generates the extract code, properties which need another scope are read in a helper closure. + * + * @return array{string, list} body and init code of the helper closures + */ + private function extractBody(ClassPlan $plan): array + { + $pre = []; + $scoped = []; + $fields = []; + + foreach ($plan->properties as $property) { + $field = var_export($property->field, true); + [$code, $expression] = $this->normalize($plan, $property); + + if ($property->extractScope !== null) { + $scoped[$property->extractScope][] = [$property->name, $code, $expression]; + $fields[] = sprintf('%s => $v_%s,', $field, $property->name); + + continue; + } + + if ($code !== '') { + $pre[] = $code; + } + + $fields[] = sprintf('%s => %s,', $field, $expression); + } + + $helpers = []; + + foreach ($scoped as $scope => $items) { + $slot = $this->helperSlots++; + $this->properties[] = sprintf('public Closure $ep%d;', $slot); + + $body = []; + $values = []; + $variables = []; + + foreach ($items as [$name, $code, $expression]) { + if ($code !== '') { + $body[] = $code; + } + + $values[] = $expression; + $variables[] = sprintf('$v_%s', $name); + } + + $body[] = sprintf('return [%s];', implode(', ', $values)); + + $helpers[] = Templates::render(Templates::EXTRACT_HELPER, [ + 'slot' => (string)$slot, + 'scope' => $scope, + 'body' => implode("\n", $body), + ]); + + array_unshift($pre, sprintf('[%s] = ($this->ep%d)($object, $context);', implode(', ', $variables), $slot)); + } + + $variables = [ + 'index' => (string)$plan->index, + 'class' => $plan->className(), + 'pre' => implode("\n\n", $pre), + 'fields' => implode("\n", $fields), + ]; + + if ($plan->leaf()) { + return [Templates::render(Templates::EXTRACT_PLAIN, $variables), $helpers]; + } + + return [ + Templates::render(Templates::EXTRACT_TRACKED, $variables) . "\n\n" . Templates::render(Templates::EXTRACT_PLAIN, $variables), + $helpers, + ]; + } + + /** @return array{string, string} preparing code and the expression for the normalized value */ + private function normalize(ClassPlan $plan, PropertyPlan $property): array + { + $name = $property->name; + + if ($property->kind === ValueKind::Raw) { + return ['', sprintf('$object->%s', $name)]; + } + + $variable = sprintf('$v_%s', $name); + + $inner = match ($property->kind) { + ValueKind::Normalizer => sprintf('%s = $this->n%d->normalize($object->%s, $context);', $variable, $property->slot, $name), + ValueKind::NestedObject => Templates::render(Templates::NORMALIZE_OBJECT, [ + 'variable' => $variable, + 'name' => $name, + 'flag' => (string)$property->flag, + 'slot' => (string)$property->slot, + 'nested' => $this->nestedExtractCall($property), + 'check' => $this->instanceCheck('$value', $property), + ]), + ValueKind::NestedArray => Templates::render(Templates::NORMALIZE_ARRAY, [ + 'variable' => $variable, + 'name' => $name, + 'flag' => (string)$property->flag, + 'slot' => (string)$property->slot, + 'inner' => (string)$property->inner, + 'nested' => $this->nestedExtractCall($property), + 'check' => $this->instanceCheck('$item', $property), + ]), + }; + + $code = Templates::render(Templates::NORMALIZE, [ + 'inner' => $inner, + 'class' => $plan->className(), + 'property' => var_export($name, true), + 'slot' => (string)$property->slot, + ]); + + return [$code, $variable]; + } + + /** Only exact instances are inlined, subclasses take the generic path through the hydrator. */ + private function instanceCheck(string $variable, PropertyPlan $property): string + { + $nested = $this->nested($property); + $class = $nested->className(); + + if ($nested->metadata->reflection->isFinal()) { + return sprintf('%s instanceof \\%s', $variable, $class); + } + + return sprintf('%1$s instanceof \\%2$s && %1$s::class === \\%2$s::class', $variable, $class); + } + + private function nestedHydrateCall(PropertyPlan $property): string + { + $nested = $this->nested($property); + + return $nested->scopedHydrate ? sprintf('($this->h%d)', $nested->index) : sprintf('$this->hydrate%d', $nested->index); + } + + private function nestedExtractCall(PropertyPlan $property): string + { + $nested = $this->nested($property); + + return $nested->scopedExtract ? sprintf('($this->e%d)', $nested->index) : sprintf('$this->extract%d', $nested->index); + } + + private function nested(PropertyPlan $property): ClassPlan + { + assert(is_int($property->nested)); + + return $this->classes[$property->nested]; + } +} diff --git a/src/Extension/Generated/GeneratedHydrator.php b/src/Extension/Generated/GeneratedHydrator.php new file mode 100644 index 00000000..81054838 --- /dev/null +++ b/src/Extension/Generated/GeneratedHydrator.php @@ -0,0 +1,124 @@ + false if the class has no compiled hydrator */ + private array $hydrators = []; + + /** @var array false if the class has no compiled extractor */ + private array $extractors = []; + + /** @var list|null */ + private array|null $generated = null; + + /** + * @param class-string $class + * @param array $context + * + * @return T + * + * @template T of object + */ + public function hydrate(string $class, mixed $data, array $context = []): object + { + $hydrator = $this->hydrators[$class] ?? $this->compileHydrator($class); + + if ($hydrator !== false && is_array($data)) { + $object = $hydrator($data, $context); + assert($object instanceof $class); + + return $object; + } + + return parent::hydrate($class, $data, $context); + } + + /** @param array $context */ + public function extract(object $object, array $context = []): mixed + { + $extractor = $this->extractors[$object::class] ?? $this->compileExtractor($object::class); + + if ($extractor !== false) { + return $extractor($object, $context); + } + + return parent::extract($object, $context); + } + + /** + * Decided once per class: the class must not use a class normalizer or a lazy proxy, and a generated + * middleware must handle it exclusively. Everything else is left to the stack. + * + * @param class-string $class + */ + private function compileHydrator(string $class): Closure|false + { + try { + $metadata = $this->metadata($class); + } catch (ClassNotFound) { + return $this->hydrators[$class] = false; + } + + if ($metadata->normalizer !== null || (PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->defaultLazy()))) { + return $this->hydrators[$class] = false; + } + + foreach ($this->generated() as $middleware) { + $hydrator = $middleware->compiledHydrator($metadata); + + if ($hydrator !== null) { + return $this->hydrators[$class] = $hydrator; + } + } + + return $this->hydrators[$class] = false; + } + + /** @param class-string $class */ + private function compileExtractor(string $class): Closure|false + { + $metadata = $this->metadata($class); + + if ($metadata->normalizer !== null) { + return $this->extractors[$class] = false; + } + + foreach ($this->generated() as $middleware) { + $extractor = $middleware->compiledExtractor($metadata); + + if ($extractor !== null) { + return $this->extractors[$class] = $extractor; + } + } + + return $this->extractors[$class] = false; + } + + /** @return list */ + private function generated(): array + { + return $this->generated ??= array_values(array_filter( + $this->middlewares(), + static fn ($middleware): bool => $middleware instanceof GeneratedMiddleware, + )); + } +} diff --git a/src/Extension/Generated/GeneratedMiddleware.php b/src/Extension/Generated/GeneratedMiddleware.php new file mode 100644 index 00000000..700845bb --- /dev/null +++ b/src/Extension/Generated/GeneratedMiddleware.php @@ -0,0 +1,36 @@ + $metadata + * + * @return (Closure(array, array): object)|null null if the class is not handled exclusively by this middleware + * + * @template T of object + */ + public function compiledHydrator(ClassMetadata $metadata): Closure|null; + + /** + * @param ClassMetadata $metadata + * + * @return (Closure(object, array): array)|null null if the class is not handled exclusively by this middleware + * + * @template T of object + */ + public function compiledExtractor(ClassMetadata $metadata): Closure|null; +} diff --git a/src/Extension/Generated/GeneratedMiddlewareExtension.php b/src/Extension/Generated/GeneratedMiddlewareExtension.php new file mode 100644 index 00000000..5ad9941f --- /dev/null +++ b/src/Extension/Generated/GeneratedMiddlewareExtension.php @@ -0,0 +1,102 @@ + $classes Classes for which code is generated, nested classes are discovered automatically. + * @param bool $debug Regenerate the middleware on every build. + * @param string|null $className Name of the generated class, derived from the configuration by default. + */ + public function __construct( + private readonly string $cachePath, + private readonly array $classes, + private readonly bool $debug = false, + private readonly string|null $className = null, + ) { + } + + public function configure(StackHydratorBuilder $builder): void + { + $className = $this->className ?? $this->defaultClassName(); + $fqcn = self::NAMESPACE . '\\' . $className; + + if (!class_exists($fqcn, false)) { + $this->load($fqcn, $className, $builder->getMetadataFactory()); + } + + $middleware = new $fqcn(); + assert($middleware instanceof Middleware); + + $builder->addMiddleware($middleware, Extension::PRIORITY_TRANSFORM + 1); + $builder->setHydratorFactory( + static fn (MetadataFactory $metadataFactory, array $middlewares, bool $defaultLazy): StackHydrator => new GeneratedHydrator( + $metadataFactory, + $middlewares, + $defaultLazy, + ), + ); + } + + public function defaultClassName(): string + { + return 'TransformMiddleware_' . substr( + hash('sha256', serialize([MiddlewareGenerator::VERSION, $this->classes])), + 0, + 16, + ); + } + + private function load(string $fqcn, string $className, MetadataFactory $metadataFactory): void + { + $file = sprintf('%s/%s.php', $this->cachePath, $className); + + if ($this->debug || !file_exists($file)) { + $generator = new MiddlewareGenerator($metadataFactory); + $this->write($file, $generator->dump($this->classes, $fqcn)); + } + + require $file; + } + + private function write(string $file, string $code): void + { + if (!is_dir($this->cachePath) && !@mkdir($this->cachePath, 0777, true) && !is_dir($this->cachePath)) { + throw new GeneratedMiddlewareNotWritable($file); + } + + $tmp = $file . '.' . uniqid('', true) . '.tmp'; + + if (@file_put_contents($tmp, $code) === false || !@rename($tmp, $file)) { + @unlink($tmp); + + throw new GeneratedMiddlewareNotWritable($file); + } + } +} diff --git a/src/Extension/Generated/GeneratedMiddlewareNotWritable.php b/src/Extension/Generated/GeneratedMiddlewareNotWritable.php new file mode 100644 index 00000000..8cd52fc7 --- /dev/null +++ b/src/Extension/Generated/GeneratedMiddlewareNotWritable.php @@ -0,0 +1,18 @@ + $classes + * + * @throws ClassNotGeneratable + */ + public function dump(array $classes, string $middlewareFqcn): string + { + $planner = new ClassPlanner($this->metadataFactory); + + foreach ($classes as $class) { + $planner->add($class); + } + + $parts = explode('\\', ltrim($middlewareFqcn, '\\')); + $className = array_pop($parts); + $namespace = implode('\\', $parts); + + return (new CodeEmitter($planner->plans()))->emit($namespace, $className); + } +} diff --git a/src/Extension/Generated/OutdatedGeneratedMiddleware.php b/src/Extension/Generated/OutdatedGeneratedMiddleware.php new file mode 100644 index 00000000..cb524b0d --- /dev/null +++ b/src/Extension/Generated/OutdatedGeneratedMiddleware.php @@ -0,0 +1,18 @@ +default !== null || $this->defaultSlot !== null; + } +} diff --git a/src/Extension/Generated/Templates.php b/src/Extension/Generated/Templates.php new file mode 100644 index 00000000..fd745a21 --- /dev/null +++ b/src/Extension/Generated/Templates.php @@ -0,0 +1,479 @@ + $variables */ + public static function render(string $template, array $variables): string + { + $template = trim($template, "\n"); + + foreach ($variables as $name => $value) { + $template = self::replaceIndented($template, $name, $value); + } + + return $template; + } + + private static function replaceIndented(string $template, string $name, string $value): string + { + $placeholder = '{{' . $name . '}}'; + + $result = preg_replace_callback( + '/^([ \t]*)' . preg_quote($placeholder, '/') . '$/m', + static function (array $matches) use ($value): string { + $lines = explode("\n", $value); + + return implode("\n", array_map( + static fn (string $line): string => $line === '' ? '' : $matches[1] . $line, + $lines, + )); + }, + $template, + ); + + return str_replace($placeholder, $value, $result ?? $template); + } + + public const MIDDLEWARE = <<<'PHP' + all middlewares except this one and the TransformMiddleware behind it */ + public array $others = []; + + /** @var array */ + public array $callStack = []; + + {{properties}} + + public function __construct() + { + {{constructor}} + } + + public function setHydrator(StackHydrator $hydrator): void + { + $this->hydrator = $hydrator; + $this->others = []; + + foreach ($hydrator->middlewares() as $middleware) { + if ($middleware === $this || $middleware instanceof TransformMiddleware) { + continue; + } + + $this->others[] = $middleware; + } + } + + /** Lets the GeneratedHydrator call the generated code directly, skipping the middleware stack. */ + public function compiledHydrator(ClassMetadata $metadata): Closure|null + { + if (!$this->exclusive($metadata, Skip::Hydrate)) { + return null; + } + + switch ($metadata->className) { + {{compiledHydrateCases}} + default: + return null; + } + } + + public function compiledExtractor(ClassMetadata $metadata): Closure|null + { + if (!$this->exclusive($metadata, Skip::Extract)) { + return null; + } + + switch ($metadata->className) { + {{compiledExtractCases}} + default: + return null; + } + } + + /** Classes without generated code are left to the TransformMiddleware directly. */ + public function skip(ClassMetadata $metadata): Skip + { + return match ($metadata->className) { + {{skipCases}} + => Skip::None, + default => Skip::Both, + }; + } + + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object + { + $object = null; + $populate = $context; + + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($populate[Hydrator::OBJECT_TO_POPULATE]); + } + + return match ($metadata->className) { + {{hydrateCases}} + default => $stack->next()->hydrate($metadata, $data, $context, $stack), + }; + } + + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array + { + return match ($metadata->className) { + {{extractCases}} + default => $stack->next()->extract($metadata, $object, $context, $stack), + }; + } + + {{inits}} + + {{methods}} + + private function hydrator(): StackHydrator + { + return $this->hydrator ?? throw new HydratorNotSet(self::class); + } + + /** @param array $expected property name => [field name, has normalizer] */ + private function metadata(string $class, array $expected): ClassMetadata + { + $metadata = $this->hydrator()->metadata($class); + $actual = []; + + foreach ($metadata->properties as $property) { + $actual[$property->propertyName] = [$property->fieldName, $property->normalizer !== null]; + } + + if ($actual !== $expected) { + throw new OutdatedGeneratedMiddleware($class); + } + + return $metadata; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedMiddleware($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedMiddleware($metadata->className); + } + + private function inlineObject(Normalizer $normalizer, string $class, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the nested class must be handled + * by this middleware, must not be lazy and every other middleware must skip the class in this direction. + */ + private function inlinable(string $class, Skip $direction): bool + { + try { + $metadata = $this->hydrator()->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator()->defaultLazy())) { + return false; + } + + return $this->exclusive($metadata, $direction); + } + + /** Whether every other middleware skips the class in this direction, so this middleware is the only one to run. */ + private function exclusive(ClassMetadata $metadata, Skip $direction): bool + { + foreach ($this->others as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} + +PHP; + + public const COMPILED_CASE = <<<'PHP' +case \{{class}}::class: + $this->init{{index}}(); + + return {{closure}}; +PHP; + + public const FAST_HYDRATE = <<<'PHP' +public function fastHydrate{{index}}(array $data, array $context): object +{ + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return {{call}}($data, $context, $object); + } + + return {{call}}($data, $context); +} +PHP; + + public const INIT = <<<'PHP' +private function init{{index}}(): void +{ + {{body}} +} +PHP; + + public const CLOSURE = <<<'PHP' +$this->{{slot}} = Closure::bind(function ({{signature}}): {{return}} { + {{body}} +}, $this, \{{class}}::class); +PHP; + + public const METHOD = <<<'PHP' +public function {{name}}({{signature}}): {{return}} +{ + {{body}} +} +PHP; + + public const HYDRATE_HELPER = <<<'PHP' +$this->hp{{slot}} = Closure::bind(function (object $object, array $data, array $context): void { + {{body}} +}, $this, \{{scope}}::class); +PHP; + + public const EXTRACT_TRACKED = <<<'PHP' +if ($this->tracked{{index}}) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \{{class}}::class; + + try { + {{pre}} + + return [ + {{fields}} + ]; + } finally { + unset($this->callStack[$id]); + } +} +PHP; + + public const RECURSIVE = <<<'PHP' +private function recursive{{index}}(): bool +{ + if ($this->recursive{{index}} !== null) { + return $this->recursive{{index}}; + } + + $this->recursive{{index}} = true; + + return $this->recursive{{index}} = {{checks}}; +} +PHP; + + public const EXTRACT_PLAIN = <<<'PHP' +{{pre}} + +return [ + {{fields}} +]; +PHP; + + public const EXTRACT_HELPER = <<<'PHP' +$this->ep{{slot}} = Closure::bind(function (object $object, array $context): array { + {{body}} +}, $this, \{{scope}}::class); +PHP; + + public const ASSIGN = <<<'PHP' +if (\array_key_exists({{field}}, $data)) { + {{value}} + + try { + $object->{{name}} = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\{{class}}::class, {{property}}, $e); + } +} +PHP; + + public const ASSIGN_DEFAULT = <<<'PHP' +if (\array_key_exists({{field}}, $data)) { + {{value}} + + try { + $object->{{name}} = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\{{class}}::class, {{property}}, $e); + } +} else { + $object->{{name}} = {{default}}; +} +PHP; + + public const DENORMALIZE = <<<'PHP' +try { + {{inner}} +} catch (Throwable $e) { + throw new DenormalizationFailure(\{{class}}::class, {{property}}, $this->n{{slot}}::class, $e); +} +PHP; + + public const DENORMALIZE_OBJECT = <<<'PHP' +{{variable}} = $data[{{field}}]; + +if ($this->ih{{flag}} && \is_array({{variable}})) { + {{variable}} = {{nested}}({{variable}}, $context); +} else { + {{variable}} = $this->n{{slot}}->denormalize({{variable}}, $context); +} +PHP; + + public const DENORMALIZE_ARRAY = <<<'PHP' +{{variable}} = $data[{{field}}]; + +if ($this->ih{{flag}} && \is_array({{variable}})) { + $items = []; + + foreach ({{variable}} as $key => $item) { + $items[$key] = \is_array($item) ? {{nested}}($item, $context) : $this->n{{inner}}->denormalize($item, $context); + } + + {{variable}} = $items; +} else { + {{variable}} = $this->n{{slot}}->denormalize({{variable}}, $context); +} +PHP; + + public const NORMALIZE = <<<'PHP' +try { + {{inner}} +} catch (CircularReference $e) { + throw $e; +} catch (Throwable $e) { + throw new NormalizationFailure(\{{class}}::class, {{property}}, $this->n{{slot}}::class, $e); +} +PHP; + + public const NORMALIZE_OBJECT = <<<'PHP' +$value = $object->{{name}}; +{{variable}} = $this->ie{{flag}} && {{check}} ? {{nested}}($value, $context) : $this->n{{slot}}->normalize($value, $context); +PHP; + + public const NORMALIZE_ARRAY = <<<'PHP' +$value = $object->{{name}}; + +if ($this->ie{{flag}} && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = {{check}} ? {{nested}}($item, $context) : $this->n{{inner}}->normalize($item, $context); + } + + {{variable}} = $items; +} else { + {{variable}} = $this->n{{slot}}->normalize($value, $context); +} +PHP; +} diff --git a/src/Extension/Generated/ValueKind.php b/src/Extension/Generated/ValueKind.php new file mode 100644 index 00000000..e7a8072b --- /dev/null +++ b/src/Extension/Generated/ValueKind.php @@ -0,0 +1,25 @@ + $middlewares */ + /** + * @param non-empty-list $middlewares + * @param int $index Position of the middleware returned by the next call to next(). + */ public function __construct( private readonly array $middlewares, + private int $index = 0, ) { } diff --git a/src/StackHydrator.php b/src/StackHydrator.php index 568b0cc5..36d28a38 100644 --- a/src/StackHydrator.php +++ b/src/StackHydrator.php @@ -9,6 +9,7 @@ use Patchlevel\Hydrator\Metadata\ClassNotFound; use Patchlevel\Hydrator\Metadata\MetadataFactory; use Patchlevel\Hydrator\Middleware\AllMiddlewaresSkipped; +use Patchlevel\Hydrator\Middleware\HydratorAwareMiddleware; use Patchlevel\Hydrator\Middleware\Middleware; use Patchlevel\Hydrator\Middleware\Skip; use Patchlevel\Hydrator\Middleware\SkippableMiddleware; @@ -22,7 +23,8 @@ use const PHP_VERSION_ID; -final class StackHydrator implements Hydrator +/** @final this is only not final anymore because of bc reasons for the generated hydrator. DONT extend this class! */ +class StackHydrator implements Hydrator { /** @var array */ private array $classMetadata = []; @@ -50,14 +52,29 @@ public function __construct( foreach ($middlewares as $middleware) { if ($middleware instanceof SkippableMiddleware) { $hasSkippableMiddlewares = true; + } - break; + if (!$middleware instanceof HydratorAwareMiddleware) { + continue; } + + $middleware->setHydrator($this); } $this->hasSkippableMiddlewares = $hasSkippableMiddlewares; } + /** @return list */ + public function middlewares(): array + { + return $this->middlewares; + } + + public function defaultLazy(): bool + { + return $this->defaultLazy; + } + /** * @param class-string $class * @param array $context @@ -69,7 +86,8 @@ public function __construct( public function hydrate(string $class, mixed $data, array $context = []): object { try { - $metadata = $this->metadata($class); + /** @var ClassMetadata $metadata */ + $metadata = $this->classMetadata[$class] ?? $this->metadata($class); } catch (ClassNotFound $e) { throw new ClassNotSupported($class, $e); } @@ -89,24 +107,24 @@ public function hydrate(string $class, mixed $data, array $context = []): object } if (PHP_VERSION_ID < 80400) { - $stack = new Stack($this->middlewaresFor($metadata, Skip::Hydrate)); + $middlewares = $this->middlewaresFor($metadata, Skip::Hydrate); - return $stack->next()->hydrate($metadata, $data, $context, $stack); + return $middlewares[0]->hydrate($metadata, $data, $context, new Stack($middlewares, 1)); } $lazy = $metadata->lazy ?? $this->defaultLazy; if (!$lazy) { - $stack = new Stack($this->middlewaresFor($metadata, Skip::Hydrate)); + $middlewares = $this->middlewaresFor($metadata, Skip::Hydrate); - return $stack->next()->hydrate($metadata, $data, $context, $stack); + return $middlewares[0]->hydrate($metadata, $data, $context, new Stack($middlewares, 1)); } return (new ReflectionClass($class))->newLazyProxy( function () use ($metadata, $data, $context): object { - $stack = new Stack($this->middlewaresFor($metadata, Skip::Hydrate)); + $middlewares = $this->middlewaresFor($metadata, Skip::Hydrate); - return $stack->next()->hydrate($metadata, $data, $context, $stack); + return $middlewares[0]->hydrate($metadata, $data, $context, new Stack($middlewares, 1)); }, ); } @@ -118,15 +136,15 @@ function () use ($metadata, $data, $context): object { */ public function extract(object $object, array $context = []): mixed { - $metadata = $this->metadata($object::class); + $metadata = $this->classMetadata[$object::class] ?? $this->metadata($object::class); if ($metadata->normalizer) { return $metadata->normalizer->normalize($object, $context); } - $stack = new Stack($this->middlewaresFor($metadata, Skip::Extract)); + $middlewares = $this->middlewaresFor($metadata, Skip::Extract); - return $stack->next()->extract($metadata, $object, $context, $stack); + return $middlewares[0]->extract($metadata, $object, $context, new Stack($middlewares, 1)); } /** diff --git a/src/StackHydratorBuilder.php b/src/StackHydratorBuilder.php index 92c251cd..7f50f290 100644 --- a/src/StackHydratorBuilder.php +++ b/src/StackHydratorBuilder.php @@ -4,11 +4,13 @@ namespace Patchlevel\Hydrator; +use Closure; use Patchlevel\Hydrator\Guesser\ChainGuesser; use Patchlevel\Hydrator\Guesser\Guesser; use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; use Patchlevel\Hydrator\Metadata\EnrichingMetadataFactory; use Patchlevel\Hydrator\Metadata\MetadataEnricher; +use Patchlevel\Hydrator\Metadata\MetadataFactory; use Patchlevel\Hydrator\Metadata\Psr16MetadataFactory; use Patchlevel\Hydrator\Metadata\Psr6MetadataFactory; use Patchlevel\Hydrator\Middleware\Middleware; @@ -33,6 +35,9 @@ final class StackHydratorBuilder private CacheItemPoolInterface|CacheInterface|null $cache = null; + /** @var (Closure(MetadataFactory, list, bool): StackHydrator)|null */ + private Closure|null $hydratorFactory = null; + /** @return $this */ public function addMiddleware(Middleware $middleware, int $priority = Extension::PRIORITY_BEFORE_TRANSFORM): static { @@ -78,14 +83,28 @@ public function setCache(CacheItemPoolInterface|CacheInterface|null $cache): sta return $this; } + /** + * Replaces how the hydrator is created. This exists for the GeneratedHydrator of the generated middleware + * extension, the StackHydrator itself is not meant to be extended (see its @final annotation). + * + * @param Closure(MetadataFactory, list, bool): StackHydrator $factory receives the metadata factory, the middlewares and the default lazy flag + * + * @return $this + */ + public function setHydratorFactory(Closure $factory): static + { + if ($this->hydratorFactory !== null) { + throw new HydratorFactoryAlreadySet(); + } + + $this->hydratorFactory = $factory; + + return $this; + } + public function build(): StackHydrator { - $metadataFactory = new EnrichingMetadataFactory( - new AttributeMetadataFactory( - guesser: new ChainGuesser($this->guessers()), - ), - $this->metadataEnrichers(), - ); + $metadataFactory = $this->getMetadataFactory(); if ($this->cache instanceof CacheItemPoolInterface) { $metadataFactory = new Psr6MetadataFactory($metadataFactory, $this->cache); @@ -95,6 +114,10 @@ public function build(): StackHydrator $metadataFactory = new Psr16MetadataFactory($metadataFactory, $this->cache); } + if ($this->hydratorFactory !== null) { + return ($this->hydratorFactory)($metadataFactory, $this->middlewares(), $this->defaultLazy); + } + return new StackHydrator( $metadataFactory, $this->middlewares(), @@ -130,4 +153,14 @@ public function metadataEnrichers(): array return array_merge(...$this->metadataEnrichers); } + + public function getMetadataFactory(): MetadataFactory + { + return new EnrichingMetadataFactory( + new AttributeMetadataFactory( + guesser: new ChainGuesser($this->guessers()), + ), + $this->metadataEnrichers(), + ); + } } diff --git a/tests/Architecture/FinalClassesTest.php b/tests/Architecture/FinalClassesTest.php index 1da5cd61..412125b8 100644 --- a/tests/Architecture/FinalClassesTest.php +++ b/tests/Architecture/FinalClassesTest.php @@ -4,6 +4,7 @@ namespace Patchlevel\Hydrator\Tests\Architecture; +use Patchlevel\Hydrator\StackHydrator; use PHPat\Selector\Selector; use PHPat\Test\Builder\Rule; use PHPat\Test\PHPat; @@ -18,6 +19,7 @@ public function testFinalClasses(): Rule Selector::inNamespace('Patchlevel\Hydrator'), Selector::NOT(Selector::isAbstract()), Selector::NOT(Selector::isInterface()), + Selector::NOT(Selector::classname(StackHydrator::class)), ), ) ->shouldBeFinal(); diff --git a/tests/Benchmark/HydratorBench.php b/tests/Benchmark/GeneratedHydratorBench.php similarity index 74% rename from tests/Benchmark/HydratorBench.php rename to tests/Benchmark/GeneratedHydratorBench.php index 0af5f89c..2d8fd3f2 100644 --- a/tests/Benchmark/HydratorBench.php +++ b/tests/Benchmark/GeneratedHydratorBench.php @@ -5,6 +5,7 @@ namespace Patchlevel\Hydrator\Tests\Benchmark; use Patchlevel\Hydrator\CoreExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; @@ -13,7 +14,7 @@ use PhpBench\Attributes as Bench; #[Bench\BeforeMethods('setUp')] -final class HydratorBench +final class GeneratedHydratorBench { private Hydrator $hydrator; @@ -21,6 +22,14 @@ public function __construct() { $this->hydrator = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedMiddlewareExtension( + __DIR__ . '/../../var/cache', + [ + ProfileCreated::class, + Skill::class, + ], + debug: true, + )) ->build(); } @@ -41,7 +50,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate(ProfileCreated::class, [ @@ -54,7 +63,7 @@ public function benchHydrate1Object(): void ]); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -100,36 +109,4 @@ public function benchExtract1000Objects(): void $this->hydrator->extract($object); } } - - #[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 benchExtract1000000Objects(): void - { - $object = new ProfileCreated( - ProfileId::fromString('1'), - 'foo', - [ - new Skill('php'), - new Skill('symfony'), - ], - ); - - for ($i = 0; $i < 1_000_000; $i++) { - $this->hydrator->extract($object); - } - } } diff --git a/tests/Benchmark/HydratorWithCryptographyBench.php b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php similarity index 75% rename from tests/Benchmark/HydratorWithCryptographyBench.php rename to tests/Benchmark/GeneratedHydratorWithCryptographyBench.php index 1b93a77a..efe76740 100644 --- a/tests/Benchmark/HydratorWithCryptographyBench.php +++ b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php @@ -8,6 +8,7 @@ use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer; use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension; use Patchlevel\Hydrator\Extension\Cryptography\Store\InMemoryCipherKeyStore; +use Patchlevel\Hydrator\Extension\Generated\GeneratedMiddlewareExtension; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; @@ -16,7 +17,7 @@ use PhpBench\Attributes as Bench; #[Bench\BeforeMethods('setUp')] -final class HydratorWithCryptographyBench +final class GeneratedHydratorWithCryptographyBench { private InMemoryCipherKeyStore $store; @@ -28,6 +29,7 @@ public function __construct() $this->hydrator = (new StackHydratorBuilder()) ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedMiddlewareExtension(__DIR__ . '/../../var/cache', [ProfileCreated::class, Skill::class], debug: true)) ->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($this->store))) ->build(); } @@ -51,7 +53,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate( @@ -67,7 +69,7 @@ public function benchHydrate1Object(): void ); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -116,39 +118,4 @@ public function benchExtract1000Objects(): void $this->hydrator->extract($object); } } - - #[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 benchExtract1000000Objects(): void - { - $object = new ProfileCreated( - ProfileId::fromString('1'), - 'foo', - [ - new Skill('php'), - new Skill('symfony'), - ], - ); - - for ($i = 0; $i < 1_000_000; $i++) { - $this->hydrator->extract($object); - } - } } diff --git a/tests/Benchmark/StackHydratorBench.php b/tests/Benchmark/StackHydratorBench.php index 4b1f7cf8..7287694b 100644 --- a/tests/Benchmark/StackHydratorBench.php +++ b/tests/Benchmark/StackHydratorBench.php @@ -41,7 +41,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate(ProfileCreated::class, [ @@ -54,7 +54,7 @@ public function benchHydrate1Object(): void ]); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -100,36 +100,4 @@ public function benchExtract1000Objects(): void $this->hydrator->extract($object); } } - - #[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 benchExtract1000000Objects(): void - { - $object = new ProfileCreated( - ProfileId::fromString('1'), - 'foo', - [ - new Skill('php'), - new Skill('symfony'), - ], - ); - - for ($i = 0; $i < 1_000_000; $i++) { - $this->hydrator->extract($object); - } - } } diff --git a/tests/Benchmark/StackHydratorWithCryptographyBench.php b/tests/Benchmark/StackHydratorWithCryptographyBench.php index 5d5f54b9..44f4f37a 100644 --- a/tests/Benchmark/StackHydratorWithCryptographyBench.php +++ b/tests/Benchmark/StackHydratorWithCryptographyBench.php @@ -51,7 +51,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate( @@ -67,7 +67,7 @@ public function benchHydrate1Object(): void ); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -116,39 +116,4 @@ public function benchExtract1000Objects(): void $this->hydrator->extract($object); } } - - #[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 benchExtract1000000Objects(): void - { - $object = new ProfileCreated( - ProfileId::fromString('1'), - 'foo', - [ - new Skill('php'), - new Skill('symfony'), - ], - ); - - for ($i = 0; $i < 1_000_000; $i++) { - $this->hydrator->extract($object); - } - } } diff --git a/tests/Benchmark/HydratorWithLazyBench.php b/tests/Benchmark/StackHydratorWithLazyBench.php similarity index 72% rename from tests/Benchmark/HydratorWithLazyBench.php rename to tests/Benchmark/StackHydratorWithLazyBench.php index a85224b7..c8504280 100644 --- a/tests/Benchmark/HydratorWithLazyBench.php +++ b/tests/Benchmark/StackHydratorWithLazyBench.php @@ -11,7 +11,7 @@ use PhpBench\Attributes as Bench; #[Bench\BeforeMethods('setUp')] -final class HydratorWithLazyBench +final class StackHydratorWithLazyBench { private Hydrator $hydrator; @@ -40,7 +40,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate(ProfileCreated::class, [ @@ -53,7 +53,7 @@ public function benchHydrate1Object(): void ]); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1ObjectTriggerInit(): void { $object = $this->hydrator->hydrate(ProfileCreated::class, [ @@ -99,36 +99,4 @@ public function benchHydrate1000ObjectsTriggerInit(): void $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/Extension/Generated/GeneratedMiddlewareExtensionTest.php b/tests/Unit/Extension/Generated/GeneratedMiddlewareExtensionTest.php new file mode 100644 index 00000000..a991c0c0 --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedMiddlewareExtensionTest.php @@ -0,0 +1,85 @@ +defaultClassName(), (new GeneratedMiddlewareExtension($path, [ProfileCreated::class]))->defaultClassName()); + self::assertNotSame($a->defaultClassName(), $b->defaultClassName()); + } + + public function testGeneratedFileIsCachedAndReused(): void + { + $path = GeneratedStackHydratorTest::cachePath() . '/' . uniqid('cache', true); + $className = 'Cached_' . uniqid(); + $file = sprintf('%s/%s.php', $path, $className); + + $extension = new GeneratedMiddlewareExtension($path, [ProfileCreated::class], className: $className); + $hydrator = (new StackHydratorBuilder())->useExtension(new CoreExtension())->useExtension($extension)->build(); + + self::assertFileExists($file); + $mtime = filemtime($file); + touch($file, $mtime - 100); + + // the class is already loaded, the file is neither regenerated nor required again + (new StackHydratorBuilder())->useExtension(new CoreExtension())->useExtension($extension)->build(); + self::assertSame($mtime - 100, filemtime($file)); + + $event = $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + self::assertEquals(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); + } + + public function testMiddlewareStack(): void + { + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension(new GeneratedMiddlewareExtension(GeneratedStackHydratorTest::cachePath(), [ProfileCreated::class])) + ->build(); + + $middlewares = $hydrator->middlewares(); + + self::assertCount(2, $middlewares); + self::assertStringStartsWith(GeneratedMiddlewareExtension::NAMESPACE . '\\', $middlewares[0]::class); + self::assertInstanceOf(TransformMiddleware::class, $middlewares[1]); + } + + public function testNotWritable(): void + { + $this->expectException(GeneratedMiddlewareNotWritable::class); + + $file = sys_get_temp_dir() . '/' . uniqid('not-a-directory'); + touch($file); + + (new StackHydratorBuilder()) + ->useExtension(new GeneratedMiddlewareExtension($file, [ProfileCreated::class], className: 'NotWritable_' . uniqid())) + ->build(); + } +} diff --git a/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php new file mode 100644 index 00000000..74a3b0f5 --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php @@ -0,0 +1,995 @@ +hydrator = $this->builder()->build(); + } + + /** @param list $classes */ + private function builder(array $classes = self::CLASSES): StackHydratorBuilder + { + return (new StackHydratorBuilder())->useExtension(new CoreExtension())->useExtension($this->extension($classes)); + } + + /** @param list $classes */ + private function extension(array $classes = self::CLASSES): GeneratedMiddlewareExtension + { + return new GeneratedMiddlewareExtension( + self::cachePath(), + $classes, + debug: true, + ); + } + + public static function cachePath(): string + { + return sys_get_temp_dir() . '/patchlevel-hydrator-tests'; + } + + public function testExtract(): void + { + $event = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + self::assertEquals( + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + $this->hydrator->extract($event), + ); + } + + public function testExtractWithInheritance(): void + { + $event = new ParentDto( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + self::assertEquals( + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + $this->hydrator->extract($event), + ); + } + + public function testExtractWithHydratorAwareNormalizer(): void + { + $event = new ProfileCreatedWrapper( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + ); + + self::assertEquals( + ['event' => ['profileId' => '1', 'email' => 'info@patchlevel.de']], + $this->hydrator->extract($event), + ); + } + + public function testExtractPassesContextToNormalizer(): void + { + $dto = new ContextAwareDto('value'); + + $data = $this->hydrator->extract($dto, ['prefix' => 'ctx-']); + + self::assertSame(['value' => 'ctx-value'], $data); + } + + public function testExtractCircularReference(): void + { + $this->expectException(CircularReference::class); + $this->expectExceptionMessage('Circular reference detected: Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto -> Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto -> Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto -> Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto'); + + $dto1 = new Circle1Dto(); + $dto2 = new Circle2Dto(); + $dto3 = new Circle3Dto(); + + $dto1->to = $dto2; + $dto2->to = $dto3; + $dto3->to = $dto1; + + $this->hydrator->extract($dto1); + } + + public function testExtractWithInferNormalizer(): void + { + $result = $this->hydrator->extract( + new InferNormalizerWithNullableDto( + null, + null, + profileId: ProfileId::fromString('1'), + ), + ); + + self::assertEquals( + [ + 'status' => null, + 'dateTimeImmutable' => null, + 'dateTime' => null, + 'dateTimeZone' => null, + 'profileId' => '1', + ], + $result, + ); + } + + public function testExtractWithContext(): void + { + $object = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $expect = [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + 'array' => ['foo'], + ]; + + $middleware = $this->createMock(Middleware::class); + $middleware + ->expects($this->once()) + ->method('extract') + ->with( + $this->isInstanceOf(ClassMetadata::class), + $object, + ['context' => '123'], + $this->isInstanceOf(Stack::class), + )->willReturn($expect); + + $hydrator = $this->builder() + ->addMiddleware($middleware) + ->build(); + + $data = $hydrator->extract($object, ['context' => '123']); + + self::assertEquals($expect, $data); + } + + #[RequiresPhp('>=8.5')] + public function testExtractWithInlineNormalizer(): void + { + $event = new ProfileCreatedWithInlineNormalizer( + ProfileId::fromString('1'), + ValueObject::fromString('foo'), + ); + + self::assertEquals( + ['profileId' => '1', 'valueObject' => 'foo'], + $this->hydrator->extract($event), + ); + } + + public function testExtractWithClassNormalizer(): void + { + $data = $this->hydrator->extract( + ProfileId::fromString('id'), + ); + + self::assertEquals('id', $data); + } + + public function testExtractPrivateProperties(): void + { + $dto = new PrivatePropertiesDto(ProfileId::fromString('1'), 'foo', 12); + + self::assertSame( + ['profileId' => '1', 'name' => 'foo', 'age' => 12], + $this->hydrator->extract($dto), + ); + } + + public function testExtractPrivatePropertiesOfParent(): void + { + $dto = new PrivateChildDto(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de'), 'note', 'foo'); + + self::assertSame( + ['profileId' => '1', 'name' => 'foo', 'email' => 'info@patchlevel.de', 'note' => 'note'], + $this->hydrator->extract($dto), + ); + self::assertSame((new StackHydrator())->extract($dto), $this->hydrator->extract($dto)); + } + + public function testHydrate(): void + { + $expected = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $event = $this->hydrator->hydrate( + ProfileCreated::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + self::assertEquals($expected, $event); + } + + public function testHydratePassesContextToNormalizer(): void + { + $event = $this->hydrator->hydrate( + ContextAwareDto::class, + ['value' => 'value'], + ['suffix' => '-ctx'], + ); + + self::assertSame('value-ctx', $event->value); + } + + public function testHydrateUnknownClass(): void + { + $this->expectException(ClassNotSupported::class); + $this->expectExceptionCode(0); + + $this->hydrator->hydrate( + // @phpstan-ignore argument.type + 'Unknown', + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + } + + public function testHydrateWithArrayDataRequired(): void + { + $this->expectException(ArrayDataRequired::class); + + $this->hydrator->hydrate( + ProfileCreated::class, + 'foo', + ); + } + + public function testHydrateWithDefaults(): void + { + $object = $this->hydrator->hydrate( + DefaultDto::class, + ['name' => 'test'], + ); + + self::assertEquals('test', $object->name); + self::assertEquals(new Email('info@patchlevel.de'), $object->email); + self::assertEquals(true, $object->admin); + } + + public function testHydrateWithInheritance(): void + { + $expected = new ParentDto( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $event = $this->hydrator->hydrate( + ParentDto::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithHydratorAwareNormalizer(): void + { + $expected = new ProfileCreatedWrapper( + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + ); + + $event = $this->hydrator->hydrate( + ProfileCreatedWrapper::class, + [ + 'event' => ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithObjectToPopulate(): void + { + $id = ProfileId::fromString('1'); + $dto = new ProfileCreated($id, Email::fromString('foo@patchlevel.de')); + + $processedDto = $this->hydrator->hydrate( + $dto::class, + ['email' => 'other@patchlevel.de'], + [StackHydrator::OBJECT_TO_POPULATE => $dto], + ); + + self::assertSame($dto, $processedDto); + self::assertSame($id, $processedDto->profileId); + self::assertEquals(Email::fromString('other@patchlevel.de'), $processedDto->email); + } + + public function testHydrateWithTypeMismatch(): void + { + $this->expectException(TypeMismatch::class); + $this->expectExceptionMessage('The value could not be set because the expected type of the property "profileId" in class "Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated" does not match.'); + + $this->hydrator->hydrate( + ProfileCreated::class, + ['profileId' => null, 'email' => null], + ); + } + + public function testHydrateWithContext(): void + { + $expect = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $data = [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + 'array' => ['foo'], + ]; + + $middleware = $this->createMock(Middleware::class); + $middleware + ->expects($this->once()) + ->method('hydrate') + ->with( + $this->isInstanceOf(ClassMetadata::class), + $data, + ['context' => '123'], + $this->isInstanceOf(Stack::class), + )->willReturn($expect); + + $hydrator = $this->builder() + ->addMiddleware($middleware) + ->build(); + + $object = $hydrator->hydrate(InferNormalizerDto::class, $data, ['context' => '123']); + + self::assertEquals($expect, $object); + } + + #[RequiresPhp('>=8.5')] + public function testHydrateWithInlineNormalizer(): void + { + $expected = new ProfileCreatedWithInlineNormalizer( + ProfileId::fromString('1'), + ValueObject::fromString('foo'), + ); + + $event = $this->hydrator->hydrate( + ProfileCreatedWithInlineNormalizer::class, + ['profileId' => '1', 'valueObject' => 'foo'], + ); + + self::assertEquals($expected, $event); + } + + public function testDenormalizationFailure(): void + { + $this->expectException(DenormalizationFailure::class); + + $this->hydrator->hydrate( + ProfileCreated::class, + ['profileId' => 123, 'email' => 123], + ); + } + + public function testNormalizationFailure(): void + { + $this->expectException(NormalizationFailure::class); + + $this->hydrator->extract( + new WrongNormalizer(true), + ); + } + + public function testHydrateWithNormalizerInBaseClass(): void + { + $expected = new NormalizerInBaseClassDefinedDto( + StatusWithNormalizer::Draft, + new ProfileCreatedWithNormalizer( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + [StatusWithNormalizer::Draft], + [StatusWithNormalizer::Draft], + [StatusWithNormalizer::Draft], + [ + 'foo' => new Skill('php'), + 'bar' => new Skill('symfony'), + ], + [ + 'foo' => 'php', + 'bar' => 15, + 'baz' => ['test'], + ], + ); + + $event = $this->hydrator->hydrate( + NormalizerInBaseClassDefinedDto::class, + [ + 'status' => 'draft', + 'profileCreated' => ['profileId' => '1', 'email' => 'info@patchlevel.de'], + 'defaultArray' => ['draft'], + 'listArray' => ['draft'], + 'iterableArray' => ['draft'], + 'skillsHashMap' => ['foo' => ['name' => 'php'], 'bar' => ['name' => 'symfony']], + 'jsonArray' => ['foo' => 'php', 'bar' => 15, 'baz' => ['test']], + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithInferNormalizer(): void + { + $expected = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $event = $this->hydrator->hydrate( + InferNormalizerDto::class, + [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + 'array' => ['foo'], + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithInferNormalizerAndNullableProperties(): void + { + $expected = new InferNormalizerWithNullableDto( + null, + null, + null, + null, + ); + + $event = $this->hydrator->hydrate( + InferNormalizerWithNullableDto::class, + [ + 'status' => null, + 'dateTimeImmutable' => null, + 'dateTime' => null, + 'dateTimeZone' => null, + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithInferNormalizerWitIterables(): void + { + $expected = new InferNormalizerWithIterablesDto( + [Status::Draft], + [Status::Draft], + [Status::Draft], + [ + 'foo' => Status::Draft, + 'bar' => Status::Draft, + ], + [ + 'foo' => [Status::Draft], + 'bar' => [Status::Draft], + ], + [ + 'foo' => 'php', + 'bar' => 15, + 'baz' => ['test'], + ], + [ + 'status' => Status::Draft, + 'other' => [Status::Draft], + ], + ); + + $event = $this->hydrator->hydrate( + InferNormalizerWithIterablesDto::class, + [ + 'defaultArray' => ['draft'], + 'listArray' => ['draft'], + 'iterableArray' => ['draft'], + 'hashMap' => ['foo' => 'draft', 'bar' => 'draft'], + 'nested' => ['foo' => ['draft'], 'bar' => ['draft']], + 'jsonArray' => ['foo' => 'php', 'bar' => 15, 'baz' => ['test']], + 'shapeArray' => ['status' => 'draft', 'other' => ['draft']], + ], + ); + + self::assertEquals($expected, $event); + } + + public function testHydrateWithClassNormalizer(): void + { + $object = $this->hydrator->hydrate( + ProfileId::class, + 'id', + ); + + self::assertEquals(ProfileId::fromString('id'), $object); + } + + public function testHydratePrivateProperties(): void + { + $dto = $this->hydrator->hydrate( + PrivatePropertiesDto::class, + ['profileId' => '1', 'name' => 'foo', 'age' => 12], + ); + + self::assertEquals(ProfileId::fromString('1'), $dto->profileId()); + self::assertSame('foo', $dto->name()); + self::assertSame(12, $dto->age()); + } + + public function testHydratePrivatePropertiesOfParent(): void + { + $dto = $this->hydrator->hydrate( + PrivateChildDto::class, + ['email' => 'info@patchlevel.de', 'note' => 'note', 'profileId' => '1', 'name' => 'foo'], + ); + + self::assertEquals(ProfileId::fromString('1'), $dto->profileId); + self::assertEquals(Email::fromString('info@patchlevel.de'), $dto->email()); + self::assertSame('note', $dto->note()); + self::assertSame('foo', $dto->name()); + } + + #[RequiresPhp('>=8.4')] + public function testHydrateAsymmetricVisibility(): void + { + $data = ['name' => 'foo', 'age' => 12, 'note' => 'note']; + $hydrator = $this->builder([AsymmetricVisibilityDto::class])->build(); + + $dto = $hydrator->hydrate(AsymmetricVisibilityDto::class, $data); + + self::assertEquals(new AsymmetricVisibilityDto('foo', 12, 'note'), $dto); + self::assertSame($data, $hydrator->extract($dto)); + } + + public function testHydrateCoercesScalarsLikeReflection(): void + { + $dto = $this->hydrator->hydrate( + PrivatePropertiesDto::class, + ['profileId' => '1', 'name' => 'foo', 'age' => '12'], + ); + + self::assertSame(12, $dto->age()); + } + + public function testHydrateUnknownClassFallsBackToTransformMiddleware(): void + { + $hydrator = $this->builder([ProfileCreated::class])->build(); + + $event = $hydrator->hydrate( + ParentDto::class, + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + + self::assertEquals(new ParentDto(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); + self::assertEquals(['profileId' => '1', 'email' => 'info@patchlevel.de'], $hydrator->extract($event)); + } + + #[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'), + ); + + $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 testDecrypt(): void + { + $object = new SensitiveDataProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $encryptedPayload = ['id' => '1', 'email' => 'encrypted']; + + $cryptographer = $this->createMock(Cryptographer::class); + $cryptographer + ->expects($this->once()) + ->method('supports') + ->with('encrypted') + ->willReturn(true); + + $cryptographer + ->expects($this->once()) + ->method('decrypt') + ->with('1', 'encrypted') + ->willReturn('info@patchlevel.de'); + + $hydrator = $this->builder() + ->useExtension(new CryptographyExtension($cryptographer)) + ->build(); + + $return = $hydrator->hydrate(SensitiveDataProfileCreated::class, $encryptedPayload); + + self::assertEquals($object, $return); + } + + public function testEncrypt(): void + { + $object = new SensitiveDataProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $encryptedPayload = [ + 'id' => '1', + 'email' => [ + '__enc' => 'v1', + 'data' => 'encrypted', + 'method' => 'foo', + 'iv' => 'bar', + ], + ]; + + $cryptographer = $this->createMock(Cryptographer::class); + + $cryptographer + ->expects($this->never()) + ->method('supports'); + + $cryptographer + ->expects($this->once()) + ->method('encrypt') + ->with('1', 'info@patchlevel.de') + ->willReturn([ + '__enc' => 'v1', + 'data' => 'encrypted', + 'method' => 'foo', + 'iv' => 'bar', + ]); + + $hydrator = $this->builder() + ->useExtension(new CryptographyExtension($cryptographer)) + ->build(); + + $return = $hydrator->extract($object); + + self::assertSame($encryptedPayload, $return); + } + + /** + * Reads the inlining decisions of the generated middleware. + * + * @return array flag name => inlined + */ + private static function inlined(StackHydrator $hydrator): array + { + foreach ($hydrator->middlewares() as $middleware) { + if (str_starts_with($middleware::class, GeneratedMiddlewareExtension::NAMESPACE . '\\')) { + /** @var array $flags */ + $flags = array_filter( + get_object_vars($middleware), + static fn (string $name): bool => preg_match('/^i[he]\d+$/', $name) === 1, + ARRAY_FILTER_USE_KEY, + ); + + return $flags; + } + } + + self::fail('generated middleware not found'); + } + + public function testNestedObjectsAreInlinedWithoutOtherMiddlewares(): void + { + $hydrator = $this->builder([NestedLifecycleDto::class])->build(); + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame(['ih0' => true, 'ie0' => true, 'ih1' => true, 'ie1' => true], self::inlined($hydrator)); + } + + public function testNestedObjectsAreInlinedWhenOtherMiddlewaresSkipThem(): void + { + $counter = new CountingMiddleware([LifecycleFixture::class => Skip::Both]); + + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension($this->extension([NestedLifecycleDto::class])) + ->addMiddleware($counter) + ->build(); + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + + self::assertSame(['ih0' => true, 'ie0' => true, 'ih1' => true, 'ie1' => true], self::inlined($hydrator)); + self::assertSame([NestedLifecycleDto::class => 1], $counter->hydrated); + } + + public function testInliningIsDecidedPerDirection(): void + { + // an upcaster which can not be introspected keeps the upcast middleware in the hydrate stack + $upcaster = new class implements Upcaster { + /** + * @param array $data + * @param array $context + * + * @return array + */ + public function upcast(ClassMetadata $metadata, array $data, array $context): array + { + return $data; + } + }; + + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension($this->extension([NestedLifecycleDto::class])) + ->useExtension(new UpcastExtension([$upcaster])) + ->build(); + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame(['ih0' => false, 'ie0' => true, 'ih1' => false, 'ie1' => true], self::inlined($hydrator)); + + // callback upcasters for other classes do not prevent inlining + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension($this->extension([NestedLifecycleDto::class])) + ->useExtension(new UpcastExtension([CallbackUpcaster::forClass(ProfileCreated::class, static fn (array $data): array => $data)])) + ->build(); + + self::assertSame($data, $hydrator->extract($hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame(['ih0' => true, 'ie0' => true, 'ih1' => true, 'ie1' => true], self::inlined($hydrator)); + } + + public function testCompiledClosures(): void + { + $hydrator = $this->builder([ProfileCreated::class])->build(); + $middleware = $hydrator->middlewares()[0]; + + self::assertInstanceOf(GeneratedHydrator::class, $hydrator); + self::assertInstanceOf(GeneratedMiddleware::class, $middleware); + self::assertNotNull($middleware->compiledHydrator($hydrator->metadata(ProfileCreated::class))); + self::assertNotNull($middleware->compiledExtractor($hydrator->metadata(ProfileCreated::class))); + self::assertNull($middleware->compiledHydrator($hydrator->metadata(ParentDto::class))); + self::assertNull($middleware->compiledExtractor($hydrator->metadata(ParentDto::class))); + + // with a middleware which does not skip the class, the stack has to run + $hydrator = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension($this->extension([ProfileCreated::class])) + ->addMiddleware(new CountingMiddleware()) + ->build(); + $middleware = $hydrator->middlewares()[1]; + + self::assertInstanceOf(GeneratedMiddleware::class, $middleware); + self::assertNull($middleware->compiledHydrator($hydrator->metadata(ProfileCreated::class))); + self::assertNull($middleware->compiledExtractor($hydrator->metadata(ProfileCreated::class))); + } + + public function testUnknownClassesAreSkipped(): void + { + $hydrator = $this->builder([ProfileCreated::class])->build(); + $middleware = $hydrator->middlewares()[0]; + + self::assertInstanceOf(SkippableMiddleware::class, $middleware); + self::assertSame(Skip::None, $middleware->skip($hydrator->metadata(ProfileCreated::class))); + self::assertSame(Skip::Both, $middleware->skip($hydrator->metadata(ParentDto::class))); + } + + public function testNestedObjectsAreNotInlinedWithNonSkippableMiddleware(): void + { + $counter = new CountingMiddleware(); + + $hydrator = $this->builder()->build(); + $direct = (new StackHydratorBuilder()) + ->useExtension(new CoreExtension()) + ->useExtension($this->extension()) + ->addMiddleware($counter) + ->build(); + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b'], ['name' => 'c']]]; + + $object = $hydrator->hydrate(NestedLifecycleDto::class, $data); + self::assertSame($data, $hydrator->extract($object)); + + // with another middleware in the stack, nested objects go through the whole stack + $object = $direct->hydrate(NestedLifecycleDto::class, $data); + self::assertSame($data, $direct->extract($object)); + + self::assertSame([NestedLifecycleDto::class => 1, LifecycleFixture::class => 3], $counter->hydrated); + self::assertSame([NestedLifecycleDto::class => 1, LifecycleFixture::class => 3], $counter->extracted); + self::assertNotContains(true, self::inlined($direct)); + } + + public function testNestedObjectsRespectOtherMiddlewares(): void + { + $hydrator = $this->builder() + ->useExtension(new LifecycleExtension()) + ->build(); + + $object = $hydrator->hydrate( + NestedLifecycleDto::class, + ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]], + ); + + self::assertSame('a [preHydrate] [postHydrate]', $object->child->name); + self::assertSame('b [preHydrate] [postHydrate]', $object->items[0]->name); + + self::assertSame( + [ + 'child' => ['name' => 'a [preHydrate] [postHydrate] [preExtract] [postExtract]'], + 'items' => [['name' => 'b [preHydrate] [postHydrate] [preExtract] [postExtract]']], + ], + $hydrator->extract($object), + ); + } +} diff --git a/tests/Unit/Extension/Generated/MiddlewareGeneratorTest.php b/tests/Unit/Extension/Generated/MiddlewareGeneratorTest.php new file mode 100644 index 00000000..47f8922a --- /dev/null +++ b/tests/Unit/Extension/Generated/MiddlewareGeneratorTest.php @@ -0,0 +1,153 @@ +dump([ProfileCreated::class, Skill::class], 'Foo\\Bar'); + $second = $generator->dump([ProfileCreated::class, Skill::class], 'Foo\\Bar'); + + self::assertSame($first, $second); + self::assertStringContainsString('namespace Foo;', $first); + self::assertStringContainsString('final class Bar implements SkippableMiddleware, HydratorAwareMiddleware', $first); + self::assertStringNotContainsString('strict_types=1', $first); + } + + /** + * The snapshot makes every change of the generated code visible in the diff of a pull request. + * Run "make snapshot" after an intended change of the generator. + */ + public function testGeneratedCodeMatchesSnapshot(): void + { + $file = __DIR__ . '/Snapshot/SnapshotMiddleware.php'; + $code = (new MiddlewareGenerator(new AttributeMetadataFactory()))->dump( + GeneratedStackHydratorTest::CLASSES, + 'Patchlevel\\Hydrator\\Tests\\Snapshot\\SnapshotMiddleware', + ); + + if (getenv('UPDATE_SNAPSHOTS') === '1') { + file_put_contents($file, $code); + } + + self::assertStringEqualsFile($file, $code, 'The generated code changed, run "make snapshot" if the change is intended.'); + } + + public function testClassesWithClassNormalizerAreSkipped(): void + { + $generator = new MiddlewareGenerator(new AttributeMetadataFactory()); + + $code = $generator->dump([ProfileId::class], 'Foo\\Bar'); + + self::assertStringNotContainsString(sprintf('\\%s::class', ProfileId::class), $code); + } + + public function testAbstractClassIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + (new MiddlewareGenerator(new AttributeMetadataFactory()))->dump([ChildDto::class], 'Foo\\Bar'); + } + + public function testUnknownClassIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + /** @phpstan-ignore argument.type */ + (new MiddlewareGenerator(new AttributeMetadataFactory()))->dump(['Unknown'], 'Foo\\Bar'); + } + + public function testMiddlewareRequiresHydrator(): void + { + $middleware = $this->load([ProfileCreated::class]); + $metadata = (new AttributeMetadataFactory())->metadata(ProfileCreated::class); + + $this->expectException(HydratorNotSet::class); + + $middleware->hydrate($metadata, ['profileId' => '1', 'email' => 'info@patchlevel.de'], [], new Stack([$middleware])); + } + + public function testOutdatedMiddlewareIsDetected(): void + { + $middleware = $this->load([ProfileCreated::class]); + + $hydrator = (new StackHydratorBuilder()) + ->addMiddleware($middleware) + ->addMetadataEnricher(new class implements MetadataEnricher { + public function enrich(ClassMetadata $classMetadata): void + { + foreach ($classMetadata->properties as $property) { + $property->fieldName = 'renamed_' . $property->fieldName; + } + } + }) + ->build(); + + $this->expectException(OutdatedGeneratedMiddleware::class); + + $hydrator->hydrate(ProfileCreated::class, ['renamed_profileId' => '1', 'renamed_email' => 'info@patchlevel.de']); + } + + /** @param list $classes */ + private function load(array $classes): Middleware + { + $fqcn = 'Patchlevel\\Hydrator\\Tests\\Generated\\Middleware' . self::$counter++; + $code = (new MiddlewareGenerator(new AttributeMetadataFactory()))->dump($classes, $fqcn); + + $file = tempnam(GeneratedStackHydratorTest::cachePath(), 'middleware'); + assert($file !== false); + file_put_contents($file, $code); + + require $file; + unlink($file); + + assert(is_a($fqcn, Middleware::class, true)); + + return new $fqcn(); + } +} diff --git a/tests/Unit/Extension/Generated/Snapshot/SnapshotMiddleware.php b/tests/Unit/Extension/Generated/Snapshot/SnapshotMiddleware.php new file mode 100644 index 00000000..033e6158 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/SnapshotMiddleware.php @@ -0,0 +1,3997 @@ + all middlewares except this one and the TransformMiddleware behind it */ + public array $others = []; + + /** @var array */ + public array $callStack = []; + + public Normalizer $n0; + public Normalizer $n1; + public bool $ready0 = false; + public bool|null $recursive0 = null; + public Closure $h0; + public Closure $e0; + public ReflectionClass $r0; + public bool $tracked0 = true; + public Normalizer $n2; + public Normalizer $n3; + public bool $ready1 = false; + public bool|null $recursive1 = null; + public Closure $h1; + public Closure $e1; + public ReflectionClass $r1; + public bool $tracked1 = true; + public Normalizer $n4; + public bool $ih0 = false; + public bool $ie0 = false; + public bool $ready2 = false; + public bool|null $recursive2 = null; + public Closure $h2; + public Closure $e2; + public ReflectionClass $r2; + public bool $tracked2 = true; + public Normalizer $n5; + public bool $ih3 = false; + public bool $ie3 = false; + public bool $ready3 = false; + public bool|null $recursive3 = null; + public Closure $h3; + public Closure $e3; + public ReflectionClass $r3; + public bool $tracked3 = true; + public Normalizer $n6; + public bool $ih2 = false; + public bool $ie2 = false; + public bool $ready4 = false; + public bool|null $recursive4 = null; + public Closure $h4; + public Closure $e4; + public ReflectionClass $r4; + public bool $tracked4 = true; + public Normalizer $n7; + public bool $ih1 = false; + public bool $ie1 = false; + public bool $ready5 = false; + public bool|null $recursive5 = null; + public Closure $h5; + public Closure $e5; + public ReflectionClass $r5; + public bool $tracked5 = true; + public Normalizer $n8; + public bool $ready6 = false; + public bool|null $recursive6 = null; + public Closure $h6; + public Closure $e6; + public ReflectionClass $r6; + public bool $tracked6 = true; + public Normalizer $n9; + public bool $ih4 = false; + public bool $ie4 = false; + public ReflectionParameter $d0; + public bool $ready7 = false; + public bool|null $recursive7 = null; + public Closure $h7; + public Closure $e7; + public ReflectionClass $r7; + public bool $tracked7 = true; + public bool $ready8 = false; + public bool|null $recursive8 = null; + public Closure $h8; + public Closure $e8; + public ReflectionClass $r8; + public Normalizer $n10; + public Normalizer $n11; + public Normalizer $n12; + public Normalizer $n13; + public bool $ready9 = false; + public bool|null $recursive9 = null; + public Closure $h9; + public Closure $e9; + public ReflectionClass $r9; + public bool $tracked9 = true; + public Normalizer $n14; + public Normalizer $n15; + public Normalizer $n16; + public Normalizer $n17; + public Normalizer $n18; + public bool $ready10 = false; + public bool|null $recursive10 = null; + public Closure $h10; + public Closure $e10; + public ReflectionClass $r10; + public bool $tracked10 = true; + public Normalizer $n19; + public Normalizer $n20; + public Normalizer $n21; + public Normalizer $n22; + public Normalizer $n23; + public Normalizer $n24; + public bool $ready11 = false; + public bool|null $recursive11 = null; + public Closure $h11; + public Closure $e11; + public ReflectionClass $r11; + public bool $tracked11 = true; + public Normalizer $n25; + public Normalizer $n26; + public bool $ih5 = false; + public bool $ie5 = false; + public Normalizer $n29; + public Normalizer $n30; + public Normalizer $n31; + public Normalizer $n32; + public ObjectNormalizer $n33; + public bool $ih6 = false; + public bool $ie6 = false; + public bool $ready12 = false; + public bool|null $recursive12 = null; + public Closure $h12; + public Closure $e12; + public ReflectionClass $r12; + public bool $tracked12 = true; + public Normalizer $n27; + public Normalizer $n28; + public bool $ready13 = false; + public bool|null $recursive13 = null; + public Closure $h13; + public Closure $e13; + public ReflectionClass $r13; + public bool $tracked13 = true; + public bool $ready14 = false; + public bool|null $recursive14 = null; + public Closure $h14; + public Closure $e14; + public ReflectionClass $r14; + public Normalizer $n34; + public Normalizer $n35; + public bool $ready15 = false; + public bool|null $recursive15 = null; + public Closure $h15; + public Closure $e15; + public ReflectionClass $r15; + public bool $tracked15 = true; + public Normalizer $n36; + public bool $ready16 = false; + public bool|null $recursive16 = null; + public Closure $h16; + public Closure $e16; + public ReflectionClass $r16; + public bool $tracked16 = true; + public Normalizer $n37; + public Normalizer $n38; + public bool $ready17 = false; + public bool|null $recursive17 = null; + public Closure $h17; + public Closure $e17; + public ReflectionClass $r17; + public bool $tracked17 = true; + public Normalizer $n39; + public bool $ready18 = false; + public bool|null $recursive18 = null; + public Closure $h18; + public Closure $e18; + public ReflectionClass $r18; + public bool $tracked18 = true; + public Normalizer $n40; + public Normalizer $n41; + public bool $ready19 = false; + public bool|null $recursive19 = null; + public Closure $h19; + public Closure $e19; + public ReflectionClass $r19; + public Closure $hp0; + public Closure $ep1; + public bool $tracked19 = true; + public Normalizer $n42; + public Normalizer $n43; + public bool $ready20 = false; + public bool|null $recursive20 = null; + public Closure $h20; + public Closure $e20; + public ReflectionClass $r20; + public bool $tracked20 = true; + public Normalizer $n44; + public bool $ih7 = false; + public bool $ie7 = false; + public Normalizer $n45; + public ObjectNormalizer $n46; + public bool $ih8 = false; + public bool $ie8 = false; + public bool $ready21 = false; + public bool|null $recursive21 = null; + public Closure $h21; + public Closure $e21; + public ReflectionClass $r21; + public bool $tracked21 = true; + public bool $ready22 = false; + public bool|null $recursive22 = null; + public Closure $h22; + public Closure $e22; + public ReflectionClass $r22; + + public function __construct() + { + $this->h0 = function (array $data, array $context, object|null $object = null): object { $this->init0(); return ($this->h0)($data, $context, $object); }; + $this->e0 = function (object $object, array $context): array { $this->init0(); return ($this->e0)($object, $context); }; + $this->h1 = function (array $data, array $context, object|null $object = null): object { $this->init1(); return ($this->h1)($data, $context, $object); }; + $this->e1 = function (object $object, array $context): array { $this->init1(); return ($this->e1)($object, $context); }; + $this->h2 = function (array $data, array $context, object|null $object = null): object { $this->init2(); return ($this->h2)($data, $context, $object); }; + $this->e2 = function (object $object, array $context): array { $this->init2(); return ($this->e2)($object, $context); }; + $this->h3 = function (array $data, array $context, object|null $object = null): object { $this->init3(); return ($this->h3)($data, $context, $object); }; + $this->e3 = function (object $object, array $context): array { $this->init3(); return ($this->e3)($object, $context); }; + $this->h4 = function (array $data, array $context, object|null $object = null): object { $this->init4(); return ($this->h4)($data, $context, $object); }; + $this->e4 = function (object $object, array $context): array { $this->init4(); return ($this->e4)($object, $context); }; + $this->h5 = function (array $data, array $context, object|null $object = null): object { $this->init5(); return ($this->h5)($data, $context, $object); }; + $this->e5 = function (object $object, array $context): array { $this->init5(); return ($this->e5)($object, $context); }; + $this->h6 = function (array $data, array $context, object|null $object = null): object { $this->init6(); return ($this->h6)($data, $context, $object); }; + $this->e6 = function (object $object, array $context): array { $this->init6(); return ($this->e6)($object, $context); }; + $this->h7 = function (array $data, array $context, object|null $object = null): object { $this->init7(); return ($this->h7)($data, $context, $object); }; + $this->e7 = function (object $object, array $context): array { $this->init7(); return ($this->e7)($object, $context); }; + $this->h8 = function (array $data, array $context, object|null $object = null): object { $this->init8(); return ($this->h8)($data, $context, $object); }; + $this->e8 = function (object $object, array $context): array { $this->init8(); return ($this->e8)($object, $context); }; + $this->h9 = function (array $data, array $context, object|null $object = null): object { $this->init9(); return ($this->h9)($data, $context, $object); }; + $this->e9 = function (object $object, array $context): array { $this->init9(); return ($this->e9)($object, $context); }; + $this->h10 = function (array $data, array $context, object|null $object = null): object { $this->init10(); return ($this->h10)($data, $context, $object); }; + $this->e10 = function (object $object, array $context): array { $this->init10(); return ($this->e10)($object, $context); }; + $this->h11 = function (array $data, array $context, object|null $object = null): object { $this->init11(); return ($this->h11)($data, $context, $object); }; + $this->e11 = function (object $object, array $context): array { $this->init11(); return ($this->e11)($object, $context); }; + $this->h12 = function (array $data, array $context, object|null $object = null): object { $this->init12(); return ($this->h12)($data, $context, $object); }; + $this->e12 = function (object $object, array $context): array { $this->init12(); return ($this->e12)($object, $context); }; + $this->h13 = function (array $data, array $context, object|null $object = null): object { $this->init13(); return ($this->h13)($data, $context, $object); }; + $this->e13 = function (object $object, array $context): array { $this->init13(); return ($this->e13)($object, $context); }; + $this->h14 = function (array $data, array $context, object|null $object = null): object { $this->init14(); return ($this->h14)($data, $context, $object); }; + $this->e14 = function (object $object, array $context): array { $this->init14(); return ($this->e14)($object, $context); }; + $this->h15 = function (array $data, array $context, object|null $object = null): object { $this->init15(); return ($this->h15)($data, $context, $object); }; + $this->e15 = function (object $object, array $context): array { $this->init15(); return ($this->e15)($object, $context); }; + $this->h16 = function (array $data, array $context, object|null $object = null): object { $this->init16(); return ($this->h16)($data, $context, $object); }; + $this->e16 = function (object $object, array $context): array { $this->init16(); return ($this->e16)($object, $context); }; + $this->h17 = function (array $data, array $context, object|null $object = null): object { $this->init17(); return ($this->h17)($data, $context, $object); }; + $this->e17 = function (object $object, array $context): array { $this->init17(); return ($this->e17)($object, $context); }; + $this->h18 = function (array $data, array $context, object|null $object = null): object { $this->init18(); return ($this->h18)($data, $context, $object); }; + $this->e18 = function (object $object, array $context): array { $this->init18(); return ($this->e18)($object, $context); }; + $this->h19 = function (array $data, array $context, object|null $object = null): object { $this->init19(); return ($this->h19)($data, $context, $object); }; + $this->e19 = function (object $object, array $context): array { $this->init19(); return ($this->e19)($object, $context); }; + $this->h20 = function (array $data, array $context, object|null $object = null): object { $this->init20(); return ($this->h20)($data, $context, $object); }; + $this->e20 = function (object $object, array $context): array { $this->init20(); return ($this->e20)($object, $context); }; + $this->h21 = function (array $data, array $context, object|null $object = null): object { $this->init21(); return ($this->h21)($data, $context, $object); }; + $this->e21 = function (object $object, array $context): array { $this->init21(); return ($this->e21)($object, $context); }; + $this->h22 = function (array $data, array $context, object|null $object = null): object { $this->init22(); return ($this->h22)($data, $context, $object); }; + $this->e22 = function (object $object, array $context): array { $this->init22(); return ($this->e22)($object, $context); }; + } + + public function setHydrator(StackHydrator $hydrator): void + { + $this->hydrator = $hydrator; + $this->others = []; + + foreach ($hydrator->middlewares() as $middleware) { + if ($middleware === $this || $middleware instanceof TransformMiddleware) { + continue; + } + + $this->others[] = $middleware; + } + } + + /** Lets the GeneratedHydrator call the generated code directly, skipping the middleware stack. */ + public function compiledHydrator(ClassMetadata $metadata): Closure|null + { + if (!$this->exclusive($metadata, Skip::Hydrate)) { + return null; + } + + switch ($metadata->className) { + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class: + $this->init0(); + + return $this->fastHydrate0(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class: + $this->init1(); + + return $this->fastHydrate1(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class: + $this->init2(); + + return $this->fastHydrate2(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class: + $this->init3(); + + return $this->fastHydrate3(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class: + $this->init4(); + + return $this->fastHydrate4(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class: + $this->init5(); + + return $this->fastHydrate5(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class: + $this->init6(); + + return $this->fastHydrate6(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class: + $this->init7(); + + return $this->fastHydrate7(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class: + $this->init8(); + + return $this->fastHydrate8(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class: + $this->init9(); + + return $this->fastHydrate9(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class: + $this->init10(); + + return $this->fastHydrate10(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class: + $this->init11(); + + return $this->fastHydrate11(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class: + $this->init12(); + + return $this->fastHydrate12(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class: + $this->init13(); + + return $this->fastHydrate13(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class: + $this->init14(); + + return $this->fastHydrate14(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class: + $this->init15(); + + return $this->fastHydrate15(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class: + $this->init16(); + + return $this->fastHydrate16(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class: + $this->init17(); + + return $this->fastHydrate17(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class: + $this->init18(); + + return $this->fastHydrate18(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class: + $this->init19(); + + return $this->fastHydrate19(...); + case \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class: + $this->init20(); + + return $this->fastHydrate20(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class: + $this->init21(); + + return $this->fastHydrate21(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class: + $this->init22(); + + return $this->fastHydrate22(...); + default: + return null; + } + } + + public function compiledExtractor(ClassMetadata $metadata): Closure|null + { + if (!$this->exclusive($metadata, Skip::Extract)) { + return null; + } + + switch ($metadata->className) { + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class: + $this->init0(); + + return $this->extract0(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class: + $this->init1(); + + return $this->extract1(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class: + $this->init2(); + + return $this->extract2(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class: + $this->init3(); + + return $this->extract3(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class: + $this->init4(); + + return $this->extract4(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class: + $this->init5(); + + return $this->extract5(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class: + $this->init6(); + + return $this->extract6(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class: + $this->init7(); + + return $this->extract7(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class: + $this->init8(); + + return $this->e8; + case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class: + $this->init9(); + + return $this->extract9(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class: + $this->init10(); + + return $this->extract10(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class: + $this->init11(); + + return $this->extract11(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class: + $this->init12(); + + return $this->extract12(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class: + $this->init13(); + + return $this->extract13(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class: + $this->init14(); + + return $this->extract14(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class: + $this->init15(); + + return $this->extract15(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class: + $this->init16(); + + return $this->extract16(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class: + $this->init17(); + + return $this->extract17(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class: + $this->init18(); + + return $this->e18; + case \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class: + $this->init19(); + + return $this->e19; + case \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class: + $this->init20(); + + return $this->extract20(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class: + $this->init21(); + + return $this->extract21(...); + case \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class: + $this->init22(); + + return $this->extract22(...); + default: + return null; + } + } + + /** Classes without generated code are left to the TransformMiddleware directly. */ + public function skip(ClassMetadata $metadata): Skip + { + return match ($metadata->className) { + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, + \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, + \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, + => Skip::None, + default => Skip::Both, + }; + } + + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object + { + $object = null; + $populate = $context; + + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($populate[Hydrator::OBJECT_TO_POPULATE]); + } + + return match ($metadata->className) { + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class => $this->ready0 ? $this->hydrate0($data, $populate, $object) : ($this->h0)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class => $this->ready1 ? $this->hydrate1($data, $populate, $object) : ($this->h1)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class => $this->ready2 ? $this->hydrate2($data, $populate, $object) : ($this->h2)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class => $this->ready3 ? $this->hydrate3($data, $populate, $object) : ($this->h3)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class => $this->ready4 ? $this->hydrate4($data, $populate, $object) : ($this->h4)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class => $this->ready5 ? $this->hydrate5($data, $populate, $object) : ($this->h5)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class => $this->ready6 ? $this->hydrate6($data, $populate, $object) : ($this->h6)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class => $this->ready7 ? $this->hydrate7($data, $populate, $object) : ($this->h7)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class => ($this->h8)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class => $this->ready9 ? $this->hydrate9($data, $populate, $object) : ($this->h9)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class => $this->ready10 ? $this->hydrate10($data, $populate, $object) : ($this->h10)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class => $this->ready11 ? $this->hydrate11($data, $populate, $object) : ($this->h11)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class => $this->ready12 ? $this->hydrate12($data, $populate, $object) : ($this->h12)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class => $this->ready13 ? $this->hydrate13($data, $populate, $object) : ($this->h13)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class => $this->ready14 ? $this->hydrate14($data, $populate, $object) : ($this->h14)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class => $this->ready15 ? $this->hydrate15($data, $populate, $object) : ($this->h15)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class => $this->ready16 ? $this->hydrate16($data, $populate, $object) : ($this->h16)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class => $this->ready17 ? $this->hydrate17($data, $populate, $object) : ($this->h17)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class => ($this->h18)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class => ($this->h19)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class => $this->ready20 ? $this->hydrate20($data, $populate, $object) : ($this->h20)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class => $this->ready21 ? $this->hydrate21($data, $populate, $object) : ($this->h21)($data, $populate, $object), + \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class => $this->ready22 ? $this->hydrate22($data, $populate, $object) : ($this->h22)($data, $populate, $object), + default => $stack->next()->hydrate($metadata, $data, $context, $stack), + }; + } + + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array + { + return match ($metadata->className) { + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class => $this->ready0 ? $this->extract0($object, $context) : ($this->e0)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class => $this->ready1 ? $this->extract1($object, $context) : ($this->e1)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class => $this->ready2 ? $this->extract2($object, $context) : ($this->e2)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class => $this->ready3 ? $this->extract3($object, $context) : ($this->e3)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class => $this->ready4 ? $this->extract4($object, $context) : ($this->e4)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class => $this->ready5 ? $this->extract5($object, $context) : ($this->e5)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class => $this->ready6 ? $this->extract6($object, $context) : ($this->e6)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class => $this->ready7 ? $this->extract7($object, $context) : ($this->e7)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class => ($this->e8)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class => $this->ready9 ? $this->extract9($object, $context) : ($this->e9)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class => $this->ready10 ? $this->extract10($object, $context) : ($this->e10)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class => $this->ready11 ? $this->extract11($object, $context) : ($this->e11)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class => $this->ready12 ? $this->extract12($object, $context) : ($this->e12)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class => $this->ready13 ? $this->extract13($object, $context) : ($this->e13)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class => $this->ready14 ? $this->extract14($object, $context) : ($this->e14)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class => $this->ready15 ? $this->extract15($object, $context) : ($this->e15)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class => $this->ready16 ? $this->extract16($object, $context) : ($this->e16)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class => $this->ready17 ? $this->extract17($object, $context) : ($this->e17)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class => ($this->e18)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class => ($this->e19)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class => $this->ready20 ? $this->extract20($object, $context) : ($this->e20)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class => $this->ready21 ? $this->extract21($object, $context) : ($this->e21)($object, $context), + \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class => $this->ready22 ? $this->extract22($object, $context) : ($this->e22)($object, $context), + default => $stack->next()->extract($metadata, $object, $context, $stack), + }; + } + + private function init0(): void + { + if ($this->ready0) { return; } + $this->ready0 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, ['profileId' => ['profileId', true], 'email' => ['email', true]]); + $this->r0 = $metadata->reflection; + $this->n0 = $this->normalizer($metadata, 'profileId'); + $this->n1 = $this->normalizer($metadata, 'email'); + $this->h0 = $this->hydrate0(...); + $this->e0 = $this->extract0(...); + $this->tracked0 = $this->recursive0(); + } + + private function init1(): void + { + if ($this->ready1) { return; } + $this->ready1 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, ['profileId' => ['profileId', true], 'email' => ['email', true]]); + $this->r1 = $metadata->reflection; + $this->n2 = $this->normalizer($metadata, 'profileId'); + $this->n3 = $this->normalizer($metadata, 'email'); + $this->h1 = $this->hydrate1(...); + $this->e1 = $this->extract1(...); + $this->tracked1 = $this->recursive1(); + } + + private function init2(): void + { + if ($this->ready2) { return; } + $this->ready2 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, ['profileCreated' => ['event', true]]); + $this->r2 = $metadata->reflection; + $this->n4 = $this->normalizer($metadata, 'profileCreated'); + $this->ih0 = $this->inlineObject($this->n4, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Skip::Hydrate); + $this->ie0 = $this->inlineObject($this->n4, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Skip::Extract); + $this->h2 = $this->hydrate2(...); + $this->e2 = $this->extract2(...); + $this->init0(); + $this->tracked2 = $this->recursive2(); + } + + private function init3(): void + { + if ($this->ready3) { return; } + $this->ready3 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, ['to' => ['to', true]]); + $this->r3 = $metadata->reflection; + $this->n5 = $this->normalizer($metadata, 'to'); + $this->ih3 = $this->inlineObject($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Skip::Hydrate); + $this->ie3 = $this->inlineObject($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Skip::Extract); + $this->h3 = $this->hydrate3(...); + $this->e3 = $this->extract3(...); + $this->init4(); + $this->tracked3 = $this->recursive3(); + } + + private function init4(): void + { + if ($this->ready4) { return; } + $this->ready4 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, ['to' => ['to', true]]); + $this->r4 = $metadata->reflection; + $this->n6 = $this->normalizer($metadata, 'to'); + $this->ih2 = $this->inlineObject($this->n6, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Skip::Hydrate); + $this->ie2 = $this->inlineObject($this->n6, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Skip::Extract); + $this->h4 = $this->hydrate4(...); + $this->e4 = $this->extract4(...); + $this->init5(); + $this->tracked4 = $this->recursive4(); + } + + private function init5(): void + { + if ($this->ready5) { return; } + $this->ready5 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, ['to' => ['to', true]]); + $this->r5 = $metadata->reflection; + $this->n7 = $this->normalizer($metadata, 'to'); + $this->ih1 = $this->inlineObject($this->n7, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Skip::Hydrate); + $this->ie1 = $this->inlineObject($this->n7, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Skip::Extract); + $this->h5 = $this->hydrate5(...); + $this->e5 = $this->extract5(...); + $this->init3(); + $this->tracked5 = $this->recursive5(); + } + + private function init6(): void + { + if ($this->ready6) { return; } + $this->ready6 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, ['value' => ['value', true]]); + $this->r6 = $metadata->reflection; + $this->n8 = $this->normalizer($metadata, 'value'); + $this->h6 = $this->hydrate6(...); + $this->e6 = $this->extract6(...); + $this->tracked6 = $this->recursive6(); + } + + private function init7(): void + { + if ($this->ready7) { return; } + $this->ready7 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, ['admin' => ['admin', false], 'name' => ['name', false], 'email' => ['email', true]]); + $this->r7 = $metadata->reflection; + $this->d0 = $this->promotedDefault($metadata, 'email'); + $this->n9 = $this->normalizer($metadata, 'email'); + $this->ih4 = $this->inlineObject($this->n9, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Skip::Hydrate); + $this->ie4 = $this->inlineObject($this->n9, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Skip::Extract); + $this->h7 = $this->hydrate7(...); + $this->e7 = $this->extract7(...); + $this->init8(); + $this->tracked7 = $this->recursive7(); + } + + private function init8(): void + { + if ($this->ready8) { return; } + $this->ready8 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, ['value' => ['value', false]]); + $this->r8 = $metadata->reflection; + $this->h8 = Closure::bind(function (array $data, array $context, object|null $object = null): object { + $object ??= $this->r8->newInstanceWithoutConstructor(); + + if (\array_key_exists('value', $data)) { + $value = $data['value']; + + try { + $object->value = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, 'value', $e); + } + } + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class); + $this->e8 = Closure::bind(function (object $object, array $context): array { + + + return [ + 'value' => $object->value, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class); + } + + private function init9(): void + { + if ($this->ready9) { return; } + $this->ready9 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, ['status' => ['status', true], 'dateTimeImmutable' => ['dateTimeImmutable', true], 'dateTime' => ['dateTime', true], 'dateTimeZone' => ['dateTimeZone', true], 'array' => ['array', false]]); + $this->r9 = $metadata->reflection; + $this->n10 = $this->normalizer($metadata, 'status'); + $this->n11 = $this->normalizer($metadata, 'dateTimeImmutable'); + $this->n12 = $this->normalizer($metadata, 'dateTime'); + $this->n13 = $this->normalizer($metadata, 'dateTimeZone'); + $this->h9 = $this->hydrate9(...); + $this->e9 = $this->extract9(...); + $this->tracked9 = $this->recursive9(); + } + + private function init10(): void + { + if ($this->ready10) { return; } + $this->ready10 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, ['status' => ['status', true], 'dateTimeImmutable' => ['dateTimeImmutable', true], 'dateTime' => ['dateTime', true], 'dateTimeZone' => ['dateTimeZone', true], 'profileId' => ['profileId', true]]); + $this->r10 = $metadata->reflection; + $this->n14 = $this->normalizer($metadata, 'status'); + $this->n15 = $this->normalizer($metadata, 'dateTimeImmutable'); + $this->n16 = $this->normalizer($metadata, 'dateTime'); + $this->n17 = $this->normalizer($metadata, 'dateTimeZone'); + $this->n18 = $this->normalizer($metadata, 'profileId'); + $this->h10 = $this->hydrate10(...); + $this->e10 = $this->extract10(...); + $this->tracked10 = $this->recursive10(); + } + + private function init11(): void + { + if ($this->ready11) { return; } + $this->ready11 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, ['defaultArray' => ['defaultArray', true], 'listArray' => ['listArray', true], 'iterableArray' => ['iterableArray', true], 'hashMap' => ['hashMap', true], 'nested' => ['nested', true], 'jsonArray' => ['jsonArray', false], 'shapeArray' => ['shapeArray', true]]); + $this->r11 = $metadata->reflection; + $this->n19 = $this->normalizer($metadata, 'defaultArray'); + $this->n20 = $this->normalizer($metadata, 'listArray'); + $this->n21 = $this->normalizer($metadata, 'iterableArray'); + $this->n22 = $this->normalizer($metadata, 'hashMap'); + $this->n23 = $this->normalizer($metadata, 'nested'); + $this->n24 = $this->normalizer($metadata, 'shapeArray'); + $this->h11 = $this->hydrate11(...); + $this->e11 = $this->extract11(...); + $this->tracked11 = $this->recursive11(); + } + + private function init12(): void + { + if ($this->ready12) { return; } + $this->ready12 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, ['status' => ['status', true], 'profileCreated' => ['profileCreated', true], 'defaultArray' => ['defaultArray', true], 'listArray' => ['listArray', true], 'iterableArray' => ['iterableArray', true], 'skillsHashMap' => ['skillsHashMap', true], 'jsonArray' => ['jsonArray', false]]); + $this->r12 = $metadata->reflection; + $this->n25 = $this->normalizer($metadata, 'status'); + $this->n26 = $this->normalizer($metadata, 'profileCreated'); + $this->ih5 = $this->inlineObject($this->n26, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Skip::Hydrate); + $this->ie5 = $this->inlineObject($this->n26, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Skip::Extract); + $this->n29 = $this->normalizer($metadata, 'defaultArray'); + $this->n30 = $this->normalizer($metadata, 'listArray'); + $this->n31 = $this->normalizer($metadata, 'iterableArray'); + $this->n32 = $this->normalizer($metadata, 'skillsHashMap'); + $this->ih6 = $this->inlineArray($this->n32, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Skip::Hydrate); + $this->ie6 = $this->inlineArray($this->n32, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Skip::Extract); + if ($this->ih6 || $this->ie6) { $this->n33 = $this->n32->innerNormalizer(); } + $this->h12 = $this->hydrate12(...); + $this->e12 = $this->extract12(...); + $this->init13(); + $this->init14(); + $this->tracked12 = $this->recursive12(); + } + + private function init13(): void + { + if ($this->ready13) { return; } + $this->ready13 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, ['profileId' => ['profileId', true], 'email' => ['email', true]]); + $this->r13 = $metadata->reflection; + $this->n27 = $this->normalizer($metadata, 'profileId'); + $this->n28 = $this->normalizer($metadata, 'email'); + $this->h13 = $this->hydrate13(...); + $this->e13 = $this->extract13(...); + $this->tracked13 = $this->recursive13(); + } + + private function init14(): void + { + if ($this->ready14) { return; } + $this->ready14 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, ['name' => ['name', false]]); + $this->r14 = $metadata->reflection; + $this->h14 = $this->hydrate14(...); + $this->e14 = $this->extract14(...); + } + + private function init15(): void + { + if ($this->ready15) { return; } + $this->ready15 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, ['profileId' => ['profileId', true], 'email' => ['email', true]]); + $this->r15 = $metadata->reflection; + $this->n34 = $this->normalizer($metadata, 'profileId'); + $this->n35 = $this->normalizer($metadata, 'email'); + $this->h15 = $this->hydrate15(...); + $this->e15 = $this->extract15(...); + $this->tracked15 = $this->recursive15(); + } + + private function init16(): void + { + if ($this->ready16) { return; } + $this->ready16 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, ['email' => ['email', true]]); + $this->r16 = $metadata->reflection; + $this->n36 = $this->normalizer($metadata, 'email'); + $this->h16 = $this->hydrate16(...); + $this->e16 = $this->extract16(...); + $this->tracked16 = $this->recursive16(); + } + + private function init17(): void + { + if ($this->ready17) { return; } + $this->ready17 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, ['profileId' => ['profileId', true], 'valueObject' => ['valueObject', true]]); + $this->r17 = $metadata->reflection; + $this->n37 = $this->normalizer($metadata, 'profileId'); + $this->n38 = $this->normalizer($metadata, 'valueObject'); + $this->h17 = $this->hydrate17(...); + $this->e17 = $this->extract17(...); + $this->tracked17 = $this->recursive17(); + } + + private function init18(): void + { + if ($this->ready18) { return; } + $this->ready18 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, ['profileId' => ['profileId', true], 'name' => ['name', false], 'age' => ['age', false]]); + $this->r18 = $metadata->reflection; + $this->n39 = $this->normalizer($metadata, 'profileId'); + $this->h18 = Closure::bind(function (array $data, array $context, object|null $object = null): object { + $object ??= $this->r18->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n39->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n39::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $e); + } + } + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'name', $e); + } + } + + if (\array_key_exists('age', $data)) { + $value = $data['age']; + + try { + $object->age = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'age', $e); + } + } else { + $object->age = 42; + } + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); + $this->e18 = Closure::bind(function (object $object, array $context): array { + if ($this->tracked18) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class; + + try { + try { + $v_profileId = $this->n39->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n39::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'age' => $object->age, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_profileId = $this->n39->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class, 'profileId', $this->n39::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'age' => $object->age, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); + $this->tracked18 = $this->recursive18(); + } + + private function init19(): void + { + if ($this->ready19) { return; } + $this->ready19 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, ['profileId' => ['profileId', true], 'name' => ['name', false], 'email' => ['email', true], 'note' => ['note', false]]); + $this->r19 = $metadata->reflection; + $this->n40 = $this->normalizer($metadata, 'profileId'); + $this->n41 = $this->normalizer($metadata, 'email'); + $this->hp0 = Closure::bind(function (object $object, array $data, array $context): void { + if (\array_key_exists('email', $data)) { + try { + $value = $this->n41->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $this->n41::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $e); + } + } + if (\array_key_exists('note', $data)) { + $value = $data['note']; + + try { + $object->note = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'note', $e); + } + } + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateParentDto::class); + $this->h19 = Closure::bind(function (array $data, array $context, object|null $object = null): object { + $object ??= $this->r19->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n40->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n40::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $e); + } + } + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'name', $e); + } + } + + ($this->hp0)($object, $data, $context); + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->ep1 = Closure::bind(function (object $object, array $context): array { + try { + $v_email = $this->n41->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'email', $this->n41::class, $e); + } + return [$v_email, $object->note]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateParentDto::class); + $this->e19 = Closure::bind(function (object $object, array $context): array { + if ($this->tracked19) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class; + + try { + [$v_email, $v_note] = ($this->ep1)($object, $context); + + try { + $v_profileId = $this->n40->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n40::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'email' => $v_email, + 'note' => $v_note, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + [$v_email, $v_note] = ($this->ep1)($object, $context); + + try { + $v_profileId = $this->n40->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class, 'profileId', $this->n40::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'email' => $v_email, + 'note' => $v_note, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->tracked19 = $this->recursive19(); + } + + private function init20(): void + { + if ($this->ready20) { return; } + $this->ready20 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, ['profileId' => ['id', true], 'email' => ['email', true]]); + $this->r20 = $metadata->reflection; + $this->n42 = $this->normalizer($metadata, 'profileId'); + $this->n43 = $this->normalizer($metadata, 'email'); + $this->h20 = $this->hydrate20(...); + $this->e20 = $this->extract20(...); + $this->tracked20 = $this->recursive20(); + } + + private function init21(): void + { + if ($this->ready21) { return; } + $this->ready21 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, ['child' => ['child', true], 'items' => ['items', true]]); + $this->r21 = $metadata->reflection; + $this->n44 = $this->normalizer($metadata, 'child'); + $this->ih7 = $this->inlineObject($this->n44, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Skip::Hydrate); + $this->ie7 = $this->inlineObject($this->n44, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Skip::Extract); + $this->n45 = $this->normalizer($metadata, 'items'); + $this->ih8 = $this->inlineArray($this->n45, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Skip::Hydrate); + $this->ie8 = $this->inlineArray($this->n45, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Skip::Extract); + if ($this->ih8 || $this->ie8) { $this->n46 = $this->n45->innerNormalizer(); } + $this->h21 = $this->hydrate21(...); + $this->e21 = $this->extract21(...); + $this->init22(); + $this->tracked21 = $this->recursive21(); + } + + private function init22(): void + { + if ($this->ready22) { return; } + $this->ready22 = true; + $metadata = $this->metadata(\Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, ['name' => ['name', false]]); + $this->r22 = $metadata->reflection; + $this->h22 = $this->hydrate22(...); + $this->e22 = $this->extract22(...); + } + + public function fastHydrate0(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate0($data, $context, $object); + } + + return $this->hydrate0($data, $context); + } + + public function hydrate0(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r0->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n0->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $e); + } + } + + return $object; + } + + public function extract0(object $object, array $context): array + { + if ($this->tracked0) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class; + + try { + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_profileId = $this->n0->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_email = $this->n1->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive0(): bool + { + if ($this->recursive0 !== null) { + return $this->recursive0; + } + + $this->recursive0 = true; + + return $this->recursive0 = $this->n0 instanceof HydratorAwareNormalizer + || $this->n1 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate1(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate1($data, $context, $object); + } + + return $this->hydrate1($data, $context); + } + + public function hydrate1(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r1->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n2->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $this->n2::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n3->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n3::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $e); + } + } + + return $object; + } + + public function extract1(object $object, array $context): array + { + if ($this->tracked1) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class; + + try { + try { + $v_profileId = $this->n2->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $this->n2::class, $e); + } + + try { + $v_email = $this->n3->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n3::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_profileId = $this->n2->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'profileId', $this->n2::class, $e); + } + + try { + $v_email = $this->n3->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n3::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive1(): bool + { + if ($this->recursive1 !== null) { + return $this->recursive1; + } + + $this->recursive1 = true; + + return $this->recursive1 = $this->n2 instanceof HydratorAwareNormalizer + || $this->n3 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate2(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate2($data, $context, $object); + } + + return $this->hydrate2($data, $context); + } + + public function hydrate2(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r2->newInstanceWithoutConstructor(); + + if (\array_key_exists('event', $data)) { + try { + $value = $data['event']; + + if ($this->ih0 && \is_array($value)) { + $value = $this->hydrate0($value, $context); + } else { + $value = $this->n4->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n4::class, $e); + } + + try { + $object->profileCreated = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $e); + } + } + + return $object; + } + + public function extract2(object $object, array $context): array + { + if ($this->tracked2) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class; + + try { + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->extract0($value, $context) : $this->n4->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n4::class, $e); + } + + return [ + 'event' => $v_profileCreated, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->extract0($value, $context) : $this->n4->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n4::class, $e); + } + + return [ + 'event' => $v_profileCreated, + ]; + } + + private function recursive2(): bool + { + if ($this->recursive2 !== null) { + return $this->recursive2; + } + + $this->recursive2 = true; + + return $this->recursive2 = ($this->ie0 ? $this->recursive0() : $this->n4 instanceof HydratorAwareNormalizer); + } + + public function fastHydrate3(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate3($data, $context, $object); + } + + return $this->hydrate3($data, $context); + } + + public function hydrate3(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r3->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih3 && \is_array($value)) { + $value = $this->hydrate4($value, $context); + } else { + $value = $this->n5->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n5::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract3(object $object, array $context): array + { + if ($this->tracked3) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie3 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract4($value, $context) : $this->n5->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n5::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie3 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->extract4($value, $context) : $this->n5->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n5::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive3(): bool + { + if ($this->recursive3 !== null) { + return $this->recursive3; + } + + $this->recursive3 = true; + + return $this->recursive3 = ($this->ie3 ? $this->recursive4() : $this->n5 instanceof HydratorAwareNormalizer); + } + + public function fastHydrate4(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate4($data, $context, $object); + } + + return $this->hydrate4($data, $context); + } + + public function hydrate4(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r4->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih2 && \is_array($value)) { + $value = $this->hydrate5($value, $context); + } else { + $value = $this->n6->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n6::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract4(object $object, array $context): array + { + if ($this->tracked4) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract5($value, $context) : $this->n6->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n6::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie2 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->extract5($value, $context) : $this->n6->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n6::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive4(): bool + { + if ($this->recursive4 !== null) { + return $this->recursive4; + } + + $this->recursive4 = true; + + return $this->recursive4 = ($this->ie2 ? $this->recursive5() : $this->n6 instanceof HydratorAwareNormalizer); + } + + public function fastHydrate5(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate5($data, $context, $object); + } + + return $this->hydrate5($data, $context); + } + + public function hydrate5(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r5->newInstanceWithoutConstructor(); + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih1 && \is_array($value)) { + $value = $this->hydrate3($value, $context); + } else { + $value = $this->n7->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n7::class, $e); + } + + try { + $object->to = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $e); + } + } + + return $object; + } + + public function extract5(object $object, array $context): array + { + if ($this->tracked5) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract3($value, $context) : $this->n7->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n7::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie1 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->extract3($value, $context) : $this->n7->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n7::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + private function recursive5(): bool + { + if ($this->recursive5 !== null) { + return $this->recursive5; + } + + $this->recursive5 = true; + + return $this->recursive5 = ($this->ie1 ? $this->recursive3() : $this->n7 instanceof HydratorAwareNormalizer); + } + + public function fastHydrate6(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate6($data, $context, $object); + } + + return $this->hydrate6($data, $context); + } + + public function hydrate6(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r6->newInstanceWithoutConstructor(); + + if (\array_key_exists('value', $data)) { + try { + $value = $this->n8->denormalize($data['value'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n8::class, $e); + } + + try { + $object->value = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $e); + } + } + + return $object; + } + + public function extract6(object $object, array $context): array + { + if ($this->tracked6) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class; + + try { + try { + $v_value = $this->n8->normalize($object->value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n8::class, $e); + } + + return [ + 'value' => $v_value, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_value = $this->n8->normalize($object->value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n8::class, $e); + } + + return [ + 'value' => $v_value, + ]; + } + + private function recursive6(): bool + { + if ($this->recursive6 !== null) { + return $this->recursive6; + } + + $this->recursive6 = true; + + return $this->recursive6 = $this->n8 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate7(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate7($data, $context, $object); + } + + return $this->hydrate7($data, $context); + } + + public function hydrate7(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r7->newInstanceWithoutConstructor(); + + if (\array_key_exists('admin', $data)) { + $value = $data['admin']; + + try { + $object->admin = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'admin', $e); + } + } + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'name', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $data['email']; + + if ($this->ih4 && \is_array($value)) { + $value = ($this->h8)($value, $context); + } else { + $value = $this->n9->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n9::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $e); + } + } else { + $object->email = $this->d0->getDefaultValue(); + } + + return $object; + } + + public function extract7(object $object, array $context): array + { + if ($this->tracked7) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class; + + try { + try { + $value = $object->email; + $v_email = $this->ie4 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? ($this->e8)($value, $context) : $this->n9->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n9::class, $e); + } + + return [ + 'admin' => $object->admin, + 'name' => $object->name, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $value = $object->email; + $v_email = $this->ie4 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? ($this->e8)($value, $context) : $this->n9->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n9::class, $e); + } + + return [ + 'admin' => $object->admin, + 'name' => $object->name, + 'email' => $v_email, + ]; + } + + private function recursive7(): bool + { + if ($this->recursive7 !== null) { + return $this->recursive7; + } + + $this->recursive7 = true; + + return $this->recursive7 = ($this->ie4 ? false : $this->n9 instanceof HydratorAwareNormalizer); + } + + public function fastHydrate8(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return ($this->h8)($data, $context, $object); + } + + return ($this->h8)($data, $context); + } + + public function fastHydrate9(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate9($data, $context, $object); + } + + return $this->hydrate9($data, $context); + } + + public function hydrate9(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r9->newInstanceWithoutConstructor(); + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n10->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n10::class, $e); + } + + try { + $object->status = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $e); + } + } + + if (\array_key_exists('dateTimeImmutable', $data)) { + try { + $value = $this->n11->denormalize($data['dateTimeImmutable'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n11::class, $e); + } + + try { + $object->dateTimeImmutable = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $e); + } + } + + if (\array_key_exists('dateTime', $data)) { + try { + $value = $this->n12->denormalize($data['dateTime'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n12::class, $e); + } + + try { + $object->dateTime = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $e); + } + } + + if (\array_key_exists('dateTimeZone', $data)) { + try { + $value = $this->n13->denormalize($data['dateTimeZone'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n13::class, $e); + } + + try { + $object->dateTimeZone = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $e); + } + } + + if (\array_key_exists('array', $data)) { + $value = $data['array']; + + try { + $object->array = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'array', $e); + } + } + + return $object; + } + + public function extract9(object $object, array $context): array + { + if ($this->tracked9) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class; + + try { + try { + $v_status = $this->n10->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n10::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n11->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n11::class, $e); + } + + try { + $v_dateTime = $this->n12->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n12::class, $e); + } + + try { + $v_dateTimeZone = $this->n13->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n13::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'array' => $object->array, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_status = $this->n10->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n10::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n11->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n11::class, $e); + } + + try { + $v_dateTime = $this->n12->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n12::class, $e); + } + + try { + $v_dateTimeZone = $this->n13->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n13::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'array' => $object->array, + ]; + } + + private function recursive9(): bool + { + if ($this->recursive9 !== null) { + return $this->recursive9; + } + + $this->recursive9 = true; + + return $this->recursive9 = $this->n10 instanceof HydratorAwareNormalizer + || $this->n11 instanceof HydratorAwareNormalizer + || $this->n12 instanceof HydratorAwareNormalizer + || $this->n13 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate10(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate10($data, $context, $object); + } + + return $this->hydrate10($data, $context); + } + + public function hydrate10(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r10->newInstanceWithoutConstructor(); + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n14->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n14::class, $e); + } + + try { + $object->status = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $e); + } + } + + if (\array_key_exists('dateTimeImmutable', $data)) { + try { + $value = $this->n15->denormalize($data['dateTimeImmutable'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n15::class, $e); + } + + try { + $object->dateTimeImmutable = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $e); + } + } + + if (\array_key_exists('dateTime', $data)) { + try { + $value = $this->n16->denormalize($data['dateTime'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n16::class, $e); + } + + try { + $object->dateTime = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $e); + } + } else { + $object->dateTime = NULL; + } + + if (\array_key_exists('dateTimeZone', $data)) { + try { + $value = $this->n17->denormalize($data['dateTimeZone'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n17::class, $e); + } + + try { + $object->dateTimeZone = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $e); + } + } else { + $object->dateTimeZone = NULL; + } + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n18->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n18::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $e); + } + } else { + $object->profileId = NULL; + } + + return $object; + } + + public function extract10(object $object, array $context): array + { + if ($this->tracked10) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class; + + try { + try { + $v_status = $this->n14->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n14::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n15->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n15::class, $e); + } + + try { + $v_dateTime = $this->n16->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n16::class, $e); + } + + try { + $v_dateTimeZone = $this->n17->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n17::class, $e); + } + + try { + $v_profileId = $this->n18->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n18::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'profileId' => $v_profileId, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_status = $this->n14->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n14::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n15->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n15::class, $e); + } + + try { + $v_dateTime = $this->n16->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n16::class, $e); + } + + try { + $v_dateTimeZone = $this->n17->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n17::class, $e); + } + + try { + $v_profileId = $this->n18->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n18::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'profileId' => $v_profileId, + ]; + } + + private function recursive10(): bool + { + if ($this->recursive10 !== null) { + return $this->recursive10; + } + + $this->recursive10 = true; + + return $this->recursive10 = $this->n14 instanceof HydratorAwareNormalizer + || $this->n15 instanceof HydratorAwareNormalizer + || $this->n16 instanceof HydratorAwareNormalizer + || $this->n17 instanceof HydratorAwareNormalizer + || $this->n18 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate11(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate11($data, $context, $object); + } + + return $this->hydrate11($data, $context); + } + + public function hydrate11(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r11->newInstanceWithoutConstructor(); + + if (\array_key_exists('defaultArray', $data)) { + try { + $value = $this->n19->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n19::class, $e); + } + + try { + $object->defaultArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $e); + } + } else { + $object->defaultArray = []; + } + + if (\array_key_exists('listArray', $data)) { + try { + $value = $this->n20->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n20::class, $e); + } + + try { + $object->listArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $e); + } + } else { + $object->listArray = []; + } + + if (\array_key_exists('iterableArray', $data)) { + try { + $value = $this->n21->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n21::class, $e); + } + + try { + $object->iterableArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $e); + } + } else { + $object->iterableArray = []; + } + + if (\array_key_exists('hashMap', $data)) { + try { + $value = $this->n22->denormalize($data['hashMap'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n22::class, $e); + } + + try { + $object->hashMap = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $e); + } + } else { + $object->hashMap = []; + } + + if (\array_key_exists('nested', $data)) { + try { + $value = $this->n23->denormalize($data['nested'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n23::class, $e); + } + + try { + $object->nested = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $e); + } + } else { + $object->nested = []; + } + + if (\array_key_exists('jsonArray', $data)) { + $value = $data['jsonArray']; + + try { + $object->jsonArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'jsonArray', $e); + } + } else { + $object->jsonArray = NULL; + } + + if (\array_key_exists('shapeArray', $data)) { + try { + $value = $this->n24->denormalize($data['shapeArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n24::class, $e); + } + + try { + $object->shapeArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $e); + } + } else { + $object->shapeArray = NULL; + } + + return $object; + } + + public function extract11(object $object, array $context): array + { + if ($this->tracked11) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class; + + try { + try { + $v_defaultArray = $this->n19->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n19::class, $e); + } + + try { + $v_listArray = $this->n20->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n20::class, $e); + } + + try { + $v_iterableArray = $this->n21->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n21::class, $e); + } + + try { + $v_hashMap = $this->n22->normalize($object->hashMap, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n22::class, $e); + } + + try { + $v_nested = $this->n23->normalize($object->nested, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n23::class, $e); + } + + try { + $v_shapeArray = $this->n24->normalize($object->shapeArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n24::class, $e); + } + + return [ + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'hashMap' => $v_hashMap, + 'nested' => $v_nested, + 'jsonArray' => $object->jsonArray, + 'shapeArray' => $v_shapeArray, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_defaultArray = $this->n19->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n19::class, $e); + } + + try { + $v_listArray = $this->n20->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n20::class, $e); + } + + try { + $v_iterableArray = $this->n21->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n21::class, $e); + } + + try { + $v_hashMap = $this->n22->normalize($object->hashMap, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n22::class, $e); + } + + try { + $v_nested = $this->n23->normalize($object->nested, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n23::class, $e); + } + + try { + $v_shapeArray = $this->n24->normalize($object->shapeArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n24::class, $e); + } + + return [ + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'hashMap' => $v_hashMap, + 'nested' => $v_nested, + 'jsonArray' => $object->jsonArray, + 'shapeArray' => $v_shapeArray, + ]; + } + + private function recursive11(): bool + { + if ($this->recursive11 !== null) { + return $this->recursive11; + } + + $this->recursive11 = true; + + return $this->recursive11 = $this->n19 instanceof HydratorAwareNormalizer + || $this->n20 instanceof HydratorAwareNormalizer + || $this->n21 instanceof HydratorAwareNormalizer + || $this->n22 instanceof HydratorAwareNormalizer + || $this->n23 instanceof HydratorAwareNormalizer + || $this->n24 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate12(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate12($data, $context, $object); + } + + return $this->hydrate12($data, $context); + } + + public function hydrate12(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r12->newInstanceWithoutConstructor(); + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n25->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n25::class, $e); + } + + try { + $object->status = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $e); + } + } + + if (\array_key_exists('profileCreated', $data)) { + try { + $value = $data['profileCreated']; + + if ($this->ih5 && \is_array($value)) { + $value = $this->hydrate13($value, $context); + } else { + $value = $this->n26->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n26::class, $e); + } + + try { + $object->profileCreated = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $e); + } + } + + if (\array_key_exists('defaultArray', $data)) { + try { + $value = $this->n29->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n29::class, $e); + } + + try { + $object->defaultArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $e); + } + } else { + $object->defaultArray = []; + } + + if (\array_key_exists('listArray', $data)) { + try { + $value = $this->n30->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n30::class, $e); + } + + try { + $object->listArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $e); + } + } else { + $object->listArray = []; + } + + if (\array_key_exists('iterableArray', $data)) { + try { + $value = $this->n31->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n31::class, $e); + } + + try { + $object->iterableArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $e); + } + } else { + $object->iterableArray = []; + } + + if (\array_key_exists('skillsHashMap', $data)) { + try { + $value = $data['skillsHashMap']; + + if ($this->ih6 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->hydrate14($item, $context) : $this->n33->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n32->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n32::class, $e); + } + + try { + $object->skillsHashMap = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $e); + } + } else { + $object->skillsHashMap = []; + } + + if (\array_key_exists('jsonArray', $data)) { + $value = $data['jsonArray']; + + try { + $object->jsonArray = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'jsonArray', $e); + } + } else { + $object->jsonArray = NULL; + } + + return $object; + } + + public function extract12(object $object, array $context): array + { + if ($this->tracked12) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class; + + try { + try { + $v_status = $this->n25->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n25::class, $e); + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie5 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->extract13($value, $context) : $this->n26->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n26::class, $e); + } + + try { + $v_defaultArray = $this->n29->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n29::class, $e); + } + + try { + $v_listArray = $this->n30->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n30::class, $e); + } + + try { + $v_iterableArray = $this->n31->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n31::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->ie6 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->extract14($item, $context) : $this->n33->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n32->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n32::class, $e); + } + + return [ + 'status' => $v_status, + 'profileCreated' => $v_profileCreated, + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'skillsHashMap' => $v_skillsHashMap, + 'jsonArray' => $object->jsonArray, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_status = $this->n25->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n25::class, $e); + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie5 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->extract13($value, $context) : $this->n26->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n26::class, $e); + } + + try { + $v_defaultArray = $this->n29->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n29::class, $e); + } + + try { + $v_listArray = $this->n30->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n30::class, $e); + } + + try { + $v_iterableArray = $this->n31->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n31::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->ie6 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->extract14($item, $context) : $this->n33->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n32->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n32::class, $e); + } + + return [ + 'status' => $v_status, + 'profileCreated' => $v_profileCreated, + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'skillsHashMap' => $v_skillsHashMap, + 'jsonArray' => $object->jsonArray, + ]; + } + + private function recursive12(): bool + { + if ($this->recursive12 !== null) { + return $this->recursive12; + } + + $this->recursive12 = true; + + return $this->recursive12 = $this->n25 instanceof HydratorAwareNormalizer + || ($this->ie5 ? $this->recursive13() : $this->n26 instanceof HydratorAwareNormalizer) + || $this->n29 instanceof HydratorAwareNormalizer + || $this->n30 instanceof HydratorAwareNormalizer + || $this->n31 instanceof HydratorAwareNormalizer + || ($this->ie6 ? false : $this->n32 instanceof HydratorAwareNormalizer); + } + + public function fastHydrate13(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate13($data, $context, $object); + } + + return $this->hydrate13($data, $context); + } + + public function hydrate13(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r13->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n27->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n27::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n28->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n28::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $e); + } + } + + return $object; + } + + public function extract13(object $object, array $context): array + { + if ($this->tracked13) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class; + + try { + try { + $v_profileId = $this->n27->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n27::class, $e); + } + + try { + $v_email = $this->n28->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n28::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_profileId = $this->n27->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'profileId', $this->n27::class, $e); + } + + try { + $v_email = $this->n28->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, 'email', $this->n28::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive13(): bool + { + if ($this->recursive13 !== null) { + return $this->recursive13; + } + + $this->recursive13 = true; + + return $this->recursive13 = $this->n27 instanceof HydratorAwareNormalizer + || $this->n28 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate14(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate14($data, $context, $object); + } + + return $this->hydrate14($data, $context); + } + + public function hydrate14(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r14->newInstanceWithoutConstructor(); + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, 'name', $e); + } + } + + return $object; + } + + public function extract14(object $object, array $context): array + { + + + return [ + 'name' => $object->name, + ]; + } + + public function fastHydrate15(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate15($data, $context, $object); + } + + return $this->hydrate15($data, $context); + } + + public function hydrate15(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r15->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n34->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $this->n34::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n35->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n35::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $e); + } + } + + return $object; + } + + public function extract15(object $object, array $context): array + { + if ($this->tracked15) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class; + + try { + try { + $v_profileId = $this->n34->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $this->n34::class, $e); + } + + try { + $v_email = $this->n35->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n35::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_profileId = $this->n34->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'profileId', $this->n34::class, $e); + } + + try { + $v_email = $this->n35->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n35::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive15(): bool + { + if ($this->recursive15 !== null) { + return $this->recursive15; + } + + $this->recursive15 = true; + + return $this->recursive15 = $this->n34 instanceof HydratorAwareNormalizer + || $this->n35 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate16(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate16($data, $context, $object); + } + + return $this->hydrate16($data, $context); + } + + public function hydrate16(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r16->newInstanceWithoutConstructor(); + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n36->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n36::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $e); + } + } + + return $object; + } + + public function extract16(object $object, array $context): array + { + if ($this->tracked16) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class; + + try { + try { + $v_email = $this->n36->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n36::class, $e); + } + + return [ + 'email' => $v_email, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_email = $this->n36->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n36::class, $e); + } + + return [ + 'email' => $v_email, + ]; + } + + private function recursive16(): bool + { + if ($this->recursive16 !== null) { + return $this->recursive16; + } + + $this->recursive16 = true; + + return $this->recursive16 = $this->n36 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate17(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate17($data, $context, $object); + } + + return $this->hydrate17($data, $context); + } + + public function hydrate17(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r17->newInstanceWithoutConstructor(); + + if (\array_key_exists('profileId', $data)) { + try { + $value = $this->n37->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n37::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $e); + } + } + + if (\array_key_exists('valueObject', $data)) { + try { + $value = $this->n38->denormalize($data['valueObject'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n38::class, $e); + } + + try { + $object->valueObject = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $e); + } + } + + return $object; + } + + public function extract17(object $object, array $context): array + { + if ($this->tracked17) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class; + + try { + try { + $v_profileId = $this->n37->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n37::class, $e); + } + + try { + $v_valueObject = $this->n38->normalize($object->valueObject, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n38::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'valueObject' => $v_valueObject, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_profileId = $this->n37->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n37::class, $e); + } + + try { + $v_valueObject = $this->n38->normalize($object->valueObject, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n38::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'valueObject' => $v_valueObject, + ]; + } + + private function recursive17(): bool + { + if ($this->recursive17 !== null) { + return $this->recursive17; + } + + $this->recursive17 = true; + + return $this->recursive17 = $this->n37 instanceof HydratorAwareNormalizer + || $this->n38 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate18(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return ($this->h18)($data, $context, $object); + } + + return ($this->h18)($data, $context); + } + + private function recursive18(): bool + { + if ($this->recursive18 !== null) { + return $this->recursive18; + } + + $this->recursive18 = true; + + return $this->recursive18 = $this->n39 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate19(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return ($this->h19)($data, $context, $object); + } + + return ($this->h19)($data, $context); + } + + private function recursive19(): bool + { + if ($this->recursive19 !== null) { + return $this->recursive19; + } + + $this->recursive19 = true; + + return $this->recursive19 = $this->n40 instanceof HydratorAwareNormalizer + || $this->n41 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate20(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate20($data, $context, $object); + } + + return $this->hydrate20($data, $context); + } + + public function hydrate20(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r20->newInstanceWithoutConstructor(); + + if (\array_key_exists('id', $data)) { + try { + $value = $this->n42->denormalize($data['id'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n42::class, $e); + } + + try { + $object->profileId = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $e); + } + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n43->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n43::class, $e); + } + + try { + $object->email = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $e); + } + } + + return $object; + } + + public function extract20(object $object, array $context): array + { + if ($this->tracked20) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class; + + try { + try { + $v_profileId = $this->n42->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n42::class, $e); + } + + try { + $v_email = $this->n43->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n43::class, $e); + } + + return [ + 'id' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $v_profileId = $this->n42->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n42::class, $e); + } + + try { + $v_email = $this->n43->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n43::class, $e); + } + + return [ + 'id' => $v_profileId, + 'email' => $v_email, + ]; + } + + private function recursive20(): bool + { + if ($this->recursive20 !== null) { + return $this->recursive20; + } + + $this->recursive20 = true; + + return $this->recursive20 = $this->n42 instanceof HydratorAwareNormalizer + || $this->n43 instanceof HydratorAwareNormalizer; + } + + public function fastHydrate21(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate21($data, $context, $object); + } + + return $this->hydrate21($data, $context); + } + + public function hydrate21(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r21->newInstanceWithoutConstructor(); + + if (\array_key_exists('child', $data)) { + try { + $value = $data['child']; + + if ($this->ih7 && \is_array($value)) { + $value = $this->hydrate22($value, $context); + } else { + $value = $this->n44->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n44::class, $e); + } + + try { + $object->child = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $e); + } + } + + if (\array_key_exists('items', $data)) { + try { + $value = $data['items']; + + if ($this->ih8 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->hydrate22($item, $context) : $this->n46->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n45->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n45::class, $e); + } + + try { + $object->items = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $e); + } + } else { + $object->items = []; + } + + return $object; + } + + public function extract21(object $object, array $context): array + { + if ($this->tracked21) { + $id = \spl_object_id($object); + + if (isset($this->callStack[$id])) { + $references = \array_values($this->callStack); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class; + + try { + try { + $value = $object->child; + $v_child = $this->ie7 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract22($value, $context) : $this->n44->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n44::class, $e); + } + + try { + $value = $object->items; + + if ($this->ie8 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract22($item, $context) : $this->n46->normalize($item, $context); + } + + $v_items = $items; + } else { + $v_items = $this->n45->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n45::class, $e); + } + + return [ + 'child' => $v_child, + 'items' => $v_items, + ]; + } finally { + unset($this->callStack[$id]); + } + } + + try { + $value = $object->child; + $v_child = $this->ie7 && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract22($value, $context) : $this->n44->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n44::class, $e); + } + + try { + $value = $object->items; + + if ($this->ie8 && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->extract22($item, $context) : $this->n46->normalize($item, $context); + } + + $v_items = $items; + } else { + $v_items = $this->n45->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n45::class, $e); + } + + return [ + 'child' => $v_child, + 'items' => $v_items, + ]; + } + + private function recursive21(): bool + { + if ($this->recursive21 !== null) { + return $this->recursive21; + } + + $this->recursive21 = true; + + return $this->recursive21 = ($this->ie7 ? false : $this->n44 instanceof HydratorAwareNormalizer) + || ($this->ie8 ? false : $this->n45 instanceof HydratorAwareNormalizer); + } + + public function fastHydrate22(array $data, array $context): object + { + if (\array_key_exists(Hydrator::OBJECT_TO_POPULATE, $context)) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + + return $this->hydrate22($data, $context, $object); + } + + return $this->hydrate22($data, $context); + } + + public function hydrate22(array $data, array $context, object|null $object = null): object + { + $object ??= $this->r22->newInstanceWithoutConstructor(); + + if (\array_key_exists('name', $data)) { + $value = $data['name']; + + try { + $object->name = $value; + } catch (TypeError $e) { + throw new TypeMismatch(\Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, 'name', $e); + } + } + + return $object; + } + + public function extract22(object $object, array $context): array + { + + + return [ + 'name' => $object->name, + ]; + } + + private function hydrator(): StackHydrator + { + return $this->hydrator ?? throw new HydratorNotSet(self::class); + } + + /** @param array $expected property name => [field name, has normalizer] */ + private function metadata(string $class, array $expected): ClassMetadata + { + $metadata = $this->hydrator()->metadata($class); + $actual = []; + + foreach ($metadata->properties as $property) { + $actual[$property->propertyName] = [$property->fieldName, $property->normalizer !== null]; + } + + if ($actual !== $expected) { + throw new OutdatedGeneratedMiddleware($class); + } + + return $metadata; + } + + private function normalizer(ClassMetadata $metadata, string $property): Normalizer + { + return $metadata->properties[$property]->normalizer ?? throw new OutdatedGeneratedMiddleware($metadata->className); + } + + private function promotedDefault(ClassMetadata $metadata, string $property): ReflectionParameter + { + return $metadata->promotedConstructorDefaults()[$property] ?? throw new OutdatedGeneratedMiddleware($metadata->className); + } + + private function inlineObject(Normalizer $normalizer, string $class, Skip $direction): bool + { + if (!$normalizer instanceof ObjectNormalizer) { + return false; + } + + try { + if ($normalizer->className() !== $class) { + return false; + } + } catch (Throwable) { + return false; + } + + return $this->inlinable($class, $direction); + } + + private function inlineArray(Normalizer $normalizer, string $class, Skip $direction): bool + { + return $normalizer instanceof ArrayNormalizer && $this->inlineObject($normalizer->innerNormalizer(), $class, $direction); + } + + /** + * Nested objects are only inlined when this does not change the behaviour: the nested class must be handled + * by this middleware, must not be lazy and every other middleware must skip the class in this direction. + */ + private function inlinable(string $class, Skip $direction): bool + { + try { + $metadata = $this->hydrator()->metadata($class); + } catch (Throwable) { + return false; + } + + if ($metadata->normalizer !== null) { + return false; + } + + if (\PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->hydrator()->defaultLazy())) { + return false; + } + + return $this->exclusive($metadata, $direction); + } + + /** Whether every other middleware skips the class in this direction, so this middleware is the only one to run. */ + private function exclusive(ClassMetadata $metadata, Skip $direction): bool + { + foreach ($this->others as $middleware) { + if (!$middleware instanceof SkippableMiddleware) { + return false; + } + + $skip = $middleware->skip($metadata); + + if ($skip !== Skip::Both && $skip !== $direction) { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Unit/Fixture/AsymmetricVisibilityDto.php b/tests/Unit/Fixture/AsymmetricVisibilityDto.php new file mode 100644 index 00000000..4d072150 --- /dev/null +++ b/tests/Unit/Fixture/AsymmetricVisibilityDto.php @@ -0,0 +1,16 @@ + $skip */ + public function __construct( + private readonly array $skip = [], + ) { + } + + /** @var array */ + public array $hydrated = []; + + /** @var array */ + public array $extracted = []; + + /** + * @param ClassMetadata $metadata + * @param array $data + * @param array $context + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object + { + $this->hydrated[$metadata->className] = ($this->hydrated[$metadata->className] ?? 0) + 1; + + return $stack->next()->hydrate($metadata, $data, $context, $stack); + } + + /** + * @param ClassMetadata $metadata + * @param T $object + * @param array $context + * + * @return array + * + * @template T of object + */ + public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array + { + $this->extracted[$metadata->className] = ($this->extracted[$metadata->className] ?? 0) + 1; + + return $stack->next()->extract($metadata, $object, $context, $stack); + } + + /** + * @param ClassMetadata $metadata + * + * @template T of object + */ + public function skip(ClassMetadata $metadata): Skip + { + return $this->skip[$metadata->className] ?? Skip::None; + } +} diff --git a/tests/Unit/Fixture/NestedLifecycleDto.php b/tests/Unit/Fixture/NestedLifecycleDto.php new file mode 100644 index 00000000..994f5d5a --- /dev/null +++ b/tests/Unit/Fixture/NestedLifecycleDto.php @@ -0,0 +1,15 @@ + $items */ + public function __construct( + public LifecycleFixture $child, + public array $items = [], + ) { + } +} diff --git a/tests/Unit/Fixture/PrivateChildDto.php b/tests/Unit/Fixture/PrivateChildDto.php new file mode 100644 index 00000000..6e3cd349 --- /dev/null +++ b/tests/Unit/Fixture/PrivateChildDto.php @@ -0,0 +1,23 @@ +name; + } +} diff --git a/tests/Unit/Fixture/PrivateParentDto.php b/tests/Unit/Fixture/PrivateParentDto.php new file mode 100644 index 00000000..fa2e6d3d --- /dev/null +++ b/tests/Unit/Fixture/PrivateParentDto.php @@ -0,0 +1,25 @@ +email; + } + + public function note(): string + { + return $this->note; + } +} diff --git a/tests/Unit/Fixture/PrivatePropertiesDto.php b/tests/Unit/Fixture/PrivatePropertiesDto.php new file mode 100644 index 00000000..8f0a735b --- /dev/null +++ b/tests/Unit/Fixture/PrivatePropertiesDto.php @@ -0,0 +1,31 @@ +profileId; + } + + public function name(): string + { + return $this->name; + } + + public function age(): int + { + return $this->age; + } +} diff --git a/tests/Unit/StackHydratorBuilderTest.php b/tests/Unit/StackHydratorBuilderTest.php index 274d8dbe..1842c154 100644 --- a/tests/Unit/StackHydratorBuilderTest.php +++ b/tests/Unit/StackHydratorBuilderTest.php @@ -7,9 +7,11 @@ use Patchlevel\Hydrator\Extension; use Patchlevel\Hydrator\Guesser\ChainGuesser; use Patchlevel\Hydrator\Guesser\Guesser; +use Patchlevel\Hydrator\HydratorFactoryAlreadySet; use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory; use Patchlevel\Hydrator\Metadata\EnrichingMetadataFactory; use Patchlevel\Hydrator\Metadata\MetadataEnricher; +use Patchlevel\Hydrator\Metadata\MetadataFactory; use Patchlevel\Hydrator\Metadata\Psr16MetadataFactory; use Patchlevel\Hydrator\Metadata\Psr6MetadataFactory; use Patchlevel\Hydrator\Middleware\Middleware; @@ -128,6 +130,37 @@ public function testUseExtension(): void $builder->useExtension($extension); } + public function testHydratorFactory(): void + { + $subclass = new class (new AttributeMetadataFactory(), [$this->createMock(Middleware::class)]) extends StackHydrator { + }; + + $builder = new StackHydratorBuilder(); + $builder->setHydratorFactory( + static function (MetadataFactory $metadataFactory, array $middlewares, bool $defaultLazy) use ($subclass): StackHydrator { + self::assertInstanceOf(EnrichingMetadataFactory::class, $metadataFactory); + self::assertCount(1, $middlewares); + self::assertTrue($defaultLazy); + + return $subclass; + }, + ); + $builder->addMiddleware($this->createMock(Middleware::class)); + $builder->enableDefaultLazy(); + + self::assertSame($subclass, $builder->build()); + } + + public function testHydratorFactoryCanOnlyBeSetOnce(): void + { + $builder = new StackHydratorBuilder(); + $builder->setHydratorFactory(static fn (MetadataFactory $metadataFactory, array $middlewares, bool $defaultLazy): StackHydrator => new StackHydrator($metadataFactory, $middlewares, $defaultLazy)); + + $this->expectException(HydratorFactoryAlreadySet::class); + + $builder->setHydratorFactory(static fn (MetadataFactory $metadataFactory, array $middlewares, bool $defaultLazy): StackHydrator => new StackHydrator($metadataFactory, $middlewares, $defaultLazy)); + } + public function testCachePsr6(): void { $cache = $this->createMock(CacheItemPoolInterface::class);