Passed
Pull Request — master (#76)
by
unknown
14:50 queued 04:29
created

ResponseService::isJsonResponse()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 3
c 1
b 0
f 0
nc 8
nop 0
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 4
rs 10
1
<?php
2
3
namespace CybozuHttp\Service;
4
5
use CybozuHttp\Exception\RuntimeException;
6
use GuzzleHttp\Exception\ClientException;
7
use GuzzleHttp\Exception\RequestException;
8
use GuzzleHttp\Exception\ServerException;
9
use Psr\Http\Message\RequestInterface;
10
use Psr\Http\Message\ResponseInterface;
11
12
/**
13
 * @author ochi51 <[email protected]>
14
 */
15
class ResponseService
16
{
17
    /**
18
     * @var RequestInterface
19
     */
20
    private $request;
21
22
    /**
23
     * @var ResponseInterface
24
     */
25
    private $response;
26
27
    /**
28
     * @var string|null
29
     */
30
    private $responseBody = null;
31
32
    /**
33
     * @var \Throwable|null
34
     */
35
    private $previousThrowable;
36
37
    /**
38
     * ResponseService constructor.
39
     * @param RequestInterface $request
40
     * @param ResponseInterface $response
41
     * @param \Throwable|null $previousThrowable
42
     */
43 64
    public function __construct(RequestInterface $request, ResponseInterface $response, ?\Throwable $previousThrowable = null)
44
    {
45 64
        $this->request = $request;
46 64
        $this->response = $response;
47 64
        $this->previousThrowable = $previousThrowable;
48
    }
49
50
    /**
51
     * @return bool
52
     */
53 63
    public function isJsonResponse(): bool
54
    {
55 63
        $contentType = $this->response->getHeader('Content-Type');
56 63
        $contentType = is_array($contentType) && isset($contentType[0]) ? $contentType[0] : $contentType;
57
58 63
        return is_string($contentType) && strpos($contentType, 'application/json') === 0;
59
    }
60
61
    /**
62
     * @return bool
63
     */
64 2
    public function isHtmlResponse(): bool
65
    {
66 2
        $contentType = $this->response->getHeader('Content-Type');
67 2
        $contentType = is_array($contentType) && isset($contentType[0]) ? $contentType[0] : $contentType;
68
69 2
        return is_string($contentType) && strpos($contentType, 'text/html') === 0;
70
    }
71
72
    /**
73
     * @throws RequestException
74
     * @throws RuntimeException
75
     */
76 4
    public function handleError(): void
77
    {
78 4
        if ($this->isJsonResponse()) {
79 4
            $this->handleJsonError();
80 1
        } else if ($this->isHtmlResponse()) {
81 1
            $this->handleDomError();
82
        }
83
84 1
        throw $this->createRuntimeException('Failed to extract error message because Content-Type of error response is unexpected.');
85
    }
86
87
    /**
88
     * @throws RequestException
89
     * @throws RuntimeException
90
     */
91 1
    private function handleDomError(): void
92
    {
93 1
        $body = $this->getResponseBody();
94 1
        $dom = new \DOMDocument('1.0', 'UTF-8');
95 1
        $dom->preserveWhiteSpace = false;
96 1
        $dom->formatOutput = true;
97 1
        if ($dom->loadHTML($body)) {
98 1
            $title = $dom->getElementsByTagName('title');
99 1
            if (is_object($title)) {
100 1
                $title = $title->item(0)->nodeValue;
101
            }
102 1
            if ($title === 'Error') {
103 1
                $message = $dom->getElementsByTagName('h3')->item(0)->nodeValue;
104 1
                if (is_null($message)) {
0 ignored issues
show
introduced by
The condition is_null($message) is always false.
Loading history...
105 1
                    throw $this->createRuntimeException('Failed to extract error message from DOM response.');
106
                }
107 1
                throw $this->createException($message);
108
            }
109 1
            if ($title === 'Unauthorized') {
110 1
                $message = $dom->getElementsByTagName('h2')->item(0)->nodeValue;
111 1
                if (is_null($message)) {
0 ignored issues
show
introduced by
The condition is_null($message) is always false.
Loading history...
112 1
                    throw $this->createRuntimeException('Failed to extract error message from DOM response.');
113
                }
114 1
                throw $this->createException($message);
115
            }
116
117 1
            throw $this->createException('Invalid auth.');
118
        }
119
120
        throw new \InvalidArgumentException('Body is not DOM.');
121
    }
122
123
    /**
124
     * @throws RequestException
125
     * @throws RuntimeException
126
     */
127 4
    private function handleJsonError(): void
128
    {
129 4
        $body = $this->getResponseBody();
130 4
        $json = \GuzzleHttp\json_decode($body, true);
0 ignored issues
show
Deprecated Code introduced by
The function GuzzleHttp\json_decode() has been deprecated: json_decode will be removed in guzzlehttp/guzzle:8.0. Use Utils::jsonDecode instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

130
        $json = /** @scrutinizer ignore-deprecated */ \GuzzleHttp\json_decode($body, true);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
131
132 4
        $message = $json['message'];
133 4
        if (isset($json['errors']) && is_array($json['errors'])) {
134 1
            $message .= $this->addErrorMessages($json['errors']);
135
        }
136 4
        if (is_null($message) && isset($json['reason'])) {
137 1
            $message = $json['reason'];
138
        }
139
140 4
        if (is_null($message)) {
141 1
            throw $this->createRuntimeException('Failed to extract error message from JSON response.');
142
        }
143 4
        throw $this->createException($message);
144
    }
145
146
    /**
147
     * @param array $errors
148
     * @return string
149
     */
150 1
    private function addErrorMessages(array $errors): string
151
    {
152 1
        $message = ' (';
153 1
        foreach ($errors as $k => $err) {
154 1
            $message .= $k . ' : ';
155 1
            if (is_array($err['messages'])) {
156 1
                foreach ($err['messages'] as $m) {
157 1
                    $message .= $m . ' ';
158
                }
159
            } else {
160 1
                $message .= $err['messages'];
161
            }
162
        }
163 1
        $message .= ')';
164
165 1
        return $message;
166
    }
167
168
    /**
169
     * In stream mode, contents can be obtained only once, so this method makes it reusable.
170
     * @return string
171
     */
172 4
    private function getResponseBody(): string
173
    {
174 4
        if (is_null($this->responseBody)) {
175 4
            $this->responseBody = $this->response->getBody()->getContents();
176
        }
177
178 4
        return $this->responseBody;
179
    }
180
181
    /**
182
     * @param string $message
183
     * @return RequestException
184
     */
185 4
    private function createException(string $message): RequestException
186
    {
187 4
        $level = (int) floor($this->response->getStatusCode() / 100);
188 4
        $className = RequestException::class;
189
190 4
        if ($level === 4) {
191 2
            $className = ClientException::class;
192 3
        } elseif ($level === 5) {
193 3
            $className = ServerException::class;
194
        }
195
196 4
        return new $className($message, $this->request, $this->response);
197
    }
198
199
    /**
200
     * @param string $message
201
     * @return RuntimeException
202
     */
203 1
    private function createRuntimeException(string $message): RuntimeException
204
    {
205 1
        return new RuntimeException(
206 1
            $message,
207 1
            0,
208 1
            $this->previousThrowable,
209 1
            ['responseBody' => $this->getResponseBody()]
210 1
        );
211
    }
212
}
213