Added HandlerContext and ->call method on Dependencies.
This commit is contained in:
parent
a5f9e9121d
commit
d0b9b2d556
Notes:
flash
2025-03-07 02:51:12 +00:00
i hope i remember to check this but fuck the term middleware actually; preprocessor and postprocessor maybe instead maybe??
9 changed files with 209 additions and 47 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2503.70031
|
||||
0.2503.70242
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<?php
|
||||
// Dependencies.php
|
||||
// Created: 2025-01-18
|
||||
// Updated: 2025-01-22
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index;
|
||||
|
||||
use Closure;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionClass;
|
||||
use ReflectionException;
|
||||
use ReflectionFunction;
|
||||
use ReflectionFunctionAbstract;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionParameter;
|
||||
use RuntimeException;
|
||||
|
||||
class Dependencies {
|
||||
|
@ -35,12 +36,18 @@ class Dependencies {
|
|||
* @return array<string|int, mixed>
|
||||
*/
|
||||
private function resolveArgs(ReflectionFunctionAbstract $constructor, array $args): array {
|
||||
if($constructor->getNumberOfRequiredParameters() > 0 && (empty($args) || !array_is_list($args))) {
|
||||
$params = XArray::where(
|
||||
$constructor->getParameters(),
|
||||
fn(ReflectionParameter $param) => !$param->isOptional() && $param->hasType() && !array_key_exists($param->getName(), $args)
|
||||
);
|
||||
foreach($params as $paramInfo) {
|
||||
$realArgs = [];
|
||||
|
||||
$params = $constructor->getParameters();
|
||||
foreach($params as $paramInfo) {
|
||||
$paramName = $paramInfo->getName();
|
||||
if(array_key_exists($paramName, $args)) {
|
||||
$realArgs[] = $args[$paramName];
|
||||
unset($args[$paramName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$typeInfo = $paramInfo->getType();
|
||||
$allowsNull = $typeInfo === null || $typeInfo->allowsNull();
|
||||
$value = null;
|
||||
|
@ -58,11 +65,35 @@ class Dependencies {
|
|||
|
||||
if($value === null && !$allowsNull)
|
||||
throw new RuntimeException('required parameter has unsupported type definition');
|
||||
$args[$paramInfo->getName()] = $value;
|
||||
|
||||
$realArgs[] = $value;
|
||||
continue;
|
||||
} catch(RuntimeException $ex) {
|
||||
if(count($args) > 0) {
|
||||
$realArgs[] = array_shift($args);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!$paramInfo->isOptional())
|
||||
throw $ex;
|
||||
|
||||
$realArgs[] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
return $realArgs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a closure using dependencies.
|
||||
*
|
||||
* @param Closure $closure Function to call.
|
||||
* @param mixed ...$args Additional arguments to be passed to the constructor.
|
||||
* @return mixed Return value of the function or method.
|
||||
*/
|
||||
public function call(Closure $closure, mixed ...$args): mixed {
|
||||
$function = new ReflectionFunction($closure);
|
||||
return $function->invokeArgs($this->resolveArgs($function, $args));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,7 +101,7 @@ class Dependencies {
|
|||
*
|
||||
* @template T of object
|
||||
* @param class-string<T> $class Class string.
|
||||
* @param mixed ...$args Arguments to be passed to the constructor.
|
||||
* @param mixed ...$args Additional arguments to be passed to the constructor.
|
||||
* @return T
|
||||
*/
|
||||
public function construct(string $class, mixed ...$args): object {
|
||||
|
|
|
@ -11,7 +11,7 @@ use Closure;
|
|||
/**
|
||||
* Provides an attribute for marking methods in a class as a filter.
|
||||
*/
|
||||
abstract class FilterAttribute extends RouterAttribute {
|
||||
abstract class FilterAttribute extends HandlerAttribute {
|
||||
/**
|
||||
* Flags to set for the Attribute attribute.
|
||||
*/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?php
|
||||
// RouterAttribute.php
|
||||
// HandlerAttribute.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2025-03-07
|
||||
|
||||
|
@ -11,7 +11,7 @@ use ReflectionObject;
|
|||
/**
|
||||
* Provides base for attributes that mark methods in a class as handlers.
|
||||
*/
|
||||
abstract class RouterAttribute {
|
||||
abstract class HandlerAttribute {
|
||||
/**
|
||||
* Reads attributes from methods in a RouteHandler instance and registers them to a given Router instance.
|
||||
*
|
||||
|
@ -23,7 +23,7 @@ abstract class RouterAttribute {
|
|||
$methodInfos = $objectInfo->getMethods();
|
||||
|
||||
foreach($methodInfos as $methodInfo) {
|
||||
$attrInfos = $methodInfo->getAttributes(RouterAttribute::class, ReflectionAttribute::IS_INSTANCEOF);
|
||||
$attrInfos = $methodInfo->getAttributes(self::class, ReflectionAttribute::IS_INSTANCEOF);
|
||||
|
||||
foreach($attrInfos as $attrInfo) {
|
||||
$handlerInfo = $attrInfo->newInstance();
|
95
src/Http/Routing/HandlerContext.php
Normal file
95
src/Http/Routing/HandlerContext.php
Normal file
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
// HandlerContext.php
|
||||
// Created: 2025-03-07
|
||||
// Updated: 2025-03-07
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
use Closure;
|
||||
use ReflectionFunction;
|
||||
use Index\Dependencies;
|
||||
use Index\Http\{HttpResponseBuilder,HttpRequest};
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
/**
|
||||
* Contains the context for the routing pipeline.
|
||||
*/
|
||||
class HandlerContext {
|
||||
/**
|
||||
* Contains the response builder.
|
||||
*/
|
||||
public private(set) HttpResponseBuilder $response;
|
||||
|
||||
/**
|
||||
* Contains the request.
|
||||
*/
|
||||
public private(set) HttpRequest $request;
|
||||
|
||||
/**
|
||||
* Dependency bag for the handlers.
|
||||
*/
|
||||
public private(set) Dependencies $deps;
|
||||
|
||||
/**
|
||||
* Contains arguments passed to the route handler.
|
||||
* Not including ones matches on the route's path or whatever.
|
||||
*
|
||||
* @var mixed[]
|
||||
*/
|
||||
public private(set) array $args = [];
|
||||
|
||||
/**
|
||||
* If true, the pipeline must be halted and $response must be output.
|
||||
*/
|
||||
public private(set) bool $stopped = false;
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request Request that is being handled.
|
||||
*/
|
||||
public function __construct(
|
||||
ServerRequestInterface $request
|
||||
) {
|
||||
$this->deps = new Dependencies;
|
||||
$this->deps->register($this);
|
||||
$this->deps->register($this->response = new HttpResponseBuilder);
|
||||
$this->deps->register($this->request = HttpRequest::castRequest($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets $stopped to true to indicate that the handling pipeline should be stopped.
|
||||
*/
|
||||
public function halt(): void {
|
||||
$this->stopped = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an argument to be used in method calls.
|
||||
*
|
||||
* @param mixed $value Argument to register
|
||||
*/
|
||||
public function addArgument(mixed $value): void {
|
||||
$this->args[] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a named argument to be used in method calls.
|
||||
*
|
||||
* @param string $name Name of the argument.
|
||||
* @param mixed $value Argument to register
|
||||
*/
|
||||
public function setArgument(string $name, mixed $value): void {
|
||||
$this->args[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a closure using dependencies and arguments.
|
||||
*
|
||||
* @param Closure $closure Function to call.
|
||||
* @param ?mixed[] $args Additional arguments to be passed to the constructor.
|
||||
* @return mixed Return value of the function or method.
|
||||
*/
|
||||
public function call(Closure $closure, ?array $args = null): mixed {
|
||||
$args = $args === null ? $this->args : array_merge($this->args, $args);
|
||||
return $this->deps->call($closure, ...$args);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ use Closure;
|
|||
/**
|
||||
* Provides an attribute for marking methods in a class as a route.
|
||||
*/
|
||||
abstract class RouteAttribute extends RouterAttribute {
|
||||
abstract class RouteAttribute extends HandlerAttribute {
|
||||
/**
|
||||
* Flags to set for the Attribute attribute.
|
||||
*/
|
||||
|
|
|
@ -7,10 +7,10 @@ namespace Index\Http\Routing;
|
|||
|
||||
/**
|
||||
* Provides an implementation of RouteHandler::registerRoutes that uses the attributes.
|
||||
* For more advanced use, everything can be use'd separately and RouterAttribute::register called manually.
|
||||
* For more advanced use, everything can be use'd separately and HandlerAttribute::register called manually.
|
||||
*/
|
||||
trait RouteHandlerCommon {
|
||||
public function registerRoutes(Router $router): void {
|
||||
RouterAttribute::register($router, $this);
|
||||
HandlerAttribute::register($router, $this);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,18 @@ use Index\Http\{
|
|||
use Psr\Http\Message\{ResponseInterface,ServerRequestInterface,UriInterface};
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
/**
|
||||
* Implements a router for HTTP requests.
|
||||
*
|
||||
* There are three different kinds of handler you can register.
|
||||
* - Filter: Always runs on a given path or prefix regardless of a route being matched.
|
||||
* - Middleware: Must be explicitly imported by a route handler. Middleware is used to transform the input or output of a route.
|
||||
* - Route: An actual proper handler for a request.
|
||||
*
|
||||
* [HTTP Request] -> [Run Filters] -> [Run 'before' Middleware] -> [Run route handler] -> [Run 'after' Middleware] -> [HTTP Response]
|
||||
*
|
||||
* 'before' middleware has the same abilities as filters do, but as mentioned are included explicitly.
|
||||
*/
|
||||
class Router implements RequestHandlerInterface {
|
||||
/** @var FilterInfo[] */
|
||||
private array $filters = [];
|
||||
|
@ -115,22 +127,17 @@ class Router implements RequestHandlerInterface {
|
|||
}
|
||||
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface {
|
||||
$request = HttpRequest::castRequest($request);
|
||||
$builder = new HttpResponseBuilder;
|
||||
$args = [$builder, $request];
|
||||
|
||||
$path = $request->uri->getPath();
|
||||
if(str_ends_with($path, '/'))
|
||||
$path = substr($path, 0, -1);
|
||||
$context = new HandlerContext($request);
|
||||
$args = [$context->response, $context->request];
|
||||
|
||||
$filters = [];
|
||||
|
||||
foreach($this->filters as $filterInfo) {
|
||||
$match = $filterInfo->matcher->match($request->uri);
|
||||
$match = $filterInfo->matcher->match($context->request->uri);
|
||||
if($match !== false) {
|
||||
$info = ['info' => $filterInfo];
|
||||
if(is_array($match))
|
||||
$info['matches'] = $match;
|
||||
|
||||
//$filters[] = $info;
|
||||
|
||||
$filters[] = [$filterInfo->handler, is_array($match) ? $match : []];
|
||||
|
@ -141,7 +148,7 @@ class Router implements RequestHandlerInterface {
|
|||
|
||||
$routes = [];
|
||||
foreach($this->routes as $routeInfo) {
|
||||
$match = $routeInfo->matcher->match($request->uri);
|
||||
$match = $routeInfo->matcher->match($context->request->uri);
|
||||
if($match !== false) {
|
||||
$info = ['info' => $routeInfo];
|
||||
if(is_array($match))
|
||||
|
@ -152,7 +159,7 @@ class Router implements RequestHandlerInterface {
|
|||
}
|
||||
}
|
||||
|
||||
$method = strtoupper($request->method);
|
||||
$method = strtoupper($context->request->method);
|
||||
if(array_key_exists($method, $methods)) {
|
||||
[$handler, $args] = $methods[$method];
|
||||
} elseif($method === 'OPTIONS') {
|
||||
|
@ -187,7 +194,7 @@ class Router implements RequestHandlerInterface {
|
|||
$result = 404;
|
||||
} else {
|
||||
$result = 405;
|
||||
$builder->setHeader('Allow', implode(', ', $routeInfo->supportedMethods));
|
||||
$context->response->setHeader('Allow', implode(', ', $routeInfo->supportedMethods));
|
||||
}
|
||||
} else
|
||||
$result = $routeInfo->dispatch($args);
|
||||
|
@ -195,15 +202,15 @@ class Router implements RequestHandlerInterface {
|
|||
|
||||
if(is_int($result)) {
|
||||
if($result >= 100 && $result < 600)
|
||||
$this->writeErrorPage($builder, $request, $result);
|
||||
} elseif($builder->body === null) {
|
||||
$this->writeErrorPage($context->response, $context->request, $result);
|
||||
} elseif($context->response->body === null) {
|
||||
if(is_scalar($result)) {
|
||||
$result = (string)$result;
|
||||
$builder->body = HttpStream::createStream($result);
|
||||
$context->response->body = HttpStream::createStream($result);
|
||||
|
||||
if(!$builder->hasContentType()) {
|
||||
if(!$context->response->hasContentType()) {
|
||||
if(strtolower(substr($result, 0, 14)) === '<!doctype html')
|
||||
$builder->setTypeHtml($this->charSet);
|
||||
$context->response->setTypeHtml($this->charSet);
|
||||
else {
|
||||
$charset = mb_detect_encoding($result);
|
||||
if($charset !== false)
|
||||
|
@ -211,18 +218,18 @@ class Router implements RequestHandlerInterface {
|
|||
$charset = $charset === false ? 'utf-8' : strtolower($charset);
|
||||
|
||||
if(strtolower(substr($result, 0, 5)) === '<?xml')
|
||||
$builder->setTypeXml($charset);
|
||||
$context->response->setTypeXml($charset);
|
||||
else
|
||||
$builder->setTypePlain($charset);
|
||||
$context->response->setTypePlain($charset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if($request->method === 'HEAD' && $builder->body !== null)
|
||||
$builder->body = null;
|
||||
if($context->request->method === 'HEAD' && $context->response->body !== null)
|
||||
$context->response->body = null;
|
||||
|
||||
return $builder->toResponse();
|
||||
return $context->response->toResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DependenciesTest.php
|
||||
// Created: 2025-01-21
|
||||
// Updated: 2025-01-22
|
||||
// Updated: 2025-03-07
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -9,14 +9,43 @@ namespace {
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use PHPUnit\Framework\Attributes\{CoversClass,UsesClass};
|
||||
use Index\Dependencies;
|
||||
use Index\Db\DbBackend;
|
||||
use Index\Db\MariaDb\MariaDbBackend;
|
||||
use Index\Db\Sqlite\SqliteBackend;
|
||||
|
||||
#[CoversClass(Dependencies::class)]
|
||||
#[UsesClass(MariaDbBackend::class)]
|
||||
#[UsesClass(SqliteBackend::class)]
|
||||
final class DependenciesTest extends TestCase {
|
||||
public function testDependencyCall(): void {
|
||||
$deps = new Dependencies;
|
||||
$deps->register(\Index\Db\DbResultIterator::class, construct: fn($result) => null);
|
||||
$deps->register(\Index\Db\NullDb\NullDbResult::class);
|
||||
|
||||
$func = function(
|
||||
\Index\Db\DbResult $result,
|
||||
string $soup,
|
||||
?\Index\Db\DbConnection $conn,
|
||||
string $named = '',
|
||||
?\Index\Db\NullDb\NullDbTransaction $transaction = null
|
||||
) {
|
||||
return $result instanceof \Index\Db\NullDb\NullDbResult
|
||||
&& $soup === 'beans'
|
||||
&& $conn === null
|
||||
&& $named === 'bowl'
|
||||
&& $transaction === null;
|
||||
};
|
||||
$this->assertTrue($deps->call($func, 'beans', named: 'bowl'));
|
||||
|
||||
$outOfOrder = function(
|
||||
string $soup,
|
||||
\Index\Db\DbResult $result,
|
||||
string $named = '',
|
||||
?\Index\Db\DbResultIterator $iterator = null
|
||||
) {
|
||||
return $result instanceof \Index\Db\NullDb\NullDbResult
|
||||
&& $soup === 'beans'
|
||||
&& $named === 'bowl'
|
||||
&& $iterator instanceof \Index\Db\DbResultIterator;
|
||||
};
|
||||
$this->assertTrue($deps->call($outOfOrder, 'beans', named: 'bowl'));
|
||||
}
|
||||
|
||||
public function testDependencyResolver(): void {
|
||||
$deps = new Dependencies;
|
||||
$deps->register(\Index\Db\DbResultIterator::class, construct: fn($result) => null);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue