Skip to content

Commit

Permalink
feat: add prioritised handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
ph-fritsche committed May 28, 2021
1 parent e8c941a commit 4cd8dd1
Show file tree
Hide file tree
Showing 15 changed files with 390 additions and 120 deletions.
15 changes: 15 additions & 0 deletions src/Configuration/DefaultContentType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
namespace Pitch\AdrBundle\Configuration;

use Attribute;
use Doctrine\Common\Annotations\Annotation;
use Pitch\Annotation\AbstractAnnotation;

/**
* @Annotation
*/
#[Attribute]
class DefaultContentType extends AbstractAnnotation
{
public ?string $value = null;
}
13 changes: 11 additions & 2 deletions src/DependencyInjection/PitchAdrExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@
namespace Pitch\AdrBundle\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Pitch\AdrBundle\EventSubscriber\ControllerSubscriber;
use Pitch\AdrBundle\EventSubscriber\GracefulSubscriber;
use Pitch\AdrBundle\EventSubscriber\ResponderSubscriber;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;

class PitchAdrExtension extends Extension
{
const ALIAS = 'pitch_adr';
const PARAMETER_DEFAULT_CONTENT_TYPE = 'pitch_adr.defaultContentType';

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

$container->findDefinition(ControllerSubscriber::class)->setArgument('$globalGraceful', $config['graceful']);
$container->findDefinition(GracefulSubscriber::class)->setArgument('$globalGraceful', $config['graceful']);

$container->findDefinition(ResponderSubscriber::class)->setArgument(
'$defaultContentType',
$container->hasParameter(static::PARAMETER_DEFAULT_CONTENT_TYPE)
? $container->getParameter(static::PARAMETER_DEFAULT_CONTENT_TYPE)
: null,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;

class ControllerSubscriber implements EventSubscriberInterface
class GracefulSubscriber implements EventSubscriberInterface
{
private array $globalGraceful;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
<?php
namespace Pitch\AdrBundle\EventSubscriber;

use Pitch\AdrBundle\Configuration\DefaultContentType;
use Pitch\AdrBundle\Responder\Responder;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Pitch\AdrBundle\Responder\ResponsePayloadEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ViewSubscriber implements EventSubscriberInterface
class ResponderSubscriber implements EventSubscriberInterface
{
private Responder $responder;
private ?string $defaultContentType;

public function __construct(
Responder $responder
Responder $responder,
?string $defaultContentType = null
) {
$this->responder = $responder;
$this->defaultContentType = $defaultContentType;
}

public static function getSubscribedEvents()
Expand All @@ -27,9 +31,17 @@ public static function getSubscribedEvents()

public function onKernelView(ViewEvent $event)
{
$request = $event->getRequest();
if ($this->defaultContentType && !$request->attributes->has('_' . DefaultContentType::class)) {
$request->attributes->set(
'_' . DefaultContentType::class,
new DefaultContentType($this->defaultContentType),
);
}

$payloadEvent = new ResponsePayloadEvent(
$event->getControllerResult(),
$event->getRequest(),
$request,
);

$result = $this->responder->handleResponsePayload($payloadEvent);
Expand Down
8 changes: 4 additions & 4 deletions src/Resources/config/adr.php
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
<?php
namespace Pitch\AdrBundle\Resources\config;

use Pitch\AdrBundle\EventSubscriber\ControllerSubscriber;
use Pitch\AdrBundle\EventSubscriber\ViewSubscriber;
use Pitch\AdrBundle\EventSubscriber\GracefulSubscriber;
use Pitch\AdrBundle\EventSubscriber\ResponderSubscriber;
use Pitch\AdrBundle\Responder\Responder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $container) {
$container->services()
->defaults()
->autowire()
->set(ControllerSubscriber::class)
->set(GracefulSubscriber::class)
->tag('kernel.event_subscriber')
->set(ViewSubscriber::class)
->set(ResponderSubscriber::class)
->tag('kernel.event_subscriber')
->set(Responder::class)
;
Expand Down
12 changes: 12 additions & 0 deletions src/Responder/PrioritisedResponseHandlerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
namespace Pitch\AdrBundle\Responder;

interface PrioritisedResponseHandlerInterface extends ResponseHandlerInterface
{
/**
* Consecutive handlers implementing this interface will be executed in the order
* of the return value of this function in descending order.
* If you return `null`, the handler will be skipped.
*/
public function getResponseHandlerPriority(ResponsePayloadEvent $event): ?float;
}
118 changes: 96 additions & 22 deletions src/Responder/Responder.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class Responder
*/
private array $handlerMap = [];

/** @var
/**
* @var ResponseHandlerInterface[] $handlerObjects
* [id => object]
*/
private array $handlerObjects = [];
Expand Down Expand Up @@ -61,39 +62,112 @@ public function handleResponsePayload(
}

foreach ($types as $t) {
foreach ($this->handlerMap[$t] ?? [] as $stackEntry) {
$serviceId = \is_array($stackEntry)? $stackEntry['name'] ?? $stackEntry[0] : (string) $stackEntry;

if (!isset($this->handlerObjects[$serviceId])) {
$this->handlerObjects[$serviceId] = $this->container->get($serviceId);
}

if (isset($usedHandlersPayload[$serviceId])
&& \in_array($payloadEvent->payload, $usedHandlersPayload[$serviceId], true)
) {
throw new CircularHandlerException($usedHandlersLog);
$stack = $this->handlerMap[$t] ?? [];

do {
$prioritisedHandlers = [];
$currentHandler = null;
while ($stackEntry = \current($stack)) {
$serviceId = \is_array($stackEntry)
? $stackEntry['name'] ?? $stackEntry[0]
: (string) $stackEntry;

if (!isset($this->handlerObjects[$serviceId])) {
$this->handlerObjects[$serviceId] = $this->container->get($serviceId);
}

\next($stack);

if ($this->handlerObjects[$serviceId] instanceof PrioritisedResponseHandlerInterface) {
$prioritisedHandlers[] = $serviceId;
continue;
} else {
$currentHandler = $serviceId;
break;
}
}

$oldPayload = $payloadEvent->payload;

$this->handlerObjects[$serviceId]->handleResponsePayload($payloadEvent);

if ($payloadEvent->stopPropagation) {
break 3;
$handlers = \count($prioritisedHandlers)
? $this->sortPrioritisedHandlers($prioritisedHandlers, $payloadEvent)
: [];
if (isset($currentHandler)) {
$handlers[] = $currentHandler;
}

if ($payloadEvent->payload !== $oldPayload) {
$usedHandlersPayload[$serviceId][] = $oldPayload;
$usedHandlersLog[] = [$serviceId, $t];
$continueHandling = $this->applyHandlers(
$usedHandlersLog,
$usedHandlersPayload,
$t,
$handlers,
$payloadEvent,
);

if ($continueHandling === true) {
continue 3;
} elseif ($continueHandling === false) {
break 3;
}
}
} while ($currentHandler !== null);
}

break;
} while (true);

return $payloadEvent->payload;
}

/**
* @param string[] $prioritisedHandlers
* @return string[]
*/
protected function sortPrioritisedHandlers(
array $prioritisedHandlers,
ResponsePayloadEvent $responsePayloadEvent
): array {
/** @var PrioritisedResponseHandlerInterface[] */
$handlers = &$this->handlerObjects;
$priorities = [];
foreach ($prioritisedHandlers as $i => $id) {
$priorities[$id] = $handlers[$id]->getResponseHandlerPriority($responsePayloadEvent);
if ($priorities[$id] === null) {
unset($prioritisedHandlers[$i]);
}
}

uasort($prioritisedHandlers, fn(string $id0, string $id1) => $priorities[$id1] <=> $priorities[$id0]);

return $prioritisedHandlers;
}

protected function applyHandlers(
array &$usedHandlersLog,
array &$usedHandlersPayload,
string $type,
array $handlers,
ResponsePayloadEvent $payloadEvent
): ?bool {
foreach ($handlers as $serviceId) {
if (isset($usedHandlersPayload[$serviceId])
&& \in_array($payloadEvent->payload, $usedHandlersPayload[$serviceId], true)
) {
throw new CircularHandlerException($usedHandlersLog);
}

$oldPayload = $payloadEvent->payload;

$this->handlerObjects[$serviceId]->handleResponsePayload($payloadEvent);

if ($payloadEvent->stopPropagation) {
return false;
}

if ($payloadEvent->payload !== $oldPayload) {
$usedHandlersPayload[$serviceId][] = $oldPayload;
$usedHandlersLog[] = [$serviceId, $type];

return true;
}
}
return null;
}
}
19 changes: 17 additions & 2 deletions test/DependencyInjection/PitchAdrExtensionTest.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<?php
namespace Pitch\AdrBundle\DependencyInjection;

use Pitch\AdrBundle\EventSubscriber\ControllerSubscriber;
use Pitch\AdrBundle\EventSubscriber\GracefulSubscriber;
use Pitch\AdrBundle\EventSubscriber\ResponderSubscriber;
use Symfony\Component\DependencyInjection\ContainerBuilder;

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

$this->assertEquals(
$config['graceful'],
$container->findDefinition(ControllerSubscriber::class)->getArgument('$globalGraceful')
$container->findDefinition(GracefulSubscriber::class)->getArgument('$globalGraceful')
);
}

public function testInjectDefaultContentType()
{
$container = new ContainerBuilder();
$container->setParameter('pitch_adr.defaultContentType', 'foo');

$extension = new PitchAdrExtension();
$extension->load([], $container);

$this->assertEquals(
'foo',
$container->findDefinition(ResponderSubscriber::class)->getArgument('$defaultContentType')
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;

class ControllerSubscriberTest extends EventSubscriberTest
class GracefulSubscriberTest extends EventSubscriberTest
{
public function provideGraceful(): array
{
Expand Down Expand Up @@ -50,11 +50,9 @@ public function testSetActionProxy(
$globalGraceful,
$controllerGraceful
) {
$controllerSubscriber = $this->getSubscriberObject($globalGraceful);

$event = $this->getControllerArgumentsEvent($this->getGracefulForArray($controllerGraceful));

$controllerSubscriber->onKernelControllerArguments($event);
$this->getSubscriberObject($globalGraceful)->onKernelControllerArguments($event);

$controller = $event->getController();

Expand Down Expand Up @@ -106,8 +104,8 @@ function () {
protected function getSubscriberObject(
array $globalGraceful = [],
bool $reader = false
): ControllerSubscriber {
return new ControllerSubscriber(
): GracefulSubscriber {
return new GracefulSubscriber(
$globalGraceful,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;

class ViewSubscriberTest extends EventSubscriberTest
class ResponderSubscriberTest extends EventSubscriberTest
{
public function provideRelayPayload(): array
{
Expand Down Expand Up @@ -54,7 +54,7 @@ public function testRelayPayload(

protected function getSubscriberObject(
Responder $responderMock = null
): ViewSubscriber {
return new ViewSubscriber($responderMock ?? $this->createMock(Responder::class));
): ResponderSubscriber {
return new ResponderSubscriber($responderMock ?? $this->createMock(Responder::class));
}
}
Loading

0 comments on commit 4cd8dd1

Please sign in to comment.