Passed
Push — master ( 6282ae...86ce5d )
by Gabor
04:01
created

ServiceAdapter::renderOutput()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 28
ccs 0
cts 0
cp 0
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 13
nc 3
nop 0
crap 20
1
<?php
2
/**
3
 * WebHemi.
4
 *
5
 * PHP version 7.1
6
 *
7
 * @copyright 2012 - 2018 Gixx-web (http://www.gixx-web.com)
8
 * @license   https://opensource.org/licenses/MIT The MIT License (MIT)
9
 *
10
 * @link      http://www.gixx-web.com
11
 */
12
declare(strict_types = 1);
13
14
namespace WebHemi\Application\ServiceAdapter\Base;
15
16
use Throwable;
17
use RuntimeException;
18
use Psr\Http\Message\StreamInterface;
19
use WebHemi\Application\ServiceInterface;
20
use WebHemi\Application\ServiceAdapter\AbstractAdapter;
21
use WebHemi\Environment\ServiceInterface as EnvironmentInterface;
22
use WebHemi\Http\ResponseInterface;
23
use WebHemi\Http\ServerRequestInterface;
24
use WebHemi\MiddlewarePipeline\ServiceInterface as PipelineInterface;
25
use WebHemi\Middleware\Common as CommonMiddleware;
26
use WebHemi\Middleware\MiddlewareInterface;
27
use WebHemi\Renderer\ServiceInterface as RendererInterface;
28
use WebHemi\Session\ServiceInterface as SessionInterface;
29
30
/**
31
 * Class ServiceAdapter
32
 */
33
class ServiceAdapter extends AbstractAdapter
34
{
35
    /**
36
     * Starts the session.
37
     *
38
     * @return ServiceInterface
39
     *
40
     * @codeCoverageIgnore - not testing session (yet)
41
     */
42
    public function initSession() : ServiceInterface
43
    {
44
        if (defined('PHPUNIT_WEBHEMI_TESTSUITE')) {
45
            return $this;
46
        }
47
48
        /** @var SessionInterface $sessionManager */
49
        $sessionManager = $this->container->get(SessionInterface::class);
50
        /** @var EnvironmentInterface $environmentManager */
51
        $environmentManager = $this->container->get(EnvironmentInterface::class);
52
53
        $name = $environmentManager->getSelectedApplication();
54
        $timeOut = 3600;
55
        $path = $environmentManager->getSelectedApplicationUri();
56
        $domain = $environmentManager->getApplicationDomain();
57
        $secure = $environmentManager->isSecuredApplication();
58
        $httpOnly = true;
59
60
        $sessionManager->start($name, $timeOut, $path, $domain, $secure, $httpOnly);
61
62
        return $this;
63
    }
64
65
    /**
66
     * Runs the application. This is where the magic happens.
67
     * According tho the environment settings this must build up the middleware pipeline and execute it.
68
     *
69
     * a Pre-Routing Middleware can be; priority < 0:
70
     *  - LockCheck - check if the client IP is banned > S102|S403
71
     *  - Auth - if the user is not logged in, but there's a "Remember me" cookie, then logs in > S102
72
     *
73
     * Routing Middleware is fixed (RoutingMiddleware::class); priority = 0:
74
     *  - A middleware that routes the incoming Request and delegates to the matched middleware. > S102|S404|S405
75
     *    The RouteResult should be attached to the Request.
76
     *    If the Routing is not defined explicitly in the pipeline, then it will be injected with priority 0.
77
     *
78
     * a Post-Routing Middleware can be; priority between 0 and 100:
79
     *  - Acl - checks if the given route is available for the client. Also checks the auth > S102|S401|S403
80
     *  - CacheReader - checks if a suitable response body is cached. > S102|S200
81
     *
82
     * Dispatcher Middleware is fixed (DispatcherMiddleware::class); priority = 100:
83
     *  - A middleware which gets the corresponding Action middleware and applies it > S102
84
     *    If the Dispatcher is not defined explicitly in the pipeline, then it will be injected with priority 100.
85
     *    The Dispatcher should not set the response Status Code to 200 to let Post-Dispatchers to be called.
86
     *
87
     * a Post-Dispatch Middleware can be; priority > 100:
88
     *  - CacheWriter - writes response body into DataStorage (DB, File etc.) > S102
89
     *
90
     * Final Middleware is fixed (FinalMiddleware:class):
91
     *  - This middleware behaves a bit differently. It cannot be ordered, it's always the last called middleware:
92
     *    - when the middleware pipeline reached its end (typically when the Status Code is still 102)
93
     *    - when one item of the middleware pipeline returns with return response (status code is set to 200|40*|500)
94
     *    - when during the pipeline process an Exception is thrown.
95
     *
96
     * When the middleware pipeline is finished the application prints the header and the output.
97
     *
98
     * If a middleware other than the Routing, Dispatcher and Final Middleware has no priority set, it will be
99
     * considered to have priority = 50.
100
     *
101
     * @return ServiceInterface
102
     */
103 5
    public function run() : ServiceInterface
104
    {
105
        /** @var PipelineInterface $pipelineManager */
106 5
        $pipelineManager = $this->container->get(PipelineInterface::class);
107
108
        try {
109
            /** @var string $middlewareClass */
110 5
            $middlewareClass = $pipelineManager->start();
111
112 5
            while ($middlewareClass !== null
113 5
                && $this->response->getStatusCode() == ResponseInterface::STATUS_PROCESSING
114
            ) {
115 5
                $this->invokeMiddleware($middlewareClass);
116 5
                $middlewareClass = $pipelineManager->next();
117
            };
118
119
            // If there was no error, we mark as ready for output.
120 1
            if ($this->response->getStatusCode() == ResponseInterface::STATUS_PROCESSING) {
121 1
                $this->response = $this->response->withStatus(ResponseInterface::STATUS_OK);
122
            }
123 4
        } catch (Throwable $exception) {
124 4
            $code = ResponseInterface::STATUS_INTERNAL_SERVER_ERROR;
125
126 4
            if (in_array(
127 4
                $exception->getCode(),
128
                [
129 4
                    ResponseInterface::STATUS_BAD_REQUEST,
130 4
                    ResponseInterface::STATUS_UNAUTHORIZED,
131 4
                    ResponseInterface::STATUS_FORBIDDEN,
132 4
                    ResponseInterface::STATUS_NOT_FOUND,
133 4
                    ResponseInterface::STATUS_BAD_METHOD,
134 4
                    ResponseInterface::STATUS_NOT_IMPLEMENTED,
135
                ]
136
            )) {
137 3
                $code = $exception->getCode();
138
            }
139
140 4
            $this->response = $this->response->withStatus($code);
141 4
            $this->request = $this->request
142 4
                ->withAttribute(ServerRequestInterface::REQUEST_ATTR_MIDDLEWARE_EXCEPTION, $exception);
143
        }
144
145
        /** @var CommonMiddleware\FinalMiddleware $finalMiddleware */
146 5
        $finalMiddleware = $this->container->get(CommonMiddleware\FinalMiddleware::class);
147
        // Check response and log errors if necessary
148 5
        $finalMiddleware($this->request, $this->response);
149
150 5
        return $this;
151
    }
152
153
    /**
154
     * Renders the response body and sends it to the client.
155
     *
156
     * @return void
157
     *
158
     * @codeCoverageIgnore - no output for tests
159
     */
160
    public function renderOutput() : void
161
    {
162
        // Create template only when there's no redirect
163
        if (!$this->request->isXmlHttpRequest()
164
            && ResponseInterface::STATUS_REDIRECT != $this->response->getStatusCode()
165
        ) {
166
            /** @var RendererInterface $templateRenderer */
167
            $templateRenderer = $this->container->get(RendererInterface::class);
168
            /** @var string $template */
169
            $template = $this->request->getAttribute(ServerRequestInterface::REQUEST_ATTR_DISPATCH_TEMPLATE);
170
            /** @var array $data */
171
            $data = $this->request->getAttribute(ServerRequestInterface::REQUEST_ATTR_DISPATCH_DATA);
172
            /** @var null|Throwable $exception */
173
            $exception = $this->request->getAttribute(ServerRequestInterface::REQUEST_ATTR_MIDDLEWARE_EXCEPTION);
174
175
            // If there was any error, change the remplate
176
            if (!empty($exception)) {
177
                $template = 'error-'.$this->response->getStatusCode();
178
                $data['exception'] = $exception;
179
            }
180
181
            /** @var StreamInterface $body */
182
            $body = $templateRenderer->render($template, $data);
183
            $this->response = $this->response->withBody($body);
184
        }
185
186
        $this->sendOutput();
187
    }
188
189
    /**
190
     * Sends the response body to the client.
191
     *
192
     * @return void
193
     *
194
     * @codeCoverageIgnore - no output for tests
195
     */
196
    public function sendOutput() : void
197
    {
198
        // @codeCoverageIgnoreStart
199
        if (!defined('PHPUNIT_WEBHEMI_TESTSUITE') && headers_sent()) {
200
            throw new RuntimeException('Unable to emit response; headers already sent', 1000);
201
        }
202
        // @codeCoverageIgnoreEnd
203
204
        $output = $this->response->getBody();
205
        $contentLength = $this->response->getBody()->getSize();
206
207
        if ($this->request->isXmlHttpRequest()) {
208
            /** @var array $templateData */
209
            $templateData = $this->request->getAttribute(ServerRequestInterface::REQUEST_ATTR_DISPATCH_DATA);
210
            $templateData['output'] = (string) $output;
211
            /** @var null|Throwable $exception */
212
            $exception = $this->request->getAttribute(ServerRequestInterface::REQUEST_ATTR_MIDDLEWARE_EXCEPTION);
213
214
            if (!empty($exception)) {
215
                $templateData['exception'] = $exception;
216
            }
217
218
            $output = json_encode($templateData);
219
            $contentLength = strlen($output);
220
            $this->response = $this->response->withHeader('Content-Type', 'application/json; charset=UTF-8');
221
        }
222
223
        $this->injectContentLength($contentLength);
224
        $this->sendHttpHeader();
225
        $this->sendOutputHeaders($this->response->getHeaders());
226
        echo $output;
227
    }
228
229
    /**
230
     * Instantiates and invokes a middleware
231
     *
232
     * @param string $middlewareClass
233
     * @return void
234
     */
235 5
    protected function invokeMiddleware(string $middlewareClass) : void
236
    {
237
        /** @var MiddlewareInterface $middleware */
238 5
        $middleware = $this->container->get($middlewareClass);
239 5
        $requestAttributes = $this->request->getAttributes();
240
241
        // As an extra step if an action middleware is resolved, it should be invoked by the dispatcher.
242 5
        if (isset($requestAttributes[ServerRequestInterface::REQUEST_ATTR_RESOLVED_ACTION_CLASS])
243 5
            && $middleware instanceof CommonMiddleware\DispatcherMiddleware
244
        ) {
245
            /** @var MiddlewareInterface $actionMiddleware */
246 3
            $actionMiddleware = $this->container
247 3
                ->get($requestAttributes[ServerRequestInterface::REQUEST_ATTR_RESOLVED_ACTION_CLASS]);
248 3
            $this->request = $this->request->withAttribute(
249 3
                ServerRequestInterface::REQUEST_ATTR_ACTION_MIDDLEWARE,
250 3
                $actionMiddleware
251
            );
252
        }
253
254 5
        $middleware($this->request, $this->response);
255 5
    }
256
257
    /**
258
     * Inject the Content-Length header if is not already present.
259
     *
260
     * NOTE: if there will be chunk content displayed, check if the response getSize counts the real size correctly
261
     *
262
     * @param null|int $contentLength
263
     * @return void
264
     *
265
     * @codeCoverageIgnore - no putput for tests.
266
     */
267
    protected function injectContentLength(? int $contentLength) : void
268
    {
269
        $contentLength = intval($contentLength);
270
271
        if (!$this->response->hasHeader('Content-Length') && $contentLength > 0) {
272
            $this->response = $this->response->withHeader('Content-Length', (string) $contentLength);
273
        }
274
    }
275
276
    /**
277
     * Filter a header name to word case.
278
     *
279
     * @param string $headerName
280
     * @return string
281
     */
282 5
    protected function filterHeaderName(string $headerName) : string
283
    {
284 5
        $filtered = str_replace('-', ' ', $headerName);
285 5
        $filtered = ucwords($filtered);
286 5
        return str_replace(' ', '-', $filtered);
287
    }
288
289
    /**
290
     * Sends the HTTP header.
291
     *
292
     * @return void
293
     *
294
     * @codeCoverageIgnore - vendor and core function calls
295
     */
296
    protected function sendHttpHeader() : void
297
    {
298
        $reasonPhrase = $this->response->getReasonPhrase();
299
        header(sprintf(
300
            'HTTP/%s %d%s',
301
            $this->response->getProtocolVersion(),
302
            $this->response->getStatusCode(),
303
            ($reasonPhrase ? ' '.$reasonPhrase : '')
304
        ));
305
    }
306
307
    /**
308
     * Sends out output headers.
309
     *
310
     * @param array $headers
311
     * @return void
312
     *
313
     * @codeCoverageIgnore - vendor and core function calls in loop
314
     */
315
    protected function sendOutputHeaders(array $headers) : void
316
    {
317
        foreach ($headers as $headerName => $values) {
318
            $name  = $this->filterHeaderName($headerName);
319
            $first = true;
320
321
            foreach ($values as $value) {
322
                header(sprintf('%s: %s', $name, $value), $first);
323
                $first = false;
324
            }
325
        }
326
    }
327
}
328