Skip to content

Commit 4cd8dd1

Browse files
committed
feat: add prioritised handlers
1 parent e8c941a commit 4cd8dd1

File tree

15 files changed

+390
-120
lines changed

15 files changed

+390
-120
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
namespace Pitch\AdrBundle\Configuration;
3+
4+
use Attribute;
5+
use Doctrine\Common\Annotations\Annotation;
6+
use Pitch\Annotation\AbstractAnnotation;
7+
8+
/**
9+
* @Annotation
10+
*/
11+
#[Attribute]
12+
class DefaultContentType extends AbstractAnnotation
13+
{
14+
public ?string $value = null;
15+
}

src/DependencyInjection/PitchAdrExtension.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22
namespace Pitch\AdrBundle\DependencyInjection;
33

44
use Symfony\Component\Config\FileLocator;
5-
use Pitch\AdrBundle\EventSubscriber\ControllerSubscriber;
5+
use Pitch\AdrBundle\EventSubscriber\GracefulSubscriber;
6+
use Pitch\AdrBundle\EventSubscriber\ResponderSubscriber;
67
use Symfony\Component\DependencyInjection\ContainerBuilder;
78
use Symfony\Component\DependencyInjection\Extension\Extension;
89
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
910

1011
class PitchAdrExtension extends Extension
1112
{
1213
const ALIAS = 'pitch_adr';
14+
const PARAMETER_DEFAULT_CONTENT_TYPE = 'pitch_adr.defaultContentType';
1315

1416
public function getAlias(): string
1517
{
@@ -30,6 +32,13 @@ public function load(array $configs, ContainerBuilder $container)
3032
$loader->load('handler.php');
3133
}
3234

33-
$container->findDefinition(ControllerSubscriber::class)->setArgument('$globalGraceful', $config['graceful']);
35+
$container->findDefinition(GracefulSubscriber::class)->setArgument('$globalGraceful', $config['graceful']);
36+
37+
$container->findDefinition(ResponderSubscriber::class)->setArgument(
38+
'$defaultContentType',
39+
$container->hasParameter(static::PARAMETER_DEFAULT_CONTENT_TYPE)
40+
? $container->getParameter(static::PARAMETER_DEFAULT_CONTENT_TYPE)
41+
: null,
42+
);
3443
}
3544
}

src/EventSubscriber/ControllerSubscriber.php renamed to src/EventSubscriber/GracefulSubscriber.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
88
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
99

10-
class ControllerSubscriber implements EventSubscriberInterface
10+
class GracefulSubscriber implements EventSubscriberInterface
1111
{
1212
private array $globalGraceful;
1313

src/EventSubscriber/ViewSubscriber.php renamed to src/EventSubscriber/ResponderSubscriber.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
<?php
22
namespace Pitch\AdrBundle\EventSubscriber;
33

4+
use Pitch\AdrBundle\Configuration\DefaultContentType;
45
use Pitch\AdrBundle\Responder\Responder;
56
use Symfony\Component\HttpFoundation\Response;
67
use Symfony\Component\HttpKernel\KernelEvents;
78
use Symfony\Component\HttpKernel\Event\ViewEvent;
89
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
910
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1011

11-
class ViewSubscriber implements EventSubscriberInterface
12+
class ResponderSubscriber implements EventSubscriberInterface
1213
{
1314
private Responder $responder;
15+
private ?string $defaultContentType;
1416

1517
public function __construct(
16-
Responder $responder
18+
Responder $responder,
19+
?string $defaultContentType = null
1720
) {
1821
$this->responder = $responder;
22+
$this->defaultContentType = $defaultContentType;
1923
}
2024

2125
public static function getSubscribedEvents()
@@ -27,9 +31,17 @@ public static function getSubscribedEvents()
2731

2832
public function onKernelView(ViewEvent $event)
2933
{
34+
$request = $event->getRequest();
35+
if ($this->defaultContentType && !$request->attributes->has('_' . DefaultContentType::class)) {
36+
$request->attributes->set(
37+
'_' . DefaultContentType::class,
38+
new DefaultContentType($this->defaultContentType),
39+
);
40+
}
41+
3042
$payloadEvent = new ResponsePayloadEvent(
3143
$event->getControllerResult(),
32-
$event->getRequest(),
44+
$request,
3345
);
3446

3547
$result = $this->responder->handleResponsePayload($payloadEvent);

src/Resources/config/adr.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
<?php
22
namespace Pitch\AdrBundle\Resources\config;
33

4-
use Pitch\AdrBundle\EventSubscriber\ControllerSubscriber;
5-
use Pitch\AdrBundle\EventSubscriber\ViewSubscriber;
4+
use Pitch\AdrBundle\EventSubscriber\GracefulSubscriber;
5+
use Pitch\AdrBundle\EventSubscriber\ResponderSubscriber;
66
use Pitch\AdrBundle\Responder\Responder;
77
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
88

99
return static function (ContainerConfigurator $container) {
1010
$container->services()
1111
->defaults()
1212
->autowire()
13-
->set(ControllerSubscriber::class)
13+
->set(GracefulSubscriber::class)
1414
->tag('kernel.event_subscriber')
15-
->set(ViewSubscriber::class)
15+
->set(ResponderSubscriber::class)
1616
->tag('kernel.event_subscriber')
1717
->set(Responder::class)
1818
;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
namespace Pitch\AdrBundle\Responder;
3+
4+
interface PrioritisedResponseHandlerInterface extends ResponseHandlerInterface
5+
{
6+
/**
7+
* Consecutive handlers implementing this interface will be executed in the order
8+
* of the return value of this function in descending order.
9+
* If you return `null`, the handler will be skipped.
10+
*/
11+
public function getResponseHandlerPriority(ResponsePayloadEvent $event): ?float;
12+
}

src/Responder/Responder.php

Lines changed: 96 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ class Responder
2121
*/
2222
private array $handlerMap = [];
2323

24-
/** @var
24+
/**
25+
* @var ResponseHandlerInterface[] $handlerObjects
2526
* [id => object]
2627
*/
2728
private array $handlerObjects = [];
@@ -61,39 +62,112 @@ public function handleResponsePayload(
6162
}
6263

6364
foreach ($types as $t) {
64-
foreach ($this->handlerMap[$t] ?? [] as $stackEntry) {
65-
$serviceId = \is_array($stackEntry)? $stackEntry['name'] ?? $stackEntry[0] : (string) $stackEntry;
66-
67-
if (!isset($this->handlerObjects[$serviceId])) {
68-
$this->handlerObjects[$serviceId] = $this->container->get($serviceId);
69-
}
70-
71-
if (isset($usedHandlersPayload[$serviceId])
72-
&& \in_array($payloadEvent->payload, $usedHandlersPayload[$serviceId], true)
73-
) {
74-
throw new CircularHandlerException($usedHandlersLog);
65+
$stack = $this->handlerMap[$t] ?? [];
66+
67+
do {
68+
$prioritisedHandlers = [];
69+
$currentHandler = null;
70+
while ($stackEntry = \current($stack)) {
71+
$serviceId = \is_array($stackEntry)
72+
? $stackEntry['name'] ?? $stackEntry[0]
73+
: (string) $stackEntry;
74+
75+
if (!isset($this->handlerObjects[$serviceId])) {
76+
$this->handlerObjects[$serviceId] = $this->container->get($serviceId);
77+
}
78+
79+
\next($stack);
80+
81+
if ($this->handlerObjects[$serviceId] instanceof PrioritisedResponseHandlerInterface) {
82+
$prioritisedHandlers[] = $serviceId;
83+
continue;
84+
} else {
85+
$currentHandler = $serviceId;
86+
break;
87+
}
7588
}
7689

77-
$oldPayload = $payloadEvent->payload;
78-
79-
$this->handlerObjects[$serviceId]->handleResponsePayload($payloadEvent);
80-
81-
if ($payloadEvent->stopPropagation) {
82-
break 3;
90+
$handlers = \count($prioritisedHandlers)
91+
? $this->sortPrioritisedHandlers($prioritisedHandlers, $payloadEvent)
92+
: [];
93+
if (isset($currentHandler)) {
94+
$handlers[] = $currentHandler;
8395
}
8496

85-
if ($payloadEvent->payload !== $oldPayload) {
86-
$usedHandlersPayload[$serviceId][] = $oldPayload;
87-
$usedHandlersLog[] = [$serviceId, $t];
97+
$continueHandling = $this->applyHandlers(
98+
$usedHandlersLog,
99+
$usedHandlersPayload,
100+
$t,
101+
$handlers,
102+
$payloadEvent,
103+
);
88104

105+
if ($continueHandling === true) {
89106
continue 3;
107+
} elseif ($continueHandling === false) {
108+
break 3;
90109
}
91-
}
110+
} while ($currentHandler !== null);
92111
}
93112

94113
break;
95114
} while (true);
96115

97116
return $payloadEvent->payload;
98117
}
118+
119+
/**
120+
* @param string[] $prioritisedHandlers
121+
* @return string[]
122+
*/
123+
protected function sortPrioritisedHandlers(
124+
array $prioritisedHandlers,
125+
ResponsePayloadEvent $responsePayloadEvent
126+
): array {
127+
/** @var PrioritisedResponseHandlerInterface[] */
128+
$handlers = &$this->handlerObjects;
129+
$priorities = [];
130+
foreach ($prioritisedHandlers as $i => $id) {
131+
$priorities[$id] = $handlers[$id]->getResponseHandlerPriority($responsePayloadEvent);
132+
if ($priorities[$id] === null) {
133+
unset($prioritisedHandlers[$i]);
134+
}
135+
}
136+
137+
uasort($prioritisedHandlers, fn(string $id0, string $id1) => $priorities[$id1] <=> $priorities[$id0]);
138+
139+
return $prioritisedHandlers;
140+
}
141+
142+
protected function applyHandlers(
143+
array &$usedHandlersLog,
144+
array &$usedHandlersPayload,
145+
string $type,
146+
array $handlers,
147+
ResponsePayloadEvent $payloadEvent
148+
): ?bool {
149+
foreach ($handlers as $serviceId) {
150+
if (isset($usedHandlersPayload[$serviceId])
151+
&& \in_array($payloadEvent->payload, $usedHandlersPayload[$serviceId], true)
152+
) {
153+
throw new CircularHandlerException($usedHandlersLog);
154+
}
155+
156+
$oldPayload = $payloadEvent->payload;
157+
158+
$this->handlerObjects[$serviceId]->handleResponsePayload($payloadEvent);
159+
160+
if ($payloadEvent->stopPropagation) {
161+
return false;
162+
}
163+
164+
if ($payloadEvent->payload !== $oldPayload) {
165+
$usedHandlersPayload[$serviceId][] = $oldPayload;
166+
$usedHandlersLog[] = [$serviceId, $type];
167+
168+
return true;
169+
}
170+
}
171+
return null;
172+
}
99173
}

test/DependencyInjection/PitchAdrExtensionTest.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
<?php
22
namespace Pitch\AdrBundle\DependencyInjection;
33

4-
use Pitch\AdrBundle\EventSubscriber\ControllerSubscriber;
4+
use Pitch\AdrBundle\EventSubscriber\GracefulSubscriber;
5+
use Pitch\AdrBundle\EventSubscriber\ResponderSubscriber;
56
use Symfony\Component\DependencyInjection\ContainerBuilder;
67

78
class PitchAdrExtensionTest extends \PHPUnit\Framework\TestCase
@@ -28,7 +29,21 @@ public function testInjectGraceful()
2829

2930
$this->assertEquals(
3031
$config['graceful'],
31-
$container->findDefinition(ControllerSubscriber::class)->getArgument('$globalGraceful')
32+
$container->findDefinition(GracefulSubscriber::class)->getArgument('$globalGraceful')
33+
);
34+
}
35+
36+
public function testInjectDefaultContentType()
37+
{
38+
$container = new ContainerBuilder();
39+
$container->setParameter('pitch_adr.defaultContentType', 'foo');
40+
41+
$extension = new PitchAdrExtension();
42+
$extension->load([], $container);
43+
44+
$this->assertEquals(
45+
'foo',
46+
$container->findDefinition(ResponderSubscriber::class)->getArgument('$defaultContentType')
3247
);
3348
}
3449
}

test/EventSubscriber/ControllerSubscriberTest.php renamed to test/EventSubscriber/GracefulSubscriberTest.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
use Symfony\Component\HttpKernel\HttpKernelInterface;
88
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
99

10-
class ControllerSubscriberTest extends EventSubscriberTest
10+
class GracefulSubscriberTest extends EventSubscriberTest
1111
{
1212
public function provideGraceful(): array
1313
{
@@ -50,11 +50,9 @@ public function testSetActionProxy(
5050
$globalGraceful,
5151
$controllerGraceful
5252
) {
53-
$controllerSubscriber = $this->getSubscriberObject($globalGraceful);
54-
5553
$event = $this->getControllerArgumentsEvent($this->getGracefulForArray($controllerGraceful));
5654

57-
$controllerSubscriber->onKernelControllerArguments($event);
55+
$this->getSubscriberObject($globalGraceful)->onKernelControllerArguments($event);
5856

5957
$controller = $event->getController();
6058

@@ -106,8 +104,8 @@ function () {
106104
protected function getSubscriberObject(
107105
array $globalGraceful = [],
108106
bool $reader = false
109-
): ControllerSubscriber {
110-
return new ControllerSubscriber(
107+
): GracefulSubscriber {
108+
return new GracefulSubscriber(
111109
$globalGraceful,
112110
);
113111
}

test/EventSubscriber/ViewSubscriberTest.php renamed to test/EventSubscriber/ResponderSubscriberTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Symfony\Component\HttpFoundation\Response;
99
use Symfony\Component\HttpKernel\HttpKernelInterface;
1010

11-
class ViewSubscriberTest extends EventSubscriberTest
11+
class ResponderSubscriberTest extends EventSubscriberTest
1212
{
1313
public function provideRelayPayload(): array
1414
{
@@ -54,7 +54,7 @@ public function testRelayPayload(
5454

5555
protected function getSubscriberObject(
5656
Responder $responderMock = null
57-
): ViewSubscriber {
58-
return new ViewSubscriber($responderMock ?? $this->createMock(Responder::class));
57+
): ResponderSubscriber {
58+
return new ResponderSubscriber($responderMock ?? $this->createMock(Responder::class));
5959
}
6060
}

0 commit comments

Comments
 (0)