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
|
|||||
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
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
![]() |
|||||
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 |
Let?s assume that you have a directory layout like this:
and let?s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/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 beforeOtherDir/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: