diff --git a/.gitattributes b/.gitattributes index 3cf0600..6631cbf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -Tests/ export-ignore +tests/ export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore diff --git a/.gitignore b/.gitignore index ee2b4cc..2198cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ build/ vendor/ composer.lock phpunit.xml -/.idea /ECNFeatureToggleBundle.iml -/.idea/ /.phpunit.result.cache +.php-cs-fixer.cache diff --git a/.php-cs-fixer.src.php b/.php-cs-fixer.src.php new file mode 100644 index 0000000..3a4d418 --- /dev/null +++ b/.php-cs-fixer.src.php @@ -0,0 +1,27 @@ +in(__DIR__.'/src') +; + +return (new PhpCsFixer\Config()) + ->setRules([ + // Symfony Coding Standard (includes PSR2 and PSR12) but disable some settings to not violate the current Otelo Coding Standard + '@Symfony' => true, + '@Symfony:risky' => true, + // So declare_strict_type can be on the same line as the opening tag + 'blank_line_after_opening_tag' => false, + 'linebreak_after_opening_tag' => false, + // Would otherwise remove @inheritdoc tags from classes does not inherit. + 'phpdoc_no_useless_inheritdoc' => false, + // Would otherwise remove @param, @return and @var tags that don't provide any useful information. + 'no_superfluous_phpdoc_tags' => false, + // In @PHP80Migration:risky containing 'declare_strict_types' is not needed on interfaces, + // so you can remove them there @see https://github.com/Automattic/phpcs-neutron-standard/issues/20 + '@PHP81Migration' => true, + '@DoctrineAnnotation' => true, + // Risky formatting. Use --allow-risky=true in the command to use them. + '@PHP80Migration:risky' => true, + ]) + ->setFinder($finder) +; diff --git a/.php-cs-fixer.tests.php b/.php-cs-fixer.tests.php new file mode 100644 index 0000000..7ec7050 --- /dev/null +++ b/.php-cs-fixer.tests.php @@ -0,0 +1,30 @@ +in(__DIR__.'/tests') +; + +return (new PhpCsFixer\Config()) + ->setRules([ + // Symfony Coding Standard (includes PSR2 and PSR12) but disable some settings to not violate the current Otelo Coding Standard + '@Symfony' => true, + '@Symfony:risky' => true, + // So declare_strict_type can be on the same line as the opening tag + 'blank_line_after_opening_tag' => false, + 'linebreak_after_opening_tag' => false, + // Would otherwise remove @inheritdoc tags from classes does not inherit. + 'phpdoc_no_useless_inheritdoc' => false, + // Would otherwise remove @param, @return and @var tags that don't provide any useful information. + 'no_superfluous_phpdoc_tags' => false, + // In @PHP80Migration:risky containing 'declare_strict_types' is not needed on interfaces, + // so you can remove them there @see https://github.com/Automattic/phpcs-neutron-standard/issues/20 + '@PHP81Migration' => true, + '@DoctrineAnnotation' => true, + // Risky formatting. Use --allow-risky=true in the command to use them. + '@PHP80Migration:risky' => true, + '@PHPUnit100Migration:risky' => true, + // avoid that tests get void as return value (for setup is fine -> add manually) + 'void_return' => false, + ]) + ->setFinder($finder) +; diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b8d5aa..52a4d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Change Log +## 3.1.0 - 2023-05-12 + +### Added + +- added support for PHP >=8 +- added refactoring tools (php-cs-fixer, rector) + +## 3.0.0 - 2022-02-01 + +### Added + +- added support for PHP 8 and 8.1 +- added support for Symfony 5.4 and 6 +- added use of attributes + +### Changed + +- ControllerFilterEvent to ControllerEvent + +### Removed + +- removed support for PHP 7 and lower +- removed support for Symfony 3 and 4 +- removed annotations ## 2.0.0 - 2019-12-25 diff --git a/EcnFeatureToggleBundle.php b/EcnFeatureToggleBundle.php index 0859aa7..3b8e935 100644 --- a/EcnFeatureToggleBundle.php +++ b/EcnFeatureToggleBundle.php @@ -12,8 +12,8 @@ namespace Ecn\FeatureToggleBundle; use Ecn\FeatureToggleBundle\DependencyInjection\Compiler\VoterCompilerPass; -use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; /** * @author Pierre Groth diff --git a/README.md b/README.md index 11568e3..d0fb4c7 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ In order to install ECNFeatureToggleBundle, you need at least -- PHP 7.2 or greater -- Symfony 3.4, Symfony 4.2 or greater +- PHP 8.0 or greater +- Symfony 5.4 or greater ## Installation @@ -21,6 +21,11 @@ In order to install ECNFeatureToggleBundle, you need at least $ composer require ecn/featuretoggle-bundle ``` +Functionality of twig is only optional. Require it to your composer if you need it to use it. +```bash +$ composer require twig/twig +``` + ### Step 2: Activate the bundle @@ -105,12 +110,12 @@ if ($this->get('feature')->has('MyNewFeature')) { ## Voters -In order to decide if a feature is available or not, voters are being used. Currently there are five voters included. +In order to decide if a feature is available or not, voters are being used. Currently, there are five voters included. ### AlwaysTrueVoter -This is the default voter and it will always pass. So if you have a feature defined, it will always be displayed. +This is the default voter, and it will always pass. So if you have a feature defined, it will always be displayed. The full configuration for using this voter looks like this: @@ -143,7 +148,7 @@ This voter passes on a given ratio between 0 and 1, which makes it suitable for The higher the ratio, the more likely the voter will pass. A value of 1 will make it pass every time, 0 will make it never pass. -Additionally, the result of the first check can be bound to the users session. This is useful if you need a feature +Additionally, the result of the first check can be bound to the users' session. This is useful if you need a feature to be persistent across multiple requests. To enable this, just set `sticky` to `true`. If you want to use this voter, this is the full configuration: diff --git a/composer.json b/composer.json index adff497..330c615 100644 --- a/composer.json +++ b/composer.json @@ -10,39 +10,56 @@ } ], "require": { - "php": "^7.2", - "symfony/framework-bundle": "~3.4.31|~4.2.7|^4.3.4", - "doctrine/annotations": "~1.6", - "doctrine/common": "2.10.0" + "php": "^8.1", + "symfony/framework-bundle": "^5.4 || ^6.0", + "doctrine/common": "^3.4.3" }, "require-dev": { - "phpunit/phpunit": "^8.2|^7.5", - "squizlabs/php_codesniffer": "^3.3", - "escapestudios/symfony2-coding-standard": "^3.4.1", - "vimeo/psalm": "~3.4" + "phpunit/phpunit": "^10.1.3", + "squizlabs/php_codesniffer": "^3.7.2", + "escapestudios/symfony2-coding-standard": "^3.13.0", + "vimeo/psalm": "^5.12.0", + "jetbrains/phpstorm-attributes": "^1.0", + "friendsofphp/php-cs-fixer": "^3.17.0", + "rector/rector": "^0.16.0", + "roave/security-advisories": "dev-master", + "twig/twig": "3.6.0" }, "conflict": { - "phpunit/phpunit": "<5.4.3" + "phpunit/phpunit": "<9.0" + }, + "suggest": { + "twig/twig": "^3.6.0" }, "autoload": { "psr-4": { - "Ecn\\FeatureToggleBundle\\": "" - }, - "exclude-from-classmap": [ - "Tests/" - ] + "Ecn\\FeatureToggleBundle\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Ecn\\FeatureToggleBundle\\Tests\\": "tests/" + } }, "scripts": { "test": "vendor/bin/phpunit", "test-no-twig": "vendor/bin/phpunit --testsuite without_twig", "psalm": "vendor/bin/psalm", "psalm-no-twig": "vendor/bin/psalm --config=psalm.without_twig.xml", - "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover build/coverage.xml" + "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover build/coverage.xml", + "rector": "vendor/bin/rector process --dry-run", + "cs-fixer-src": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.src.php --verbose --diff --allow-risky=yes --dry-run", + "cs-fixer-tests": "vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.tests.php --verbose --diff --allow-risky=yes --dry-run" }, - "minimum-stability": "dev", + "minimum-stability": "stable", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" + } + }, + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true } } } diff --git a/Resources/config/services.xml b/config/services.xml similarity index 97% rename from Resources/config/services.xml rename to config/services.xml index 18da193..a251a32 100644 --- a/Resources/config/services.xml +++ b/config/services.xml @@ -2,7 +2,7 @@ + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> diff --git a/phpcs.xml b/phpcs.xml index f4fd054..870440e 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,9 +1,9 @@ - PSR2 Coding Standard on EcnFeatureToggleBundle + Modified PSR12 Coding Standard on EcnFeatureToggleBundle ./ - */Tests/* + */tests/* */vendor/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2384922..e1a5010 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,25 +1,27 @@ - - - - - - ./Tests - - - ./Tests - ./Tests/Twig - - - - - - ./ - - ./Resources - ./Tests - ./vendor - - - + + + + + ./tests + + + ./tests + ./tests/Twig + + + + + ./src + + + ./config + ./tests + ./vendor + + diff --git a/psalm.without_twig.xml b/psalm.without_twig.xml index 5788677..51f118d 100644 --- a/psalm.without_twig.xml +++ b/psalm.without_twig.xml @@ -2,14 +2,15 @@ - + - + - + @@ -23,7 +24,6 @@ - diff --git a/psalm.xml b/psalm.xml index 7d0c441..5855613 100644 --- a/psalm.xml +++ b/psalm.xml @@ -2,12 +2,13 @@ - + - + @@ -22,7 +23,6 @@ - diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..6e7068e --- /dev/null +++ b/rector.php @@ -0,0 +1,56 @@ +paths([ + __DIR__.'/src', + __DIR__.'/tests', + ]); + + $rectorConfig->phpVersion(PhpVersion::PHP_81); + + // register a single rule + $rectorConfig->rules([ + InlineConstructorDefaultToPropertyRector::class, + AddParamTypeSplFixedArrayRector::class, + ]); + + $rectorConfig->skip([ + RemoveAnnotationRector::class, + RemoveUselessParamTagRector::class, + IntersectionTypesRector::class, + MixedTypeRector::class, + UnionTypesRector::class, + ClassPropertyAssignToConstructorPromotionRector::class, + AddSeeTestAnnotationRector::class, + RenameClassRector::class, + ]); + + // define sets of rules + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_81, + PHPUnitLevelSetList::UP_TO_PHPUNIT_100, + PHPUnitSetList::PHPUNIT_100, + PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES, + PHPUnitSetList::PHPUNIT_CODE_QUALITY, + SymfonySetList::SYMFONY_62, + SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES, + SymfonySetList::SYMFONY_CODE_QUALITY, + ]); +}; diff --git a/Configuration/Feature.php b/src/Attributes/Feature.php similarity index 55% rename from Configuration/Feature.php rename to src/Attributes/Feature.php index feadd31..bd8da1b 100644 --- a/Configuration/Feature.php +++ b/src/Attributes/Feature.php @@ -1,5 +1,4 @@ - - * - * @Annotation() - * - * @Target({"CLASS", "METHOD"}) */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] class Feature { /** * Feature name to be checked. * - * @var string - * - * @Required() + * @param string $name */ - public $name = ""; + #[Required] + public function __construct(public string $name = '') + { + } } diff --git a/DependencyInjection/Compiler/VoterCompilerPass.php b/src/DependencyInjection/Compiler/VoterCompilerPass.php similarity index 93% rename from DependencyInjection/Compiler/VoterCompilerPass.php rename to src/DependencyInjection/Compiler/VoterCompilerPass.php index 88a2490..f413821 100644 --- a/DependencyInjection/Compiler/VoterCompilerPass.php +++ b/src/DependencyInjection/Compiler/VoterCompilerPass.php @@ -1,5 +1,4 @@ -addMethodCall( 'addVoter', - [new Reference($id), $attributes["alias"]] + [new Reference($id), $attributes['alias']] ); } } diff --git a/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php similarity index 71% rename from DependencyInjection/Configuration.php rename to src/DependencyInjection/Configuration.php index 7fb265b..e835b73 100644 --- a/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -1,5 +1,4 @@ -getRootNode(); - } else { - /** @psalm-suppress UndefinedMethod */ - /** @psalm-suppress DeprecatedMethod */ - $rootNode = $treeBuilder->root('ecn_feature_toggle'); - } + /** @var ArrayNodeDefinition $rootNode */ + $rootNode = $treeBuilder->getRootNode(); + /** + * @psalm-suppress PossiblyNullReference + * @psalm-suppress PossiblyUndefinedMethod + * @psalm-suppress UndefinedInterfaceMethod + */ $rootNode ->children() ->arrayNode('default') @@ -51,7 +46,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->scalarNode('voter')->defaultValue('AlwaysTrueVoter')->end() - ->variableNode('params')->defaultValue(array())->end() + ->variableNode('params')->defaultValue([])->end() ->end() ->end() @@ -60,7 +55,7 @@ public function getConfigTreeBuilder(): TreeBuilder ->children() ->scalarNode('voter')->defaultValue(null)->end() - ->variableNode('params')->defaultValue(array())->end() + ->variableNode('params')->defaultValue([])->end() ->end() ->end() diff --git a/DependencyInjection/EcnFeatureToggleExtension.php b/src/DependencyInjection/EcnFeatureToggleExtension.php similarity index 77% rename from DependencyInjection/EcnFeatureToggleExtension.php rename to src/DependencyInjection/EcnFeatureToggleExtension.php index 2feee09..6a8643b 100644 --- a/DependencyInjection/EcnFeatureToggleExtension.php +++ b/src/DependencyInjection/EcnFeatureToggleExtension.php @@ -1,5 +1,4 @@ -processConfiguration($configuration, $configs); - $features = array_key_exists('features', $config) ? $config['features'] : []; - $default = array_key_exists('default', $config) ? $config['default'] : []; + $features = $config['features'] ?? []; + $default = $config['default'] ?? []; $container->setParameter('features', $features); $container->setParameter('default', $default); - $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../config')); $loader->load('services.xml'); } } diff --git a/EventListener/ControllerListener.php b/src/EventListener/ControllerListener.php similarity index 62% rename from EventListener/ControllerListener.php rename to src/EventListener/ControllerListener.php index 73af2eb..a6b3cb5 100644 --- a/EventListener/ControllerListener.php +++ b/src/EventListener/ControllerListener.php @@ -1,5 +1,4 @@ - */ class ControllerListener implements EventSubscriberInterface { - /** - * @var Reader - */ - protected $reader; + protected FeatureService $featureService; /** - * @var FeatureService - */ - protected $featureService; - - /** - * @param Reader $reader * @param FeatureService $featureService */ - public function __construct(Reader $reader, FeatureService $featureService) + public function __construct(FeatureService $featureService) { - $this->reader = $reader; $this->featureService = $featureService; } /** - * @param FilterControllerEvent $event - * - * @psalm-suppress DeprecatedClass - * + * @param ControllerEvent $event * * @throws \ReflectionException */ - public function onKernelController(FilterControllerEvent $event): void + public function onKernelController(ControllerEvent $event): void { // We can't resolve the controller name from non-array callables. $controller = $event->getController(); @@ -70,14 +56,14 @@ public function onKernelController(FilterControllerEvent $event): void return; } - $className = $this->getRealClass(is_object($controller[0]) ? \get_class($controller[0]) : $controller[0]); + /** @var class-string */ + $className = $this->getRealClass(\is_object($controller[0]) ? $controller[0]::class : $controller[0]); - /** @psalm-suppress ArgumentTypeCoercion */ $object = new \ReflectionClass($className); $method = $object->getMethod($controller[1]); - $controllerAnnotations = $this->reader->getClassAnnotations($object); - $actionAnnotations = $this->reader->getMethodAnnotations($method); + $controllerAnnotations = $object->getAttributes(); + $actionAnnotations = $method->getAttributes(); $this->checkFeature($controllerAnnotations); $this->checkFeature($actionAnnotations); @@ -86,6 +72,7 @@ public function onKernelController(FilterControllerEvent $event): void /** * {@inheritDoc} */ + #[ArrayShape([KernelEvents::CONTROLLER => 'string'])] public static function getSubscribedEvents(): array { return [ @@ -96,14 +83,16 @@ public static function getSubscribedEvents(): array /** * Checks for features in annotations. * - * @param array $annotations + * @param array $attributes * - * @throws NotFoundHttpException If a feature is found, but not enabled. + * @throws NotFoundHttpException if a feature is found, but not enabled */ - private function checkFeature(array $annotations): void + private function checkFeature(array $attributes): void { - foreach ($annotations as $feature) { - if (($feature instanceof Feature) && !$this->featureService->has($feature->name)) { + /** @var \ReflectionAttribute $feature */ + foreach ($attributes as $feature) { + $featureInstance = $feature->newInstance(); + if (($featureInstance instanceof Feature) && !$this->featureService->has($featureInstance->name)) { throw new NotFoundHttpException(); } } diff --git a/Exception/VoterNotFoundException.php b/src/Exception/VoterNotFoundException.php similarity index 92% rename from Exception/VoterNotFoundException.php rename to src/Exception/VoterNotFoundException.php index 674d34b..2e4ad67 100644 --- a/Exception/VoterNotFoundException.php +++ b/src/Exception/VoterNotFoundException.php @@ -1,5 +1,4 @@ -features)) { + if (!isset($this->features[$feature])) { return false; } - try { - $voter = $this->initVoter($feature); - - return $voter->pass(); - } catch (VoterNotFoundException $exception) { - throw $exception; - } + return $this->initVoter($feature)->pass(); } /** - * Initializes a voter for a specific feature + * Initializes a voter for a specific feature. * * @param string $feature * @@ -90,7 +71,6 @@ protected function initVoter(string $feature): VoterInterface $voterName = $featureDefinition['voter'] ?: $this->defaultVoter['voter']; $voter = $this->voterRegistry->getVoter($voterName); - $defaultParams = $this->defaultVoter['params']; $params = $featureDefinition['params'] ?: $defaultParams; diff --git a/Twig/FeatureToggleExtension.php b/src/Twig/FeatureToggleExtension.php similarity index 83% rename from Twig/FeatureToggleExtension.php rename to src/Twig/FeatureToggleExtension.php index 4288f9c..8dfffd8 100644 --- a/Twig/FeatureToggleExtension.php +++ b/src/Twig/FeatureToggleExtension.php @@ -1,4 +1,4 @@ -hasFeature(...)), ]; } /** - * @return FeatureToggleTokenParser[] + * @return array [] */ + #[Pure] public function getTokenParsers(): array { return [ - new FeatureToggleTokenParser(), + new FeatureToggleTokenParser(), ]; } diff --git a/Twig/FeatureToggleNode.php b/src/Twig/FeatureToggleNode.php similarity index 97% rename from Twig/FeatureToggleNode.php rename to src/Twig/FeatureToggleNode.php index efea52b..e39f58a 100644 --- a/Twig/FeatureToggleNode.php +++ b/src/Twig/FeatureToggleNode.php @@ -1,5 +1,4 @@ -expect(Token::NAME_TYPE)->getValue(); $stream->expect(Token::BLOCK_END_TYPE); - $feature = $this->parser->subparse([$this, 'decideFeatureEnd'], true); + $feature = $this->parser->subparse($this->decideFeatureEnd(...), true); $stream->expect(Token::BLOCK_END_TYPE); return new FeatureToggleNode($name, $feature, $lineno, $this->getTag()); diff --git a/Voters/AlwaysFalseVoter.php b/src/Voters/AlwaysFalseVoter.php similarity index 94% rename from Voters/AlwaysFalseVoter.php rename to src/Voters/AlwaysFalseVoter.php index e7906b5..c218def 100644 --- a/Voters/AlwaysFalseVoter.php +++ b/src/Voters/AlwaysFalseVoter.php @@ -1,5 +1,4 @@ -session = $session; } @@ -50,8 +37,8 @@ public function __construct(SessionInterface $session = null) */ public function setParams(array $params): void { - $this->ratio = array_key_exists('ratio', $params) ? $params['ratio'] : 0.5; - $this->sticky = array_key_exists('sticky', $params) ? $params['sticky'] : false; + $this->ratio = $params['ratio'] ?? 0.5; + $this->sticky = $params['sticky'] ?? false; } /** @@ -69,14 +56,14 @@ public function pass(): bool } /** - * Get a persisted pass value + * Get a persisted pass value. * * @return bool */ protected function getStickyRatioPass(): bool { if (null === $this->session) { - throw new InvalidArgumentException(sprintf('The service "%s" has a dependency on the session', get_class($this))); + throw new \InvalidArgumentException(sprintf('The service "%s" has a dependency on the session', static::class)); } $sessionKey = '_ecn_featuretoggle_'.$this->feature; @@ -92,14 +79,16 @@ protected function getStickyRatioPass(): bool } /** - * Check if the ratio passes + * Check if the ratio passes. * * @return bool + * + * @throws \Exception */ protected function getRatioPass(): bool { $ratio = $this->ratio * 100; - return rand(0, 99) < $ratio; + return random_int(0, 99) < $ratio; } } diff --git a/Voters/RequestHeaderVoter.php b/src/Voters/RequestHeaderVoter.php similarity index 77% rename from Voters/RequestHeaderVoter.php rename to src/Voters/RequestHeaderVoter.php index c4bf5fe..60bb465 100644 --- a/Voters/RequestHeaderVoter.php +++ b/src/Voters/RequestHeaderVoter.php @@ -1,5 +1,4 @@ -checkHeaderValues = $headers ? static::isAssociativeArray($headers) : false; - $this->headers = $headers; + $this->checkHeaderValues = $headers && self::isAssociativeArray($headers); + $this->headers = $headers; } /** @@ -95,6 +83,6 @@ public function pass(): bool */ public static function isAssociativeArray(array $arr): bool { - return array_keys($arr) !== range(0, count($arr) - 1); + return array_keys($arr) !== range(0, \count($arr) - 1); } } diff --git a/Voters/ScheduleVoter.php b/src/Voters/ScheduleVoter.php similarity index 74% rename from Voters/ScheduleVoter.php rename to src/Voters/ScheduleVoter.php index dc95b78..7cb05c7 100644 --- a/Voters/ScheduleVoter.php +++ b/src/Voters/ScheduleVoter.php @@ -1,5 +1,4 @@ - */ @@ -20,18 +22,16 @@ final class ScheduleVoter implements VoterInterface use VoterTrait; /** - * A valid DateTime representation - * - * @var string|null + * A valid DateTime representation. */ - private $schedule; + private string $schedule = ''; /** * {@inheritdoc} */ public function setParams(array $params): void { - $this->schedule = array_key_exists('schedule', $params) ? $params['schedule'] : null; + $this->schedule = $params['schedule'] ?? ''; } /** @@ -40,13 +40,13 @@ public function setParams(array $params): void public function pass(): bool { // Don't pass if schedule is invalid - if (null === $this->schedule) { + if ('' === $this->schedule) { return true; } try { $schedule = new \DateTime($this->schedule); - } catch (\Throwable $e) { + } catch (Exception) { return false; } diff --git a/Voters/VoterInterface.php b/src/Voters/VoterInterface.php similarity index 78% rename from Voters/VoterInterface.php rename to src/Voters/VoterInterface.php index b5afbde..4bee766 100644 --- a/Voters/VoterInterface.php +++ b/src/Voters/VoterInterface.php @@ -1,5 +1,4 @@ - */ - private $voters = []; + private array $voters = []; /** * @param VoterInterface $voter The voter service to add @@ -34,7 +33,7 @@ public function addVoter(VoterInterface $voter, string $alias): void } /** - * Returns a voter by its alias + * Returns a voter by its alias. * * @param string $alias * @@ -44,7 +43,7 @@ public function addVoter(VoterInterface $voter, string $alias): void */ public function getVoter(string $alias): VoterInterface { - if (array_key_exists($alias, $this->voters)) { + if (isset($this->voters[$alias])) { return $this->voters[$alias]; } diff --git a/Voters/VoterTrait.php b/src/Voters/VoterTrait.php similarity index 81% rename from Voters/VoterTrait.php rename to src/Voters/VoterTrait.php index de3a3dd..6d3db58 100644 --- a/Voters/VoterTrait.php +++ b/src/Voters/VoterTrait.php @@ -1,5 +1,4 @@ - */ trait VoterTrait { - /** - * @var string - */ - private $feature = ""; + private string $feature = ''; /** * {@inheritdoc} diff --git a/Tests/EcnFeatureToggleBundleTest.php b/tests/EcnFeatureToggleBundleTest.php similarity index 72% rename from Tests/EcnFeatureToggleBundleTest.php rename to tests/EcnFeatureToggleBundleTest.php index d75bb66..bda4932 100644 --- a/Tests/EcnFeatureToggleBundleTest.php +++ b/tests/EcnFeatureToggleBundleTest.php @@ -1,5 +1,4 @@ -createConfiguration([ - 'features' => ['testfeature' => []] + 'features' => ['testFeature' => []], ]); // Load feature config $features = $this->configuration->getParameter('features'); $default = $this->configuration->getParameter('default'); - $this->assertEquals(['voter' => 'AlwaysTrueVoter', 'params' => []], $default); - $this->assertEquals([], $features['testfeature']['params']); + $this->assertSame(['voter' => 'AlwaysTrueVoter', 'params' => []], $default); + $this->assertSame([], $features['testFeature']['params']); } /** - * @param array $config - * - * @throws Exception + * @throws \Exception */ - private function createConfiguration($config = []): void + private function createConfiguration(array $config = []): void { $this->configuration = new ContainerBuilder(); diff --git a/Tests/EventListener/ControllerListenerTest.php b/tests/EventListener/ControllerListenerTest.php similarity index 54% rename from Tests/EventListener/ControllerListenerTest.php rename to tests/EventListener/ControllerListenerTest.php index 1dbe9cb..a607211 100644 --- a/Tests/EventListener/ControllerListenerTest.php +++ b/tests/EventListener/ControllerListenerTest.php @@ -1,5 +1,4 @@ -listener = new ControllerListener( - new AnnotationReader(), - new FeatureService([], [], new VoterRegistry()) - ); - $this->request = $this->createRequest(); + $this->listener = new ControllerListener(new FeatureService([], [], new VoterRegistry())); + $this->request = new Request([], [], []); - // trigger the autoloading of the @Feature annotation + // trigger to autoload the @Feature annotation class_exists(Feature::class); } /** - * @return Request - */ - protected function createRequest(): Request - { - return new Request([], [], []); - } - - public function tearDown(): void - { - $this->listener = null; - $this->request = null; - } - - /** - * Test Annotation Feature at method + * Test Annotation Feature at method. * - * @throws ReflectionException + * @throws \ReflectionException + * @throws Exception */ public function testFeatureAnnotationAtMethod(): void { @@ -100,15 +65,16 @@ public function testFeatureAnnotationAtMethod(): void $controller = new FooControllerFeatureAtMethod(); - $this->event = $this->getFilterControllerEvent([$controller, 'barAction'], $this->request); + $this->event = $this->getControllerEvent($controller->barAction(...), $this->request); $this->listener->onKernelController($this->event); } /** - * Test Annotation Feature at static method + * Test Annotation Feature at static method. * - * @throws ReflectionException + * @throws \ReflectionException + * @throws Exception */ public function testFeatureAnnotationAtStaticMethod(): void { @@ -116,59 +82,59 @@ public function testFeatureAnnotationAtStaticMethod(): void $controller = new FooControllerFeatureAtStaticMethod(); - $this->event = $this->getFilterControllerEvent([$controller, 'barAction'], $this->request); + $this->event = $this->getControllerEvent($controller->barAction(...), $this->request); $this->listener->onKernelController($this->event); } /** - * @param $controller - * @param Request $request - * - * @return FilterControllerEvent + * @throws Exception */ - protected function getFilterControllerEvent($controller, Request $request): FilterControllerEvent + protected function getControllerEvent(object|array|string $controller, Request $request): ControllerEvent { - /** @var Kernel|MockBuilder $mockKernel */ - $mockKernel = $this->getMockForAbstractClass(Kernel::class, ['', '']); + /** @var Kernel $mockKernel */ + $mockKernel = $this->getMockForAbstractClass(HttpKernelInterface::class); - return new FilterControllerEvent($mockKernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); + return new ControllerEvent($mockKernel, $controller, $request, HttpKernelInterface::MAIN_REQUEST); } /** - * Test Annotation Feature at class + * Test Annotation Feature at class. * - * @throws ReflectionException + * @throws \ReflectionException + * @throws Exception */ public function testFeatureAnnotationAtClass(): void { $this->expectException(NotFoundHttpException::class); $controller = new FooControllerFeatureAtClass(); - $this->event = $this->getFilterControllerEvent([$controller, 'barAction'], $this->request); + $this->event = $this->getControllerEvent($controller->barAction(...), $this->request); $this->listener->onKernelController($this->event); } /** - * Test Annotation Feature at method and class + * Test Annotation Feature at method and class. * - * @throws ReflectionException + * @throws \ReflectionException + * @throws Exception */ public function testFeatureAnnotationAtClassAndMethod(): void { $this->expectException(NotFoundHttpException::class); $controller = new FooControllerFeatureAtClassAndMethod(); - $this->event = $this->getFilterControllerEvent([$controller, 'barAction'], $this->request); + $this->event = $this->getControllerEvent($controller->barAction(...), $this->request); $this->listener->onKernelController($this->event); } /** - * Test Annotation Feature at __invoke method + * Test Annotation Feature at __invoke method. * - * @throws ReflectionException + * @throws \ReflectionException + * @throws Exception */ public function testFeatureAnnotationAtObjectInvoke(): void { @@ -176,15 +142,16 @@ public function testFeatureAnnotationAtObjectInvoke(): void $controller = new FooControllerFeatureAtInvoke(); - $this->event = $this->getFilterControllerEvent($controller, $this->request); + $this->event = $this->getControllerEvent($controller, $this->request); $this->listener->onKernelController($this->event); } /** - * Test Proxy extends class with Annotation Feature + * Test Proxy extends class with Annotation Feature. * - * @throws ReflectionException + * @throws \ReflectionException + * @throws Exception */ public function testFeatureProxyExtendsAnnotation(): void { @@ -192,19 +159,16 @@ public function testFeatureProxyExtendsAnnotation(): void $controller = new ProxyFooControllerFeatureAtClass(); - $this->event = $this->getFilterControllerEvent([$controller, 'barAction'], $this->request); + $this->event = $this->getControllerEvent([$controller, 'barAction'], $this->request); $this->listener->onKernelController($this->event); } /** - * @param callable $controller - * - * @throws ReflectionException - * @throws AnnotationException - * - * @dataProvider callableDataProvider + * @throws \ReflectionException + * @throws Exception */ + #[DataProvider('callableDataProvider')] public function testAvoidClosure(callable $controller): void { /** @var FeatureService&MockObject $featureService */ @@ -212,22 +176,19 @@ public function testAvoidClosure(callable $controller): void $featureService->expects($this->never())->method('has'); $listener = new ControllerListener( - new AnnotationReader(), $featureService ); - $event = $this->getFilterControllerEvent($controller, $this->request); + /** @psalm-suppress InvalidArgument */ + $event = $this->getControllerEvent($controller, $this->request); $listener->onKernelController($event); } - /** - * @return array - */ - public function callableDataProvider(): array + public static function callableDataProvider(): array { return [ - [static function () {return 'test';}] + [static fn () => 'test'], ]; } } diff --git a/Tests/EventListener/Fixture/FooControllerFeatureAtClass.php b/tests/EventListener/Fixture/FooControllerFeatureAtClass.php similarity index 58% rename from Tests/EventListener/Fixture/FooControllerFeatureAtClass.php rename to tests/EventListener/Fixture/FooControllerFeatureAtClass.php index 0b3f094..8895418 100644 --- a/Tests/EventListener/Fixture/FooControllerFeatureAtClass.php +++ b/tests/EventListener/Fixture/FooControllerFeatureAtClass.php @@ -1,12 +1,10 @@ - [ + 'testFeature' => [ 'voter' => 'AlwaysTrueVoter', - 'params' => [] - ] + 'params' => [], + ], ]; $default = [ 'voter' => null, - 'params' => [] + 'params' => [], ]; // Create service $service = new FeatureService($features, $default, $this->getRegistry()); - $this->assertTrue($service->has('testfeature')); - $this->assertFalse($service->has('unknownfeature')); + $this->assertTrue($service->has('testFeature')); + $this->assertFalse($service->has('unknownFeature')); } /** - * Test Default voter + * Test Default voter. */ public function testDefaultVoter(): void { // Define a feature $features = [ - 'testfeature' => [ + 'testFeature' => [ 'voter' => null, - 'params' => [] - ] + 'params' => [], + ], ]; $default = [ 'voter' => 'AlwaysTrueVoter', - 'params' => [] + 'params' => [], ]; // Create service $service = new FeatureService($features, $default, $this->getRegistry()); - $this->assertTrue($service->has('testfeature')); - $this->assertFalse($service->has('unknownfeature')); + $this->assertTrue($service->has('testFeature')); + $this->assertFalse($service->has('unknownFeature')); } /** - * Test new feature + * Test new feature. */ - public function testFeatureUnknownedVoter(): void + public function testFeatureUnknownVoter(): void { $this->expectException(VoterNotFoundException::class); $this->expectExceptionMessage('No voter with this alias: "testVoter" is registered'); // Define a feature $features = [ - 'testfeature' => [ + 'testFeature' => [ 'voter' => 'testVoter', - 'params' => [] - ] + 'params' => [], + ], ]; $default = [ 'voter' => null, - 'params' => [] + 'params' => [], ]; // Create service $service = new FeatureService($features, $default, $this->getRegistry()); - $service->has('testfeature'); + $service->has('testFeature'); } /** diff --git a/Tests/Twig/FeatureToggleExtensionTest.php b/tests/Twig/FeatureToggleExtensionTest.php similarity index 74% rename from Tests/Twig/FeatureToggleExtensionTest.php rename to tests/Twig/FeatureToggleExtensionTest.php index 6ae78ff..a5b2edf 100644 --- a/Tests/Twig/FeatureToggleExtensionTest.php +++ b/tests/Twig/FeatureToggleExtensionTest.php @@ -1,5 +1,4 @@ -getMockBuilder(FeatureService::class) - ->disableOriginalConstructor() - ->getMock(); + $service = $this->createMock(FeatureService::class); $service ->method('has') @@ -46,7 +47,8 @@ public function testCallable(): void $functions = $extension->getFunctions(); // Check if functions are returned as array - $this->assertIsArray($functions); + // removed because TokenParser is always returned in an array + // $this->assertIsArray($functions); // Check if the function is a twig function $this->assertInstanceOf(TwigFunction::class, $functions[0]); @@ -54,10 +56,9 @@ public function testCallable(): void $callable = $functions[0]->getCallable(); // Check if callable returns true for a known feature - $this->assertTrue($callable('testfeature')); + $this->assertTrue($callable('testFeature')); // Check if callable returns false for an unknown feature - $this->assertFalse($callable('unknownfeature')); - + $this->assertFalse($callable('unknownFeature')); } } diff --git a/Tests/Twig/FeatureToggleNodeTest.php b/tests/Twig/FeatureToggleNodeTest.php similarity index 76% rename from Tests/Twig/FeatureToggleNodeTest.php rename to tests/Twig/FeatureToggleNodeTest.php index e849eff..4657b16 100644 --- a/Tests/Twig/FeatureToggleNodeTest.php +++ b/tests/Twig/FeatureToggleNodeTest.php @@ -1,5 +1,4 @@ -expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); $voter = $this->getRatioVoter(0.5, true, false); $voter->pass(); } /** - * @dataProvider dataProvider - * * @param bool $hasSession */ + #[DataProvider('dataProvider')] public function testHighRatioVoterPass(bool $hasSession): void { $voter = $this->getRatioVoter(0.9, false, $hasSession); @@ -56,121 +54,114 @@ public function testHighRatioVoterPass(bool $hasSession): void } /** - * @dataProvider dataProvider - * * @param bool $hasSession */ + #[DataProvider('dataProvider')] public function testZeroRatioVoterPass(bool $hasSession): void { $voter = $this->getRatioVoter(0, false, $hasSession); $hits = $this->executeTestIteration($voter); - $this->assertEquals(0, $hits); + $this->assertSame(0, $hits); } /** - * @dataProvider dataProvider - * * @param bool $hasSession */ + #[DataProvider('dataProvider')] public function testOneRatioVoterPass(bool $hasSession): void { $voter = $this->getRatioVoter(1, false, $hasSession); $hits = $this->executeTestIteration($voter); - $this->assertEquals(100, $hits); + $this->assertSame(100, $hits); } /** - * Simple data provider for $hasSession - * - * @return array + * Simple data provider for $hasSession. */ - public function dataProvider(): array + public static function dataProvider(): \Iterator { - return [ - [true], - [false], - ]; + yield [true]; + yield [false]; } public function testStickyRatioVoterPass(): void { $voter = $this->getRatioVoter(0.5, true); $initialPass = $voter->pass(); - $this->stickyValues = ['_ecn_featuretoggle_ratiotest' => $initialPass]; + $this->stickyValues = ['_ecn_featuretoggle_ratioTest' => $initialPass]; if ($initialPass) { - $requiredHits = $this->executeTestIteration($voter) === 100; + $requiredHits = 100 === $this->executeTestIteration($voter); } else { - $requiredHits = $this->executeTestIteration($voter) === 0; + $requiredHits = 0 === $this->executeTestIteration($voter); } $this->assertTrue($requiredHits); } /** - * Callback for session stub + * Callback for session stub. * - * @param $key + * @param string $key * * @return bool */ - public function hasStickyCallback($key): bool + public function hasStickyCallback(string $key): bool { - return array_key_exists($key, $this->stickyValues); + return isset($this->stickyValues[$key]); } /** - * Callback for session stub + * Callback for session stub. * - * @param $key + * @param string $key * - * @return mixed|null + * @return bool|null */ - public function getStickyCallback($key) + public function getStickyCallback(string $key): bool|null { return $this->stickyValues[$key] ?? null; } - protected function getRatioVoter($ratio, $sticky = false, $hasSession = true): RatioVoter + protected function getRatioVoter(float $ratio, bool $sticky = false, bool $hasSession = true): RatioVoter { $session = null; - if($hasSession) - { + if ($hasSession) { // Create service stub /** @var Session&MockObject $session */ $session = $this->createMock(SessionInterface::class); - $session->method('get')->willReturnCallback([$this, 'getStickyCallback']); - $session->method('has')->willReturnCallback([$this, 'hasStickyCallback']); + $session->method('get')->willReturnCallback($this->getStickyCallback(...)); + $session->method('has')->willReturnCallback($this->hasStickyCallback(...)); } $params = ['ratio' => $ratio, 'sticky' => $sticky]; $voter = new RatioVoter($session); - $voter->setFeature('ratiotest'); + $voter->setFeature('ratioTest'); $voter->setParams($params); return $voter; } /** - * Executes the tests n time returning the number of passes + * Executes the tests n time returning the number of passes. * * @param RatioVoter $ratioVoter - * @param int $iterationCount * * @return int */ - private function executeTestIteration(RatioVoter $ratioVoter, $iterationCount = 100): int + private function executeTestIteration(RatioVoter $ratioVoter): int { $hits = 0; + $iterationCount = 100; - for ($i = 1; $i <= $iterationCount; $i++) { + for ($i = 1; $i <= $iterationCount; ++$i) { if ($ratioVoter->pass()) { - $hits++; + ++$hits; } } diff --git a/Tests/Voters/RequestHeaderVoterTest.php b/tests/Voters/RequestHeaderVoterTest.php similarity index 95% rename from Tests/Voters/RequestHeaderVoterTest.php rename to tests/Voters/RequestHeaderVoterTest.php index 847119d..44fc2c5 100644 --- a/Tests/Voters/RequestHeaderVoterTest.php +++ b/tests/Voters/RequestHeaderVoterTest.php @@ -1,5 +1,4 @@ -getRequestHeaderVoter(new RequestStack(), null); + $voter = $this->getRequestHeaderVoter(new RequestStack(), []); $this->assertFalse($voter->pass()); } - private function getRequestHeaderVoter(RequestStack $requestStack, $requestHeaders = null): RequestHeaderVoter + private function getRequestHeaderVoter(RequestStack $requestStack, array $requestHeaders = []): RequestHeaderVoter { $voter = new RequestHeaderVoter(); $voter->setRequest($requestStack); @@ -50,14 +49,14 @@ private function getRequestHeaderVoter(RequestStack $requestStack, $requestHeade public function testNoRequestHeadersProvided(): void { - $voter = $this->getRequestHeaderVoter($this->getFakeRequestStack(), null); + $voter = $this->getRequestHeaderVoter($this->getFakeRequestStack(), []); $this->assertFalse($voter->pass()); } - private function getFakeRequestStack($headers = []): RequestStack + private function getFakeRequestStack(array $headers = []): RequestStack { - $fakeRequest = Request::create('/', 'GET'); + $fakeRequest = Request::create('/'); $fakeRequest->headers->add($headers); $requestStack = new RequestStack(); diff --git a/Tests/Voters/ScheduleVoterTest.php b/tests/Voters/ScheduleVoterTest.php similarity index 74% rename from Tests/Voters/ScheduleVoterTest.php rename to tests/Voters/ScheduleVoterTest.php index 99897ae..e04a8b4 100644 --- a/Tests/Voters/ScheduleVoterTest.php +++ b/tests/Voters/ScheduleVoterTest.php @@ -1,5 +1,4 @@ -getScheduleVoter(null); + $voter = $this->getScheduleVoter(''); $this->assertTrue($voter->pass()); } - protected function getScheduleVoter($schedule): ScheduleVoter + protected function getScheduleVoter(string $schedule): ScheduleVoter { $voter = new ScheduleVoter(); $voter->setParams(['schedule' => $schedule]); @@ -45,14 +43,14 @@ public function testInvalidScheduleIsSet(): void public function testEarlierScheduleIsSet(): void { - $voter = $this->getScheduleVoter((new DateTime())->modify('-1 second')->format(DateTime::RSS)); + $voter = $this->getScheduleVoter((new \DateTime())->modify('-1 second')->format(\DateTimeInterface::RSS)); $this->assertTrue($voter->pass()); } public function testLaterScheduleIsSet(): void { - $voter = $this->getScheduleVoter((new DateTime())->modify('+1 second')->format(DateTime::RSS)); + $voter = $this->getScheduleVoter((new \DateTime())->modify('+1 second')->format(\DateTimeInterface::RSS)); $this->assertFalse($voter->pass()); } diff --git a/Tests/Voters/VoterRegistryTest.php b/tests/Voters/VoterRegistryTest.php similarity index 85% rename from Tests/Voters/VoterRegistryTest.php rename to tests/Voters/VoterRegistryTest.php index e0c11b2..06a15d7 100644 --- a/Tests/Voters/VoterRegistryTest.php +++ b/tests/Voters/VoterRegistryTest.php @@ -1,5 +1,4 @@ - */ class VoterRegistryTest extends TestCase { + /** + * @throws Exception + */ public function testAddVotersToRegistry(): void { // Mock a voter @@ -36,9 +39,11 @@ public function testAddVotersToRegistry(): void // Test for voters $this->assertSame($voterOne, $registry->getVoter('myFirstTestVoter')); $this->assertSame($voterTwo, $registry->getVoter('mySecondTestVoter')); - } + /** + * @throws Exception + */ public function testUnknownVoterException(): void { $this->expectException(VoterNotFoundException::class); @@ -49,6 +54,7 @@ public function testUnknownVoterException(): void $registry = new VoterRegistry(); $registry->addVoter($voterOne, 'myFirstTestVoter'); - $unknownVoter = $registry->getVoter('unknownVoter'); + // unknownVoter should throw VoterNotFoundException + $registry->getVoter('unknownVoter'); } } diff --git a/Tests/Voters/VoterTraitTest.php b/tests/Voters/VoterTraitTest.php similarity index 96% rename from Tests/Voters/VoterTraitTest.php rename to tests/Voters/VoterTraitTest.php index 9630e9a..5e5dab6 100644 --- a/Tests/Voters/VoterTraitTest.php +++ b/tests/Voters/VoterTraitTest.php @@ -1,5 +1,4 @@ -