Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.98% covered (success)
92.98%
106 / 114
90.00% covered (success)
90.00%
9 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
HttpClient
92.98% covered (success)
92.98%
106 / 114
90.00% covered (success)
90.00%
9 / 10
36.45
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 request
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createRequest
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 sendCore
90.24% covered (success)
90.24%
74 / 82
0.00% covered (danger)
0.00%
0 / 1
25.58
 send
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 post
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 put
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 patch
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Http\Client;
6
7use Exception;
8use PeServer\Core\Binary;
9use PeServer\Core\Collection\Arr;
10use PeServer\Core\Collection\Collections;
11use PeServer\Core\DisposerBase;
12use PeServer\Core\Http\Client\HttpClientOptions;
13use PeServer\Core\Http\Client\HttpClientRequest;
14use PeServer\Core\Http\Client\HttpClientResponse;
15use PeServer\Core\Http\HttpHeader;
16use PeServer\Core\Http\HttpMethod;
17use PeServer\Core\Text;
18use PeServer\Core\Throws\HttpClientRequestException;
19use PeServer\Core\Throws\HttpClientStatusException;
20use PeServer\Core\Throws\NotImplementedException;
21use PeServer\Core\Web\Url;
22
23/**
24 * HTTPへ旅立つのだ。
25 */
26class HttpClient extends DisposerBase
27{
28    public function __construct(
29        private HttpClientOptions $options
30    ) {
31    }
32
33    #region function
34
35    public static function request(HttpClientRequest $request, HttpClientOptions $options): HttpClientResponse
36    {
37        $client = new self($options);
38        return $client->send($request);
39    }
40
41    /**
42     * リクエストを内部用に変換。
43     *
44     * @param HttpClientRequest $raw
45     * @return HttpClientRequest
46     */
47    protected function createRequest(HttpClientRequest $raw): HttpClientRequest
48    {
49        // HTTPヘッダをオプションから構築してユーザー設定で上書き
50        $header = HttpHeader::createClientRequestHeader();
51        $header->setValue('User-Agent', $this->options->userAgent);
52
53        if ($raw->header !== null) {
54            foreach ($raw->header->getHeaderNames() as $name) {
55                $overwriteValues = $raw->header->getValues($name);
56                $header->setValues($name, $overwriteValues);
57            }
58        }
59
60        return new HttpClientRequest(
61            $raw->url,
62            $raw->method,
63            $header,
64            $raw->content
65        );
66    }
67
68    protected function sendCore(HttpClientRequest $request): HttpClientResponse
69    {
70        $curlHandle = curl_init();
71
72        /** @var array<int,mixed> */
73        $curlOptions = [];
74
75        //--------------------------------------------------------------
76        // 共通
77        $curlOptions[CURLOPT_URL] = $request->url->toString($this->options->urlEncoding); //cspell:disable-line
78        $curlOptions[CURLOPT_RETURNTRANSFER] = true; //cspell:disable-line
79        $curlOptions[CURLOPT_HEADER] = true; //cspell:disable-line
80        $curlOptions[CURLOPT_CRLF] = false; //cspell:disable-line
81
82        //--------------------------------------------------------------
83        // リクエスト設定
84        // メソッド
85        /** @var HttpHeader|null */
86        $overwriteHeader = null;
87        //cspell:disable-next-line
88        $curlOptions[CURLOPT_CUSTOMREQUEST] = $request->method->value;
89        switch ($request->method) {
90            case HttpMethod::Get:
91            case HttpMethod::Head:
92                break;
93
94            case HttpMethod::Post:
95            case HttpMethod::Put:
96            case HttpMethod::Delete:
97            case HttpMethod::Connect:
98            case HttpMethod::Options:
99            case HttpMethod::Trace:
100            case HttpMethod::Patch:
101                if ($request->content) {
102                    $overwriteHeader = HttpHeader::createClientRequestHeader();
103                    $customHeader = $request->content->toHeader();
104                    foreach ($customHeader->getHeaderNames() as $name) {
105                        $overwriteHeader->setValues($name, $customHeader->getValues($name));
106                    }
107                    //cspell:disable-next-line
108                    $curlOptions[CURLOPT_POSTFIELDS] = $request->content->toBody()->raw;
109                } else {
110                    //cspell:disable-next-line
111                    $curlOptions[CURLOPT_POSTFIELDS] = Text::EMPTY;
112                }
113                break;
114
115            default:
116                throw new NotImplementedException();
117        }
118
119        // ヘッダ
120        $headers = [];
121        if ($request->header) {
122            $headers = $request->header->getHeaders();
123
124            if ($overwriteHeader) {
125                $headers = Arr::replace($headers, $overwriteHeader->getHeaders());
126            }
127        } elseif ($overwriteHeader) {
128            // 上で使ってる $request->header は差し替えられたものなので何かしらオブジェクトが入っている
129            // なのでこっちに来ることはないと思うよ
130            $headers = $overwriteHeader->getHeaders();
131        }
132        if (0 < Arr::getCount($headers)) {
133            $headerList = [];
134            foreach ($headers as $name => $values) {
135                $headerList[] = "$name$values";
136            }
137            //cspell:disable-next-line
138            $curlOptions[CURLOPT_HTTPHEADER] = $headerList;
139        }
140
141        //--------------------------------------------------------------
142        // オプション設定
143        // リダイレクト
144        $curlOptions[CURLOPT_FOLLOWLOCATION] = $this->options->redirect->isEnabled; //cspell:disable-line
145        $curlOptions[CURLOPT_MAXREDIRS] = $this->options->redirect->count; //cspell:disable-line
146        $curlOptions[CURLOPT_AUTOREFERER] = $this->options->redirect->autoReferer; //cspell:disable-line
147
148        // セキュリティ
149        $curlOptions[CURLOPT_DISALLOW_USERNAME_IN_URL] = !$this->options->security->urlAllowAuthentication; //cspell:disable-line
150        $curlOptions[CURLOPT_SSL_VERIFYPEER] = $this->options->security->sslVerifyPeer; //cspell:disable-line
151        $curlOptions[CURLOPT_SSL_VERIFYHOST] = $this->options->security->sslVerifyHost ? 2 : 0; //cspell:disable-line
152
153        // プロキシ
154        if ($this->options->proxy) {
155            $curlOptions[CURLOPT_HTTPPROXYTUNNEL] = true; //cspell:disable-line
156            $curlOptions[CURLOPT_PROXYPORT] = $this->options->proxy->port; //cspell:disable-line
157            $curlOptions[CURLOPT_PROXY] = $this->options->proxy->host; //cspell:disable-line
158            if (!Text::isNullOrEmpty($this->options->proxy->userName) || !Text::isNullOrEmpty($this->options->proxy->password)) {
159                $curlOptions[CURLOPT_PROXYUSERPWD] = $this->options->proxy->userName . ':' . $this->options->proxy->password;
160            }
161        }
162
163        curl_setopt_array($curlHandle, $curlOptions);
164
165        /** @var string|false */
166        $response = curl_exec($curlHandle);
167        $clientStatus = HttpClientStatus::create($curlHandle);
168        $information = HttpClientInformation::create($this->options->urlEncoding, $request, $curlHandle);
169        $aaa = $information->getEffectiveUrl();
170
171        /** @var HttpHeader|null */
172        $responseHeader = null;
173        /** @var Binary|null */
174        $responseContent = null;
175
176        if ($response === false) {
177            $responseContent = new Binary(Text::EMPTY);
178            $responseHeader = HttpHeader::getClientResponseHeader(new Binary(Text::EMPTY));
179        } else {
180            $rawResponse = new Binary($response);
181
182            $headerSize = $information->getHeaderSize();
183            $rawHeader = $rawResponse->getRange(0, $headerSize);
184            $responseHeader = HttpHeader::getClientResponseHeader($rawHeader);
185            $responseContent = $rawResponse->getRange($headerSize);
186        }
187
188        $result = new HttpClientResponse(
189            $this->options,
190            $request,
191            $curlHandle,
192            $responseHeader,
193            $responseContent,
194            $information,
195            $clientStatus
196        );
197
198
199        if ($result->clientStatus->isError()) {
200            throw new HttpClientStatusException($result);
201        }
202
203        $httpStatus = $result->information->getHttpStatus();
204        if ($httpStatus->isError()) {
205            throw new HttpClientRequestException($result);
206        }
207
208        return $result;
209    }
210
211    public function send(HttpClientRequest $request): HttpClientResponse
212    {
213        $request = $this->createRequest($request);
214        return $this->sendCore($request);
215    }
216
217    final public function get(Url $url, ?HttpHeader $header = null): HttpClientResponse
218    {
219        $requestData = new HttpClientRequest($url, HttpMethod::Get, $header, null);
220        $request = $this->createRequest($requestData);
221        return $this->sendCore($request);
222    }
223
224    final public function post(Url $url, ?HttpHeader $header = null, ?HttpClientContentBase $content = null): HttpClientResponse
225    {
226        $requestData = new HttpClientRequest($url, HttpMethod::Post, $header, $content);
227        $request = $this->createRequest($requestData);
228        return $this->sendCore($request);
229    }
230
231    final public function put(Url $url, ?HttpHeader $header = null, ?HttpClientContentBase $content = null): HttpClientResponse
232    {
233        $requestData = new HttpClientRequest($url, HttpMethod::Put, $header, $content);
234        $request = $this->createRequest($requestData);
235        return $this->sendCore($request);
236    }
237
238    final public function patch(Url $url, ?HttpHeader $header = null, ?HttpClientContentBase $content = null): HttpClientResponse
239    {
240        $requestData = new HttpClientRequest($url, HttpMethod::Patch, $header, $content);
241        $request = $this->createRequest($requestData);
242        return $this->sendCore($request);
243    }
244
245    final public function delete(Url $url, ?HttpHeader $header = null, ?HttpClientContentBase $content = null): HttpClientResponse
246    {
247        $requestData = new HttpClientRequest($url, HttpMethod::Delete, $header, $content);
248        $request = $this->createRequest($requestData);
249        return $this->sendCore($request);
250    }
251
252    #endregion
253}