Added HandlerContext and ->call method on Dependencies.

This commit is contained in:
flash 2025-03-07 02:42:45 +00:00
parent a5f9e9121d
commit d0b9b2d556
Signed by: flash
GPG key ID: 2C9C2C574D47FE3E
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

View file

@ -1 +1 @@
0.2503.70031
0.2503.70242

View file

@ -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 {

View file

@ -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.
*/

View file

@ -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();

View 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);
}
}

View file

@ -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.
*/

View file

@ -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);
}
}

View file

@ -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();
}
/**

View file

@ -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);