1 | <?php |
||
2 | |||
3 | namespace Phprest; |
||
4 | |||
5 | use Closure; |
||
6 | use Doctrine\Common\Annotations\AnnotationRegistry; |
||
7 | use League\Container\ContainerAwareTrait; |
||
8 | use League\Event\EmitterInterface; |
||
9 | use League\Event\EmitterTrait; |
||
10 | use League\Event\ListenerAcceptorInterface; |
||
11 | use Phprest\Middleware\ApiVersion; |
||
12 | use Phprest\Router\RouteCollection; |
||
13 | use Phprest\Service; |
||
14 | use Symfony\Component\HttpFoundation\Request; |
||
15 | use Symfony\Component\HttpFoundation\Response; |
||
16 | use Symfony\Component\HttpKernel\HttpKernelInterface; |
||
17 | use Symfony\Component\HttpKernel\TerminableInterface; |
||
18 | use LogicException; |
||
19 | use Exception; |
||
20 | use Stack; |
||
21 | |||
22 | class Application implements |
||
23 | HttpKernelInterface, |
||
24 | TerminableInterface, |
||
25 | ListenerAcceptorInterface |
||
26 | { |
||
27 | use EmitterTrait; |
||
28 | use ContainerAwareTrait; |
||
29 | use Service\Hateoas\Getter; |
||
30 | use Service\Hateoas\Util; |
||
0 ignored issues
–
show
introduced
by
![]() |
|||
31 | use Service\Logger\Getter; |
||
32 | |||
33 | public const CONTAINER_ID_DEBUG = 'debug'; |
||
34 | public const CONTAINER_ID_VENDOR = 'vendor'; |
||
35 | public const CONTAINER_ID_API_VERSION = 'api-version'; |
||
36 | public const CONTAINER_ID_ROUTER = 'router'; |
||
37 | public const API_VERSION_REG_EXP = '((?:[0-9](?:\.[0-9])?){1})'; |
||
38 | |||
39 | protected Config $configuration; |
||
40 | protected Stack\Builder $stackBuilder; |
||
41 | protected RouteCollection $router; |
||
42 | |||
43 | /** |
||
44 | * @var callable |
||
45 | */ |
||
46 | protected $exceptionDecorator; |
||
47 | |||
48 | 8 | public function __construct(Config $configuration) |
|
49 | { |
||
50 | 8 | $this->configuration = $configuration; |
|
51 | 8 | $this->container = $configuration->getContainer(); |
|
52 | 8 | $this->router = $configuration->getRouter(); |
|
53 | 8 | $this->emitter = $configuration->getEventEmitter(); |
|
54 | |||
55 | 8 | AnnotationRegistry::registerLoader('class_exists'); |
|
56 | |||
57 | 8 | $this->registerService($configuration->getHateoasService(), $configuration->getHateoasConfig()); |
|
58 | 8 | $this->registerService($configuration->getLoggerService(), $configuration->getLoggerConfig()); |
|
59 | |||
60 | 8 | $this->setErrorHandler(); |
|
61 | |||
62 | 8 | $this->container->add(self::CONTAINER_ID_VENDOR, $configuration->getVendor()); |
|
63 | 8 | $this->container->add(self::CONTAINER_ID_API_VERSION, $configuration->getApiVersion()); |
|
64 | 8 | $this->container->add(self::CONTAINER_ID_DEBUG, $configuration->isDebug()); |
|
65 | $this->container->add(self::CONTAINER_ID_ROUTER, function () { |
||
66 | 1 | return $this->router; |
|
67 | 8 | }); |
|
68 | |||
69 | 8 | $this->stackBuilder = new Stack\Builder(); |
|
70 | 8 | } |
|
71 | |||
72 | 8 | public function registerService(Service\Serviceable $service, Service\Configurable $config): void |
|
73 | { |
||
74 | 8 | $service->register($this->container, $config); |
|
75 | 8 | } |
|
76 | |||
77 | /** |
||
78 | * @param string $class Namespaced class name |
||
79 | */ |
||
80 | 1 | public function registerController(string $class): void |
|
81 | { |
||
82 | 1 | $controller = new $class($this->container); |
|
83 | |||
84 | $this->container->add($class, static function () use ($controller) { |
||
85 | 1 | return $controller; |
|
86 | 1 | }); |
|
87 | 1 | } |
|
88 | |||
89 | 2 | public function registerMiddleware(string $classPath, array $arguments = []): void |
|
90 | { |
||
91 | 2 | call_user_func_array([$this->stackBuilder, 'push'], array_merge([$classPath], $arguments)); |
|
92 | 2 | } |
|
93 | |||
94 | /** |
||
95 | * Run the application |
||
96 | */ |
||
97 | 2 | public function run(Request $request = null): void |
|
98 | { |
||
99 | 2 | if (null === $request) { |
|
100 | 1 | $request = Request::createFromGlobals(); |
|
101 | } |
||
102 | |||
103 | 2 | $this->registerMiddleware(ApiVersion::class); |
|
104 | |||
105 | 2 | $app = $this->stackBuilder->resolve($this); |
|
106 | |||
107 | 2 | $response = $app->handle($request, self::MASTER_REQUEST, false); |
|
108 | 1 | $response->send(); |
|
109 | |||
110 | 1 | $app->terminate($request, $response); |
|
111 | 1 | } |
|
112 | |||
113 | /** |
||
114 | * Handle the request. |
||
115 | * |
||
116 | * @param Request $request |
||
117 | * @param int $type |
||
118 | * @param bool $catch |
||
119 | * |
||
120 | * @return Response |
||
121 | * @throws LogicException |
||
122 | * |
||
123 | * @throws Exception |
||
124 | */ |
||
125 | 2 | public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true): ?Response |
|
126 | { |
||
127 | // Passes the request to the container |
||
128 | 2 | $this->getContainer()->add(Request::class, $request); |
|
129 | |||
130 | try { |
||
131 | 2 | $this->emit('request.received', $request); |
|
132 | |||
133 | 2 | $dispatcher = $this->getRouter()->getDispatcher(); |
|
134 | 2 | $response = $dispatcher->dispatch( |
|
135 | 2 | $request->getMethod(), |
|
136 | 2 | $request->getPathInfo() |
|
137 | ); |
||
138 | |||
139 | 1 | $this->emit('response.created', $request, $response); |
|
140 | |||
141 | 1 | return $response; |
|
142 | 1 | } catch (Exception $e) { |
|
143 | 1 | if (!$catch) { |
|
144 | 1 | throw $e; |
|
145 | } |
||
146 | |||
147 | $response = call_user_func($this->exceptionDecorator, $e); |
||
148 | if (!$response instanceof Response) { |
||
149 | throw new LogicException( |
||
150 | 'Exception decorator did not return an instance of Symfony\Component\HttpFoundation\Response' |
||
151 | ); |
||
152 | } |
||
153 | |||
154 | $this->emit('response.created', $request, $response); |
||
155 | |||
156 | return $response; |
||
157 | } |
||
158 | } |
||
159 | |||
160 | /** |
||
161 | * Add a HEAD route |
||
162 | * |
||
163 | * @param string|Closure $action |
||
164 | */ |
||
165 | 1 | public function head(string $route, $action): self |
|
166 | { |
||
167 | 1 | $this->router->addRoute('HEAD', $route, $action); |
|
168 | |||
169 | 1 | return $this; |
|
170 | } |
||
171 | |||
172 | /** |
||
173 | * Add a OPTIONS route |
||
174 | * |
||
175 | * @param string|Closure $action |
||
176 | */ |
||
177 | 1 | public function options(string $route, $action): self |
|
178 | { |
||
179 | 1 | $this->router->addRoute('OPTIONS', $route, $action); |
|
180 | |||
181 | 1 | return $this; |
|
182 | } |
||
183 | |||
184 | /** |
||
185 | * Add a GET route. |
||
186 | * |
||
187 | * @param string|Closure $action |
||
188 | */ |
||
189 | 2 | public function get(string $route, $action): self |
|
190 | { |
||
191 | 2 | $this->getRouter()->addRoute('GET', $route, $action); |
|
192 | |||
193 | 2 | return $this; |
|
194 | } |
||
195 | |||
196 | /** |
||
197 | * Add a POST route. |
||
198 | * |
||
199 | * @param string|Closure $action |
||
200 | */ |
||
201 | public function post(string $route, $action): self |
||
202 | { |
||
203 | $this->getRouter()->addRoute('POST', $route, $action); |
||
204 | |||
205 | return $this; |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * Add a PUT route. |
||
210 | * |
||
211 | * @param string|Closure $action |
||
212 | */ |
||
213 | public function put(string $route, $action): self |
||
214 | { |
||
215 | $this->getRouter()->addRoute('PUT', $route, $action); |
||
216 | |||
217 | return $this; |
||
218 | } |
||
219 | |||
220 | /** |
||
221 | * Add a DELETE route. |
||
222 | * |
||
223 | * @param string|Closure $action |
||
224 | */ |
||
225 | public function delete(string $route, $action): self |
||
226 | { |
||
227 | $this->getRouter()->addRoute('DELETE', $route, $action); |
||
228 | |||
229 | return $this; |
||
230 | } |
||
231 | |||
232 | /** |
||
233 | * Add a PATCH route. |
||
234 | * |
||
235 | * @param string|Closure $action |
||
236 | */ |
||
237 | public function patch(string $route, $action): self |
||
238 | { |
||
239 | $this->getRouter()->addRoute('PATCH', $route, $action); |
||
240 | |||
241 | return $this; |
||
242 | } |
||
243 | |||
244 | 3 | public function getConfiguration(): Config |
|
245 | { |
||
246 | 3 | return $this->configuration; |
|
247 | } |
||
248 | |||
249 | 6 | public function getRouter(): RouteCollection |
|
250 | { |
||
251 | 6 | return $this->router; |
|
252 | } |
||
253 | |||
254 | /** |
||
255 | * Return the event emitter. |
||
256 | */ |
||
257 | 1 | public function getEventEmitter(): EmitterInterface |
|
258 | { |
||
259 | 1 | return $this->getEmitter(); |
|
260 | } |
||
261 | |||
262 | /** |
||
263 | * Terminates a request/response cycle. |
||
264 | */ |
||
265 | 1 | public function terminate(Request $request, Response $response): void |
|
266 | { |
||
267 | 1 | $this->emit('response.sent', $request, $response); |
|
268 | 1 | } |
|
269 | |||
270 | /** |
||
271 | * Subscribe to an event. |
||
272 | * |
||
273 | * @param string $event |
||
274 | * @param callable $listener |
||
275 | * @param int $priority |
||
276 | */ |
||
277 | public function subscribe($event, $listener, $priority = ListenerAcceptorInterface::P_NORMAL): void |
||
278 | { |
||
279 | $this->addListener($event, $listener, $priority); |
||
280 | } |
||
281 | |||
282 | /** |
||
283 | * Set the exception decorator. |
||
284 | */ |
||
285 | 8 | public function setExceptionDecorator(callable $func): void |
|
286 | { |
||
287 | 8 | $this->exceptionDecorator = $func; |
|
288 | 8 | } |
|
289 | |||
290 | 8 | protected function setErrorHandler(): void |
|
291 | { |
||
292 | 8 | $app = $this; |
|
293 | |||
294 | 8 | $this->configuration->getLogHandler()->setLogger($this->serviceLogger()); |
|
295 | |||
296 | 8 | $this->configuration->getErrorHandler()->pushHandler($this->configuration->getLogHandler()); |
|
297 | 8 | $this->configuration->getErrorHandler()->register(); |
|
298 | |||
299 | $this->setExceptionDecorator(static function (Exception $e) use ($app) { |
||
300 | $formatter = new ErrorHandler\Formatter\JsonXml($app->configuration); |
||
301 | |||
302 | return new Response($formatter->format($e), http_response_code()); |
||
303 | 8 | }); |
|
304 | 8 | } |
|
305 | } |
||
306 |