Completed
Push — master ( b7f6a5...e297c9 )
by Dawid
05:58
created

Application::createPipeline()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 0
dl 0
loc 15
ccs 10
cts 11
cp 0.9091
crap 3.0067
rs 9.4285
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Igni\Http;
4
5
use FastRoute\DataGenerator\GroupCountBased as StandardDataGenerator;
6
use FastRoute\RouteParser\Std as StandardRouteParser;
7
use Igni\Application\Application as AbstractApplication;
8
use Igni\Application\Controller\ControllerAggregate as AbstractControllerAggregate;
9
use Igni\Application\Exception\ApplicationException;
10
use Igni\Container\DependencyResolver;
11
use Igni\Http\Controller\ControllerAggregate;
12
use Igni\Http\Exception\HttpModuleException;
13
use Igni\Http\Middleware\ErrorMiddleware;
14
use Igni\Http\Server\OnRequest;
15
use Psr\Container\ContainerInterface;
16
use Psr\Http\Message\ResponseInterface;
17
use Psr\Http\Message\ServerRequestInterface;
18
use Psr\Http\Server\MiddlewareInterface;
19
use Psr\Http\Server\RequestHandlerInterface;
20
use Zend\Diactoros\Response\SapiEmitter;
21
use Zend\Stratigility\Middleware\CallableMiddlewareDecorator;
22
use Zend\Stratigility\MiddlewarePipe;
23
use Throwable;
24
25
/**
26
 * @see \Igni\Application\Application
27
 *
28
 * @package Igni\Http
29
 */
30
class Application
31
    extends AbstractApplication
32
    implements MiddlewareAggregate, MiddlewareInterface, RequestHandlerInterface, OnRequest {
33
34
    /**
35
     * @var Router
36
     */
37
    private $router;
38
39
    /**
40
     * @var ControllerAggregate
41
     */
42
    private $controllerAggregate;
43
44
    /**
45
     * @var DependencyResolver
46
     */
47
    private $resolver;
48
49
    /**
50
     * @var string|MiddlewareInterface[]
51
     */
52
    private $middlewares = [];
53
54
    /**
55
     * @var MiddlewarePipe
56
     */
57
    private $pipeline;
58
59
    /**
60
     * Application constructor.
61
     *
62
     * @param ContainerInterface|null $container
63
     */
64 12
    public function __construct(ContainerInterface $container = null)
65
    {
66 12
        parent::__construct($container);
67
68 12
        $this->router = new Router(new StandardRouteParser(), new StandardDataGenerator());
69 12
        $this->resolver = new DependencyResolver($this->serviceLocator);
70 12
        $this->controllerAggregate = new ControllerAggregate($this->router);
71 12
    }
72
73
    /**
74
     * While testing call this method before handle method.
75
     */
76 8
    public function startup(): void
77
    {
78 8
        $this->initialize();
79 8
        $this->handleOnBootListeners();
80 8
    }
81
82
    /**
83
     * While testing, call this method after handle method.
84
     */
85 1
    public function shutdown(): void
86
    {
87 1
        $this->handleOnShutDownListeners();
88 1
    }
89
90
    /**
91
     * Startups and run application with/or without dedicated server.
92
     * Once application is run it will listen to incoming http requests,
93
     * and takes care of the entire request flow process.
94
     *
95
     * @param Server|null $server
96
     */
97
    public function run(Server $server = null): void
98
    {
99
        $this->startup();
100
101
        if ($server) {
102
            $server->addListener($this);
103
            $server->start();
104
        } else {
105
            $response = $this->handle(ServerRequest::fromGlobals());
106
            $emitter = new SapiEmitter();
107
            $emitter->emit($response);
108
            if ($response instanceof Response) {
109
                $response->end();
110
            }
111
        }
112
113
        $this->shutdown();
114
    }
115
116
    /**
117
     * Registers PSR-15 compatible middelware.
118
     * Middleware can be either callable object which accepts PSR-7 server request interface and returns
119
     * response interface, or just class name that implements psr-15 middleware or its instance.
120
     *
121
     * @param MiddlewareInterface|callable $middleware
122
     */
123 1
    public function use($middleware): void
124
    {
125 1
        if (!is_subclass_of($middleware, MiddlewareInterface::class)) {
126 1
            if (!is_callable($middleware)) {
127
                throw new ApplicationException('Middleware must be either class or object that implements ' . MiddlewareInterface::class);
128
            }
129
130 1
            $middleware = 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 Zend\Stratigility\Middle...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

130
            $middleware = new CallableMiddlewareDecorator(/** @scrutinizer ignore-type */ $middleware);
Loading history...
131
        }
132
133 1
        $this->middlewares[] = $middleware;
134 1
    }
135
136
    /**
137
     * Handles request flow process.
138
     *
139
     * @see MiddlewareInterface::process()
140
     *
141
     * @param ServerRequestInterface $request
142
     * @param RequestHandlerInterface $next
143
     * @return ResponseInterface
144
     */
145 11
    public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
146
    {
147
        /** @var Route $route */
148 11
        $route = $this->router->findRoute(
149 11
            $request->getMethod(),
150 11
            $request->getUri()->getPath()
151
        );
152
153 9
        $controller = $route->getDelegator();
154
155 9
        if ($request instanceof ServerRequest) {
156 9
            $request = $request->withAttributes($route->getAttributes());
157
        }
158
159 9
        if (is_string($controller) &&
160 9
            class_exists($controller) &&
161 9
            in_array(Controller::class, class_implements($controller))
162
        ) {
163
            /** @var Controller $instance */
164
            $instance = $this->dependencyResolver->resolve($controller);
165
            return $instance($request);
166
        }
167
168 9
        if (is_callable($controller)) {
169 9
            $response = $controller($request);
170 9
            if (!$response instanceof ResponseInterface) {
171
                throw HttpModuleException::controllerMustReturnValidResponse();
172
            }
173
174 9
            return $response;
175
        }
176
177
        throw HttpModuleException::couldNotRetrieveControllerForRoute($route->getExpression());
178
    }
179
180
    /**
181
     * Runs application listeners and handles request flow process.
182
     *
183
     * @param ServerRequestInterface $request
184
     * @return ResponseInterface
185
     */
186 11
    public function handle(ServerRequestInterface $request): ResponseInterface
187
    {
188 11
        $this->handleOnRunListeners();
189 11
        $response = $this->getMiddlewarePipe()->handle($request);
190 11
        return $response;
191
    }
192
193
    /**
194
     * Decorator for handle method, used by server instance.
195
     * @see Application::handle()
196
     * @see Server::addListener()
197
     *
198
     * @param ServerRequestInterface $request
199
     * @return ResponseInterface
200
     */
201
    public function onRequest(ServerRequestInterface $request): ResponseInterface
202
    {
203
        return $this->handle($request);
204
    }
205
206
    /**
207
     * Registers new controller that accepts get request
208
     * when request uri matches passed route pattern.
209
     *
210
     * @param string $route
211
     * @param callable $controller
212
     */
213 3
    public function get(string $route, callable $controller): void
214
    {
215 3
        $this->controllerAggregate->add($controller, Route::get($route));
216 3
    }
217
218
    /**
219
     * Registers new controller that accepts post request
220
     * when request uri matches passed route pattern.
221
     *
222
     * @param string $route
223
     * @param callable $controller
224
     */
225 1
    public function post(string $route, callable $controller): void
226
    {
227 1
        $this->controllerAggregate->add($controller, Route::post($route));
228 1
    }
229
230
    /**
231
     * Registers new controller that accepts put request
232
     * when request uri matches passed route pattern.
233
     *
234
     * @param string $route
235
     * @param callable $controller
236
     */
237 1
    public function put(string $route, callable $controller): void
238
    {
239 1
        $this->controllerAggregate->add($controller, Route::put($route));
240 1
    }
241
242
    /**
243
     * Registers new controller that accepts patch request
244
     * when request uri matches passed route pattern.
245
     *
246
     * @param string $route
247
     * @param callable $controller
248
     */
249 1
    public function patch(string $route, callable $controller): void
250
    {
251 1
        $this->controllerAggregate->add($controller, Route::patch($route));
252 1
    }
253
254
    /**
255
     * Registers new controller that accepts delete request
256
     * when request uri matches passed route pattern.
257
     *
258
     * @param string $route
259
     * @param callable $controller
260
     */
261 1
    public function delete(string $route, callable $controller): void
262
    {
263 1
        $this->controllerAggregate->add($controller, Route::delete($route));
264 1
    }
265
266
    /**
267
     * Registers new controller that accepts options request
268
     * when request uri matches passed route pattern.
269
     *
270
     * @param string $route
271
     * @param callable $controller
272
     */
273 1
    public function options(string $route, callable $controller): void
274
    {
275 1
        $this->controllerAggregate->add($controller, Route::options($route));
276 1
    }
277
278 1
    public function head(string $route, callable $controller): void
279
    {
280 1
        $this->controllerAggregate->add($controller, Route::head($route));
281 1
    }
282
283
    /**
284
     * Returns application's controller aggregate.
285
     *
286
     * @return AbstractControllerAggregate
287
     */
288
    public function getControllerAggregate(): AbstractControllerAggregate
289
    {
290
        return $this->controllerAggregate;
291
    }
292
293 11
    protected function getMiddlewarePipe(): MiddlewarePipe
294
    {
295 11
        if ($this->pipeline) {
296
            return $this->pipeline;
297
        }
298
299 11
        return $this->pipeline = $this->createPipeline();
300
    }
301
302 11
    private function createPipeline(): MiddlewarePipe
303
    {
304 11
        $pipe = new MiddlewarePipe();
305 11
        $pipe->pipe(new ErrorMiddleware(function(Throwable $exception) {
306 2
            $this->handleOnErrorListeners($exception);
307 11
        }));
308 11
        foreach ($this->middlewares as $middleware) {
309 1
            if (is_string($middleware)) {
310
                $middleware = $this->resolver->resolve($middleware);
311
            }
312 1
            $pipe->pipe($middleware);
313
        }
314 11
        $pipe->pipe($this);
315
316 11
        return $pipe;
317
    }
318
}
319