diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a913ce0..59a25ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,16 +33,26 @@ jobs: - '4.*' - '5.*' - '6.*' + - '7.*' dependency: + - 'lowest' - 'highest' - include: - - { php: '7.0', symfony: '3.*', dependency: 'lowest' } exclude: + - { php: '7.0', symfony: '4.*' } + - { php: '7.0', symfony: '5.*' } - { php: '7.1', symfony: '5.*' } + - { php: '7.0', symfony: '6.*' } - { php: '7.1', symfony: '6.*' } - { php: '7.2', symfony: '6.*' } - { php: '7.3', symfony: '6.*' } - { php: '7.4', symfony: '6.*' } + - { php: '7.0', symfony: '7.*' } + - { php: '7.1', symfony: '7.*' } + - { php: '7.2', symfony: '7.*' } + - { php: '7.3', symfony: '7.*' } + - { php: '7.4', symfony: '7.*' } + - { php: '8.0', symfony: '7.*' } + - { php: '8.1', symfony: '7.*' } steps: - name: Checkout diff --git a/CHANGELOG.md b/CHANGELOG.md index 715f4b3..b4e243b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.4.0 +### Added +- Support for Symfony 7.4. + +### Changed +- `Configuration::getConfigTreeBuilder()` declares the `TreeBuilder` return type, which Symfony 7 requires. +- `PayseraNormalizationExtension` extends `Symfony\Component\DependencyInjection\Extension\Extension` instead of + `Symfony\Component\HttpKernel\DependencyInjection\Extension` (internal since Symfony 7.1, deprecated in 8.1), and + `load()` documents its `void` return. Together with the return type above, this removes the deprecation notices that + Symfony 5.4 and later report for the bundle's own classes when their debug class loader is on. +- Breaking for subclasses of these two classes: an override of `getConfigTreeBuilder()` must declare `: TreeBuilder`, and a + subclass of the extension no longer inherits the class-cache methods of Symfony's HttpKernel `Extension` + (`addAnnotatedClassesToCompile()` and its getter, deprecated since Symfony 7.1, and on Symfony 3.4 also + `addClassesToCompile()` and its getter): a call to one of them fails and an override of the getter is ignored. The + classes are not part of the bundle's public API (see "Semantic versioning" in the README), so this is a minor release. +- The bundle's services are defined in `PayseraNormalizationExtension` instead of `Resources/config/services.xml` and + `Resources/config/services/date_time_normalizer.xml`, which are removed, so Symfony 7.4 reports no "XML configuration + format is deprecated" notice. The service ids, classes, arguments, tags and aliases are unchanged. +- `symfony/config`, `symfony/dependency-injection` and `symfony/http-kernel`, which the bundle's code uses, are required + explicitly. `symfony/framework-bundle`, which only the bundle's tests use, moves to `require-dev`. +- `paysera/lib-normalization` starts at 1.3.1: in earlier releases `DateTimeNormalizer` fails on PHP 8.2 and later, + where `date_get_last_errors()` returns `false`. + ## 1.3.1 ### Added - `void` phpdoc typehint to `PayseraNormalizationBundle::build` method to fix the deprecation message on Symfony 6 diff --git a/composer.json b/composer.json index 406c43b..0f85fbd 100644 --- a/composer.json +++ b/composer.json @@ -16,13 +16,16 @@ }, "require": { "php": "^7.0 || ^8.0", - "paysera/lib-normalization": "^1.2", - "symfony/framework-bundle": "^3.4.26|^4.2.7|^5.4|^6.0", + "paysera/lib-normalization": "^1.3.1", + "symfony/config": "^3.4.26|^4.2.7|^5.4|^6.0|^7.4", + "symfony/dependency-injection": "^3.4.26|^4.2.7|^5.4|^6.0|^7.4", + "symfony/http-kernel": "^3.4.26|^4.2.7|^5.4|^6.0|^7.4", "paysera/lib-dependency-injection": "^1.3.0" }, "require-dev": { - "phpunit/phpunit": "^6.0 || ^9.0", - "symfony/yaml": "^2.7|^3.0|^4.0|^5.0", + "phpunit/phpunit": "^6.0 || ^9.6.33", + "symfony/framework-bundle": "^3.4.26|^4.2.7|^5.4|^6.0|^7.4", + "symfony/yaml": "^2.7|^3.0|^4.0|^5.0|^6.0|^7.4", "mockery/mockery": "^1.2", "yoast/phpunit-polyfills": "^1.0" }, diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 2b7844c..aafc514 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -9,7 +9,7 @@ class Configuration implements ConfigurationInterface { - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('paysera_normalization'); $rootNode = method_exists($treeBuilder, 'getRootNode') diff --git a/src/DependencyInjection/PayseraNormalizationExtension.php b/src/DependencyInjection/PayseraNormalizationExtension.php index 6a25374..4be791b 100644 --- a/src/DependencyInjection/PayseraNormalizationExtension.php +++ b/src/DependencyInjection/PayseraNormalizationExtension.php @@ -3,11 +3,20 @@ namespace Paysera\Bundle\NormalizationBundle\DependencyInjection; +use Paysera\Component\Normalization\CoreDenormalizer; +use Paysera\Component\Normalization\CoreNormalizer; +use Paysera\Component\Normalization\DataFilter; +use Paysera\Component\Normalization\Normalizer\DateTimeImmutableNormalizer; +use Paysera\Component\Normalization\Normalizer\DateTimeNormalizer; +use Paysera\Component\Normalization\NormalizerRegistryInterface; +use Paysera\Component\Normalization\Registry\GroupedNormalizerRegistryProvider; use Paysera\Component\Normalization\TypeAwareInterface; +use Paysera\Component\Normalization\TypeGuesser; +use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Extension\Extension; +use Symfony\Component\DependencyInjection\Reference; class PayseraNormalizationExtension extends Extension { @@ -16,13 +25,16 @@ public static function supportsAutoconfiguration() return method_exists(ContainerBuilder::class, 'registerForAutoconfiguration'); } + /** + * @param array|null> $configs + * @return void + */ public function load(array $configs, ContainerBuilder $container) { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); - $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); - $loader->load('services.xml'); + $this->registerServices($container); if (self::supportsAutoconfiguration()) { $container @@ -34,7 +46,54 @@ public function load(array $configs, ContainerBuilder $container) if (isset($config['register_normalizers']['date_time'])) { $dateTimeFormat = $config['register_normalizers']['date_time']['format']; $container->setParameter('paysera_normalization.date_time_normalizer.format', $dateTimeFormat); - $loader->load('services/date_time_normalizer.xml'); + $this->registerDateTimeNormalizers($container); } } + + private function registerServices(ContainerBuilder $container) + { + $container->setDefinition( + 'paysera_normalization.normalizer_registry_provider', + (new Definition(GroupedNormalizerRegistryProvider::class))->setLazy(true) + ); + $container->setDefinition( + 'paysera_normalization.normalizer_registry', + (new Definition(NormalizerRegistryInterface::class)) + ->setLazy(true) + ->setFactory([ + new Reference('paysera_normalization.normalizer_registry_provider'), + 'getDefaultNormalizerRegistry', + ]) + ); + $container->setDefinition('paysera_normalization.type_guesser', new Definition(TypeGuesser::class)); + $container->setDefinition('paysera_normalization.data_filter', new Definition(DataFilter::class)); + + $container->setDefinition('paysera_normalization.core_normalizer', new Definition(CoreNormalizer::class, [ + new Reference('paysera_normalization.normalizer_registry_provider'), + new Reference('paysera_normalization.type_guesser'), + new Reference('paysera_normalization.data_filter'), + ])); + $container->setAlias(CoreNormalizer::class, new Alias('paysera_normalization.core_normalizer')); + + $container->setDefinition('paysera_normalization.core_denormalizer', new Definition(CoreDenormalizer::class, [ + new Reference('paysera_normalization.normalizer_registry_provider'), + ])); + $container->setAlias(CoreDenormalizer::class, new Alias('paysera_normalization.core_denormalizer')); + } + + private function registerDateTimeNormalizers(ContainerBuilder $container) + { + $format = '%paysera_normalization.date_time_normalizer.format%'; + $container->setDefinition( + 'paysera_normalization.date_time_normalizer', + (new Definition(DateTimeNormalizer::class, [$format])) + ->addTag('paysera_normalization.autoconfigured_normalizer') + ); + $container->setDefinition( + 'paysera_normalization.date_time_immutable_normalizer', + (new Definition(DateTimeImmutableNormalizer::class, [$format])) + ->addTag('paysera_normalization.autoconfigured_normalizer') + ->addTag('paysera_normalization.mixed_type_denormalizer', ['type' => 'DateTimeInterface']) + ); + } } diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml deleted file mode 100644 index ef31d50..0000000 --- a/src/Resources/config/services.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/services/date_time_normalizer.xml b/src/Resources/config/services/date_time_normalizer.xml deleted file mode 100644 index c1f8326..0000000 --- a/src/Resources/config/services/date_time_normalizer.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - %paysera_normalization.date_time_normalizer.format% - - - - - %paysera_normalization.date_time_normalizer.format% - - - - - diff --git a/tests/DependencyInjection/PayseraNormalizationExtensionTest.php b/tests/DependencyInjection/PayseraNormalizationExtensionTest.php new file mode 100644 index 0000000..f30bfab --- /dev/null +++ b/tests/DependencyInjection/PayseraNormalizationExtensionTest.php @@ -0,0 +1,146 @@ + ['date_time' => ['format' => 'U']]]]; + + /** + * @dataProvider loadProvider + * @param array> $configs + * @param array> $expected + */ + public function testLoadRegistersTheServices(array $configs, array $expected) + { + $container = new ContainerBuilder(); + $empty = new ContainerBuilder(); + + (new PayseraNormalizationExtension())->load($configs, $container); + + $this->assertSame(self::normalize($expected), self::normalize([ + 'definitions' => array_diff_key($container->getDefinitions(), $empty->getDefinitions()), + 'aliases' => array_diff_key($container->getAliases(), $empty->getAliases()), + 'parameters' => $container->getParameterBag()->all(), + ])); + } + + /** + * @return array>, array>}> + */ + public static function loadProvider(): array + { + $provider = new Reference('paysera_normalization.normalizer_registry_provider'); + $definitions = [ + 'paysera_normalization.normalizer_registry_provider' => + (new Definition(GroupedNormalizerRegistryProvider::class))->setLazy(true), + 'paysera_normalization.normalizer_registry' => (new Definition(NormalizerRegistryInterface::class)) + ->setLazy(true) + ->setFactory([$provider, 'getDefaultNormalizerRegistry']), + 'paysera_normalization.type_guesser' => new Definition(TypeGuesser::class), + 'paysera_normalization.data_filter' => new Definition(DataFilter::class), + 'paysera_normalization.core_normalizer' => new Definition(CoreNormalizer::class, [ + $provider, + new Reference('paysera_normalization.type_guesser'), + new Reference('paysera_normalization.data_filter'), + ]), + 'paysera_normalization.core_denormalizer' => new Definition(CoreDenormalizer::class, [$provider]), + ]; + $aliases = [ + CoreNormalizer::class => new Alias('paysera_normalization.core_normalizer'), + CoreDenormalizer::class => new Alias('paysera_normalization.core_denormalizer'), + ]; + $format = '%paysera_normalization.date_time_normalizer.format%'; + + return [ + 'without the date_time normalizers' => [ + [], + ['definitions' => $definitions, 'aliases' => $aliases, 'parameters' => []], + ], + 'with the date_time normalizers' => [ + self::DATE_TIME_CONFIGS, + [ + 'definitions' => $definitions + [ + 'paysera_normalization.date_time_normalizer' => + (new Definition(DateTimeNormalizer::class, [$format])) + ->addTag('paysera_normalization.autoconfigured_normalizer'), + 'paysera_normalization.date_time_immutable_normalizer' => + (new Definition(DateTimeImmutableNormalizer::class, [$format])) + ->addTag('paysera_normalization.autoconfigured_normalizer') + ->addTag( + 'paysera_normalization.mixed_type_denormalizer', + ['type' => 'DateTimeInterface'] + ), + ], + 'aliases' => $aliases, + 'parameters' => ['paysera_normalization.date_time_normalizer.format' => 'U'], + ], + ], + ]; + } + + public function testLoadRaisesNoDeprecation() + { + $deprecations = []; + set_error_handler(function (int $type, string $message) use (&$deprecations): bool { + $deprecations[] = $message; + + return true; + }, E_USER_DEPRECATED); + try { + (new PayseraNormalizationExtension())->load(self::DATE_TIME_CONFIGS, new ContainerBuilder()); + } finally { + restore_error_handler(); + } + + $this->assertSame([], $deprecations); + } + + /** + * @param array> $services + * @return array + */ + private static function normalize(array $services): array + { + ksort($services['definitions']); + ksort($services['aliases']); + + return self::export($services); + } + + /** + * @param mixed $value + * @return mixed + */ + private static function export($value) + { + if (is_object($value)) { + return [get_class($value) => self::export((array)$value)]; + } + if (!is_array($value)) { + return $value; + } + $exported = []; + foreach ($value as $key => $item) { + $exported[$key] = self::export($item); + } + + return $exported; + } +} diff --git a/tests/Functional/FunctionalDeprecationNoticesTest.php b/tests/Functional/FunctionalDeprecationNoticesTest.php new file mode 100644 index 0000000..d1c549b --- /dev/null +++ b/tests/Functional/FunctionalDeprecationNoticesTest.php @@ -0,0 +1,54 @@ +markTestSkipped('symfony/error-handler is not installed (Symfony below 4.4)'); + } + $this->assertSame([false, false, false], array_map(function (string $class): bool { + return class_exists($class, false); + }, $classes)); + + DebugClassLoader::enable(); + try { + $loaded = array_map(function (string $class): bool { + return class_exists($class); + }, $classes); + } finally { + DebugClassLoader::disable(); + } + } finally { + restore_error_handler(); + } + + $this->assertSame([true, true, true], $loaded); + $bundleDeprecations = array_values(array_filter($deprecations, function (string $message): bool { + return strpos($message, 'Paysera\\Bundle\\NormalizationBundle\\') !== false; + })); + $this->assertSame([], $bundleDeprecations); + } +}