ResponseService::isHtmlResponse()   A
last analyzed

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
            $message = match ($title) {
103 1
                'Error' => $dom->getElementsByTagName('h3')->item(0)->nodeValue,
104 1
                'Unauthorized' => $dom->getElementsByTagName('h2')->item(0)->nodeValue,
105 1
                default => 'Invalid auth.',
106 1
            };
107 1
            if (is_null($message)) {
108 1
                throw $this->createRuntimeException('Failed to extract error message from DOM response.');
109
            }
110 1
            throw $this->createException($message);
111
        }
112
113
        throw $this->createRuntimeException('Failed to parse DOM response.');
114
    }
115
116
    /**
117
     * @throws RequestException
118
     * @throws RuntimeException
119
     */
120 4
    private function handleJsonError(): void
121
    {
122 4
        $body = $this->getResponseBody();
123
        try {
124 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

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