From f017f2e4e7336e897a95d8867dcc87c433c7b037 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 20 Jun 2020 05:04:20 +1000 Subject: [PATCH 01/13] Drop ApplicationInterface ApplicationInterface was never properly utilized and some parts of the MVC application are implicitly depending on Laminas\Mvc\Application. It is not type safe and with the general direction towards dependency injection with container-exists-first approach there is hardly any value in keeping the interface. Signed-off-by: Aleksei Khudiakov --- src/Application.php | 1 - src/ApplicationInterface.php | 41 -------------------------- src/MvcEvent.php | 6 ++-- test/Controller/Plugin/ForwardTest.php | 10 +++---- 4 files changed, 8 insertions(+), 50 deletions(-) delete mode 100644 src/ApplicationInterface.php diff --git a/src/Application.php b/src/Application.php index b9e11fc2..86850de5 100644 --- a/src/Application.php +++ b/src/Application.php @@ -40,7 +40,6 @@ * can simply extend the class to override such behavior. */ class Application implements - ApplicationInterface, EventManagerAwareInterface { public const ERROR_CONTROLLER_CANNOT_DISPATCH = 'error-controller-cannot-dispatch'; diff --git a/src/ApplicationInterface.php b/src/ApplicationInterface.php deleted file mode 100644 index 0210434f..00000000 --- a/src/ApplicationInterface.php +++ /dev/null @@ -1,41 +0,0 @@ -setParam('application', $application); $this->application = $application; @@ -61,7 +61,7 @@ public function setApplication(ApplicationInterface $application) /** * Get application instance * - * @return ApplicationInterface + * @return Application */ public function getApplication() { diff --git a/test/Controller/Plugin/ForwardTest.php b/test/Controller/Plugin/ForwardTest.php index 7cf4a9a9..942e6ea3 100644 --- a/test/Controller/Plugin/ForwardTest.php +++ b/test/Controller/Plugin/ForwardTest.php @@ -9,7 +9,7 @@ use Laminas\EventManager\SharedEventManagerInterface; use Laminas\Http\Request; use Laminas\Http\Response; -use Laminas\Mvc\ApplicationInterface; +use Laminas\Mvc\Application; use Laminas\Mvc\Controller\ControllerManager; use Laminas\Mvc\Controller\Plugin\Forward; use Laminas\Mvc\Controller\Plugin\Forward as ForwardPlugin; @@ -45,7 +45,7 @@ class ForwardTest extends TestCase public function setUp(): void { $eventManager = $this->createEventManager(new SharedEventManager()); - $mockApplication = $this->createMock(ApplicationInterface::class); + $mockApplication = $this->createMock(Application::class); $mockApplication->expects($this->any())->method('getEventManager')->will($this->returnValue($eventManager)); $event = new MvcEvent(); @@ -202,7 +202,7 @@ static function ($e): void { ]); // @codingStandardsIgnoreEnd $events = $this->createEventManager($sharedEvents); - $application = $this->createMock(ApplicationInterface::class); + $application = $this->createMock(Application::class); $application->method('getEventManager')->willReturn($events); $event = $this->controller->getEvent(); $event->setApplication($application); @@ -234,7 +234,7 @@ public function testProblemListenersAreDetachedAndReattachedWhenPluginDispatches ->willReturn([-50 => [$myCallback]]); $events = $this->createEventManager($sharedEvents); - $application = $this->createMock(ApplicationInterface::class); + $application = $this->createMock(Application::class); $application->method('getEventManager')->willReturn($events); $event = $this->controller->getEvent(); $event->setApplication($application); @@ -269,7 +269,7 @@ public function testInvokableProblemListenersAreDetachedAndReattachedWhenPluginD ->willReturn([-50 => [$myCallback]]); $events = $this->createEventManager($sharedEvents); - $application = $this->createMock(ApplicationInterface::class); + $application = $this->createMock(Application::class); $application->method('getEventManager')->willReturn($events); $event = $this->controller->getEvent(); $event->setApplication($application); From 6b7cd1cd1c2fae007ab6d66f2881425958e73c2e Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 20 Jun 2020 05:48:01 +1000 Subject: [PATCH 02/13] Downgrade Application to EventsCapableInterface Signed-off-by: Aleksei Khudiakov --- src/Application.php | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/Application.php b/src/Application.php index 86850de5..bc236d24 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,8 +4,8 @@ namespace Laminas\Mvc; -use Laminas\EventManager\EventManagerAwareInterface; use Laminas\EventManager\EventManagerInterface; +use Laminas\EventManager\EventsCapableInterface; use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\RequestInterface; use Laminas\Stdlib\ResponseInterface; @@ -39,8 +39,7 @@ * if you wish to setup your own listeners and/or workflow; alternately, you * can simply extend the class to override such behavior. */ -class Application implements - EventManagerAwareInterface +class Application implements EventsCapableInterface { public const ERROR_CONTROLLER_CANNOT_DISPATCH = 'error-controller-cannot-dispatch'; public const ERROR_CONTROLLER_NOT_FOUND = 'error-controller-not-found'; @@ -68,8 +67,7 @@ class Application implements */ protected $event; - /** @var EventManagerInterface */ - protected $events; + protected EventManagerInterface $events; /** @var RequestInterface */ protected $request; @@ -79,11 +77,11 @@ class Application implements public function __construct( protected ServiceManager $serviceManager, - ?EventManagerInterface $events = null, + EventManagerInterface $events, ?RequestInterface $request = null, ?ResponseInterface $response = null ) { - $this->setEventManager($events ?: $serviceManager->get('EventManager')); + $this->setEventManager($events); $this->request = $request ?: $serviceManager->get('Request'); $this->response = $response ?: $serviceManager->get('Response'); @@ -173,27 +171,17 @@ public function getMvcEvent() /** * Set the event manager instance - * - * @return Application */ - public function setEventManager(EventManagerInterface $eventManager) + protected function setEventManager(EventManagerInterface $eventManager): void { $eventManager->setIdentifiers([ self::class, static::class, ]); $this->events = $eventManager; - return $this; } - /** - * Retrieve the event manager - * - * Lazy-loads an EventManager instance if none registered. - * - * @return EventManagerInterface - */ - public function getEventManager() + public function getEventManager(): EventManagerInterface { return $this->events; } From 1a79dc825c376e91001010cce16ed3a4766b1505 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 16 Mar 2023 06:13:47 +1000 Subject: [PATCH 03/13] Extract Application default listeners to dedicated listener provider Use double-dispatch approach to ensure default listeners are registered. Signed-off-by: Aleksei Khudiakov --- src/Application.php | 19 +- src/ApplicationListenersProvider.php | 91 +++++++ src/ConfigProvider.php | 67 ++--- src/Service/ApplicationFactory.php | 33 +-- .../ApplicationListenersProviderFactory.php | 31 +++ test/ApplicationListenersProviderTest.php | 234 ++++++++++++++++++ 6 files changed, 401 insertions(+), 74 deletions(-) create mode 100644 src/ApplicationListenersProvider.php create mode 100644 src/Service/ApplicationListenersProviderFactory.php create mode 100644 test/ApplicationListenersProviderTest.php diff --git a/src/Application.php b/src/Application.php index bc236d24..6eedf2c8 100644 --- a/src/Application.php +++ b/src/Application.php @@ -47,19 +47,6 @@ class Application implements EventsCapableInterface public const ERROR_EXCEPTION = 'error-exception'; public const ERROR_ROUTER_NO_MATCH = 'error-router-no-match'; - /** - * Default application event listeners - * - * @var array - */ - protected $defaultListeners = [ - 'RouteListener', - 'DispatchListener', - 'HttpMethodListener', - 'ViewManager', - 'SendResponseListener', - ]; - /** * MVC event token * @@ -78,16 +65,14 @@ class Application implements EventsCapableInterface public function __construct( protected ServiceManager $serviceManager, EventManagerInterface $events, + ApplicationListenersProvider $listenersProvider, ?RequestInterface $request = null, ?ResponseInterface $response = null ) { $this->setEventManager($events); + $listenersProvider->registerListeners($this); $this->request = $request ?: $serviceManager->get('Request'); $this->response = $response ?: $serviceManager->get('Response'); - - foreach ($this->defaultListeners as $listener) { - $serviceManager->get($listener)->attach($this->events); - } } /** diff --git a/src/ApplicationListenersProvider.php b/src/ApplicationListenersProvider.php new file mode 100644 index 00000000..6564c5c3 --- /dev/null +++ b/src/ApplicationListenersProvider.php @@ -0,0 +1,91 @@ + $listeners + */ + private function __construct(private readonly ContainerInterface $container, private readonly array $listeners) + { + } + + /** + * @param array $extraListeners + */ + public static function withDefaultListeners(ContainerInterface $container, array $extraListeners): self + { + return new self( + $container, + array_unique(array_merge(self::DEFAULT_LISTENERS, $extraListeners), SORT_REGULAR) + ); + } + + /** + * @param array $extraListeners + */ + public static function withoutDefaultListeners(ContainerInterface $container, array $extraListeners): self + { + return new self( + $container, + array_unique($extraListeners, SORT_REGULAR) + ); + } + + /** + * @return array + */ + public function getListeners(): array + { + return $this->listeners; + } + + public function registerListeners(Application $application): void + { + $events = $application->getEventManager(); + foreach ($this->listeners as $listener) { + $msg = ''; + if (is_string($listener)) { + $msg = sprintf(' with container id "%s"', $listener); + /** @var mixed $listener */ + $listener = $this->container->get($listener); + } + if (! $listener instanceof ListenerAggregateInterface) { + throw new DomainException(sprintf( + 'Application listener%s expected to be instance of %s, %s given', + $msg, + ListenerAggregateInterface::class, + get_debug_type($listener) + )); + } + + $listener->attach($events); + } + } +} diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 3f4164fa..05e55fb5 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -14,6 +14,7 @@ use Laminas\Mvc\RouteListener; use Laminas\Mvc\SendResponseListener; use Laminas\Mvc\Service\ApplicationFactory; +use Laminas\Mvc\Service\ApplicationListenersProviderFactory; use Laminas\Mvc\Service\ControllerManagerFactory; use Laminas\Mvc\Service\ControllerPluginManagerFactory; use Laminas\Mvc\Service\DispatchListenerFactory; @@ -59,7 +60,10 @@ class ConfigProvider public function __invoke(): array { return [ - 'dependencies' => $this->getDependencies(), + 'dependencies' => $this->getDependencies(), + Application::class => [ + 'listeners' => [], + ], ]; } @@ -97,36 +101,37 @@ public function getDependencies(): array ControllerManager::class => 'ControllerManager', ], 'factories' => [ - 'EventManager' => EventManagerFactory::class, - 'SharedEventManager' => static fn() => new SharedEventManager(), - 'Application' => ApplicationFactory::class, - 'ControllerManager' => ControllerManagerFactory::class, - 'ControllerPluginManager' => ControllerPluginManagerFactory::class, - 'DispatchListener' => DispatchListenerFactory::class, - 'HttpExceptionStrategy' => HttpExceptionStrategyFactory::class, - 'HttpMethodListener' => HttpMethodListenerFactory::class, - 'HttpRouteNotFoundStrategy' => HttpRouteNotFoundStrategyFactory::class, - 'HttpViewManager' => HttpViewManagerFactory::class, - 'InjectTemplateListener' => InjectTemplateListenerFactory::class, - 'PaginatorPluginManager' => PaginatorPluginManagerFactory::class, - 'Request' => RequestFactory::class, - 'Response' => ResponseFactory::class, - 'ViewHelperManager' => ViewHelperManagerFactory::class, - DefaultRenderingStrategy::class => HttpDefaultRenderingStrategyFactory::class, - 'ViewFeedStrategy' => ViewFeedStrategyFactory::class, - 'ViewJsonStrategy' => ViewJsonStrategyFactory::class, - 'ViewManager' => ViewManagerFactory::class, - 'ViewResolver' => ViewResolverFactory::class, - 'ViewTemplateMapResolver' => ViewTemplateMapResolverFactory::class, - 'ViewTemplatePathStack' => ViewTemplatePathStackFactory::class, - 'ViewPrefixPathStackResolver' => ViewPrefixPathStackResolverFactory::class, - RouteListener::class => InvokableFactory::class, - SendResponseListener::class => SendResponseListenerFactory::class, - FeedRenderer::class => InvokableFactory::class, - JsonRenderer::class => InvokableFactory::class, - PhpRenderer::class => ViewPhpRendererFactory::class, - PhpRendererStrategy::class => ViewPhpRendererStrategyFactory::class, - View::class => ViewFactory::class, + 'EventManager' => EventManagerFactory::class, + 'SharedEventManager' => static fn() => new SharedEventManager(), + 'Application' => ApplicationFactory::class, + 'ControllerManager' => ControllerManagerFactory::class, + 'ControllerPluginManager' => ControllerPluginManagerFactory::class, + 'DispatchListener' => DispatchListenerFactory::class, + 'HttpExceptionStrategy' => HttpExceptionStrategyFactory::class, + 'HttpMethodListener' => HttpMethodListenerFactory::class, + 'HttpRouteNotFoundStrategy' => HttpRouteNotFoundStrategyFactory::class, + 'HttpViewManager' => HttpViewManagerFactory::class, + 'InjectTemplateListener' => InjectTemplateListenerFactory::class, + 'PaginatorPluginManager' => PaginatorPluginManagerFactory::class, + 'Request' => RequestFactory::class, + 'Response' => ResponseFactory::class, + 'ViewHelperManager' => ViewHelperManagerFactory::class, + DefaultRenderingStrategy::class => HttpDefaultRenderingStrategyFactory::class, + 'ViewFeedStrategy' => ViewFeedStrategyFactory::class, + 'ViewJsonStrategy' => ViewJsonStrategyFactory::class, + 'ViewManager' => ViewManagerFactory::class, + 'ViewResolver' => ViewResolverFactory::class, + 'ViewTemplateMapResolver' => ViewTemplateMapResolverFactory::class, + 'ViewTemplatePathStack' => ViewTemplatePathStackFactory::class, + 'ViewPrefixPathStackResolver' => ViewPrefixPathStackResolverFactory::class, + ApplicationListenersProvider::class => ApplicationListenersProviderFactory::class, + RouteListener::class => InvokableFactory::class, + SendResponseListener::class => SendResponseListenerFactory::class, + FeedRenderer::class => InvokableFactory::class, + JsonRenderer::class => InvokableFactory::class, + PhpRenderer::class => ViewPhpRendererFactory::class, + PhpRendererStrategy::class => ViewPhpRendererStrategyFactory::class, + View::class => ViewFactory::class, ], 'shared' => [ 'EventManager' => false, diff --git a/src/Service/ApplicationFactory.php b/src/Service/ApplicationFactory.php index 879b141b..6fe1d663 100644 --- a/src/Service/ApplicationFactory.php +++ b/src/Service/ApplicationFactory.php @@ -4,40 +4,21 @@ namespace Laminas\Mvc\Service; +use Laminas\EventManager\EventManagerInterface; use Laminas\Mvc\Application; -use Laminas\ServiceManager\Factory\FactoryInterface; +use Laminas\Mvc\ApplicationListenersProvider; use Psr\Container\ContainerInterface; -class ApplicationFactory implements FactoryInterface +final class ApplicationFactory { - /** - * Create the Application service - * - * Creates a Laminas\Mvc\Application service, passing it the configuration - * service and the service manager instance. - * - * @param string $name - * @param null|array $options - * @return Application - */ - public function __invoke(ContainerInterface $container, $name, ?array $options = null) + public function __invoke(ContainerInterface $container): Application { - $application = new Application( + return new Application( $container, - $container->get('EventManager'), + $container->get(EventManagerInterface::class), + $container->get(ApplicationListenersProvider::class), $container->get('Request'), $container->get('Response') ); - - if (! $container->has('config')) { - return $application; - } - - $em = $application->getEventManager(); - $listeners = $container->get('config')[Application::class]['listeners'] ?? []; - foreach ($listeners as $listener) { - $container->get($listener)->attach($em); - } - return $application; } } diff --git a/src/Service/ApplicationListenersProviderFactory.php b/src/Service/ApplicationListenersProviderFactory.php new file mode 100644 index 00000000..75a8f075 --- /dev/null +++ b/src/Service/ApplicationListenersProviderFactory.php @@ -0,0 +1,31 @@ +get('config'); + assert(is_array($config)); + + /** @psalm-var list $listeners */ + $listeners = $config[Application::class]['listeners'] ?? []; + return ApplicationListenersProvider::withDefaultListeners($container, $listeners); + } +} diff --git a/test/ApplicationListenersProviderTest.php b/test/ApplicationListenersProviderTest.php new file mode 100644 index 00000000..f2d1a93d --- /dev/null +++ b/test/ApplicationListenersProviderTest.php @@ -0,0 +1,234 @@ +getListeners(); + foreach (ApplicationListenersProvider::DEFAULT_LISTENERS as $listener) { + self::assertContains($listener, $listeners); + } + } + + public function testWithDefaultListenersAndExtraAddsDefaults(): void + { + $container = self::createStub(ContainerInterface::class); + + $listenerProvider = ApplicationListenersProvider::withDefaultListeners($container, [ + 'ExtraListener', + ]); + + $listeners = $listenerProvider->getListeners(); + foreach (ApplicationListenersProvider::DEFAULT_LISTENERS as $listener) { + self::assertContains($listener, $listeners); + } + + self::assertContains('ExtraListener', $listeners); + } + + public function testWithoutDefaultListenersDoesNotHaveDefaults(): void + { + $container = self::createStub(ContainerInterface::class); + $listenerProvider = ApplicationListenersProvider::withoutDefaultListeners($container, []); + + $listeners = $listenerProvider->getListeners(); + foreach (ApplicationListenersProvider::DEFAULT_LISTENERS as $listener) { + self::assertNotContains($listener, $listeners); + } + } + + public function testWithoutDefaultListenersAndWithExtraDoesNotHaveDefaults(): void + { + $container = self::createStub(ContainerInterface::class); + + $listenerProvider = ApplicationListenersProvider::withoutDefaultListeners($container, [ + 'ExtraListener', + ]); + + $listeners = $listenerProvider->getListeners(); + foreach (ApplicationListenersProvider::DEFAULT_LISTENERS as $listener) { + self::assertNotContains($listener, $listeners); + } + + self::assertContains('ExtraListener', $listeners); + } + + public function testRemovesDuplicatesWithDefaults(): void + { + $container = self::createStub(ContainerInterface::class); + $listenerAggregate = self::createStub(ListenerAggregateInterface::class); + + $listenerProvider = ApplicationListenersProvider::withDefaultListeners($container, [ + ApplicationListenersProvider::DEFAULT_LISTENERS[0], + 'ExtraListener', + $listenerAggregate, + 'ExtraListener', + $listenerAggregate, + ]); + + $listeners = $listenerProvider->getListeners(); + self::assertSame(array_unique($listeners, SORT_REGULAR), $listeners); + } + + public function testRemovesDuplicatesWithoutDefaults(): void + { + $container = self::createStub(ContainerInterface::class); + $listenerAggregate = self::createStub(ListenerAggregateInterface::class); + + $listenerProvider = ApplicationListenersProvider::withoutDefaultListeners($container, [ + 'ExtraListener', + $listenerAggregate, + 'ExtraListener', + $listenerAggregate, + ]); + + $listeners = $listenerProvider->getListeners(); + self::assertSame(array_unique($listeners, SORT_REGULAR), $listeners); + } + + public function testDoesNotPullFromContainerUntilAttaching(): void + { + $container = $this->createMock(ContainerInterface::class); + $container->expects(self::never()) + ->method('get'); + + $listenerProvider = ApplicationListenersProvider::withDefaultListeners($container, [ + 'ExtraListener', + ]); + self::assertNotEmpty($listenerProvider->getListeners()); + + $listenerProvider = ApplicationListenersProvider::withoutDefaultListeners($container, [ + 'ExtraListener', + ]); + self::assertNotEmpty($listenerProvider->getListeners()); + } + + public function testRegistersListeners(): void + { + $container = self::createStub(ContainerInterface::class); + + $application = self::createStub(Application::class); + $events = self::createStub(EventManagerInterface::class); + $application->method('getEventManager') + ->willReturn($events); + + $listenerMap = []; + foreach (ApplicationListenersProvider::DEFAULT_LISTENERS as $listener) { + $listenerMock = $this->createMock(ListenerAggregateInterface::class); + $listenerMock->expects(self::once()) + ->method('attach') + ->with($events); + $listenerMap[] = [$listener, $listenerMock]; + } + $listenerMock = $this->createMock(ListenerAggregateInterface::class); + $listenerMock->expects(self::once()) + ->method('attach') + ->with($events); + $listenerMap[] = ['ExtraListener', $listenerMock]; + $container->method('get') + ->willReturnMap($listenerMap); + + $listenerInstanceMock = $this->createMock(ListenerAggregateInterface::class); + $listenerInstanceMock->expects(self::once()) + ->method('attach') + ->with($events); + + $listenerProvider = ApplicationListenersProvider::withDefaultListeners($container, [ + 'ExtraListener', + $listenerInstanceMock, + ]); + + $listenerProvider->registerListeners($application); + } + + /** + * @return array + */ + public static function invalidListenersProvider(): array + { + return [ + 'closure' => [fn () => null], + 'object' => [new stdClass()], + 'int' => [1], + ]; + } + + /** + * @dataProvider invalidListenersProvider + */ + public function testRejectsInvalidListeners(mixed $listener): void + { + $container = self::createStub(ContainerInterface::class); + + $application = self::createStub(Application::class); + $events = self::createStub(EventManagerInterface::class); + $application->method('getEventManager') + ->willReturn($events); + + /** + * @psalm-suppress MixedArgumentTypeCoercion intentional check of invalid inputs + */ + $listenerProvider = ApplicationListenersProvider::withoutDefaultListeners($container, [ + $listener, + ]); + + self::expectException(DomainException::class); + self::expectExceptionMessage(sprintf( + 'Application listener expected to be instance of %s, %s given', + ListenerAggregateInterface::class, + get_debug_type($listener), + )); + $listenerProvider->registerListeners($application); + } + + /** + * @dataProvider invalidListenersProvider + */ + public function testRejectsInvalidListenersFromContainer(mixed $listener): void + { + $container = self::createStub(ContainerInterface::class); + $container->method('get') + ->willReturnMap([['InvalidListener', $listener]]); + + $application = self::createStub(Application::class); + $events = self::createStub(EventManagerInterface::class); + $application->method('getEventManager') + ->willReturn($events); + + $listenerProvider = ApplicationListenersProvider::withoutDefaultListeners($container, ['InvalidListener']); + + self::expectException(DomainException::class); + self::expectExceptionMessage(sprintf( + 'Application listener with container id "%s" expected to be instance of %s, %s given', + 'InvalidListener', + ListenerAggregateInterface::class, + get_debug_type($listener), + )); + $listenerProvider->registerListeners($application); + } +} From 4321411eb114881634b11c945dcf726718a6ffac Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 16 Mar 2023 06:16:49 +1000 Subject: [PATCH 04/13] Provide bare minimum tests update for extracted default listeners for Application. Signed-off-by: Aleksei Khudiakov --- src/ApplicationListenersProvider.php | 5 +- test/Application/BadControllerTrait.php | 34 +++++----- .../InvalidControllerTypeTrait.php | 36 +++++----- test/Application/MissingControllerTrait.php | 39 ++++++----- test/Application/PathControllerTrait.php | 33 +++++----- test/ApplicationTest.php | 65 ++++++------------- 6 files changed, 93 insertions(+), 119 deletions(-) diff --git a/src/ApplicationListenersProvider.php b/src/ApplicationListenersProvider.php index 6564c5c3..ec2973c8 100644 --- a/src/ApplicationListenersProvider.php +++ b/src/ApplicationListenersProvider.php @@ -17,7 +17,10 @@ use const SORT_REGULAR; /** - * Provides lazy container + * Double-dispatch listener provider for Application to ensure default listeners and to ensure listeners attached + * once on Application instantiation. + * + * Delays fetching listener aggregates from container until attempt to attach them is made. */ final class ApplicationListenersProvider { diff --git a/test/Application/BadControllerTrait.php b/test/Application/BadControllerTrait.php index b80e4059..6f28de13 100644 --- a/test/Application/BadControllerTrait.php +++ b/test/Application/BadControllerTrait.php @@ -7,8 +7,9 @@ use Laminas\Http\PhpEnvironment\Request; use Laminas\Http\PhpEnvironment\Response; use Laminas\Mvc\Application; +use Laminas\Mvc\ConfigProvider; use Laminas\Mvc\Controller\ControllerManager; -use Laminas\Router\ConfigProvider; +use Laminas\Router; use Laminas\Router\Http\Literal; use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\ArrayUtils; @@ -21,8 +22,8 @@ trait BadControllerTrait { public function prepareApplication(): Application { - $config = [ - 'router' => [ + $testConfig = [ + 'router' => [ 'routes' => [ 'path' => [ 'type' => Literal::class, @@ -36,14 +37,7 @@ public function prepareApplication(): Application ], ], ], - ]; - - $serviceConfig = ArrayUtils::merge( - ArrayUtils::merge( - (new \Laminas\Mvc\ConfigProvider())->getDependencies(), - (new ConfigProvider())->getDependencyConfig(), - ), - [ + 'dependencies' => [ 'aliases' => [ 'ControllerLoader' => ControllerManager::class, ], @@ -62,13 +56,19 @@ public function prepareApplication(): Application 'SendResponseListener' => MockSendResponseListener::class, 'BootstrapListener' => StubBootstrapListener::class, ], - 'services' => [ - 'config' => $config, - ], - ] + ], + ]; + + $config = ArrayUtils::merge( + ArrayUtils::merge( + (new ConfigProvider())(), + (new Router\ConfigProvider())(), + ), + $testConfig ); - $services = new ServiceManager($serviceConfig); - $application = $services->get('Application'); + $config['dependencies']['services']['config'] = $config; + $services = new ServiceManager($config['dependencies']); + $application = $services->get('Application'); $request = $services->get('Request'); $request->setUri('http://example.local/bad'); diff --git a/test/Application/InvalidControllerTypeTrait.php b/test/Application/InvalidControllerTypeTrait.php index bbf0d240..fb050fee 100644 --- a/test/Application/InvalidControllerTypeTrait.php +++ b/test/Application/InvalidControllerTypeTrait.php @@ -7,8 +7,9 @@ use Laminas\Http\PhpEnvironment\Request; use Laminas\Http\PhpEnvironment\Response; use Laminas\Mvc\Application; +use Laminas\Mvc\ConfigProvider; use Laminas\Mvc\Controller\ControllerManager; -use Laminas\Router\ConfigProvider; +use Laminas\Router; use Laminas\Router\Http\Literal; use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\ArrayUtils; @@ -21,8 +22,8 @@ trait InvalidControllerTypeTrait { public function prepareApplication(): Application { - $config = [ - 'router' => [ + $testConfig = [ + 'router' => [ 'routes' => [ 'path' => [ 'type' => Literal::class, @@ -36,14 +37,7 @@ public function prepareApplication(): Application ], ], ], - ]; - - $serviceConfig = ArrayUtils::merge( - ArrayUtils::merge( - (new \Laminas\Mvc\ConfigProvider())->getDependencies(), - (new ConfigProvider())->getDependencyConfig(), - ), - [ + 'dependencies' => [ 'aliases' => [ 'ControllerLoader' => 'ControllerManager', ], @@ -53,7 +47,6 @@ public function prepareApplication(): Application 'bad' => static fn(): stdClass => new stdClass(), ], ]), - 'Router' => static fn($services) => $services->get('HttpRouter'), ], 'invokables' => [ 'Request' => Request::class, @@ -62,13 +55,20 @@ public function prepareApplication(): Application 'SendResponseListener' => MockSendResponseListener::class, 'BootstrapListener' => StubBootstrapListener::class, ], - 'services' => [ - 'config' => $config, - ], - ] + ], + ]; + + $config = ArrayUtils::merge( + ArrayUtils::merge( + (new ConfigProvider())(), + (new Router\ConfigProvider())(), + ), + $testConfig ); - $services = new ServiceManager($serviceConfig); - $application = $services->get('Application'); + $config['dependencies']['services']['config'] = $config; + + $services = new ServiceManager($config['dependencies']); + $application = $services->get('Application'); $request = $services->get('Request'); $request->setUri('http://example.local/bad'); diff --git a/test/Application/MissingControllerTrait.php b/test/Application/MissingControllerTrait.php index 7340e2da..bd093a13 100644 --- a/test/Application/MissingControllerTrait.php +++ b/test/Application/MissingControllerTrait.php @@ -7,7 +7,8 @@ use Laminas\Http\PhpEnvironment\Request; use Laminas\Http\PhpEnvironment\Response; use Laminas\Mvc\Application; -use Laminas\Router\ConfigProvider; +use Laminas\Mvc\ConfigProvider; +use Laminas\Router; use Laminas\Router\Http\Literal; use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\ArrayUtils; @@ -19,8 +20,8 @@ trait MissingControllerTrait { public function prepareApplication(): Application { - $config = [ - 'router' => [ + $testConfig = [ + 'router' => [ 'routes' => [ 'path' => [ 'type' => Literal::class, @@ -34,17 +35,7 @@ public function prepareApplication(): Application ], ], ], - ]; - - $serviceConfig = ArrayUtils::merge( - ArrayUtils::merge( - (new \Laminas\Mvc\ConfigProvider())->getDependencies(), - (new ConfigProvider())->getDependencyConfig(), - ), - [ - 'factories' => [ - 'Router' => static fn($services) => $services->get('HttpRouter'), - ], + 'dependencies' => [ 'invokables' => [ 'Request' => Request::class, 'Response' => Response::class, @@ -52,13 +43,21 @@ public function prepareApplication(): Application 'SendResponseListener' => MockSendResponseListener::class, 'BootstrapListener' => StubBootstrapListener::class, ], - 'services' => [ - 'config' => $config, - ], - ] + ], + ]; + + $config = ArrayUtils::merge( + ArrayUtils::merge( + (new ConfigProvider())(), + (new Router\ConfigProvider())(), + ), + $testConfig ); - $services = new ServiceManager($serviceConfig); - $application = $services->get('Application'); + + $config['dependencies']['services']['config'] = $config; + + $services = new ServiceManager($config['dependencies']); + $application = $services->get('Application'); $request = $services->get('Request'); $request->setUri('http://example.local/bad'); diff --git a/test/Application/PathControllerTrait.php b/test/Application/PathControllerTrait.php index 9d2d0eaf..625e2a7d 100644 --- a/test/Application/PathControllerTrait.php +++ b/test/Application/PathControllerTrait.php @@ -22,8 +22,8 @@ trait PathControllerTrait { public function prepareApplication(): Application { - $config = [ - 'router' => [ + $testConfig = [ + 'router' => [ 'routes' => [ 'path' => [ 'type' => Literal::class, @@ -36,14 +36,7 @@ public function prepareApplication(): Application ], ], ], - ]; - - $serviceConfig = ArrayUtils::merge( - ArrayUtils::merge( - (new ConfigProvider())->getDependencies(), - (new Router\ConfigProvider())->getDependencyConfig(), - ), - [ + 'dependencies' => [ 'aliases' => [ 'ControllerLoader' => ControllerManager::class, ], @@ -53,7 +46,6 @@ public function prepareApplication(): Application 'path' => static fn() => new TestAsset\PathController(), ], ]), - 'Router' => static fn($services) => $services->get('HttpRouter'), ], 'invokables' => [ 'Request' => Request::class, @@ -62,13 +54,20 @@ public function prepareApplication(): Application 'SendResponseListener' => MockSendResponseListener::class, 'BootstrapListener' => StubBootstrapListener::class, ], - 'services' => [ - 'config' => $config, - ], - ] + ], + ]; + + $config = ArrayUtils::merge( + ArrayUtils::merge( + (new ConfigProvider())(), + (new Router\ConfigProvider())(), + ), + $testConfig ); - $services = new ServiceManager($serviceConfig); - $application = $services->get('Application'); + $config['dependencies']['services']['config'] = $config; + + $services = new ServiceManager($config['dependencies']); + $application = $services->get('Application'); $request = $services->get('Request'); $request->setUri('http://example.local/path'); diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index 5123f444..3d3f7fed 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -10,12 +10,12 @@ use Laminas\Http\PhpEnvironment\Request; use Laminas\Http\PhpEnvironment\Response; use Laminas\Mvc\Application; +use Laminas\Mvc\ConfigProvider; use Laminas\Mvc\Controller\ControllerManager; use Laminas\Mvc\MvcEvent; -use Laminas\Router\ConfigProvider; +use Laminas\Router; use Laminas\Router\Http\Literal; use Laminas\Router\RouteMatch; -use Laminas\Router\RouterFactory; use Laminas\Router\SimpleRouteStack; use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\ArrayUtils; @@ -32,9 +32,7 @@ use ReflectionProperty; use stdClass; -use function array_shift; use function array_values; -use function is_array; use function sprintf; use function var_export; @@ -50,12 +48,8 @@ class ApplicationTest extends TestCase public function setUp(): void { - $serviceConfig = ArrayUtils::merge( - ArrayUtils::merge( - (new \Laminas\Mvc\ConfigProvider())->getDependencies(), - (new ConfigProvider())->getDependencyConfig(), - ), - [ + $testConfig = [ + 'dependencies' => [ 'invokables' => [ 'Request' => Request::class, 'Response' => Response::class, @@ -63,15 +57,22 @@ public function setUp(): void 'SendResponseListener' => MockSendResponseListener::class, 'BootstrapListener' => StubBootstrapListener::class, ], - 'factories' => [ - 'Router' => RouterFactory::class, - ], 'services' => [ 'config' => [], ], - ] + ], + ]; + + $config = ArrayUtils::merge( + ArrayUtils::merge( + (new ConfigProvider())(), + (new Router\ConfigProvider())(), + ), + $testConfig ); - $this->serviceManager = new ServiceManager($serviceConfig); + $config['dependencies']['services']['config'] = $config; + + $this->serviceManager = new ServiceManager($config['dependencies']); $this->serviceManager->setAllowOverride(true); $this->application = $this->serviceManager->get('Application'); } @@ -130,9 +131,9 @@ private function getIdentifiersFromSharedEventManager(SharedEventManager $events } /** - * @dataProvider bootstrapRegistersListenersProvider + * @dataProvider defaultApplicationListenersProvider */ - public function testBootstrapRegistersListeners(string $listenerServiceName, string $event, string $method): void + public function testHasRegisteredDefaultListeners(string $listenerServiceName, string $event, string $method): void { $listenerService = $this->serviceManager->get($listenerServiceName); $this->application->bootstrap(); @@ -142,7 +143,7 @@ public function testBootstrapRegistersListeners(string $listenerServiceName, str $this->assertContains([$listenerService, $method], $listeners); } - public function bootstrapRegistersListenersProvider(): array + public function defaultApplicationListenersProvider(): array { // @codingStandardsIgnoreStart // [ Service Name, Event, Method, isCustom ] @@ -156,34 +157,6 @@ public function bootstrapRegistersListenersProvider(): array // @codingStandardsIgnoreEnd } - public function testBootstrapAlwaysRegistersDefaultListeners(): void - { - $r = new ReflectionProperty($this->application, 'defaultListeners'); - $r->setAccessible(true); - $defaultListenersNames = $r->getValue($this->application); - $defaultListeners = []; - foreach ($defaultListenersNames as $defaultListenerName) { - $defaultListeners[] = $this->serviceManager->get($defaultListenerName); - } - - $this->application->bootstrap(); - $eventManager = $this->application->getEventManager(); - - $registeredListeners = []; - foreach ($this->getEventsFromEventManager($eventManager) as $event) { - foreach ($this->getListenersForEvent($event, $eventManager) as $listener) { - if (is_array($listener)) { - $listener = array_shift($listener); - } - $registeredListeners[] = $listener; - } - } - - foreach ($defaultListeners as $defaultListener) { - $this->assertContains($defaultListener, $registeredListeners); - } - } - public function testBootstrapRegistersConfiguredMvcEvent(): void { $this->assertNull($this->application->getMvcEvent()); From 7ab32f8bc338acce932e363743ed4ba635bf4ea9 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Thu, 16 Mar 2023 03:49:42 +1000 Subject: [PATCH 05/13] Update psalm baseline Signed-off-by: Aleksei Khudiakov --- psalm-baseline.xml | 106 +-------------------------------------------- 1 file changed, 2 insertions(+), 104 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index aecacea9..d34cb61d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,19 +1,13 @@ - + - - Application - $r - get('EventManager')]]> - $listener get('Router')]]> - $listener $response $response request]]> @@ -22,9 +16,6 @@ array|object - - attach - serviceManager->get('config')]]> @@ -176,9 +167,6 @@ getContent()]]> getMethod()]]> - - customHttpMethodsMap[$method]]]> - $action $action @@ -331,7 +319,6 @@ $request - $modelAcceptString $modelName $modelString @@ -343,7 +330,6 @@ getMatchedAgainst()->params[self::INJECT_VIEWMODEL_NAME]]]> - $modelAcceptString $modelName $modelString $modelStrings @@ -351,9 +337,6 @@ new $name() - - $modelAcceptString - ModelInterface|null @@ -622,11 +605,6 @@ - - $application - $application - $application - reportMonitorEvent @@ -660,11 +638,6 @@ dispatch - - $application::ERROR_CONTROLLER_INVALID - $application::ERROR_CONTROLLER_NOT_FOUND - $application::ERROR_EXCEPTION - $container - get('EventManager')]]> get('Request')]]> get('Response')]]> - $listener - - $listener - $listeners - - - attach - @@ -1296,9 +1260,6 @@ attach - - $services - @@ -1309,9 +1270,6 @@ - - $serviceConfig - $services $services @@ -1429,16 +1387,10 @@ - - $serviceConfig - $services $services - - $services->get('HttpRouter')]]> - $services @@ -1451,20 +1403,10 @@ bootstrap - get setUri - - $serviceConfig - - - $services - - - $services->get('HttpRouter')]]> - $application $request @@ -1474,21 +1416,15 @@ bootstrap - get setUri - - $serviceConfig - $services $services - - $services->get('HttpRouter')]]> - + $services @@ -1501,7 +1437,6 @@ bootstrap - get setUri @@ -1509,25 +1444,13 @@ $e - - $log - - - - - - - getRouteMatch - $eventManager - $eventManager $events - $serviceConfig setMethods @@ -1569,7 +1492,6 @@ static function (MvcEvent $event) { - $defaultListenerName getContent()]]> getContent()]]> $router @@ -1580,17 +1502,10 @@ $controllerLoader $controllerManager - $defaultListener - $defaultListenerName - $defaultListenersNames - $defaultListeners[] $error $error $events - $listener - $listener $listenerService - $registeredListeners[] $request $request $request @@ -2471,16 +2386,6 @@ $e $e - - $log - - - - - - - - @@ -2517,10 +2422,6 @@ $e - - - - @@ -2819,9 +2720,6 @@ testDetachesListeners - - $child - From 9b67bcd0ebcadbb759f45bf7a4e189a1e9c39531 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Fri, 17 Mar 2023 03:38:07 +1000 Subject: [PATCH 06/13] Drop getConfig() from Application Signed-off-by: Aleksei Khudiakov --- src/Application.php | 10 ---------- test/ApplicationTest.php | 12 ------------ 2 files changed, 22 deletions(-) diff --git a/src/Application.php b/src/Application.php index 6eedf2c8..7653c69d 100644 --- a/src/Application.php +++ b/src/Application.php @@ -75,16 +75,6 @@ public function __construct( $this->response = $response ?: $serviceManager->get('Response'); } - /** - * Retrieve the application configuration - * - * @return array|object - */ - public function getConfig() - { - return $this->serviceManager->get('config'); - } - /** * Bootstrap the application * diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index 3d3f7fed..5cdcc5fd 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -34,7 +34,6 @@ use function array_values; use function sprintf; -use function var_export; class ApplicationTest extends TestCase { @@ -112,17 +111,6 @@ public function testServiceManagerIsPopulated(): void $this->assertSame($this->serviceManager, $this->application->getServiceManager()); } - public function testConfigIsPopulated(): void - { - $smConfig = $this->serviceManager->get('config'); - $appConfig = $this->application->getConfig(); - $this->assertEquals( - $smConfig, - $appConfig, - sprintf('SM config: %s; App config: %s', var_export($smConfig, true), var_export($appConfig, true)) - ); - } - private function getIdentifiersFromSharedEventManager(SharedEventManager $events): array { $r = new ReflectionProperty($events, 'identifiers'); From ea92e4344ec47abd3001ec7528e55ee7249111e4 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Fri, 17 Mar 2023 07:11:05 +1000 Subject: [PATCH 07/13] Switch Application to use ContainerInterface Deprecate `Application::getServiceManager()` to be removed in next major. Remove usage of `Application::getServiceManager()` in ViewManager listener. Signed-off-by: Aleksei Khudiakov --- src/Application.php | 20 +++++++++---------- src/Service/HttpViewManagerFactory.php | 2 +- src/View/Http/ViewManager.php | 27 ++++++++++++++------------ test/ApplicationTest.php | 10 ++++------ 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/Application.php b/src/Application.php index 7653c69d..127e66a6 100644 --- a/src/Application.php +++ b/src/Application.php @@ -6,9 +6,9 @@ use Laminas\EventManager\EventManagerInterface; use Laminas\EventManager\EventsCapableInterface; -use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\RequestInterface; use Laminas\Stdlib\ResponseInterface; +use Psr\Container\ContainerInterface; /** * Main application class for invoking applications @@ -63,7 +63,7 @@ class Application implements EventsCapableInterface protected $response; public function __construct( - protected ServiceManager $serviceManager, + protected ContainerInterface $container, EventManagerInterface $events, ApplicationListenersProvider $listenersProvider, ?RequestInterface $request = null, @@ -71,8 +71,8 @@ public function __construct( ) { $this->setEventManager($events); $listenersProvider->registerListeners($this); - $this->request = $request ?: $serviceManager->get('Request'); - $this->response = $response ?: $serviceManager->get('Response'); + $this->request = $request ?: $container->get('Request'); + $this->response = $response ?: $container->get('Response'); } /** @@ -86,8 +86,8 @@ public function __construct( */ public function bootstrap() { - $serviceManager = $this->serviceManager; - $events = $this->events; + $container = $this->container; + $events = $this->events; // Setup MVC Event $this->event = $event = new MvcEvent(); @@ -96,7 +96,7 @@ public function bootstrap() $event->setApplication($this); $event->setRequest($this->request); $event->setResponse($this->response); - $event->setRouter($serviceManager->get('Router')); + $event->setRouter($container->get('Router')); // Trigger bootstrap events $events->triggerEvent($event); @@ -107,11 +107,11 @@ public function bootstrap() /** * Retrieve the service manager * - * @return ServiceManager + * @deprecated Since 4.0.0 and will be removed in 5.0.0 */ - public function getServiceManager() + public function getServiceManager(): ContainerInterface { - return $this->serviceManager; + return $this->container; } /** diff --git a/src/Service/HttpViewManagerFactory.php b/src/Service/HttpViewManagerFactory.php index 84a19d18..cbbe7363 100644 --- a/src/Service/HttpViewManagerFactory.php +++ b/src/Service/HttpViewManagerFactory.php @@ -19,6 +19,6 @@ class HttpViewManagerFactory implements FactoryInterface */ public function __invoke(ContainerInterface $container, $name, ?array $options = null) { - return new HttpViewManager(); + return new HttpViewManager($container); } } diff --git a/src/View/Http/ViewManager.php b/src/View/Http/ViewManager.php index e1c0d6a6..15df1839 100644 --- a/src/View/Http/ViewManager.php +++ b/src/View/Http/ViewManager.php @@ -10,10 +10,10 @@ use Laminas\EventManager\ListenerAggregateInterface; use Laminas\Mvc\MvcEvent; use Laminas\Mvc\View\Http\InjectTemplateListener; -use Laminas\ServiceManager\ServiceManager; use Laminas\Stdlib\DispatchableInterface; use Laminas\View\Model\ModelInterface; use Laminas\View\View; +use Psr\Container\ContainerInterface; use Traversable; use function is_array; @@ -49,13 +49,18 @@ class ViewManager extends AbstractListenerAggregate /** @var MvcEvent */ protected $event; - /** @var ServiceManager */ - protected $services; + protected ContainerInterface $services; protected ?View $view = null; protected ?ModelInterface $viewModel = null; + public function __construct( + ContainerInterface $container + ) { + $this->services = $container; + } + /** * {@inheritDoc} */ @@ -70,26 +75,24 @@ public function attach(EventManagerInterface $events, $priority = 1) public function onBootstrap(MvcEvent $event): void { $application = $event->getApplication(); - $services = $application->getServiceManager(); - $config = $services->get('config'); + $config = $this->services->get('config'); $events = $application->getEventManager(); $sharedEvents = $events->getSharedManager(); - $this->config = isset($config['view_manager']) + $this->config = isset($config['view_manager']) && (is_array($config['view_manager']) || $config['view_manager'] instanceof ArrayAccess) ? $config['view_manager'] : []; - $this->services = $services; - $this->event = $event; + $this->event = $event; - $routeNotFoundStrategy = $services->get('HttpRouteNotFoundStrategy'); - $exceptionStrategy = $services->get('HttpExceptionStrategy'); - $mvcRenderingStrategy = $services->get('HttpDefaultRenderingStrategy'); + $routeNotFoundStrategy = $this->services->get('HttpRouteNotFoundStrategy'); + $exceptionStrategy = $this->services->get('HttpExceptionStrategy'); + $mvcRenderingStrategy = $this->services->get('HttpDefaultRenderingStrategy'); $this->injectViewModelIntoPlugin(); - $injectTemplateListener = $services->get(InjectTemplateListener::class); + $injectTemplateListener = $this->services->get(InjectTemplateListener::class); $createViewModelListener = new CreateViewModelListener(); $injectViewModelListener = new InjectViewModelListener(); diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index 5cdcc5fd..dd31698c 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -283,9 +283,8 @@ public function testRoutingFailureShouldTriggerDispatchError(): void public function testLocatorExceptionShouldTriggerDispatchError(): void { - $application = $this->setupPathController(false); - $controllerLoader = $application->getServiceManager()->get('ControllerManager'); - $response = new Response(); + $application = $this->setupPathController(false); + $response = new Response(); $application->getEventManager()->attach(MvcEvent::EVENT_DISPATCH_ERROR, static fn($e): Response => $response); $result = $application->run(); @@ -365,9 +364,8 @@ public function testApplicationShouldBeEventTargetAtFinishEvent(): void public function testOnDispatchErrorEventPassedToTriggersShouldBeTheOriginalOne(): void { - $application = $this->setupPathController(false); - $controllerManager = $application->getServiceManager()->get('ControllerManager'); - $model = $this->createMock(ViewModel::class); + $application = $this->setupPathController(false); + $model = $this->createMock(ViewModel::class); $application->getEventManager()->attach( MvcEvent::EVENT_DISPATCH_ERROR, static function ($e) use ($model): void { From 0891d0165553af24132ef44b38b3813469c287e3 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sun, 10 Dec 2023 12:25:13 +1000 Subject: [PATCH 08/13] Remove request and response from bootstrap event Signed-off-by: Aleksei Khudiakov --- src/Application.php | 42 ++++++++--------- src/MvcEvent.php | 8 ++-- src/Service/ApplicationFactory.php | 6 ++- .../AllowsReturningEarlyFromRoutingTest.php | 6 +-- .../ControllerIsDispatchedTest.php | 4 +- ...hableShouldRaiseDispatchErrorEventTest.php | 8 +++- ...ntrollerShouldTriggerDispatchErrorTest.php | 7 ++- ...veControllerShouldTriggerExceptionTest.php | 7 ++- ...llerTypeShouldTriggerDispatchErrorTest.php | 7 ++- test/ApplicationTest.php | 46 +++++++------------ 10 files changed, 73 insertions(+), 68 deletions(-) diff --git a/src/Application.php b/src/Application.php index 127e66a6..b8e23f51 100644 --- a/src/Application.php +++ b/src/Application.php @@ -4,8 +4,11 @@ namespace Laminas\Mvc; +use Closure; use Laminas\EventManager\EventManagerInterface; use Laminas\EventManager\EventsCapableInterface; +use Laminas\Http\Request; +use Laminas\Http\Response; use Laminas\Stdlib\RequestInterface; use Laminas\Stdlib\ResponseInterface; use Psr\Container\ContainerInterface; @@ -56,23 +59,19 @@ class Application implements EventsCapableInterface protected EventManagerInterface $events; - /** @var RequestInterface */ - protected $request; - - /** @var ResponseInterface */ - protected $response; - + /** + * @param Closure():Request $requestFactory + * @param Closure():Response $responseFactory + */ public function __construct( protected ContainerInterface $container, EventManagerInterface $events, ApplicationListenersProvider $listenersProvider, - ?RequestInterface $request = null, - ?ResponseInterface $response = null + protected Closure $requestFactory, + protected Closure $responseFactory, ) { $this->setEventManager($events); $listenersProvider->registerListeners($this); - $this->request = $request ?: $container->get('Request'); - $this->response = $response ?: $container->get('Response'); } /** @@ -94,8 +93,6 @@ public function bootstrap() $event->setName(MvcEvent::EVENT_BOOTSTRAP); $event->setTarget($this); $event->setApplication($this); - $event->setRequest($this->request); - $event->setResponse($this->response); $event->setRouter($container->get('Router')); // Trigger bootstrap events @@ -117,21 +114,25 @@ public function getServiceManager(): ContainerInterface /** * Get the request object * - * @return RequestInterface + * @deprecated Since 4.0.0 and will be removed in 5.0.0 + * + * @return null|RequestInterface */ public function getRequest() { - return $this->request; + return $this->event->getRequest(); } /** * Get the response object * - * @return ResponseInterface + * @deprecated Since 4.0.0 and will be removed in 5.0.0 + * + * @return null|ResponseInterface */ public function getResponse() { - return $this->response; + return $this->event->getResponse(); } /** @@ -182,7 +183,10 @@ public function run() $events = $this->events; $event = $this->event; - // Define callback used to determine whether or not to short-circuit + $event->setRequest(($this->requestFactory)()); + $event->setResponse(($this->responseFactory)()); + + // Define callback used to determine whether to short-circuit $shortCircuit = static function ($r) use ($event): bool { if ($r instanceof ResponseInterface) { return true; @@ -205,7 +209,6 @@ public function run() $event->setResponse($response); $event->stopPropagation(false); // Clear before triggering $events->triggerEvent($event); - $this->response = $response; return $this; } } @@ -227,12 +230,9 @@ public function run() $event->setResponse($response); $event->stopPropagation(false); // Clear before triggering $events->triggerEvent($event); - $this->response = $response; return $this; } - $response = $this->response; - $event->setResponse($response); return $this->completeRequest($event); } diff --git a/src/MvcEvent.php b/src/MvcEvent.php index 77e95fcc..c50c3447 100644 --- a/src/MvcEvent.php +++ b/src/MvcEvent.php @@ -28,10 +28,10 @@ class MvcEvent extends Event /** @var Application */ protected $application; - /** @var Request */ + /** @var null|Request */ protected $request; - /** @var Response */ + /** @var null|Response */ protected $response; /** @var mixed */ @@ -115,7 +115,7 @@ public function setRouteMatch(RouteMatch $matches) /** * Get request * - * @return Request + * @return null|Request */ public function getRequest() { @@ -137,7 +137,7 @@ public function setRequest(Request $request) /** * Get response * - * @return Response + * @return null|Response */ public function getResponse() { diff --git a/src/Service/ApplicationFactory.php b/src/Service/ApplicationFactory.php index 6fe1d663..4f1cac68 100644 --- a/src/Service/ApplicationFactory.php +++ b/src/Service/ApplicationFactory.php @@ -5,6 +5,8 @@ namespace Laminas\Mvc\Service; use Laminas\EventManager\EventManagerInterface; +use Laminas\Http\Request; +use Laminas\Http\Response; use Laminas\Mvc\Application; use Laminas\Mvc\ApplicationListenersProvider; use Psr\Container\ContainerInterface; @@ -17,8 +19,8 @@ public function __invoke(ContainerInterface $container): Application $container, $container->get(EventManagerInterface::class), $container->get(ApplicationListenersProvider::class), - $container->get('Request'), - $container->get('Response') + static fn (): Request => $container->get('Request'), + static fn (): Response => $container->get('Response') ); } } diff --git a/test/Application/AllowsReturningEarlyFromRoutingTest.php b/test/Application/AllowsReturningEarlyFromRoutingTest.php index a56b3f86..f6215b16 100644 --- a/test/Application/AllowsReturningEarlyFromRoutingTest.php +++ b/test/Application/AllowsReturningEarlyFromRoutingTest.php @@ -20,8 +20,8 @@ public function testAllowsReturningEarlyFromRouting(): void $application->getEventManager()->attach(MvcEvent::EVENT_ROUTE, static fn($e): Response => $response); - $result = $application->run(); - $this->assertSame($application, $result); - $this->assertSame($response, $result->getResponse()); + $application->run(); + + $this->assertSame($response, $application->getMvcEvent()->getResponse()); } } diff --git a/test/Application/ControllerIsDispatchedTest.php b/test/Application/ControllerIsDispatchedTest.php index dd340638..bdad606c 100644 --- a/test/Application/ControllerIsDispatchedTest.php +++ b/test/Application/ControllerIsDispatchedTest.php @@ -15,7 +15,9 @@ public function testControllerIsDispatchedDuringRun(): void { $application = $this->prepareApplication(); - $response = $application->run()->getResponse(); + $application->run(); + + $response = $application->getMvcEvent()->getResponse(); $this->assertStringContainsString('PathController', $response->getContent()); $this->assertStringContainsString(MvcEvent::EVENT_DISPATCH, $response->toString()); } diff --git a/test/Application/ExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEventTest.php b/test/Application/ExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEventTest.php index d2a498b4..580dcd17 100644 --- a/test/Application/ExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEventTest.php +++ b/test/Application/ExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEventTest.php @@ -4,6 +4,7 @@ namespace LaminasTest\Mvc\Application; +use Laminas\Http\Response; use Laminas\Mvc\MvcEvent; use Laminas\Stdlib\ResponseInterface; use PHPUnit\Framework\TestCase; @@ -19,16 +20,19 @@ public function testExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEvent( { $application = $this->prepareApplication(); - $response = $application->getResponse(); $events = $application->getEventManager(); - $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, function ($e) use ($response): ResponseInterface { + $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, function (MvcEvent $e): ResponseInterface { $exception = $e->getParam('exception'); $this->assertInstanceOf('Exception', $exception); + $response = $e->getResponse(); $response->setContent($exception->getMessage()); return $response; }); $application->run(); + + $response = $application->getMvcEvent()->getResponse(); + self::assertInstanceOf(Response::class, $response); $this->assertStringContainsString('Raised an exception', $response->getContent()); } } diff --git a/test/Application/InabilityToRetrieveControllerShouldTriggerDispatchErrorTest.php b/test/Application/InabilityToRetrieveControllerShouldTriggerDispatchErrorTest.php index bcc59ab3..766c8bf5 100644 --- a/test/Application/InabilityToRetrieveControllerShouldTriggerDispatchErrorTest.php +++ b/test/Application/InabilityToRetrieveControllerShouldTriggerDispatchErrorTest.php @@ -20,16 +20,19 @@ public function testInabilityToRetrieveControllerShouldTriggerDispatchError(): v { $application = $this->prepareApplication(); - $response = $application->getResponse(); $events = $application->getEventManager(); - $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function ($e) use ($response): ResponseInterface { + $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { $error = $e->getError(); $controller = $e->getController(); + $response = $e->getResponse(); $response->setContent("Code: " . $error . '; Controller: ' . $controller); return $response; }); $application->run(); + + $response = $application->getMvcEvent()->getResponse(); + self::assertInstanceOf(ResponseInterface::class, $response); $this->assertStringContainsString(Application::ERROR_CONTROLLER_NOT_FOUND, $response->getContent()); $this->assertStringContainsString('bad', $response->getContent()); } diff --git a/test/Application/InabilityToRetrieveControllerShouldTriggerExceptionTest.php b/test/Application/InabilityToRetrieveControllerShouldTriggerExceptionTest.php index b7eb5c6e..42a944b2 100644 --- a/test/Application/InabilityToRetrieveControllerShouldTriggerExceptionTest.php +++ b/test/Application/InabilityToRetrieveControllerShouldTriggerExceptionTest.php @@ -20,16 +20,19 @@ public function testInabilityToRetrieveControllerShouldTriggerExceptionError(): { $application = $this->prepareApplication(); - $response = $application->getResponse(); $events = $application->getEventManager(); - $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function ($e) use ($response): ResponseInterface { + $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { $error = $e->getError(); $controller = $e->getController(); + $response = $e->getResponse(); $response->setContent("Code: " . $error . '; Controller: ' . $controller); return $response; }); $application->run(); + + $response = $application->getMvcEvent()->getResponse(); + self::assertInstanceOf(ResponseInterface::class, $response); $this->assertStringContainsString(Application::ERROR_CONTROLLER_NOT_FOUND, $response->getContent()); $this->assertStringContainsString('bad', $response->getContent()); } diff --git a/test/Application/InvalidControllerTypeShouldTriggerDispatchErrorTest.php b/test/Application/InvalidControllerTypeShouldTriggerDispatchErrorTest.php index 1195db59..3695690e 100644 --- a/test/Application/InvalidControllerTypeShouldTriggerDispatchErrorTest.php +++ b/test/Application/InvalidControllerTypeShouldTriggerDispatchErrorTest.php @@ -20,17 +20,20 @@ public function testInvalidControllerTypeShouldTriggerDispatchError(): void { $application = $this->prepareApplication(); - $response = $application->getResponse(); $events = $application->getEventManager(); - $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function ($e) use ($response): ResponseInterface { + $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { $error = $e->getError(); $controller = $e->getController(); $class = $e->getControllerClass(); + $response = $e->getResponse(); $response->setContent("Code: " . $error . '; Controller: ' . $controller . '; Class: ' . $class); return $response; }); $application->run(); + + $response = $application->getMvcEvent()->getResponse(); + self::assertInstanceOf(ResponseInterface::class, $response); $this->assertStringContainsString(Application::ERROR_CONTROLLER_INVALID, $response->getContent()); $this->assertStringContainsString('bad', $response->getContent()); } diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index dd31698c..ce2fe733 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -76,18 +76,6 @@ public function setUp(): void $this->application = $this->serviceManager->get('Application'); } - public function testRequestIsPopulatedFromServiceManager(): void - { - $request = $this->serviceManager->get('Request'); - $this->assertSame($request, $this->application->getRequest()); - } - - public function testResponseIsPopulatedFromServiceManager(): void - { - $response = $this->serviceManager->get('Response'); - $this->assertSame($response, $this->application->getResponse()); - } - public function testEventManagerIsPopulated(): void { $events = $this->serviceManager->get('EventManager'); @@ -152,13 +140,11 @@ public function testBootstrapRegistersConfiguredMvcEvent(): void $event = $this->application->getMvcEvent(); $this->assertInstanceOf(MvcEvent::class, $event); - $request = $this->application->getRequest(); - $response = $this->application->getResponse(); $router = $this->serviceManager->get('HttpRouter'); $this->assertFalse($event->isError()); - $this->assertSame($request, $event->getRequest()); - $this->assertSame($response, $event->getResponse()); + $this->assertNull($event->getRequest()); + $this->assertNull($event->getResponse()); $this->assertSame($router, $event->getRouter()); $this->assertSame($this->application, $event->getApplication()); $this->assertSame($this->application, $event->getTarget()); @@ -268,15 +254,18 @@ public function testRoutingFailureShouldTriggerDispatchError(): void $event = $application->getMvcEvent(); $event->setRouter($router); - $response = $application->getResponse(); $events = $application->getEventManager(); - $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function ($e) use ($response): ResponseInterface { + $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { $error = $e->getError(); + $response = $e->getResponse(); $response->setContent("Code: " . $error); return $response; }); $application->run(); + + $response = $event->getResponse(); + self::assertInstanceOf(ResponseInterface::class, $response); $this->assertTrue($event->isError()); $this->assertStringContainsString(Application::ERROR_ROUTER_NO_MATCH, $response->getContent()); } @@ -287,9 +276,8 @@ public function testLocatorExceptionShouldTriggerDispatchError(): void $response = new Response(); $application->getEventManager()->attach(MvcEvent::EVENT_DISPATCH_ERROR, static fn($e): Response => $response); - $result = $application->run(); - $this->assertSame($application, $result, $result::class); - $this->assertSame($response, $result->getResponse(), $result::class); + $application->run(); + $this->assertSame($response, $application->getMvcEvent()->getResponse()); } public function testFailureForRouteToReturnRouteMatchShouldPopulateEventError(): void @@ -299,10 +287,10 @@ public function testFailureForRouteToReturnRouteMatchShouldPopulateEventError(): $event = $application->getMvcEvent(); $event->setRouter($router); - $response = $application->getResponse(); $events = $application->getEventManager(); - $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function ($e) use ($response): ResponseInterface { + $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { $error = $e->getError(); + $response = $e->getResponse(); $response->setContent("Code: " . $error); return $response; }); @@ -315,7 +303,7 @@ public function testFailureForRouteToReturnRouteMatchShouldPopulateEventError(): public function testFinishShouldRunEvenIfRouteEventReturnsResponse(): void { $this->application->bootstrap(); - $response = $this->application->getResponse(); + $response = new Response(); $events = $this->application->getEventManager(); $events->attach(MvcEvent::EVENT_ROUTE, static fn($e): ResponseInterface => $response, 100); @@ -332,7 +320,7 @@ public function testFinishShouldRunEvenIfRouteEventReturnsResponse(): void public function testFinishShouldRunEvenIfDispatchEventReturnsResponse(): void { $this->application->bootstrap(); - $response = $this->application->getResponse(); + $response = new Response(); $events = $this->application->getEventManager(); $events->clearListeners(MvcEvent::EVENT_ROUTE); $events->attach(MvcEvent::EVENT_DISPATCH, static fn($e): ResponseInterface => $response, 100); @@ -352,13 +340,15 @@ public function testApplicationShouldBeEventTargetAtFinishEvent(): void $application = $this->setupActionController(); $events = $application->getEventManager(); - $response = $application->getResponse(); - $events->attach(MvcEvent::EVENT_FINISH, static function ($e) use ($response): ResponseInterface { + $events->attach(MvcEvent::EVENT_FINISH, static function (MvcEvent $e): ResponseInterface { + $response = $e->getResponse(); $response->setContent("EventClass: " . $e->getTarget()::class); return $response; }); $application->run(); + $response = $application->getMvcEvent()->getResponse(); + self::assertInstanceOf(ResponseInterface::class, $response); $this->assertStringContainsString(Application::class, $response->getContent()); } @@ -382,7 +372,6 @@ public function testReturnsResponseFromListenerWhenRouteEventShortCircuits(): vo { $this->application->bootstrap(); $testResponse = new Response(); - $response = $this->application->getResponse(); $events = $this->application->getEventManager(); $events->clearListeners(MvcEvent::EVENT_DISPATCH); $events->attach(MvcEvent::EVENT_ROUTE, static function ($e) use ($testResponse): Response { @@ -404,7 +393,6 @@ public function testReturnsResponseFromListenerWhenDispatchEventShortCircuits(): { $this->application->bootstrap(); $testResponse = new Response(); - $response = $this->application->getResponse(); $events = $this->application->getEventManager(); $events->clearListeners(MvcEvent::EVENT_ROUTE); $events->attach(MvcEvent::EVENT_DISPATCH, static function ($e) use ($testResponse): Response { From b95dbc9d930f3163dc08fe82d64f73a1a16ce2a7 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 6 Jan 2024 15:15:08 +1000 Subject: [PATCH 09/13] Reorder event constants to match triggering order Signed-off-by: Aleksei Khudiakov --- src/MvcEvent.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/MvcEvent.php b/src/MvcEvent.php index c50c3447..f87040c3 100644 --- a/src/MvcEvent.php +++ b/src/MvcEvent.php @@ -18,12 +18,12 @@ class MvcEvent extends Event * Mvc events triggered by eventmanager */ public const EVENT_BOOTSTRAP = 'bootstrap'; + public const EVENT_ROUTE = 'route'; public const EVENT_DISPATCH = 'dispatch'; public const EVENT_DISPATCH_ERROR = 'dispatch.error'; - public const EVENT_FINISH = 'finish'; public const EVENT_RENDER = 'render'; public const EVENT_RENDER_ERROR = 'render.error'; - public const EVENT_ROUTE = 'route'; + public const EVENT_FINISH = 'finish'; /** @var Application */ protected $application; @@ -115,6 +115,7 @@ public function setRouteMatch(RouteMatch $matches) /** * Get request * + * @psalm-ignore-nullable-return Has actual request object most of the time except for bootstrap event * @return null|Request */ public function getRequest() @@ -137,6 +138,7 @@ public function setRequest(Request $request) /** * Get response * + * @psalm-ignore-nullable-return Has actual response object most of the time except for bootstrap event * @return null|Response */ public function getResponse() From 6be5d9661aa44e51c144e77347740f3aee9ef2d0 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sat, 6 Jan 2024 15:56:25 +1000 Subject: [PATCH 10/13] Remove deprecated Application::getResponse() usage and fix cs Signed-off-by: Aleksei Khudiakov --- src/DispatchListener.php | 2 +- src/Service/ViewResolverFactory.php | 6 +++--- ...spatchableShouldRaiseDispatchErrorEventTest.php | 2 +- ...eveControllerShouldTriggerDispatchErrorTest.php | 4 ++-- ...etrieveControllerShouldTriggerExceptionTest.php | 4 ++-- ...ontrollerTypeShouldTriggerDispatchErrorTest.php | 4 ++-- test/ApplicationTest.php | 14 +++++++------- test/DispatchListenerTest.php | 4 +--- 8 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/DispatchListener.php b/src/DispatchListener.php index cd1ea800..ef41bb3c 100644 --- a/src/DispatchListener.php +++ b/src/DispatchListener.php @@ -110,7 +110,7 @@ public function onDispatch(MvcEvent $e) } $request = $e->getRequest(); - $response = $application->getResponse(); + $response = $e->getResponse(); try { $return = $controller->dispatch($request, $response); diff --git a/src/Service/ViewResolverFactory.php b/src/Service/ViewResolverFactory.php index af023b9e..90e0da95 100644 --- a/src/Service/ViewResolverFactory.php +++ b/src/Service/ViewResolverFactory.php @@ -24,11 +24,11 @@ public function __invoke(ContainerInterface $container, $name, ?array $options = { $resolver = new ViewResolver\AggregateResolver(); - /** @var ResolverInterface $mapResolver */ + /** @var ViewResolver\ResolverInterface $mapResolver */ $mapResolver = $container->get('ViewTemplateMapResolver'); - /** @var ResolverInterface $pathResolver */ + /** @var ViewResolver\ResolverInterface $pathResolver */ $pathResolver = $container->get('ViewTemplatePathStack'); - /** @var ResolverInterface $prefixPathStackResolver */ + /** @var ViewResolver\ResolverInterface $prefixPathStackResolver */ $prefixPathStackResolver = $container->get('ViewPrefixPathStackResolver'); $resolver diff --git a/test/Application/ExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEventTest.php b/test/Application/ExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEventTest.php index 580dcd17..0b0fe3df 100644 --- a/test/Application/ExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEventTest.php +++ b/test/Application/ExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEventTest.php @@ -20,7 +20,7 @@ public function testExceptionsRaisedInDispatchableShouldRaiseDispatchErrorEvent( { $application = $this->prepareApplication(); - $events = $application->getEventManager(); + $events = $application->getEventManager(); $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, function (MvcEvent $e): ResponseInterface { $exception = $e->getParam('exception'); $this->assertInstanceOf('Exception', $exception); diff --git a/test/Application/InabilityToRetrieveControllerShouldTriggerDispatchErrorTest.php b/test/Application/InabilityToRetrieveControllerShouldTriggerDispatchErrorTest.php index 766c8bf5..455ac482 100644 --- a/test/Application/InabilityToRetrieveControllerShouldTriggerDispatchErrorTest.php +++ b/test/Application/InabilityToRetrieveControllerShouldTriggerDispatchErrorTest.php @@ -20,11 +20,11 @@ public function testInabilityToRetrieveControllerShouldTriggerDispatchError(): v { $application = $this->prepareApplication(); - $events = $application->getEventManager(); + $events = $application->getEventManager(); $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { $error = $e->getError(); $controller = $e->getController(); - $response = $e->getResponse(); + $response = $e->getResponse(); $response->setContent("Code: " . $error . '; Controller: ' . $controller); return $response; }); diff --git a/test/Application/InabilityToRetrieveControllerShouldTriggerExceptionTest.php b/test/Application/InabilityToRetrieveControllerShouldTriggerExceptionTest.php index 42a944b2..3b39b6be 100644 --- a/test/Application/InabilityToRetrieveControllerShouldTriggerExceptionTest.php +++ b/test/Application/InabilityToRetrieveControllerShouldTriggerExceptionTest.php @@ -20,11 +20,11 @@ public function testInabilityToRetrieveControllerShouldTriggerExceptionError(): { $application = $this->prepareApplication(); - $events = $application->getEventManager(); + $events = $application->getEventManager(); $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { $error = $e->getError(); $controller = $e->getController(); - $response = $e->getResponse(); + $response = $e->getResponse(); $response->setContent("Code: " . $error . '; Controller: ' . $controller); return $response; }); diff --git a/test/Application/InvalidControllerTypeShouldTriggerDispatchErrorTest.php b/test/Application/InvalidControllerTypeShouldTriggerDispatchErrorTest.php index 3695690e..b922cbbe 100644 --- a/test/Application/InvalidControllerTypeShouldTriggerDispatchErrorTest.php +++ b/test/Application/InvalidControllerTypeShouldTriggerDispatchErrorTest.php @@ -20,12 +20,12 @@ public function testInvalidControllerTypeShouldTriggerDispatchError(): void { $application = $this->prepareApplication(); - $events = $application->getEventManager(); + $events = $application->getEventManager(); $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { $error = $e->getError(); $controller = $e->getController(); $class = $e->getControllerClass(); - $response = $e->getResponse(); + $response = $e->getResponse(); $response->setContent("Code: " . $error . '; Controller: ' . $controller . '; Class: ' . $class); return $response; }); diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index ce2fe733..6e4ba751 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -140,7 +140,7 @@ public function testBootstrapRegistersConfiguredMvcEvent(): void $event = $this->application->getMvcEvent(); $this->assertInstanceOf(MvcEvent::class, $event); - $router = $this->serviceManager->get('HttpRouter'); + $router = $this->serviceManager->get('HttpRouter'); $this->assertFalse($event->isError()); $this->assertNull($event->getRequest()); @@ -242,7 +242,7 @@ public function testFinishEventIsTriggeredAfterDispatching(): void $application->run(); $this->assertStringContainsString( 'foobar', - $this->application->getResponse()->getBody(), + $application->getMvcEvent()->getResponse()->getBody(), 'The "finish" event was not triggered ("foobar" not in response)' ); } @@ -254,9 +254,9 @@ public function testRoutingFailureShouldTriggerDispatchError(): void $event = $application->getMvcEvent(); $event->setRouter($router); - $events = $application->getEventManager(); + $events = $application->getEventManager(); $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { - $error = $e->getError(); + $error = $e->getError(); $response = $e->getResponse(); $response->setContent("Code: " . $error); return $response; @@ -287,9 +287,9 @@ public function testFailureForRouteToReturnRouteMatchShouldPopulateEventError(): $event = $application->getMvcEvent(); $event->setRouter($router); - $events = $application->getEventManager(); + $events = $application->getEventManager(); $events->attach(MvcEvent::EVENT_DISPATCH_ERROR, static function (MvcEvent $e): ResponseInterface { - $error = $e->getError(); + $error = $e->getError(); $response = $e->getResponse(); $response->setContent("Code: " . $error); return $response; @@ -339,7 +339,7 @@ public function testApplicationShouldBeEventTargetAtFinishEvent(): void { $application = $this->setupActionController(); - $events = $application->getEventManager(); + $events = $application->getEventManager(); $events->attach(MvcEvent::EVENT_FINISH, static function (MvcEvent $e): ResponseInterface { $response = $e->getResponse(); $response->setContent("EventClass: " . $e->getTarget()::class); diff --git a/test/DispatchListenerTest.php b/test/DispatchListenerTest.php index 17fdb729..e17a6338 100644 --- a/test/DispatchListenerTest.php +++ b/test/DispatchListenerTest.php @@ -29,7 +29,6 @@ class DispatchListenerTest extends TestCase public function createMvcEvent(string $controllerMatched): MvcEvent { - $response = new Response(); $routeMatch = $this->prophesize(RouteMatch::class); $routeMatch->getParam('controller', 'not-found')->willReturn($controllerMatched); @@ -37,11 +36,10 @@ public function createMvcEvent(string $controllerMatched): MvcEvent $application = $this->prophesize(Application::class); $application->getEventManager()->willReturn($eventManager); - $application->getResponse()->willReturn($response); $event = new MvcEvent(); $event->setRequest(new Request()); - $event->setResponse($response); + $event->setResponse(new Response()); $event->setApplication($application->reveal()); $event->setRouteMatch($routeMatch->reveal()); From f7410d65152d2db8f26ebdb7044811bd338ef047 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sun, 7 Jan 2024 05:25:42 +1000 Subject: [PATCH 11/13] Add new MvcEvent::EVENT_PREPARE to perform request aware setup logic Signed-off-by: Aleksei Khudiakov --- src/Application.php | 6 ++++++ src/MvcEvent.php | 1 + test/ApplicationTest.php | 30 ++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/Application.php b/src/Application.php index b8e23f51..be3cd62f 100644 --- a/src/Application.php +++ b/src/Application.php @@ -186,6 +186,12 @@ public function run() $event->setRequest(($this->requestFactory)()); $event->setResponse(($this->responseFactory)()); + // Trigger prepare event + $event->setName(MvcEvent::EVENT_PREPARE); + $event->stopPropagation(false); + $event->setTarget($this); + $events->triggerEvent($event); + // Define callback used to determine whether to short-circuit $shortCircuit = static function ($r) use ($event): bool { if ($r instanceof ResponseInterface) { diff --git a/src/MvcEvent.php b/src/MvcEvent.php index f87040c3..4cb4288d 100644 --- a/src/MvcEvent.php +++ b/src/MvcEvent.php @@ -18,6 +18,7 @@ class MvcEvent extends Event * Mvc events triggered by eventmanager */ public const EVENT_BOOTSTRAP = 'bootstrap'; + public const EVENT_PREPARE = 'prepare'; public const EVENT_ROUTE = 'route'; public const EVENT_DISPATCH = 'dispatch'; public const EVENT_DISPATCH_ERROR = 'dispatch.error'; diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index 6e4ba751..e25d4e39 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -150,6 +150,36 @@ public function testBootstrapRegistersConfiguredMvcEvent(): void $this->assertSame($this->application, $event->getTarget()); } + public function testRequestIsPopulatedForPrepareEvent(): void + { + $this->application->bootstrap(); + $request = null; + + $this->application + ->getEventManager() + ->attach(MvcEvent::EVENT_PREPARE, static function (MvcEvent $e) use (&$request) { + $request = $e->getRequest(); + }); + + $this->application->run(); + $this->assertInstanceOf(Request::class, $request); + } + + public function testResponseIsPopulatedForPrepareEvent(): void + { + $this->application->bootstrap(); + $response = null; + + $this->application + ->getEventManager() + ->attach(MvcEvent::EVENT_PREPARE, static function (MvcEvent $e) use (&$response) { + $response = $e->getResponse(); + }); + + $this->application->run(); + $this->assertInstanceOf(Response::class, $response); + } + public function setupPathController(bool $addService = true): Application { $request = $this->serviceManager->get('Request'); From 294d87c4cee3c4730be184105e3595acd3e2eff2 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sun, 7 Jan 2024 06:11:11 +1000 Subject: [PATCH 12/13] Update inline documentation for Application Signed-off-by: Aleksei Khudiakov --- src/Application.php | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/Application.php b/src/Application.php index be3cd62f..25551d11 100644 --- a/src/Application.php +++ b/src/Application.php @@ -16,31 +16,18 @@ /** * Main application class for invoking applications * - * Expects the user will provide a configured ServiceManager, configured with - * the following services: - * - * - EventManager - * - ModuleManager - * - Request - * - Response - * - RouteListener - * - Router - * - DispatchListener - * - ViewManager - * * The most common workflow is: * - * $services = new Laminas\ServiceManager\ServiceManager($servicesConfig); - * $app = new Application($appConfig, $services); + * $container = new Laminas\ServiceManager\ServiceManager($dependencies); + * $app = $container->get('Application'); * $app->bootstrap(); - * $response = $app->run(); - * $response->send(); + * $app->run(); * * - * bootstrap() opts in to the default route, dispatch, and view listeners, - * sets up the MvcEvent, and triggers the bootstrap event. This can be omitted - * if you wish to setup your own listeners and/or workflow; alternately, you - * can simply extend the class to override such behavior. + * bootstrap() sets up the MvcEvent and triggers the bootstrap event. Bootstrap event could be used to configure + * application. Most of the application and its dependencies preparation is expected to be handled by dependency + * injection container. Request specific configuration can be performed during prepare event triggered early in the + * run(). */ class Application implements EventsCapableInterface { @@ -77,9 +64,7 @@ public function __construct( /** * Bootstrap the application * - * Defines and binds the MvcEvent, and passes it the request, response, and - * router. Attaches the ViewManager as a listener. Triggers the bootstrap - * event. + * Defines and binds the MvcEvent, and passes it the router. Triggers the bootstrap event. * * @return Application */ @@ -165,6 +150,8 @@ public function getEventManager(): EventManagerInterface /** * Run the application * + * @triggers prepare(MvcEvent) + * Allows last minute request dependent preparation logic. * @triggers route(MvcEvent) * Routes the request, and sets the RouteMatch object in the event. * @triggers dispatch(MvcEvent) @@ -176,6 +163,8 @@ public function getEventManager(): EventManagerInterface * discovered controller, and controller class (if known). * Typically, a handler should return a populated Response object * that can be returned immediately. + * @triggers finish(MvcEvent) + * Event for post-request cleanup. Responsible for emitting response via SendResponseListener. * @return self */ public function run() From f2258d4b561c253689aba68662337fb0e6794b64 Mon Sep 17 00:00:00 2001 From: Aleksei Khudiakov Date: Sun, 7 Jan 2024 07:43:34 +1000 Subject: [PATCH 13/13] Create MvcEvent instance in constructor rather than bootstrap() for type safety Signed-off-by: Aleksei Khudiakov --- src/Application.php | 12 +++++------- test/ApplicationTest.php | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Application.php b/src/Application.php index 25551d11..c3777fe8 100644 --- a/src/Application.php +++ b/src/Application.php @@ -39,10 +39,8 @@ class Application implements EventsCapableInterface /** * MVC event token - * - * @var MvcEvent */ - protected $event; + protected MvcEvent $event; protected EventManagerInterface $events; @@ -59,6 +57,8 @@ public function __construct( ) { $this->setEventManager($events); $listenersProvider->registerListeners($this); + $this->event = new MvcEvent(); + $this->event->setApplication($this); } /** @@ -74,7 +74,7 @@ public function bootstrap() $events = $this->events; // Setup MVC Event - $this->event = $event = new MvcEvent(); + $event = $this->event; $event->setName(MvcEvent::EVENT_BOOTSTRAP); $event->setTarget($this); $event->setApplication($this); @@ -122,10 +122,8 @@ public function getResponse() /** * Get the MVC event instance - * - * @return MvcEvent */ - public function getMvcEvent() + public function getMvcEvent(): MvcEvent { return $this->event; } diff --git a/test/ApplicationTest.php b/test/ApplicationTest.php index e25d4e39..8403743f 100644 --- a/test/ApplicationTest.php +++ b/test/ApplicationTest.php @@ -135,7 +135,6 @@ public function defaultApplicationListenersProvider(): array public function testBootstrapRegistersConfiguredMvcEvent(): void { - $this->assertNull($this->application->getMvcEvent()); $this->application->bootstrap(); $event = $this->application->getMvcEvent(); $this->assertInstanceOf(MvcEvent::class, $event);