Skip to content

Commit 34d379d

Browse files
authored
Merge pull request #94 from clue-labs/container-class
Use DI container to load global middleware classes
2 parents 01b5524 + 40f6e5b commit 34d379d

File tree

5 files changed

+260
-123
lines changed

5 files changed

+260
-123
lines changed

src/App.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,18 @@ class App
3737
*/
3838
public function __construct(...$middleware)
3939
{
40+
$container = new Container();
4041
$errorHandler = new ErrorHandler();
41-
$this->router = new RouteHandler();
42+
$this->router = new RouteHandler($container);
43+
44+
if ($middleware) {
45+
$middleware = array_map(
46+
function ($handler) use ($container) {
47+
return is_callable($handler) ? $handler : $container->callable($handler);
48+
},
49+
$middleware
50+
);
51+
}
4252

4353
// new MiddlewareHandler([$accessLogHandler, $errorHandler, ...$middleware, $routeHandler])
4454
\array_unshift($middleware, $errorHandler);

src/Container.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace FrameworkX;
4+
5+
use Psr\Http\Message\ServerRequestInterface;
6+
7+
/**
8+
* @internal
9+
*/
10+
class Container
11+
{
12+
/** @var array<class-string,object> */
13+
private $container;
14+
15+
/**
16+
* @param class-string $class
17+
* @return callable
18+
*/
19+
public function callable(string $class): callable
20+
{
21+
return function (ServerRequestInterface $request, callable $next = null) use ($class) {
22+
// Check `$class` references a valid class name that can be autoloaded
23+
if (!\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) {
24+
throw new \BadMethodCallException('Request handler class ' . $class . ' not found');
25+
}
26+
27+
try {
28+
$handler = $this->load($class);
29+
} catch (\Throwable $e) {
30+
throw new \BadMethodCallException(
31+
'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(),
32+
0,
33+
$e
34+
);
35+
}
36+
37+
// Check `$handler` references a class name that is callable, i.e. has an `__invoke()` method.
38+
// This initial version is intentionally limited to checking the method name only.
39+
// A follow-up version will likely use reflection to check request handler argument types.
40+
if (!is_callable($handler)) {
41+
throw new \BadMethodCallException('Request handler class "' . $class . '" has no public __invoke() method');
42+
}
43+
44+
// invoke request handler as middleware handler or final controller
45+
if ($next === null) {
46+
return $handler($request);
47+
}
48+
return $handler($request, $next);
49+
};
50+
}
51+
52+
/**
53+
* @param class-string $name
54+
* @return object
55+
* @throws \BadMethodCallException
56+
*/
57+
private function load(string $name, int $depth = 64)
58+
{
59+
if (isset($this->container[$name])) {
60+
return $this->container[$name];
61+
}
62+
63+
// Check `$name` references a valid class name that can be autoloaded
64+
if (!\class_exists($name, true) && !interface_exists($name, false) && !trait_exists($name, false)) {
65+
throw new \BadMethodCallException('Class ' . $name . ' not found');
66+
}
67+
68+
$class = new \ReflectionClass($name);
69+
if (!$class->isInstantiable()) {
70+
$modifier = 'class';
71+
if ($class->isInterface()) {
72+
$modifier = 'interface';
73+
} elseif ($class->isAbstract()) {
74+
$modifier = 'abstract class';
75+
} elseif ($class->isTrait()) {
76+
$modifier = 'trait';
77+
}
78+
throw new \BadMethodCallException('Cannot instantiate ' . $modifier . ' '. $name);
79+
}
80+
81+
// build list of constructor parameters based on parameter types
82+
$params = [];
83+
$ctor = $class->getConstructor();
84+
assert($ctor === null || $ctor instanceof \ReflectionMethod);
85+
foreach ($ctor !== null ? $ctor->getParameters() : [] as $parameter) {
86+
assert($parameter instanceof \ReflectionParameter);
87+
88+
// stop building parameters when encountering first optional parameter
89+
if ($parameter->isOptional()) {
90+
break;
91+
}
92+
93+
// ensure parameter is typed
94+
$type = $parameter->getType();
95+
if ($type === null) {
96+
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
97+
}
98+
99+
// if allowed, use null value without injecting any instances
100+
assert($type instanceof \ReflectionType);
101+
if ($type->allowsNull()) {
102+
$params[] = null;
103+
continue;
104+
}
105+
106+
// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
107+
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
108+
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type); // @codeCoverageIgnore
109+
}
110+
111+
assert($type instanceof \ReflectionNamedType);
112+
if ($type->isBuiltin()) {
113+
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
114+
}
115+
116+
// abort for unreasonably deep nesting or recursive types
117+
if ($depth < 1) {
118+
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
119+
}
120+
121+
$params[] = $this->load($type->getName(), --$depth);
122+
}
123+
124+
// instantiate with list of parameters
125+
return $this->container[$name] = $params === [] ? new $name() : $class->newInstance(...$params);
126+
}
127+
128+
private static function parameterError(\ReflectionParameter $parameter): string
129+
{
130+
return 'Argument ' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . explode("\0", $parameter->getDeclaringClass()->getName())[0] . '::' . $parameter->getDeclaringFunction()->getName() . '()';
131+
}
132+
}

src/RouteHandler.php

Lines changed: 6 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ class RouteHandler
2424
/** @var ErrorHandler */
2525
private $errorHandler;
2626

27-
/** @var array<string,mixed> */
28-
private static $container = [];
27+
/** @var Container */
28+
private $container;
2929

30-
public function __construct()
30+
public function __construct(Container $container = null)
3131
{
3232
$this->routeCollector = new RouteCollector(new RouteParser(), new RouteGenerator());
3333
$this->errorHandler = new ErrorHandler();
34+
$this->container = $container ?? new Container();
3435
}
3536

3637
/**
@@ -44,12 +45,12 @@ public function map(array $methods, string $route, $handler, ...$handlers): void
4445
if ($handlers) {
4546
$handler = new MiddlewareHandler(array_map(
4647
function ($handler) {
47-
return is_callable($handler) ? $handler : self::callable($handler);
48+
return is_callable($handler) ? $handler : $this->container->callable($handler);
4849
},
4950
array_merge([$handler], $handlers)
5051
));
5152
} elseif (!is_callable($handler)) {
52-
$handler = self::callable($handler);
53+
$handler = $this->container->callable($handler);
5354
}
5455

5556
$this->routeDispatcher = null;
@@ -86,117 +87,4 @@ public function __invoke(ServerRequestInterface $request)
8687
return $handler($request);
8788
}
8889
} // @codeCoverageIgnore
89-
90-
/**
91-
* @param class-string $class
92-
* @return callable
93-
*/
94-
private static function callable($class): callable
95-
{
96-
return function (ServerRequestInterface $request, callable $next = null) use ($class) {
97-
// Check `$class` references a valid class name that can be autoloaded
98-
if (!\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) {
99-
throw new \BadMethodCallException('Request handler class ' . $class . ' not found');
100-
}
101-
102-
try {
103-
$handler = self::load($class);
104-
} catch (\Throwable $e) {
105-
throw new \BadMethodCallException(
106-
'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(),
107-
0,
108-
$e
109-
);
110-
}
111-
112-
// Check `$handler` references a class name that is callable, i.e. has an `__invoke()` method.
113-
// This initial version is intentionally limited to checking the method name only.
114-
// A follow-up version will likely use reflection to check request handler argument types.
115-
if (!is_callable($handler)) {
116-
throw new \BadMethodCallException('Request handler class "' . $class . '" has no public __invoke() method');
117-
}
118-
119-
// invoke request handler as middleware handler or final controller
120-
if ($next === null) {
121-
return $handler($request);
122-
}
123-
return $handler($request, $next);
124-
};
125-
}
126-
127-
private static function load(string $name, int $depth = 64)
128-
{
129-
if (isset(self::$container[$name])) {
130-
return self::$container[$name];
131-
}
132-
133-
// Check `$name` references a valid class name that can be autoloaded
134-
if (!\class_exists($name, true) && !interface_exists($name, false) && !trait_exists($name, false)) {
135-
throw new \BadMethodCallException('Class ' . $name . ' not found');
136-
}
137-
138-
$class = new \ReflectionClass($name);
139-
if (!$class->isInstantiable()) {
140-
$modifier = 'class';
141-
if ($class->isInterface()) {
142-
$modifier = 'interface';
143-
} elseif ($class->isAbstract()) {
144-
$modifier = 'abstract class';
145-
} elseif ($class->isTrait()) {
146-
$modifier = 'trait';
147-
}
148-
throw new \BadMethodCallException('Cannot instantiate ' . $modifier . ' '. $name);
149-
}
150-
151-
// build list of constructor parameters based on parameter types
152-
$params = [];
153-
$ctor = $class->getConstructor();
154-
assert($ctor === null || $ctor instanceof \ReflectionMethod);
155-
foreach ($ctor !== null ? $ctor->getParameters() : [] as $parameter) {
156-
assert($parameter instanceof \ReflectionParameter);
157-
158-
// stop building parameters when encountering first optional parameter
159-
if ($parameter->isOptional()) {
160-
break;
161-
}
162-
163-
// ensure parameter is typed
164-
$type = $parameter->getType();
165-
if ($type === null) {
166-
throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type');
167-
}
168-
169-
// if allowed, use null value without injecting any instances
170-
assert($type instanceof \ReflectionType);
171-
if ($type->allowsNull()) {
172-
$params[] = null;
173-
continue;
174-
}
175-
176-
// abort for union types (PHP 8.0+) and intersection types (PHP 8.1+)
177-
if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) {
178-
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type); // @codeCoverageIgnore
179-
}
180-
181-
assert($type instanceof \ReflectionNamedType);
182-
if ($type->isBuiltin()) {
183-
throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName());
184-
}
185-
186-
// abort for unreasonably deep nesting or recursive types
187-
if ($depth < 1) {
188-
throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive');
189-
}
190-
191-
$params[] = self::load($type->getName(), --$depth);
192-
}
193-
194-
// instantiate with list of parameters
195-
return self::$container[$name] = $params === [] ? new $name() : $class->newInstance(...$params);
196-
}
197-
198-
private static function parameterError(\ReflectionParameter $parameter): string
199-
{
200-
return 'Argument ' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . explode("\0", $parameter->getDeclaringClass()->getName())[0] . '::' . $parameter->getDeclaringFunction()->getName() . '()';
201-
}
20290
}

0 commit comments

Comments
 (0)