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); |
|
|
|
|
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
|
|
|
|
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.