Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.71% covered (warning)
85.71%
66 / 77
71.43% covered (warning)
71.43%
20 / 28
CRAP
0.00% covered (danger)
0.00%
0 / 4
HttpHeader
98.15% covered (success)
98.15%
53 / 54
95.00% covered (success)
95.00%
19 / 20
31
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
 throwIfInvalidHeaderName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setValue
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setValues
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 existsHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 existsContentType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContentType
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setContentType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeaderNames
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getValues
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 clearHeader
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 existsRedirect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRedirect
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 clearRedirect
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getHeaders
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getRedirect
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 createClientRequestHeader
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClientResponseHeader
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRequestHeader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
LocalHttpClientRequestHeader
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 4
20
0.00% covered (danger)
0.00%
0 / 1
 existsRedirect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setRedirect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clearRedirect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRedirect
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
LocalClientResponseHttpHeader
92.86% covered (success)
92.86%
13 / 14
50.00% covered (danger)
50.00%
1 / 2
8.02
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 throwIfInvalidHeaderName
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
LocalHttpRequestHeader
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 2
12
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 throwIfInvalidHeaderName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Http;
6
7use PeServer\Core\Binary;
8use PeServer\Core\Collection\Arr;
9use PeServer\Core\Collection\CaseInsensitiveKeyArray;
10use PeServer\Core\Encoding;
11use PeServer\Core\Http\ContentType;
12use PeServer\Core\Http\HttpStatus;
13use PeServer\Core\Http\RedirectSetting;
14use PeServer\Core\Text;
15use PeServer\Core\Throws\ArgumentException;
16use PeServer\Core\Throws\InvalidOperationException;
17use PeServer\Core\Throws\KeyNotFoundException;
18use PeServer\Core\Throws\NotSupportedException;
19use PeServer\Core\Web\Url;
20
21/**
22 * HTTPヘッダー。
23 */
24class HttpHeader
25{
26    #region variable
27
28    /**
29     * ヘッダ一覧。
30     *
31     * リダイレクト(Location)とは共存しない。
32     *
33     * @var CaseInsensitiveKeyArray
34     * @phpstan-var CaseInsensitiveKeyArray<non-empty-string,string[]>
35     */
36    private CaseInsensitiveKeyArray $headers;
37
38    /**
39     * リダイレクト設定。
40     *
41     * @var RedirectSetting|null
42     */
43    private ?RedirectSetting $redirect = null;
44
45    #endregion
46
47    public function __construct()
48    {
49        $this->headers = new CaseInsensitiveKeyArray();
50    }
51
52    #region function
53
54    /**
55     * HTTPヘッダ名が不正であれば例外を投げる。
56     *
57     * @param string $name ヘッダ名
58     * @throws ArgumentException HTTPヘッダ名不正。
59     * @phpstan-assert non-empty-string $name
60     */
61    protected function throwIfInvalidHeaderName(string $name): void
62    {
63        if (Text::isNullOrWhiteSpace($name)) {
64            throw new ArgumentException('$name');
65        }
66        if (Text::toLower($name) === 'location') {
67            throw new ArgumentException('$name: setRedirect()');
68        }
69    }
70
71    /**
72     * HTTPヘッダ設定
73     *
74     * @param non-empty-string $name ヘッダ名。
75     * @param string $value 値。
76     * @throws ArgumentException ヘッダ名不正。
77     */
78    public function setValue(string $name, string $value): void
79    {
80        $this->throwIfInvalidHeaderName($name); //@phpstan-ignore-line [DOCTYPE]
81
82        $this->headers[$name] = [$value];
83    }
84
85    /**
86     * ヘッダに値一覧を設定。
87     *
88     * @param non-empty-string $name ヘッダ名。
89     * @param string[] $values 値一覧。
90     * @throws ArgumentException ヘッダ名不正。
91     */
92    public function setValues(string $name, array $values): void
93    {
94        $this->throwIfInvalidHeaderName($name); //@phpstan-ignore-line [DOCTYPE]
95
96        $this->headers[$name] = $values;
97    }
98
99    /**
100     * ヘッダに値を追加。
101     *
102     * @param non-empty-string $name ヘッダ名。
103     * @param string $value 値。
104     * @throws ArgumentException HTTPヘッダ名不正。
105     */
106    public function addValue(string $name, string $value): void
107    {
108        $this->throwIfInvalidHeaderName($name); //@phpstan-ignore-line [DOCTYPE]
109
110        if (isset($this->headers[$name])) {
111            $array = $this->headers[$name];
112            $array[] = $value;
113            $this->headers[$name] = $array;
114            return;
115        }
116
117        $this->headers[$name] = [$value];
118    }
119
120    /**
121     * ヘッダ名が存在するか。
122     *
123     * @param non-empty-string $name ヘッダ名。
124     */
125    public function existsHeader(string $name): bool
126    {
127        return isset($this->headers[$name]);
128    }
129
130    public function existsContentType(): bool
131    {
132        return $this->existsHeader(ContentType::NAME);
133    }
134
135    public function getContentType(): ContentType
136    {
137        $contentType = $this->getValues(ContentType::NAME);
138        return ContentType::from($contentType[0]);
139    }
140
141    public function setContentType(ContentType $value): void
142    {
143        $this->setValues(ContentType::NAME, $value->toValues());
144    }
145
146    /**
147     * ヘッダ名一覧を取得。
148     *
149     * @return non-empty-string[]
150     */
151    public function getHeaderNames(): array
152    {
153        $result = [];
154
155        foreach ($this->headers as $name => $_) {
156            $result[] = $name;
157        }
158
159        return $result;
160    }
161
162    /**
163     * ヘッダの値を取得。
164     *
165     * @param non-empty-string $name ヘッダ名。
166     * @return string[] 値一覧。
167     * @throws KeyNotFoundException
168     */
169    public function getValues(string $name): array
170    {
171        if (isset($this->headers[$name])) {
172            return $this->headers[$name];
173        }
174
175        throw new KeyNotFoundException();
176    }
177
178    /**
179     * ヘッダの削除。
180     *
181     * @param non-empty-string $name ヘッダ名。
182     * @return bool 削除できたか。
183     */
184    public function clearHeader(string $name): bool
185    {
186        $this->throwIfInvalidHeaderName($name); //@phpstan-ignore-line [DOCTYPE]
187
188        if (!isset($this->headers[$name])) {
189            return false;
190        }
191
192        unset($this->headers[$name]);
193
194        return true;
195    }
196
197    /**
198     * リダイレクト設定は存在するか。
199     * @phpstan-assert-if-true RedirectSetting $this->redirect
200     */
201    public function existsRedirect(): bool
202    {
203        return $this->redirect !== null;
204    }
205
206    /**
207     * リダイレクト設定を割り当て。
208     *
209     * @param Url $url
210     * @param HttpStatus|null $status
211     * @return void
212     */
213    public function setRedirect(Url $url, ?HttpStatus $status): void
214    {
215        if ($status === null) {
216            $this->redirect = new RedirectSetting($url, HttpStatus::MovedPermanently);
217        } else {
218            $this->redirect = new RedirectSetting($url, $status);
219        }
220    }
221
222    /**
223     * リダイレクト設定を破棄。
224     *
225     * @return boolean 破棄したか。
226     */
227    public function clearRedirect(): bool
228    {
229        if ($this->redirect === null) {
230            return false;
231        }
232        $this->redirect = null;
233
234        return true;
235    }
236
237    /**
238     * 現在のヘッダ一覧を取得。
239     *
240     * 同一名ヘッダは , でまとめられる。
241     *
242     * @return array<string,string>
243     */
244    public function getHeaders(): array
245    {
246        /** @var array<string,string> */
247        $joinHeaders = [];
248
249        foreach ($this->headers as $name => $values) {
250            $value = Text::join(', ', $values);
251            if (0 < Text::getLength($value)) {
252                $joinHeaders[$name] = $value;
253            }
254        }
255
256        return $joinHeaders;
257    }
258
259    /**
260     * リダイレクト情報を取得。
261     *
262     * @return RedirectSetting
263     * @throws InvalidOperationException リダイレクト設定が未割り当て。
264     */
265    public function getRedirect(): RedirectSetting
266    {
267        if (!$this->existsRedirect()) {
268            throw new InvalidOperationException();
269        }
270
271        return $this->redirect;
272    }
273
274    /**
275     * HTTPクライアントリクエストヘッダの生成。
276     *
277     * @return HttpHeader
278     */
279    public static function createClientRequestHeader(): HttpHeader
280    {
281        return new LocalHttpClientRequestHeader();
282    }
283
284    public static function getClientResponseHeader(Binary $responseHeader, ?Encoding $encoding = null): HttpHeader
285    {
286        $encoding ??= Encoding::getDefaultEncoding();
287        return new LocalClientResponseHttpHeader($responseHeader, $encoding);
288    }
289
290    /**
291     * リクエストヘッダの取得。
292     *
293     * @return HttpHeader
294     */
295    public static function getRequestHeader(): HttpHeader
296    {
297        return new LocalHttpRequestHeader();
298    }
299
300    #endregion
301}
302
303/**
304 * 要求時のヘッダー一覧。
305 */
306//phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses
307class LocalHttpClientRequestHeader extends HttpHeader
308{
309    public function existsRedirect(): bool
310    {
311        throw new NotSupportedException();
312    }
313
314    public function setRedirect(Url $url, ?HttpStatus $status): void
315    {
316        throw new NotSupportedException();
317    }
318
319    public function clearRedirect(): bool
320    {
321        throw new NotSupportedException();
322    }
323
324    public function getRedirect(): RedirectSetting
325    {
326        throw new NotSupportedException();
327    }
328}
329
330
331//phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses
332class LocalClientResponseHttpHeader extends LocalHttpClientRequestHeader
333{
334    private bool $initialized = false;
335
336    public function __construct(Binary $responseHeader, Encoding $encoding)
337    {
338        parent::__construct();
339
340        $headerString = $encoding->toString($responseHeader);
341        $headers = Text::splitLines($headerString);
342        foreach ($headers as $index => $header) {
343            if ($index === 0) {
344                // HTTP仕様の最初のあれなんでいらない
345                continue;
346            }
347            if ($header === '') {
348                // 改行分割後に何もないのであればHTTPの仕様に従って本文開始になるのでもう何もしない
349                break;
350            }
351
352            $kv = Text::split($header, ':', 2);
353            $name = Text::trim($kv[0]);
354            if (!Text::isNullOrWhiteSpace($name)) {
355                $this->setValue($name, isset($kv[1]) ? Text::trim($kv[1]) : Text::EMPTY);
356            }
357        }
358    }
359
360    protected function throwIfInvalidHeaderName(string $name): void
361    {
362        if ($this->initialized) {
363            throw new NotSupportedException();
364        }
365    }
366}
367
368//phpcs:ignore PSR1.Classes.ClassDeclaration.MultipleClasses
369class LocalHttpRequestHeader extends LocalHttpClientRequestHeader
370{
371    public function __construct()
372    {
373        parent::__construct();
374
375        $headers = getallheaders();
376        foreach ($headers as $name => $value) {
377            //@phpstan-ignore-next-line non-empty
378            $this->setValue($name, $value);
379        }
380    }
381
382    protected function throwIfInvalidHeaderName(string $name): void
383    {
384        //NOP
385    }
386}