Passed
Push — master ( f2e1d1...eea237 )
by Divine Niiquaye
03:30
created

Application::handle()   B

Complexity

Conditions 7
Paths 202

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.7656

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 16
c 1
b 0
f 0
nc 202
nop 2
dl 0
loc 28
ccs 12
cts 16
cp 0.75
crap 7.7656
rs 7.975
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of DivineNii opensource projects.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 DivineNii (https://divinenii.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Rade;
19
20
use Biurad\Http\{Request, Response, Response\HtmlResponse};
21
use Biurad\Http\Factory\Psr17Factory;
22
use Biurad\Http\Interfaces\Psr17Interface;
23
use Flight\Routing\{Exceptions\RouteNotFoundException, Route, RouteCollection, Router};
24
use Fig\Http\Message\RequestMethodInterface;
25
use Flight\Routing\Generator\GeneratedUri;
26
use Flight\Routing\Interfaces\{RouteMatcherInterface, UrlGeneratorInterface};
27
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface};
28
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
29
use Laminas\Stratigility\Middleware\{CallableMiddlewareDecorator, RequestHandlerMiddleware};
30
use Laminas\{HttpHandlerRunner\Emitter\SapiStreamEmitter, Stratigility\Utils};
0 ignored issues
show
Bug introduced by
The type Laminas\HttpHandlerRunne...itter\SapiStreamEmitter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
31
use Psr\EventDispatcher\EventDispatcherInterface;
32
use Rade\DI\Definitions\{Reference, Statement};
33
use Symfony\Component\Console\Application as ConsoleApplication;
34
use Symfony\Component\HttpFoundation\RequestStack;
35
36
/**
37
 * The Rade framework core class.
38
 *
39
 * @author Divine Niiquaye Ibok <[email protected]>
40
 */
41
class Application extends DI\Container implements RouterInterface, KernelInterface
42
{
43
    use Traits\HelperTrait;
44
45
    public const VERSION = '2.0.0-DEV';
46
47
    /**
48
     * Instantiate a new Application.
49
     */
50 14
    public function __construct(Psr17Interface $psr17Factory = null, EventDispatcherInterface $dispatcher = null, bool $debug = false)
51
    {
52 14
        parent::__construct();
53 14
        $this->parameters['debug'] ??= $debug;
54
55 14
        if (!isset($this->parameters['project.compiled_container_class'])) {
56 14
            $this->definitions = [
57 14
                'http.router' => $this->services['http.router'] = new Router(),
58 14
                'request_stack' => $this->services['request_stack'] = new RequestStack(),
59 14
                'psr17.factory' => $this->services['psr17.factory'] = $psr17Factory = $psr17Factory ?? new Psr17Factory(),
60 14
                'events.dispatcher' => $this->services['events.dispatcher'] = $dispatcher = $dispatcher ?? new Handler\EventHandler(),
61 14
                RequestHandlerInterface::class => $this->services[RequestHandlerInterface::class] = new Handler\RouteHandler($this),
62
            ];
63 14
            $this->types += [
64
                RequestStack::class => ['request_stack'],
65
                Router::class => ['http.router'],
66
                RouteMatcherInterface::class => ['http.router'],
67
                UrlGeneratorInterface::class => ['http.router'],
68
            ];
69 14
            $this->types(['psr17.factory' => DI\Resolver::autowireService($psr17Factory), 'events.dispatcher' => DI\Resolver::autowireService($dispatcher)]);
70
        }
71
    }
72
73
    /**
74
     * If true, exception will be thrown on resolvable services with are not typed.
75
     */
76
    public function strictAutowiring(bool $boolean = true): void
77
    {
78
        $this->resolver->setStrictAutowiring($boolean);
79
    }
80
81 4
    public function getDispatcher(): EventDispatcherInterface
82
    {
83 4
        return $this->services['events.dispatcher'] ?? $this->get('events.dispatcher');
84
    }
85
86 7
    public function getRouter(): Router
87
    {
88 7
        return $this->services['http.router'] ?? $this->get('http.router');
89
    }
90
91
    /**
92
     * {@inheritdoc}
93
     *
94
     * @param MiddlewareInterface|RequestHandlerInterface|Reference|Statement|callable ...$middlewares
95
     */
96
    public function pipe(object ...$middlewares): void
97
    {
98
        $this->getRouter()->pipe(...$this->resolveMiddlewares($middlewares));
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     *
104
     * @param MiddlewareInterface|RequestHandlerInterface|Reference|Statement|callable ...$middlewares
105
     */
106
    public function pipes(string $named, object ...$middlewares): void
107
    {
108
        $this->getRouter()->pipes($named, ...$this->resolveMiddlewares($middlewares));
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
115
    {
116
        return $this->getRouter()->generateUri($routeName, $parameters);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getRouter(...routeName, $parameters) returns the type Flight\Routing\Generator\GeneratedUri which is incompatible with the return type mandated by Rade\RouterInterface::generateUri() of Rade\GeneratedUri.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
117
    }
118
119
    /**
120
     * {@inheritdoc}
121
     */
122 7
    public function match(string $pattern, array $methods = Route::DEFAULT_METHODS, $to = null): Route
123
    {
124 7
        return $this->getRouter()->getCollection()->add(new Route($pattern, $methods, $to), false)->getRoute();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getRouter(...to), false)->getRoute() could return the type null which is incompatible with the type-hinted return Flight\Routing\Route. Consider adding an additional type-check to rule them out.
Loading history...
125
    }
126
127
    /**
128
     * {@inheritdoc}
129
     */
130 1
    public function post(string $pattern, $to = null): Route
131
    {
132 1
        return $this->match($pattern, [RequestMethodInterface::METHOD_POST], $to);
133
    }
134
135
    /**
136
     * {@inheritdoc}
137
     */
138 1
    public function put(string $pattern, $to = null): Route
139
    {
140 1
        return $this->match($pattern, [RequestMethodInterface::METHOD_PUT], $to);
141
    }
142
143
    /**
144
     * {@inheritdoc}
145
     */
146 1
    public function delete(string $pattern, $to = null): Route
147
    {
148 1
        return $this->match($pattern, [RequestMethodInterface::METHOD_DELETE], $to);
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     */
154
    public function options(string $pattern, $to = null): Route
155
    {
156
        return $this->match($pattern, [RequestMethodInterface::METHOD_OPTIONS], $to);
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 1
    public function patch(string $pattern, $to = null): Route
163
    {
164 1
        return $this->match($pattern, [RequestMethodInterface::METHOD_PATCH], $to);
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 1
    public function group(string $prefix, $collection = null): RouteCollection
171
    {
172 1
        return $this->getRouter()->getCollection()->group($prefix, $collection);
173
    }
174
175
    /**
176
     * Handles the request and delivers the response.
177
     *
178
     * @param ServerRequestInterface|null $request Request to process
179
     *
180
     * @throws \Throwable
181
     *
182
     * @return int|bool
183
     */
184
    public function run(ServerRequestInterface $request = null, bool $catch = true)
185
    {
186
        if ($this->isRunningInConsole()) {
187
            $this->get(ConsoleApplication::class)->run();
188
        }
189
190
        if (null === $request) {
191
            $request = $this->get('psr17.factory')->fromGlobalRequest();
192
        }
193
194
        $response = $this->handle($request, $catch);
195
196
        if ($response instanceof Response) {
197
            $response->getResponse()->send();
198
199
            return true;
200
        }
201
202
        if (!\class_exists(SapiStreamEmitter::class)) {
203
            throw new \RuntimeException(\sprintf('Unable to emit response onto the browser. Try running "composer require laminas/laminas-httphandlerrunner".'));
204
        }
205
206
        return (new SapiStreamEmitter())->emit($response);
207
    }
208
209
    /**
210
     * Handles a request to convert it to a response.
211
     *
212
     * Exceptions are not caught.
213
     *
214
     * @param bool $catch Whether to catch exceptions or not
215
     */
216 4
    public function handle(ServerRequestInterface $request, bool $catch = true): ResponseInterface
217
    {
218
        try {
219 4
            $this->getDispatcher()->dispatch($event = new Event\RequestEvent($this, $request));
220 4
            $request = $event->getRequest();
221
222 4
            if ($supportStack = $request instanceof Request) {
223 4
                $this->get('request_stack')->push($request->getRequest());
224
            }
225
226 4
            $response = !$event->hasResponse() ? $this->getRouter()->process($request, $this->get(RequestHandlerInterface::class)) : $event->getResponse();
227
228 4
            if ($supportStack) {
229 4
                $request = $request->withRequest($this->get('request_stack')->getCurrentRequest());
0 ignored issues
show
Bug introduced by
The method withRequest() does not exist on Psr\Http\Message\ServerRequestInterface. Did you maybe mean withRequestTarget()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

229
                /** @scrutinizer ignore-call */ 
230
                $request = $request->withRequest($this->get('request_stack')->getCurrentRequest());

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
230
            }
231
232 4
            $this->getDispatcher()->dispatch($event = new Event\ResponseEvent($this, $request, $response));
233
        } catch (\Throwable $e) {
234
            if (!$catch || $this->isRunningInConsole()) {
235
                throw $e;
236
            }
237
238
            return $this->handleThrowable($e, $request);
239 4
        } finally {
240 4
            $this->getDispatcher()->dispatch(new Event\TerminateEvent($this, $request));
241
        }
242
243 4
        return $event->getResponse();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $event->getResponse() could return the type null which is incompatible with the type-hinted return Psr\Http\Message\ResponseInterface. Consider adding an additional type-check to rule them out.
Loading history...
244
    }
245
246
    /**
247
     * Handle RouteNotFoundException for Flight Routing.
248
     *
249
     * @return RouteNotFoundException|ResponseInterface
250
     */
251
    protected function handleRouterException(RouteNotFoundException $e, ServerRequestInterface $request)
252
    {
253
        if (empty($pathInfo = $request->getServerParams()['PATH_INFO'] ?? '')) {
254
            $pathInfo = $request->getUri()->getPath();
255
        }
256
257
        if ('/' === $pathInfo) {
258
            return $this->createWelcomeResponse();
259
        }
260
261
        $message = $e->getMessage();
262
263
        if ('' !== $referer = $request->getHeaderLine('referer')) {
264
            $message .= \sprintf(' (from "%s")', $referer);
265
        }
266
267
        return new RouteNotFoundException($message, 404);
268
    }
269
270
    /**
271
     * Handles a throwable by trying to convert it to a Response.
272
     *
273
     * @throws \Throwable
274
     */
275
    protected function handleThrowable(\Throwable $e, ServerRequestInterface $request): ResponseInterface
276
    {
277
        if ($request instanceof Request) {
278
            $this->get('request_stack')->push($request->getRequest());
279
        }
280
281
        $this->getDispatcher()->dispatch($event = new Event\ExceptionEvent($this, $request, $e));
282
283
        // a listener might have replaced the exception
284
        $e = $event->getThrowable();
285
286
        if (null === $response = $event->getResponse()) {
287
            if ($e instanceof RouteNotFoundException) {
288
                $e = $this->handleRouterException($e, $request);
289
290
                if ($e instanceof ResponseInterface) {
291
                    return $e;
292
                }
293
            }
294
295
            throw $e;
296
        }
297
298
        // ensure that we actually have an error response and keep the HTTP status code and headers
299
        if (!$event->isAllowingCustomResponseCode()) {
300
            $response = $response->withStatus(Utils::getStatusCode($e, $response));
301
        }
302
303
        return $response;
304
    }
305
306
    /**
307
     * The default welcome page for application.
308
     */
309
    protected function createWelcomeResponse(): ResponseInterface
310
    {
311
        $debug = $this->parameters['debug'];
312
        $version = self::VERSION;
313
        $docVersion = $version[0] . '.x.x';
314
315
        \ob_start();
316
317
        include __DIR__ . '/Resources/welcome.phtml';
318
319
        return new HtmlResponse((string) \ob_get_clean(), 404);
320
    }
321
322
    /**
323
     * Resolve Middlewares.
324
     *
325
     * @param array<int,MiddlewareInterface|RequestHandlerInterface|Reference|Statement|callable> $middlewares
326
     *
327
     * @return array<int,MiddlewareInterface>
328
     */
329
    protected function resolveMiddlewares(array $middlewares): array
330
    {
331
        foreach ($middlewares as $offset => $middleware) {
332
            if ($middleware instanceof RequestHandlerInterface) {
333
                $middlewares[$offset] = new RequestHandlerMiddleware($middleware);
334
            } elseif ($middleware instanceof Statement) {
335
                $middlewares[$offset] = $this->resolver->resolve($middleware->getValue(), $middleware->getArguments());
336
            } elseif ($middleware instanceof Reference) {
337
                $middlewares[$offset] = $this->get((string) $middleware);
338
            } elseif (\is_callable($middleware)) {
339
                $middlewares[$offset] = new CallableMiddlewareDecorator($middleware);
0 ignored issues
show
Bug introduced by
It seems like $middleware can also be of type Psr\Http\Server\MiddlewareInterface; however, parameter $middleware of Laminas\Stratigility\Mid...ecorator::__construct() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

339
                $middlewares[$offset] = new CallableMiddlewareDecorator(/** @scrutinizer ignore-type */ $middleware);
Loading history...
340
            }
341
        }
342
343
        return $middlewares;
344
    }
345
}
346