diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b0d576..2f08985 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] ### Added - \#80 Support for multiple features in route definition @ajgarlag +- \#84 ContextResolveEvent to allow context customization when checking for a feature @ajgarlag ### Changed - \#82 Restore support for Symfony 4.4 LTS @ajgarlag diff --git a/src/Event/ContextResolveEvent.php b/src/Event/ContextResolveEvent.php new file mode 100644 index 0000000..98b8769 --- /dev/null +++ b/src/Event/ContextResolveEvent.php @@ -0,0 +1,50 @@ + + * @package Flagception\Bundle\FlagceptionBundle\Listener + */ +class ContextResolveEvent extends Event +{ + /** + * The feature + * + * @var string + */ + private $feature; + + /** + * The context + * + * @var Context + */ + private $context; + + public function __construct(string $feature, Context $context = null) + { + $this->feature = $feature; + $this->context = $context ?? new Context(); + } + + public function getFeature(): string + { + return $this->feature; + } + + public function getContext(): Context + { + return $this->context; + } + + public function setContext(Context $context): void + { + $this->context = $context; + } +} diff --git a/src/Listener/AnnotationSubscriber.php b/src/Listener/AnnotationSubscriber.php index 3989c31..3c57cbc 100644 --- a/src/Listener/AnnotationSubscriber.php +++ b/src/Listener/AnnotationSubscriber.php @@ -2,15 +2,17 @@ namespace Flagception\Bundle\FlagceptionBundle\Listener; +use Doctrine\Common\Annotations\Reader; use Flagception\Bundle\FlagceptionBundle\Annotations\Feature; +use Flagception\Bundle\FlagceptionBundle\Event\ContextResolveEvent; use Flagception\Manager\FeatureManagerInterface; -use Doctrine\Common\Annotations\Reader; use ReflectionClass; use ReflectionException; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Class AnnotationSubscriber @@ -34,16 +36,28 @@ class AnnotationSubscriber implements EventSubscriberInterface */ private $manager; + /** + * The event dispatcher + * + * @var ?EventDispatcherInterface + */ + private $eventDispatcher; + /** * FeatureListener constructor. * * @param Reader $reader * @param FeatureManagerInterface $manager + * @param EventDispatcherInterface $eventDispatcher */ - public function __construct(Reader $reader, FeatureManagerInterface $manager) - { + public function __construct( + Reader $reader, + FeatureManagerInterface $manager, + EventDispatcherInterface $eventDispatcher = null + ) { $this->reader = $reader; $this->manager = $manager; + $this->eventDispatcher = $eventDispatcher; } /** @@ -73,10 +87,18 @@ public function onKernelController(ControllerEvent $event) return; } + + $object = new ReflectionClass($controller[0]); foreach ($this->reader->getClassAnnotations($object) as $annotation) { if ($annotation instanceof Feature) { - if (!$this->manager->isActive($annotation->name)) { + $context = null; + if (null !== $this->eventDispatcher) { + $contextEvent = $this->eventDispatcher->dispatch(new ContextResolveEvent($annotation->name)); + $context = $contextEvent->getContext(); + } + + if (!$this->manager->isActive($annotation->name, $context)) { throw new NotFoundHttpException('Feature for this class is not active.'); } } @@ -84,8 +106,13 @@ public function onKernelController(ControllerEvent $event) $method = $object->getMethod($controller[1]); foreach ($this->reader->getMethodAnnotations($method) as $annotation) { + $context = null; + if (null !== $this->eventDispatcher) { + $contextEvent = $this->eventDispatcher->dispatch(new ContextResolveEvent($annotation->name)); + $context = $contextEvent->getContext(); + } if ($annotation instanceof Feature) { - if (!$this->manager->isActive($annotation->name)) { + if (!$this->manager->isActive($annotation->name, $context)) { throw new NotFoundHttpException('Feature for this method is not active.'); } } diff --git a/src/Listener/RoutingMetadataSubscriber.php b/src/Listener/RoutingMetadataSubscriber.php index abd8a4e..c6ed484 100644 --- a/src/Listener/RoutingMetadataSubscriber.php +++ b/src/Listener/RoutingMetadataSubscriber.php @@ -2,11 +2,13 @@ namespace Flagception\Bundle\FlagceptionBundle\Listener; +use Flagception\Bundle\FlagceptionBundle\Event\ContextResolveEvent; use Flagception\Manager\FeatureManagerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Class RoutingMetadataSubscriber @@ -30,14 +32,23 @@ class RoutingMetadataSubscriber implements EventSubscriberInterface */ private $manager; + /** + * The event dispatcher + * + * @var ?EventDispatcherInterface + */ + private $eventDispatcher; + /** * RoutingMetadataSubscriber constructor. * * @param FeatureManagerInterface $manager + * @param EventDispatcherInterface $eventDispatcher */ - public function __construct(FeatureManagerInterface $manager) + public function __construct(FeatureManagerInterface $manager, EventDispatcherInterface $eventDispatcher = null) { $this->manager = $manager; + $this->eventDispatcher = $eventDispatcher; } /** @@ -56,7 +67,12 @@ public function onKernelController(ControllerEvent $event) $featureNames = (array) $event->getRequest()->attributes->get(static::FEATURE_KEY); foreach ($featureNames as $featureName) { - if (!$this->manager->isActive($featureName)) { + $context = null; + if (null !== $this->eventDispatcher) { + $contextEvent = $this->eventDispatcher->dispatch(new ContextResolveEvent($featureName)); + $context = $contextEvent->getContext(); + } + if (!$this->manager->isActive($featureName, $context)) { throw new NotFoundHttpException('Feature for this class is not active.'); } } diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index d8af1cd..790f691 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -17,6 +17,7 @@ services: class: Flagception\Bundle\FlagceptionBundle\Twig\ToggleExtension arguments: - '@flagception.manager.feature_manager' + - '@event_dispatcher' tags: - { name: twig.extension } public: false @@ -69,6 +70,7 @@ services: arguments: - '@annotations.reader' - '@flagception.manager.feature_manager' + - '@event_dispatcher' tags: - { name: kernel.event_subscriber } public: true @@ -78,6 +80,7 @@ services: class: Flagception\Bundle\FlagceptionBundle\Listener\RoutingMetadataSubscriber arguments: - '@flagception.manager.feature_manager' + - '@event_dispatcher' tags: - { name: kernel.event_subscriber } public: true diff --git a/src/Twig/ToggleExtension.php b/src/Twig/ToggleExtension.php index 6141b43..a338e8e 100644 --- a/src/Twig/ToggleExtension.php +++ b/src/Twig/ToggleExtension.php @@ -2,8 +2,10 @@ namespace Flagception\Bundle\FlagceptionBundle\Twig; +use Flagception\Bundle\FlagceptionBundle\Event\ContextResolveEvent; use Flagception\Manager\FeatureManagerInterface; use Flagception\Model\Context; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; use Twig\TwigTest; @@ -23,14 +25,23 @@ class ToggleExtension extends AbstractExtension */ private $manager; + /** + * The event dispatcher + * + * @var ?EventDispatcherInterface + */ + private $eventDispatcher; + /** * ToggleExtension constructor. * * @param FeatureManagerInterface $manager + * @param EventDispatcherInterface $eventDispatcher */ - public function __construct(FeatureManagerInterface $manager) + public function __construct(FeatureManagerInterface $manager, EventDispatcherInterface $eventDispatcher = null) { $this->manager = $manager; + $this->eventDispatcher = $eventDispatcher; } /** @@ -48,6 +59,11 @@ public function isActive($name, array $contextValues = []) $context->add($contextKey, $contextValue); } + if (null !== $this->eventDispatcher) { + $contextEvent = $this->eventDispatcher->dispatch(new ContextResolveEvent($name, $context)); + $context = $contextEvent->getContext(); + } + return $this->manager->isActive($name, $context); } diff --git a/tests/Listener/AnnotationSubscriberTest.php b/tests/Listener/AnnotationSubscriberTest.php index fbd4181..80f58e3 100644 --- a/tests/Listener/AnnotationSubscriberTest.php +++ b/tests/Listener/AnnotationSubscriberTest.php @@ -3,10 +3,13 @@ namespace Flagception\Tests\FlagceptionBundle\Listener; use Doctrine\Common\Annotations\AnnotationReader; +use Flagception\Bundle\FlagceptionBundle\Event\ContextResolveEvent; use Flagception\Bundle\FlagceptionBundle\Listener\AnnotationSubscriber; use Flagception\Manager\FeatureManagerInterface; +use Flagception\Model\Context; use Flagception\Tests\FlagceptionBundle\Fixtures\Helper\AnnotationTestClass; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerEvent; @@ -153,6 +156,37 @@ public function testOnMethodIsInactive() $subscriber->onKernelController($event); } + /** + * Test event is dispatched when event dispatcher exists + * + * @return void + */ + public function testEventIsDispatchedWhenEventDispatcherExists() + { + $context = new Context(); + $manager = $this->createMock(FeatureManagerInterface::class); + $manager->method('isActive')->with('feature_abc', $context)->willReturn(true); + + $listener = function (ContextResolveEvent $event) use ($context) { + $this->assertSame('feature_abc', $event->getFeature()); + $this->assertNotSame($event->getContext(), $context); + $event->setContext($context); + $this->assertSame($event->getContext(), $context); + return $event; + }; + + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addListener(ContextResolveEvent::class, $listener); + + $event = $this->createControllerEvent([ + new AnnotationTestClass(), + 'normalMethod' + ]); + + $subscriber = new AnnotationSubscriber(new AnnotationReader(), $manager, $eventDispatcher); + $subscriber->onKernelController($event); + } + /** * Create ControllerEvent * diff --git a/tests/Listener/RoutingMetadataSubscriberTest.php b/tests/Listener/RoutingMetadataSubscriberTest.php index f7fb9c1..22be856 100644 --- a/tests/Listener/RoutingMetadataSubscriberTest.php +++ b/tests/Listener/RoutingMetadataSubscriberTest.php @@ -2,9 +2,12 @@ namespace Flagception\Tests\FlagceptionBundle\Listener; +use Flagception\Bundle\FlagceptionBundle\Event\ContextResolveEvent; use Flagception\Bundle\FlagceptionBundle\Listener\RoutingMetadataSubscriber; use Flagception\Manager\FeatureManagerInterface; +use Flagception\Model\Context; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -184,4 +187,38 @@ function () { $subscriber = new RoutingMetadataSubscriber($manager); $subscriber->onKernelController($event); } + + /** + * Test event is dispatched when event dispatcher exists + * + * @return void + */ + public function testEventIsDispatchedWhenEventDispatcherExists() + { + $context = new Context(); + + $request = new Request([], [], ['_feature' => 'feature_abc']); + $event = $this->createControllerEvent($request); + + $manager = $this->createMock(FeatureManagerInterface::class); + $manager + ->expects(static::once()) + ->method('isActive') + ->with('feature_abc', $context) + ->willReturn(true); + + $listener = function (ContextResolveEvent $event) use ($context) { + $this->assertSame('feature_abc', $event->getFeature()); + $this->assertNotSame($event->getContext(), $context); + $event->setContext($context); + $this->assertSame($event->getContext(), $context); + return $event; + }; + + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addListener(ContextResolveEvent::class, $listener); + + $subscriber = new RoutingMetadataSubscriber($manager, $eventDispatcher); + $subscriber->onKernelController($event); + } } diff --git a/tests/Twig/ToggleExtensionTest.php b/tests/Twig/ToggleExtensionTest.php index a34e137..7334a99 100644 --- a/tests/Twig/ToggleExtensionTest.php +++ b/tests/Twig/ToggleExtensionTest.php @@ -2,10 +2,12 @@ namespace Flagception\Tests\FlagceptionBundle\Twig; +use Flagception\Bundle\FlagceptionBundle\Event\ContextResolveEvent; +use Flagception\Bundle\FlagceptionBundle\Twig\ToggleExtension; use Flagception\Manager\FeatureManagerInterface; use Flagception\Model\Context; -use Flagception\Bundle\FlagceptionBundle\Twig\ToggleExtension; use PHPUnit\Framework\TestCase; +use Symfony\Component\EventDispatcher\EventDispatcher; /** * Class ToggleExtensionTest @@ -75,4 +77,37 @@ public function testGetName() $extension = new ToggleExtension($this->createMock(FeatureManagerInterface::class)); static::assertEquals('flagception', $extension->getName()); } + + /** + * Test event is dispatched when event dispatcher exists + * + * @return void + */ + public function testEventIsDispatchedWhenEventDispatcherExists() + { + $context = new Context(); + + $listener = function (ContextResolveEvent $event) use ($context) { + $this->assertSame('feature_foo', $event->getFeature()); + $this->assertNotSame($event->getContext(), $context); + $event->setContext($context); + $this->assertSame($event->getContext(), $context); + return $event; + }; + + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addListener(ContextResolveEvent::class, $listener); + + $extension = new ToggleExtension( + $manager = $this->createMock(FeatureManagerInterface::class), + $eventDispatcher + ); + + $manager + ->method('isActive') + ->with('feature_foo', $context) + ->willReturn(true); + + static::assertEquals(true, $extension->isActive('feature_foo')); + } }