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..f480c35a 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 snapshots of the generated transformers + 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 4c47de30..e91954ce 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -23,13 +23,14 @@ $hydrator = (new StackHydratorBuilder()) ``` ## Built-in extensions -The library ships with three extensions out of the box: +The library ships with four extensions out of the box: | Extension | Purpose | | --- | --- | | `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. | +| `GeneratedTransformerExtension` | [Generated code](generated-code.md), use generated mapping code instead of reflection. | ## Middleware @@ -162,10 +163,11 @@ transformer for a class, for example generated code. use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Transformer\ClassTransformer; use Patchlevel\Hydrator\Transformer\ClassTransformerFactory; +use Patchlevel\Hydrator\Transformer\TransformerResolver; final class MoneyTransformerFactory implements ClassTransformerFactory { - public function create(ClassMetadata $metadata): ClassTransformer|null + public function create(ClassMetadata $metadata, TransformerResolver $resolver): ClassTransformer|null { if ($metadata->className !== Money::class) { return null; @@ -184,6 +186,16 @@ higher priority is asked first. The `ReflectionTransformerFactory` is always asked last, so every class without its own transformer is transformed with reflection. +The `TransformerResolver` gives the transformer insight into the hydrator which +created it. With `direct()` it asks whether the hydrator would call the +transformer of a nested class directly, without a middleware, a class +normalizer or a lazy proxy. Only then a transformer may map nested objects in +place, like the [generated transformers](generated-code.md) do. +:::warning +Use the resolver once the transformer is used, not inside `create()`. The +hydrator caches the transformer only after `create()` returned. +::: + ## Writing your own extension An extension implements the `Extension` interface and configures the builder. @@ -207,4 +219,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..3118a528 --- /dev/null +++ b/docs/generated-code.md @@ -0,0 +1,108 @@ +# Generated Code + +By default the hydrator maps arrays to objects with reflection: the +`ReflectionTransformer` 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 +`GeneratedTransformerExtension` uses plain PHP code generated for your classes +instead. + +The generation and the runtime are strictly separated: a warmup step generates +the code ahead of time, and the hydrator only loads it. + +## Enable the extension + +Register the `GeneratedTransformerExtension` with the directory which contains +the generated code. + +```php +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; +use Patchlevel\Hydrator\StackHydratorBuilder; + +$hydrator = (new StackHydratorBuilder()) + ->useExtension(new GeneratedTransformerExtension(__DIR__ . '/var/cache/hydrator')) + ->build(); +``` +For every class the hydrator looks for a generated transformer in this +directory. Classes without one are transformed with reflection as usual. + +:::success +The generated code behaves exactly like the `ReflectionTransformer`: 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. +::: + +## Warm up + +The `GeneratedTransformerWarmer` generates the transformers and writes them to +the directory. Run it in a cache warmer or a deployment step, with the metadata +factory of the builder, so the code matches the metadata of the hydrator, +including the guessers and enrichers of all registered extensions. + +```php +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerWarmer; +use Patchlevel\Hydrator\StackHydratorBuilder; + +$cachePath = __DIR__ . '/var/cache/hydrator'; + +$builder = (new StackHydratorBuilder()) + ->useExtension(new GeneratedTransformerExtension($cachePath)); + +$warmer = new GeneratedTransformerWarmer($builder->metadataFactory(), $cachePath); +$warmer->warmup([ + ProfileCreated::class, + NameChanged::class, +]); +``` +Classes referenced by these classes, such as nested value objects, get a +transformer too. The warmup throws a `ClassNotGeneratable` exception for a +class which can not be generated, for example an abstract class or a class with +a class normalizer. + +:::note +Only the warmer knows how to generate code. At runtime the extension only loads +existing files, the generator is never loaded. +::: + +## Changed classes + +The name of a generated file contains a fingerprint of everything the code +depends on: the properties with their field names, normalizers, visibility and +default values. If a class changes after the warmup, its generated transformer +is simply not found and the class is transformed with reflection until you warm +up again. + +:::warning +A missing warmup does not fail, it only makes the hydration slower. Warm up the +transformers on every deployment, like you do for other generated files. +::: + +## Nested objects + +The biggest speedup comes from mapping nested objects in place: instead of +calling the hydrator again for every nested object, the generated code calls +the generated transformer of the nested class directly. This only happens if it +does not change the behaviour, the hydrator must call the transformer of the +nested class directly too, so the nested class has no class normalizer, is not +[lazy](lazy.md) and no [middleware](extensions.md) runs for it. + +The decision is made per class and per direction: with the +[cryptography](cryptography.md) extension for example, value objects without +sensitive data are mapped in place while classes with sensitive data still go +through the middleware. + +:::note +If a hydrator wraps the `StackHydrator`, nested objects are not mapped in +place for its calls, so it sees every nested object, see +[normalizers](normalizer.md). +::: + +## 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 409fb697..4df70d54 100644 --- a/docs/hydrator.md +++ b/docs/hydrator.md @@ -180,3 +180,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 1181fb49..44c77b88 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-baseline.neon b/phpstan-baseline.neon index a7f097ea..e5e89c19 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -126,6 +126,12 @@ parameters: count: 1 path: src/StackHydrator.php + - + message: '#^Parameter \#1 \$data of method Patchlevel\\Hydrator\\Transformer\\ClassTransformer\:\:hydrate\(\) expects array\, array\ given\.$#' + identifier: argument.type + count: 2 + path: src/StackHydrator.php + - message: '#^Parameter \#2 \$data of method Patchlevel\\Hydrator\\StackHydrator\:\:hydrateWithMiddlewares\(\) expects array\, array\ given\.$#' identifier: argument.type diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 81e9ded9..7a209d3d 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: @@ -19,3 +21,7 @@ services: class: Patchlevel\Hydrator\Tests\Architecture\ExceptionImplementsHydratorExceptionTest tags: - phpat.test + - + class: Patchlevel\Hydrator\Tests\Architecture\NoCodeGenerationAtRuntimeTest + tags: + - phpat.test diff --git a/src/Extension/Generated/ClassFingerprint.php b/src/Extension/Generated/ClassFingerprint.php new file mode 100644 index 00000000..548f76c1 --- /dev/null +++ b/src/Extension/Generated/ClassFingerprint.php @@ -0,0 +1,104 @@ + $metadata + * + * @template T of object + */ + public static function of(ClassMetadata $metadata): string + { + $defaults = $metadata->promotedConstructorDefaults(); + $shape = [$metadata->reflection->isFinal()]; + + foreach ($metadata->properties as $property) { + $reflection = $property->reflection; + $default = null; + + if ($reflection->isPromoted() && isset($defaults[$property->propertyName])) { + // defaults which can not be inlined are resolved at runtime, their value does not matter here + $default = Literal::export($defaults[$property->propertyName]->getDefaultValue()) ?? 'runtime'; + } + + $declaringClass = $reflection->getDeclaringClass()->getName(); + $nested = null; + + if ($property->normalizer !== null) { + $nested = self::nested($property->normalizer); + } + + $shape[] = [ + $property->propertyName, + $property->fieldName, + $property->normalizer !== null, + $reflection->getModifiers(), + // inherited properties may need the scope of their declaring class + $declaringClass === $metadata->className ? null : $declaringClass, + $default, + $nested, + ]; + } + + return substr(hash('sha256', serialize($shape)), 0, 16); + } + + /** + * Nested objects are mapped in place if they are normalized with an object normalizer, directly or in an array. + * + * @return array{class: class-string, array: bool, final: bool}|null + */ + public static function nested(Normalizer $normalizer): array|null + { + $array = false; + + if ($normalizer instanceof ArrayNormalizer) { + $array = true; + $normalizer = $normalizer->innerNormalizer(); + } + + if (!$normalizer instanceof ObjectNormalizer) { + return null; + } + + try { + $class = $normalizer->className(); + } catch (InvalidType) { + return null; + } + + if (!class_exists($class)) { + return null; + } + + return [ + 'class' => $class, + 'array' => $array, + // the generated code checks the exact class of nested objects, which is cheaper for final classes + 'final' => (new ReflectionClass($class))->isFinal(), + ]; + } +} diff --git a/src/Extension/Generated/ClassNotGeneratable.php b/src/Extension/Generated/ClassNotGeneratable.php new file mode 100644 index 00000000..8b4f821c --- /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 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; + } + + /** @return array slot => nested class whose objects may be mapped in place */ + public function nested(): array + { + $nested = []; + + foreach ($this->properties as $property) { + if ($property->nested === null) { + continue; + } + + $nested[$property->nested->slot] = $property->nested; + } + + return $nested; + } +} diff --git a/src/Extension/Generated/ClassPlanner.php b/src/Extension/Generated/ClassPlanner.php new file mode 100644 index 00000000..b0cce904 --- /dev/null +++ b/src/Extension/Generated/ClassPlanner.php @@ -0,0 +1,170 @@ + nested class => slot of its transformer */ + private array $nestedSlots = []; + + /** @param ClassMetadata $metadata */ + public function plan(ClassMetadata $metadata): ClassPlan + { + $properties = []; + + foreach ($metadata->properties as $property) { + $properties[] = $this->property($metadata, $property); + } + + [$scopedHydrate, $scopedExtract] = self::scoped($metadata, $properties); + + return new ClassPlan($metadata, $properties, $scopedHydrate, $scopedExtract); + } + + /** + * Constructor visibility does not matter, the generated code runs in the scope of the class. Anonymous classes + * have no stable name the generated code could refer to. + * + * @param ClassMetadata $metadata + */ + public static function generatable(ClassMetadata $metadata): bool + { + $reflection = $metadata->reflection; + + return !$reflection->isInternal() + && !$reflection->isAbstract() + && !$reflection->isInterface() + && !$reflection->isEnum() + && !$reflection->isAnonymous(); + } + + /** @param ClassMetadata $metadata */ + private function property(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 = ClassFingerprint::nested($normalizer); + + if ($nested !== null) { + $kind = $nested['array'] ? ValueKind::NestedArray : ValueKind::NestedObject; + $flag = $this->flagSlots++; + $this->nestedSlots[$nested['class']] ??= count($this->nestedSlots); + + if ($normalizer instanceof ArrayNormalizer) { + $inner = $this->normalizerSlots++; + } + } + } + + 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 = Literal::export($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 === null ? null : new NestedPlan( + $nested['class'], + $this->nestedSlots[$nested['class']], + $nested['final'], + ), + default: $default, + defaultSlot: $defaultSlot, + hydrateScope: $inherited && ($reflection->isPrivate() || $reflection->isReadOnly() || self::privateSet($reflection)) ? $declaringClass : null, + extractScope: $inherited && $reflection->isPrivate() ? $declaringClass : null, + ); + } + + /** + * Code runs in the scope of the class (a bound closure) only if needed, otherwise in the methods of the transformer. + * + * @param ClassMetadata $metadata + * @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]; + } + + /** 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(); + } +} diff --git a/src/Extension/Generated/CodeEmitter.php b/src/Extension/Generated/CodeEmitter.php new file mode 100644 index 00000000..07d436d6 --- /dev/null +++ b/src/Extension/Generated/CodeEmitter.php @@ -0,0 +1,408 @@ + property declarations of the generated class */ + private array $properties = []; + + private int $helperSlots = 0; + + public function __construct( + private readonly ClassPlan $plan, + ) { + } + + public function emit(string $namespace, string $className): string + { + $plan = $this->plan; + $initialize = []; + + foreach ($plan->properties as $property) { + if ($property->defaultSlot !== null) { + $this->properties[] = sprintf('public ReflectionParameter $d%d;', $property->defaultSlot); + $initialize[] = sprintf('$this->d%d = $this->promotedDefault(%s);', $property->defaultSlot, var_export($property->name, true)); + } + + if ($property->slot === null) { + continue; + } + + $this->properties[] = sprintf('public Normalizer $n%d;', $property->slot); + $initialize[] = sprintf('$this->n%d = $this->normalizer(%s);', $property->slot, var_export($property->name, true)); + + if ($property->nested === null || $property->flag === null) { + continue; + } + + $flag = $property->flag; + $nested = $property->nested; + + $this->properties[] = sprintf('public bool $ih%d = false;', $flag); + $this->properties[] = sprintf('public bool $ie%d = false;', $flag); + $initialize[] = sprintf('$h%d = $this->nested($this->n%d, \\%s::class, Direction::Hydrate);', $flag, $property->slot, $nested->class); + $initialize[] = sprintf('$e%d = $this->nested($this->n%d, \\%s::class, Direction::Extract);', $flag, $property->slot, $nested->class); + $initialize[] = sprintf('$this->ih%1$d = $h%1$d !== null;', $flag); + $initialize[] = sprintf('$this->ie%1$d = $e%1$d !== null;', $flag); + $initialize[] = sprintf('if ($this->ih%1$d || $this->ie%1$d) { $this->x%2$d = $h%1$d ?? $e%1$d; }', $flag, $nested->slot); + + if ($property->inner === null) { + continue; + } + + $this->properties[] = sprintf('public ObjectNormalizer $n%d;', $property->inner); + $initialize[] = sprintf('if ($this->ih%1$d || $this->ie%1$d) { $this->n%2$d = $this->n%3$d->innerNormalizer(); }', $flag, $property->inner, $property->slot); + } + + foreach ($plan->nested() as $slot => $nested) { + $this->properties[] = sprintf('public GeneratedTransformer $x%d;', $slot); + } + + [$hydrate, $hydrateHelpers] = $this->hydrateBody(); + [$extract, $extractHelpers] = $this->extractBody(); + + foreach ([...$hydrateHelpers, ...$extractHelpers] as $helper) { + $initialize[] = $helper; + } + + if ($plan->scopedHydrate) { + $this->properties[] = 'public Closure $h;'; + $initialize[] = Templates::render(Templates::CLOSURE, [ + 'slot' => 'h', + 'signature' => 'object $object, array $data, array $context, bool $inline', + 'return' => 'object', + 'body' => $hydrate, + 'class' => $plan->className(), + ]); + $hydrate = 'return ($this->h)($object, $data, $context, $inline);'; + } + + if ($plan->scopedExtract) { + $this->properties[] = 'public Closure $e;'; + $initialize[] = Templates::render(Templates::CLOSURE, [ + 'slot' => 'e', + 'signature' => 'object $object, array $context, bool $inline', + 'return' => 'array', + 'body' => $extract, + 'class' => $plan->className(), + ]); + $extract = 'return ($this->e)($object, $context, $inline);'; + } + + foreach ($plan->nested() as $slot => $nested) { + // the nested transformers are initialized last, they may lead back to this one + $initialize[] = sprintf('if (isset($this->x%1$d)) { $this->x%1$d->init(); }', $slot); + } + + $code = Templates::render(Templates::TRANSFORMER, [ + 'namespace' => $namespace, + 'className' => $className, + 'class' => $plan->className(), + 'version' => (string)TransformerFiles::VERSION, + 'properties' => implode("\n", $this->properties), + // the entry points of the hydrator check whether nested objects can be mapped in place, the nested entry + // points are only called if they can + 'hydrate' => self::withInline($hydrate, $plan->nested() === [] ? 'false' : Templates::OWNER), + 'extract' => self::withInline($extract, $plan->nested() === [] ? 'false' : Templates::OWNER), + 'hydrateNested' => self::withInline($hydrate, 'true'), + 'extractNested' => self::withInline($extract, 'true'), + 'initialize' => $initialize === [] ? '// nothing to resolve' : implode("\n", $initialize), + 'recursive' => $this->recursive(), + ]); + + // empty placeholders leave blank lines behind + $code = preg_replace(["/\n{3,}/", "/{\n\n/"], ["\n\n", "{\n"], $code); + assert($code !== null); + + return $code; + } + + /** Code without nested objects does not use $inline, the code of scoped classes gets it as argument. */ + private static function withInline(string $code, string $inline): string + { + if (!str_contains($code, '$inline')) { + return $code; + } + + return sprintf("\$inline = %s;\n\n%s", $inline, $code); + } + + /** + * Circular references are only possible through normalizers which extract objects with the hydrator. A class + * whose normalizers can not reach the class itself again does not need the (expensive) call stack tracking. + */ + private function recursive(): string + { + $checks = []; + + foreach ($this->plan->properties as $property) { + if ($property->kind === ValueKind::Raw) { + continue; + } + + if ($property->nested === null) { + $checks[] = sprintf('self::mayRecurse($this->n%d)', $property->slot); + + continue; + } + + $checks[] = sprintf( + '($this->ie%d ? $this->x%d->recursive() : self::mayRecurse($this->n%d))', + $property->flag, + $property->nested->slot, + $property->slot, + ); + } + + return $checks === [] ? 'false' : implode("\n || ", $checks); + } + + /** + * 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(): array + { + $main = []; + $scoped = []; + + foreach ($this->plan->properties as $property) { + if ($property->hydrateScope !== null) { + $scoped[$property->hydrateScope][] = $this->assignment($property); + + continue; + } + + $main[] = $this->assignment($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, $inline);', $slot); + } + + $main[] = 'return $object;'; + + return [implode("\n\n", $main), $helpers]; + } + + private function assignment(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' => $this->plan->className(), + 'value' => $this->denormalize($property, '$value'), + 'default' => $property->default ?? sprintf('$this->d%d->getDefaultValue()', $property->defaultSlot), + ]); + } + + /** Code which denormalizes the field into the given variable. */ + private function denormalize(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->nestedCall($property, 'hydrateNested'), + ]), + 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->nestedCall($property, 'hydrateNested'), + ]), + }; + + return Templates::render(Templates::DENORMALIZE, [ + 'inner' => $inner, + 'class' => $this->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(): array + { + $pre = []; + $scoped = []; + $fields = []; + + foreach ($this->plan->properties as $property) { + $field = var_export($property->field, true); + [$code, $expression] = $this->normalize($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, $inline);', implode(', ', $variables), $slot)); + } + + $variables = [ + 'class' => $this->plan->className(), + 'pre' => implode("\n\n", $pre), + 'fields' => implode("\n", $fields), + ]; + + if ($this->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(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->nestedCall($property, 'extractNested'), + 'check' => self::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->nestedCall($property, 'extractNested'), + 'check' => self::instanceCheck('$item', $property), + ]), + }; + + $code = Templates::render(Templates::NORMALIZE, [ + 'inner' => $inner, + 'class' => $this->plan->className(), + 'property' => var_export($name, true), + 'slot' => (string)$property->slot, + ]); + + return [$code, $variable]; + } + + /** Only exact instances are mapped in place, subclasses take the generic path through the hydrator. */ + private static function instanceCheck(string $variable, PropertyPlan $property): string + { + $nested = $property->nested; + assert($nested !== null); + + if ($nested->final) { + return sprintf('%s instanceof \\%s', $variable, $nested->class); + } + + return sprintf('%1$s instanceof \\%2$s && %1$s::class === \\%2$s::class', $variable, $nested->class); + } + + private function nestedCall(PropertyPlan $property, string $method): string + { + assert($property->nested !== null); + + return sprintf('$this->x%d->%s', $property->nested->slot, $method); + } +} diff --git a/src/Extension/Generated/GeneratedCode.php b/src/Extension/Generated/GeneratedCode.php new file mode 100644 index 00000000..b7e44531 --- /dev/null +++ b/src/Extension/Generated/GeneratedCode.php @@ -0,0 +1,19 @@ + $nested classes whose objects may be mapped in place, they need a transformer too + */ + public function __construct( + public readonly string $code, + public readonly array $nested, + ) { + } +} diff --git a/src/Extension/Generated/GeneratedTransformer.php b/src/Extension/Generated/GeneratedTransformer.php new file mode 100644 index 00000000..2555b7b7 --- /dev/null +++ b/src/Extension/Generated/GeneratedTransformer.php @@ -0,0 +1,175 @@ + */ + public ReflectionClass $reflection; + + /** @param ClassMetadata $metadata */ + public function __construct( + public readonly ClassMetadata $metadata, + public readonly TransformerResolver $resolver, + public readonly CallStack $callStack, + ) { + } + + /** + * Hydrates a nested object in place. Called by an initialized transformer for calls from the owner, without an + * object to populate. + * + * @param array $data + * @param array $context + */ + abstract public function hydrateNested(array $data, array $context): object; + + /** + * Extracts a nested object in place, see {@see self::hydrateNested()}. + * + * @param array $context + * + * @return array + */ + abstract public function extractNested(object $object, array $context): array; + + final public function init(): void + { + if ($this->ready || $this->initializing) { + return; + } + + // ready is only set at the end, so a failed initialization runs again on the next call instead of leaving + // a half initialized transformer behind. initializing stops the recursion between nested classes meanwhile. + $this->initializing = true; + + try { + $this->owner = $this->resolver->hydrator(); + $this->reflection = $this->metadata->reflection; + $this->initialize(); + $this->tracked = $this->recursive(); + $this->ready = true; + } finally { + $this->initializing = false; + } + } + + /** Whether an extracted object can lead back to an object of this class, see {@see self::$tracked}. */ + final public function recursive(): bool + { + if ($this->recursive !== null) { + return $this->recursive; + } + + // cycles are treated as recursive + $this->recursive = true; + + return $this->recursive = $this->mayReachItself(); + } + + /** Resolves the normalizers and the nested transformers. */ + abstract protected function initialize(): void; + + abstract protected function mayReachItself(): bool; + + final protected function normalizer(string $property): Normalizer + { + return $this->metadata->properties[$property]->normalizer + ?? throw new OutdatedGeneratedTransformer($this->metadata->className); + } + + final protected function promotedDefault(string $property): ReflectionParameter + { + return $this->metadata->promotedConstructorDefaults()[$property] + ?? throw new OutdatedGeneratedTransformer($this->metadata->className); + } + + /** + * The generated transformer of a nested class, if nested objects can be mapped in place: the hydrator must call + * the generated transformer of the nested class directly, without a middleware, a class normalizer or a lazy proxy. + * + * @param class-string $class + */ + final protected function nested(Normalizer $normalizer, string $class, Direction $direction): GeneratedTransformer|null + { + if ($normalizer instanceof ArrayNormalizer) { + $normalizer = $normalizer->innerNormalizer(); + } + + if (!$normalizer instanceof ObjectNormalizer) { + return null; + } + + try { + if ($normalizer->className() !== $class) { + return null; + } + + $transformer = $this->resolver->direct($class, $direction); + } catch (Throwable) { + return null; + } + + // initialized by the caller once all its normalizers are resolved, the nested class may lead back to it + return $transformer instanceof self ? $transformer : null; + } + + /** Whether the normalizer extracts objects with the hydrator, which can lead back to the same object. */ + final protected static function mayRecurse(Normalizer $normalizer): bool + { + if ($normalizer instanceof ArrayNormalizer) { + return self::mayRecurse($normalizer->innerNormalizer()); + } + + if ($normalizer instanceof ArrayShapeNormalizer) { + foreach ($normalizer->innerNormalizers() as $inner) { + if (self::mayRecurse($inner)) { + return true; + } + } + + return false; + } + + return $normalizer instanceof ObjectNormalizer || $normalizer instanceof ObjectMapNormalizer; + } +} diff --git a/src/Extension/Generated/GeneratedTransformerExtension.php b/src/Extension/Generated/GeneratedTransformerExtension.php new file mode 100644 index 00000000..cfe7ce7b --- /dev/null +++ b/src/Extension/Generated/GeneratedTransformerExtension.php @@ -0,0 +1,26 @@ +addTransformerFactory(new GeneratedTransformerFactory(new TransformerFiles($this->cachePath))); + } +} diff --git a/src/Extension/Generated/GeneratedTransformerFactory.php b/src/Extension/Generated/GeneratedTransformerFactory.php new file mode 100644 index 00000000..00f8f673 --- /dev/null +++ b/src/Extension/Generated/GeneratedTransformerFactory.php @@ -0,0 +1,56 @@ + the generated transformers of a hydrator share their call stack */ + private WeakMap $callStacks; + + public function __construct( + private readonly TransformerFiles $files, + ) { + $this->callStacks = new WeakMap(); + } + + public function create(ClassMetadata $metadata, TransformerResolver $resolver): ClassTransformer|null + { + $className = $this->files->className($metadata); + $fqcn = $this->files->fqcn($className); + + if (!class_exists($fqcn, false)) { + $file = $this->files->path($className); + + if (!is_file($file)) { + return null; + } + + require_once $file; + } + + $transformer = new $fqcn($metadata, $resolver, $this->callStacks[$resolver] ??= new CallStack()); + assert($transformer instanceof GeneratedTransformer); + + return $transformer; + } +} diff --git a/src/Extension/Generated/GeneratedTransformerNotWritable.php b/src/Extension/Generated/GeneratedTransformerNotWritable.php new file mode 100644 index 00000000..4208469b --- /dev/null +++ b/src/Extension/Generated/GeneratedTransformerNotWritable.php @@ -0,0 +1,18 @@ +metadataFactory(), $cachePath))->warmup([ProfileCreated::class]); + */ +final class GeneratedTransformerWarmer +{ + private readonly TransformerFiles $files; + + public function __construct( + private readonly MetadataFactory $metadataFactory, + string $cachePath, + ) { + $this->files = new TransformerFiles($cachePath); + } + + /** + * Generates a transformer for every class and every nested class whose objects may be mapped in place. + * + * @param list $classes + * + * @return list all classes a transformer was generated for + * + * @throws ClassNotGeneratable if a class of the list can not be generated. + * @throws GeneratedTransformerNotWritable + */ + public function warmup(array $classes): array + { + $generator = new TransformerGenerator(); + $queue = []; + + foreach ($classes as $class) { + $queue[] = [ltrim($class, '\\'), true]; + } + + $done = []; + $generated = []; + + while ($queue !== []) { + [$class, $requested] = array_shift($queue); + + if (isset($done[$class])) { + continue; + } + + $done[$class] = true; + + try { + $metadata = $this->metadataFactory->metadata($class); + $className = $this->files->className($metadata); + $code = $generator->generate($metadata, $this->files->fqcn($className)); + } catch (ClassNotFound) { + if ($requested) { + throw new ClassNotGeneratable($class, 'class not found'); + } + + continue; + } catch (ClassNotGeneratable $e) { + // nested classes without a transformer are hydrated by the hydrator + if ($requested) { + throw $e; + } + + continue; + } + + $this->write($this->files->path($className), $code->code); + $generated[] = $metadata->className; + + foreach ($code->nested as $nested) { + $queue[] = [$nested, false]; + } + } + + return $generated; + } + + /** @throws GeneratedTransformerNotWritable */ + private function write(string $file, string $code): void + { + $directory = $this->files->cachePath; + + if (!is_dir($directory) && !@mkdir($directory, 0777, true) && !is_dir($directory)) { + throw new GeneratedTransformerNotWritable($file); + } + + $tmp = $file . '.' . uniqid('', true) . '.tmp'; + + if (@file_put_contents($tmp, $code) === false || !@rename($tmp, $file)) { + @unlink($tmp); + + throw new GeneratedTransformerNotWritable($file); + } + } +} diff --git a/src/Extension/Generated/Literal.php b/src/Extension/Generated/Literal.php new file mode 100644 index 00000000..3e469bbe --- /dev/null +++ b/src/Extension/Generated/Literal.php @@ -0,0 +1,86 @@ +name); + } + + if (is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $literal = self::export($item); + + if ($literal === null) { + return null; + } + + $items[] = sprintf('%s => %s', is_string($key) ? self::string($key) : var_export($key, true), $literal); + } + + return '[' . implode(', ', $items) . ']'; + } + + return null; + } + + /** + * var_export() writes control characters like newlines verbatim, which breaks the indentation of the + * generated code. Such strings are written as double quoted literals with escape sequences instead. + */ + private static function string(string $value): string + { + if (preg_match('/[\x00-\x1f\x7f]/', $value) !== 1) { + return var_export($value, true); + } + + $escaped = preg_replace_callback( + '/[\x00-\x1f\x7f"\\\\$]/', + static fn (array $matches): string => match ($matches[0]) { + '"' => '\\"', + '\\' => '\\\\', + '$' => '\\$', + default => sprintf('\\x%02X', ord($matches[0])), + }, + $value, + ); + + return '"' . $escaped . '"'; + } +} diff --git a/src/Extension/Generated/NestedPlan.php b/src/Extension/Generated/NestedPlan.php new file mode 100644 index 00000000..ed85dc27 --- /dev/null +++ b/src/Extension/Generated/NestedPlan.php @@ -0,0 +1,25 @@ +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..9a992473 --- /dev/null +++ b/src/Extension/Generated/Templates.php @@ -0,0 +1,286 @@ + $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 TRANSFORMER = <<<'PHP' +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + {{hydrate}} + } + + public function extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + {{extract}} + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + {{hydrateNested}} + } + + public function extractNested(object $object, array $context): array + { + {{extractNested}} + } + + protected function initialize(): void + { + {{initialize}} + } + + protected function mayReachItself(): bool + { + return {{recursive}}; + } +} + +PHP; + + public const CLOSURE = <<<'PHP' +$this->{{slot}} = Closure::bind(function ({{signature}}): {{return}} { + {{body}} +}, $this, \{{class}}::class); +PHP; + + public const HYDRATE_HELPER = <<<'PHP' +$this->hp{{slot}} = Closure::bind(function (object $object, array $data, array $context, bool $inline): void { + {{body}} +}, $this, \{{scope}}::class); +PHP; + + public const EXTRACT_TRACKED = <<<'PHP' +if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \{{class}}::class; + + try { + {{pre}} + + return [ + {{fields}} + ]; + } finally { + unset($this->callStack->objects[$id]); + } +} +PHP; + + /** Nested objects are only mapped in place for calls from the hydrator which created the transformers. */ + public const OWNER = '($context[Hydrator::HYDRATOR] ?? null) === $this->owner'; + + public const EXTRACT_PLAIN = <<<'PHP' +{{pre}} + +return [ + {{fields}} +]; +PHP; + + public const EXTRACT_HELPER = <<<'PHP' +$this->ep{{slot}} = Closure::bind(function (object $object, array $context, bool $inline): 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}} && $inline && \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}} && $inline && \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}} && $inline && {{check}} ? {{nested}}($value, $context) : $this->n{{slot}}->normalize($value, $context); +PHP; + + public const NORMALIZE_ARRAY = <<<'PHP' +$value = $object->{{name}}; + +if ($this->ie{{flag}} && $inline && \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/TransformerFiles.php b/src/Extension/Generated/TransformerFiles.php new file mode 100644 index 00000000..88fdd3e4 --- /dev/null +++ b/src/Extension/Generated/TransformerFiles.php @@ -0,0 +1,59 @@ + $metadata + * + * @template T of object + */ + public function className(ClassMetadata $metadata): string + { + $hash = hash('sha256', serialize([self::VERSION, $metadata->className, ClassFingerprint::of($metadata)])); + + return sprintf( + '%sTransformer_%s', + preg_replace('/[^A-Za-z0-9_]/', '', $metadata->reflection->getShortName()), + substr($hash, 0, 16), + ); + } + + public function fqcn(string $className): string + { + return self::NAMESPACE . '\\' . $className; + } + + public function path(string $className): string + { + return sprintf('%s/%s.php', $this->cachePath, $className); + } +} diff --git a/src/Extension/Generated/TransformerGenerator.php b/src/Extension/Generated/TransformerGenerator.php new file mode 100644 index 00000000..e6c473e2 --- /dev/null +++ b/src/Extension/Generated/TransformerGenerator.php @@ -0,0 +1,56 @@ + $metadata + * + * @throws ClassNotGeneratable + * + * @template T of object + */ + public function generate(ClassMetadata $metadata, string $fqcn): GeneratedCode + { + if (!ClassPlanner::generatable($metadata)) { + throw new ClassNotGeneratable($metadata->className, 'the class is internal, abstract, anonymous, an enum or an interface'); + } + + if ($metadata->normalizer !== null) { + throw new ClassNotGeneratable($metadata->className, 'the class has a class normalizer'); + } + + $plan = (new ClassPlanner())->plan($metadata); + + $parts = explode('\\', ltrim($fqcn, '\\')); + $className = array_pop($parts); + $namespace = implode('\\', $parts); + + $nested = []; + + foreach ($plan->nested() as $class) { + $nested[] = $class->class; + } + + return new GeneratedCode( + (new CodeEmitter($plan))->emit($namespace, $className), + $nested, + ); + } +} diff --git a/src/Extension/Generated/ValueKind.php b/src/Extension/Generated/ValueKind.php new file mode 100644 index 00000000..734d110a --- /dev/null +++ b/src/Extension/Generated/ValueKind.php @@ -0,0 +1,25 @@ + */ private array $transformers = []; + /** @var array false if the hydrator does more than calling the transformer */ + private array $directHydrate = []; + + /** @var array false if the hydrator does more than calling the transformer */ + private array $directExtract = []; + private readonly bool $hasSkippableMiddlewares; + private readonly TransformerResolver $resolver; + + /** + * The context of calls without context, built once: adding the hydrator to an empty context on every call would + * allocate a new array each time. + * + * @var array + */ + private readonly array $context; + /** * @param list $middlewares run before the data is transformed, in this order * @param ClassTransformerFactory $transformerFactory provides the transformer which maps the data of a class @@ -60,6 +78,8 @@ public function __construct( } $this->hasSkippableMiddlewares = $hasSkippableMiddlewares; + $this->resolver = new StackTransformerResolver($this, $this->direct(...)); + $this->context = [self::HYDRATOR => $this]; } /** @@ -72,14 +92,54 @@ public function __construct( */ public function hydrate(string $class, mixed $data, array $context = []): object { - $context[self::HYDRATOR] ??= $this; + if ($context === []) { + $context = $this->context; + } else { + $context[self::HYDRATOR] ??= $this; + } + + $transformer = $this->directHydrate[$class] ?? false; + + // nothing else to do for this class, the transformer is called right away + if ($transformer !== false && is_array($data)) { + $object = $transformer->hydrate($data, $context); + assert($object instanceof $class); + + return $object; + } + + return $this->hydrateClass($class, $data, $context); + } + /** + * The path for classes seen the first time and for classes which need more than the transformer, kept out of + * {@see self::hydrate()} to keep the direct path small. + * + * @param class-string $class + * @param array $context + * + * @return T + * + * @template T of object + */ + private function hydrateClass(string $class, mixed $data, array $context): object + { try { - $metadata = $this->metadata($class); + $transformer = $this->directHydrate[$class] ?? $this->direct($class, Direction::Hydrate); } catch (ClassNotFound $e) { throw new ClassNotSupported($class, $e); } + if ($transformer !== false && is_array($data)) { + $object = $transformer->hydrate($data, $context); + assert($object instanceof $class); + + return $object; + } + + /** @var ClassMetadata $metadata the metadata is already loaded */ + $metadata = $this->classMetadata[$class]; + if ($metadata->normalizer) { $return = $metadata->normalizer->denormalize($data, $context); @@ -94,9 +154,7 @@ public function hydrate(string $class, mixed $data, array $context = []): object throw new ArrayDataRequired($class); } - $lazy = PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->defaultLazy); - - if (!$lazy) { + if (!$this->lazy($metadata)) { return $this->hydrateWithMiddlewares($metadata, $data, $context); } @@ -112,21 +170,78 @@ public function hydrate(string $class, mixed $data, array $context = []): object */ public function extract(object $object, array $context = []): mixed { - $context[self::HYDRATOR] ??= $this; + if ($context === []) { + $context = $this->context; + } else { + $context[self::HYDRATOR] ??= $this; + } + + $transformer = $this->directExtract[$object::class] ?? false; + + // nothing else to do for this class, the transformer is called right away + if ($transformer !== false) { + return $transformer->extract($object, $context); + } + + return $this->extractClass($object, $context); + } + + /** + * The path for classes seen the first time and for classes which need more than the transformer, kept out of + * {@see self::extract()} to keep the direct path small. + * + * @param array $context + */ + private function extractClass(object $object, array $context): mixed + { + $transformer = $this->directExtract[$object::class] ?? $this->direct($object::class, Direction::Extract); + + if ($transformer !== false) { + return $transformer->extract($object, $context); + } - $metadata = $this->metadata($object::class); + $metadata = $this->classMetadata[$object::class]; if ($metadata->normalizer) { return $metadata->normalizer->normalize($object, $context); } - $middlewares = $this->middlewaresFor($metadata, Skip::Extract); - $transformer = $this->transformer($metadata); + return (new Next($this->middlewaresFor($metadata, Direction::Extract), $this->transformer($metadata))) + ->extract($metadata, $object, $context); + } - // without middlewares the transformer is called without building the stack - return $middlewares === [] - ? $transformer->extract($object, $context) - : (new Next($middlewares, $transformer))->extract($metadata, $object, $context); + /** + * Decided once per class and direction: the transformer can be called directly if the class has no class + * normalizer, is not lazy and no middleware runs for it. + * + * @param class-string $class + * + * @throws ClassNotFound + */ + private function direct(string $class, Direction $direction): ClassTransformer|false + { + $metadata = $this->metadata($class); + $direct = false; + + if ( + $metadata->normalizer === null + && ($direction === Direction::Extract || !$this->lazy($metadata)) + && $this->middlewaresFor($metadata, $direction) === [] + ) { + $direct = $this->transformer($metadata); + } + + if ($direction === Direction::Hydrate) { + return $this->directHydrate[$class] = $direct; + } + + return $this->directExtract[$class] = $direct; + } + + /** @param ClassMetadata $metadata */ + private function lazy(ClassMetadata $metadata): bool + { + return PHP_VERSION_ID >= 80400 && ($metadata->lazy ?? $this->defaultLazy); } /** @@ -140,7 +255,7 @@ public function extract(object $object, array $context = []): mixed */ private function hydrateWithMiddlewares(ClassMetadata $metadata, array $data, array $context): object { - $middlewares = $this->middlewaresFor($metadata, Skip::Hydrate); + $middlewares = $this->middlewaresFor($metadata, Direction::Hydrate); $transformer = $this->transformer($metadata); if ($middlewares === []) { @@ -162,25 +277,24 @@ private function hydrateWithMiddlewares(ClassMetadata $metadata, array $data, ar private function transformer(ClassMetadata $metadata): ClassTransformer { return $this->transformers[$metadata->className] - ??= $this->transformerFactory->create($metadata) + ??= $this->transformerFactory->create($metadata, $this->resolver) ?? throw new ClassNotSupported($metadata->className); } /** - * @param ClassMetadata $metadata - * @param Skip::Hydrate|Skip::Extract $direction + * @param ClassMetadata $metadata * * @return list * * @template T of object */ - private function middlewaresFor(ClassMetadata $metadata, Skip $direction): array + private function middlewaresFor(ClassMetadata $metadata, Direction $direction): array { if (!$this->hasSkippableMiddlewares) { return $this->middlewares; } - $cached = $direction === Skip::Hydrate + $cached = $direction === Direction::Hydrate ? $this->hydrateMiddlewares[$metadata->className] ?? null : $this->extractMiddlewares[$metadata->className] ?? null; @@ -189,12 +303,13 @@ private function middlewaresFor(ClassMetadata $metadata, Skip $direction): array } $middlewares = []; + $skipped = $direction === Direction::Hydrate ? Skip::Hydrate : Skip::Extract; foreach ($this->middlewares as $middleware) { if ($middleware instanceof SkippableMiddleware) { $skip = $middleware->skip($metadata); - if ($skip === $direction || $skip === Skip::Both) { + if ($skip === $skipped || $skip === Skip::Both) { continue; } } @@ -202,7 +317,7 @@ private function middlewaresFor(ClassMetadata $metadata, Skip $direction): array $middlewares[] = $middleware; } - if ($direction === Skip::Hydrate) { + if ($direction === Direction::Hydrate) { return $this->hydrateMiddlewares[$metadata->className] = $middlewares; } diff --git a/src/StackHydratorBuilder.php b/src/StackHydratorBuilder.php index 4d3240d5..8ab6ce8f 100644 --- a/src/StackHydratorBuilder.php +++ b/src/StackHydratorBuilder.php @@ -10,6 +10,7 @@ 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; @@ -102,6 +103,24 @@ public function setCache(CacheItemPoolInterface|CacheInterface|null $cache): sta } public function build(): StackHydrator + { + $transformerFactories = $this->transformerFactories(); + + return new StackHydrator( + $this->metadataFactory(), + $this->middlewares(), + $this->defaultLazy, + $transformerFactories === [] + ? new ReflectionTransformerFactory() + : new ChainTransformerFactory([...$transformerFactories, new ReflectionTransformerFactory()]), + ); + } + + /** + * The metadata factory the hydrator is built with, for example to generate code or warm up caches with the same + * metadata the hydrator ends up with. Every call creates a new instance. + */ + public function metadataFactory(): MetadataFactory { $metadataFactory = new EnrichingMetadataFactory( new AttributeMetadataFactory( @@ -118,16 +137,7 @@ public function build(): StackHydrator $metadataFactory = new Psr16MetadataFactory($metadataFactory, $this->cache); } - $transformerFactories = $this->transformerFactories(); - - return new StackHydrator( - $metadataFactory, - $this->middlewares(), - $this->defaultLazy, - $transformerFactories === [] - ? new ReflectionTransformerFactory() - : new ChainTransformerFactory([...$transformerFactories, new ReflectionTransformerFactory()]), - ); + return $metadataFactory; } public function defaultLazy(): bool diff --git a/src/StackTransformerResolver.php b/src/StackTransformerResolver.php new file mode 100644 index 00000000..e00d5e44 --- /dev/null +++ b/src/StackTransformerResolver.php @@ -0,0 +1,45 @@ +hydrator; + } + + /** + * @param class-string $class + * + * @return ClassMetadata + * + * @template T of object + */ + public function metadata(string $class): ClassMetadata + { + return $this->hydrator->metadata($class); + } + + /** @param class-string $class */ + public function direct(string $class, Direction $direction): ClassTransformer|null + { + return ($this->direct)($class, $direction) ?: null; + } +} diff --git a/src/Transformer/ChainTransformerFactory.php b/src/Transformer/ChainTransformerFactory.php index 2b6d1071..e9e3b8fe 100644 --- a/src/Transformer/ChainTransformerFactory.php +++ b/src/Transformer/ChainTransformerFactory.php @@ -14,10 +14,10 @@ public function __construct( ) { } - public function create(ClassMetadata $metadata): ClassTransformer|null + public function create(ClassMetadata $metadata, TransformerResolver $resolver): ClassTransformer|null { foreach ($this->factories as $factory) { - $transformer = $factory->create($metadata); + $transformer = $factory->create($metadata, $resolver); if ($transformer !== null) { return $transformer; diff --git a/src/Transformer/ClassTransformerFactory.php b/src/Transformer/ClassTransformerFactory.php index 32c49faf..83df3265 100644 --- a/src/Transformer/ClassTransformerFactory.php +++ b/src/Transformer/ClassTransformerFactory.php @@ -16,11 +16,12 @@ interface ClassTransformerFactory /** * Called once per class and hydrator, the transformer is cached by the hydrator. * - * @param ClassMetadata $metadata + * @param ClassMetadata $metadata + * @param TransformerResolver $resolver of the hydrator which asks, keep it to map nested objects in place * * @return ClassTransformer|null null if this factory is not responsible for the class * * @template T of object */ - public function create(ClassMetadata $metadata): ClassTransformer|null; + public function create(ClassMetadata $metadata, TransformerResolver $resolver): ClassTransformer|null; } diff --git a/src/Transformer/Direction.php b/src/Transformer/Direction.php new file mode 100644 index 00000000..e8d1c436 --- /dev/null +++ b/src/Transformer/Direction.php @@ -0,0 +1,11 @@ +callStack); } diff --git a/src/Transformer/TransformerResolver.php b/src/Transformer/TransformerResolver.php new file mode 100644 index 00000000..ebfd2e68 --- /dev/null +++ b/src/Transformer/TransformerResolver.php @@ -0,0 +1,46 @@ + $class + * + * @return ClassMetadata + * + * @throws ClassNotFound + * + * @template T of object + */ + public function metadata(string $class): ClassMetadata; + + /** + * The transformer the hydrator calls directly for the class in this direction: the class has no class + * normalizer, is not lazy and no middleware runs for it. Mapping such a class in place behaves exactly like + * calling the hydrator, as long as the call came from {@see self::hydrator()}. + * + * @param class-string $class + * + * @return ClassTransformer|null null if the hydrator does more than calling the transformer + * + * @throws ClassNotFound + */ + public function direct(string $class, Direction $direction): ClassTransformer|null; +} diff --git a/tests/Benchmark/HydratorBench.php b/tests/Benchmark/GeneratedHydratorBench.php similarity index 70% rename from tests/Benchmark/HydratorBench.php rename to tests/Benchmark/GeneratedHydratorBench.php index d760e689..27203e38 100644 --- a/tests/Benchmark/HydratorBench.php +++ b/tests/Benchmark/GeneratedHydratorBench.php @@ -4,6 +4,8 @@ namespace Patchlevel\Hydrator\Tests\Benchmark; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerWarmer; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; @@ -12,14 +14,18 @@ use PhpBench\Attributes as Bench; #[Bench\BeforeMethods('setUp')] -final class HydratorBench +final class GeneratedHydratorBench { private Hydrator $hydrator; public function __construct() { - $this->hydrator = (new StackHydratorBuilder()) - ->build(); + $builder = (new StackHydratorBuilder()) + ->useExtension(new GeneratedTransformerExtension(__DIR__ . '/../../var/cache')); + + (new GeneratedTransformerWarmer($builder->metadataFactory(), __DIR__ . '/../../var/cache'))->warmup([ProfileCreated::class]); + + $this->hydrator = $builder->build(); } public function setUp(): void @@ -39,7 +45,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate(ProfileCreated::class, [ @@ -52,7 +58,7 @@ public function benchHydrate1Object(): void ]); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -98,36 +104,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 72% rename from tests/Benchmark/HydratorWithCryptographyBench.php rename to tests/Benchmark/GeneratedHydratorWithCryptographyBench.php index 0851d555..9171754f 100644 --- a/tests/Benchmark/HydratorWithCryptographyBench.php +++ b/tests/Benchmark/GeneratedHydratorWithCryptographyBench.php @@ -7,6 +7,8 @@ use Patchlevel\Hydrator\Extension\Cryptography\BaseCryptographer; use Patchlevel\Hydrator\Extension\Cryptography\CryptographyExtension; use Patchlevel\Hydrator\Extension\Cryptography\Store\InMemoryCipherKeyStore; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerExtension; +use Patchlevel\Hydrator\Extension\Generated\GeneratedTransformerWarmer; use Patchlevel\Hydrator\Hydrator; use Patchlevel\Hydrator\StackHydratorBuilder; use Patchlevel\Hydrator\Tests\Benchmark\Fixture\ProfileCreated; @@ -15,7 +17,7 @@ use PhpBench\Attributes as Bench; #[Bench\BeforeMethods('setUp')] -final class HydratorWithCryptographyBench +final class GeneratedHydratorWithCryptographyBench { private InMemoryCipherKeyStore $store; @@ -25,9 +27,13 @@ public function __construct() { $this->store = new InMemoryCipherKeyStore(); - $this->hydrator = (new StackHydratorBuilder()) - ->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($this->store))) - ->build(); + $builder = (new StackHydratorBuilder()) + ->useExtension(new GeneratedTransformerExtension(__DIR__ . '/../../var/cache')) + ->useExtension(new CryptographyExtension(BaseCryptographer::createWithOpenssl($this->store))); + + (new GeneratedTransformerWarmer($builder->metadataFactory(), __DIR__ . '/../../var/cache'))->warmup([ProfileCreated::class]); + + $this->hydrator = $builder->build(); } public function setUp(): void @@ -49,7 +55,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate( @@ -65,7 +71,7 @@ public function benchHydrate1Object(): void ); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -114,39 +120,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 7a7fe0e1..9d658951 100644 --- a/tests/Benchmark/StackHydratorBench.php +++ b/tests/Benchmark/StackHydratorBench.php @@ -39,7 +39,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate(ProfileCreated::class, [ @@ -52,7 +52,7 @@ public function benchHydrate1Object(): void ]); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -98,36 +98,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 965e40f8..8b2e589b 100644 --- a/tests/Benchmark/StackHydratorWithCryptographyBench.php +++ b/tests/Benchmark/StackHydratorWithCryptographyBench.php @@ -49,7 +49,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate( @@ -65,7 +65,7 @@ public function benchHydrate1Object(): void ); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchExtract1Object(): void { $object = new ProfileCreated( @@ -114,39 +114,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 71% rename from tests/Benchmark/HydratorWithLazyBench.php rename to tests/Benchmark/StackHydratorWithLazyBench.php index 19cc97ef..eb0a3ff6 100644 --- a/tests/Benchmark/HydratorWithLazyBench.php +++ b/tests/Benchmark/StackHydratorWithLazyBench.php @@ -10,7 +10,7 @@ use PhpBench\Attributes as Bench; #[Bench\BeforeMethods('setUp')] -final class HydratorWithLazyBench +final class StackHydratorWithLazyBench { private Hydrator $hydrator; @@ -38,7 +38,7 @@ public function setUp(): void $this->hydrator->extract($object); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1Object(): void { $this->hydrator->hydrate(ProfileCreated::class, [ @@ -51,7 +51,7 @@ public function benchHydrate1Object(): void ]); } - #[Bench\Revs(5)] + #[Bench\Revs(1000)] public function benchHydrate1ObjectTriggerInit(): void { $object = $this->hydrator->hydrate(ProfileCreated::class, [ @@ -97,36 +97,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/GeneratedStackHydratorTest.php b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php new file mode 100644 index 00000000..f5db6fe3 --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedStackHydratorTest.php @@ -0,0 +1,1103 @@ += 80400) { + $classes[] = AsymmetricVisibilityDto::class; + } + + (new GeneratedTransformerWarmer((new StackHydratorBuilder())->metadataFactory(), self::$cachePath))->warmup($classes); + } + + public static function tearDownAfterClass(): void + { + foreach (glob(self::$cachePath . '/*.php') ?: [] as $file) { + unlink($file); + } + + rmdir(self::$cachePath); + } + + public function setUp(): void + { + $this->hydrator = $this->builder()->build(); + } + + /** + * Closures in attributes (used by the inline normalizer fixture) are only supported since PHP 8.5. + * + * @return list + */ + private static function supportedClasses(): array + { + if (PHP_VERSION_ID >= 80500) { + return self::CLASSES; + } + + return array_values(array_filter( + self::CLASSES, + static fn (string $class): bool => $class !== ProfileCreatedWithInlineNormalizer::class, + )); + } + + private function builder(): StackHydratorBuilder + { + return (new StackHydratorBuilder())->useExtension($this->extension()); + } + + private function extension(): GeneratedTransformerExtension + { + return new GeneratedTransformerExtension(self::$cachePath); + } + + 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, + $this->callback(static fn (array $context): bool => $context['context'] === '123' + && $context[Hydrator::HYDRATOR] instanceof StackHydrator), + $this->isInstanceOf(Next::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, + $this->callback(static fn (array $context): bool => $context['context'] === '123' + && $context[Hydrator::HYDRATOR] instanceof StackHydrator), + $this->isInstanceOf(Next::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()->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 testChangedClassFallsBackToReflection(): void + { + // the renamed fields change the fingerprint, like a class which changed after the warmup + $hydrator = $this->builder() + ->addMetadataEnricher(new class implements MetadataEnricher { + public function enrich(ClassMetadata $classMetadata): void + { + foreach ($classMetadata->properties as $property) { + $property->fieldName = 'renamed_' . $property->fieldName; + } + } + }) + ->build(); + + $data = ['renamed_profileId' => '1', 'renamed_email' => 'info@patchlevel.de']; + $event = $hydrator->hydrate(ProfileCreated::class, $data); + + self::assertEquals(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de')), $event); + self::assertSame($data, $hydrator->extract($event)); + self::assertInstanceOf(ReflectionTransformer::class, self::transformers($hydrator)[ProfileCreated::class]); + } + + #[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 transformers the hydrator uses. + * + * @return array flag name => mapped in place + */ + private static function inlined(StackHydrator $hydrator, string $class): array + { + $transformer = self::transformers($hydrator)[$class] ?? null; + self::assertInstanceOf(GeneratedTransformer::class, $transformer); + + /** @var array $flags */ + $flags = array_filter( + get_object_vars($transformer), + static fn (string $name): bool => preg_match('/^i[he]\d+$/', $name) === 1, + ARRAY_FILTER_USE_KEY, + ); + + return $flags; + } + + /** @return array */ + private static function transformers(StackHydrator $hydrator): array + { + /** @var array $transformers */ + $transformers = (new ReflectionProperty(StackHydrator::class, 'transformers'))->getValue($hydrator); + + return $transformers; + } + + public function testGeneratedTransformersAreUsed(): void + { + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + $this->hydrator->extract($this->hydrator->hydrate(ProfileCreated::class, $data)); + + $transformer = self::transformers($this->hydrator)[ProfileCreated::class]; + + self::assertInstanceOf(GeneratedTransformer::class, $transformer); + self::assertStringStartsWith(TransformerFiles::NAMESPACE . '\\', $transformer::class); + } + + public function testNestedObjectsAreInlinedWithoutOtherMiddlewares(): void + { + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + + self::assertSame($data, $this->hydrator->extract($this->hydrator->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame( + ['ih0' => true, 'ie0' => true, 'ih1' => true, 'ie1' => true], + self::inlined($this->hydrator, NestedLifecycleDto::class), + ); + } + + public function testNestedObjectsAreInlinedWhenOtherMiddlewaresSkipThem(): void + { + $counter = new CountingMiddleware([LifecycleFixture::class => Skip::Both]); + + $hydrator = $this->builder() + ->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, NestedLifecycleDto::class), + ); + 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 = $this->builder() + ->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, NestedLifecycleDto::class), + ); + + // callback upcasters for other classes do not prevent inlining + $hydrator = $this->builder() + ->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, NestedLifecycleDto::class), + ); + } + + public function testGeneratedTransformerRunsAfterOtherMiddlewares(): void + { + $counter = new CountingMiddleware(); + + $hydrator = $this->builder() + ->addMiddleware($counter) + ->build(); + + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + self::assertSame($data, $hydrator->extract($hydrator->hydrate(ProfileCreated::class, $data))); + + self::assertInstanceOf(GeneratedTransformer::class, self::transformers($hydrator)[ProfileCreated::class]); + self::assertSame([ProfileCreated::class => 1], $counter->hydrated); + self::assertSame([ProfileCreated::class => 1], $counter->extracted); + } + + public function testNestedObjectsAreNotInlinedWithNonSkippableMiddleware(): void + { + $counter = new CountingMiddleware(); + + $hydrator = $this->builder() + ->addMiddleware($counter) + ->build(); + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b'], ['name' => 'c']]]; + + // with another middleware in the stack, nested objects go through the whole stack + $object = $hydrator->hydrate(NestedLifecycleDto::class, $data); + self::assertSame($data, $hydrator->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($hydrator, NestedLifecycleDto::class)); + } + + 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), + ); + } + + public function testOuterHydratorIsUsedForNestedObjects(): void + { + $outer = new class ($this->hydrator) implements Hydrator { + /** @var list */ + public array $calls = []; + + public function __construct(private readonly Hydrator $inner) + { + } + + /** + * @param class-string $class + * @param array $context + * + * @return T + * + * @template T of object + */ + public function hydrate(string $class, mixed $data, array $context = []): object + { + $context[Hydrator::HYDRATOR] ??= $this; + $this->calls[] = 'hydrate ' . $class; + + return $this->inner->hydrate($class, $data, $context); + } + + /** @param array $context */ + public function extract(object $object, array $context = []): mixed + { + $context[Hydrator::HYDRATOR] ??= $this; + $this->calls[] = 'extract ' . $object::class; + + return $this->inner->extract($object, $context); + } + }; + + $data = ['child' => ['name' => 'a'], 'items' => [['name' => 'b']]]; + + // nested objects are mapped in place for calls of the hydrator itself + self::assertSame($data, $this->hydrator->extract($this->hydrator->hydrate(NestedLifecycleDto::class, $data))); + + // but go through the outer hydrator for its calls + self::assertSame($data, $outer->extract($outer->hydrate(NestedLifecycleDto::class, $data))); + self::assertSame( + [ + 'hydrate ' . NestedLifecycleDto::class, + 'hydrate ' . LifecycleFixture::class, + 'hydrate ' . LifecycleFixture::class, + 'extract ' . NestedLifecycleDto::class, + 'extract ' . LifecycleFixture::class, + 'extract ' . LifecycleFixture::class, + ], + $outer->calls, + ); + } +} diff --git a/tests/Unit/Extension/Generated/GeneratedTransformerWarmerTest.php b/tests/Unit/Extension/Generated/GeneratedTransformerWarmerTest.php new file mode 100644 index 00000000..6c8cf87e --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedTransformerWarmerTest.php @@ -0,0 +1,148 @@ +cachePath = sys_get_temp_dir() . '/' . uniqid('patchlevel-hydrator-warmer-', true); + } + + public function tearDown(): void + { + if (!is_dir($this->cachePath)) { + return; + } + + foreach (glob($this->cachePath . '/*') ?: [] as $file) { + unlink($file); + } + + rmdir($this->cachePath); + } + + public function testWarmupGeneratesNestedClasses(): void + { + $metadataFactory = new AttributeMetadataFactory(); + $warmer = new GeneratedTransformerWarmer($metadataFactory, $this->cachePath); + + self::assertSame( + [ProfileCreatedWrapper::class, ProfileCreated::class], + $warmer->warmup([ProfileCreatedWrapper::class]), + ); + + $files = new TransformerFiles($this->cachePath); + + foreach ([ProfileCreatedWrapper::class, ProfileCreated::class] as $class) { + self::assertFileExists($files->path($files->className($metadataFactory->metadata($class)))); + } + } + + public function testWarmupIsDeterministic(): void + { + $metadataFactory = new AttributeMetadataFactory(); + $files = new TransformerFiles($this->cachePath); + $file = $files->path($files->className($metadataFactory->metadata(ProfileCreated::class))); + + (new GeneratedTransformerWarmer($metadataFactory, $this->cachePath))->warmup([ProfileCreated::class]); + $first = file_get_contents($file); + + (new GeneratedTransformerWarmer($metadataFactory, $this->cachePath))->warmup([ProfileCreated::class]); + + self::assertSame($first, file_get_contents($file)); + } + + public function testWarmedTransformersAreUsed(): void + { + $builder = (new StackHydratorBuilder())->useExtension(new GeneratedTransformerExtension($this->cachePath)); + (new GeneratedTransformerWarmer($builder->metadataFactory(), $this->cachePath))->warmup([ProfileCreatedWrapper::class]); + $hydrator = $builder->build(); + + $data = ['event' => ['profileId' => '1', 'email' => 'info@patchlevel.de']]; + $object = $hydrator->hydrate(ProfileCreatedWrapper::class, $data); + + self::assertEquals( + new ProfileCreatedWrapper(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('info@patchlevel.de'))), + $object, + ); + self::assertSame($data, $hydrator->extract($object)); + + $transformers = (new ReflectionProperty(StackHydrator::class, 'transformers'))->getValue($hydrator); + self::assertIsArray($transformers); + self::assertInstanceOf(GeneratedTransformer::class, $transformers[ProfileCreatedWrapper::class]); + self::assertInstanceOf(GeneratedTransformer::class, $transformers[ProfileCreated::class]); + } + + public function testUnknownClassIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + /** @phpstan-ignore argument.type */ + (new GeneratedTransformerWarmer(new AttributeMetadataFactory(), $this->cachePath))->warmup(['Unknown']); + } + + public function testAbstractClassIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + (new GeneratedTransformerWarmer(new AttributeMetadataFactory(), $this->cachePath))->warmup([ChildDto::class]); + } + + public function testClassWithClassNormalizerIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + (new GeneratedTransformerWarmer(new AttributeMetadataFactory(), $this->cachePath))->warmup([ProfileId::class]); + } + + public function testNotWritable(): void + { + $this->expectException(GeneratedTransformerNotWritable::class); + + $file = $this->cachePath . '-file'; + touch($file); + + try { + (new GeneratedTransformerWarmer(new AttributeMetadataFactory(), $file))->warmup([ProfileCreated::class]); + } finally { + unlink($file); + } + } +} diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php new file mode 100644 index 00000000..01ad4728 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle1DtoTransformer.php @@ -0,0 +1,220 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('to'); + $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Direction::Hydrate); + $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, Direction::Extract); + $this->ih0 = $h0 !== null; + $this->ie0 = $e0 !== null; + if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + if (isset($this->x0)) { $this->x0->init(); } + } + + protected function mayReachItself(): bool + { + return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php new file mode 100644 index 00000000..2a73e5aa --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle2DtoTransformer.php @@ -0,0 +1,220 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle2Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('to'); + $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Direction::Hydrate); + $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, Direction::Extract); + $this->ih0 = $h0 !== null; + $this->ie0 = $e0 !== null; + if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + if (isset($this->x0)) { $this->x0->init(); } + } + + protected function mayReachItself(): bool + { + return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php new file mode 100644 index 00000000..cda39f0a --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/Circle3DtoTransformer.php @@ -0,0 +1,220 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('to', $data)) { + try { + $value = $data['to']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class; + + try { + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->to; + $v_to = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\Circle3Dto::class, 'to', $this->n0::class, $e); + } + + return [ + 'to' => $v_to, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('to'); + $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Direction::Hydrate); + $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Circle1Dto::class, Direction::Extract); + $this->ih0 = $h0 !== null; + $this->ie0 = $e0 !== null; + if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + if (isset($this->x0)) { $this->x0->init(); } + } + + protected function mayReachItself(): bool + { + return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php new file mode 100644 index 00000000..3d024740 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ContextAwareDtoTransformer.php @@ -0,0 +1,187 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('value', $data)) { + try { + $value = $this->n0->denormalize($data['value'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class; + + try { + try { + $v_value = $this->n0->normalize($object->value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::class, $e); + } + + return [ + 'value' => $v_value, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_value = $this->n0->normalize($object->value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::class, $e); + } + + return [ + 'value' => $v_value, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('value', $data)) { + try { + $value = $this->n0->denormalize($data['value'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class; + + try { + try { + $v_value = $this->n0->normalize($object->value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::class, $e); + } + + return [ + 'value' => $v_value, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_value = $this->n0->normalize($object->value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ContextAwareDto::class, 'value', $this->n0::class, $e); + } + + return [ + 'value' => $v_value, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('value'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/DefaultDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/DefaultDtoTransformer.php new file mode 100644 index 00000000..9dec094b --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/DefaultDtoTransformer.php @@ -0,0 +1,274 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + 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->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class; + + try { + try { + $value = $object->email; + $v_email = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::class, $e); + } + + return [ + 'admin' => $object->admin, + 'name' => $object->name, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->email; + $v_email = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::class, $e); + } + + return [ + 'admin' => $object->admin, + 'name' => $object->name, + 'email' => $v_email, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + 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->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class; + + try { + try { + $value = $object->email; + $v_email = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::class, $e); + } + + return [ + 'admin' => $object->admin, + 'name' => $object->name, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->email; + $v_email = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Email ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\DefaultDto::class, 'email', $this->n0::class, $e); + } + + return [ + 'admin' => $object->admin, + 'name' => $object->name, + 'email' => $v_email, + ]; + } + + protected function initialize(): void + { + $this->d0 = $this->promotedDefault('email'); + $this->n0 = $this->normalizer('email'); + $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Direction::Hydrate); + $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\Email::class, Direction::Extract); + $this->ih0 = $h0 !== null; + $this->ie0 = $e0 !== null; + if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + if (isset($this->x0)) { $this->x0->init(); } + } + + protected function mayReachItself(): bool + { + return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php new file mode 100644 index 00000000..56c1a575 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerDtoTransformer.php @@ -0,0 +1,412 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::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->n1->denormalize($data['dateTimeImmutable'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::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->n2->denormalize($data['dateTime'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::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->n3->denormalize($data['dateTimeZone'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'array' => $object->array, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'array' => $object->array, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::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->n1->denormalize($data['dateTimeImmutable'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::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->n2->denormalize($data['dateTime'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::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->n3->denormalize($data['dateTimeZone'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'array' => $object->array, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'array' => $object->array, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('status'); + $this->n1 = $this->normalizer('dateTimeImmutable'); + $this->n2 = $this->normalizer('dateTime'); + $this->n3 = $this->normalizer('dateTimeZone'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1) + || self::mayRecurse($this->n2) + || self::mayRecurse($this->n3); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoTransformer.php new file mode 100644 index 00000000..3337fa63 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithIterablesDtoTransformer.php @@ -0,0 +1,574 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('defaultArray', $data)) { + try { + $value = $this->n0->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::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->n1->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::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->n2->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::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->n3->denormalize($data['hashMap'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::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->n4->denormalize($data['nested'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::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->n5->denormalize($data['shapeArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class; + + try { + try { + $v_defaultArray = $this->n0->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::class, $e); + } + + try { + $v_listArray = $this->n1->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::class, $e); + } + + try { + $v_iterableArray = $this->n2->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::class, $e); + } + + try { + $v_hashMap = $this->n3->normalize($object->hashMap, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::class, $e); + } + + try { + $v_nested = $this->n4->normalize($object->nested, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::class, $e); + } + + try { + $v_shapeArray = $this->n5->normalize($object->shapeArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::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->objects[$id]); + } + } + + try { + $v_defaultArray = $this->n0->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::class, $e); + } + + try { + $v_listArray = $this->n1->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::class, $e); + } + + try { + $v_iterableArray = $this->n2->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::class, $e); + } + + try { + $v_hashMap = $this->n3->normalize($object->hashMap, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::class, $e); + } + + try { + $v_nested = $this->n4->normalize($object->nested, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::class, $e); + } + + try { + $v_shapeArray = $this->n5->normalize($object->shapeArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::class, $e); + } + + return [ + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'hashMap' => $v_hashMap, + 'nested' => $v_nested, + 'jsonArray' => $object->jsonArray, + 'shapeArray' => $v_shapeArray, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('defaultArray', $data)) { + try { + $value = $this->n0->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::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->n1->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::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->n2->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::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->n3->denormalize($data['hashMap'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::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->n4->denormalize($data['nested'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::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->n5->denormalize($data['shapeArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class; + + try { + try { + $v_defaultArray = $this->n0->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::class, $e); + } + + try { + $v_listArray = $this->n1->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::class, $e); + } + + try { + $v_iterableArray = $this->n2->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::class, $e); + } + + try { + $v_hashMap = $this->n3->normalize($object->hashMap, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::class, $e); + } + + try { + $v_nested = $this->n4->normalize($object->nested, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::class, $e); + } + + try { + $v_shapeArray = $this->n5->normalize($object->shapeArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::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->objects[$id]); + } + } + + try { + $v_defaultArray = $this->n0->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'defaultArray', $this->n0::class, $e); + } + + try { + $v_listArray = $this->n1->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'listArray', $this->n1::class, $e); + } + + try { + $v_iterableArray = $this->n2->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'iterableArray', $this->n2::class, $e); + } + + try { + $v_hashMap = $this->n3->normalize($object->hashMap, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'hashMap', $this->n3::class, $e); + } + + try { + $v_nested = $this->n4->normalize($object->nested, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'nested', $this->n4::class, $e); + } + + try { + $v_shapeArray = $this->n5->normalize($object->shapeArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithIterablesDto::class, 'shapeArray', $this->n5::class, $e); + } + + return [ + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'hashMap' => $v_hashMap, + 'nested' => $v_nested, + 'jsonArray' => $object->jsonArray, + 'shapeArray' => $v_shapeArray, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('defaultArray'); + $this->n1 = $this->normalizer('listArray'); + $this->n2 = $this->normalizer('iterableArray'); + $this->n3 = $this->normalizer('hashMap'); + $this->n4 = $this->normalizer('nested'); + $this->n5 = $this->normalizer('shapeArray'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1) + || self::mayRecurse($this->n2) + || self::mayRecurse($this->n3) + || self::mayRecurse($this->n4) + || self::mayRecurse($this->n5); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoTransformer.php new file mode 100644 index 00000000..46da4059 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/InferNormalizerWithNullableDtoTransformer.php @@ -0,0 +1,467 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::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->n1->denormalize($data['dateTimeImmutable'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::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->n2->denormalize($data['dateTime'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::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->n3->denormalize($data['dateTimeZone'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::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->n4->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + try { + $v_profileId = $this->n4->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'profileId' => $v_profileId, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + try { + $v_profileId = $this->n4->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'profileId' => $v_profileId, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::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->n1->denormalize($data['dateTimeImmutable'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::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->n2->denormalize($data['dateTime'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::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->n3->denormalize($data['dateTimeZone'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::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->n4->denormalize($data['profileId'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + try { + $v_profileId = $this->n4->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'profileId' => $v_profileId, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'status', $this->n0::class, $e); + } + + try { + $v_dateTimeImmutable = $this->n1->normalize($object->dateTimeImmutable, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeImmutable', $this->n1::class, $e); + } + + try { + $v_dateTime = $this->n2->normalize($object->dateTime, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTime', $this->n2::class, $e); + } + + try { + $v_dateTimeZone = $this->n3->normalize($object->dateTimeZone, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'dateTimeZone', $this->n3::class, $e); + } + + try { + $v_profileId = $this->n4->normalize($object->profileId, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\InferNormalizerWithNullableDto::class, 'profileId', $this->n4::class, $e); + } + + return [ + 'status' => $v_status, + 'dateTimeImmutable' => $v_dateTimeImmutable, + 'dateTime' => $v_dateTime, + 'dateTimeZone' => $v_dateTimeZone, + 'profileId' => $v_profileId, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('status'); + $this->n1 = $this->normalizer('dateTimeImmutable'); + $this->n2 = $this->normalizer('dateTime'); + $this->n3 = $this->normalizer('dateTimeZone'); + $this->n4 = $this->normalizer('profileId'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1) + || self::mayRecurse($this->n2) + || self::mayRecurse($this->n3) + || self::mayRecurse($this->n4); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedTransformer.php b/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedTransformer.php new file mode 100644 index 00000000..3b9a33c2 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/LazyProfileCreatedTransformer.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->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\LazyProfileCreated::class, 'profileId', $this->n0::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->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n1::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::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\LazyProfileCreated::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\LazyProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$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\LazyProfileCreated::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\LazyProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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\LazyProfileCreated::class, 'profileId', $this->n0::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->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::class, 'email', $this->n1::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\LazyProfileCreated::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\LazyProfileCreated::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\LazyProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$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\LazyProfileCreated::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\LazyProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php new file mode 100644 index 00000000..767ed875 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/NestedLifecycleDtoTransformer.php @@ -0,0 +1,372 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if (\array_key_exists('child', $data)) { + try { + $value = $data['child']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::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->ih1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->x0->hydrateNested($item, $context) : $this->n2->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class; + + try { + try { + $value = $object->child; + $v_child = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::class, $e); + } + + try { + $value = $object->items; + + if ($this->ie1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + + $v_items = $items; + } else { + $v_items = $this->n1->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::class, $e); + } + + return [ + 'child' => $v_child, + 'items' => $v_items, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->child; + $v_child = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::class, $e); + } + + try { + $value = $object->items; + + if ($this->ie1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + + $v_items = $items; + } else { + $v_items = $this->n1->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::class, $e); + } + + return [ + 'child' => $v_child, + 'items' => $v_items, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('child', $data)) { + try { + $value = $data['child']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::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->ih1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->x0->hydrateNested($item, $context) : $this->n2->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class; + + try { + try { + $value = $object->child; + $v_child = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::class, $e); + } + + try { + $value = $object->items; + + if ($this->ie1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + + $v_items = $items; + } else { + $v_items = $this->n1->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::class, $e); + } + + return [ + 'child' => $v_child, + 'items' => $v_items, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->child; + $v_child = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'child', $this->n0::class, $e); + } + + try { + $value = $object->items; + + if ($this->ie1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture ? $this->x0->extractNested($item, $context) : $this->n2->normalize($item, $context); + } + + $v_items = $items; + } else { + $v_items = $this->n1->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NestedLifecycleDto::class, 'items', $this->n1::class, $e); + } + + return [ + 'child' => $v_child, + 'items' => $v_items, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('child'); + $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Hydrate); + $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Extract); + $this->ih0 = $h0 !== null; + $this->ie0 = $e0 !== null; + if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + $this->n1 = $this->normalizer('items'); + $h1 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Hydrate); + $e1 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\LifecycleFixture::class, Direction::Extract); + $this->ih1 = $h1 !== null; + $this->ie1 = $e1 !== null; + if ($this->ih1 || $this->ie1) { $this->x0 = $h1 ?? $e1; } + if ($this->ih1 || $this->ie1) { $this->n2 = $this->n1->innerNormalizer(); } + if (isset($this->x0)) { $this->x0->init(); } + } + + protected function mayReachItself(): bool + { + return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)) + || ($this->ie1 ? $this->x0->recursive() : self::mayRecurse($this->n1)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php new file mode 100644 index 00000000..755a59ef --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/NormalizerInBaseClassDefinedDtoTransformer.php @@ -0,0 +1,682 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::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->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::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->n2->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::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->n3->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::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->n4->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::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->ih1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->x1->hydrateNested($item, $context) : $this->n6->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n5->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::class, $e); + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::class, $e); + } + + try { + $v_defaultArray = $this->n2->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::class, $e); + } + + try { + $v_listArray = $this->n3->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::class, $e); + } + + try { + $v_iterableArray = $this->n4->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->ie1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n5->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::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->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::class, $e); + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::class, $e); + } + + try { + $v_defaultArray = $this->n2->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::class, $e); + } + + try { + $v_listArray = $this->n3->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::class, $e); + } + + try { + $v_iterableArray = $this->n4->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->ie1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n5->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::class, $e); + } + + return [ + 'status' => $v_status, + 'profileCreated' => $v_profileCreated, + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'skillsHashMap' => $v_skillsHashMap, + 'jsonArray' => $object->jsonArray, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('status', $data)) { + try { + $value = $this->n0->denormalize($data['status'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::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->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n1->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::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->n2->denormalize($data['defaultArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::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->n3->denormalize($data['listArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::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->n4->denormalize($data['iterableArray'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::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->ih1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = \is_array($item) ? $this->x1->hydrateNested($item, $context) : $this->n6->denormalize($item, $context); + } + + $value = $items; + } else { + $value = $this->n5->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class; + + try { + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::class, $e); + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::class, $e); + } + + try { + $v_defaultArray = $this->n2->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::class, $e); + } + + try { + $v_listArray = $this->n3->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::class, $e); + } + + try { + $v_iterableArray = $this->n4->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->ie1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n5->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::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->objects[$id]); + } + } + + try { + $v_status = $this->n0->normalize($object->status, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'status', $this->n0::class, $e); + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer ? $this->x0->extractNested($value, $context) : $this->n1->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'profileCreated', $this->n1::class, $e); + } + + try { + $v_defaultArray = $this->n2->normalize($object->defaultArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'defaultArray', $this->n2::class, $e); + } + + try { + $v_listArray = $this->n3->normalize($object->listArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'listArray', $this->n3::class, $e); + } + + try { + $v_iterableArray = $this->n4->normalize($object->iterableArray, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'iterableArray', $this->n4::class, $e); + } + + try { + $value = $object->skillsHashMap; + + if ($this->ie1 && $inline && \is_array($value)) { + $items = []; + + foreach ($value as $key => $item) { + $items[$key] = $item instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill ? $this->x1->extractNested($item, $context) : $this->n6->normalize($item, $context); + } + + $v_skillsHashMap = $items; + } else { + $v_skillsHashMap = $this->n5->normalize($value, $context); + } + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\NormalizerInBaseClassDefinedDto::class, 'skillsHashMap', $this->n5::class, $e); + } + + return [ + 'status' => $v_status, + 'profileCreated' => $v_profileCreated, + 'defaultArray' => $v_defaultArray, + 'listArray' => $v_listArray, + 'iterableArray' => $v_iterableArray, + 'skillsHashMap' => $v_skillsHashMap, + 'jsonArray' => $object->jsonArray, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('status'); + $this->n1 = $this->normalizer('profileCreated'); + $h0 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Direction::Hydrate); + $e0 = $this->nested($this->n1, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithNormalizer::class, Direction::Extract); + $this->ih0 = $h0 !== null; + $this->ie0 = $e0 !== null; + if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + $this->n2 = $this->normalizer('defaultArray'); + $this->n3 = $this->normalizer('listArray'); + $this->n4 = $this->normalizer('iterableArray'); + $this->n5 = $this->normalizer('skillsHashMap'); + $h1 = $this->nested($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Direction::Hydrate); + $e1 = $this->nested($this->n5, \Patchlevel\Hydrator\Tests\Unit\Fixture\Skill::class, Direction::Extract); + $this->ih1 = $h1 !== null; + $this->ie1 = $e1 !== null; + if ($this->ih1 || $this->ie1) { $this->x1 = $h1 ?? $e1; } + if ($this->ih1 || $this->ie1) { $this->n6 = $this->n5->innerNormalizer(); } + if (isset($this->x0)) { $this->x0->init(); } + if (isset($this->x1)) { $this->x1->init(); } + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n1)) + || self::mayRecurse($this->n2) + || self::mayRecurse($this->n3) + || self::mayRecurse($this->n4) + || ($this->ie1 ? $this->x1->recursive() : self::mayRecurse($this->n5)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php new file mode 100644 index 00000000..6b4d7598 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ParentDtoTransformer.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->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\ParentDto::class, 'profileId', $this->n0::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->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n1::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::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\ParentDto::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\ParentDto::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$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\ParentDto::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\ParentDto::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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\ParentDto::class, 'profileId', $this->n0::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->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::class, 'email', $this->n1::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ParentDto::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\ParentDto::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\ParentDto::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$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\ParentDto::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\ParentDto::class, 'email', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'email' => $v_email, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoTransformer.php new file mode 100644 index 00000000..107275a9 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/PrivateChildDtoTransformer.php @@ -0,0 +1,211 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = false; + + return ($this->h)($object, $data, $context, $inline); + } + + public function extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = false; + + return ($this->e)($object, $context, $inline); + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + return ($this->h)($object, $data, $context, $inline); + } + + public function extractNested(object $object, array $context): array + { + $inline = true; + + return ($this->e)($object, $context, $inline); + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + $this->hp0 = Closure::bind(function (object $object, array $data, array $context, bool $inline): void { + 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\PrivateChildDto::class, 'email', $this->n1::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->ep1 = Closure::bind(function (object $object, array $context, bool $inline): array { + 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\PrivateChildDto::class, 'email', $this->n1::class, $e); + } + return [$v_email, $object->note]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateParentDto::class); + $this->h = Closure::bind(function (object $object, array $data, array $context, bool $inline): object { + 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\PrivateChildDto::class, 'profileId', $this->n0::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, $inline); + + return $object; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + $this->e = Closure::bind(function (object $object, array $context, bool $inline): array { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class; + + try { + [$v_email, $v_note] = ($this->ep1)($object, $context, $inline); + + 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\PrivateChildDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'email' => $v_email, + 'note' => $v_note, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + [$v_email, $v_note] = ($this->ep1)($object, $context, $inline); + + 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\PrivateChildDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'email' => $v_email, + 'note' => $v_note, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivateChildDto::class); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php b/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php new file mode 100644 index 00000000..98caadf8 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/PrivatePropertiesDtoTransformer.php @@ -0,0 +1,176 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = false; + + return ($this->h)($object, $data, $context, $inline); + } + + public function extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = false; + + return ($this->e)($object, $context, $inline); + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + return ($this->h)($object, $data, $context, $inline); + } + + public function extractNested(object $object, array $context): array + { + $inline = true; + + return ($this->e)($object, $context, $inline); + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->h = Closure::bind(function (object $object, array $data, array $context, bool $inline): object { + 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\PrivatePropertiesDto::class, 'profileId', $this->n0::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->e = Closure::bind(function (object $object, array $context, bool $inline): array { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::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\PrivatePropertiesDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'age' => $object->age, + ]; + } finally { + unset($this->callStack->objects[$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\PrivatePropertiesDto::class, 'profileId', $this->n0::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'name' => $object->name, + 'age' => $object->age, + ]; + }, $this, \Patchlevel\Hydrator\Tests\Unit\Fixture\PrivatePropertiesDto::class); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php new file mode 100644 index 00000000..a1e00e1c --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedTransformer.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$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->objects[$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, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$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->objects[$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, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerTransformer.php new file mode 100644 index 00000000..897c3afd --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWithInlineNormalizerTransformer.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::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->n1->denormalize($data['valueObject'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_valueObject = $this->n1->normalize($object->valueObject, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'valueObject' => $v_valueObject, + ]; + } finally { + unset($this->callStack->objects[$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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_valueObject = $this->n1->normalize($object->valueObject, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'valueObject' => $v_valueObject, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::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->n1->denormalize($data['valueObject'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_valueObject = $this->n1->normalize($object->valueObject, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'valueObject' => $v_valueObject, + ]; + } finally { + unset($this->callStack->objects[$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\ProfileCreatedWithInlineNormalizer::class, 'profileId', $this->n0::class, $e); + } + + try { + $v_valueObject = $this->n1->normalize($object->valueObject, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWithInlineNormalizer::class, 'valueObject', $this->n1::class, $e); + } + + return [ + 'profileId' => $v_profileId, + 'valueObject' => $v_valueObject, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('valueObject'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperTransformer.php b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperTransformer.php new file mode 100644 index 00000000..4a7c45e4 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/ProfileCreatedWrapperTransformer.php @@ -0,0 +1,220 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if (\array_key_exists('event', $data)) { + try { + $value = $data['event']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + $inline = ($context[Hydrator::HYDRATOR] ?? null) === $this->owner; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class; + + try { + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::class, $e); + } + + return [ + 'event' => $v_profileCreated, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::class, $e); + } + + return [ + 'event' => $v_profileCreated, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + $inline = true; + + if (\array_key_exists('event', $data)) { + try { + $value = $data['event']; + + if ($this->ih0 && $inline && \is_array($value)) { + $value = $this->x0->hydrateNested($value, $context); + } else { + $value = $this->n0->denormalize($value, $context); + } + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::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 extractNested(object $object, array $context): array + { + $inline = true; + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class; + + try { + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::class, $e); + } + + return [ + 'event' => $v_profileCreated, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $value = $object->profileCreated; + $v_profileCreated = $this->ie0 && $inline && $value instanceof \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated ? $this->x0->extractNested($value, $context) : $this->n0->normalize($value, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreatedWrapper::class, 'profileCreated', $this->n0::class, $e); + } + + return [ + 'event' => $v_profileCreated, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileCreated'); + $h0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Direction::Hydrate); + $e0 = $this->nested($this->n0, \Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated::class, Direction::Extract); + $this->ih0 = $h0 !== null; + $this->ie0 = $e0 !== null; + if ($this->ih0 || $this->ie0) { $this->x0 = $h0 ?? $e0; } + if (isset($this->x0)) { $this->x0->init(); } + } + + protected function mayReachItself(): bool + { + return ($this->ie0 ? $this->x0->recursive() : self::mayRecurse($this->n0)); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php b/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php new file mode 100644 index 00000000..8d547a87 --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/SensitiveDataProfileCreatedTransformer.php @@ -0,0 +1,254 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('id', $data)) { + try { + $value = $this->n0->denormalize($data['id'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n0::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->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'id' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'id' => $v_profileId, + 'email' => $v_email, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('id', $data)) { + try { + $value = $this->n0->denormalize($data['id'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'profileId', $this->n0::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->n1->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'id' => $v_profileId, + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::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\Extension\Cryptography\Fixture\SensitiveDataProfileCreated::class, 'email', $this->n1::class, $e); + } + + return [ + 'id' => $v_profileId, + 'email' => $v_email, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('profileId'); + $this->n1 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0) + || self::mayRecurse($this->n1); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerTransformer.php b/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerTransformer.php new file mode 100644 index 00000000..d31e410a --- /dev/null +++ b/tests/Unit/Extension/Generated/Snapshot/WrongNormalizerTransformer.php @@ -0,0 +1,187 @@ +ready) { + $this->init(); + } + + if (isset($context[Hydrator::OBJECT_TO_POPULATE])) { + $object = $context[Hydrator::OBJECT_TO_POPULATE]; + unset($context[Hydrator::OBJECT_TO_POPULATE]); + } else { + $object = $this->reflection->newInstanceWithoutConstructor(); + } + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n0->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::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 extract(object $object, array $context): array + { + if (!$this->ready) { + $this->init(); + } + + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class; + + try { + try { + $v_email = $this->n0->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::class, $e); + } + + return [ + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_email = $this->n0->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::class, $e); + } + + return [ + 'email' => $v_email, + ]; + } + + public function hydrateNested(array $data, array $context): object + { + $object = $this->reflection->newInstanceWithoutConstructor(); + + if (\array_key_exists('email', $data)) { + try { + $value = $this->n0->denormalize($data['email'], $context); + } catch (Throwable $e) { + throw new DenormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::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 extractNested(object $object, array $context): array + { + if ($this->tracked) { + $id = \spl_object_id($object); + + if (isset($this->callStack->objects[$id])) { + $references = \array_values($this->callStack->objects); + $references[] = $object::class; + + throw new CircularReference($references); + } + + $this->callStack->objects[$id] = \Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class; + + try { + try { + $v_email = $this->n0->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::class, $e); + } + + return [ + 'email' => $v_email, + ]; + } finally { + unset($this->callStack->objects[$id]); + } + } + + try { + $v_email = $this->n0->normalize($object->email, $context); + } catch (CircularReference $e) { + throw $e; + } catch (Throwable $e) { + throw new NormalizationFailure(\Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer::class, 'email', $this->n0::class, $e); + } + + return [ + 'email' => $v_email, + ]; + } + + protected function initialize(): void + { + $this->n0 = $this->normalizer('email'); + } + + protected function mayReachItself(): bool + { + return self::mayRecurse($this->n0); + } +} \ No newline at end of file diff --git a/tests/Unit/Extension/Generated/TransformerGeneratorTest.php b/tests/Unit/Extension/Generated/TransformerGeneratorTest.php new file mode 100644 index 00000000..5447b586 --- /dev/null +++ b/tests/Unit/Extension/Generated/TransformerGeneratorTest.php @@ -0,0 +1,216 @@ +metadata(ProfileCreated::class); + + $first = (new TransformerGenerator())->generate($metadata, 'Foo\\Bar'); + $second = (new TransformerGenerator())->generate($metadata, 'Foo\\Bar'); + + self::assertSame($first->code, $second->code); + self::assertStringContainsString('namespace Foo;', $first->code); + self::assertStringContainsString('final class Bar extends GeneratedTransformer', $first->code); + self::assertStringNotContainsString('strict_types=1', $first->code); + } + + public function testNestedClasses(): void + { + $code = (new TransformerGenerator())->generate( + (new AttributeMetadataFactory())->metadata(NestedLifecycleDto::class), + 'Foo\\Bar', + ); + + self::assertSame([LifecycleFixture::class], $code->nested); + } + + /** + * The snapshots make every change of the generated code visible in the diff of a pull request. + * Run "make snapshot" after an intended change of the generator. + */ + #[RequiresPhp('>=8.5')] + public function testGeneratedCodeMatchesSnapshot(): void + { + $metadataFactory = new AttributeMetadataFactory(); + + foreach (GeneratedStackHydratorTest::CLASSES as $class) { + $metadata = $metadataFactory->metadata($class); + $className = $metadata->reflection->getShortName() . 'Transformer'; + $file = __DIR__ . '/Snapshot/' . $className . '.php'; + + $code = (new TransformerGenerator())->generate($metadata, 'Patchlevel\\Hydrator\\Tests\\Snapshot\\' . $className)->code; + + 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 testDefaultsWithControlCharactersAreInlinedUnchanged(): void + { + $transformer = $this->load((new AttributeMetadataFactory())->metadata(MultilineDefaultDto::class)); + + $object = $transformer->hydrate([], []); + + self::assertInstanceOf(MultilineDefaultDto::class, $object); + self::assertSame("foo bar\nbaz", $object->name); + self::assertSame("first\nsecond", $object->text); + self::assertSame(["a\tb" => "c\nd \$e \"f\""], $object->lines); + } + + public function testFailedInitializationIsRetried(): void + { + $resolver = $this->createMock(TransformerResolver::class); + $resolver + ->expects($this->exactly(2)) + ->method('hydrator') + ->willReturnOnConsecutiveCalls( + self::throwException(new RuntimeException('not ready')), + $this->createStub(Hydrator::class), + ); + + $metadata = (new AttributeMetadataFactory())->metadata(ProfileCreated::class); + $transformer = $this->load($metadata, $resolver); + $data = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + + try { + $transformer->hydrate($data, []); + self::fail('the first call must fail'); + } catch (RuntimeException) { + } + + // the failed initialization must not leave the transformer half initialized + $object = $transformer->hydrate($data, []); + + self::assertInstanceOf(ProfileCreated::class, $object); + self::assertSame('info@patchlevel.de', $object->email->toString()); + } + + /** @param class-string $changed a class with the same properties as FingerprintDto but one changed detail */ + #[DataProvider('changedClasses')] + public function testChangedClassChangesTheFingerprint(string $changed): void + { + $metadataFactory = new AttributeMetadataFactory(); + + self::assertNotSame( + ClassFingerprint::of($metadataFactory->metadata(FingerprintDto::class)), + ClassFingerprint::of($metadataFactory->metadata($changed)), + ); + } + + /** @return iterable */ + public static function changedClasses(): iterable + { + yield 'changed default' => [FingerprintChangedDefaultDto::class]; + yield 'changed visibility' => [FingerprintChangedVisibilityDto::class]; + } + + public function testClassNormalizerIsNotGeneratable(): void + { + $this->expectException(ClassNotGeneratable::class); + + (new TransformerGenerator())->generate( + (new AttributeMetadataFactory())->metadata(ProfileId::class), + 'Foo\\Bar', + ); + } + + public function testAnonymousClassIsNotGeneratable(): void + { + $object = new class { + public string $name = 'foo'; + }; + + $this->expectException(ClassNotGeneratable::class); + + (new TransformerGenerator())->generate((new AttributeMetadataFactory())->metadata($object::class), 'Foo\\Bar'); + } + + /** + * @param ClassMetadata $metadata + * + * @template T of object + */ + private function load(ClassMetadata $metadata, TransformerResolver|null $resolver = null): GeneratedTransformer + { + $fqcn = 'Patchlevel\\Hydrator\\Tests\\Generated\\Transformer' . self::$counter++; + $code = (new TransformerGenerator())->generate($metadata, $fqcn)->code; + + $file = tempnam(sys_get_temp_dir(), 'transformer'); + assert($file !== false); + file_put_contents($file, $code); + + require $file; + unlink($file); + + assert(is_a($fqcn, GeneratedTransformer::class, true)); + + if ($resolver === null) { + $resolver = $this->createStub(TransformerResolver::class); + $resolver->method('hydrator')->willReturn($this->createStub(Hydrator::class)); + } + + return new $fqcn($metadata, $resolver, new CallStack()); + } +} 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, Next $next): object + { + $this->hydrated[$metadata->className] = ($this->hydrated[$metadata->className] ?? 0) + 1; + + return $next->hydrate($metadata, $data, $context); + } + + /** + * @param ClassMetadata $metadata + * @param T $object + * @param array $context + * + * @return array + * + * @template T of object + */ + public function extract(ClassMetadata $metadata, object $object, array $context, Next $next): array + { + $this->extracted[$metadata->className] = ($this->extracted[$metadata->className] ?? 0) + 1; + + return $next->extract($metadata, $object, $context); + } + + /** + * @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/FingerprintChangedDefaultDto.php b/tests/Unit/Fixture/FingerprintChangedDefaultDto.php new file mode 100644 index 00000000..1f05ab67 --- /dev/null +++ b/tests/Unit/Fixture/FingerprintChangedDefaultDto.php @@ -0,0 +1,20 @@ +text; + } +} diff --git a/tests/Unit/Fixture/FingerprintChangedVisibilityDto.php b/tests/Unit/Fixture/FingerprintChangedVisibilityDto.php new file mode 100644 index 00000000..0d78638a --- /dev/null +++ b/tests/Unit/Fixture/FingerprintChangedVisibilityDto.php @@ -0,0 +1,20 @@ +text; + } +} diff --git a/tests/Unit/Fixture/FingerprintDto.php b/tests/Unit/Fixture/FingerprintDto.php new file mode 100644 index 00000000..5410b336 --- /dev/null +++ b/tests/Unit/Fixture/FingerprintDto.php @@ -0,0 +1,20 @@ +text; + } +} diff --git a/tests/Unit/Fixture/MultilineDefaultDto.php b/tests/Unit/Fixture/MultilineDefaultDto.php new file mode 100644 index 00000000..3df847f0 --- /dev/null +++ b/tests/Unit/Fixture/MultilineDefaultDto.php @@ -0,0 +1,21 @@ + $lines */ + public function __construct( + // phpcs:disable Squiz.Functions.MultiLineFunctionDeclaration.Indent -- the sniff does not understand the nowdoc body + public string $name = <<<'TEXT' + foo bar + baz + TEXT, + // phpcs:enable + public string $text = "first\nsecond", + public array $lines = ["a\tb" => "c\nd \$e \"f\""], + ) { + } +} 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/Fixture/UpperCaseSkillTransformerFactory.php b/tests/Unit/Fixture/UpperCaseSkillTransformerFactory.php index 37825b7e..522435b8 100644 --- a/tests/Unit/Fixture/UpperCaseSkillTransformerFactory.php +++ b/tests/Unit/Fixture/UpperCaseSkillTransformerFactory.php @@ -7,6 +7,7 @@ use Patchlevel\Hydrator\Metadata\ClassMetadata; use Patchlevel\Hydrator\Transformer\ClassTransformer; use Patchlevel\Hydrator\Transformer\ClassTransformerFactory; +use Patchlevel\Hydrator\Transformer\TransformerResolver; use function assert; use function is_string; @@ -17,7 +18,7 @@ final class UpperCaseSkillTransformerFactory implements ClassTransformerFactory { public int $created = 0; - public function create(ClassMetadata $metadata): ClassTransformer|null + public function create(ClassMetadata $metadata, TransformerResolver $resolver): ClassTransformer|null { if ($metadata->className !== Skill::class) { return null; diff --git a/tests/Unit/StackHydratorTest.php b/tests/Unit/StackHydratorTest.php index 74903b16..01f73ca6 100644 --- a/tests/Unit/StackHydratorTest.php +++ b/tests/Unit/StackHydratorTest.php @@ -46,8 +46,12 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\ValueObject; use Patchlevel\Hydrator\Tests\Unit\Fixture\WrongNormalizer; use Patchlevel\Hydrator\Transformer\ChainTransformerFactory; +use Patchlevel\Hydrator\Transformer\ClassTransformer; +use Patchlevel\Hydrator\Transformer\ClassTransformerFactory; +use Patchlevel\Hydrator\Transformer\Direction; use Patchlevel\Hydrator\Transformer\ReflectionTransformer; use Patchlevel\Hydrator\Transformer\ReflectionTransformerFactory; +use Patchlevel\Hydrator\Transformer\TransformerResolver; use Patchlevel\Hydrator\TypeMismatch; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\RequiresPhp; @@ -855,6 +859,115 @@ public function testAllMiddlewaresSkipped(): void self::assertSame(['profileId' => '1', 'email' => 'info@patchlevel.de'], $hydrator->extract($event)); } + public function testTransformerResolver(): void + { + $factory = new class implements ClassTransformerFactory { + public TransformerResolver|null $resolver = null; + + public function create(ClassMetadata $metadata, TransformerResolver $resolver): ClassTransformer|null + { + $this->resolver = $resolver; + + return null; + } + }; + + $middleware = new class implements SkippableMiddleware { + /** + * @param ClassMetadata $metadata + * @param array $data + * @param array $context + * + * @return T + * + * @template T of object + */ + public function hydrate(ClassMetadata $metadata, array $data, array $context, Next $next): object + { + return $next->hydrate($metadata, $data, $context); + } + + /** + * @param ClassMetadata $metadata + * @param T $object + * @param array $context + * + * @return array + * + * @template T of object + */ + public function extract(ClassMetadata $metadata, object $object, array $context, Next $next): array + { + return $next->extract($metadata, $object, $context); + } + + public function skip(ClassMetadata $metadata): Skip + { + return $metadata->className === Skill::class ? Skip::Extract : Skip::Both; + } + }; + + $hydrator = new StackHydrator( + middlewares: [$middleware], + transformerFactory: new ChainTransformerFactory([$factory, new ReflectionTransformerFactory()]), + ); + + $hydrator->hydrate(ProfileCreated::class, ['profileId' => '1', 'email' => 'info@patchlevel.de']); + + $resolver = $factory->resolver; + self::assertInstanceOf(TransformerResolver::class, $resolver); + self::assertSame($hydrator, $resolver->hydrator()); + self::assertSame($hydrator->metadata(ProfileCreated::class), $resolver->metadata(ProfileCreated::class)); + + // no middleware runs for the class + self::assertInstanceOf(ReflectionTransformer::class, $resolver->direct(ProfileCreated::class, Direction::Hydrate)); + self::assertInstanceOf(ReflectionTransformer::class, $resolver->direct(ProfileCreated::class, Direction::Extract)); + + // the middleware only runs while hydrating + self::assertNull($resolver->direct(Skill::class, Direction::Hydrate)); + self::assertInstanceOf(ReflectionTransformer::class, $resolver->direct(Skill::class, Direction::Extract)); + + // the class normalizer is called instead of a transformer + self::assertNull($resolver->direct(ProfileId::class, Direction::Hydrate)); + self::assertNull($resolver->direct(ProfileId::class, Direction::Extract)); + } + + #[RequiresPhp('>=8.4')] + public function testTransformerResolverWithLazyClass(): void + { + $factory = new class implements ClassTransformerFactory { + public TransformerResolver|null $resolver = null; + + public function create(ClassMetadata $metadata, TransformerResolver $resolver): ClassTransformer|null + { + $this->resolver = $resolver; + + return null; + } + }; + + $hydrator = new StackHydrator( + transformerFactory: new ChainTransformerFactory([$factory, new ReflectionTransformerFactory()]), + ); + $hydrator->extract(new Skill('php')); + + $resolver = $factory->resolver; + self::assertInstanceOf(TransformerResolver::class, $resolver); + + // lazy objects are only created while hydrating + self::assertNull($resolver->direct(LazyProfileCreated::class, Direction::Hydrate)); + self::assertInstanceOf(ReflectionTransformer::class, $resolver->direct(LazyProfileCreated::class, Direction::Extract)); + } + + public function testEmptyContextGetsTheHydrator(): void + { + $hydrator = new StackHydrator(); + + // the same context is reused for calls without context, it must not leak values between calls + self::assertSame(['value' => 'ctx-value'], $hydrator->extract(new ContextAwareDto('value'), ['prefix' => 'ctx-'])); + self::assertSame(['value' => 'value'], $hydrator->extract(new ContextAwareDto('value'))); + } + public function testTransformerFactory(): void { $factory = new UpperCaseSkillTransformerFactory(); diff --git a/tests/Unit/Transformer/ReflectionTransformerTest.php b/tests/Unit/Transformer/ReflectionTransformerTest.php index 80fd77f1..d0b44e96 100644 --- a/tests/Unit/Transformer/ReflectionTransformerTest.php +++ b/tests/Unit/Transformer/ReflectionTransformerTest.php @@ -9,6 +9,7 @@ use Patchlevel\Hydrator\Tests\Unit\Fixture\Skill; use Patchlevel\Hydrator\Transformer\ReflectionTransformer; use Patchlevel\Hydrator\Transformer\ReflectionTransformerFactory; +use Patchlevel\Hydrator\Transformer\TransformerResolver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -40,7 +41,10 @@ public function testHydrateObjectToPopulate(): void public function testFactoryCreatesReflectionTransformer(): void { $factory = new ReflectionTransformerFactory(); - $transformer = $factory->create((new AttributeMetadataFactory())->metadata(Skill::class)); + $transformer = $factory->create( + (new AttributeMetadataFactory())->metadata(Skill::class), + $this->createStub(TransformerResolver::class), + ); self::assertEquals(new Skill('php'), $transformer->hydrate(['name' => 'php'], [])); }