You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
6.5 KiB
PHP
218 lines
6.5 KiB
PHP
<?php
|
|
|
|
namespace BitCommunism\Http;
|
|
|
|
|
|
use DI\Container;
|
|
use FastRoute\Dispatcher;
|
|
use FastRoute\RouteCollector;
|
|
use GuzzleHttp\Psr7\Response;
|
|
use GuzzleHttp\Psr7\ServerRequest;
|
|
use Invoker\Invoker;
|
|
use Invoker\ParameterResolver\AssociativeArrayResolver;
|
|
use Invoker\ParameterResolver\Container\TypeHintContainerResolver;
|
|
use Invoker\ParameterResolver\DefaultValueResolver;
|
|
use Invoker\ParameterResolver\NumericArrayResolver;
|
|
use Invoker\ParameterResolver\ResolverChain;
|
|
use Invoker\ParameterResolver\TypeHintResolver;
|
|
use Psr\Http\Message\ResponseInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use function FastRoute\simpleDispatcher;
|
|
use function GuzzleHttp\Psr7\copy_to_stream;
|
|
use function GuzzleHttp\Psr7\stream_for;
|
|
|
|
class Server
|
|
{
|
|
private $routes = [];
|
|
private $container;
|
|
/**
|
|
* @var Invoker
|
|
*/
|
|
private $invoker;
|
|
|
|
/**
|
|
* @var Dispatcher
|
|
*/
|
|
private $router;
|
|
|
|
public function __construct($routes, Container $container)
|
|
{
|
|
$this->routes = $routes;
|
|
$this->container = $container;
|
|
$this->invoker = $this->createDefaultInvoker();
|
|
}
|
|
|
|
private function iterateRoutes()
|
|
{
|
|
yield from $this->iterateRoutesFromArray($this->routes);
|
|
}
|
|
|
|
private function iterateRoutesFromArray($routeArr, $prefix = '', $known = [], $middleware = [])
|
|
{
|
|
$route = $routeArr['middleware'] ?? [];
|
|
$middleware = array_merge($middleware, array_map(function ($route) {
|
|
if (!$route instanceof Handle) {
|
|
return new Handle($route);
|
|
}
|
|
|
|
return $route;
|
|
}, (array)$route));
|
|
|
|
foreach ($routeArr as $key => $route) {
|
|
if ($key === 'middleware') {
|
|
continue;
|
|
}
|
|
|
|
if (is_array($route) && !(isset($route[0]) && isset($route[1]))) {
|
|
yield from $this->iterateRoutesFromArray($route, $prefix . $key, $known, $middleware);
|
|
continue;
|
|
}
|
|
|
|
if (!$route instanceof Handle) {
|
|
$route = new Handle($route);
|
|
}
|
|
|
|
$route = $route->withPrependedMiddleware($middleware);
|
|
|
|
if ($key[0] !== '/') {
|
|
yield [$key, $prefix, $route];
|
|
|
|
continue;
|
|
}
|
|
|
|
yield ['GET', $prefix . $key, $route];
|
|
}
|
|
}
|
|
|
|
public function buildRoutes()
|
|
{
|
|
$this->router = simpleDispatcher(function (RouteCollector $collector) {
|
|
foreach ($this->iterateRoutes() as $route) {
|
|
$collector->addRoute(...$route);
|
|
}
|
|
});
|
|
}
|
|
|
|
public function serve()
|
|
{
|
|
$request = ServerRequest::fromGlobals();
|
|
|
|
$response = $this->respond($request);
|
|
|
|
header('HTTP/' . $response->getProtocolVersion() . ' ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase());
|
|
|
|
foreach ($response->getHeaders() as $headerName => $headerValues) {
|
|
$first = true;
|
|
foreach ($headerValues as $headerValue) {
|
|
header($headerName . ': ' . $headerValue, $first === true);
|
|
$first = false;
|
|
}
|
|
}
|
|
|
|
$stdout = fopen('php://output', 'w+');
|
|
|
|
copy_to_stream($response->getBody(), stream_for($stdout));
|
|
}
|
|
|
|
public function respond(ServerRequestInterface $request, ResponseInterface $response = null): ResponseInterface
|
|
{
|
|
$path = $request->getUri()->getPath();
|
|
$method = $request->getMethod();
|
|
$routeInfo = $this->router->dispatch($method, $path);
|
|
|
|
switch ($routeInfo[0]) {
|
|
case Dispatcher::NOT_FOUND:
|
|
// todo: Error page
|
|
break;
|
|
case Dispatcher::METHOD_NOT_ALLOWED:
|
|
// todo: Error page
|
|
break;
|
|
case Dispatcher::FOUND:
|
|
[$_, $handler, $vars] = $routeInfo;
|
|
$response = $this->handleHttp($request, $handler, $vars, $response);
|
|
break;
|
|
}
|
|
|
|
if ($response === null) {
|
|
$response = (new Response())
|
|
->withStatus(200);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
private function createDefaultInvoker() {
|
|
return new Invoker(new ResolverChain(array(
|
|
new NumericArrayResolver(),
|
|
new AssociativeArrayResolver(),
|
|
new DefaultValueResolver(),
|
|
new TypeHintResolver(),
|
|
new TypeHintContainerResolver($this->container),
|
|
)), $this->container);
|
|
}
|
|
|
|
public function handleHttp(ServerRequestInterface $request, Handle $handler, array $vars, ResponseInterface $response = null): ResponseInterface
|
|
{
|
|
$middlewareArray = $handler->getMiddleware();
|
|
$middlewarePointer = 0;
|
|
|
|
$next = null;
|
|
|
|
if ($response === null) {
|
|
$response = new Response(200, [], stream_for('Empty response'));
|
|
}
|
|
|
|
$context = new Context($request, $response, $vars);
|
|
|
|
$next = function () use ($context, $handler, $middlewareArray, &$middlewarePointer, &$next) {
|
|
$output = null;
|
|
|
|
if (count($middlewareArray) > $middlewarePointer) {
|
|
$handler = $middlewareArray[$middlewarePointer++];
|
|
}
|
|
|
|
$call = $handler->getCall();
|
|
if (is_array($call)) {
|
|
$obj = $this->container->get($call[0]);
|
|
$call = [$obj, $call[1]];
|
|
}
|
|
|
|
$context->setSettings($handler->getSettings());
|
|
|
|
$output = $this->invoker->call($call, [
|
|
'request' => $context->getRequest(),
|
|
ServerRequestInterface::class => $context->getRequest(),
|
|
'response' => $context->getResponse(),
|
|
ResponseInterface::class => $context->getResponse(),
|
|
'context' => $context,
|
|
Context::class => $context,
|
|
'vars' => $context->getVars(),
|
|
'settings' => $context->getSettings(),
|
|
'next' => $next,
|
|
]);
|
|
|
|
$this->normalizeResponse($output, $context);
|
|
};
|
|
|
|
$next();
|
|
|
|
return $context->renderResponse();
|
|
}
|
|
|
|
private function normalizeResponse($response, Context $context)
|
|
{
|
|
if ($response === null) {
|
|
return;
|
|
}
|
|
|
|
if ($response instanceof ResponseInterface) {
|
|
$context->setResponse($response);
|
|
return;
|
|
}
|
|
|
|
$body = stream_for($response);
|
|
$context->withResponse(function (ResponseInterface $response) use ($body) {
|
|
return $response->withBody($body);
|
|
});
|
|
}
|
|
} |