From e764b8f5514c972e117e8b56ffa821929aecadb7 Mon Sep 17 00:00:00 2001 From: Piotr Kreft Date: Sat, 23 May 2020 14:42:03 +0200 Subject: [PATCH] Eager resettable services bundle --- .github/workflows/ci.yml | 35 ++++ .github/workflows/tests.yml | 32 ++++ .gitignore | 5 + README.md | 21 +++ composer.json | 47 ++++++ infection.json.dist | 13 ++ phpunit.xml | 20 +++ .../Compiler/EagerResettableServicesPass.php | 37 ++++ .../Compiler/ValidateServicesPass.php | 31 ++++ src/DependencyInjection/Configuration.php | 28 +++ .../PKEagerResettableServicesExtension.php | 23 +++ src/PKEagerResettableServicesBundle.php | 22 +++ .../EagerResettableServicesPassTest.php | 159 ++++++++++++++++++ .../Compiler/ValidateServicesPassTest.php | 53 ++++++ ...PKEagerResettableServicesExtensionTest.php | 41 +++++ .../Controller/HomePageController.php | 19 +++ .../DependencyInjection/ResetService.php | 23 +++ tests/Fixtures/Kernel.php | 49 ++++++ tests/Fixtures/Resources/config/config.yaml | 17 ++ tests/Fixtures/Resources/config/routing.yaml | 3 + tests/ServicesResetterFunctionalTest.php | 45 +++++ 21 files changed, 723 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 infection.json.dist create mode 100644 phpunit.xml create mode 100644 src/DependencyInjection/Compiler/EagerResettableServicesPass.php create mode 100644 src/DependencyInjection/Compiler/ValidateServicesPass.php create mode 100644 src/DependencyInjection/Configuration.php create mode 100644 src/DependencyInjection/PKEagerResettableServicesExtension.php create mode 100644 src/PKEagerResettableServicesBundle.php create mode 100644 tests/DependencyInjection/Compiler/EagerResettableServicesPassTest.php create mode 100644 tests/DependencyInjection/Compiler/ValidateServicesPassTest.php create mode 100644 tests/DependencyInjection/PKEagerResettableServicesExtensionTest.php create mode 100644 tests/Fixtures/Controller/HomePageController.php create mode 100644 tests/Fixtures/DependencyInjection/ResetService.php create mode 100644 tests/Fixtures/Kernel.php create mode 100644 tests/Fixtures/Resources/config/config.yaml create mode 100644 tests/Fixtures/Resources/config/routing.yaml create mode 100644 tests/ServicesResetterFunctionalTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ee2e84e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + push: + branches: [ master ] + +jobs: + ci: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + coverage: pcov + + - name: Install dependencies + run: composer update --prefer-dist --no-progress --no-suggest --prefer-stable + + - name: PHPUnit with coverage + run: vendor/bin/phpunit --coverage-clover=build/logs/clover.xml tests + + - name: Report coverage + run: vendor/bin/php-coveralls + env: + COVERALLS_RUN_LOCALLY: yes + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + + - name: Infection mutations + run: vendor/bin/infection --show-mutations + env: + INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..37f05bc --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,32 @@ +name: Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + tests: + runs-on: ubuntu-latest + strategy: + matrix: + php-versions: ['7.3','7.4'] + composer-opts: ['--prefer-lowest', ''] + + steps: + - uses: actions/checkout@v2 + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + + - name: Validate dependencies + run: composer validate + + - name: Install dependencies + run: composer update --prefer-dist --no-progress --no-suggest ${{ matrix.composer-opts }} --prefer-stable + + - name: Run test suite + run: composer test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..413169b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/var/ +/vendor/ +.idea +composer.lock +infection.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfae615 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Eager resettable services bundle + +![CI](https://github.com/piotrkreft/eager-resettable-services-bundle/workflows/CI/badge.svg) +[![Coverage Status](https://coveralls.io/repos/github/piotrkreft/eager-resettable-services-bundle/badge.svg)](https://coveralls.io/github/piotrkreft/eager-resettable-services-bundle) +[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fpiotrkreft%2Feager-resettable-services-bundle%2Fmaster)](https://infection.github.io) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/piotrkreft/eager-resettable-services-bundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/piotrkreft/eager-resettable-services-bundle/?branch=master) + +Symfony bundle for eager instantiating resettable services. + +## Introduction +For some edge cases it might be required that service gets reset regardless of being referenced by other services. + +An example of that would be `doctrine` Registry holding Entity Managers. +It does not reset managers unless it is being referenced by other services and therefore instantiated by the container. + +This bundle by the configuration allows you to reconfigure services to be eagerly instantiated within Services Resetter. + +## Example +[example configuration](tests/Fixtures/Resources/config/config.yaml) + +Alternatively all services can be eager loaded wth `all_services` configuration flag. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..811756f --- /dev/null +++ b/composer.json @@ -0,0 +1,47 @@ +{ + "name": "piotrkreft/eager-resettable-services-bundle", + "type": "symfony-bundle", + "description": "Flexible management of Symfony resettable services", + "license": "MIT", + "authors": [ + { + "name": "Piotr Kreft", + "email": "kreftpiotrek@gmail.com" + } + ], + "require": { + "php": "^7.3", + "symfony/config": "^4.2|^5.0", + "symfony/dependency-injection": "^4.0|^5.0", + "symfony/http-kernel": "^4.0|^5.0" + }, + "require-dev": { + "piotrkreft/ci": "^0.1", + "symfony/framework-bundle": "^4.0|^5.0", + "symfony/yaml": "^4.0|^5.0" + }, + "autoload": { + "psr-4": { + "PK\\EagerResettableServicesBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "PK\\Tests\\EagerResettableServicesBundle\\": "tests/" + } + }, + "minimum-stability": "dev", + "scripts": { + "test": [ + "@prepare-cache", + "vendor/bin/pk-tests --cache-dir=./var/cache run" + ], + "fix": [ + "@prepare-cache", + "vendor/bin/pk-tests --cache-dir=./var/cache fix" + ], + "prepare-cache": [ + "mkdir -p var/cache" + ] + } +} diff --git a/infection.json.dist b/infection.json.dist new file mode 100644 index 0000000..2fbe213 --- /dev/null +++ b/infection.json.dist @@ -0,0 +1,13 @@ +{ + "source": { + "directories": [ + "src" + ] + }, + "logs": { + "text": "var/cache/infection.log", + "badge": { + "branch": "master" + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..01aa8f3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,20 @@ + + + + + + tests + + + + + + src + + + diff --git a/src/DependencyInjection/Compiler/EagerResettableServicesPass.php b/src/DependencyInjection/Compiler/EagerResettableServicesPass.php new file mode 100644 index 0000000..1ab3822 --- /dev/null +++ b/src/DependencyInjection/Compiler/EagerResettableServicesPass.php @@ -0,0 +1,37 @@ +getParameter('pk_eager_resettable_services.all_services'); + $services = $container->getParameter('pk_eager_resettable_services.services'); + + if (!$container->has('services_resetter') || !$services && !$allServices) { + return; + } + + /** @var ArgumentInterface $resettableServices */ + $resettableServices = $container->getDefinition('services_resetter')->getArgument(0); + $overrideResettableServices = []; + + foreach ($resettableServices->getValues() as $serviceId => $reference) { + if (($allServices || in_array($serviceId, $services)) && $container->hasDefinition($serviceId)) { + $overrideResettableServices[$serviceId] = new Reference($serviceId); + continue; + } + $overrideResettableServices[$serviceId] = $reference; + } + + $resettableServices->setValues($overrideResettableServices); + } +} diff --git a/src/DependencyInjection/Compiler/ValidateServicesPass.php b/src/DependencyInjection/Compiler/ValidateServicesPass.php new file mode 100644 index 0000000..6ef3d40 --- /dev/null +++ b/src/DependencyInjection/Compiler/ValidateServicesPass.php @@ -0,0 +1,31 @@ +getParameter('pk_eager_resettable_services.services') as $serviceId) { + if ($container->hasDefinition($serviceId) || $container->hasAlias($serviceId)) { + continue; + } + $notExisting[] = $serviceId; + } + if (!$notExisting) { + return; + } + + throw new InvalidConfigurationException(sprintf( + 'Missing resettable services for eager initialization (%s).', + implode(', ', $notExisting) + )); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000..618adb9 --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,28 @@ +getRootNode() + ->children() + ->arrayNode('services') + ->scalarPrototype()->end() + ->end() + ->booleanNode('all_services')->defaultFalse()->end() + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/DependencyInjection/PKEagerResettableServicesExtension.php b/src/DependencyInjection/PKEagerResettableServicesExtension.php new file mode 100644 index 0000000..534bf04 --- /dev/null +++ b/src/DependencyInjection/PKEagerResettableServicesExtension.php @@ -0,0 +1,23 @@ +getConfiguration($configs, $container); + $config = $this->processConfiguration($configuration, $configs); + + $container->setParameter('pk_eager_resettable_services.services', $config['services']); + $container->setParameter('pk_eager_resettable_services.all_services', $config['all_services']); + } +} diff --git a/src/PKEagerResettableServicesBundle.php b/src/PKEagerResettableServicesBundle.php new file mode 100644 index 0000000..3d2f510 --- /dev/null +++ b/src/PKEagerResettableServicesBundle.php @@ -0,0 +1,22 @@ +addCompilerPass(new ValidateServicesPass()) + ->addCompilerPass(new EagerResettableServicesPass(), PassConfig::TYPE_REMOVE) + ; + } +} diff --git a/tests/DependencyInjection/Compiler/EagerResettableServicesPassTest.php b/tests/DependencyInjection/Compiler/EagerResettableServicesPassTest.php new file mode 100644 index 0000000..90c7391 --- /dev/null +++ b/tests/DependencyInjection/Compiler/EagerResettableServicesPassTest.php @@ -0,0 +1,159 @@ +compilerPass = new EagerResettableServicesPass(); + } + + public function testShouldSkipWhenNoResetter(): void + { + // given + $container = new ContainerBuilder(); + $container->setParameter('pk_eager_resettable_services.services', []); + $container->setParameter('pk_eager_resettable_services.all_services', true); + + // when + $this->compilerPass->process($container); + + // then + $this->assertFalse($container->hasDefinition('services_resetter')); + } + + /** + * @dataProvider casesProvider + * + * @param Reference[] $expectedArguments + */ + public function testShouldOverrideServicesArgument( + ContainerBuilder $container, + array $expectedArguments + ): void { + // when + $this->compilerPass->process($container); + + // then + $this->assertEquals( + $expectedArguments, + $container->getDefinition('services_resetter')->getArgument(0)->getValues() + ); + } + + /** + * @return mixed[][] + */ + public function casesProvider(): array + { + return [ + 'no eager loaded' => [ + $this->buildContainer() + ->setParameter('pk_eager_resettable_services.services', []) + ->setParameter('pk_eager_resettable_services.all_services', false), + [ + 'definition1' => new Reference( + 'definition1', + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE + ), + 'definition2' => new Reference( + 'definition2', + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE + ), + 'definition3' => new Reference( + 'definition3', + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE + ), + ], + ], + 'one eager loaded' => [ + $this->buildContainer() + ->setParameter('pk_eager_resettable_services.services', ['definition1']) + ->setParameter('pk_eager_resettable_services.all_services', false), + [ + 'definition1' => new Reference('definition1'), + 'definition2' => new Reference( + 'definition2', + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE + ), + 'definition3' => new Reference( + 'definition3', + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE + ), + ], + ], + 'all eager loaded' => [ + $this->buildContainer() + ->setParameter('pk_eager_resettable_services.services', []) + ->setParameter('pk_eager_resettable_services.all_services', true), + [ + 'definition1' => new Reference('definition1'), + 'definition2' => new Reference( + 'definition2', + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE + ), + 'definition3' => new Reference('definition3'), + ], + ], + 'all eager loaded with excessive configuration' => [ + $this->buildContainer() + ->setParameter('pk_eager_resettable_services.services', ['definition1']) + ->setParameter('pk_eager_resettable_services.all_services', true), + [ + 'definition1' => new Reference('definition1'), + 'definition2' => new Reference( + 'definition2', + ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE + ), + 'definition3' => new Reference('definition3'), + ], + ], + ]; + } + + private function buildContainer(): ContainerBuilder + { + $definition1 = (new Definition()); + $definition3 = (new Definition()); + $resettableServicesArgument = new IteratorArgument([ + 'definition1' => new Reference('definition1', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), + 'definition2' => new Reference('definition2', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), + 'definition3' => new Reference('definition3', ContainerInterface::IGNORE_ON_UNINITIALIZED_REFERENCE), + ]); + $servicesResetter = (new Definition())->setArgument(0, $resettableServicesArgument); + $container = new class () extends ContainerBuilder { + /** + * {@inheritdoc} + */ + public function setParameter($name, $value) + { + parent::setParameter($name, $value); + + return $this; + } + }; + $container->addDefinitions([ + 'definition1' => $definition1, + 'definition3' => $definition3, + 'services_resetter' => $servicesResetter, + ]); + + return $container; + } +} diff --git a/tests/DependencyInjection/Compiler/ValidateServicesPassTest.php b/tests/DependencyInjection/Compiler/ValidateServicesPassTest.php new file mode 100644 index 0000000..f8bfd35 --- /dev/null +++ b/tests/DependencyInjection/Compiler/ValidateServicesPassTest.php @@ -0,0 +1,53 @@ +compilerPass = new ValidateServicesPass(); + } + + public function testShouldProcess(): void + { + // given + $container = new ContainerBuilder(); + $container->setParameter('pk_eager_resettable_services.services', ['service_id']); + $container->setDefinition('service_id', new Definition()); + $this->expectNotToPerformAssertions(); + + // when + $this->compilerPass->process($container); + } + + public function testShouldThrowExceptionOnNonExistingServices(): void + { + // given + $container = new ContainerBuilder(); + $container->setParameter( + 'pk_eager_resettable_services.services', + ['service_1_id', 'services_2_id', 'services_3_id'] + ); + $container->setDefinition('service_1_id', new Definition()); + $this->expectExceptionObject(new InvalidConfigurationException( + 'Missing resettable services for eager initialization (services_2_id, services_3_id).' + )); + + // when + $this->compilerPass->process($container); + } +} diff --git a/tests/DependencyInjection/PKEagerResettableServicesExtensionTest.php b/tests/DependencyInjection/PKEagerResettableServicesExtensionTest.php new file mode 100644 index 0000000..616b493 --- /dev/null +++ b/tests/DependencyInjection/PKEagerResettableServicesExtensionTest.php @@ -0,0 +1,41 @@ +extension = new PKEagerResettableServicesExtension(); + } + + public function testShouldLoad(): void + { + // given + $configuration = [ + 'pk_eager_resettable_services' => [ + 'all_services' => false, + 'services' => ['service_id'], + ], + ]; + $container = new ContainerBuilder(); + + // when + $this->extension->load($configuration, $container); + + // then + $this->assertEquals(['service_id'], $container->getParameter('pk_eager_resettable_services.services')); + $this->assertEquals(false, $container->getParameter('pk_eager_resettable_services.all_services')); + } +} diff --git a/tests/Fixtures/Controller/HomePageController.php b/tests/Fixtures/Controller/HomePageController.php new file mode 100644 index 0000000..8d4cc7e --- /dev/null +++ b/tests/Fixtures/Controller/HomePageController.php @@ -0,0 +1,19 @@ +resetted = true; + } + + public function isResetted(): bool + { + return $this->resetted; + } +} diff --git a/tests/Fixtures/Kernel.php b/tests/Fixtures/Kernel.php new file mode 100644 index 0000000..22bd1dc --- /dev/null +++ b/tests/Fixtures/Kernel.php @@ -0,0 +1,49 @@ +import(__DIR__ . '/Resources/config/routing.yaml'); + } + + public function configureContainer(ContainerBuilder $container, LoaderInterface $loader): void + { + $loader->load(__DIR__ . '/Resources/config/config.yaml'); + } + + public function getCacheDir(): string + { + return __DIR__ . '/../../var/cache/' . $this->environment; + } + + public function getLogDir(): string + { + return __DIR__ . '/../../var/logs'; + } +} diff --git a/tests/Fixtures/Resources/config/config.yaml b/tests/Fixtures/Resources/config/config.yaml new file mode 100644 index 0000000..28487bb --- /dev/null +++ b/tests/Fixtures/Resources/config/config.yaml @@ -0,0 +1,17 @@ +framework: + secret: '' + test: true + +pk_eager_resettable_services: + services: ['pk_reset_service_eager'] + +services: + pk_reset_service_eager: + class: PK\Tests\EagerResettableServicesBundle\Fixtures\DependencyInjection\ResetService + public: true + tags: [{name: 'kernel.reset', method: 'reset'}] + + pk_reset_service_lazy: + class: PK\Tests\EagerResettableServicesBundle\Fixtures\DependencyInjection\ResetService + public: true + tags: [{name: 'kernel.reset', method: 'reset'}] diff --git a/tests/Fixtures/Resources/config/routing.yaml b/tests/Fixtures/Resources/config/routing.yaml new file mode 100644 index 0000000..37bd8dc --- /dev/null +++ b/tests/Fixtures/Resources/config/routing.yaml @@ -0,0 +1,3 @@ +controllers: + resource: '../../Controller' + type: annotation diff --git a/tests/ServicesResetterFunctionalTest.php b/tests/ServicesResetterFunctionalTest.php new file mode 100644 index 0000000..9a44f86 --- /dev/null +++ b/tests/ServicesResetterFunctionalTest.php @@ -0,0 +1,45 @@ +kernel = new Kernel('test', false); + $this->kernel->boot(); + } + + protected function tearDown(): void + { + (new Filesystem())->remove($this->kernel->getCacheDir()); + $this->kernel->shutdown(); + } + + public function testShouldResetTheServiceOnEveryButFirstRequest(): void + { + // given + $request = new Request(); + + // when + $this->kernel->handle($request); + // First kernel handle does not invoke resetters + $this->kernel->handle($request); + + // then + $this->assertTrue($this->kernel->getContainer()->get('pk_reset_service_eager')->isResetted()); + $this->assertFalse($this->kernel->getContainer()->get('pk_reset_service_lazy')->isResetted()); + } +}