Skip to content

Commit 18572bc

Browse files
committed
Extract Application default listeners to dedicated listener provider
Use double-dispatch approach to ensure default listeners are registered. Signed-off-by: Aleksei Khudiakov <aleksey@xerkus.pro>
1 parent acde0ff commit 18572bc

7 files changed

+403
-76
lines changed

src/Application.php

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,6 @@ class Application implements EventsCapableInterface
4747
public const ERROR_EXCEPTION = 'error-exception';
4848
public const ERROR_ROUTER_NO_MATCH = 'error-router-no-match';
4949

50-
/**
51-
* Default application event listeners
52-
*
53-
* @var array
54-
*/
55-
protected $defaultListeners = [
56-
'RouteListener',
57-
'DispatchListener',
58-
'HttpMethodListener',
59-
'ViewManager',
60-
'SendResponseListener',
61-
];
62-
6350
/**
6451
* MVC event token
6552
*
@@ -78,16 +65,14 @@ class Application implements EventsCapableInterface
7865
public function __construct(
7966
protected ServiceManager $serviceManager,
8067
EventManagerInterface $events,
68+
ApplicationListenerProvider $listenerProvider,
8169
?RequestInterface $request = null,
8270
?ResponseInterface $response = null
8371
) {
8472
$this->setEventManager($events);
73+
$listenerProvider->registerListeners($this);
8574
$this->request = $request ?: $serviceManager->get('Request');
8675
$this->response = $response ?: $serviceManager->get('Response');
87-
88-
foreach ($this->defaultListeners as $listener) {
89-
$serviceManager->get($listener)->attach($this->events);
90-
}
9176
}
9277

9378
/**

src/ApplicationListenerProvider.php

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laminas\Mvc;
6+
7+
use Laminas\EventManager\ListenerAggregateInterface;
8+
use Laminas\Mvc\Exception\DomainException;
9+
use Psr\Container\ContainerInterface;
10+
11+
use function array_merge;
12+
use function array_unique;
13+
use function get_debug_type;
14+
use function is_string;
15+
use function sprintf;
16+
17+
use const SORT_REGULAR;
18+
19+
/**
20+
* Provides lazy container
21+
*/
22+
final class ApplicationListenerProvider
23+
{
24+
public const DEFAULT_LISTENERS = [
25+
'RouteListener',
26+
'DispatchListener',
27+
'HttpMethodListener',
28+
'ViewManager',
29+
'SendResponseListener',
30+
];
31+
32+
/**
33+
* @param array<string|ListenerAggregateInterface> $listeners
34+
*/
35+
private function __construct(private readonly ContainerInterface $container, private readonly array $listeners)
36+
{
37+
}
38+
39+
/**
40+
* @param array<string|ListenerAggregateInterface> $extraListeners
41+
*/
42+
public static function withDefaultListeners(ContainerInterface $container, array $extraListeners): self
43+
{
44+
return new self(
45+
$container,
46+
array_unique(array_merge(self::DEFAULT_LISTENERS, $extraListeners), SORT_REGULAR)
47+
);
48+
}
49+
50+
/**
51+
* @param array<string|ListenerAggregateInterface> $extraListeners
52+
*/
53+
public static function withoutDefaultListeners(ContainerInterface $container, array $extraListeners): self
54+
{
55+
return new self(
56+
$container,
57+
array_unique($extraListeners, SORT_REGULAR)
58+
);
59+
}
60+
61+
/**
62+
* @return array<string|ListenerAggregateInterface>
63+
*/
64+
public function getListeners(): array
65+
{
66+
return $this->listeners;
67+
}
68+
69+
public function registerListeners(Application $application): void
70+
{
71+
$events = $application->getEventManager();
72+
foreach ($this->listeners as $listener) {
73+
$msg = '';
74+
if (is_string($listener)) {
75+
$msg = sprintf(' with container id "%s"', $listener);
76+
/** @var mixed $listener */
77+
$listener = $this->container->get($listener);
78+
}
79+
if (! $listener instanceof ListenerAggregateInterface) {
80+
throw new DomainException(sprintf(
81+
'Application listener%s expected to be instance of %s, %s given',
82+
$msg,
83+
ListenerAggregateInterface::class,
84+
get_debug_type($listener)
85+
));
86+
}
87+
88+
$listener->attach($events);
89+
}
90+
}
91+
}

src/ConfigProvider.php

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Laminas\Mvc\RouteListener;
1515
use Laminas\Mvc\SendResponseListener;
1616
use Laminas\Mvc\Service\ApplicationFactory;
17+
use Laminas\Mvc\Service\ApplicationListenerProviderFactory;
1718
use Laminas\Mvc\Service\ControllerManagerFactory;
1819
use Laminas\Mvc\Service\ControllerPluginManagerFactory;
1920
use Laminas\Mvc\Service\DispatchListenerFactory;
@@ -59,7 +60,10 @@ class ConfigProvider
5960
public function __invoke(): array
6061
{
6162
return [
62-
'dependencies' => $this->getDependencies(),
63+
'dependencies' => $this->getDependencies(),
64+
Application::class => [
65+
'listeners' => [],
66+
],
6367
];
6468
}
6569

@@ -97,36 +101,37 @@ public function getDependencies(): array
97101
ControllerManager::class => 'ControllerManager',
98102
],
99103
'factories' => [
100-
'EventManager' => EventManagerFactory::class,
101-
'SharedEventManager' => static fn() => new SharedEventManager(),
102-
'Application' => ApplicationFactory::class,
103-
'ControllerManager' => ControllerManagerFactory::class,
104-
'ControllerPluginManager' => ControllerPluginManagerFactory::class,
105-
'DispatchListener' => DispatchListenerFactory::class,
106-
'HttpExceptionStrategy' => HttpExceptionStrategyFactory::class,
107-
'HttpMethodListener' => HttpMethodListenerFactory::class,
108-
'HttpRouteNotFoundStrategy' => HttpRouteNotFoundStrategyFactory::class,
109-
'HttpViewManager' => HttpViewManagerFactory::class,
110-
'InjectTemplateListener' => InjectTemplateListenerFactory::class,
111-
'PaginatorPluginManager' => PaginatorPluginManagerFactory::class,
112-
'Request' => RequestFactory::class,
113-
'Response' => ResponseFactory::class,
114-
'ViewHelperManager' => ViewHelperManagerFactory::class,
115-
DefaultRenderingStrategy::class => HttpDefaultRenderingStrategyFactory::class,
116-
'ViewFeedStrategy' => ViewFeedStrategyFactory::class,
117-
'ViewJsonStrategy' => ViewJsonStrategyFactory::class,
118-
'ViewManager' => ViewManagerFactory::class,
119-
'ViewResolver' => ViewResolverFactory::class,
120-
'ViewTemplateMapResolver' => ViewTemplateMapResolverFactory::class,
121-
'ViewTemplatePathStack' => ViewTemplatePathStackFactory::class,
122-
'ViewPrefixPathStackResolver' => ViewPrefixPathStackResolverFactory::class,
123-
RouteListener::class => InvokableFactory::class,
124-
SendResponseListener::class => SendResponseListenerFactory::class,
125-
FeedRenderer::class => InvokableFactory::class,
126-
JsonRenderer::class => InvokableFactory::class,
127-
PhpRenderer::class => ViewPhpRendererFactory::class,
128-
PhpRendererStrategy::class => ViewPhpRendererStrategyFactory::class,
129-
View::class => ViewFactory::class,
104+
'EventManager' => EventManagerFactory::class,
105+
'SharedEventManager' => static fn() => new SharedEventManager(),
106+
'Application' => ApplicationFactory::class,
107+
'ControllerManager' => ControllerManagerFactory::class,
108+
'ControllerPluginManager' => ControllerPluginManagerFactory::class,
109+
'DispatchListener' => DispatchListenerFactory::class,
110+
'HttpExceptionStrategy' => HttpExceptionStrategyFactory::class,
111+
'HttpMethodListener' => HttpMethodListenerFactory::class,
112+
'HttpRouteNotFoundStrategy' => HttpRouteNotFoundStrategyFactory::class,
113+
'HttpViewManager' => HttpViewManagerFactory::class,
114+
'InjectTemplateListener' => InjectTemplateListenerFactory::class,
115+
'PaginatorPluginManager' => PaginatorPluginManagerFactory::class,
116+
'Request' => RequestFactory::class,
117+
'Response' => ResponseFactory::class,
118+
'ViewHelperManager' => ViewHelperManagerFactory::class,
119+
DefaultRenderingStrategy::class => HttpDefaultRenderingStrategyFactory::class,
120+
'ViewFeedStrategy' => ViewFeedStrategyFactory::class,
121+
'ViewJsonStrategy' => ViewJsonStrategyFactory::class,
122+
'ViewManager' => ViewManagerFactory::class,
123+
'ViewResolver' => ViewResolverFactory::class,
124+
'ViewTemplateMapResolver' => ViewTemplateMapResolverFactory::class,
125+
'ViewTemplatePathStack' => ViewTemplatePathStackFactory::class,
126+
'ViewPrefixPathStackResolver' => ViewPrefixPathStackResolverFactory::class,
127+
ApplicationListenerProvider::class => ApplicationListenerProviderFactory::class,
128+
RouteListener::class => InvokableFactory::class,
129+
SendResponseListener::class => SendResponseListenerFactory::class,
130+
FeedRenderer::class => InvokableFactory::class,
131+
JsonRenderer::class => InvokableFactory::class,
132+
PhpRenderer::class => ViewPhpRendererFactory::class,
133+
PhpRendererStrategy::class => ViewPhpRendererStrategyFactory::class,
134+
View::class => ViewFactory::class,
130135
],
131136
'shared' => [
132137
'EventManager' => false,

src/Service/ApplicationFactory.php

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,21 @@
44

55
namespace Laminas\Mvc\Service;
66

7+
use Laminas\EventManager\EventManagerInterface;
78
use Laminas\Mvc\Application;
8-
use Laminas\ServiceManager\Factory\FactoryInterface;
9+
use Laminas\Mvc\ApplicationListenerProvider;
910
use Psr\Container\ContainerInterface;
1011

11-
class ApplicationFactory implements FactoryInterface
12+
final class ApplicationFactory
1213
{
13-
/**
14-
* Create the Application service
15-
*
16-
* Creates a Laminas\Mvc\Application service, passing it the configuration
17-
* service and the service manager instance.
18-
*
19-
* @param string $name
20-
* @param null|array $options
21-
* @return Application
22-
*/
23-
public function __invoke(ContainerInterface $container, $name, ?array $options = null)
14+
public function __invoke(ContainerInterface $container): Application
2415
{
25-
$application = new Application(
16+
return new Application(
2617
$container,
27-
$container->get('EventManager'),
18+
$container->get(EventManagerInterface::class),
19+
$container->get(ApplicationListenerProvider::class),
2820
$container->get('Request'),
2921
$container->get('Response')
3022
);
31-
32-
if (! $container->has('config')) {
33-
return $application;
34-
}
35-
36-
$em = $application->getEventManager();
37-
$listeners = $container->get('config')[Application::class]['listeners'] ?? [];
38-
foreach ($listeners as $listener) {
39-
$container->get($listener)->attach($em);
40-
}
41-
return $application;
4223
}
4324
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Laminas\Mvc\Service;
6+
7+
use Laminas\Mvc\Application;
8+
use Laminas\Mvc\ApplicationListenerProvider;
9+
use Psr\Container\ContainerInterface;
10+
11+
use function assert;
12+
use function is_array;
13+
14+
final class ApplicationListenerProviderFactory
15+
{
16+
/**
17+
* For default listeners @see ApplicationListenerProvider::DEFAULT_LISTENERS
18+
*
19+
* Extra listeners could be specified via configuration at `$config[Application::class]['listeners']`
20+
* or overridden via delegator factory.
21+
*/
22+
public function __invoke(ContainerInterface $container): ApplicationListenerProvider
23+
{
24+
$config = $container->get('config');
25+
assert(is_array($config));
26+
27+
/** @psalm-var list<string> $listeners */
28+
$listeners = $config[Application::class]['listeners'] ?? [];
29+
return ApplicationListenerProvider::withDefaultListeners($container, $listeners);
30+
}
31+
}

0 commit comments

Comments
 (0)