From 32c3ceb10f8277c49d0aede645f962c3df30baae Mon Sep 17 00:00:00 2001 From: lotyp Date: Wed, 19 Jun 2024 00:47:50 +0300 Subject: [PATCH] refactor: rewrote package to be more configurable and flexible --- composer.json | 4 +- composer.lock | 132 ++++---- config/serializer.php | 66 ++-- phpstan-baseline.neon | 225 ------------- phpstan.neon.dist | 2 +- phpunit.xml.dist | 3 + psalm-baseline.xml | 315 ++++++------------ src/Bridge/Laravel/Facades/Manager.php | 15 + src/{ => Bridge/Laravel/Http}/HttpCode.php | 11 +- .../Laravel/Http}/ResponseFactory.php | 29 +- .../Providers/SerializerServiceProvider.php | 134 ++++---- src/Config.php | 67 +++- src/Contracts/ConfigRepository.php | 10 +- src/Contracts/EncoderRegistrationStrategy.php | 16 + ...rface.php => EncoderRegistryInterface.php} | 7 +- .../NormalizerRegistrationStrategy.php | 16 + ...ce.php => NormalizerRegistryInterface.php} | 5 +- src/Contracts/SerializerException.php | 11 + src/Contracts/SerializerInterface.php | 21 +- src/Contracts/SerializerRegistryInterface.php | 9 +- src/DefaultEncoderRegistrationStrategy.php | 29 ++ src/DefaultNormalizerRegistrationStrategy.php | 62 ++++ src/EncoderRegistry.php | 50 +++ src/EncodersRegistry.php | 68 ---- src/Exceptions/MissingRequiredAttributes.php | 3 +- ...ndException.php => SerializerNotFound.php} | 3 +- src/Exceptions/UnsupportedTypeException.php | 16 - src/Manager/Serializer.php | 39 +++ src/Manager/SerializerManager.php | 53 +++ src/{ => Manager}/SerializerRegistry.php | 24 +- src/NormalizerRegistry.php | 62 ++++ src/Normalizers/RamseyUuidNormalizer.php | 10 + src/NormalizersRegistry.php | 107 ------ src/Serializer.php | 91 ----- src/SerializerManager.php | 40 --- tests/app/NestedObjects/City.php | 3 + tests/app/{Object => Objects}/Author.php | 2 +- tests/app/{Object => Objects}/Post.php | 2 +- tests/app/{Object => Objects}/Product.php | 2 +- tests/app/{Object => Objects}/User.php | 2 +- tests/app/{ => Response}/Item.php | 14 +- tests/app/{ => Response}/Response.php | 16 +- tests/app/{ => Response}/array.php | 6 +- .../Bridge/Laravel/Facades/ManagerTest.php | 22 ++ .../Laravel/Http}/ResponseFactoryTest.php | 43 ++- .../SerializerServiceProviderTest.php | 48 ++- tests/src/Functional/ConfigTest.php | 66 ++++ tests/src/Functional/EncoderRegistryTest.php | 82 +++++ tests/src/Functional/EncodersRegistryTest.php | 85 ----- .../Manager/SerializerManagerTest.php | 21 ++ .../Manager/SerializerRegistryTest.php | 76 +++++ .../src/Functional/Manager/SerializerTest.php | 29 ++ ...tryTest.php => NormalizerRegistryTest.php} | 58 ++-- .../Normalizers/RamseyUuidNormalizerTest.php | 33 +- .../src/Functional/SerializerManagerTest.php | 97 ------ tests/src/Functional/SerializerTest.php | 184 ---------- tests/src/Functional/TestCase.php | 7 - .../Normalizers/RamseyUuidNormalizerTest.php | 80 +++++ 58 files changed, 1336 insertions(+), 1397 deletions(-) create mode 100644 src/Bridge/Laravel/Facades/Manager.php rename src/{ => Bridge/Laravel/Http}/HttpCode.php (91%) rename src/{ => Bridge/Laravel/Http}/ResponseFactory.php (75%) create mode 100644 src/Contracts/EncoderRegistrationStrategy.php rename src/Contracts/{EncodersRegistryInterface.php => EncoderRegistryInterface.php} (59%) create mode 100644 src/Contracts/NormalizerRegistrationStrategy.php rename src/Contracts/{NormalizersRegistryInterface.php => NormalizerRegistryInterface.php} (81%) create mode 100644 src/Contracts/SerializerException.php create mode 100644 src/DefaultEncoderRegistrationStrategy.php create mode 100644 src/DefaultNormalizerRegistrationStrategy.php create mode 100644 src/EncoderRegistry.php delete mode 100644 src/EncodersRegistry.php rename src/Exceptions/{SerializerNotFoundException.php => SerializerNotFound.php} (65%) delete mode 100644 src/Exceptions/UnsupportedTypeException.php create mode 100644 src/Manager/Serializer.php create mode 100644 src/Manager/SerializerManager.php rename src/{ => Manager}/SerializerRegistry.php (63%) create mode 100644 src/NormalizerRegistry.php delete mode 100644 src/NormalizersRegistry.php delete mode 100644 src/Serializer.php delete mode 100644 src/SerializerManager.php rename tests/app/{Object => Objects}/Author.php (85%) rename tests/app/{Object => Objects}/Post.php (86%) rename tests/app/{Object => Objects}/Product.php (93%) rename tests/app/{Object => Objects}/User.php (85%) rename tests/app/{ => Response}/Item.php (71%) rename tests/app/{ => Response}/Response.php (60%) rename tests/app/{ => Response}/array.php (60%) create mode 100644 tests/src/Functional/Bridge/Laravel/Facades/ManagerTest.php rename tests/src/Functional/{ => Bridge/Laravel/Http}/ResponseFactoryTest.php (61%) create mode 100644 tests/src/Functional/ConfigTest.php create mode 100644 tests/src/Functional/EncoderRegistryTest.php delete mode 100644 tests/src/Functional/EncodersRegistryTest.php create mode 100644 tests/src/Functional/Manager/SerializerManagerTest.php create mode 100644 tests/src/Functional/Manager/SerializerRegistryTest.php create mode 100644 tests/src/Functional/Manager/SerializerTest.php rename tests/src/Functional/{NormalizersRegistryTest.php => NormalizerRegistryTest.php} (68%) delete mode 100644 tests/src/Functional/SerializerManagerTest.php delete mode 100644 tests/src/Functional/SerializerTest.php create mode 100644 tests/src/Unit/Normalizers/RamseyUuidNormalizerTest.php diff --git a/composer.json b/composer.json index 17b4ffca..1f06cc2a 100644 --- a/composer.json +++ b/composer.json @@ -58,7 +58,7 @@ "wayofdev/cs-fixer-config": "^1.5" }, "suggest": { - "symfony/yaml": "For using the YamlEncoder." + "symfony/yaml": "Required only if YamlEncoder support is needed." }, "autoload": { "psr-4": { @@ -118,7 +118,7 @@ "stan:ci": "phpstan analyse --memory-limit=2G --error-format=github", "test": [ "@putenv XDEBUG_MODE=coverage", - "pest --color=always" + "pest --color=always -v" ], "test:arch": [ "@putenv XDEBUG_MODE=coverage", diff --git a/composer.lock b/composer.lock index d8526346..b662e3f6 100644 --- a/composer.lock +++ b/composer.lock @@ -1052,16 +1052,16 @@ }, { "name": "laravel/framework", - "version": "v11.10.0", + "version": "v11.11.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "99b4255194912044b75ab72329f8c19e6345720e" + "reference": "194102876df42f9f5bb618efa55fa7e15ebf40aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/99b4255194912044b75ab72329f8c19e6345720e", - "reference": "99b4255194912044b75ab72329f8c19e6345720e", + "url": "https://api.github.com/repos/laravel/framework/zipball/194102876df42f9f5bb618efa55fa7e15ebf40aa", + "reference": "194102876df42f9f5bb618efa55fa7e15ebf40aa", "shasum": "" }, "require": { @@ -1164,7 +1164,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.0.15", + "orchestra/testbench-core": "^9.1.5", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.5|^11.0", @@ -1253,20 +1253,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2024-06-04T13:45:55+00:00" + "time": "2024-06-18T17:40:27+00:00" }, { "name": "laravel/prompts", - "version": "v0.1.23", + "version": "v0.1.24", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "9bc4df7c699b0452c6b815e64a2d84b6d7f99400" + "reference": "409b0b4305273472f3754826e68f4edbd0150149" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/9bc4df7c699b0452c6b815e64a2d84b6d7f99400", - "reference": "9bc4df7c699b0452c6b815e64a2d84b6d7f99400", + "url": "https://api.github.com/repos/laravel/prompts/zipball/409b0b4305273472f3754826e68f4edbd0150149", + "reference": "409b0b4305273472f3754826e68f4edbd0150149", "shasum": "" }, "require": { @@ -1309,9 +1309,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.23" + "source": "https://github.com/laravel/prompts/tree/v0.1.24" }, - "time": "2024-05-27T13:53:20+00:00" + "time": "2024-06-17T13:58:22+00:00" }, { "name": "laravel/serializable-closure", @@ -6820,16 +6820,16 @@ }, { "name": "ergebnis/phpunit-slow-test-detector", - "version": "2.14.0", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/ergebnis/phpunit-slow-test-detector.git", - "reference": "9138b0ebffdd2c579eb4b0567ef3bef8c714fdc2" + "reference": "d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ergebnis/phpunit-slow-test-detector/zipball/9138b0ebffdd2c579eb4b0567ef3bef8c714fdc2", - "reference": "9138b0ebffdd2c579eb4b0567ef3bef8c714fdc2", + "url": "https://api.github.com/repos/ergebnis/phpunit-slow-test-detector/zipball/d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29", + "reference": "d9e4ea11d0e7bf1e54df5385bfd94b5156ab0a29", "shasum": "" }, "require": { @@ -6837,14 +6837,14 @@ "phpunit/phpunit": "^6.5.0 || ^7.5.0 || ^8.5.19 || ^9.0.0 || ^10.0.0 || ^11.0.0" }, "require-dev": { - "ergebnis/composer-normalize": "^2.42.0", + "ergebnis/composer-normalize": "^2.43.0", "ergebnis/license": "^2.4.0", - "ergebnis/php-cs-fixer-config": "^6.25.1", + "ergebnis/php-cs-fixer-config": "^6.31.0", "fakerphp/faker": "~1.20.0", "psalm/plugin-phpunit": "~0.19.0", "psr/container": "~1.0.0", - "rector/rector": "^1.0.4", - "vimeo/psalm": "^5.23.1" + "rector/rector": "^1.1.0", + "vimeo/psalm": "^5.24.0" }, "type": "library", "extra": { @@ -6883,7 +6883,7 @@ "security": "https://github.com/ergebnis/phpunit-slow-test-detector/blob/main/.github/SECURITY.md", "source": "https://github.com/ergebnis/phpunit-slow-test-detector" }, - "time": "2024-04-08T06:35:34+00:00" + "time": "2024-06-16T18:21:35+00:00" }, { "name": "evenement/evenement", @@ -7230,16 +7230,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.58.1", + "version": "v3.59.3", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "04e9424025677a86914b9a4944dbbf4060bb0aff" + "reference": "30ba9ecc2b0e5205e578fe29973c15653d9bfd29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/04e9424025677a86914b9a4944dbbf4060bb0aff", - "reference": "04e9424025677a86914b9a4944dbbf4060bb0aff", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/30ba9ecc2b0e5205e578fe29973c15653d9bfd29", + "reference": "30ba9ecc2b0e5205e578fe29973c15653d9bfd29", "shasum": "" }, "require": { @@ -7269,16 +7269,16 @@ "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" }, "require-dev": { - "facile-it/paraunit": "^1.3 || ^2.0", - "infection/infection": "^0.27.11", + "facile-it/paraunit": "^1.3 || ^2.3", + "infection/infection": "^0.29.5", "justinrainbow/json-schema": "^5.2", "keradus/cli-executor": "^2.1", "mikey179/vfsstream": "^1.6.11", "php-coveralls/php-coveralls": "^2.7", "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.4", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.4", - "phpunit/phpunit": "^9.6 || ^10.5.5 || ^11.0.2", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", "symfony/yaml": "^5.4 || ^6.0 || ^7.0" }, @@ -7293,7 +7293,10 @@ "autoload": { "psr-4": { "PhpCsFixer\\": "src/" - } + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -7318,7 +7321,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.58.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.59.3" }, "funding": [ { @@ -7326,7 +7329,7 @@ "type": "github" } ], - "time": "2024-05-29T16:39:07+00:00" + "time": "2024-06-16T14:17:03+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -8412,25 +8415,25 @@ }, { "name": "orchestra/canvas", - "version": "v9.0.2", + "version": "v9.0.3", "source": { "type": "git", "url": "https://github.com/orchestral/canvas.git", - "reference": "1bb5fea96fbba7a63c1f6f80651ac670ceb1fb5e" + "reference": "bed541346ffae1b9246afae4d196e8fc10ac9067" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/orchestral/canvas/zipball/1bb5fea96fbba7a63c1f6f80651ac670ceb1fb5e", - "reference": "1bb5fea96fbba7a63c1f6f80651ac670ceb1fb5e", + "url": "https://api.github.com/repos/orchestral/canvas/zipball/bed541346ffae1b9246afae4d196e8fc10ac9067", + "reference": "bed541346ffae1b9246afae4d196e8fc10ac9067", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "composer/semver": "^3.0", - "illuminate/console": "^11.1.1", - "illuminate/database": "^11.1.1", - "illuminate/filesystem": "^11.1.1", - "illuminate/support": "^11.1.1", + "illuminate/console": "^11.10", + "illuminate/database": "^11.10", + "illuminate/filesystem": "^11.10", + "illuminate/support": "^11.10", "orchestra/canvas-core": "^9.0", "orchestra/testbench-core": "^9.0", "php": "^8.2", @@ -8438,10 +8441,10 @@ "symfony/yaml": "^7.0" }, "require-dev": { - "laravel/framework": "^11.1.1", + "laravel/framework": "^11.10", "laravel/pint": "^1.6", "mockery/mockery": "^1.6", - "phpstan/phpstan": "^1.10.6", + "phpstan/phpstan": "^1.11", "phpunit/phpunit": "^11.0", "spatie/laravel-ray": "^1.35" }, @@ -8481,9 +8484,9 @@ "description": "Code Generators for Laravel Applications and Packages", "support": { "issues": "https://github.com/orchestral/canvas/issues", - "source": "https://github.com/orchestral/canvas/tree/v9.0.2" + "source": "https://github.com/orchestral/canvas/tree/v9.0.3" }, - "time": "2024-03-28T17:06:53+00:00" + "time": "2024-06-18T10:10:33+00:00" }, { "name": "orchestra/canvas-core", @@ -9560,16 +9563,16 @@ }, { "name": "phpstan/phpstan", - "version": "1.11.4", + "version": "1.11.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "9100a76ce8015b9aa7125b9171ae3a76887b6c82" + "reference": "490f0ae1c92b082f154681d7849aee776a7c1443" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9100a76ce8015b9aa7125b9171ae3a76887b6c82", - "reference": "9100a76ce8015b9aa7125b9171ae3a76887b6c82", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/490f0ae1c92b082f154681d7849aee776a7c1443", + "reference": "490f0ae1c92b082f154681d7849aee776a7c1443", "shasum": "" }, "require": { @@ -9614,7 +9617,7 @@ "type": "github" } ], - "time": "2024-06-06T12:19:22+00:00" + "time": "2024-06-17T15:10:54+00:00" }, { "name": "phpstan/phpstan-deprecation-rules", @@ -11156,16 +11159,16 @@ }, { "name": "sanmai/pipeline", - "version": "v6.10", + "version": "v6.11", "source": { "type": "git", "url": "https://github.com/sanmai/pipeline.git", - "reference": "cbd2ea30ba8bef596b8dad1adb9c92fb2987e430" + "reference": "a5fa2a6c6ca93efa37e7c24aab72f47448a6b110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sanmai/pipeline/zipball/cbd2ea30ba8bef596b8dad1adb9c92fb2987e430", - "reference": "cbd2ea30ba8bef596b8dad1adb9c92fb2987e430", + "url": "https://api.github.com/repos/sanmai/pipeline/zipball/a5fa2a6c6ca93efa37e7c24aab72f47448a6b110", + "reference": "a5fa2a6c6ca93efa37e7c24aab72f47448a6b110", "shasum": "" }, "require": { @@ -11209,7 +11212,7 @@ "description": "General-purpose collections pipeline", "support": { "issues": "https://github.com/sanmai/pipeline/issues", - "source": "https://github.com/sanmai/pipeline/tree/v6.10" + "source": "https://github.com/sanmai/pipeline/tree/v6.11" }, "funding": [ { @@ -11217,7 +11220,7 @@ "type": "github" } ], - "time": "2024-03-16T01:33:30+00:00" + "time": "2024-06-15T03:11:19+00:00" }, { "name": "sebastian/cli-parser", @@ -13270,16 +13273,16 @@ }, { "name": "wayofdev/cs-fixer-config", - "version": "v1.5.1", + "version": "v1.5.3", "source": { "type": "git", "url": "https://github.com/wayofdev/php-cs-fixer-config.git", - "reference": "1edacc13db903e85ab42d5ff7d5fc04d11663d8e" + "reference": "aa0aae244772672f617de6e74d85ff81532f7e82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wayofdev/php-cs-fixer-config/zipball/1edacc13db903e85ab42d5ff7d5fc04d11663d8e", - "reference": "1edacc13db903e85ab42d5ff7d5fc04d11663d8e", + "url": "https://api.github.com/repos/wayofdev/php-cs-fixer-config/zipball/aa0aae244772672f617de6e74d85ff81532f7e82", + "reference": "aa0aae244772672f617de6e74d85ff81532f7e82", "shasum": "" }, "require": { @@ -13295,10 +13298,10 @@ "phpstan/phpstan-phpunit": "^1.4", "phpstan/phpstan-strict-rules": "^1.6", "phpunit/phpunit": "^10.5", - "psalm/plugin-phpunit": "~0.19.0", + "psalm/plugin-phpunit": "^0.19", "rector/rector": "^1.1", "roave/infection-static-analysis-plugin": "^1.35", - "vimeo/psalm": "^5.23.1" + "vimeo/psalm": "^5.24" }, "type": "library", "extra": { @@ -13322,7 +13325,7 @@ "email": "the@wayof.dev" } ], - "description": "Package adds custom rule-sets to php-cs-fixer", + "description": "🧹 Adds custom rule-sets to PHP CS Fixer for consistent coding standards.", "homepage": "https://wayof.dev", "keywords": [ "code-quality", @@ -13337,6 +13340,7 @@ ], "support": { "issues": "https://github.com/wayofdev/php-cs-fixer-config/issues", + "security": "https://github.com/wayofdev/php-cs-fixer-config/blob/master/.github/SECURITY.md", "source": "https://github.com/wayofdev/php-cs-fixer-config" }, "funding": [ @@ -13345,7 +13349,7 @@ "type": "github" } ], - "time": "2024-06-05T19:23:33+00:00" + "time": "2024-06-18T09:13:20+00:00" }, { "name": "zbateson/mail-mime-parser", diff --git a/config/serializer.php b/config/serializer.php index 4750a60d..006073d1 100644 --- a/config/serializer.php +++ b/config/serializer.php @@ -2,43 +2,63 @@ declare(strict_types=1); +use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; + +/** + * @return array{ + * default: string, + * debug: bool, + * normalizerRegistrationStrategy: class-string, + * encoderRegistrationStrategy: class-string, + * metadataLoader: class-string|null, + * } + */ return [ /* * The 'default' key specifies the name (format) of the default serializer * that will be registered in SerializerManager. This can be overridden * by setting the SERIALIZER_DEFAULT_FORMAT environment variable. */ - 'default' => env('SERIALIZER_DEFAULT_FORMAT', 'json'), + 'default' => env('SERIALIZER_DEFAULT_FORMAT', 'symfony-json'), /* - * The 'serializers' key lists the supported serializers: json, csv, xml, yaml. - * Set a serializer to "false" to disable it. This can be overridden by setting - * the corresponding SERIALIZER_USE_* environment variable. + * Specifies whether to enable debug mode for ProblemNormalizer. */ - 'serializers' => [ - 'json' => env('SERIALIZER_USE_JSON', true), - 'csv' => env('SERIALIZER_USE_CSV', false), - 'xml' => env('SERIALIZER_USE_XML', false), - 'yaml' => env('SERIALIZER_USE_YAML', false), - ], + 'debug' => env('SERIALIZER_DEBUG_MODE', env('APP_DEBUG', false)), /* - * The 'normalizers' key allows you to register your custom normalizers. - * Default normalizers are registered in src/NormalizersRegistry.php. - * Uncomment the line below and replace with your custom normalizer if needed - * to merge with default ones. + * Allows to specify additional, custom serializers that will be registered in SerializerManager. */ - 'normalizers' => [ - // Symfony\Component\Messenger\Transport\Serialization\Normalizer\FlattenExceptionNormalizer + 'manager' => [ + 'serializers' => [ + 'json' => '', + 'php' => '', + ], ], /* - * The 'encoders' key allows you to register your custom encoders. - * Default encoders are registered in src/EncodersRegistry.php. - * Default encoders include JsonEncoder, CsvEncoder, XmlEncoder, and YamlEncoder. - * Uncomment the line below and replace with your custom encoder if needed. + * Allows you to specify the strategy class for registering your normalizers. + * Default is 'WayOfDev\Serializer\DefaultNormalizerRegistrationStrategy'. */ - 'encoders' => [ - // Symfony\Component\Serializer\Encoder\JsonEncoder - ], + 'normalizerRegistrationStrategy' => WayOfDev\Serializer\DefaultNormalizerRegistrationStrategy::class, + + /* + * Allows you to register your custom encoders. + * Default encoders are registered in src/DefaultEncoderRegistrationStrategy.php. + * + * Default encoders include: + * JsonEncoder, + * CsvEncoder, + * XmlEncoder, + * YamlEncoder. + * + * You can replace the default encoders with your custom ones by implementing + * your own registration strategy. + */ + 'encoderRegistrationStrategy' => WayOfDev\Serializer\DefaultEncoderRegistrationStrategy::class, + + /* + * Allows you to register your custom metadata loader. + */ + 'metadataLoader' => null, ]; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0221ab17..713ae18d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,75 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Access to an undefined property WayOfDev\\\\Serializer\\\\Config\\:\\:\\$config\\.$#" - count: 1 - path: src/Config.php - - - - message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" - count: 1 - path: src/Config.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Config\\:\\:__construct\\(\\) has parameter \\$encoders with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Config.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Config\\:\\:__construct\\(\\) has parameter \\$normalizers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Config.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Config\\:\\:encoders\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Config.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Config\\:\\:fromArray\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Config.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Config\\:\\:normalizers\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Config.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Contracts\\\\ConfigRepository\\:\\:encoders\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/ConfigRepository.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Contracts\\\\ConfigRepository\\:\\:normalizers\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/ConfigRepository.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Contracts\\\\NormalizersRegistryInterface\\:\\:all\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/NormalizersRegistryInterface.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Contracts\\\\SerializerInterface\\:\\:normalize\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Contracts/SerializerInterface.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Contracts\\\\SerializerInterface\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Contracts/SerializerInterface.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Normalizers\\\\RamseyUuidNormalizer\\:\\:denormalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizers/RamseyUuidNormalizer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Normalizers\\\\RamseyUuidNormalizer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizers/RamseyUuidNormalizer.php - - message: "#^Method WayOfDev\\\\Serializer\\\\Normalizers\\\\RamseyUuidNormalizer\\:\\:normalize\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -80,162 +10,7 @@ parameters: count: 1 path: src/Normalizers/RamseyUuidNormalizer.php - - - message: "#^Method WayOfDev\\\\Serializer\\\\Normalizers\\\\RamseyUuidNormalizer\\:\\:supportsDenormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizers/RamseyUuidNormalizer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Normalizers\\\\RamseyUuidNormalizer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizers/RamseyUuidNormalizer.php - - message: "#^Parameter \\#1 \\$object \\(Ramsey\\\\Uuid\\\\UuidInterface\\) of method WayOfDev\\\\Serializer\\\\Normalizers\\\\RamseyUuidNormalizer\\:\\:normalize\\(\\) should be contravariant with parameter \\$object \\(mixed\\) of method Symfony\\\\Component\\\\Serializer\\\\Normalizer\\\\NormalizerInterface\\:\\:normalize\\(\\)$#" count: 1 path: src/Normalizers/RamseyUuidNormalizer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\NormalizersRegistry\\:\\:__construct\\(\\) has parameter \\$normalizers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/NormalizersRegistry.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\ResponseFactory\\:\\:fromArray\\(\\) has parameter \\$response with no value type specified in iterable type array\\.$#" - count: 1 - path: src/ResponseFactory.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\ResponseFactory\\:\\:serializeResponse\\(\\) has parameter \\$response with no value type specified in iterable type array\\.$#" - count: 1 - path: src/ResponseFactory.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\ResponseFactory\\:\\:withContext\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/ResponseFactory.php - - - - message: "#^Property WayOfDev\\\\Serializer\\\\ResponseFactory\\:\\:\\$context type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/ResponseFactory.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:decode\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:denormalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:encode\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:normalize\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:normalize\\(\\) return type with generic class ArrayObject does not specify its types\\: TKey, TValue$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:serialize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:supportsDecoding\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:supportsDenormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:supportsEncoding\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\Serializer\\:\\:unserialize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Serializer.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\SerializerManager\\:\\:normalize\\(\\) has no return type specified\\.$#" - count: 1 - path: src/SerializerManager.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\SerializerManager\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/SerializerManager.php - - - - message: "#^Method WayOfDev\\\\Serializer\\\\SerializerRegistry\\:\\:__construct\\(\\) has parameter \\$serializers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/SerializerRegistry.php - - - - message: "#^Method WayOfDev\\\\App\\\\NestedObjects\\\\City\\:\\:jsonSerialize\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: tests/app/NestedObjects/City.php - - - - message: "#^Class WayOfDev\\\\App\\\\Response extends generic class ArrayIterator but does not specify its types\\: TKey, TValue$#" - count: 1 - path: tests/app/Response.php - - - - message: "#^Method WayOfDev\\\\App\\\\Response\\:\\:__construct\\(\\) has parameter \\$items with no value type specified in iterable type array\\.$#" - count: 1 - path: tests/app/Response.php - - - - message: "#^Method WayOfDev\\\\App\\\\Response\\:\\:create\\(\\) has parameter \\$items with no value type specified in iterable type array\\.$#" - count: 1 - path: tests/app/Response.php - - - - message: "#^Method WayOfDev\\\\Tests\\\\Functional\\\\Normalizers\\\\RamseyUuidNormalizerTest\\:\\:serializeDataProvider\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: tests/src/Functional/Normalizers/RamseyUuidNormalizerTest.php - - - - message: "#^Method WayOfDev\\\\Tests\\\\Functional\\\\NormalizersRegistryTest\\:\\:assertContainsInstanceOf\\(\\) has parameter \\$array with no value type specified in iterable type array\\.$#" - count: 1 - path: tests/src/Functional/NormalizersRegistryTest.php - - - - message: "#^Method WayOfDev\\\\Tests\\\\Functional\\\\SerializerManagerTest\\:\\:serializeDataProvider\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: tests/src/Functional/SerializerManagerTest.php - - - - message: "#^Method WayOfDev\\\\Tests\\\\Functional\\\\SerializerManagerTest\\:\\:unserializeDataProvider\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: tests/src/Functional/SerializerManagerTest.php - - - - message: "#^Method WayOfDev\\\\Tests\\\\Functional\\\\SerializerTest\\:\\:serializeDataProvider\\(\\) return type has no value type specified in iterable type Traversable\\.$#" - count: 1 - path: tests/src/Functional/SerializerTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 87a4bd57..31cb23fb 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -2,7 +2,7 @@ includes: - phpstan-baseline.neon parameters: - level: 6 + level: 8 paths: - config/ - src/ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 16fc71a2..48d54a27 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,6 +17,9 @@ tests/src/Functional + + tests/src/Unit + diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 03e37337..77b5d468 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,273 +1,180 @@ + + + + + - get(LoaderInterface::class)]]> - encoders()]]> - normalizers()]]> - all()]]> - - all()]]> - - + + - $encoder instanceof EncoderInterface ? $encoder : $app->get($encoder), - $config->encoders() - )]]> + $config->get('serializer.default'), + 'debug' => $config->get('serializer.debug'), + 'normalizerRegistrationStrategy' => $config->get('serializer.normalizerRegistrationStrategy'), + 'encoderRegistrationStrategy' => $config->get('serializer.encoderRegistrationStrategy'), + 'metadataLoader' => $config->get('serializer.metadataLoader'), + ]]]> - - - - - - + + - + + - - - - - - - - make(ConfigRepository::class)->metadataLoader()]]> + make($strategyFQCN)]]> + make($strategyFQCN, [ + 'loader' => $loader, + 'debugMode' => $config->debug(), + ])]]> - - - - - - - config['metadataLoader']]]> - - - - - - config['metadataLoader']]]> - - - defaultSerializer]]> - encoders]]> - normalizers]]> - - - - - - - - + + encoderRegistrationStrategy === null]]> + normalizerRegistrationStrategy === null]]> + + + + + + + - - - + - - - - - - - - - + + + + - - - - - - - - - - - - - - - - - - + - + - - - - + - - - - - - - + + + + + - - - - - - + - + - - - - + + - - - - - - - - + + + + + - + - + + + + - - - - - + + + - + - serialize($payload, $format)]]> + app?->make(EncoderRegistrationStrategy::class)]]> + app?->make(EncoderRegistrationStrategy::class)]]> - - - - - - - - - - - - + + app?->make(EncoderRegistrationStrategy::class)]]> + app?->make(EncoderRegistrationStrategy::class)]]> + - - - + + + - - - - + - - - - + - - - - - - - - + - - - - - + + + + - - - serializer]]> - - - - + - - - - - - + - + - serialize($payload, $format)]]> + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + serialize($payload, $format)]]> + - - - - - - + + - - + + - - - - + + + + + + + + + + diff --git a/src/Bridge/Laravel/Facades/Manager.php b/src/Bridge/Laravel/Facades/Manager.php new file mode 100644 index 00000000..39c4c2c6 --- /dev/null +++ b/src/Bridge/Laravel/Facades/Manager.php @@ -0,0 +1,15 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + declare(strict_types=1); -namespace WayOfDev\Serializer; +namespace WayOfDev\Serializer\Bridge\Laravel\Http; interface HttpCode { diff --git a/src/ResponseFactory.php b/src/Bridge/Laravel/Http/ResponseFactory.php similarity index 75% rename from src/ResponseFactory.php rename to src/Bridge/Laravel/Http/ResponseFactory.php index 568afed9..e9929b31 100644 --- a/src/ResponseFactory.php +++ b/src/Bridge/Laravel/Http/ResponseFactory.php @@ -2,25 +2,27 @@ declare(strict_types=1); -namespace WayOfDev\Serializer; +namespace WayOfDev\Serializer\Bridge\Laravel\Http; use Illuminate\Http\Response as IlluminateResponse; +use Stringable; use WayOfDev\Serializer\Contracts\SerializerInterface; +use WayOfDev\Serializer\Manager\SerializerManager; final class ResponseFactory { - /** - * @property SerializerInterface $serializer - */ private readonly SerializerInterface $serializer; + /** + * @var array + */ private array $context = []; private int $status = HttpCode::HTTP_OK; public function __construct(SerializerManager $serializer) { - $this->serializer = $serializer->getSerializer('json'); + $this->serializer = $serializer->serializer('symfony-json'); } public function withStatusCode(int $code): self @@ -30,6 +32,9 @@ public function withStatusCode(int $code): self return $this; } + /** + * @param array $context + */ public function withContext(array $context): self { $this->context = $context; @@ -44,6 +49,9 @@ public function create(object $response): IlluminateResponse return $this->respondWithJson($content, $this->status); } + /** + * @param array $response + */ public function fromArray(array $response): IlluminateResponse { $content = $this->serializeResponse($response); @@ -56,14 +64,15 @@ public function empty(): IlluminateResponse return $this->respondWithJson('', HttpCode::HTTP_NO_CONTENT); } - private function serializeResponse(array|object $response): string + /** + * @param array|object $response + */ + private function serializeResponse(array|object $response): string|Stringable { - $normalizedResponse = $this->serializer->normalize( - data: $response, + return $this->serializer->serialize( + payload: $response, context: $this->context ); - - return $this->serializer->serialize($normalizedResponse); } private function respondWithJson(mixed $content, int $status): IlluminateResponse diff --git a/src/Bridge/Laravel/Providers/SerializerServiceProvider.php b/src/Bridge/Laravel/Providers/SerializerServiceProvider.php index b2b4c5e2..10fbc9b9 100644 --- a/src/Bridge/Laravel/Providers/SerializerServiceProvider.php +++ b/src/Bridge/Laravel/Providers/SerializerServiceProvider.php @@ -7,26 +7,23 @@ use Illuminate\Contracts\Config\Repository; use Illuminate\Contracts\Foundation\Application; use Illuminate\Support\ServiceProvider; -use Symfony\Component\Serializer\Encoder\EncoderInterface; use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Serializer as SymfonySerializer; use Symfony\Component\Serializer\SerializerInterface as SymfonySerializerInterface; use Symfony\Component\Yaml\Dumper; use WayOfDev\Serializer\Config; use WayOfDev\Serializer\Contracts\ConfigRepository; -use WayOfDev\Serializer\Contracts\EncodersRegistryInterface; -use WayOfDev\Serializer\Contracts\NormalizersRegistryInterface; -use WayOfDev\Serializer\Contracts\SerializerInterface; +use WayOfDev\Serializer\Contracts\EncoderRegistrationStrategy; +use WayOfDev\Serializer\Contracts\EncoderRegistryInterface; +use WayOfDev\Serializer\Contracts\NormalizerRegistrationStrategy; +use WayOfDev\Serializer\Contracts\NormalizerRegistryInterface; use WayOfDev\Serializer\Contracts\SerializerRegistryInterface; -use WayOfDev\Serializer\EncodersRegistry; -use WayOfDev\Serializer\NormalizersRegistry; -use WayOfDev\Serializer\Serializer; -use WayOfDev\Serializer\SerializerManager; -use WayOfDev\Serializer\SerializerRegistry; +use WayOfDev\Serializer\EncoderRegistry; +use WayOfDev\Serializer\Manager\Serializer; +use WayOfDev\Serializer\Manager\SerializerManager; +use WayOfDev\Serializer\Manager\SerializerRegistry; +use WayOfDev\Serializer\NormalizerRegistry; -use function array_map; use function class_exists; final class SerializerServiceProvider extends ServiceProvider @@ -47,10 +44,10 @@ public function register(): void $this->mergeConfigFrom(__DIR__ . '/../../../../config/serializer.php', 'serializer'); $this->registerConfig(); - $this->registerNormalizersRegistry(); - $this->registerEncodersRegistry(); - $this->registerSerializerRegistry(); $this->registerLoader(); + $this->registerNormalizerRegistry(); + $this->registerEncoderRegistry(); + $this->registerSerializerRegistry(); $this->registerSerializerManager(); $this->registerSymfonySerializer(); } @@ -59,58 +56,80 @@ private function registerConfig(): void { $this->app->singleton(ConfigRepository::class, static function (Application $app) { /** @var Repository $config */ - $config = $app['config']; + $config = $app->get(Repository::class); return Config::fromArray([ 'default' => $config->get('serializer.default'), - 'normalizers' => $config->get('serializer.normalizers'), - 'encoders' => $config->get('serializer.encoders'), + 'debug' => $config->get('serializer.debug'), + 'normalizerRegistrationStrategy' => $config->get('serializer.normalizerRegistrationStrategy'), + 'encoderRegistrationStrategy' => $config->get('serializer.encoderRegistrationStrategy'), + 'metadataLoader' => $config->get('serializer.metadataLoader'), ]); }); } - private function registerNormalizersRegistry(): void + private function registerLoader(): void { - $this->app->singleton(NormalizersRegistryInterface::class, function (Application $app): NormalizersRegistryInterface { + $this->app->singleton(LoaderInterface::class, static function (Application $app): LoaderInterface { + /** @var Config $config */ $config = $app->make(ConfigRepository::class); - $normalizers = array_map(fn (mixed $normalizer) => match (true) { - $normalizer instanceof NormalizerInterface, $normalizer instanceof DenormalizerInterface => $normalizer, - default => $this->app->get($normalizer) - }, $config->normalizers()); + return $config->metadataLoader(); + }); + } - return new NormalizersRegistry( - $app->get(LoaderInterface::class), - true, - $normalizers - ); + private function registerNormalizerRegistry(): void + { + $this->app->singleton(NormalizerRegistrationStrategy::class, static function (Application $app): NormalizerRegistrationStrategy { + /** @var Config $config */ + $config = $app->make(ConfigRepository::class); + + /** @var LoaderInterface $loader */ + $loader = $app->get(LoaderInterface::class); + + $strategyFQCN = $config->normalizerRegistrationStrategy(); + + return $app->make($strategyFQCN, [ + 'loader' => $loader, + 'debugMode' => $config->debug(), + ]); + }); + + $this->app->singleton(NormalizerRegistryInterface::class, static function (Application $app): NormalizerRegistryInterface { + $strategy = $app->get(NormalizerRegistrationStrategy::class); + + return new NormalizerRegistry($strategy); }); } - private function registerEncodersRegistry(): void + private function registerEncoderRegistry(): void { - $this->app->singleton(EncodersRegistryInterface::class, static function (Application $app): EncodersRegistryInterface { + $this->app->singleton(EncoderRegistrationStrategy::class, static function (Application $app): EncoderRegistrationStrategy { + /** @var Config $config */ $config = $app->make(ConfigRepository::class); - return new EncodersRegistry( - array_map( - static fn (string|EncoderInterface $encoder) => $encoder instanceof EncoderInterface ? $encoder : $app->get($encoder), - $config->encoders() - ) - ); + $strategyFQCN = $config->encoderRegistrationStrategy(); + + return $app->make($strategyFQCN); + }); + + $this->app->singleton(EncoderRegistryInterface::class, static function (Application $app): EncoderRegistryInterface { + $strategy = $app->get(EncoderRegistrationStrategy::class); + + return new EncoderRegistry($strategy); }); } private function registerSerializerRegistry(): void { $this->app->singleton(SerializerRegistryInterface::class, static function (Application $app): SerializerRegistryInterface { - // $config = $app->make(ConfigRepository::class); + /** @var SymfonySerializer $serializer */ $serializer = $app->make(SymfonySerializerInterface::class); $serializers = [ - 'json' => new Serializer($serializer, 'json'), - 'csv' => new Serializer($serializer, 'csv'), - 'xml' => new Serializer($serializer, 'xml'), + 'symfony-json' => new Serializer($serializer, 'json'), + 'symfony-csv' => new Serializer($serializer, 'csv'), + 'symfony-xml' => new Serializer($serializer, 'xml'), ]; if (class_exists(Dumper::class)) { @@ -121,11 +140,22 @@ private function registerSerializerRegistry(): void }); } - private function registerLoader(): void + private function registerSymfonySerializer(): void { - $this->app->singleton(LoaderInterface::class, static function (Application $app): LoaderInterface { - return $app->make(ConfigRepository::class)->metadataLoader(); + $this->app->singleton(SymfonySerializerInterface::class, static function (Application $app): SymfonySerializer { + /** @var NormalizerRegistryInterface $normalizers */ + $normalizers = $app->make(NormalizerRegistryInterface::class); + + /** @var EncoderRegistryInterface $encoders */ + $encoders = $app->make(EncoderRegistryInterface::class); + + return new SymfonySerializer( + $normalizers->all(), + $encoders->all() + ); }); + + $this->app->singleton(SymfonySerializer::class, SymfonySerializerInterface::class); } private function registerSerializerManager(): void @@ -133,23 +163,13 @@ private function registerSerializerManager(): void $this->app->singleton(SerializerManager::class, static function (Application $app): SerializerManager { /** @var Config $config */ $config = $app->make(ConfigRepository::class); + + /** @var SerializerRegistry $serializers */ $serializers = $app->make(SerializerRegistryInterface::class); return new SerializerManager($serializers, $config->defaultSerializer()); }); - $this->app->alias(SerializerManager::class, SerializerInterface::class); - } - - private function registerSymfonySerializer(): void - { - $this->app->singleton(SymfonySerializerInterface::class, static function (Application $app): SymfonySerializer { - $normalizers = $app->make(NormalizersRegistryInterface::class); - $encoders = $app->make(EncodersRegistryInterface::class); - - return new SymfonySerializer($normalizers->all(), $encoders->all()); - }); - - $this->app->singleton(Serializer::class, SerializerInterface::class); + $this->app->alias(SerializerManager::class, 'serializer.manager'); } } diff --git a/src/Config.php b/src/Config.php index b446ecb1..5998e195 100644 --- a/src/Config.php +++ b/src/Config.php @@ -7,6 +7,8 @@ use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; use WayOfDev\Serializer\Contracts\ConfigRepository; +use WayOfDev\Serializer\Contracts\EncoderRegistrationStrategy; +use WayOfDev\Serializer\Contracts\NormalizerRegistrationStrategy; use WayOfDev\Serializer\Exceptions\MissingRequiredAttributes; use function array_diff; @@ -17,17 +19,33 @@ final class Config implements ConfigRepository { private const REQUIRED_FIELDS = [ 'default', - 'normalizers', - 'encoders', + 'debug', + 'normalizerRegistrationStrategy', + 'encoderRegistrationStrategy', + 'metadataLoader', ]; public function __construct( - private readonly string $defaultSerializer, - private readonly array $normalizers, - private readonly array $encoders + private readonly string $defaultSerializer = 'symfony-json', + private readonly bool $debug = false, + /** @var class-string */ + private readonly ?string $normalizerRegistrationStrategy = null, + /** @var class-string */ + private readonly ?string $encoderRegistrationStrategy = null, + /** @var class-string|null */ + private readonly ?string $metadataLoader = null, ) { } + /** + * @param array{ + * default: string, + * debug: bool, + * normalizerRegistrationStrategy: class-string, + * encoderRegistrationStrategy: class-string, + * metadataLoader: class-string|null, + * } $config + */ public static function fromArray(array $config): self { $missingAttributes = array_diff(self::REQUIRED_FIELDS, array_keys($config)); @@ -40,30 +58,51 @@ public static function fromArray(array $config): self return new self( $config['default'], - $config['normalizers'], - $config['encoders'], + $config['debug'], + $config['normalizerRegistrationStrategy'], + $config['encoderRegistrationStrategy'], + $config['metadataLoader'] ); } public function defaultSerializer(): string { - return $this->defaultSerializer ?? 'json'; + return $this->defaultSerializer; } - public function normalizers(): array + public function debug(): bool { - return $this->normalizers ?? []; + return $this->debug; } - public function encoders(): array + /** + * @return class-string + */ + public function normalizerRegistrationStrategy(): string { - return $this->encoders ?? []; + if ($this->normalizerRegistrationStrategy === null) { + return DefaultNormalizerRegistrationStrategy::class; + } + + return $this->normalizerRegistrationStrategy; + } + + /** + * @return class-string + */ + public function encoderRegistrationStrategy(): string + { + if ($this->encoderRegistrationStrategy === null) { + return DefaultEncoderRegistrationStrategy::class; + } + + return $this->encoderRegistrationStrategy; } public function metadataLoader(): LoaderInterface { - if (! empty($this->config['metadataLoader'])) { - return $this->config['metadataLoader']; + if ($this->metadataLoader !== null) { + return new ($this->metadataLoader); } return new AttributeLoader(); diff --git a/src/Contracts/ConfigRepository.php b/src/Contracts/ConfigRepository.php index e979b750..192b86a2 100644 --- a/src/Contracts/ConfigRepository.php +++ b/src/Contracts/ConfigRepository.php @@ -8,9 +8,15 @@ interface ConfigRepository { - public function normalizers(): array; + /** + * @return class-string + */ + public function normalizerRegistrationStrategy(): string; - public function encoders(): array; + /** + * @return class-string + */ + public function encoderRegistrationStrategy(): string; public function metadataLoader(): LoaderInterface; } diff --git a/src/Contracts/EncoderRegistrationStrategy.php b/src/Contracts/EncoderRegistrationStrategy.php new file mode 100644 index 00000000..334c31c7 --- /dev/null +++ b/src/Contracts/EncoderRegistrationStrategy.php @@ -0,0 +1,16 @@ + + */ + public function encoders(): iterable; +} diff --git a/src/Contracts/EncodersRegistryInterface.php b/src/Contracts/EncoderRegistryInterface.php similarity index 59% rename from src/Contracts/EncodersRegistryInterface.php rename to src/Contracts/EncoderRegistryInterface.php index d1f55b49..072267a8 100644 --- a/src/Contracts/EncodersRegistryInterface.php +++ b/src/Contracts/EncoderRegistryInterface.php @@ -4,19 +4,20 @@ namespace WayOfDev\Serializer\Contracts; +use Symfony\Component\Serializer\Encoder\DecoderInterface; use Symfony\Component\Serializer\Encoder\EncoderInterface; -interface EncodersRegistryInterface +interface EncoderRegistryInterface { public function register(EncoderInterface $encoder): void; /** - * @return EncoderInterface[] + * @return list */ public function all(): array; /** - * @phpstan-param class-string $className + * @param class-string $className */ public function has(string $className): bool; } diff --git a/src/Contracts/NormalizerRegistrationStrategy.php b/src/Contracts/NormalizerRegistrationStrategy.php new file mode 100644 index 00000000..ca4e2bc7 --- /dev/null +++ b/src/Contracts/NormalizerRegistrationStrategy.php @@ -0,0 +1,16 @@ +}> + */ + public function normalizers(): iterable; +} diff --git a/src/Contracts/NormalizersRegistryInterface.php b/src/Contracts/NormalizerRegistryInterface.php similarity index 81% rename from src/Contracts/NormalizersRegistryInterface.php rename to src/Contracts/NormalizerRegistryInterface.php index d7c30e94..890cd658 100644 --- a/src/Contracts/NormalizersRegistryInterface.php +++ b/src/Contracts/NormalizerRegistryInterface.php @@ -7,13 +7,16 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -interface NormalizersRegistryInterface +interface NormalizerRegistryInterface { /** * @param int<0, max> $priority */ public function register(NormalizerInterface|DenormalizerInterface $normalizer, int $priority = 0): void; + /** + * @return array + */ public function all(): array; /** diff --git a/src/Contracts/SerializerException.php b/src/Contracts/SerializerException.php new file mode 100644 index 00000000..835bfd7b --- /dev/null +++ b/src/Contracts/SerializerException.php @@ -0,0 +1,11 @@ + $context Options normalizers/encoders have access to + */ + public function serialize(mixed $payload, ?string $format = null, ?array $context = []): string|Stringable; - public function unserialize(string|Stringable $payload, string|object|null $type = null): mixed; - - public function normalize(mixed $data, ?string $format = null, array $context = []); + /** + * Deserializes data into the given type. + * + * @param array $context + */ + public function deserialize( + string|Stringable $payload, + string|object|null $type = null, + ?string $format = null, + ?array $context = [] + ): mixed; } diff --git a/src/Contracts/SerializerRegistryInterface.php b/src/Contracts/SerializerRegistryInterface.php index cf571f6b..24efb4f1 100644 --- a/src/Contracts/SerializerRegistryInterface.php +++ b/src/Contracts/SerializerRegistryInterface.php @@ -4,16 +4,21 @@ namespace WayOfDev\Serializer\Contracts; -use WayOfDev\Serializer\Exceptions\SerializerNotFoundException; +use WayOfDev\Serializer\Exceptions\SerializerNotFound; interface SerializerRegistryInterface { public function register(string $name, SerializerInterface $serializer): void; /** - * @throws SerializerNotFoundException + * @throws SerializerNotFound */ public function get(string $name): SerializerInterface; + /** + * @return list + */ + public function all(): array; + public function has(string $name): bool; } diff --git a/src/DefaultEncoderRegistrationStrategy.php b/src/DefaultEncoderRegistrationStrategy.php new file mode 100644 index 00000000..65cadd9d --- /dev/null +++ b/src/DefaultEncoderRegistrationStrategy.php @@ -0,0 +1,29 @@ + + */ + public function encoders(): iterable + { + yield ['encoder' => new Encoder\JsonEncoder()]; + yield ['encoder' => new Encoder\CsvEncoder()]; + yield ['encoder' => new Encoder\XmlEncoder()]; + + if (class_exists(Dumper::class)) { + yield ['encoder' => new Encoder\YamlEncoder()]; + } + } +} diff --git a/src/DefaultNormalizerRegistrationStrategy.php b/src/DefaultNormalizerRegistrationStrategy.php new file mode 100644 index 00000000..5298d8c0 --- /dev/null +++ b/src/DefaultNormalizerRegistrationStrategy.php @@ -0,0 +1,62 @@ +}> + */ + public function normalizers(): iterable + { + $factory = new ClassMetadataFactory($this->loader); + + yield ['normalizer' => new Normalizer\UnwrappingDenormalizer(), 'priority' => 50]; + yield ['normalizer' => new Normalizer\ProblemNormalizer(debug: $this->debugMode), 'priority' => 100]; + yield ['normalizer' => new Normalizer\UidNormalizer(), 'priority' => 150]; + yield ['normalizer' => new Normalizer\JsonSerializableNormalizer(), 'priority' => 200]; + yield ['normalizer' => new Normalizer\DateTimeNormalizer(), 'priority' => 250]; + yield ['normalizer' => new Normalizer\ConstraintViolationListNormalizer(), 'priority' => 300]; + yield ['normalizer' => new Normalizer\MimeMessageNormalizer(new Normalizer\PropertyNormalizer()), 'priority' => 350]; + yield ['normalizer' => new Normalizer\DateTimeZoneNormalizer(), 'priority' => 400]; + yield ['normalizer' => new Normalizer\DateIntervalNormalizer(), 'priority' => 450]; + yield ['normalizer' => new Normalizer\FormErrorNormalizer(), 'priority' => 500]; + yield ['normalizer' => new Normalizer\BackedEnumNormalizer(), 'priority' => 550]; + yield ['normalizer' => new Normalizer\DataUriNormalizer(), 'priority' => 600]; + yield ['normalizer' => new Normalizer\ArrayDenormalizer(), 'priority' => 650]; + yield ['normalizer' => new Normalizer\ObjectNormalizer( + classMetadataFactory: $factory, + nameConverter: new MetadataAwareNameConverter($factory), + propertyTypeExtractor: new PropertyInfoExtractor( + typeExtractors: [new PhpDocExtractor(), new ReflectionExtractor()] + ) + ), 'priority' => 700]; + + if (interface_exists(UuidInterface::class)) { + yield ['normalizer' => new RamseyUuidNormalizer(), 'priority' => 155]; + } + } +} diff --git a/src/EncoderRegistry.php b/src/EncoderRegistry.php new file mode 100644 index 00000000..bda149ab --- /dev/null +++ b/src/EncoderRegistry.php @@ -0,0 +1,50 @@ + + */ + private array $encoders = []; + + public function __construct(EncoderRegistrationStrategy $strategy) + { + foreach ($strategy->encoders() as $encoder) { + $this->register($encoder['encoder']); + } + } + + public function register(EncoderInterface|DecoderInterface $encoder): void + { + if (! $this->has($encoder::class)) { + $this->encoders[$encoder::class] = $encoder; + } + } + + /** + * @return list + */ + public function all(): array + { + return array_values($this->encoders); + } + + /** + * @phpstan-param class-string $className + */ + public function has(string $className): bool + { + return isset($this->encoders[$className]); + } +} diff --git a/src/EncodersRegistry.php b/src/EncodersRegistry.php deleted file mode 100644 index 172591f7..00000000 --- a/src/EncodersRegistry.php +++ /dev/null @@ -1,68 +0,0 @@ - - */ - private array $encoders = []; - - /** - * @param EncoderInterface[] $encoders - */ - public function __construct(array $encoders = []) - { - if ($encoders === []) { - $this->registerDefaultEncoders(); - } else { - foreach ($encoders as $encoder) { - $this->register($encoder); - } - } - } - - public function register(EncoderInterface $encoder): void - { - if (! $this->has($encoder::class)) { - $this->encoders[$encoder::class] = $encoder; - } - } - - /** - * @return EncoderInterface[] - */ - public function all(): array - { - return array_values($this->encoders); - } - - /** - * @phpstan-param class-string $className - */ - public function has(string $className): bool - { - return isset($this->encoders[$className]); - } - - private function registerDefaultEncoders(): void - { - $this->register(new Encoder\JsonEncoder()); - $this->register(new Encoder\CsvEncoder()); - $this->register(new Encoder\XmlEncoder()); - if (class_exists(Dumper::class)) { - $this->register(new Encoder\YamlEncoder()); - } - } -} diff --git a/src/Exceptions/MissingRequiredAttributes.php b/src/Exceptions/MissingRequiredAttributes.php index d8da1524..36c678f2 100644 --- a/src/Exceptions/MissingRequiredAttributes.php +++ b/src/Exceptions/MissingRequiredAttributes.php @@ -5,10 +5,11 @@ namespace WayOfDev\Serializer\Exceptions; use InvalidArgumentException; +use WayOfDev\Serializer\Contracts\SerializerException; use function sprintf; -final class MissingRequiredAttributes extends InvalidArgumentException +final class MissingRequiredAttributes extends InvalidArgumentException implements SerializerException { public static function fromArray(string $fields): self { diff --git a/src/Exceptions/SerializerNotFoundException.php b/src/Exceptions/SerializerNotFound.php similarity index 65% rename from src/Exceptions/SerializerNotFoundException.php rename to src/Exceptions/SerializerNotFound.php index f3024268..e918ff04 100644 --- a/src/Exceptions/SerializerNotFoundException.php +++ b/src/Exceptions/SerializerNotFound.php @@ -5,10 +5,11 @@ namespace WayOfDev\Serializer\Exceptions; use RuntimeException; +use WayOfDev\Serializer\Contracts\SerializerException; use function sprintf; -final class SerializerNotFoundException extends RuntimeException +final class SerializerNotFound extends RuntimeException implements SerializerException { public function __construct(string $name) { diff --git a/src/Exceptions/UnsupportedTypeException.php b/src/Exceptions/UnsupportedTypeException.php deleted file mode 100644 index 6c674855..00000000 --- a/src/Exceptions/UnsupportedTypeException.php +++ /dev/null @@ -1,16 +0,0 @@ -serializer->serialize($payload, $this->format, $context); + } + + public function deserialize(Stringable|string $payload, object|string|null $type = null, ?string $format = null, ?array $context = []): mixed + { + $context = $context ?? []; + + return $this->serializer->deserialize( + (string) $payload, + is_object($type) ? $type::class : $type, // @phpstan-ignore-line + $this->format, + $context + ); + } +} diff --git a/src/Manager/SerializerManager.php b/src/Manager/SerializerManager.php new file mode 100644 index 00000000..c1b7baed --- /dev/null +++ b/src/Manager/SerializerManager.php @@ -0,0 +1,53 @@ +defaultFormat; + } + + public function serializer(?string $format = null): SerializerInterface + { + return $this->serializers->get($format ?? $this->defaultFormat); + } + + /** + * @param array $context + */ + public function serialize(mixed $payload, ?string $format = null, ?array $context = []): string|Stringable + { + $format = $format ?? $this->defaultFormat; + + return $this->serializer($format)->serialize($payload, $format, $context); + } + + /** + * Deserializes data into the given type. + * + * @param array $context + */ + public function deserialize( + string|Stringable $payload, + string|object|null $type = null, + ?string $format = null, + ?array $context = [] + ): mixed { + $format = $format ?? $this->defaultFormat; + + return $this->serializer($format)->deserialize($payload, $type, $format, $context); + } +} diff --git a/src/SerializerRegistry.php b/src/Manager/SerializerRegistry.php similarity index 63% rename from src/SerializerRegistry.php rename to src/Manager/SerializerRegistry.php index f3ceb346..b181e4df 100644 --- a/src/SerializerRegistry.php +++ b/src/Manager/SerializerRegistry.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace WayOfDev\Serializer; +namespace WayOfDev\Serializer\Manager; use WayOfDev\Serializer\Contracts\SerializerInterface; use WayOfDev\Serializer\Contracts\SerializerRegistryInterface; -use WayOfDev\Serializer\Exceptions\SerializerNotFoundException; +use WayOfDev\Serializer\Exceptions\SerializerNotFound; + +use function array_values; class SerializerRegistry implements SerializerRegistryInterface { /** - * @var SerializerInterface[] + * @param array $serializers */ - private array $serializers = []; - - public function __construct(array $serializers = []) + public function __construct(private array $serializers = []) { foreach ($serializers as $name => $serializer) { $this->register($name, $serializer); @@ -28,11 +28,19 @@ public function register(string $name, SerializerInterface $serializer): void } /** - * @throws SerializerNotFoundException + * @throws SerializerNotFound */ public function get(string $name): SerializerInterface { - return $this->serializers[$name] ?? throw new SerializerNotFoundException($name); + return $this->serializers[$name] ?? throw new SerializerNotFound($name); + } + + /** + * @return list + */ + public function all(): array + { + return array_values($this->serializers); } public function has(string $name): bool diff --git a/src/NormalizerRegistry.php b/src/NormalizerRegistry.php new file mode 100644 index 00000000..f9c6a061 --- /dev/null +++ b/src/NormalizerRegistry.php @@ -0,0 +1,62 @@ +, normalizer: NormalizerInterface|DenormalizerInterface}> + */ + private array $normalizers = []; + + public function __construct(NormalizerRegistrationStrategy $strategy) + { + foreach ($strategy->normalizers() as $normalizer) { + $this->register($normalizer['normalizer'], $normalizer['priority']); + } + } + + /** + * @param int<0, max> $priority + */ + public function register(NormalizerInterface|DenormalizerInterface $normalizer, int $priority = 0): void + { + if (! $this->has($normalizer::class)) { + $this->normalizers[$normalizer::class] = [ + 'priority' => $priority, + 'normalizer' => $normalizer, + ]; + } + } + + /** + * @return array + */ + public function all(): array + { + uasort( + $this->normalizers, + static fn (array $normalizer1, array $normalizer2) => $normalizer1['priority'] <=> $normalizer2['priority'] + ); + + return array_column($this->normalizers, 'normalizer'); + } + + /** + * @phpstan-param class-string $className + */ + public function has(string $className): bool + { + return isset($this->normalizers[$className]); + } +} diff --git a/src/Normalizers/RamseyUuidNormalizer.php b/src/Normalizers/RamseyUuidNormalizer.php index 717baadf..16b9443a 100644 --- a/src/Normalizers/RamseyUuidNormalizer.php +++ b/src/Normalizers/RamseyUuidNormalizer.php @@ -20,6 +20,7 @@ final class RamseyUuidNormalizer implements NormalizerInterface, DenormalizerInt { /** * @param UuidInterface $object + * @param array $context * * @psalm-suppress MoreSpecificImplementedParamType */ @@ -28,11 +29,17 @@ public function normalize(mixed $object, ?string $format = null, array $context return $object->toString(); } + /** + * @param array $context + */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { return $data instanceof UuidInterface; } + /** + * @param array $context + */ public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): UuidInterface { try { @@ -44,6 +51,9 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a } } + /** + * @param array $context + */ public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { return is_string($data) && is_a($type, UuidInterface::class, true) && Uuid::isValid($data); diff --git a/src/NormalizersRegistry.php b/src/NormalizersRegistry.php deleted file mode 100644 index 4cea9bbb..00000000 --- a/src/NormalizersRegistry.php +++ /dev/null @@ -1,107 +0,0 @@ -, normalizer: NormalizerInterface|DenormalizerInterface}> - */ - private array $normalizers = []; - - public function __construct( - protected readonly LoaderInterface $loader, - protected readonly bool $debugMode, - array $normalizers = [] - ) { - $this->registerDefaultNormalizers(); - - foreach ($normalizers as $normalizer) { - $this->register($normalizer); - } - } - - /** - * @param int<0, max> $priority - */ - public function register(NormalizerInterface|DenormalizerInterface $normalizer, int $priority = 0): void - { - if (! $this->has($normalizer::class)) { - $this->normalizers[$normalizer::class] = [ - 'priority' => $priority, - 'normalizer' => $normalizer, - ]; - } - } - - /** - * @return array - */ - public function all(): array - { - // Sort normalizers by priority - uasort( - $this->normalizers, - static fn (array $normalizer1, array $normalizer2) => $normalizer1['priority'] <=> $normalizer2['priority'] - ); - - return array_column($this->normalizers, 'normalizer'); - } - - /** - * @phpstan-param class-string $className - */ - public function has(string $className): bool - { - return isset($this->normalizers[$className]); - } - - private function registerDefaultNormalizers(): void - { - $factory = new ClassMetadataFactory($this->loader); - - $this->register(new Normalizer\UnwrappingDenormalizer(), 50); - $this->register(new Normalizer\ProblemNormalizer(debug: $this->debugMode), 100); - $this->register(new Normalizer\UidNormalizer(), 150); - $this->register(new Normalizer\JsonSerializableNormalizer(), 200); - $this->register(new Normalizer\DateTimeNormalizer(), 250); - $this->register(new Normalizer\ConstraintViolationListNormalizer(), 300); - $this->register(new Normalizer\MimeMessageNormalizer(new Normalizer\PropertyNormalizer()), 350); - $this->register(new Normalizer\DateTimeZoneNormalizer(), 400); - $this->register(new Normalizer\DateIntervalNormalizer(), 450); - $this->register(new Normalizer\FormErrorNormalizer(), 500); - $this->register(new Normalizer\BackedEnumNormalizer(), 550); - $this->register(new Normalizer\DataUriNormalizer(), 600); - $this->register(new Normalizer\ArrayDenormalizer(), 650); - $this->register(new Normalizer\ObjectNormalizer( - classMetadataFactory: $factory, - nameConverter: new MetadataAwareNameConverter($factory), - propertyTypeExtractor: new PropertyInfoExtractor( - typeExtractors: [new PhpDocExtractor(), new ReflectionExtractor()] - ) - ), 700); - - if (interface_exists(UuidInterface::class)) { - $this->register(new RamseyUuidNormalizer(), 155); - } - } -} diff --git a/src/Serializer.php b/src/Serializer.php deleted file mode 100644 index a5921eb6..00000000 --- a/src/Serializer.php +++ /dev/null @@ -1,91 +0,0 @@ -serializer->serialize($payload, $this->format); - } - - public function unserialize( - Stringable|string $payload, - object|string|null $type = null, - array $context = [] - ): mixed { - if ($type === null) { - throw new UnsupportedTypeException(); - } - - return $this->serializer->deserialize( - (string) $payload, - is_object($type) ? $type::class : $type, - $this->format, - $context - ); - } - - /** - * @throws ExceptionInterface - */ - public function normalize( - mixed $data, - ?string $format = null, - array $context = [] - ): array|string|int|float|bool|ArrayObject|null { - return $this->serializer->normalize($data, $format, $context); - } - - public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): mixed - { - return $this->serializer->denormalize($data, $type, $format, $context); - } - - public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool - { - return $this->serializer->supportsNormalization($data, $format, $context); - } - - public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool - { - return $this->serializer->supportsDenormalization($data, $type, $format, $context); - } - - public function encode(mixed $data, string $format, array $context = []): string - { - return $this->serializer->encode($data, $format, $context); - } - - public function decode(string $data, string $format, array $context = []): mixed - { - return $this->serializer->decode($data, $format, $context); - } - - public function supportsEncoding(string $format, array $context = []): bool - { - return $this->serializer->supportsEncoding($format, $context); - } - - public function supportsDecoding(string $format, array $context = []): bool - { - return $this->serializer->supportsDecoding($format, $context); - } -} diff --git a/src/SerializerManager.php b/src/SerializerManager.php deleted file mode 100644 index 23ef59a5..00000000 --- a/src/SerializerManager.php +++ /dev/null @@ -1,40 +0,0 @@ -serializers->get($format ?? $this->defaultFormat); - } - - public function serialize(mixed $payload, ?string $format = null): string|Stringable - { - return $this->getSerializer($format ?? $this->defaultFormat)->serialize($payload); - } - - public function unserialize( - string|Stringable $payload, - string|object|null $type = null, - ?string $format = null - ): mixed { - return $this->getSerializer($format ?? $this->defaultFormat)->unserialize($payload, $type); - } - - public function normalize(mixed $data, ?string $format = null, array $context = []) - { - return $this->getSerializer($format ?? $this->defaultFormat)->normalize($data, $format, $context); - } -} diff --git a/tests/app/NestedObjects/City.php b/tests/app/NestedObjects/City.php index 0e0c2884..e70ddb2a 100644 --- a/tests/app/NestedObjects/City.php +++ b/tests/app/NestedObjects/City.php @@ -15,6 +15,9 @@ public function __construct( ) { } + /** + * @return array + */ public function jsonSerialize(): array { return [ diff --git a/tests/app/Object/Author.php b/tests/app/Objects/Author.php similarity index 85% rename from tests/app/Object/Author.php rename to tests/app/Objects/Author.php index 4de0b717..7ce4b87f 100644 --- a/tests/app/Object/Author.php +++ b/tests/app/Objects/Author.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WayOfDev\App\Object; +namespace WayOfDev\App\Objects; use Ramsey\Uuid\UuidInterface; diff --git a/tests/app/Object/Post.php b/tests/app/Objects/Post.php similarity index 86% rename from tests/app/Object/Post.php rename to tests/app/Objects/Post.php index 65b6ad62..c10efedc 100644 --- a/tests/app/Object/Post.php +++ b/tests/app/Objects/Post.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WayOfDev\App\Object; +namespace WayOfDev\App\Objects; class Post { diff --git a/tests/app/Object/Product.php b/tests/app/Objects/Product.php similarity index 93% rename from tests/app/Object/Product.php rename to tests/app/Objects/Product.php index 0c5f7efe..6807a610 100644 --- a/tests/app/Object/Product.php +++ b/tests/app/Objects/Product.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WayOfDev\App\Object; +namespace WayOfDev\App\Objects; use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\SerializedName; diff --git a/tests/app/Object/User.php b/tests/app/Objects/User.php similarity index 85% rename from tests/app/Object/User.php rename to tests/app/Objects/User.php index e73aa5bb..e779e0b6 100644 --- a/tests/app/Object/User.php +++ b/tests/app/Objects/User.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace WayOfDev\App\Object; +namespace WayOfDev\App\Objects; use DateTimeInterface; diff --git a/tests/app/Item.php b/tests/app/Response/Item.php similarity index 71% rename from tests/app/Item.php rename to tests/app/Response/Item.php index c41bbd6d..02927b4c 100644 --- a/tests/app/Item.php +++ b/tests/app/Response/Item.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace WayOfDev\App; +namespace WayOfDev\App\Response; +use DateTimeImmutable; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; use Symfony\Component\Serializer\Annotation\Groups; @@ -22,9 +23,13 @@ final class Item #[Groups(['private'])] private string $onlyForAdmin = 'secret'; - public function __construct() + #[Groups(['default', 'private'])] + private ?DateTimeImmutable $dateTime; + + public function __construct(?DateTimeImmutable $dateTime = null) { $this->id = Uuid::fromString('0cd74c72-8920-4e4e-86c3-19fdd5103514'); + $this->dateTime = $dateTime; } public function id(): UuidInterface @@ -42,6 +47,11 @@ public function value(): int return $this->value; } + public function dateTime(): ?DateTimeImmutable + { + return $this->dateTime; + } + public function onlyForAdmin(): string { return $this->onlyForAdmin; diff --git a/tests/app/Response.php b/tests/app/Response/Response.php similarity index 60% rename from tests/app/Response.php rename to tests/app/Response/Response.php index 1301d78e..9e66aea3 100644 --- a/tests/app/Response.php +++ b/tests/app/Response/Response.php @@ -2,13 +2,22 @@ declare(strict_types=1); -namespace WayOfDev\App; +namespace WayOfDev\App\Response; use ArrayIterator; use Webmozart\Assert\Assert; +/** + * @template TKey of array-key + * @template TValue + * + * @extends ArrayIterator + */ final class Response extends ArrayIterator { + /** + * @param array $items + */ private function __construct(array $items) { Assert::allIsInstanceOf($items, Item::class); @@ -17,6 +26,11 @@ private function __construct(array $items) parent::__construct($items); } + /** + * @param array $items + * + * @return self + */ public static function create(array $items): self { return new self($items); diff --git a/tests/app/array.php b/tests/app/Response/array.php similarity index 60% rename from tests/app/array.php rename to tests/app/Response/array.php index 2980e270..e00e6588 100644 --- a/tests/app/array.php +++ b/tests/app/Response/array.php @@ -6,10 +6,10 @@ 'random_object' => [ 'person' => [ 'first_name' => 'John Doe', - 'last_name' => 'Zhmyshenko', + 'last_name' => 'Deer', 'birthdate' => '01.01.1976', - 'birth_place' => 'Chuguev', - 'nationality' => 'ukrainian', + 'birth_place' => 'SomePlace', + 'nationality' => 'alien', ], ], ]; diff --git a/tests/src/Functional/Bridge/Laravel/Facades/ManagerTest.php b/tests/src/Functional/Bridge/Laravel/Facades/ManagerTest.php new file mode 100644 index 00000000..f024324f --- /dev/null +++ b/tests/src/Functional/Bridge/Laravel/Facades/ManagerTest.php @@ -0,0 +1,22 @@ +create(Response::create([new Item()])); self::assertEquals(200, $response->getStatusCode()); - self::assertEquals('[{"id":"0cd74c72-8920-4e4e-86c3-19fdd5103514","key":"magic_number","value":12}]', $response->getContent()); + self::assertEquals( + '[{"id":"0cd74c72-8920-4e4e-86c3-19fdd5103514","key":"magic_number","value":12,"dateTime":null}]', + $response->getContent() + ); } #[Test] public function it_creates_response_from_array(): void { $responseFactory = new ResponseFactory(app(SerializerManager::class)); - $response = $responseFactory->fromArray(require __DIR__ . '/../../app/array.php'); + $response = $responseFactory->fromArray(require __DIR__ . '/../../../../../app/Response/array.php'); self::assertEquals(200, $response->getStatusCode()); self::assertEquals( - '{"random_object":{"person":{"first_name":"John Doe","last_name":"Zhmyshenko","birthdate":"01.01.1976","birth_place":"Chuguev","nationality":"ukrainian"}}}', + '{"random_object":{"person":{"first_name":"John Doe","last_name":"Deer","birthdate":"01.01.1976","birth_place":"SomePlace","nationality":"alien"}}}', $response->getContent() ); } @@ -61,7 +69,10 @@ public function it_sets_non_default_status_code(): void $response = $responseFactory->create(new Item()); self::assertEquals(404, $response->getStatusCode()); - self::assertEquals('{"id":"0cd74c72-8920-4e4e-86c3-19fdd5103514","key":"magic_number","value":12}', $response->getContent()); + self::assertEquals( + '{"id":"0cd74c72-8920-4e4e-86c3-19fdd5103514","key":"magic_number","value":12,"dateTime":null}', + $response->getContent() + ); } #[Test] @@ -70,9 +81,17 @@ public function it_uses_given_context(): void $responseFactory = new ResponseFactory(app(SerializerManager::class)); $responseFactory->withContext(['groups' => 'private']); - $response = $responseFactory->create(new Item()); + $dateTime = new DateTimeImmutable(); + + $response = $responseFactory->create(new Item($dateTime)); + + $formattedDateTime = $dateTime->format(DateTimeInterface::ATOM); + $expectedResponse = sprintf( + '{"id":"0cd74c72-8920-4e4e-86c3-19fdd5103514","onlyForAdmin":"secret","dateTime":"%s"}', + $formattedDateTime + ); self::assertEquals(200, $response->getStatusCode()); - self::assertEquals('{"id":"0cd74c72-8920-4e4e-86c3-19fdd5103514","onlyForAdmin":"secret"}', $response->getContent()); + self::assertEquals($expectedResponse, $response->getContent()); } } diff --git a/tests/src/Functional/Bridge/Laravel/Providers/SerializerServiceProviderTest.php b/tests/src/Functional/Bridge/Laravel/Providers/SerializerServiceProviderTest.php index cc3ac2f7..4863816d 100644 --- a/tests/src/Functional/Bridge/Laravel/Providers/SerializerServiceProviderTest.php +++ b/tests/src/Functional/Bridge/Laravel/Providers/SerializerServiceProviderTest.php @@ -5,14 +5,17 @@ namespace WayOfDev\Tests\Functional\Bridge\Laravel\Providers; use PHPUnit\Framework\Attributes\Test; -use Symfony\Component\Serializer\Serializer; -use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\Serializer\Encoder\EncoderInterface; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; +use Symfony\Component\Serializer\Serializer as SymfonySerializer; +use Symfony\Component\Serializer\SerializerInterface as SymfonySerializerInterface; use WayOfDev\Serializer\Config; use WayOfDev\Serializer\Contracts\ConfigRepository; -use WayOfDev\Serializer\Contracts\EncodersRegistryInterface; -use WayOfDev\Serializer\Contracts\NormalizersRegistryInterface; -use WayOfDev\Serializer\EncodersRegistry; -use WayOfDev\Serializer\NormalizersRegistry; +use WayOfDev\Serializer\Contracts\EncoderRegistryInterface; +use WayOfDev\Serializer\Contracts\NormalizerRegistryInterface; +use WayOfDev\Serializer\EncoderRegistry; +use WayOfDev\Serializer\NormalizerRegistry; use WayOfDev\Tests\Functional\TestCase; final class SerializerServiceProviderTest extends TestCase @@ -20,28 +23,45 @@ final class SerializerServiceProviderTest extends TestCase #[Test] public function it_registers_config(): void { - self::assertTrue($this->app->bound(ConfigRepository::class)); + self::assertTrue($this->app?->bound(ConfigRepository::class)); self::assertInstanceOf(Config::class, $this->app->make(ConfigRepository::class)); } + #[Test] + public function it_registers_loader(): void + { + self::assertTrue($this->app?->bound(LoaderInterface::class)); + self::assertInstanceOf(AttributeLoader::class, $this->app->make(LoaderInterface::class)); + } + #[Test] public function it_registers_normalizers_registry(): void { - self::assertTrue($this->app->bound(NormalizersRegistryInterface::class)); - self::assertInstanceOf(NormalizersRegistry::class, $this->app->make(NormalizersRegistryInterface::class)); + self::assertTrue($this->app?->bound(NormalizerRegistryInterface::class)); + self::assertInstanceOf(NormalizerRegistry::class, $this->app->make(NormalizerRegistryInterface::class)); + + $normalizersRegistry = $this->app->make(NormalizerRegistryInterface::class); + $normalizers = $normalizersRegistry->all(); + self::assertNotEmpty($normalizers); } #[Test] public function it_registers_encoders_registry(): void { - self::assertTrue($this->app->bound(EncodersRegistryInterface::class)); - self::assertInstanceOf(EncodersRegistry::class, $this->app->make(EncodersRegistryInterface::class)); + self::assertTrue($this->app?->bound(EncoderRegistryInterface::class)); + self::assertInstanceOf(EncoderRegistry::class, $this->app->make(EncoderRegistryInterface::class)); + + $encodersRegistry = $this->app->make(EncoderRegistryInterface::class); + $encoders = $encodersRegistry->all(); + self::assertNotEmpty($encoders); + self::assertContainsOnlyInstancesOf(EncoderInterface::class, $encoders); } #[Test] - public function it_registers_serializer(): void + public function it_registers_symfony_serializer_interface(): void { - self::assertTrue($this->app->bound(SerializerInterface::class)); - self::assertInstanceOf(Serializer::class, $this->app->make(SerializerInterface::class)); + self::assertTrue($this->app?->bound(SymfonySerializerInterface::class)); + self::assertTrue($this->app->bound(SymfonySerializer::class)); + self::assertInstanceOf(SymfonySerializer::class, $this->app->make(SymfonySerializerInterface::class)); } } diff --git a/tests/src/Functional/ConfigTest.php b/tests/src/Functional/ConfigTest.php new file mode 100644 index 00000000..bcae47fd --- /dev/null +++ b/tests/src/Functional/ConfigTest.php @@ -0,0 +1,66 @@ + 'json', + 'debug' => true, + 'normalizerRegistrationStrategy' => DefaultNormalizerRegistrationStrategy::class, + 'encoderRegistrationStrategy' => DefaultEncoderRegistrationStrategy::class, + 'metadataLoader' => AttributeLoader::class, + ]; + + $config = Config::fromArray($configArray); + + self::assertSame('json', $config->defaultSerializer()); + self::assertSame(DefaultNormalizerRegistrationStrategy::class, $config->normalizerRegistrationStrategy()); + self::assertSame(DefaultEncoderRegistrationStrategy::class, $config->encoderRegistrationStrategy()); + self::assertInstanceOf(AttributeLoader::class, $config->metadataLoader()); + } + + #[Test] + public function it_fails_to_create_config_due_to_missing_attributes(): void + { + $this->expectException(MissingRequiredAttributes::class); + + $configArray = [ + 'default' => 'json', + 'debug' => true, + 'encoderRegistrationStrategy' => DefaultEncoderRegistrationStrategy::class, + 'metadataLoader' => AttributeLoader::class, + ]; + + // @phpstan-ignore-next-line + Config::fromArray($configArray); + } + + #[Test] + public function it_creates_config_with_default_metadata_loader(): void + { + $configArray = [ + 'default' => 'json', + 'debug' => true, + 'normalizerRegistrationStrategy' => DefaultNormalizerRegistrationStrategy::class, + 'encoderRegistrationStrategy' => DefaultEncoderRegistrationStrategy::class, + 'metadataLoader' => null, + ]; + + $config = Config::fromArray($configArray); + + self::assertInstanceOf(AttributeLoader::class, $config->metadataLoader()); + } +} diff --git a/tests/src/Functional/EncoderRegistryTest.php b/tests/src/Functional/EncoderRegistryTest.php new file mode 100644 index 00000000..459e76cd --- /dev/null +++ b/tests/src/Functional/EncoderRegistryTest.php @@ -0,0 +1,82 @@ +app?->make(EncoderRegistrationStrategy::class) + ); + + self::assertCount(4, $registry->all()); + + self::assertTrue($registry->has(JsonEncoder::class)); + self::assertTrue($registry->has(CsvEncoder::class)); + self::assertTrue($registry->has(XmlEncoder::class)); + self::assertTrue($registry->has(YamlEncoder::class)); + } + + #[Test] + public function it_creates_registry_with_user_defined_encoders(): void + { + $strategy = new class() implements EncoderRegistrationStrategy { + public function encoders(): iterable + { + yield ['encoder' => new JsonEncoder()]; + yield ['encoder' => new CsvEncoder()]; + } + }; + + $registry = new EncoderRegistry($strategy); + + self::assertCount(2, $registry->all()); + + self::assertTrue($registry->has(JsonEncoder::class)); + self::assertTrue($registry->has(CsvEncoder::class)); + self::assertFalse($registry->has(XmlEncoder::class)); + self::assertFalse($registry->has(YamlEncoder::class)); + } + + /** + * @throws Exception + */ + #[Test] + public function it_registers_additional_encoders(): void + { + $registry = new EncoderRegistry( + $this->app?->make(EncoderRegistrationStrategy::class) + ); + + $encoderFirst = $this->createMock(EncoderInterface::class); + + self::assertCount(4, $registry->all()); + $registry->register($encoderFirst); + self::assertCount(5, $registry->all()); + self::assertTrue($registry->has($encoderFirst::class)); + + $registry->register($encoderFirst); + self::assertCount(5, $registry->all()); + self::assertTrue($registry->has($encoderFirst::class)); + + $encoderSecond = $this->createMock(DecoderInterface::class); + $registry->register($encoderSecond); + self::assertCount(6, $registry->all()); + self::assertTrue($registry->has($encoderSecond::class)); + } +} diff --git a/tests/src/Functional/EncodersRegistryTest.php b/tests/src/Functional/EncodersRegistryTest.php deleted file mode 100644 index 43f2c3df..00000000 --- a/tests/src/Functional/EncodersRegistryTest.php +++ /dev/null @@ -1,85 +0,0 @@ -all()); - - self::assertTrue($registry->has(JsonEncoder::class)); - self::assertTrue($registry->has(CsvEncoder::class)); - self::assertTrue($registry->has(XmlEncoder::class)); - self::assertTrue($registry->has(YamlEncoder::class)); - } - - #[Test] - public function construct_with_encoders(): void - { - $registry = new EncodersRegistry([new JsonEncoder(), new CsvEncoder()]); - - self::assertCount(2, $registry->all()); - - self::assertTrue($registry->has(JsonEncoder::class)); - self::assertTrue($registry->has(CsvEncoder::class)); - self::assertFalse($registry->has(XmlEncoder::class)); - self::assertFalse($registry->has(YamlEncoder::class)); - } - - #[Test] - public function register(): void - { - $registry = new EncodersRegistry(); - - $encoder = $this->createMock(EncoderInterface::class); - - self::assertCount(4, $registry->all()); - $registry->register($encoder); - self::assertCount(5, $registry->all()); - self::assertTrue($registry->has($encoder::class)); - - $registry->register($encoder); - self::assertCount(5, $registry->all()); - self::assertTrue($registry->has($encoder::class)); - } - - #[Test] - public function all(): void - { - $json = new JsonEncoder(); - $csv = new CsvEncoder(); - - $registry = new EncodersRegistry([$json, $csv]); - self::assertSame([$json, $csv], $registry->all()); - } - - /** - * @throws Exception - */ - #[Test] - public function has(): void - { - $encoder = $this->createMock(EncoderInterface::class); - - $registry = new EncodersRegistry(); - self::assertFalse($registry->has($encoder::class)); - - $registry->register($encoder); - self::assertTrue($registry->has($encoder::class)); - } -} diff --git a/tests/src/Functional/Manager/SerializerManagerTest.php b/tests/src/Functional/Manager/SerializerManagerTest.php new file mode 100644 index 00000000..1c83bef0 --- /dev/null +++ b/tests/src/Functional/Manager/SerializerManagerTest.php @@ -0,0 +1,21 @@ +app?->make(SerializerManager::class); + + self::assertSame('symfony-json', $manager->format()); + } +} diff --git a/tests/src/Functional/Manager/SerializerRegistryTest.php b/tests/src/Functional/Manager/SerializerRegistryTest.php new file mode 100644 index 00000000..443a9501 --- /dev/null +++ b/tests/src/Functional/Manager/SerializerRegistryTest.php @@ -0,0 +1,76 @@ +createMock(SerializerInterface::class); + $registry = new SerializerRegistry(['default' => $serializerMock]); + + self::assertCount(1, $registry->all()); + self::assertTrue($registry->has('default')); + self::assertSame($serializerMock, $registry->get('default')); + } + + /** + * @throws Exception + */ + #[Test] + public function it_registers_additional_serializers(): void + { + $serializerMock1 = $this->createMock(SerializerInterface::class); + $serializerMock2 = $this->createMock(SerializerInterface::class); + + $registry = new SerializerRegistry(); + $registry->register('first', $serializerMock1); + $registry->register('second', $serializerMock2); + + self::assertCount(2, $registry->all()); + self::assertTrue($registry->has('first')); + self::assertTrue($registry->has('second')); + self::assertSame($serializerMock1, $registry->get('first')); + self::assertSame($serializerMock2, $registry->get('second')); + } + + #[Test] + public function it_throws_exception_for_missing_serializer(): void + { + $this->expectException(SerializerNotFound::class); + + $registry = new SerializerRegistry(); + $registry->get('nonexistent'); + } + + /** + * @throws Exception + */ + #[Test] + public function it_lists_all_serializers(): void + { + $serializerMock1 = $this->createMock(SerializerInterface::class); + $serializerMock2 = $this->createMock(SerializerInterface::class); + + $registry = new SerializerRegistry(['first' => $serializerMock1, 'second' => $serializerMock2]); + + $serializers = $registry->all(); + + self::assertCount(2, $serializers); + self::assertContains($serializerMock1, $serializers); + self::assertContains($serializerMock2, $serializers); + } +} diff --git a/tests/src/Functional/Manager/SerializerTest.php b/tests/src/Functional/Manager/SerializerTest.php new file mode 100644 index 00000000..910037e0 --- /dev/null +++ b/tests/src/Functional/Manager/SerializerTest.php @@ -0,0 +1,29 @@ +app?->make(SymfonySerializerInterface::class); + + $serializer = new Serializer($symfonySerializer, 'json'); + + $json = $serializer->serialize(new class() { + public string $name = 'some'; + }); + + self::assertSame('{"name":"some"}', $json); + } +} diff --git a/tests/src/Functional/NormalizersRegistryTest.php b/tests/src/Functional/NormalizerRegistryTest.php similarity index 68% rename from tests/src/Functional/NormalizersRegistryTest.php rename to tests/src/Functional/NormalizerRegistryTest.php index 9192e444..10a368ce 100644 --- a/tests/src/Functional/NormalizersRegistryTest.php +++ b/tests/src/Functional/NormalizerRegistryTest.php @@ -6,20 +6,21 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\Exception; -use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; use Symfony\Component\Serializer\Normalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use WayOfDev\Serializer\Contracts\NormalizerRegistrationStrategy; +use WayOfDev\Serializer\NormalizerRegistry; use WayOfDev\Serializer\Normalizers\RamseyUuidNormalizer; -use WayOfDev\Serializer\NormalizersRegistry; use function sprintf; -final class NormalizersRegistryTest extends TestCase +final class NormalizerRegistryTest extends TestCase { /** * Asserts that an array contains an instance of a class. * - * @param string $className The class name - * @param array $array The array + * @param array $array */ public static function assertContainsInstanceOf(string $className, array $array): void { @@ -34,16 +35,10 @@ public static function assertContainsInstanceOf(string $className, array $array) self::fail(sprintf('Failed asserting that the array contains an instance of %s.', $className)); } - /** - * @throws Exception - */ #[Test] public function construct_with_default_normalizers(): void { - $registry = new NormalizersRegistry( - $this->createMock(LoaderInterface::class), - true, - ); + $registry = new NormalizerRegistry(app(NormalizerRegistrationStrategy::class)); self::assertCount(15, $registry->all()); @@ -68,40 +63,37 @@ public function construct_with_default_normalizers(): void * @throws Exception */ #[Test] - public function register(): void + public function it_registers_additional_normalizers(): void { - $registry = new NormalizersRegistry( - $this->createMock(LoaderInterface::class), - true + $registry = new NormalizerRegistry( + $this->createMock(NormalizerRegistrationStrategy::class), ); - $normalizer = $this->createMock(Normalizer\NormalizerInterface::class); - $normalizer2 = $this->createMock(Normalizer\DenormalizerInterface::class); + $normalizer = $this->createMock(NormalizerInterface::class); + $normalizer2 = $this->createMock(DenormalizerInterface::class); $registry->register($normalizer, 2); - self::assertCount(16, $registry->all()); + self::assertCount(1, $registry->all()); self::assertTrue($registry->has($normalizer::class)); $registry->register($normalizer2, 1); - self::assertCount(17, $registry->all()); + self::assertCount(2, $registry->all()); self::assertTrue($registry->has($normalizer2::class)); self::assertSame($normalizer2, $registry->all()[0]); self::assertSame($normalizer, $registry->all()[1]); } - /** - * @throws Exception - */ #[Test] - public function all(): void + public function it_gets_all_registered_normalizers(): void { - $registry = new NormalizersRegistry( - $this->createMock(LoaderInterface::class), - true, - [new Normalizer\UnwrappingDenormalizer(), new Normalizer\ObjectNormalizer()] + $registry = new NormalizerRegistry( + app(NormalizerRegistrationStrategy::class), ); + $registry->register(new Normalizer\UnwrappingDenormalizer(), 1); + $registry->register(new Normalizer\ObjectNormalizer(), 1); + $allNormalizers = $registry->all(); self::assertContainsInstanceOf(Normalizer\UnwrappingDenormalizer::class, $allNormalizers); @@ -112,14 +104,14 @@ public function all(): void * @throws Exception */ #[Test] - public function has(): void + public function it_has_normalizer_in_registry(): void { - $normalizer = $this->createMock(Normalizer\NormalizerInterface::class); + $normalizer = $this->createMock(NormalizerInterface::class); - $registry = new NormalizersRegistry( - $this->createMock(LoaderInterface::class), - true + $registry = new NormalizerRegistry( + app(NormalizerRegistrationStrategy::class), ); + self::assertFalse($registry->has($normalizer::class)); $registry->register($normalizer); diff --git a/tests/src/Functional/Normalizers/RamseyUuidNormalizerTest.php b/tests/src/Functional/Normalizers/RamseyUuidNormalizerTest.php index de8787bb..731d3130 100644 --- a/tests/src/Functional/Normalizers/RamseyUuidNormalizerTest.php +++ b/tests/src/Functional/Normalizers/RamseyUuidNormalizerTest.php @@ -6,27 +6,32 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; use Ramsey\Uuid\Uuid; use Traversable; -use WayOfDev\App\Object\Author; -use WayOfDev\Serializer\SerializerManager; +use WayOfDev\App\Objects\Author; +use WayOfDev\Serializer\Manager\SerializerManager; use WayOfDev\Tests\Functional\TestCase; use function preg_replace; final class RamseyUuidNormalizerTest extends TestCase { + /** + * @return Traversable + */ public static function serializeDataProvider(): Traversable { yield [ '{"uuid":"1d96a152-9838-43a0-a189-159befc9e38f","name":"some"}', new Author(Uuid::fromString('1d96a152-9838-43a0-a189-159befc9e38f'), 'some'), - 'json', + 'symfony-json', ]; yield [ 'uuid,name1d96a152-9838-43a0-a189-159befc9e38f,some', new Author(Uuid::fromString('1d96a152-9838-43a0-a189-159befc9e38f'), 'some'), - 'csv', + 'symfony-csv', ]; yield [ '{uuid:1d96a152-9838-43a0-a189-159befc9e38f,name:some}', @@ -35,24 +40,32 @@ public static function serializeDataProvider(): Traversable ]; } + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ #[DataProvider('serializeDataProvider')] #[Test] - public function serialize(string $expected, mixed $payload, string $format): void + public function it_serializes_using_serializer_manager(string $expected, mixed $payload, string $format): void { - $manager = $this->app->get(SerializerManager::class); + $manager = $this->app?->get(SerializerManager::class); self::assertSame($expected, preg_replace('/\s+/', '', $manager->serialize($payload, $format))); } + /** + * @throws NotFoundExceptionInterface + * @throws ContainerExceptionInterface + */ #[Test] - public function unserialize(): void + public function it_deserializes_using_serialize_manager(): void { - $manager = $this->app->get(SerializerManager::class); + $manager = $this->app?->get(SerializerManager::class); - $result = $manager->unserialize( + $result = $manager->deserialize( '{"uuid":"1d96a152-9838-43a0-a189-159befc9e38f","name":"some"}', Author::class, - 'json' + 'symfony-json' ); self::assertInstanceOf(Author::class, $result); diff --git a/tests/src/Functional/SerializerManagerTest.php b/tests/src/Functional/SerializerManagerTest.php deleted file mode 100644 index c4c45fb1..00000000 --- a/tests/src/Functional/SerializerManagerTest.php +++ /dev/null @@ -1,97 +0,0 @@ -serializer = $this->app->make(SerializerManager::class); - } - - #[Test] - public function get_serializer(): void - { - self::assertInstanceOf( - Serializer::class, - $this->serializer->getSerializer('json') - ); - - self::assertInstanceOf( - Serializer::class, - $this->serializer->getSerializer('xml') - ); - - self::assertInstanceOf( - Serializer::class, - $this->serializer->getSerializer('csv') - ); - - $this->expectException(SerializerNotFoundException::class); - $this->serializer->getSerializer('bad'); - } - - #[DataProvider('serializeDataProvider')] - #[Test] - public function serialize(mixed $payload, string $expected, ?string $format = null): void - { - self::assertSame($expected, $this->serializer->serialize($payload, $format)); - } - - #[Test] - public function bad_serializer(): void - { - $this->expectException(SerializerNotFoundException::class); - $this->serializer->serialize('payload', 'bad'); - - $this->expectException(SerializerNotFoundException::class); - $this->serializer->unserialize('payload', 'bad'); - } - - #[DataProvider('unserializeDataProvider')] - #[Test] - public function unserialize(string|Stringable $payload, mixed $expected, ?string $format = null): void - { - self::assertSame($expected, $this->serializer->unserialize($payload, format: $format)); - } -} diff --git a/tests/src/Functional/SerializerTest.php b/tests/src/Functional/SerializerTest.php deleted file mode 100644 index dbd5d186..00000000 --- a/tests/src/Functional/SerializerTest.php +++ /dev/null @@ -1,184 +0,0 @@ -1some15', - new Post(1, 'some', true, 5), - 'xml', - ]; - yield [ - '{"id":1,"title":"Someproduct","price":100.0,"active":false,"product_views":5}', - new Product(1, 'Some product', 100, false, 5), - 'json', - ]; - yield [ - '{"name":"USA","cities":[{"name":"Chicago","timezone":"America\/Chicago"},{"name":"NewYork","timezone":"America\/New_York"}]}', - new Country('USA', [ - new City('Chicago', new DateTimeZone('America/Chicago')), - new City('New York', new DateTimeZone('America/New_York')), - ]), - 'json', - ]; - yield [ - 'name,cities.0.name,cities.0.timezone,cities.1.name,cities.1.timezoneUSA,Chicago,America/Chicago,"NewYork",America/New_York', - new Country('USA', [ - new City('Chicago', new DateTimeZone('America/Chicago')), - new City('New York', new DateTimeZone('America/New_York')), - ]), - 'csv', - ]; - yield [ - 'USAChicagoAmerica/ChicagoNewYorkAmerica/New_York', - new Country('USA', [ - new City('Chicago', new DateTimeZone('America/Chicago')), - new City('New York', new DateTimeZone('America/New_York')), - ]), - 'xml', - ]; - yield [ - '{"id":3,"registeredAt":"2023-06-05T22:12:55+00:00"}', - new User(3, new DateTimeImmutable('2023-06-05T22:12:55+00:00')), - 'json', - ]; - yield [ - 'id,registeredAt3,2023-06-05T22:12:55+00:00', - new User(3, new DateTimeImmutable('2023-06-05T22:12:55+00:00')), - 'csv', - ]; - yield [ - '32023-06-05T22:12:55+00:00', - new User(3, new DateTimeImmutable('2023-06-05T22:12:55+00:00')), - 'xml', - ]; - } - - #[DataProvider('serializeDataProvider')] - #[Test] - public function serialize(string $expected, mixed $payload, string $format): void - { - $manager = $this->app->get(SerializerManager::class); - - self::assertSame($expected, preg_replace('/\s+/', '', $manager->serialize($payload, $format))); - } - - #[Test] - public function unserialize(): void - { - $manager = $this->app->get(SerializerManager::class); - - $result = $manager->unserialize('{"id":1,"text":"some","active":false,"views":3}', Post::class, 'json'); - self::assertInstanceOf(Post::class, $result); - self::assertSame(1, $result->id); - self::assertSame('some', $result->text); - self::assertFalse($result->active); - - $result = $manager->unserialize( - '{"id":1,"text":"some","active":false,"views":3}', - new Post(2, '', true, 1), - 'json' - ); - self::assertInstanceOf(Post::class, $result); - self::assertSame(1, $result->id); - self::assertSame('some', $result->text); - self::assertFalse($result->active); - } - - #[Test] - public function unserialize_with_attributes(): void - { - $manager = $this->app->get(SerializerManager::class); - - $result = $manager->unserialize( - '{"id":1,"title":"Some product","price":100,"active":false,"product_views":5}', - Product::class, - 'json' - ); - self::assertInstanceOf(Product::class, $result); - self::assertSame(1, $result->id); - self::assertSame('Some product', $result->title); - self::assertSame(100.0, $result->price); - self::assertFalse($result->active); - self::assertSame(5, $result->views); - } - - #[Test] - public function unserialize_nested_objects(): void - { - $manager = $this->app->get(SerializerManager::class); - - $result = $manager->unserialize( - '{"name":"USA","cities":[{"name":"Chicago","timezone":"America\/Chicago"},{"name":"NewYork","timezone":"America\/New_York"}]}', - Country::class, - 'json' - ); - - self::assertInstanceOf(Country::class, $result); - self::assertSame('USA', $result->name); - self::assertSame('Chicago', $result->cities[0]->name); - self::assertSame('NewYork', $result->cities[1]->name); - } - - #[Test] - public function unserialize_date_time_interface(): void - { - $manager = $this->app->get(SerializerManager::class); - - $result = $manager->unserialize( - '{"id":3,"registeredAt":"2023-06-05T22:12:55+00:00"}', - User::class, - 'json' - ); - - self::assertInstanceOf(User::class, $result); - self::assertSame(3, $result->id); - self::assertInstanceOf(DateTimeImmutable::class, $result->registeredAt); - self::assertSame('2023-06-05T22:12:55+00:00', $result->registeredAt->format('c')); - } - - #[Test] - public function group_normalize(): void - { - // dd($this->app->make(SerializerManager::class)); - - $manager = $this->app->get(SerializerManager::class); - /** @var Serializer $serializer */ - $serializer = $manager->getSerializer('json'); - - $product = new Product(1, 'Some product', 100, false, 5); - - self::assertSame( - ['id' => 1, 'title' => 'Some product'], - $serializer->normalize($product, null, ['groups' => 'group1']) - ); - self::assertSame( - ['price' => 100.0, 'active' => false], - $serializer->normalize($product, null, ['groups' => 'group2']) - ); - self::assertSame(['product_views' => 5], $serializer->normalize($product, null, ['groups' => 'group3'])); - } -} diff --git a/tests/src/Functional/TestCase.php b/tests/src/Functional/TestCase.php index cdae223e..2fc577a9 100644 --- a/tests/src/Functional/TestCase.php +++ b/tests/src/Functional/TestCase.php @@ -12,13 +12,6 @@ abstract class TestCase extends Orchestra protected function setUp(): void { parent::setUp(); - - config()->set('serializer.serializers', [ - 'json' => true, - 'csv' => true, - 'xml' => true, - 'yaml' => true, - ]); } protected function getPackageProviders($app): array diff --git a/tests/src/Unit/Normalizers/RamseyUuidNormalizerTest.php b/tests/src/Unit/Normalizers/RamseyUuidNormalizerTest.php new file mode 100644 index 00000000..9a2ffe23 --- /dev/null +++ b/tests/src/Unit/Normalizers/RamseyUuidNormalizerTest.php @@ -0,0 +1,80 @@ +normalizer = new RamseyUuidNormalizer(); + } + + #[Test] + public function it_normalizes(): void + { + $uuid = Uuid::uuid4(); + $normalized = $this->normalizer->normalize($uuid); + + self::assertSame($uuid->toString(), $normalized); + } + + #[Test] + public function it_supports_normalization(): void + { + $uuid = Uuid::uuid4(); + $notUuid = 'not-a-uuid'; + + self::assertTrue($this->normalizer->supportsNormalization($uuid)); + self::assertFalse($this->normalizer->supportsNormalization($notUuid)); + } + + #[Test] + public function it_denormalizes(): void + { + $uuidString = Uuid::uuid4()->toString(); + $denormalized = $this->normalizer->denormalize($uuidString, UuidInterface::class); + + self::assertSame($uuidString, $denormalized->toString()); + } + + #[Test] + public function denormalize_throws_exception_for_invalid_uuid(): void + { + $this->expectException(NotNormalizableValueException::class); + + $invalidUuidString = 'invalid-uuid'; + $this->normalizer->denormalize($invalidUuidString, UuidInterface::class); + } + + #[Test] + public function it_supports_denormalization(): void + { + $validUuidString = Uuid::uuid4()->toString(); + $invalidUuidString = 'invalid-uuid'; + $notAString = 12345; + + self::assertTrue($this->normalizer->supportsDenormalization($validUuidString, UuidInterface::class)); + self::assertFalse($this->normalizer->supportsDenormalization($invalidUuidString, UuidInterface::class)); + self::assertFalse($this->normalizer->supportsDenormalization($notAString, UuidInterface::class)); + } + + #[Test] + public function it_gets_supported_types(): void + { + $expected = [UuidInterface::class => true]; + $actual = $this->normalizer->getSupportedTypes(null); + + self::assertSame($expected, $actual); + } +}