Passed
Push — master ( 84220e...6f9ef0 )
by Divine Niiquaye
02:24
created

RouteHandler   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 123
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 45
c 2
b 0
f 0
dl 0
loc 123
ccs 43
cts 43
cp 1
rs 10
wmc 18

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A handle() 0 26 3
B wrapResponse() 0 22 7
A isJson() 0 7 1
A detectResponse() 0 16 4
A isXml() 0 9 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Flight Routing.
7
 *
8
 * PHP version 7.1 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Flight\Routing;
19
20
use JsonSerializable;
21
use Psr\Http\Message\ResponseInterface;
22
use Psr\Http\Message\ServerRequestInterface;
23
use Psr\Http\Message\StreamInterface;
24
use Psr\Http\Server\RequestHandlerInterface;
25
use stdClass;
26
use Throwable;
27
28
/**
29
 * Provides ability to invoke any handler and write it's response into ResponseInterface.
30
 *
31
 * @author Divine Niiquaye Ibok <[email protected]>
32
 */
33
final class RouteHandler implements RequestHandlerInterface
34
{
35
    public const CONTENT_TYPE = 'Content-Type';
36
37
    /** @var callable */
38
    private $callable;
39
40
    /** @var ResponseInterface */
41
    private $responseFactory;
42
43
    /**
44
     * @param callable          $callable
45
     * @param ResponseInterface $responseFactory
46
     */
47 18
    public function __construct(callable $callable, ResponseInterface $responseFactory)
48
    {
49 18
        $this->callable        = $callable;
50 18
        $this->responseFactory = $responseFactory;
51 18
    }
52
53
    /**
54
     * {@inheritdoc}
55
     *
56
     * @throws Throwable
57
     */
58 14
    public function handle(ServerRequestInterface $request): ResponseInterface
59
    {
60 14
        $outputLevel = \ob_get_level();
61 14
        \ob_start();
62
63 14
        $output = '';
64 14
        $result = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $result is dead and can be removed.
Loading history...
65
66 14
        $response = $this->responseFactory
67 14
            ->withHeader(self::CONTENT_TYPE, 'text/html; charset=utf-8');
68
69
        try {
70 14
            $result = ($this->callable)($request, $response);
71 1
        } catch (Throwable $e) {
72 1
            \ob_get_clean();
73
74 1
            throw $e;
75 13
        } finally {
76
            // @codeCoverageIgnoreStart
77
            while (\ob_get_level() > $outputLevel + 1) {
78
                $output = \ob_get_clean() . $output;
79
            }
80
            // @codeCoverageIgnoreEnd
81
        }
82
83 13
        return $this->wrapResponse($response, $result, \ob_get_clean() . $output);
84
    }
85
86
    /**
87
     * Convert endpoint result into valid PSR 7 response.
88
     * content-type fallback is "text/html; charset=utf-8".
89
     *
90
     * @param ResponseInterface $response initial pipeline response
91
     * @param mixed             $result   generated endpoint output
92
     * @param string            $output   buffer output
93
     *
94
     * @return ResponseInterface
95
     */
96 13
    private function wrapResponse(ResponseInterface $response, $result = null, string $output = ''): ResponseInterface
97
    {
98
        // Always return the response...
99 13
        if ($result instanceof ResponseInterface) {
100
            // @codeCoverageIgnoreStart
101
            if (!empty($output) && $result->getBody()->isWritable()) {
102
                $result->getBody()->write($output);
103
            }
104
            // @codeCoverageIgnoreEnd
105
106 8
            return $result;
107
        }
108
109 5
        if (\is_array($result) || $result instanceof JsonSerializable || $result instanceof stdClass) {
110 1
            $result = \json_encode($result);
111
        }
112
113 5
        $response->getBody()->write((string) $result);
114 5
        $response->getBody()->write($output);
115
116
        //Always detect response anf glue buffered output
117 5
        return $this->detectResponse($response);
118
    }
119
120 5
    private function detectResponse(ResponseInterface $response): ResponseInterface
121
    {
122 5
        if ($this->isJson($response->getBody())) {
123 1
            return $response->withHeader(self::CONTENT_TYPE, 'application/json');
124
        }
125
126 4
        if ($this->isXml($response->getBody())) {
127 1
            return $response->withHeader(self::CONTENT_TYPE, 'application/xml; charset=utf-8');
128
        }
129
130
        // Set content-type to plain text if string doesn't contain </html> tag.
131 3
        if (0 === \preg_match('/(.*)(<\/html[^>]*>)/i', (string) $response->getBody())) {
132 2
            return $response->withHeader(self::CONTENT_TYPE, 'text/plain; charset=utf-8');
133
        }
134
135 1
        return $response;
136
    }
137
138 5
    private function isJson(StreamInterface $stream): bool
139
    {
140 5
        $stream->rewind();
141
142 5
        \json_decode($stream->getContents(), true);
143
144 5
        return \JSON_ERROR_NONE === \json_last_error();
145
    }
146
147 4
    private function isXml(StreamInterface $stream): bool
148
    {
149 4
        $stream->rewind();
150
151 4
        $previousValue = \libxml_use_internal_errors(true);
152 4
        $isXml         = \simplexml_load_string($contents = $stream->getContents());
153 4
        \libxml_use_internal_errors($previousValue);
154
155 4
        return false !== $isXml && false !== \strpos($contents, '<?xml');
156
    }
157
}
158