Skip to content

Commit

Permalink
Initial commit for v2.
Browse files Browse the repository at this point in the history
	- Creating MiddlewareManager to handle the compilation of middleware into a callable RequestHandler chain.
	- Creating RouteResolverMiddleware responsible for resolving middleware and attaching route to ServerRequest instance as an attribute.
	- Allow optional MiddlewareManager instance in contructor.
  • Loading branch information
Brent Scheffler committed Nov 25, 2019
1 parent e61a476 commit 010855d
Show file tree
Hide file tree
Showing 12 changed files with 522 additions and 365 deletions.
150 changes: 49 additions & 101 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,59 @@

namespace Limber;

use Limber\Exceptions\ApplicationException;
use Limber\Middleware\CallableMiddleware;
use Limber\Middleware\ExceptionHandlerMiddleware;
use Limber\Exceptions\DispatchException;
use Limber\Middleware\PrepareHttpResponseMiddleware;
use Limber\Middleware\RequestHandler;
use Limber\Middleware\RouteResolverMiddleware;
use Limber\Router\Route;
use Limber\Router\Router;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;

class Application
{
/**
* Router instance.
*
* @var Router
*/
protected $router;

/**
/**
* Global middleware.
*
* @var array<MiddlewareInterface>|array<callable>|array<string>
*/
protected $middleware = [];

/**
* Application-level middleware.
* Application specific middleware.
*
* @var array<MiddlewareInterface>|array<callable>|array<string>
*/
protected $applicationMiddleware = [];

/**
* MiddlewareManager instance.
*
* @var MiddlewareManager
*/
protected $middlewareManager;

/**
* Application constructor.
*
* @param Router $router
*/
public function __construct(Router $router)
public function __construct(Router $router, MiddlewareManager $middlewareManager = null)
{
$this->router = $router;
}
// Create default MiddlewareManager if none provided.
if( empty($middlewareManager) ){
$middlewareManager = new MiddlewareManager;
}

$this->middlewareManager = $middlewareManager;

// Build default application level middleware to be applied.
$this->applicationMiddleware = [
new RouteResolverMiddleware($router, $this->middlewareManager),
new PrepareHttpResponseMiddleware
];
}

/**
* Set the global middleware to run.
Expand All @@ -65,29 +74,38 @@ public function setMiddleware(array $middlewares): void
*/
public function addMiddleware($middleware): void
{
$this->middleware[] = $middleware;
$this->middleware[] = $middleware;
}

/**
* Enables an internal middleware to automatically prepare and normalize your
* responses to better adhere to official HTTP specifications.
* Add a default middleware exception handler.
*
* @param callable $exceptionHandler
* @return void
*/
public function enablePrepareResponse(): void
public function setExceptionHandler(callable $exceptionHandler): void
{
$this->applicationMiddleware[] = new PrepareHttpResponseMiddleware;
$this->middlewareManager->setExceptionHandler($exceptionHandler);
}

/**
* Add a default middleware exception handler.
* Return the kernel callable.
*
* @param callable $exceptionHandler
* @return void
* @return callable
*/
public function setExceptionHandler(callable $exceptionHandler): void
private function getKernel(): callable
{
$this->applicationMiddleware[] = new ExceptionHandlerMiddleware($exceptionHandler);
return function(ServerRequestInterface $request): ResponseInterface {

/** @var Route|null $route */
$route = $request->getAttribute(Route::class);

if( empty($route) ){
throw new DispatchException("Route attribute not found on ServerRequest instance.");
}

return $route->dispatch($request);
};
}

/**
Expand All @@ -99,89 +117,19 @@ public function setExceptionHandler(callable $exceptionHandler): void
*/
public function dispatch(ServerRequestInterface $request): ResponseInterface
{
// Resolve the route now to check for Route middleware.
$route = $this->router->resolve($request);

// Compile the middleware into a RequestHandler chain.
$requestHandler = $this->compileMiddleware(
$requestHandler = $this->middlewareManager->compile(
\array_merge(
$route ? $route->getMiddleware() : [], // Apply Route level middleware last
$this->middleware, // Apply global middleware
$this->applicationMiddleware // Apply application level middleware first
$this->applicationMiddleware,
$this->middleware
),
new Kernel($this->router, $route)
$this->getKernel()
);

// Handle the request
return $requestHandler->handle($request);
}

/**
* Compile middleware into a RequestHandlerInterface chain.
*
* @param array<MiddlewareInterface|string|callable> $middleware
* @param RequestHandlerInterface $kernel
* @return RequestHandlerInterface
*/
private function compileMiddleware(array $middleware, RequestHandlerInterface $kernel): RequestHandlerInterface
{
return $this->buildHandlerChain(
$this->normalizeMiddleware($middleware),
$kernel
);
}

/**
* Build a RequestHandler chain out of middleware using provided Kernel as the final RequestHandler.
*
* @param array<MiddlewareInterface> $middleware
* @param RequestHandlerInterface $kernel
* @return RequestHandlerInterface
*/
private function buildHandlerChain(array $middleware, RequestHandlerInterface $kernel): RequestHandlerInterface
{
$middleware = \array_reverse($middleware);

return \array_reduce($middleware, function(RequestHandlerInterface $handler, MiddlewareInterface $middleware): RequestHandler {

return new RequestHandler(function(ServerRequestInterface $request) use ($handler, $middleware): ResponseInterface {

return $middleware->process($request, $handler);

});

}, $kernel);
}

/**
* Normalize the given middlewares into instances of MiddlewareInterface.
*
* @param array<MiddlewareInterface|callable|string> $middlewares
* @throws ApplicationException
* @return array<MiddlewareInterface>
*/
private function normalizeMiddleware(array $middlewares): array
{
return \array_map(function($middleware): MiddlewareInterface {

if( \is_callable($middleware) ){
$middleware = new CallableMiddleware($middleware);
}

if( \is_string($middleware) &&
\class_exists($middleware) ){
$middleware = new $middleware;
}

if( $middleware instanceof MiddlewareInterface === false ){
throw new ApplicationException("Provided middleware must be a string, a \callable, or an instance of Psr\Http\Server\MiddlewareInterface.");
}

return $middleware;

}, $middlewares);
}

/**
* Send a response back to calling client.
*
Expand Down
48 changes: 48 additions & 0 deletions src/ExceptionManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Limber;

use Psr\Http\Message\ResponseInterface;
use Throwable;

class ExceptionManager
{
/**
* Exception handler instance.
*
* @var callable|null
*/
protected $handler;

public function __construct(callable $handler = null)
{
$this->handler = $handler;
}

/**
* Set the exception handler.
*
* @param callable $handler
* @return void
*/
public function setHandler(callable $handler): void
{
$this->handler = $handler;
}

/**
* Handle an exception.
*
* @param Throwable $exception
* @throws Throwable
* @return ResponseInterface
*/
public function handle(Throwable $exception): ResponseInterface
{
if( empty($this->handler) ){
throw $exception;
}

return \call_user_func($this->handler, $exception);
}
}
9 changes: 9 additions & 0 deletions src/Exceptions/MiddlewareException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Limber\Exceptions;

use Exception;

class MiddlewareException extends Exception
{
}
48 changes: 7 additions & 41 deletions src/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,14 @@

namespace Limber;

use Limber\Exceptions\MethodNotAllowedHttpException;
use Limber\Exceptions\NotFoundHttpException;
use Limber\Exceptions\DispatchException;
use Limber\Router\Route;
use Limber\Router\Router;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class Kernel implements RequestHandlerInterface
{
/**
* Router instance.
*
* @var Router
*/
protected $router;

/**
* Route instance.
*
* @var Route|null
*/
protected $route;

/**
* Kernel constructor.
*
* @param Router $router
* @param Route|null $route
*/
public function __construct(Router $router, ?Route $route)
{
$this->router = $router;
$this->route = $route;
}

/**
* Handle the ServerRequestInteface by passing it off to the Kernel handler.
*
Expand All @@ -46,25 +18,19 @@ public function __construct(Router $router, ?Route $route)
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
if( empty($this->route) ){

$allowedMethods = $this->router->getMethods($request);

// 405 Method Not Allowed
if( $allowedMethods ){
throw new MethodNotAllowedHttpException($allowedMethods);
}
/** @var Route|null $route */
$route = $request->getAttribute(Route::class);

// 404 Not Found
throw new NotFoundHttpException("Route not found");
if( empty($route) ){
throw new DispatchException("Route attribute not found on ServerRequest instance.");
}

return \call_user_func_array(
$this->route->getCallableAction(),
$route->getCallableAction(),
\array_merge(
[$request],
\array_values(
$this->route->getPathParams($request->getUri()->getPath())
$route->getPathParams($request->getUri()->getPath())
)
)
);
Expand Down

0 comments on commit 010855d

Please sign in to comment.