diff --git a/.gitignore b/.gitignore index b0f6ffa0..f1cfc6cf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ phpstan.neon infection.log infection.html .phpbench/ +var +stub diff --git a/composer.json b/composer.json index 9e7bc517..771f5c63 100644 --- a/composer.json +++ b/composer.json @@ -26,13 +26,18 @@ "symfony/type-info": "^7.3.0 || ^8.0.0" }, "require-dev": { + "crell/serde": "^1.5", + "eventsauce/object-hydrator": "^1.8", "infection/infection": "^0.32.4", + "jms/serializer": "^3.32", "patchlevel/coding-standard": "^1.3.0", "phpat/phpat": "^0.12.2", "phpbench/phpbench": "^1.4.3", "phpstan/phpstan": "^2.1.39", "phpstan/phpstan-phpunit": "^2.0.15", "phpunit/phpunit": "^11.5.53", + "symfony/serializer": "^8.0", + "symfony/property-access": "^8.0", "symfony/var-dumper": "^5.4.29 || ^6.4.0 || ^7.0.0 || ^8.0.0" }, "config": { diff --git a/composer.lock b/composer.lock index d05c6e85..ff3bff7a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9474f60489f2575f2b839a88df24af8b", + "content-hash": "1178cb728ed19ed136143e867ad32fce", "packages": [ { "name": "psr/cache", @@ -161,16 +161,16 @@ }, { "name": "symfony/type-info", - "version": "v8.0.7", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/type-info.git", - "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195" + "reference": "622d81551770029d44d16be68969712eb47892f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/type-info/zipball/3c7de103dd6cb68be24e155838a64ef4a70ae195", - "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195", + "url": "https://api.github.com/repos/symfony/type-info/zipball/622d81551770029d44d16be68969712eb47892f1", + "reference": "622d81551770029d44d16be68969712eb47892f1", "shasum": "" }, "require": { @@ -219,7 +219,7 @@ "type" ], "support": { - "source": "https://github.com/symfony/type-info/tree/v8.0.7" + "source": "https://github.com/symfony/type-info/tree/v8.0.8" }, "funding": [ { @@ -239,7 +239,7 @@ "type": "tidelift" } ], - "time": "2026-03-04T13:55:34+00:00" + "time": "2026-03-30T15:14:47+00:00" } ], "packages-dev": [ @@ -476,6 +476,216 @@ ], "time": "2024-05-06T16:37:16+00:00" }, + { + "name": "crell/attributeutils", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/Crell/AttributeUtils.git", + "reference": "b89ad9cf419fa5ae3cd81880af15d0ba4a98aa8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Crell/AttributeUtils/zipball/b89ad9cf419fa5ae3cd81880af15d0ba4a98aa8c", + "reference": "b89ad9cf419fa5ae3cd81880af15d0ba4a98aa8c", + "shasum": "" + }, + "require": { + "crell/fp": "~1.0.0", + "php": "~8.1" + }, + "require-dev": { + "fig/cache-util": "^2.0", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "~2", + "phpunit/phpunit": "~10.5", + "psr/cache": "^3.0" + }, + "suggest": { + "psr/cache": "Caching analyzer rests is recommended, and a bridge for psr/cache is included." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Crell\\AttributeUtils\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Larry Garfield", + "email": "larry@garfieldtech.com", + "homepage": "http://www.garfieldtech.com/", + "role": "Developer" + } + ], + "description": "A robust, flexible attribute handling framework", + "homepage": "https://github.com/Crell/AttributeUtils", + "keywords": [ + "attributes", + "reflection" + ], + "support": { + "issues": "https://github.com/Crell/AttributeUtils/issues", + "source": "https://github.com/Crell/AttributeUtils/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/Crell", + "type": "github" + } + ], + "time": "2025-07-14T16:51:30+00:00" + }, + { + "name": "crell/fp", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Crell/fp.git", + "reference": "796a1eaa3e2d841f93359f9ef64e45326b68af87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Crell/fp/zipball/796a1eaa3e2d841f93359f9ef64e45326b68af87", + "reference": "796a1eaa3e2d841f93359f9ef64e45326b68af87", + "shasum": "" + }, + "require": { + "php": "~8.1" + }, + "require-dev": { + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "~10.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "files": [ + "src/array.php", + "src/composition.php", + "src/object.php", + "src/string.php" + ], + "psr-4": { + "Crell\\fp\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Larry Garfield", + "email": "larry@garfieldtech.com", + "homepage": "http://www.garfieldtech.com/", + "role": "Developer" + } + ], + "description": "Functional utilities for PHP 8 and later", + "homepage": "https://github.com/Crell/fp", + "keywords": [ + "array", + "fp", + "functional" + ], + "support": { + "issues": "https://github.com/Crell/fp/issues", + "source": "https://github.com/Crell/fp/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/Crell", + "type": "github" + } + ], + "time": "2023-10-28T20:06:08+00:00" + }, + { + "name": "crell/serde", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/Crell/Serde.git", + "reference": "aefce5d7030d6ea76f86b67f029c35fcd1774ad0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Crell/Serde/zipball/aefce5d7030d6ea76f86b67f029c35fcd1774ad0", + "reference": "aefce5d7030d6ea76f86b67f029c35fcd1774ad0", + "shasum": "" + }, + "require": { + "crell/attributeutils": "~1.3", + "crell/fp": "~1.0", + "php": "~8.2" + }, + "require-dev": { + "devium/toml": "^1.0.5", + "phpbench/phpbench": "^1.3.0", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "~10.5", + "symfony/yaml": "^5.4" + }, + "suggest": { + "devium/toml": "Enables serializing to/from TOML files.", + "symfony/yaml": "Enables serializing to/from YAML files." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "Crell\\Serde\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Larry Garfield", + "email": "larry@garfieldtech.com", + "homepage": "http://www.garfieldtech.com/", + "role": "Developer" + } + ], + "description": "A general purpose serialization and deserialization library", + "homepage": "https://github.com/Crell/Serde", + "keywords": [ + "Serde", + "database" + ], + "support": { + "issues": "https://github.com/Crell/Serde/issues", + "source": "https://github.com/Crell/Serde/tree/1.5.0" + }, + "funding": [ + { + "url": "https://github.com/Crell", + "type": "github" + } + ], + "time": "2025-07-15T15:56:00+00:00" + }, { "name": "dealerdirect/phpcodesniffer-composer-installer", "version": "v1.2.0", @@ -649,6 +859,75 @@ "abandoned": true, "time": "2024-09-05T10:17:24+00:00" }, + { + "name": "doctrine/instantiator", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/23da848e1a2308728fe5fdddabf4be17ff9720c7", + "reference": "23da848e1a2308728fe5fdddabf4be17ff9720c7", + "shasum": "" + }, + "require": { + "php": "^8.4" + }, + "require-dev": { + "doctrine/coding-standard": "^14", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5.58" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2026-01-05T06:47:08+00:00" + }, { "name": "doctrine/lexer", "version": "3.0.1", @@ -726,6 +1005,70 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "eventsauce/object-hydrator", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/EventSaucePHP/ObjectHydrator.git", + "reference": "29f66149d2b0c57f356ad4fa6dd5f88821d04d9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/EventSaucePHP/ObjectHydrator/zipball/29f66149d2b0c57f356ad4fa6dd5f88821d04d9f", + "reference": "29f66149d2b0c57f356ad4fa6dd5f88821d04d9f", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "league/construct-finder": "^1.6", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^9.5.11", + "ramsey/uuid": "^4.2" + }, + "suggest": { + "league/construct-finder": "Find all classes in a directory for the best dumped hydrators." + }, + "type": "library", + "autoload": { + "psr-4": { + "EventSauce\\ObjectHydrator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Converts structured data into strict objects.", + "keywords": [ + "construction", + "constructor", + "hydration", + "mapper" + ], + "support": { + "issues": "https://github.com/EventSaucePHP/ObjectHydrator/issues", + "source": "https://github.com/EventSaucePHP/ObjectHydrator/tree/1.8.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2026-02-13T21:06:58+00:00" + }, { "name": "fidry/cpu-core-counter", "version": "1.3.0", @@ -1153,18 +1496,183 @@ ], "time": "2025-04-29T08:19:52+00:00" }, + { + "name": "jms/metadata", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/metadata.git", + "reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/metadata/zipball/554319d2e5f0c5d8ccaeffe755eac924e14da330", + "reference": "554319d2e5f0c5d8ccaeffe755eac924e14da330", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "require-dev": { + "doctrine/cache": "^1.0|^2.0", + "doctrine/coding-standard": "^8.0", + "mikey179/vfsstream": "^1.6.7", + "phpunit/phpunit": "^8.5.42|^9.6.23", + "psr/container": "^1.0|^2.0", + "symfony/cache": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0", + "symfony/dependency-injection": "^3.1|^4.0|^5.0|^6.0|^7.0|^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Metadata\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "Class/method/property metadata management in PHP", + "keywords": [ + "annotations", + "metadata", + "xml", + "yaml" + ], + "support": { + "issues": "https://github.com/schmittjoh/metadata/issues", + "source": "https://github.com/schmittjoh/metadata/tree/2.9.0" + }, + "time": "2025-11-30T20:12:26+00:00" + }, + { + "name": "jms/serializer", + "version": "3.32.7", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/serializer.git", + "reference": "d725ebd288688bb24f47ee467b1299b0fc6d04f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/serializer/zipball/d725ebd288688bb24f47ee467b1299b0fc6d04f8", + "reference": "d725ebd288688bb24f47ee467b1299b0fc6d04f8", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2.0", + "doctrine/lexer": "^2.0 || ^3.0", + "jms/metadata": "^2.6", + "php": "^7.4 || ^8.0", + "phpstan/phpdoc-parser": "^1.20 || ^2.0" + }, + "require-dev": { + "doctrine/annotations": "^1.14 || ^2.0", + "doctrine/coding-standard": "^12.0", + "doctrine/orm": "^2.14 || ^3.0", + "doctrine/persistence": "^2.5.2 || ^3.0", + "doctrine/phpcr-odm": "^1.5.2 || ^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.3", + "ocramius/proxy-manager": "^1.0 || ^2.0", + "phpbench/phpbench": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0", + "psr/container": "^1.0 || ^2.0", + "rector/rector": "^1.0.0 || ^2.0", + "symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/form": "^5.4.45 || ^6.4.27 || ^7.0 || ^8.0", + "symfony/translation": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/uid": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/validator": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0 || ^8.0", + "twig/twig": "^1.34 || ^2.4 || ^3.0" + }, + "suggest": { + "doctrine/collections": "Required if you like to use doctrine collection types as ArrayCollection.", + "symfony/cache": "Required if you like to use cache functionality.", + "symfony/uid": "Required if you'd like to serialize UID objects.", + "symfony/yaml": "Required if you'd like to use the YAML metadata format." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "JMS\\Serializer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "Library for (de-)serializing data of any complexity; supports XML, and JSON.", + "homepage": "http://jmsyst.com/libs/serializer", + "keywords": [ + "deserialization", + "jaxb", + "json", + "serialization", + "xml" + ], + "support": { + "issues": "https://github.com/schmittjoh/serializer/issues", + "source": "https://github.com/schmittjoh/serializer/tree/3.32.7" + }, + "funding": [ + { + "url": "https://github.com/goetas", + "type": "github" + }, + { + "url": "https://github.com/scyzoryck", + "type": "github" + } + ], + "time": "2026-03-11T20:11:17+00:00" + }, { "name": "justinrainbow/json-schema", - "version": "v6.7.2", + "version": "6.8.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0" + "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/6fea66c7204683af437864e7c4e7abf383d14bc0", - "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", + "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc", "shasum": "" }, "require": { @@ -1224,9 +1732,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/v6.7.2" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.0" }, - "time": "2026-02-15T15:06:22+00:00" + "time": "2026-04-02T12:43:11+00:00" }, { "name": "marc-mabe/php-enum", @@ -1672,16 +2180,16 @@ }, { "name": "phpat/phpat", - "version": "0.12.3", + "version": "0.12.4", "source": { "type": "git", "url": "https://github.com/carlosas/phpat.git", - "reference": "2412a8959254a076e751498cbba8cf29406e0cf4" + "reference": "5319264270c335f548451209bb0f32b55aa59924" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/carlosas/phpat/zipball/2412a8959254a076e751498cbba8cf29406e0cf4", - "reference": "2412a8959254a076e751498cbba8cf29406e0cf4", + "url": "https://api.github.com/repos/carlosas/phpat/zipball/5319264270c335f548451209bb0f32b55aa59924", + "reference": "5319264270c335f548451209bb0f32b55aa59924", "shasum": "" }, "require": { @@ -1723,9 +2231,9 @@ "description": "PHP Architecture Tester", "support": { "issues": "https://github.com/carlosas/phpat/issues", - "source": "https://github.com/carlosas/phpat/tree/0.12.3" + "source": "https://github.com/carlosas/phpat/tree/0.12.4" }, - "time": "2026-02-20T11:15:22+00:00" + "time": "2026-03-17T16:47:43+00:00" }, { "name": "phpbench/container", @@ -1780,16 +2288,16 @@ }, { "name": "phpbench/phpbench", - "version": "1.5.1", + "version": "1.6.1", "source": { "type": "git", "url": "https://github.com/phpbench/phpbench.git", - "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c" + "reference": "661c8c6abbc7734986cf7bc6062c237fbb450461" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpbench/phpbench/zipball/9a28fd0833f11171b949843c6fd663eb69b6d14c", - "reference": "9a28fd0833f11171b949843c6fd663eb69b6d14c", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/661c8c6abbc7734986cf7bc6062c237fbb450461", + "reference": "661c8c6abbc7734986cf7bc6062c237fbb450461", "shasum": "" }, "require": { @@ -1867,7 +2375,7 @@ ], "support": { "issues": "https://github.com/phpbench/phpbench/issues", - "source": "https://github.com/phpbench/phpbench/tree/1.5.1" + "source": "https://github.com/phpbench/phpbench/tree/1.6.1" }, "funding": [ { @@ -1875,7 +2383,7 @@ "type": "github" } ], - "time": "2026-03-05T08:18:58+00:00" + "time": "2026-03-22T10:27:20+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -1926,11 +2434,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.40", + "version": "2.1.50", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", - "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d452086fb4cf648c6b2d8cf3b639351f79e4f3e2", + "reference": "d452086fb4cf648c6b2d8cf3b639351f79e4f3e2", "shasum": "" }, "require": { @@ -1975,7 +2483,7 @@ "type": "github" } ], - "time": "2026-02-23T15:04:35+00:00" + "time": "2026-04-17T13:10:32+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -4110,16 +4618,16 @@ }, { "name": "symfony/console", - "version": "v8.0.7", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" + "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", - "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7", + "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7", "shasum": "" }, "require": { @@ -4176,7 +4684,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.0.7" + "source": "https://github.com/symfony/console/tree/v8.0.8" }, "funding": [ { @@ -4196,7 +4704,7 @@ "type": "tidelift" } ], - "time": "2026-03-06T14:06:22+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/deprecation-contracts", @@ -4267,16 +4775,16 @@ }, { "name": "symfony/filesystem", - "version": "v8.0.6", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770" + "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/7bf9162d7a0dff98d079b72948508fa48018a770", - "reference": "7bf9162d7a0dff98d079b72948508fa48018a770", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/66b769ae743ce2d13e435528fbef4af03d623e5a", + "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a", "shasum": "" }, "require": { @@ -4313,7 +4821,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v8.0.6" + "source": "https://github.com/symfony/filesystem/tree/v8.0.8" }, "funding": [ { @@ -4333,20 +4841,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:59:43+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/finder", - "version": "v8.0.6", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" + "reference": "8da41214757b87d97f181e3d14a4179286151007" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", - "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "url": "https://api.github.com/repos/symfony/finder/zipball/8da41214757b87d97f181e3d14a4179286151007", + "reference": "8da41214757b87d97f181e3d14a4179286151007", "shasum": "" }, "require": { @@ -4381,7 +4889,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v8.0.6" + "source": "https://github.com/symfony/finder/tree/v8.0.8" }, "funding": [ { @@ -4401,20 +4909,20 @@ "type": "tidelift" } ], - "time": "2026-01-29T09:41:02+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/options-resolver", - "version": "v8.0.0", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" + "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8", + "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8", "shasum": "" }, "require": { @@ -4452,7 +4960,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.8" }, "funding": [ { @@ -4472,20 +4980,20 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:55:31+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -4535,7 +5043,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" }, "funding": [ { @@ -4555,20 +5063,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", "shasum": "" }, "require": { @@ -4617,7 +5125,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" }, "funding": [ { @@ -4637,11 +5145,11 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -4702,7 +5210,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" }, "funding": [ { @@ -4726,16 +5234,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", "shasum": "" }, "require": { @@ -4787,7 +5295,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" }, "funding": [ { @@ -4807,20 +5315,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-04-10T17:25:58+00:00" }, { "name": "symfony/polyfill-php85", - "version": "v1.33.0", + "version": "v1.36.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + "reference": "2c408a6bb0313e6001a83628dc5506100474254e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", - "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e", + "reference": "2c408a6bb0313e6001a83628dc5506100474254e", "shasum": "" }, "require": { @@ -4867,7 +5375,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0" }, "funding": [ { @@ -4887,20 +5395,20 @@ "type": "tidelift" } ], - "time": "2025-06-23T16:12:55+00:00" + "time": "2026-04-10T16:50:15+00:00" }, { "name": "symfony/process", - "version": "v8.0.5", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" + "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", - "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "url": "https://api.github.com/repos/symfony/process/zipball/cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", + "reference": "cb8939aff03470d1a9d1d1b66d08c6fa71b3bbdc", "shasum": "" }, "require": { @@ -4932,7 +5440,271 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v8.0.5" + "source": "https://github.com/symfony/process/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/property-access", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "704c7808116fcdd67327db7b17de56b8ef6169e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/704c7808116fcdd67327db7b17de56b8ef6169e4", + "reference": "704c7808116fcdd67327db7b17de56b8ef6169e4", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/property-info": "^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/property-info", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "c21711980653360d6ef5c26d0f9ca6f58a1135c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/c21711980653360d6ef5c26d0f9ca6f58a1135c6", + "reference": "c21711980653360d6ef5c26d0f9ca6f58a1135c6", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/string": "^7.4|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T15:14:47+00:00" + }, + { + "name": "symfony/serializer", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "0e3169be25dbf0c23686c8089662cee9dd714932" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/0e3169be25dbf0c23686c8089662cee9dd714932", + "reference": "0e3169be25dbf0c23686c8089662cee9dd714932", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/property-info": "<7.4", + "symfony/type-info": "<7.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/filesystem": "^7.4|^8.0", + "symfony/form": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/type-info": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0", + "symfony/var-exporter": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v8.0.8" }, "funding": [ { @@ -4952,7 +5724,7 @@ "type": "tidelift" } ], - "time": "2026-01-26T15:08:38+00:00" + "time": "2026-03-31T07:15:36+00:00" }, { "name": "symfony/service-contracts", @@ -5043,16 +5815,16 @@ }, { "name": "symfony/string", - "version": "v8.0.6", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", - "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", + "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", "shasum": "" }, "require": { @@ -5109,7 +5881,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.6" + "source": "https://github.com/symfony/string/tree/v8.0.8" }, "funding": [ { @@ -5129,20 +5901,20 @@ "type": "tidelift" } ], - "time": "2026-02-09T10:14:57+00:00" + "time": "2026-03-30T15:14:47+00:00" }, { "name": "symfony/var-dumper", - "version": "v8.0.6", + "version": "v8.0.8", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209" + "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", - "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", + "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", "shasum": "" }, "require": { @@ -5196,7 +5968,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.0.6" + "source": "https://github.com/symfony/var-dumper/tree/v8.0.8" }, "funding": [ { @@ -5216,7 +5988,7 @@ "type": "tidelift" } ], - "time": "2026-02-15T10:53:29+00:00" + "time": "2026-03-31T07:15:36+00:00" }, { "name": "thecodingmachine/safe", @@ -5413,16 +6185,16 @@ }, { "name": "webmozart/assert", - "version": "2.1.6", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/eb0d790f735ba6cff25c683a85a1da0eadeff9e4", + "reference": "eb0d790f735ba6cff25c683a85a1da0eadeff9e4", "shasum": "" }, "require": { @@ -5469,9 +6241,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.6" + "source": "https://github.com/webmozarts/assert/tree/2.3.0" }, - "time": "2026-02-27T10:28:38+00:00" + "time": "2026-04-11T10:33:05+00:00" }, { "name": "webmozart/glob", diff --git a/src/Extension/Generated/GeneratedCoreExtension.php b/src/Extension/Generated/GeneratedCoreExtension.php new file mode 100644 index 00000000..cfc79018 --- /dev/null +++ b/src/Extension/Generated/GeneratedCoreExtension.php @@ -0,0 +1,51 @@ +addGuesser(new BuiltInGuesser(), -64); // @todo this should be somehow considered in generator + $metadataFactory = $builder->getMetadataFactory(); + + $generator = new MiddlewareGenerator($metadataFactory); + $middlewareClassName = 'GeneratedTransformMiddleware'; + $fullMiddlewareClassName = 'Patchlevel\\Hydrator\\Generated\\' . $middlewareClassName; + + $middlewareCode = $generator->dump($this->classes, $fullMiddlewareClassName); + + //if (class_exists($fullMiddlewareClassName)) { + // throw new \RuntimeException(sprintf('Middleware class %s already exists', $fullMiddlewareClassName)); + //} + + $filename = sprintf('%s/%s.php', $this->cachePath, $middlewareClassName); + + //if (file_exists($filename)) { + // throw new \RuntimeException(sprintf('Middleware file %s already exists', $filename)); + //} +// + //if (!is_dir(dirname($filename))) { + // mkdir(dirname($filename), 0777, true); + //} + + file_put_contents($filename, $middlewareCode); + require_once $filename; // should not be needed if autoload config is valid? + + $builder->addMiddleware(new $fullMiddlewareClassName($metadataFactory), -64); + } +} diff --git a/src/Extension/Generated/MiddlewareGenerator.php b/src/Extension/Generated/MiddlewareGenerator.php new file mode 100644 index 00000000..3b84b3ba --- /dev/null +++ b/src/Extension/Generated/MiddlewareGenerator.php @@ -0,0 +1,369 @@ + $classes + */ + public function dump(array $classes, string $middlewareFqcn): string + { + $parts = explode('\\', $middlewareFqcn); + $middlewareClassName = array_pop($parts); + $namespace = implode('\\', $parts); + + /** @var array $allClasses */ + $allClasses = []; + $todo = $classes; + + // Phase 0: Collect all recursive classes + while ($todo !== []) { + $class = ltrim(array_shift($todo), '\\'); + if (isset($allClasses[$class])) { + continue; + } + try { + $metadata = $this->metadataFactory->metadata($class); + $allClasses[$class] = $metadata; + + foreach ($metadata->properties as $property) { + if ($property->normalizer instanceof ObjectNormalizer) { + $todo[] = $property->normalizer->className(); + } elseif ($property->normalizer instanceof ArrayNormalizer) { + $reflection = new ReflectionProperty($property->normalizer, 'normalizer'); + $inner = $reflection->getValue($property->normalizer); + if ($inner instanceof ObjectNormalizer) { + $todo[] = $inner->className(); + } + } + } + } catch (Throwable) { + // Skip if metadata not found + } + } + + $normalizers = []; + $normalizerMap = []; // [class][fieldName] => globalIndex + + // Phase 1: Collect all normalizers + foreach ($allClasses as $class => $metadata) { + + foreach ($metadata->properties as $property) { + if ($property->normalizer && !$property->normalizer instanceof ObjectNormalizer) { + if ($property->normalizer instanceof ArrayNormalizer) { + $reflection = new ReflectionProperty($property->normalizer, 'normalizer'); + $inner = $reflection->getValue($property->normalizer); + if ($inner instanceof ObjectNormalizer) { + continue; // We inline these + } + } + + // Map normalizers by the declaring class of the property to support inheritance + $declaringClass = $property->reflection->getDeclaringClass()->getName(); + + $normalizers[] = [ + 'class' => $declaringClass, + 'normalizer' => $property->normalizer::class, + 'fieldName' => $property->fieldName, + 'propertyName' => $property->propertyName, + ]; + $normalizerMap[$declaringClass][$property->fieldName] = count($normalizers) - 1; + } + } + } + + // Phase 2: Generate Properties and Setup + $propertiesCode = ''; + $setupCode = ''; + + foreach ($normalizers as $index => $info) { + $propertiesCode .= "private readonly \\{$info['normalizer']} \$n$index;\n"; + $setupCode .= "\$this->n$index = \$metadataFactory->metadata(\\{$info['class']}::class)->properties['{$info['propertyName']}']->normalizer;\n"; + } + + // Phase 3: Generate Class Methods + $methods = ''; + $hydrateCases = ''; + $extractCases = ''; + + foreach ($allClasses as $class => $metadata) { + $shortName = str_replace('\\', '', $class); + + $hydrateCases .= "\\$class::class => \$this->hydrate$shortName(\$data, \$context, \$stack),\n"; + $extractCases .= "\\$class::class => \$this->extract$shortName(\$object, \$context, \$stack),\n"; + + $methods .= $this->generateClassMethods($metadata, $shortName, $normalizerMap); + } + + return <<padLeft($propertiesCode, 1)} + + public function __construct(MetadataFactory \$metadataFactory) + { +{$this->padLeft($setupCode, 2)} + } + + public function hydrate(ClassMetadata \$metadata, array \$data, array \$context, Stack \$stack): object + { + \$object = \$this->doHydrate(\$metadata->className, \$data, \$context, \$stack); + + if (\$object === null) { + return \$stack->next()->hydrate(\$metadata, \$data, \$context, \$stack); + } + + return \$object; + } + + private function doHydrate(string \$class, array \$data, array \$context, Stack \$stack): object|null + { + return match (\$class) { +{$this->padLeft($hydrateCases, 3)} + default => null, + }; + } + + public function extract(ClassMetadata \$metadata, object \$object, array \$context, Stack \$stack): array + { + \$data = \$this->doExtract(\$object, \$context, \$stack); + + if (\$data === null) { + return \$stack->next()->extract(\$metadata, \$object, \$context, \$stack); + } + + return \$data; + } + + private function doExtract(object \$object, array \$context, Stack \$stack): array|null + { + \$objectId = spl_object_id(\$object); + + if (array_key_exists(\$objectId, \$this->callStack)) { + \$references = array_values(\$this->callStack); + \$references[] = \$object::class; + + throw new CircularReference(\$references); + } + + \$this->callStack[\$objectId] = \$object::class; + + try { + return match (\$object::class) { +{$this->padLeft($extractCases, 4)} + default => null, + }; + } finally { + \\array_pop(\$this->callStack); + } + } + +{$this->padLeft($methods, 1)} +} +PHP; + } + + private function generateClassMethods(ClassMetadata $metadata, string $shortName, array $normalizerMap): string + { + $targetClass = $metadata->className; + + $constructor = $metadata->reflection->getConstructor(); + + if ($constructor === null) { + dd($metadata->className); + } + + $befores = []; + $map = []; + + foreach ($constructor->getParameters() as $parameter) { + $tupple = $this->generatePropertyDenormalization($metadata->properties[$parameter->getName()], $normalizerMap); + + $map[] = $tupple[0]; + + if ($tupple[1] !== '') { + $befores[] = $tupple[1]; + } + } + + $methods = <<padLeft(implode("\n", $befores), 1)} + return new \\$targetClass( +{$this->padLeft(implode(",\n", $map), 2)} + ); +} + +PHP; + + $befores = []; + $map = []; + + foreach ($metadata->properties as $property) { + $tupple = $this->generatePropertyNormalization($property, $normalizerMap); + + $map[] = $tupple[0]; + + if ($tupple[1] !== '') { + $befores[] = $tupple[1]; + } + } + + $methods .= <<padLeft(implode("\n", $befores), 1)} + return [ +{$this->padLeft(implode("\n", $map), 2)} + ]; +} + +PHP; + + return $methods; + } + + /** + * @return array{string, string} + */ + private function generatePropertyDenormalization(PropertyMetadata $property, array $normalizerMap): array + { + $fieldName = $property->fieldName; + $propertyName = $property->propertyName; + $class = $property->reflection->getDeclaringClass()->getName(); + $globalIndex = $normalizerMap[$class][$fieldName] ?? null; + + $before = ''; + + if ($property->normalizer !== null) { + if ($property->normalizer instanceof ObjectNormalizer) { + $nestedClass = $property->normalizer->className(); + $valueCode = "\$this->doHydrate(\\$nestedClass::class, \$data['$fieldName'], \$context, \$stack)"; + } elseif ($property->normalizer instanceof ArrayNormalizer) { + $reflection = new ReflectionProperty($property->normalizer, 'normalizer'); + $inner = $reflection->getValue($property->normalizer); + if ($inner instanceof ObjectNormalizer) { + $nestedClass = $inner->className(); + $before = <<doHydrate(\\$nestedClass::class, \${$propertyName}Item, \$context, \$stack); +} +PHP; + $valueCode = "\$data['$fieldName']"; + } else { + $valueCode = "\$this->n{$globalIndex}->denormalize(\$data['$fieldName'], \$context)"; + } + } elseif ($globalIndex !== null) { + $valueCode = "\$this->n{$globalIndex}->denormalize(\$data['$fieldName'], \$context)"; + } else { + $valueCode = "\$data['$fieldName']"; + } + } else { + $valueCode = "\$data['$fieldName']"; + } + + if ($property->reflection->getType()?->allowsNull()) { + $valueCode = "\\array_key_exists('$fieldName', \$data) ? $valueCode : null"; + } + + return [$valueCode, $before]; + } + + /** + * @return array{string, string} + */ + private function generatePropertyNormalization(PropertyMetadata $property, array $normalizerMap): array + { + $fieldName = $property->fieldName; + $propertyName = $property->propertyName; + $class = $property->reflection->getDeclaringClass()->getName(); + $globalIndex = $normalizerMap[$class][$fieldName] ?? null; + $before = ''; + + if ($property->normalizer !== null) { + if ($property->normalizer instanceof ObjectNormalizer) { + $valueCode = "\$this->doExtract(\$object->$propertyName, \$context, \$stack)"; + } elseif ($property->normalizer instanceof ArrayNormalizer) { + $reflection = new ReflectionProperty($property->normalizer, 'normalizer'); + $inner = $reflection->getValue($property->normalizer); + if ($inner instanceof ObjectNormalizer) { + $before = <<$propertyName; +foreach (\$$propertyName as &\${$propertyName}Item) { + \${$propertyName}Item = \$this->doExtract(\${$propertyName}Item, \$context, \$stack); +} +PHP; + + $valueCode = "\$$propertyName"; + } else { + $valueCode = "\$this->n{$globalIndex}->normalize(\$object->$propertyName, \$context)"; + } + } elseif ($globalIndex !== null) { + $valueCode = "\$this->n{$globalIndex}->normalize(\$object->$propertyName, \$context)"; + } else { + $valueCode = "\$object->$propertyName"; + } + } else { + $valueCode = "\$object->$propertyName"; + } + + return ["'$fieldName' => $valueCode,", $before]; + } + + private function padLeft(string $multilineString, int $n): string + { + $result = []; + + foreach (explode("\n", $multilineString) as $line) { + $result[] = str_repeat(' ', $n * 4).$line; + } + + return implode("\n", $result); + } +} diff --git a/src/StackHydratorBuilder.php b/src/StackHydratorBuilder.php index 32fbd194..5eb5fd52 100644 --- a/src/StackHydratorBuilder.php +++ b/src/StackHydratorBuilder.php @@ -11,6 +11,7 @@ use Patchlevel\Hydrator\Metadata\MetadataEnricher; use Patchlevel\Hydrator\Metadata\Psr16MetadataFactory; use Patchlevel\Hydrator\Metadata\Psr6MetadataFactory; +use Patchlevel\Hydrator\Metadata\MetadataFactory; use Patchlevel\Hydrator\Middleware\Middleware; use Psr\Cache\CacheItemPoolInterface; use Psr\SimpleCache\CacheInterface; @@ -80,12 +81,7 @@ public function setCache(CacheItemPoolInterface|CacheInterface|null $cache): sta public function build(): StackHydrator { - $metadataFactory = new EnrichingMetadataFactory( - new AttributeMetadataFactory( - guesser: new ChainGuesser($this->guessers()), - ), - $this->metadataEnrichers(), - ); + $metadataFactory = $this->getMetadataFactory(); if ($this->cache instanceof CacheItemPoolInterface) { $metadataFactory = new Psr6MetadataFactory($metadataFactory, $this->cache); @@ -130,4 +126,14 @@ public function metadataEnrichers(): array return array_merge(...$this->metadataEnrichers); } + + public function getMetadataFactory(): MetadataFactory + { + return new EnrichingMetadataFactory( + new AttributeMetadataFactory( + guesser: new ChainGuesser($this->guessers()), + ), + $this->metadataEnrichers(), + ); + } } diff --git a/tests/Benchmark/EventSauceHydratorBench.php b/tests/Benchmark/EventSauceHydratorBench.php new file mode 100644 index 00000000..45b4b2e5 --- /dev/null +++ b/tests/Benchmark/EventSauceHydratorBench.php @@ -0,0 +1,132 @@ +hydrator = new ObjectMapperUsingReflection(); + } + + public function setUp(): void + { + $object = $this->hydrator->hydrateObject( + ProfileCreated::class, + [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ); + + $this->hydrator->serializeObject($object); + } + + #[Bench\Revs(5)] + public function benchHydrate1Object(): void + { + $this->hydrator->hydrateObject(ProfileCreated::class, [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + + #[Bench\Revs(5)] + public function benchExtract1Object(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + $this->hydrator->serializeObject($object); + } + + #[Bench\Revs(3)] + public function benchHydrate1000Objects(): void + { + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->hydrateObject(ProfileCreated::class, [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->serializeObject($object); + } + } + + #[Bench\Revs(3)] + public function benchHydrate1000000Objects(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->hydrateObject(ProfileCreated::class, [ + 'profile_id' => '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->serializeObject($object); + } + } +} diff --git a/tests/Benchmark/Fixture/ProfileCreated.php b/tests/Benchmark/Fixture/ProfileCreated.php index 5f28157c..17ddabf1 100644 --- a/tests/Benchmark/Fixture/ProfileCreated.php +++ b/tests/Benchmark/Fixture/ProfileCreated.php @@ -4,6 +4,7 @@ namespace Patchlevel\Hydrator\Tests\Benchmark\Fixture; +use JMS\Serializer\Annotation\Type; use Patchlevel\Hydrator\Extension\Cryptography\Attribute\DataSubjectId; use Patchlevel\Hydrator\Extension\Cryptography\Attribute\SensitiveData; @@ -12,10 +13,14 @@ final class ProfileCreated /** @param list $skills */ public function __construct( #[ProfileIdNormalizer] + #[ProfileIdCaster] #[DataSubjectId] public ProfileId $profileId, #[SensitiveData(fallback: 'unknown')] public string $name, + /** + * @Type("list") + */ public array $skills = [], ) { } diff --git a/tests/Benchmark/Fixture/ProfileIdCaster.php b/tests/Benchmark/Fixture/ProfileIdCaster.php new file mode 100644 index 00000000..fe5d8200 --- /dev/null +++ b/tests/Benchmark/Fixture/ProfileIdCaster.php @@ -0,0 +1,40 @@ +toString(); + } +} diff --git a/tests/Benchmark/GeneratedEventSauceHydratorBench.php b/tests/Benchmark/GeneratedEventSauceHydratorBench.php new file mode 100644 index 00000000..4281ba57 --- /dev/null +++ b/tests/Benchmark/GeneratedEventSauceHydratorBench.php @@ -0,0 +1,142 @@ +dump($classesToDump, $dumpedClassNamed); + file_put_contents(__DIR__ . '/../../var/cache/GeneratedEventSauceMapper.php', $code); + + require_once __DIR__ . '/../../var/cache/GeneratedEventSauceMapper.php'; + + $this->hydrator = new \AcmeCorp\GeneratedEventSauceMapper(); + } + + public function setUp(): void + { + $object = $this->hydrator->hydrateObject( + ProfileCreated::class, + [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ); + + $this->hydrator->serializeObject($object); + } + + #[Bench\Revs(5)] + public function benchHydrate1Object(): void + { + $this->hydrator->hydrateObject(ProfileCreated::class, [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + + #[Bench\Revs(5)] + public function benchExtract1Object(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + $this->hydrator->serializeObject($object); + } + + #[Bench\Revs(3)] + public function benchHydrate1000Objects(): void + { + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->hydrateObject(ProfileCreated::class, [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->serializeObject($object); + } + } + + #[Bench\Revs(3)] + public function benchHydrate1000000Objects(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->hydrateObject(ProfileCreated::class, [ + 'profile_id' => '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->serializeObject($object); + } + } +} diff --git a/tests/Benchmark/GeneratedHydratorBench.php b/tests/Benchmark/GeneratedHydratorBench.php new file mode 100644 index 00000000..913731fb --- /dev/null +++ b/tests/Benchmark/GeneratedHydratorBench.php @@ -0,0 +1,140 @@ +hydrator = (new StackHydratorBuilder()) + ->useExtension(new GeneratedCoreExtension( + __DIR__ . '/../../var/cache', + [ + ProfileCreated::class, + Skill::class, + ] + )) + ->build(); + } + + public function setUp(): void + { + $this->hydrator->hydrate( + ProfileCreated::class, + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ); + } + + #[Bench\Revs(5)] + public function benchHydrate1Object(): void + { + $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + + #[Bench\Revs(5)] + public function benchExtract1Object(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + $this->hydrator->extract($object); + } + + #[Bench\Revs(3)] + public function benchHydrate1000Objects(): void + { + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000; $i++) { + $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/JMSHydratorBench.php b/tests/Benchmark/JMSHydratorBench.php new file mode 100644 index 00000000..c04fa1d0 --- /dev/null +++ b/tests/Benchmark/JMSHydratorBench.php @@ -0,0 +1,141 @@ +hydrator = SerializerBuilder::create()->build(); + } + + public function setUp(): void + { + $object = $this->hydrator->fromArray( + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ProfileCreated::class, + ); + + $this->hydrator->toArray($object); + } + + #[Bench\Revs(5)] + public function benchHydrate1Object(): void + { + $this->hydrator->fromArray( + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ProfileCreated::class + ); + } + + #[Bench\Revs(5)] + public function benchExtract1Object(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + $this->hydrator->toArray($object); + } + + #[Bench\Revs(3)] + public function benchHydrate1000Objects(): void + { + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->fromArray( + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ProfileCreated::class + ); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->toArray($object); + } + } + + #[Bench\Revs(3)] + public function benchHydrate1000000Objects(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->fromArray( + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ProfileCreated::class + ); + } + } + + #[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->toArray($object); + } + } +} diff --git a/tests/Benchmark/SerdeHydratorBench.php b/tests/Benchmark/SerdeHydratorBench.php new file mode 100644 index 00000000..0ae425fc --- /dev/null +++ b/tests/Benchmark/SerdeHydratorBench.php @@ -0,0 +1,148 @@ +hydrator = new SerdeCommon($analyzer); + } + + public function setUp(): void + { + $object = $this->hydrator->deserialize( + [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + 'array', + ProfileCreated::class, + ); + + $this->hydrator->serialize($object, 'array'); + } + + #[Bench\Revs(5)] + public function benchHydrate1Object(): void + { + $this->hydrator->deserialize( + [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + 'array', + ProfileCreated::class, + ); + } + + #[Bench\Revs(5)] + public function benchExtract1Object(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + $this->hydrator->serialize($object, 'array'); + } + + #[Bench\Revs(3)] + public function benchHydrate1000Objects(): void + { + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->deserialize( + [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + 'array', + ProfileCreated::class, + ); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->serialize($object, 'array'); + } + } + + #[Bench\Revs(3)] + public function benchHydrate1000000Objects(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->deserialize( + [ + 'profile_id' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + 'array', + ProfileCreated::class, + ); + } + } + + #[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->serialize($object, 'array'); + } + } +} diff --git a/tests/Benchmark/StubGeneratedHydratorBench.php b/tests/Benchmark/StubGeneratedHydratorBench.php new file mode 100644 index 00000000..4207a66c --- /dev/null +++ b/tests/Benchmark/StubGeneratedHydratorBench.php @@ -0,0 +1,138 @@ +hydrator = (new StackHydratorBuilder()) + ->addMiddleware(new \GeneratedTransformMiddleware()) + ->addGuesser(new BuiltInGuesser(), -64) + ->build(); + } + + public function setUp(): void + { + $this->hydrator->hydrate( + ProfileCreated::class, + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ); + } + + #[Bench\Revs(5)] + public function benchHydrate1Object(): void + { + $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + + #[Bench\Revs(5)] + public function benchExtract1Object(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + $this->hydrator->extract($object); + } + + #[Bench\Revs(3)] + public function benchHydrate1000Objects(): void + { + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->hydrate(ProfileCreated::class, [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ]); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000; $i++) { + $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/SymfonyHydratorBench.php b/tests/Benchmark/SymfonyHydratorBench.php new file mode 100644 index 00000000..5b3ca659 --- /dev/null +++ b/tests/Benchmark/SymfonyHydratorBench.php @@ -0,0 +1,144 @@ +hydrator = new Serializer([new ObjectNormalizer()], []); + } + + public function setUp(): void + { + $object = $this->hydrator->denormalize( + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ProfileCreated::class, + ); + + $this->hydrator->normalize($object); + } + + #[Bench\Revs(5)] + public function benchHydrate1Object(): void + { + $this->hydrator->denormalize( + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ProfileCreated::class + ); + } + + #[Bench\Revs(5)] + public function benchExtract1Object(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + $this->hydrator->normalize($object); + } + + #[Bench\Revs(3)] + public function benchHydrate1000Objects(): void + { + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->denormalize( + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ProfileCreated::class + ); + } + } + + #[Bench\Revs(3)] + public function benchExtract1000Objects(): void + { + $object = new ProfileCreated( + ProfileId::fromString('1'), + 'foo', + [ + new Skill('php'), + new Skill('symfony'), + ], + ); + + for ($i = 0; $i < 1_000; $i++) { + $this->hydrator->normalize($object); + } + } + + #[Bench\Revs(3)] + public function benchHydrate1000000Objects(): void + { + for ($i = 0; $i < 1_000_000; $i++) { + $this->hydrator->denormalize( + [ + 'profileId' => '1', + 'name' => 'foo', + 'skills' => [ + ['name' => 'php'], + ['name' => 'symfony'], + ], + ], + ProfileCreated::class + ); + } + } + + #[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->normalize($object); + } + } +} diff --git a/tests/Unit/Extension/Generated/GeneratedMetadataHydratorTest.php b/tests/Unit/Extension/Generated/GeneratedMetadataHydratorTest.php new file mode 100644 index 00000000..107ca8c4 --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedMetadataHydratorTest.php @@ -0,0 +1,618 @@ +hydrator = (new HydratorBuilder())->useExtension(new GeneratedCoreExtension( + __DIR__ . '/../../../../var/cache', + [ + ProfileCreated::class, + ParentDto::class, + ProfileCreatedWrapper::class, + Circle1Dto::class, + Circle2Dto::class, + Circle3Dto::class, + InferNormalizerWithNullableDto::class, + InferNormalizerDto::class, + DefaultDto::class, + ProfileCreatedWrapper::class, + NormalizerInBaseClassDefinedDto::class, + InferNormalizerWithIterablesDto::class, + LazyProfileCreated::class, + WrongNormalizer::class, + ], + ))->build(); + } + + 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 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 testExtractWithInferNormalizer2(): void + { + $result = $this->hydrator->extract( + new InferNormalizerWithNullableDto( + null, + null, + profileId: ProfileId::fromString('1'), + ), + ); + + self::assertEquals( + [ + 'status' => null, + 'dateTimeImmutable' => null, + 'dateTime' => null, + 'dateTimeZone' => null, + 'profileId' => '1', + ], + $result, + ); + } + + public function testExtractWithContext(): void + { + $object = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $expect = [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + 'array' => ['foo'], + ]; + + $middleware = $this->createMock(Middleware::class); + $middleware + ->expects($this->once()) + ->method('extract') + ->with( + $this->isInstanceOf(ClassMetadata::class), + $object, + ['context' => '123'], + $this->isInstanceOf(Stack::class), + )->willReturn($expect); + + $hydrator = (new HydratorBuilder()) + ->useExtension(new GeneratedCoreExtension( + __DIR__ . '/../../../../var/cache', + [ + ProfileCreated::class, + ParentDto::class, + ProfileCreatedWrapper::class, + Circle1Dto::class, + Circle2Dto::class, + Circle3Dto::class, + InferNormalizerWithNullableDto::class, + InferNormalizerDto::class, + DefaultDto::class, + ProfileCreatedWrapper::class, + NormalizerInBaseClassDefinedDto::class, + InferNormalizerWithIterablesDto::class, + LazyProfileCreated::class, + ], + )) + ->addMiddleware($middleware) + ->build(); + + $data = $hydrator->extract($object, ['context' => '123']); + + self::assertEquals($expect, $data); + } + + 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 testHydrateUnknownClass(): void + { + $this->expectException(ClassNotSupported::class); + $this->expectExceptionCode(0); + + $this->hydrator->hydrate( + 'Unknown', + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + ); + } + + 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 testHydrateWithTypeMismatch(): void + { + $this->expectException(TypeError::class); + + $this->hydrator->hydrate( + ProfileCreated::class, + ['profileId' => null, 'email' => null], + ); + } + + public function testHydrateWithContext(): void + { + $expect = new InferNormalizerDto( + Status::Draft, + new DateTimeImmutable('2015-02-13 22:34:32+01:00'), + new DateTime('2015-02-13 22:34:32+01:00'), + new DateTimeZone('EDT'), + ['foo'], + ); + + $data = [ + 'status' => 'draft', + 'dateTimeImmutable' => '2015-02-13T22:34:32+01:00', + 'dateTime' => '2015-02-13T22:34:32+01:00', + 'dateTimeZone' => 'EDT', + 'array' => ['foo'], + ]; + + $middleware = $this->createMock(Middleware::class); + $middleware + ->expects($this->once()) + ->method('hydrate') + ->with( + $this->isInstanceOf(ClassMetadata::class), + $data, + ['context' => '123'], + $this->isInstanceOf(Stack::class), + )->willReturn($expect); + + $hydrator = (new HydratorBuilder()) + ->useExtension(new GeneratedCoreExtension( + __DIR__ . '/../../../../var/cache', + [ + InferNormalizerDto::class, + ], + )) + ->addMiddleware($middleware) + ->build(); + + $object = $hydrator->hydrate(InferNormalizerDto::class, $data, ['context' => '123']); + + self::assertEquals($expect, $object); + } + + public function testDenormalizationFailure(): void + { + $this->expectException(InvalidArgument::class); + + $this->hydrator->hydrate( + ProfileCreated::class, + ['profileId' => 123, 'email' => 123], + ); + } + + public function testNormalizationFailure(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->hydrator->extract( + new WrongNormalizer(true), + ); + } + + 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 = (new HydratorBuilder()) + ->useExtension(new GeneratedCoreExtension( + __DIR__ . '/../../../../var/cache', + [ + SensitiveDataProfileCreated::class, + ], + )) + ->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 = (new HydratorBuilder()) + ->useExtension(new GeneratedCoreExtension( + __DIR__ . '/../../../../var/cache', + [ + SensitiveDataProfileCreated::class, + ], + )) + ->useExtension(new CryptographyExtension($cryptographer)) + ->build(); + + $return = $hydrator->extract($object); + + self::assertSame($encryptedPayload, $return); + } + + 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); + } + + #[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); + } +} diff --git a/tests/Unit/Extension/Generated/GeneratedTransformerMiddlewareTest.php b/tests/Unit/Extension/Generated/GeneratedTransformerMiddlewareTest.php new file mode 100644 index 00000000..51c553f0 --- /dev/null +++ b/tests/Unit/Extension/Generated/GeneratedTransformerMiddlewareTest.php @@ -0,0 +1,100 @@ +dump([ProfileCreated::class], $fullMiddlewareClassName); + file_put_contents($filename, $middlewareCode); + + require_once $filename; + + $middleware = new $fullMiddlewareClassName(); + + $expected = new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ); + + $event = $middleware->hydrate( + $this->classMetadata(ProfileCreated::class), + ['profileId' => '1', 'email' => 'info@patchlevel.de'], + [], + new Stack([]), + ); + + self::assertEquals($expected, $event); + } + + public function testExtract(): void + { + $cachePath = __DIR__ . '/../../../var/cache'; + @mkdir($cachePath, 0777, true); + + $metadataFactory = new AttributeMetadataFactory(); + $generator = new MiddlewareGenerator($metadataFactory); + $generatedClassName = 'UnifiedMiddleware'; + $code = $generator->generate([ProfileCreated::class], $generatedClassName); + file_put_contents($cachePath . '/' . $generatedClassName . '.php', $code); + + $middleware = new AttributeTransformMiddleware( + $cachePath, + [ProfileCreated::class], + $metadataFactory + ); + + $expected = ['profileId' => '1', 'email' => 'info@patchlevel.de']; + + $data = $middleware->extract( + $this->classMetadata(ProfileCreated::class), + new ProfileCreated( + ProfileId::fromString('1'), + Email::fromString('info@patchlevel.de'), + ), + [], + new Stack([], new MetadataHydrator()), + ); + + self::assertEquals($expected, $data); + } + + /** + * @param class-string $class + * + * @return ClassMetadata + * + * @template T of object + */ + private function classMetadata(string $class): ClassMetadata + { + return (new AttributeMetadataFactory()) + ->metadata($class); + } +} diff --git a/tests/Unit/Fixture/Circle1Dto.php b/tests/Unit/Fixture/Circle1Dto.php index 799ed794..5b0a7986 100644 --- a/tests/Unit/Fixture/Circle1Dto.php +++ b/tests/Unit/Fixture/Circle1Dto.php @@ -8,6 +8,10 @@ final class Circle1Dto { - #[ObjectNormalizer(Circle2Dto::class)] - public object|null $to = null; + public function __construct( + #[ObjectNormalizer(Circle2Dto::class)] + public object|null $to = null + ) + { + } } diff --git a/tests/Unit/Fixture/Circle2Dto.php b/tests/Unit/Fixture/Circle2Dto.php index aa87e893..e536ff58 100644 --- a/tests/Unit/Fixture/Circle2Dto.php +++ b/tests/Unit/Fixture/Circle2Dto.php @@ -8,6 +8,10 @@ final class Circle2Dto { - #[ObjectNormalizer(Circle3Dto::class)] - public object|null $to = null; + public function __construct( + #[ObjectNormalizer(Circle3Dto::class)] + public object|null $to = null + ) + { + } } diff --git a/tests/Unit/Fixture/Circle3Dto.php b/tests/Unit/Fixture/Circle3Dto.php index c0b4f6de..a3240240 100644 --- a/tests/Unit/Fixture/Circle3Dto.php +++ b/tests/Unit/Fixture/Circle3Dto.php @@ -8,6 +8,10 @@ final class Circle3Dto { - #[ObjectNormalizer(Circle1Dto::class)] - public object|null $to = null; + public function __construct( + #[ObjectNormalizer(Circle1Dto::class)] + public object|null $to = null + ) + { + } }