Completed
Push — master ( 557f5d...0f207f )
by Dawid
03:50
created

HttpApplication   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 361
Duplicated Lines 0 %

Test Coverage

Coverage 77.24%

Importance

Changes 0
Metric Value
eloc 101
dl 0
loc 361
ccs 95
cts 123
cp 0.7724
rs 8.96
c 0
b 0
f 0
wmc 43

22 Methods

Rating   Name   Duplication   Size   Complexity  
A use() 0 14 3
A getControllerAggregator() 0 3 1
A composeMiddlewarePipe() 0 15 3
A post() 0 3 1
A __construct() 0 14 3
A head() 0 3 1
A delete() 0 3 1
A register() 0 25 6
A shutdown() 0 3 1
A on() 0 3 1
B process() 0 33 7
A handle() 0 5 1
A patch() 0 3 1
A options() 0 3 1
A initializeModule() 0 6 2
A get() 0 3 1
A onRequest() 0 3 1
A getMiddlewarePipe() 0 7 2
A getMiddlewareAggregator() 0 3 1
A startup() 0 5 1
A put() 0 3 1
A run() 0 15 3

How to fix   Complexity   

Complex Class

Complex classes like HttpApplication 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 HttpApplication, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Igni\Application;
4
5
use Igni\Application\Exception\ApplicationException;
6
use Igni\Application\Exception\ControllerException;
7
use Igni\Application\Http\Controller;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Igni\Application\Controller. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
8
use Igni\Application\Http\MiddlewareAggregator;
9
use Igni\Application\Http\GenericRouter;
10
use Igni\Application\Providers\MiddlewareProvider;
11
use Igni\Network\Http\Middleware\CallableMiddleware;
12
use Igni\Network\Http\Middleware\ErrorMiddleware;
13
use Igni\Network\Http\Middleware\MiddlewarePipe;
14
use Igni\Network\Http\Response;
15
use Igni\Network\Http\Route;
16
use Igni\Network\Http\Router;
17
use Igni\Network\Http\ServerRequest;
18
use Igni\Network\Server\Client;
19
use Igni\Network\Server\HttpServer;
20
use Igni\Network\Server\OnRequestListener;
21
use Psr\Container\ContainerInterface;
22
use Psr\Http\Message\ResponseInterface;
23
use Psr\Http\Message\ServerRequestInterface;
24
use Psr\Http\Server\MiddlewareInterface;
25
use Psr\Http\Server\RequestHandlerInterface;
26
use Throwable;
27
use Zend\HttpHandlerRunner\Emitter\EmitterInterface;
28
use Zend\HttpHandlerRunner\Emitter\SapiEmitter;
29
30
/**
31
 * @package Igni\Application
32
 */
33
class HttpApplication extends Application implements
34
    ControllerAggregator,
35
    MiddlewareAggregator,
36
    MiddlewareInterface,
37
    RequestHandlerInterface,
38
    OnRequestListener
39
{
40
    /**
41
     * @var Router
42
     */
43
    private $router;
44
45
    /**
46
     * @var string[]|MiddlewareInterface[]
47
     */
48
    private $middleware = [];
49
50
    /**
51
     * @var MiddlewarePipe
52
     */
53
    private $pipeline;
54
55
    /**
56
     * @var EmitterInterface
57
     */
58
    private $emitter;
59
60
    /**
61
     * Application constructor.
62
     *
63
     * @param ContainerInterface|null $container
64
     */
65 23
    public function __construct(ContainerInterface $container = null)
66
    {
67 23
        parent::__construct($container);
68
69 23
        if ($this->getContainer()->has(Router::class)) {
70 3
            $this->router = $this->getContainer()->get(Router::class);
71
        } else {
72 20
            $this->router = new GenericRouter();
73
        }
74
75 23
        if ($this->getContainer()->has(EmitterInterface::class)) {
76
            $this->emitter = $this->getContainer()->get(EmitterInterface::class);
77
        } else {
78 23
            $this->emitter = new SapiEmitter();
79
        }
80 23
    }
81
82
    /**
83
     * While testing call this method before handle method.
84
     */
85 8
    public function startup(): void
86
    {
87 8
        $this->handleOnBootListeners();
88 8
        $this->initialize();
89 8
        $this->handleOnRunListeners();
90 8
    }
91
92
    /**
93
     * While testing, call this method after handle method.
94
     */
95 1
    public function shutdown(): void
96
    {
97 1
        $this->handleOnShutDownListeners();
98 1
    }
99
100
    /**
101
     * Startups and run application with/or without dedicated server.
102
     * Once application is run it will listen to incoming http requests,
103
     * and takes care of the entire request flow process.
104
     *
105
     * @param HttpServer|null $server
106
     */
107
    public function run(HttpServer $server = null): void
108
    {
109
        $this->startup();
110
        if ($server) {
111
            $server->addListener($this);
112
            $server->start();
113
        } else {
114
            $response = $this->handle(ServerRequest::fromGlobals());
115
            $this->emitter->emit($response);
116
            if ($response instanceof Response) {
117
                $response->end();
118
            }
119
        }
120
121
        $this->shutdown();
122
    }
123
124
    /**
125
     * Registers PSR-15 compatible middelware.
126
     * Middleware can be either callable object which accepts PSR-7 server request interface and returns
127
     * response interface, or just class name that implements psr-15 middleware or its instance.
128
     *
129
     * @param MiddlewareInterface|callable $middleware
130
     */
131 2
    public function use($middleware): void
132
    {
133 2
        if (!is_subclass_of($middleware, MiddlewareInterface::class)) {
134 2
            if (!is_callable($middleware)) {
135
                throw new ApplicationException(sprintf(
136
                    'Middleware must be either class or object that implements `%s`',
137
                    MiddlewareInterface::class
138
                ));
139
            }
140
141 2
            $middleware = new CallableMiddleware($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 Igni\Network\Http\Middle...ddleware::__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

141
            $middleware = new CallableMiddleware(/** @scrutinizer ignore-type */ $middleware);
Loading history...
142
        }
143
144 2
        $this->middleware[] = $middleware;
145 2
    }
146
147 14
    public function register($controller, Route $route = null): void
148
    {
149 14
        if (is_callable($controller) && $route !== null) {
150 11
            $route = $route->withController($controller);
151 11
            $this->router->add($route);
152 11
            return;
153
        }
154
155 3
        if ($controller instanceof Controller) {
156
            /** @var Route $route */
157 1
            $route = $controller::getRoute();
158 1
            $route = $route->withController($controller);
159 1
            $this->router->add($route);
160 1
            return;
161
        }
162
163 2
        if (is_string($controller) && is_subclass_of($controller, Controller::class)) {
164
            /** @var Route $route */
165 1
            $route = $controller::getRoute();
166 1
            $route = $route->withController($controller);
167 1
            $this->router->add($route);
168 1
            return;
169
        }
170
171 1
        throw ApplicationException::forInvalidController($controller);
172
    }
173
174
    /**
175
     * Handles request flow process.
176
     *
177
     * @see MiddlewareInterface::process()
178
     *
179
     * @param ServerRequestInterface $request
180
     * @param RequestHandlerInterface $next
181
     * @return ResponseInterface
182
     */
183 13
    public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
184
    {
185
        /** @var Route $route */
186 13
        $route = $this->router->find(
187 13
            $request->getMethod(),
188 13
            $request->getUri()->getPath()
189
        );
190
191 10
        $controller = $route->getController();
192
193 10
        if ($request instanceof ServerRequest) {
194 10
            $request = $request->withAttributes($route->getAttributes());
195
        }
196
197 10
        if (is_string($controller) &&
198 10
            class_exists($controller) &&
199 10
            is_subclass_of($controller, Controller::class)
200
        ) {
201
            /** @var Controller $instance */
202
            $instance = $this->resolver->resolve($controller);
203
            return $instance($request);
204
        }
205
206 10
        if (is_callable($controller)) {
207 10
            $response = $controller($request);
208 10
            if (!$response instanceof ResponseInterface) {
209
                throw ControllerException::forInvalidReturnValue();
210
            }
211
212 10
            return $response;
213
        }
214
215
        throw ControllerException::forMissingController($route->getPath());
216
    }
217
218
    /**
219
     * Runs application listeners and handles request flow process.
220
     *
221
     * @param ServerRequestInterface $request
222
     * @return ResponseInterface
223
     */
224 13
    public function handle(ServerRequestInterface $request): ResponseInterface
225
    {
226 13
        $response = $this->getMiddlewarePipe()->handle($request);
227
228 13
        return $response;
229
    }
230
231
    /**
232
     * Decorator for handle method, used by server instance.
233
     * @see Application::handle()
234
     * @see Server::addListener()
235
     *
236
     * @param ResponseInterface $response
237
     * @param Client $client
238
     * @param ServerRequestInterface $request
239
     * @return ResponseInterface
240
     */
241
    public function onRequest(Client $client, ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
242
    {
243
        return $this->handle($request);
244
    }
245
246
    /**
247
     * Registers new controller that accepts get request
248
     * when request uri matches passed route pattern.
249
     *
250
     * @param string $route
251
     * @param callable $controller
252
     */
253 4
    public function get(string $route, callable $controller): void
254
    {
255 4
        $this->register($controller, Route::get($route));
256 4
    }
257
258
    /**
259
     * Registers new controller that accepts post request
260
     * when request uri matches passed route pattern.
261
     *
262
     * @param string $route
263
     * @param callable $controller
264
     */
265 1
    public function post(string $route, callable $controller): void
266
    {
267 1
        $this->register($controller, Route::post($route));
268 1
    }
269
270
    /**
271
     * Registers new controller that accepts put request
272
     * when request uri matches passed route pattern.
273
     *
274
     * @param string $route
275
     * @param callable $controller
276
     */
277 1
    public function put(string $route, callable $controller): void
278
    {
279 1
        $this->register($controller, Route::put($route));
280 1
    }
281
282
    /**
283
     * Registers new controller that accepts patch request
284
     * when request uri matches passed route pattern.
285
     *
286
     * @param string $route
287
     * @param callable $controller
288
     */
289 1
    public function patch(string $route, callable $controller): void
290
    {
291 1
        $this->register($controller, Route::patch($route));
292 1
    }
293
294
    /**
295
     * Registers new controller that accepts delete request
296
     * when request uri matches passed route pattern.
297
     *
298
     * @param string $route
299
     * @param callable $controller
300
     */
301 1
    public function delete(string $route, callable $controller): void
302
    {
303 1
        $this->register($controller, Route::delete($route));
304 1
    }
305
306
    /**
307
     * Registers new controller that accepts options request
308
     * when request uri matches passed route pattern.
309
     *
310
     * @param string $route
311
     * @param callable $controller
312
     */
313 1
    public function options(string $route, callable $controller): void
314
    {
315 1
        $this->register($controller, Route::options($route));
316 1
    }
317
318
    /**
319
     * Registers new controller that accepts head request
320
     * when request uri matches passed route pattern.
321
     *
322
     * @param string $route
323
     * @param callable $controller
324
     */
325 1
    public function head(string $route, callable $controller): void
326
    {
327 1
        $this->register($controller, Route::head($route));
328 1
    }
329
330
    /**
331
     * Registers new controller that listens on the passed route.
332
     *
333
     * @param Route $route
334
     * @param callable $controller
335
     */
336
    public function on(Route $route, callable $controller): void
337
    {
338
        $this->register($controller, $route);
339
    }
340
341
    /**
342
     * Returns application's controller aggregate.
343
     *
344
     * @return ControllerAggregator
345
     */
346 1
    public function getControllerAggregator(): ControllerAggregator
347
    {
348 1
        return $this;
349
    }
350
351
    /**
352
     * Middleware aggregator is used to register application's middlewares.
353
     *
354
     * @return MiddlewareAggregator
355
     */
356
    public function getMiddlewareAggregator(): MiddlewareAggregator
357
    {
358
        return $this;
359
    }
360
361 13
    protected function getMiddlewarePipe(): MiddlewarePipe
362
    {
363 13
        if ($this->pipeline) {
364
            return $this->pipeline;
365
        }
366
367 13
        return $this->pipeline = $this->composeMiddlewarePipe();
368
    }
369
370 2
    protected function initializeModule(&$module): void
371
    {
372 2
        parent::initializeModule($module);
373
374 2
        if ($module instanceof MiddlewareProvider) {
375 1
            $module->provideMiddleware($this->getMiddlewareAggregator());
376
        }
377 2
    }
378
379 13
    private function composeMiddlewarePipe(): MiddlewarePipe
380
    {
381 13
        $pipe = new MiddlewarePipe();
382
        $pipe->add(new ErrorMiddleware(function(Throwable $exception) {
383 3
            return $this->handleOnErrorListeners($exception);
384 13
        }));
385 13
        foreach ($this->middleware as $middleware) {
386 2
            if (is_string($middleware)) {
387
                $middleware = $this->resolver->resolve($middleware);
388
            }
389 2
            $pipe->add($middleware);
390
        }
391 13
        $pipe->add($this);
392
393 13
        return $pipe;
394
    }
395
}
396