Total Complexity | 42 |
Total Lines | 286 |
Duplicated Lines | 0 % |
Coverage | 81.89% |
Changes | 20 | ||
Bugs | 2 | Features | 0 |
Complex classes like App often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use App, and based on these observations, apply Extract Interface, too.
1 | <?php declare(strict_types=1); |
||
42 | class App implements RequestHandlerInterface |
||
43 | { |
||
44 | private int $offset = 0; |
||
45 | /** @var array<int, MiddlewareInterface> */ |
||
46 | private array $stack = []; |
||
47 | /** @var array<string, array> */ |
||
48 | private array $explicit = []; |
||
49 | /** @var array<string, callable|null> */ |
||
50 | private array $handlers = []; |
||
51 | private readonly mixed $responder; |
||
52 | private readonly DIContainer $container; |
||
53 | |||
54 | 21 | public function __construct( |
|
55 | array $modules = [], |
||
56 | Configuration|string $config = '', |
||
57 | private array $middleware = [], |
||
58 | private mixed $renderer = 'start_response') |
||
59 | { |
||
60 | 21 | date_default_timezone_set('UTC'); |
|
61 | 21 | $this->withErrorHandler(HTTPError::class, 'static::httpErrorHandler'); |
|
62 | 21 | $this->withErrorHandler(Exception::class, 'static::phpErrorHandler'); |
|
63 | 21 | $this->withErrorHandler(Error::class, 'static::phpErrorHandler'); |
|
64 | 21 | $this->container = new DIContainer(new Module($config), ...$modules); |
|
1 ignored issue
–
show
|
|||
65 | 21 | $this->middleware = [new GzipMiddleware, ...$middleware, CorsMiddleware::class]; |
|
66 | } |
||
67 | |||
68 | /** |
||
69 | * @return mixed |
||
70 | * @throws mixed |
||
71 | */ |
||
72 | 21 | public function __invoke(): mixed |
|
73 | { |
||
74 | try { |
||
75 | 21 | $request = $this->container |
|
76 | 21 | ->new(ServerRequestInterface::class) |
|
77 | 21 | ->withAttribute('@media', $this->container->get(Configuration::class)->get('media')); |
|
78 | |||
79 | 21 | $this->responder = $this->responder($request, $uriTemplate); |
|
80 | 21 | $this->initialize($uriTemplate); |
|
81 | 21 | $response = $this->handle($request); |
|
82 | 3 | } catch (Throwable $exception) { |
|
83 | // [NOTE]: On exception, the state of the immutable request/response |
||
84 | // objects are not updated through the middleware "request phase", |
||
85 | // therefore the object attributes (and other properties) are lost |
||
86 | 3 | $response = $this->container->get(ResponseInterface::class); |
|
87 | 3 | if (false === $this->handleException($request, $response, $exception)) { |
|
88 | 1 | throw $exception; |
|
89 | } |
||
90 | } |
||
91 | // Share the response object for (custom) renderers |
||
92 | 20 | $this->container->share($response); |
|
93 | // [OPTIMIZATION]: Consider optimizing this |
||
94 | 20 | $this->container->bind(Response::class, $response::class); |
|
95 | 20 | return ($this->container)($this->renderer); |
|
96 | } |
||
97 | |||
98 | /** |
||
99 | * @param ServerRequestInterface $request |
||
100 | * @return ResponseInterface |
||
101 | * @internal |
||
102 | */ |
||
103 | 21 | public function handle(ServerRequestInterface $request): ResponseInterface |
|
104 | { |
||
105 | 21 | if (isset($this->stack[$this->offset])) { |
|
106 | //\error_log('[mw:' . $this->offset . ']> ' . \get_debug_type($this->stack[$this->offset])); |
||
107 | 21 | return $this->stack[$this->offset]->process($request, $this->next()); |
|
108 | } |
||
109 | 21 | $this->container->share($request); |
|
110 | 21 | return ($this->container)($this->responder); |
|
111 | } |
||
112 | |||
113 | /** |
||
114 | * Create a route for URI. |
||
115 | * |
||
116 | * @param string $uriTemplate The URI template |
||
117 | * @param object|string $resource A PHP callable |
||
118 | * @param array<MiddlewareInterface|string> $middleware [optional] List of middlewares for this route |
||
119 | * @param bool $explicit [optional] If TRUE replace the global middlewares |
||
120 | * @return self |
||
121 | */ |
||
122 | 20 | public function route( |
|
123 | string $uriTemplate, |
||
124 | object|string $resource, |
||
125 | array $middleware = [], |
||
126 | bool $explicit = false): App |
||
127 | { |
||
128 | 20 | $this->container->get(Router::class)->route($uriTemplate, $resource); |
|
129 | 20 | $this->explicit[$uriTemplate] = [$explicit, $middleware]; |
|
130 | 20 | return $this; |
|
131 | } |
||
132 | |||
133 | /** |
||
134 | * Group multiple routes (adds prefix to all). |
||
135 | * See App::route() method. |
||
136 | * |
||
137 | * @param string $prefix URI prefix for all routes in this group |
||
138 | * @param array $routes A list of routes (@see App::route()) |
||
139 | * @param array<int, MiddlewareInterface|string> $middleware Additional middleware for all routes |
||
140 | * @return self |
||
141 | */ |
||
142 | public function group( |
||
143 | string $prefix, |
||
144 | array $routes, |
||
145 | array $middleware = []): App |
||
146 | { |
||
147 | foreach ($routes as $route) { |
||
148 | // (template, resource, middleware, explicit) |
||
149 | $route += ['', '', [], false]; |
||
150 | [$uriTemplate, $resource, $mw, $explicit] = $route; |
||
151 | $this->route($prefix . $uriTemplate, $resource, array_merge($middleware, $mw), $explicit); |
||
152 | } |
||
153 | return $this; |
||
154 | } |
||
155 | |||
156 | 21 | public function withErrorHandler(string $type, callable|null $handler): App |
|
157 | { |
||
158 | 21 | if (false === is_a($type, Throwable::class, true)) { |
|
159 | throw new TypeError('"type" must be an exception type', HttpStatus::CONFLICT); |
||
160 | } |
||
161 | 21 | if (null === $handler && false === method_exists($type, 'handle')) { |
|
162 | throw new TypeError('Error handler must either be specified explicitly,' . |
||
163 | ' or defined as a static method named "handle" that is a member of' . |
||
164 | ' the given exception type', HttpStatus::NOT_IMPLEMENTED); |
||
165 | } |
||
166 | 21 | $this->handlers[$type] = $handler; |
|
167 | 21 | return $this; |
|
168 | } |
||
169 | |||
170 | 1 | public function withoutErrorHandler(string $type): App |
|
174 | } |
||
175 | |||
176 | public function withErrorSerializer(callable $serializer): App |
||
177 | { |
||
178 | $this->container->named('$errorSerializer', $serializer); |
||
179 | return $this; |
||
180 | } |
||
181 | |||
182 | 21 | private function next(): RequestHandlerInterface |
|
183 | { |
||
184 | 21 | $self = clone $this; |
|
185 | 21 | $self->offset++; |
|
186 | 21 | return $self; |
|
187 | } |
||
188 | |||
189 | 21 | private function initialize(?string $uriTemplate): void |
|
190 | { |
||
191 | 21 | $this->offset = 0; |
|
192 | 21 | $this->stack = []; |
|
193 | 21 | if (empty($uriTemplate)) { |
|
194 | // Always support CORS requests |
||
195 | 1 | $this->explicit[$uriTemplate] = [true, [CorsMiddleware::class]]; |
|
196 | } |
||
197 | 21 | [$explicit, $middleware] = $this->explicit[$uriTemplate] + [true]; |
|
198 | 21 | $this->middleware = false === $explicit ? [...$this->middleware, ...$middleware] : $middleware; |
|
199 | 21 | foreach ($this->middleware as $middleware) { |
|
200 | 21 | $class = 'string' === get_debug_type($middleware) ? $middleware : get_class($middleware); |
|
201 | 21 | $this->stack[$class] = match (true) { |
|
202 | 21 | $middleware instanceof MiddlewareInterface => $middleware, |
|
203 | 21 | is_a($middleware, MiddlewareInterface::class, true) => $this->container->new($middleware), |
|
204 | is_callable($middleware) => new CallableMiddleware($middleware), |
||
205 | default => throw new InvalidArgumentException( |
||
206 | sprintf('Middleware "%s" must implement %s', $class, MiddlewareInterface::class) |
||
207 | ) |
||
208 | }; |
||
209 | } |
||
210 | 21 | $this->stack = array_values($this->stack); |
|
211 | } |
||
212 | |||
213 | 21 | private function responder( |
|
214 | ServerRequestInterface &$request, |
||
215 | string|null &$uriTemplate): callable |
||
216 | { |
||
217 | 21 | $path = rawurldecode($request->getUri()->getPath()); |
|
218 | 21 | $match = $this->container->get(Router::class)->match($path); |
|
219 | 21 | $uriTemplate = $match['template'] ?? null; |
|
220 | 21 | $resource = $match['resource'] ?? null; |
|
221 | 21 | $allowed = array_keys(map_http_methods($resource ?? new stdClass)); |
|
222 | 21 | $request = $request->withAttribute('@http_methods', $allowed); |
|
223 | 21 | foreach ($match['params'] ?? [] as $name => $value) { |
|
224 | $request = $request->withAttribute($name, $value); |
||
225 | } |
||
226 | 21 | $this->container->get(LoggerInterface::class)->debug('> {method} {dir}', [ |
|
227 | 21 | 'method' => $request->getMethod(), |
|
228 | 'dir' => $path |
||
229 | ]); |
||
230 | 21 | if (Request::OPTIONS === $method = $request->getMethod()) { |
|
231 | 5 | return create_options_response($allowed); |
|
232 | } |
||
233 | 16 | if (empty($resource)) { |
|
234 | return path_not_found($path); |
||
235 | } |
||
236 | 16 | if (Request::HEAD === $method) { |
|
237 | return head_response((string)$request->getUri(), $allowed); |
||
238 | } |
||
239 | 16 | if ($resource instanceof Closure || function_exists($resource)) { |
|
240 | 3 | return $resource; |
|
241 | } |
||
242 | 13 | $responder = $this->container->new($resource); |
|
243 | 13 | if (false === method_exists($responder, $method)) { |
|
244 | return method_not_allowed($allowed); |
||
245 | } |
||
246 | 13 | return [$responder, $method]; |
|
247 | } |
||
248 | |||
249 | 3 | private function handleException( |
|
250 | ServerRequestInterface $request, |
||
251 | ResponseInterface &$response, |
||
252 | Throwable $ex): bool |
||
253 | { |
||
254 | 3 | if (!$handler = $this->findErrorHandler($ex)) { |
|
255 | 1 | return false; |
|
256 | } |
||
257 | try { |
||
258 | 2 | call_user_func_array($handler, [$request, &$response, $ex]); |
|
259 | } catch (HTTPError $error) { |
||
260 | $this->composeErrorResponse($request, $response, $error); |
||
261 | } |
||
262 | 2 | return true; |
|
263 | } |
||
264 | |||
265 | 3 | private function findErrorHandler($ex): callable|null |
|
266 | { |
||
267 | 3 | $parents = [get_debug_type($ex)]; |
|
268 | // Iterate the class inheritance chain |
||
269 | 3 | (static function($class) use (&$parents) { |
|
270 | 3 | while ($class = get_parent_class($class)) { |
|
271 | 1 | $parents[] = $class; |
|
272 | } |
||
273 | })($ex); |
||
274 | // Find the parent that matches the exception type |
||
275 | 3 | foreach ($parents as $parent) { |
|
276 | 3 | if (isset($this->handlers[$parent]) && is_a($ex, $parent, true)) { |
|
277 | 2 | return $this->handlers[$parent]; |
|
278 | } |
||
279 | } |
||
280 | 1 | return null; |
|
281 | } |
||
282 | |||
283 | 1 | private function phpErrorHandler( |
|
284 | ServerRequestInterface $request, |
||
285 | ResponseInterface &$response, |
||
286 | Throwable $ex): void |
||
287 | { |
||
288 | 1 | error_log(sprintf("[%s] %s\n%s", |
|
289 | 1 | $title = get_debug_type($ex), |
|
290 | 1 | $ex->getMessage(), |
|
291 | 1 | $ex->getTraceAsString())); |
|
292 | |||
293 | 1 | $this->composeErrorResponse( |
|
294 | $request, |
||
295 | $response, |
||
296 | 1 | new HTTPError(HTTPError::status($ex, HttpStatus::CONFLICT), |
|
297 | title: $title, |
||
298 | 1 | detail: $ex->getMessage(), |
|
299 | previous: $ex |
||
300 | )); |
||
301 | } |
||
302 | |||
303 | 1 | private function httpErrorHandler( |
|
309 | } |
||
310 | |||
311 | 2 | private function composeErrorResponse( |
|
312 | ServerRequestInterface $request, |
||
313 | ResponseInterface &$response, |
||
314 | HTTPError $ex): void |
||
315 | { |
||
316 | 2 | $response = $response->withStatus($ex->getCode()); |
|
317 | 2 | foreach ($ex->getHeaders() as $name => $value) { |
|
328 | } |
||
329 | } |
||
330 | } |
||
331 |