Skip to content

Commit

Permalink
!!!TASK: Bootstrap instantiates request handlers internally
Browse files Browse the repository at this point in the history
It seems unnecessarily complex to have code register a request handler
as build instance. Instead it is now registered by class name.
The Flow Bootstrap will then create instances while determining the
best matching request handler. To facilitate this being lazy the
RequestHandlerInterface::getPriority method is now declared static so
that we can decide beforehand if the priority is higher than any
previously found best match.
Depending on possibly request handlers and specifically in all preselected
cases this all reduces the amount of created objects and possibly the
amount of canHandle calls. A microptimization but which affects all
requests. Primary concern was to make registration easier and internalize
instantiation.

As small simplification the
`Bootstrap->setPreselectedRequestHandlerClassName` will also register
the given class name as possible request handler removing one step.

This is breaking if you have your own request handler implementation.
The `getPrioritty` method needs to declare an `int` return type and
must be static. Additionally the registration happens via
`Bootstrap->registerRequestHandlerClassName(YourRequestHandler::class)`.
  • Loading branch information
kitsunet committed Jun 23, 2024
1 parent c7625ec commit ae54ccd
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 73 deletions.
12 changes: 7 additions & 5 deletions Neos.Flow/Classes/Cli/CommandRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,12 @@ class CommandRequestHandler implements RequestHandlerInterface
*/
protected $response;

/**
* @param Bootstrap $bootstrap
*/
public function __construct(Bootstrap $bootstrap)
public static function fromBootstrap(Bootstrap $bootstrap): RequestHandlerInterface
{
return new self($bootstrap);
}

private function __construct(Bootstrap $bootstrap)
{
$this->bootstrap = $bootstrap;
}
Expand All @@ -75,7 +77,7 @@ public function canHandleRequest(): bool
*
* @return integer The priority of the request handler.
*/
public function getPriority(): int
public static function getPriority(): int
{
return 100;
}
Expand Down
14 changes: 7 additions & 7 deletions Neos.Flow/Classes/Cli/SlaveRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ class SlaveRequestHandler implements RequestHandlerInterface
*/
protected $bootstrap;

/**
* Constructor
*
* @param Bootstrap $bootstrap
*/
public function __construct(Bootstrap $bootstrap)
public static function fromBootstrap(Bootstrap $bootstrap): RequestHandlerInterface
{
return new self($bootstrap);
}

private function __construct(Bootstrap $bootstrap)
{
$this->bootstrap = $bootstrap;
}
Expand All @@ -61,7 +61,7 @@ public function canHandleRequest(): bool
*
* @return integer The priority of the request handler.
*/
public function getPriority(): int
public static function getPriority(): int
{
return 200;
}
Expand Down
95 changes: 62 additions & 33 deletions Neos.Flow/Classes/Core/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,22 @@
*/
class Bootstrap
{
const RUNLEVEL_COMPILETIME = 'Compiletime';
const RUNLEVEL_RUNTIME = 'Runtime';
public const RUNLEVEL_COMPILETIME = 'Compiletime';
public const RUNLEVEL_RUNTIME = 'Runtime';

/**
* @var ApplicationContext
*/
protected $context;
protected ApplicationContext $context;

/**
* @var array<class-string<RequestHandlerInterface>, RequestHandlerInterface>
* @var array<int, class-string>
*/
protected $requestHandlers = [];
private array $requestHandlerClassNames = [];

/**
* @var class-string<RequestHandlerInterface>
* @var class-string<RequestHandlerInterface>|null
*/
protected $preselectedRequestHandlerClassName;
protected string|null $preselectedRequestHandlerClassName = null;

/**
* @var RequestHandlerInterface
*/
protected $activeRequestHandler;
protected RequestHandlerInterface $activeRequestHandler;

/**
* The same instance like $objectManager, but static, for use in the proxy classes.
Expand Down Expand Up @@ -144,28 +138,33 @@ public function getContext(): ApplicationContext
}

/**
* Registers a request handler which can possibly handle a request.
* All registered request handlers will be queried if they can handle a request
* when the bootstrap's run() method is called.
* Registers a request handler which can possibly handle a request.
* All registered request handlers will be queried if they can handle a request
* when the bootstrap's run() method is called.
*
* @param RequestHandlerInterface $requestHandler
* @param class-string<RequestHandlerInterface> $className
* @return void
* @api
*/
public function registerRequestHandler(RequestHandlerInterface $requestHandler)
public function registerRequestHandlerClassName(string $className): void
{
$this->requestHandlers[get_class($requestHandler)] = $requestHandler;
// No checks at this point in time as we might not be able to autolaod the class yet.
$this->requestHandlerClassNames[] = $className;
}

/**
* Preselects a specific request handler. If such a request handler exists,
* it will be used if it can handle the request – regardless of the priority
* of this or other request handlers.
*
* This will also register the classname as a possible request handler.
*
* @param class-string<RequestHandlerInterface> $className
*/
public function setPreselectedRequestHandlerClassName(string $className)
public function setPreselectedRequestHandlerClassName(string $className): void
{
if (!in_array($className, $this->requestHandlerClassNames, true)) {
$this->registerRequestHandlerClassName($className);
}
$this->preselectedRequestHandlerClassName = $className;
}

Expand All @@ -191,7 +190,7 @@ public function getActiveRequestHandler(): RequestHandlerInterface
* @param RequestHandlerInterface $requestHandler
* @return void
*/
public function setActiveRequestHandler(RequestHandlerInterface $requestHandler)
public function setActiveRequestHandler(RequestHandlerInterface $requestHandler): void
{
$this->activeRequestHandler = $requestHandler;
}
Expand Down Expand Up @@ -231,7 +230,7 @@ public function isCompiletimeCommand(string $commandIdentifier): bool
$shortControllerIdentifier = implode(':', $commandIdentifierParts);

foreach ($this->compiletimeCommands as $fullControllerIdentifier => $isCompiletimeCommandController) {
list($packageKey, $controllerName, $commandName) = explode(':', $fullControllerIdentifier);
[$packageKey, $controllerName, $commandName] = explode(':', $fullControllerIdentifier);
$packageKeyParts = explode('.', $packageKey);
$packageKeyPartsCount = count($packageKeyParts);
for ($offset = 0; $offset < $packageKeyPartsCount; $offset++) {
Expand Down Expand Up @@ -404,27 +403,57 @@ public function getObjectManager(): ObjectManagerInterface
*/
protected function resolveRequestHandler(): RequestHandlerInterface
{
if ($this->preselectedRequestHandlerClassName !== null && isset($this->requestHandlers[$this->preselectedRequestHandlerClassName])) {
$requestHandler = $this->requestHandlers[$this->preselectedRequestHandlerClassName];
if ($this->preselectedRequestHandlerClassName !== null) {
$requestHandler = $this->getRequestHandlerInstance($this->preselectedRequestHandlerClassName);
if ($requestHandler->canHandleRequest()) {
return $requestHandler;
}
}

foreach ($this->requestHandlers as $requestHandler) {
/** @var RequestHandlerInterface|null $bestMatchingRequestHandler */
$bestMatchingRequestHandler = null;
$bestMatchingPriority = -9999;
foreach ($this->requestHandlerClassNames as $requestHandlerClassName) {
if ($bestMatchingPriority > $requestHandlerClassName::getPriority()) {
continue;
}

$requestHandler = $this->getRequestHandlerInstance($requestHandlerClassName);
if ($requestHandler->canHandleRequest() > 0) {
$priority = $requestHandler->getPriority();
if (isset($suitableRequestHandlers[$priority])) {
$priority = $requestHandler::getPriority();
if ($bestMatchingPriority === $priority) {
throw new FlowException('More than one request handler with the same priority can handle the request, but only one handler may be active at a time!', 1176475350);
}
$suitableRequestHandlers[$priority] = $requestHandler;
$bestMatchingPriority = $priority;
$bestMatchingRequestHandler = $requestHandler;
}
}
if (empty($suitableRequestHandlers)) {

if (is_null($bestMatchingRequestHandler)) {
throw new FlowException('No suitable request handler could be found for the current request. This is most likely a setup-problem, so please check your composer.json and/or try removing Configuration/PackageStates.php', 1464882543);
}
ksort($suitableRequestHandlers);
return array_pop($suitableRequestHandlers);
return $bestMatchingRequestHandler;
}

/**
* @param class-string $className
* @return RequestHandlerInterface
*/
private function getRequestHandlerInstance(string $className): RequestHandlerInterface
{
if (!in_array($className, $this->requestHandlerClassNames, true)) {
throw new FlowException(sprintf('A request handler "%s" was never registered', $className), 1719060769);
}

if (isset($this->earlyInstances[$className]) && $this->earlyInstances[$className] instanceof RequestHandlerInterface) {
return $this->earlyInstances[$className];
}

if (!in_array(RequestHandlerInterface::class, class_implements($className), true)) {
throw new FlowException(sprintf('The class "%s" registered as request handler must implement the RequestHandlerInterface interface.', $className), 1719132434);
}

return $className::fromBootstrap($this);
}

/**
Expand Down
11 changes: 10 additions & 1 deletion Neos.Flow/Classes/Core/RequestHandlerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,14 @@ public function canHandleRequest();
* @return integer The priority of the request handler
* @api
*/
public function getPriority();
public static function getPriority(): int;

/**
* This is the API to create an instance of a request handler the bootstrap will use internally.
*
* @param Bootstrap $bootstrap
* @return self
* @api
*/
public static function fromBootstrap(Bootstrap $bootstrap): self;
}
15 changes: 9 additions & 6 deletions Neos.Flow/Classes/Http/RequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use GuzzleHttp\Psr7\ServerRequest;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Core\RequestHandlerInterface;
use Neos\Flow\Http\Helper\ResponseInformationHelper;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
Expand Down Expand Up @@ -52,13 +53,15 @@ class RequestHandler implements HttpRequestHandlerInterface
*/
public $exit;

/**
* @param Bootstrap $bootstrap
*/
public function __construct(Bootstrap $bootstrap)
public static function fromBootstrap(Bootstrap $bootstrap): RequestHandlerInterface
{
return new self($bootstrap);
}

private function __construct(Bootstrap $bootstrap)
{
$this->bootstrap = $bootstrap;
$this->exit = function () {
$this->exit = static function () {
exit();
};
}
Expand All @@ -81,7 +84,7 @@ public function canHandleRequest()
* @return integer The priority of the request handler.
* @api
*/
public function getPriority()
public static function getPriority(): int
{
return 100;
}
Expand Down
8 changes: 4 additions & 4 deletions Neos.Flow/Classes/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,17 +49,17 @@ public function boot(Core\Bootstrap $bootstrap)
$context = $bootstrap->getContext();

if (PHP_SAPI === 'cli') {
$bootstrap->registerRequestHandler(new Cli\SlaveRequestHandler($bootstrap));
$bootstrap->registerRequestHandler(new Cli\CommandRequestHandler($bootstrap));
$bootstrap->registerRequestHandlerClassName(Cli\SlaveRequestHandler::class);
$bootstrap->registerRequestHandlerClassName(Cli\CommandRequestHandler::class);
} else {
$bootstrap->registerRequestHandler(new Http\RequestHandler($bootstrap));
$bootstrap->registerRequestHandlerClassName(Http\RequestHandler::class);
}

if ($context->isTesting()) {
// TODO: This is technically not necessary as we can register the request handler in the functional bootstrap
// A future commit will remove this aftter BuildEssentials is adapted
/** @phpstan-ignore-next-line composer doesnt autoload this class */
$bootstrap->registerRequestHandler(new Tests\FunctionalTestRequestHandler($bootstrap));
$bootstrap->registerRequestHandlerClassName(Tests\FunctionalTestRequestHandler::class);
}

$bootstrap->registerCompiletimeCommand('neos.flow:core:*');
Expand Down
12 changes: 11 additions & 1 deletion Neos.Flow/Classes/Testing/RequestHandler/EmptyRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Neos\Flow\Testing\RequestHandler;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Core\RequestHandlerInterface;
use Neos\Flow\Tests\PhpBench\Core\BootstrapBench;

Expand All @@ -24,6 +25,15 @@
*/
final class EmptyRequestHandler implements RequestHandlerInterface
{
public static function fromBootstrap(Bootstrap $bootstrap): RequestHandlerInterface
{
return new self();
}

private function __construct()
{
}

public function handleRequest(): void
{
}
Expand All @@ -33,7 +43,7 @@ public function canHandleRequest(): bool
return true;
}

public function getPriority(): int
public static function getPriority(): int
{
return 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ class RuntimeSequenceInvokingRequestHandler implements RequestHandlerInterface
{
protected Bootstrap $bootstrap;

/**
* Constructor
*
* @param \Neos\Flow\Core\Bootstrap $bootstrap
*/
public function __construct(Bootstrap $bootstrap)
public static function fromBootstrap(Bootstrap $bootstrap): RequestHandlerInterface
{
return new self($bootstrap);
}

private function __construct(Bootstrap $bootstrap)
{
$this->bootstrap = $bootstrap;
}
Expand All @@ -56,7 +56,7 @@ public function canHandleRequest(): bool
*
* @return integer The priority of the request handler.
*/
public function getPriority(): int
public static function getPriority(): int
{
return 0;
}
Expand Down
15 changes: 8 additions & 7 deletions Neos.Flow/Tests/FunctionalTestRequestHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use GuzzleHttp\Psr7\ServerRequest;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Core\RequestHandlerInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
Expand Down Expand Up @@ -45,12 +46,12 @@ class FunctionalTestRequestHandler implements \Neos\Flow\Http\HttpRequestHandler
*/
protected $httpRequest;

/**
* Constructor
*
* @param \Neos\Flow\Core\Bootstrap $bootstrap
*/
public function __construct(Bootstrap $bootstrap)
public static function fromBootstrap(Bootstrap $bootstrap): RequestHandlerInterface
{
return new self($bootstrap);
}

private function __construct(Bootstrap $bootstrap)
{
$this->bootstrap = $bootstrap;
}
Expand All @@ -74,7 +75,7 @@ public function canHandleRequest()
*
* @return integer The priority of the request handler.
*/
public function getPriority()
public static function getPriority(): int
{
return 0;
}
Expand Down
Loading

0 comments on commit ae54ccd

Please sign in to comment.