Skip to content

Commit

Permalink
Merge pull request #94 from clue-labs/container-class
Browse files Browse the repository at this point in the history
Use DI container to load global middleware classes
  • Loading branch information
SimonFrings authored Dec 6, 2021
2 parents 01b5524 + 40f6e5b commit 34d379d
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 123 deletions.
12 changes: 11 additions & 1 deletion src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,18 @@ class App
*/
public function __construct(...$middleware)
{
$container = new Container();
$errorHandler = new ErrorHandler();
$this->router = new RouteHandler();
$this->router = new RouteHandler($container);

if ($middleware) {
$middleware = array_map(
function ($handler) use ($container) {
return is_callable($handler) ? $handler : $container->callable($handler);
},
$middleware
);
}

// new MiddlewareHandler([$accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
\array_unshift($middleware, $errorHandler);
Expand Down
132 changes: 132 additions & 0 deletions src/Container.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

namespace FrameworkX;

use Psr\Http\Message\ServerRequestInterface;

/**
* @internal
*/
class Container
{
/** @var array<class-string,object> */
private $container;

/**
* @param class-string $class
* @return callable
*/
public function callable(string $class): callable
{
return function (ServerRequestInterface $request, callable $next = null) use ($class) {
// Check `$class` references a valid class name that can be autoloaded
if (!\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) {
throw new \BadMethodCallException('Request handler class ' . $class . ' not found');
}

try {
$handler = $this->load($class);
} catch (\Throwable $e) {
throw new \BadMethodCallException(
'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(),
0,
$e
);
}

// Check `$handler` references a class name that is callable, i.e. has an `__invoke()` method.
// This initial version is intentionally limited to checking the method name only.
// A follow-up version will likely use reflection to check request handler argument types.
if (!is_callable($handler)) {
throw new \BadMethodCallException('Request handler class "' . $class . '" has no public __invoke() method');
}

// invoke request handler as middleware handler or final controller
if ($next === null) {
return $handler($request);
}
return $handler($request, $next);
};
}

/**
* @param class-string $name
* @return object
* @throws \BadMethodCallException
*/
private function load(string $name, int $depth = 64)
{
if (isset($this->container[$name])) {
return $this->container[$name];
}

// Check `$name` references a valid class name that can be autoloaded
if (!\class_exists($name, true) && !interface_exists($name, false) && !trait_exists($name, false)) {
throw new \BadMethodCallException('Class ' . $name . ' not found');
}

$class = new \ReflectionClass($name);
if (!$class->isInstantiable()) {
$modifier = 'class';
if ($class->isInterface()) {
$modifier = 'interface';
} elseif ($class->isAbstract()) {
$modifier = 'abstract class';
} elseif ($class->isTrait()) {
$modifier = 'trait';
}
throw new \BadMethodCallException('Cannot instantiate ' . $modifier . ' '. $name);
}

// build list of constructor parameters based on parameter types
$params = [];
$ctor = $class->getConstructor();
assert($ctor === null || $ctor instanceof \ReflectionMethod);
foreach ($ctor !== null ? $ctor->getParameters() : [] as $parameter) {
assert($parameter instanceof \ReflectionParameter);

// stop building parameters when encountering first optional parameter
if ($parameter->isOptional()) {
break;
}

// ensure parameter is typed
$type = $parameter->getType();
if ($type === null) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
}

// if allowed, use null value without injecting any instances
assert($type instanceof \ReflectionType);
if ($type->allowsNull()) {
$params[] = null;
continue;
}

// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type); // @codeCoverageIgnore
}

assert($type instanceof \ReflectionNamedType);
if ($type->isBuiltin()) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
}

// abort for unreasonably deep nesting or recursive types
if ($depth < 1) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
}

$params[] = $this->load($type->getName(), --$depth);
}

// instantiate with list of parameters
return $this->container[$name] = $params === [] ? new $name() : $class->newInstance(...$params);
}

private static function parameterError(\ReflectionParameter $parameter): string
{
return 'Argument ' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . explode("\0", $parameter->getDeclaringClass()->getName())[0] . '::' . $parameter->getDeclaringFunction()->getName() . '()';
}
}
124 changes: 6 additions & 118 deletions src/RouteHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ class RouteHandler
/** @var ErrorHandler */
private $errorHandler;

/** @var array<string,mixed> */
private static $container = [];
/** @var Container */
private $container;

public function __construct()
public function __construct(Container $container = null)
{
$this->routeCollector = new RouteCollector(new RouteParser(), new RouteGenerator());
$this->errorHandler = new ErrorHandler();
$this->container = $container ?? new Container();
}

/**
Expand All @@ -44,12 +45,12 @@ public function map(array $methods, string $route, $handler, ...$handlers): void
if ($handlers) {
$handler = new MiddlewareHandler(array_map(
function ($handler) {
return is_callable($handler) ? $handler : self::callable($handler);
return is_callable($handler) ? $handler : $this->container->callable($handler);
},
array_merge([$handler], $handlers)
));
} elseif (!is_callable($handler)) {
$handler = self::callable($handler);
$handler = $this->container->callable($handler);
}

$this->routeDispatcher = null;
Expand Down Expand Up @@ -86,117 +87,4 @@ public function __invoke(ServerRequestInterface $request)
return $handler($request);
}
} // @codeCoverageIgnore

/**
* @param class-string $class
* @return callable
*/
private static function callable($class): callable
{
return function (ServerRequestInterface $request, callable $next = null) use ($class) {
// Check `$class` references a valid class name that can be autoloaded
if (!\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) {
throw new \BadMethodCallException('Request handler class ' . $class . ' not found');
}

try {
$handler = self::load($class);
} catch (\Throwable $e) {
throw new \BadMethodCallException(
'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(),
0,
$e
);
}

// Check `$handler` references a class name that is callable, i.e. has an `__invoke()` method.
// This initial version is intentionally limited to checking the method name only.
// A follow-up version will likely use reflection to check request handler argument types.
if (!is_callable($handler)) {
throw new \BadMethodCallException('Request handler class "' . $class . '" has no public __invoke() method');
}

// invoke request handler as middleware handler or final controller
if ($next === null) {
return $handler($request);
}
return $handler($request, $next);
};
}

private static function load(string $name, int $depth = 64)
{
if (isset(self::$container[$name])) {
return self::$container[$name];
}

// Check `$name` references a valid class name that can be autoloaded
if (!\class_exists($name, true) && !interface_exists($name, false) && !trait_exists($name, false)) {
throw new \BadMethodCallException('Class ' . $name . ' not found');
}

$class = new \ReflectionClass($name);
if (!$class->isInstantiable()) {
$modifier = 'class';
if ($class->isInterface()) {
$modifier = 'interface';
} elseif ($class->isAbstract()) {
$modifier = 'abstract class';
} elseif ($class->isTrait()) {
$modifier = 'trait';
}
throw new \BadMethodCallException('Cannot instantiate ' . $modifier . ' '. $name);
}

// build list of constructor parameters based on parameter types
$params = [];
$ctor = $class->getConstructor();
assert($ctor === null || $ctor instanceof \ReflectionMethod);
foreach ($ctor !== null ? $ctor->getParameters() : [] as $parameter) {
assert($parameter instanceof \ReflectionParameter);

// stop building parameters when encountering first optional parameter
if ($parameter->isOptional()) {
break;
}

// ensure parameter is typed
$type = $parameter->getType();
if ($type === null) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
}

// if allowed, use null value without injecting any instances
assert($type instanceof \ReflectionType);
if ($type->allowsNull()) {
$params[] = null;
continue;
}

// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type); // @codeCoverageIgnore
}

assert($type instanceof \ReflectionNamedType);
if ($type->isBuiltin()) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
}

// abort for unreasonably deep nesting or recursive types
if ($depth < 1) {
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
}

$params[] = self::load($type->getName(), --$depth);
}

// instantiate with list of parameters
return self::$container[$name] = $params === [] ? new $name() : $class->newInstance(...$params);
}

private static function parameterError(\ReflectionParameter $parameter): string
{
return 'Argument ' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . explode("\0", $parameter->getDeclaringClass()->getName())[0] . '::' . $parameter->getDeclaringFunction()->getName() . '()';
}
}
Loading

0 comments on commit 34d379d

Please sign in to comment.