1: <?php declare(strict_types=1);
2:
3: namespace Salient\Http;
4:
5: use Psr\Http\Message\MessageInterface as PsrMessageInterface;
6: use Salient\Collection\ArrayableCollectionTrait;
7: use Salient\Collection\ReadOnlyArrayAccessTrait;
8: use Salient\Collection\ReadOnlyCollectionTrait;
9: use Salient\Contract\Collection\CollectionInterface;
10: use Salient\Contract\Core\Arrayable;
11: use Salient\Contract\Http\Message\MessageInterface;
12: use Salient\Contract\Http\CredentialInterface;
13: use Salient\Contract\Http\HeadersInterface;
14: use Salient\Core\Concern\ImmutableTrait;
15: use Salient\Http\Exception\InvalidHeaderException;
16: use Salient\Utility\Arr;
17: use Salient\Utility\Regex;
18: use Salient\Utility\Str;
19: use InvalidArgumentException;
20: use IteratorAggregate;
21: use LogicException;
22:
23: /**
24: * @api
25: *
26: * @implements IteratorAggregate<string,string[]>
27: */
28: class Headers implements HeadersInterface, IteratorAggregate, HasHttpRegex
29: {
30: /** @use ReadOnlyCollectionTrait<string,string[]> */
31: use ReadOnlyCollectionTrait;
32: /** @use ReadOnlyArrayAccessTrait<string,string[]> */
33: use ReadOnlyArrayAccessTrait;
34: /** @use ArrayableCollectionTrait<string,string[]> */
35: use ArrayableCollectionTrait;
36: use ImmutableTrait;
37:
38: /**
39: * [ key => [ name, value ], ... ]
40: *
41: * @var array<int,array{string,string}>
42: */
43: private array $Headers;
44:
45: /**
46: * [ lowercase name => [ key, ... ], ... ]
47: *
48: * @var array<string,int[]>
49: */
50: private array $Index;
51:
52: /**
53: * [ key => true, ... ]
54: *
55: * @var array<int,true>
56: */
57: private array $TrailerIndex = [];
58:
59: private bool $IsParser;
60: private bool $HasEmptyLine = false;
61: private bool $HasBadWhitespace = false;
62: private bool $HasObsoleteLineFolding = false;
63:
64: /**
65: * Trailing whitespace carried over from the previous line
66: *
67: * Inserted before `obs-fold` when a header extends over multiple lines.
68: */
69: private ?string $Carry = null;
70:
71: /**
72: * @api
73: */
74: public function __construct($items = [])
75: {
76: $items = $this->getItemsArray($items, $headers, $index);
77: $this->Headers = $headers;
78: $this->Index = $this->filterIndex($index);
79: $this->Items = $this->filterIndex($items);
80: $this->IsParser = !func_num_args();
81: }
82:
83: /**
84: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string>|PsrMessageInterface|string $headersOrPayload
85: * @return static
86: */
87: public static function from($headersOrPayload): self
88: {
89: if ($headersOrPayload instanceof static) {
90: return $headersOrPayload;
91: }
92: if ($headersOrPayload instanceof MessageInterface) {
93: return self::from($headersOrPayload->getInnerHeaders());
94: }
95: if ($headersOrPayload instanceof PsrMessageInterface) {
96: return new static($headersOrPayload->getHeaders());
97: }
98: if (is_string($headersOrPayload)) {
99: // Normalise line endings
100: $headersOrPayload = Str::setEol($headersOrPayload, "\r\n");
101: // Extract headers, split on CRLF and remove start line
102: $lines = Arr::shift(explode(
103: "\r\n",
104: explode("\r\n\r\n", $headersOrPayload, 2)[0] . "\r\n",
105: ));
106: // Parse field lines
107: $instance = new static();
108: foreach ($lines as $line) {
109: $instance = $instance->addLine("$line\r\n");
110: }
111: return $instance;
112: }
113: return new static($headersOrPayload);
114: }
115:
116: /**
117: * @inheritDoc
118: *
119: * @param bool $strict If `true`, strict \[RFC9112] compliance is enforced.
120: */
121: public function addLine(string $line, bool $strict = false)
122: {
123: if (!$this->IsParser) {
124: throw new LogicException(sprintf(
125: '%s::%s() cannot be used after headers are applied via another method',
126: static::class,
127: __FUNCTION__,
128: ));
129: }
130:
131: $instance = $this->doAddLine($line, $strict);
132: $instance->IsParser = true;
133: return $instance;
134: }
135:
136: /**
137: * @return static
138: */
139: private function doAddLine(string $line, bool $strict)
140: {
141: if ($strict && substr($line, -2) !== "\r\n") {
142: throw new InvalidHeaderException(
143: 'HTTP field line must end with CRLF',
144: );
145: }
146:
147: if ($line === "\r\n" || (!$strict && trim($line) === '')) {
148: if ($strict && $this->HasEmptyLine) {
149: throw new InvalidHeaderException(
150: 'HTTP message cannot have empty field line after body',
151: );
152: }
153: return $this->with('Carry', null)->with('HasEmptyLine', true);
154: }
155:
156: if ($strict) {
157: $line = substr($line, 0, -2);
158: if (!Regex::match(self::HTTP_FIELD_LINE_REGEX, $line, $matches, \PREG_UNMATCHED_AS_NULL)) {
159: throw new InvalidHeaderException(
160: sprintf('Invalid HTTP field line: %s', $line),
161: );
162: }
163:
164: // As per [RFC9112] Section 5.2 ("Obsolete Line Folding"), "replace
165: // each received obs-fold with one or more SP octets"
166: $instance = $this->with('Carry', (string) $matches['carry']);
167: if ($matches['extended'] !== null) {
168: if ($this->Carry === null) {
169: throw new InvalidHeaderException(
170: sprintf('Invalid HTTP field line folding: %s', $line),
171: );
172: }
173: $line = $this->Carry . ' ' . $matches['extended'];
174: return $instance->extendPreviousLine($line);
175: }
176:
177: // [RFC9112] Section 5.1 ("Field Line Parsing"):
178: // - "No whitespace is allowed between the field name and colon"
179: // - "A server MUST reject ... any received request message that
180: // contains whitespace between a header field name and colon"
181: // - "A proxy MUST remove any such whitespace from a response
182: // message before forwarding the message downstream"
183: if ($matches['bad_whitespace'] !== null) {
184: $instance = $instance->with('HasBadWhitespace', true);
185: }
186:
187: /** @var string */
188: $name = $matches['name'];
189: /** @var string */
190: $value = $matches['value'];
191: return $instance->addValue($name, $value)->maybeIndexLastHeader();
192: }
193:
194: $line = rtrim($line, "\r\n");
195: $carry = Regex::match('/\h++$/D', $line, $matches)
196: ? $matches[0]
197: : '';
198: $instance = $this->with('Carry', $carry);
199: if (strpos(" \t", $line[0]) !== false) {
200: if ($this->Carry === null) {
201: throw new InvalidHeaderException(
202: sprintf('Invalid HTTP field line folding: %s', $line),
203: );
204: }
205: $line = $this->Carry . ' ' . trim($line);
206: return $instance->extendPreviousLine($line);
207: }
208:
209: $split = explode(':', $line, 2);
210: if (count($split) !== 2) {
211: throw new InvalidHeaderException(
212: sprintf('Invalid HTTP field line: %s', $line),
213: );
214: }
215:
216: $name = rtrim($split[0]);
217: $value = trim($split[1]);
218: if ($name !== $split[0]) {
219: $instance = $instance->with('HasBadWhitespace', true);
220: }
221: return $instance->addValue($name, $value)->maybeIndexLastHeader();
222: }
223:
224: /**
225: * @return static
226: */
227: private function extendPreviousLine(string $line)
228: {
229: /** @var non-empty-array<int,array{string,string}> */
230: $headers = $this->Headers;
231: $k = array_key_last($headers);
232: [, $value] = $this->Headers[$k];
233: $headers[$k][1] = ltrim($value . $line);
234: return $this
235: ->with('HasObsoleteLineFolding', true)
236: ->maybeReplaceHeaders($headers, $this->Index);
237: }
238:
239: /**
240: * @return static
241: */
242: private function maybeIndexLastHeader()
243: {
244: if (!$this->HasEmptyLine) {
245: return $this;
246: }
247: /** @var int */
248: $k = array_key_last($this->Headers);
249: $index = $this->TrailerIndex;
250: $index[$k] = true;
251: return $this->with('TrailerIndex', $index);
252: }
253:
254: /**
255: * @inheritDoc
256: */
257: public function hasEmptyLine(): bool
258: {
259: return $this->HasEmptyLine;
260: }
261:
262: /**
263: * @inheritDoc
264: */
265: public function hasBadWhitespace(): bool
266: {
267: return $this->HasBadWhitespace;
268: }
269:
270: /**
271: * @inheritDoc
272: */
273: public function hasObsoleteLineFolding(): bool
274: {
275: return $this->HasObsoleteLineFolding;
276: }
277:
278: /**
279: * @inheritDoc
280: */
281: public function addValue($key, $value)
282: {
283: $values = (array) $value;
284: if (!$values) {
285: throw new InvalidArgumentException(
286: sprintf('No values given for header: %s', $key),
287: );
288: }
289: $lower = Str::lower($key);
290: $headers = $this->Headers;
291: $index = $this->Index;
292: $key = $this->filterName($key);
293: foreach ($values as $value) {
294: $headers[] = [$key, $this->filterValue($value)];
295: $index[$lower][] = array_key_last($headers);
296: }
297: return $this->replaceHeaders($headers, $index);
298: }
299:
300: /**
301: * @inheritDoc
302: */
303: public function set($key, $value)
304: {
305: $values = (array) $value;
306: if (!$values) {
307: throw new InvalidArgumentException(
308: sprintf('No values given for header: %s', $key),
309: );
310: }
311: $lower = Str::lower($key);
312: if (($this->Items[$lower] ?? []) === $values) {
313: // Return `$this` if existing values are being reapplied
314: return $this;
315: }
316: $headers = $this->Headers;
317: $index = $this->Index;
318: if (isset($index[$lower])) {
319: foreach ($index[$lower] as $k) {
320: unset($headers[$k]);
321: }
322: unset($index[$lower]);
323: }
324: $key = $this->filterName($key);
325: foreach ($values as $value) {
326: $headers[] = [$key, $this->filterValue($value)];
327: $index[$lower][] = array_key_last($headers);
328: }
329: return $this->replaceHeaders($headers, $index);
330: }
331:
332: /**
333: * @inheritDoc
334: */
335: public function unset($key)
336: {
337: $lower = Str::lower($key);
338: if (!isset($this->Items[$lower])) {
339: return $this;
340: }
341: $headers = $this->Headers;
342: $index = $this->Index;
343: foreach ($index[$lower] as $k) {
344: unset($headers[$k]);
345: }
346: unset($index[$lower]);
347: return $this->replaceHeaders($headers, $index);
348: }
349:
350: /**
351: * @inheritDoc
352: */
353: public function merge($items, bool $preserveValues = false)
354: {
355: $headers = $this->Headers;
356: $index = $this->Index;
357: $changed = false;
358: foreach ($this->getItems($items) as $key => $values) {
359: $lower = Str::lower($key);
360: if (
361: !$preserveValues
362: // The same header may appear in `$items` multiple times, so
363: // remove existing values once per pre-existing header only
364: && isset($this->Items[$lower])
365: && !isset($unset[$lower])
366: ) {
367: $unset[$lower] = true;
368: foreach ($index[$lower] as $k) {
369: unset($headers[$k]);
370: }
371: // Maintain `$index` order for detection of reapplied values
372: $index[$lower] = [];
373: }
374: foreach ($values as $value) {
375: $headers[] = [$key, $value];
376: $index[$lower][] = array_key_last($headers);
377: }
378: $changed = true;
379: }
380:
381: return !$changed
382: || $this->getIndexValues($headers, $index) === $this->doGetHeaders()
383: ? $this
384: : $this->replaceHeaders($headers, $index);
385: }
386:
387: /**
388: * @param array<int,array{string,string}> $headers
389: * @param array<string,int[]> $index
390: * @return array<string,string[]>
391: */
392: private function getIndexValues(array $headers, array $index): array
393: {
394: foreach ($index as $headerKeys) {
395: $key = null;
396: foreach ($headerKeys as $k) {
397: $header = $headers[$k];
398: // Preserve the case of the first appearance of each header
399: $key ??= $header[0];
400: $values[$key][] = $header[1];
401: }
402: }
403: return $values ?? [];
404: }
405:
406: /**
407: * @inheritDoc
408: */
409: public function sort()
410: {
411: $headers = Arr::sort($this->Headers, true, fn($a, $b) => $a[0] <=> $b[0]);
412: $index = Arr::sortByKey($this->Index);
413: return $this->maybeReplaceHeaders($headers, $index, true);
414: }
415:
416: /**
417: * @inheritDoc
418: */
419: public function reverse()
420: {
421: $headers = array_reverse($this->Headers, true);
422: $index = array_reverse($this->Index, true);
423: return $this->maybeReplaceHeaders($headers, $index, true);
424: }
425:
426: /**
427: * @inheritDoc
428: */
429: public function map(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE)
430: {
431: $prev = null;
432: $item = null;
433: $key = null;
434: $items = [];
435:
436: foreach ($this->Index as $nextKey => $headerKeys) {
437: $nextName = null;
438: $nextValue = [];
439: foreach ($headerKeys as $k) {
440: $header = $this->Headers[$k];
441: $nextName ??= $header[0];
442: $nextValue[] = $header[1];
443: }
444: $next = $this->getCallbackValue($mode, $nextKey, $nextValue);
445: if ($item !== null) {
446: /** @var string[] */
447: $values = $callback($item, $next, $prev);
448: /** @var string $key */
449: $items[$key] = array_merge($items[$key], array_values($values));
450: }
451: $prev = $item;
452: $item = $next;
453: // Preserve the case of the first appearance of each header
454: $key = $nextName ?? $nextKey;
455: $items[$key] ??= [];
456: }
457: if ($item !== null) {
458: /** @var string[] */
459: $values = $callback($item, null, $prev);
460: /** @var string $key */
461: $items[$key] = array_merge($items[$key], array_values($values));
462: }
463: $items = array_filter($items);
464:
465: return Arr::same($items, $this->doGetHeaders())
466: ? $this
467: : new static($items);
468: }
469:
470: /**
471: * @inheritDoc
472: */
473: public function filter(callable $callback, int $mode = CollectionInterface::CALLBACK_USE_VALUE)
474: {
475: $index = $this->Index;
476: $prev = null;
477: $item = null;
478: $key = null;
479: $changed = false;
480:
481: foreach ($index as $nextKey => $headerKeys) {
482: $nextValue = [];
483: foreach ($headerKeys as $k) {
484: $header = $this->Headers[$k];
485: $nextValue[] = $header[1];
486: }
487: $next = $this->getCallbackValue($mode, $nextKey, $nextValue);
488: if ($item !== null && !$callback($item, $next, $prev)) {
489: unset($index[$key]);
490: $changed = true;
491: }
492: $prev = $item;
493: $item = $next;
494: $key = $nextKey;
495: }
496: if ($item !== null && !$callback($item, null, $prev)) {
497: unset($index[$key]);
498: $changed = true;
499: }
500:
501: return $changed
502: ? $this->replaceHeaders(null, $index)
503: : $this;
504: }
505:
506: /**
507: * @inheritDoc
508: */
509: public function only(array $keys)
510: {
511: return $this->onlyIn(array_fill_keys($keys, true));
512: }
513:
514: /**
515: * @inheritDoc
516: */
517: public function onlyIn(array $index)
518: {
519: $index = array_change_key_case($index);
520: $index = array_intersect_key($this->Index, $index);
521: return $this->maybeReplaceHeaders(null, $index);
522: }
523:
524: /**
525: * @inheritDoc
526: */
527: public function except(array $keys)
528: {
529: return $this->exceptIn(array_fill_keys($keys, true));
530: }
531:
532: /**
533: * @inheritDoc
534: */
535: public function exceptIn(array $index)
536: {
537: $index = array_change_key_case($index);
538: $index = array_diff_key($this->Index, $index);
539: return $this->maybeReplaceHeaders(null, $index);
540: }
541:
542: /**
543: * @inheritDoc
544: */
545: public function slice(int $offset, ?int $length = null)
546: {
547: $index = array_slice($this->Index, $offset, $length, true);
548: return $this->maybeReplaceHeaders(null, $index);
549: }
550:
551: /**
552: * @inheritDoc
553: */
554: public function pop(&$last = null)
555: {
556: if (!$this->Index) {
557: $last = null;
558: return $this;
559: }
560: $index = $this->Index;
561: array_pop($index);
562: $last = Arr::last($this->Items);
563: return $this->replaceHeaders(null, $index);
564: }
565:
566: /**
567: * @inheritDoc
568: */
569: public function shift(&$first = null)
570: {
571: if (!$this->Index) {
572: $first = null;
573: return $this;
574: }
575: $index = $this->Index;
576: array_shift($index);
577: $first = Arr::first($this->Items);
578: return $this->replaceHeaders(null, $index);
579: }
580:
581: /**
582: * @inheritDoc
583: */
584: public function authorize(
585: CredentialInterface $credential,
586: string $headerName = Headers::HEADER_AUTHORIZATION
587: ) {
588: return $this->set($headerName, sprintf(
589: '%s %s',
590: $credential->getAuthenticationScheme(),
591: $credential->getCredential(),
592: ));
593: }
594:
595: /**
596: * @inheritDoc
597: */
598: public function normalise()
599: {
600: return $this->maybeReplaceHeaders($this->Headers, $this->Index, true);
601: }
602:
603: /**
604: * @inheritDoc
605: */
606: public function trailers()
607: {
608: return $this->whereHeaderIsTrailer(true);
609: }
610:
611: /**
612: * @inheritDoc
613: */
614: public function withoutTrailers()
615: {
616: return $this->whereHeaderIsTrailer(false);
617: }
618:
619: /**
620: * @return static
621: */
622: private function whereHeaderIsTrailer(bool $value)
623: {
624: $headers = [];
625: $index = [];
626: foreach ($this->Index as $lower => $headerKeys) {
627: foreach ($headerKeys as $k) {
628: $isTrailer = $this->TrailerIndex[$k] ?? false;
629: if (!($value xor $isTrailer)) {
630: $headers[$k] = $this->Headers[$k];
631: $index[$lower][] = $k;
632: }
633: }
634: }
635: ksort($headers);
636: return $this->maybeReplaceHeaders($headers, $index);
637: }
638:
639: /**
640: * @inheritDoc
641: */
642: public function getLines(
643: string $format = '%s: %s',
644: ?string $emptyFormat = null
645: ): array {
646: foreach ($this->generateHeaders() as $key => $value) {
647: $lines[] = $emptyFormat !== null && trim($value) === ''
648: ? sprintf($emptyFormat, $key, '')
649: : sprintf($format, $key, $value);
650: }
651: return $lines ?? [];
652: }
653:
654: /**
655: * @inheritDoc
656: */
657: public function getHeaders(): array
658: {
659: return $this->doGetHeaders();
660: }
661:
662: /**
663: * @return array<string,string[]>
664: */
665: private function doGetHeaders(bool $preserveCase = true): array
666: {
667: foreach ($this->Index as $lower => $headerKeys) {
668: $key = $preserveCase
669: ? null
670: : $lower;
671: foreach ($headerKeys as $k) {
672: $header = $this->Headers[$k];
673: $key ??= $header[0];
674: $value = $header[1];
675: $headers[$key][] = $value;
676: }
677: }
678: return $headers ?? [];
679: }
680:
681: /**
682: * @inheritDoc
683: */
684: public function hasHeader(string $name): bool
685: {
686: return isset($this->Items[Str::lower($name)]);
687: }
688:
689: /**
690: * @inheritDoc
691: */
692: public function getHeader(string $name): array
693: {
694: return $this->Items[Str::lower($name)] ?? [];
695: }
696:
697: /**
698: * @inheritDoc
699: */
700: public function getHeaderLine(string $name): string
701: {
702: $values = $this->Items[Str::lower($name)] ?? [];
703: return $values
704: ? implode(', ', $values)
705: : '';
706: }
707:
708: /**
709: * @inheritDoc
710: */
711: public function getHeaderLines(): array
712: {
713: foreach ($this->Items as $lower => $values) {
714: $lines[$lower] = implode(', ', $values);
715: }
716: return $lines ?? [];
717: }
718:
719: /**
720: * @inheritDoc
721: */
722: public function getHeaderValues(string $name): array
723: {
724: $line = $this->getHeaderLine($name);
725: return $line === ''
726: ? []
727: // [RFC9110] Section 5.6.1.2: "A recipient MUST parse and ignore a
728: // reasonable number of empty list elements"
729: : Str::splitDelimited(',', $line);
730: }
731:
732: /**
733: * @inheritDoc
734: */
735: public function getFirstHeaderValue(string $name): string
736: {
737: return $this->doGetHeaderValue($name, true, false);
738: }
739:
740: /**
741: * @inheritDoc
742: */
743: public function getLastHeaderValue(string $name): string
744: {
745: return $this->doGetHeaderValue($name, false, true);
746: }
747:
748: /**
749: * @inheritDoc
750: */
751: public function getOnlyHeaderValue(string $name, bool $orSame = false): string
752: {
753: return $this->doGetHeaderValue($name, false, false, $orSame);
754: }
755:
756: private function doGetHeaderValue(
757: string $name,
758: bool $first,
759: bool $last,
760: bool $orSame = false
761: ): string {
762: $values = $this->getHeaderValues($name);
763: if (!$values) {
764: return '';
765: }
766: if ($last) {
767: return end($values);
768: }
769: if (!$first) {
770: if ($orSame) {
771: $values = array_unique($values);
772: }
773: if (count($values) > 1) {
774: throw new InvalidHeaderException(
775: sprintf('HTTP header has more than one value: %s', $name),
776: );
777: }
778: }
779: return reset($values);
780: }
781:
782: /**
783: * @inheritDoc
784: */
785: public function __toString(): string
786: {
787: return implode("\r\n", $this->getLines());
788: }
789:
790: /**
791: * @inheritDoc
792: */
793: public function jsonSerialize(): array
794: {
795: foreach ($this->generateHeaders() as $name => $value) {
796: $headers[] = ['name' => $name, 'value' => $value];
797: }
798: return $headers ?? [];
799: }
800:
801: /**
802: * @param array<int,array{string,string}>|null $headers
803: * @param array<string,int[]> $index
804: * @return static
805: */
806: private function maybeReplaceHeaders(
807: ?array $headers,
808: array $index,
809: bool $filterHeaders = false
810: ) {
811: $headers ??= $this->getIndexHeaders($index);
812:
813: if ($filterHeaders) {
814: $headers = $this->filterHeaders($headers);
815: }
816:
817: return $headers === $this->Headers
818: && $index === $this->Index
819: ? $this
820: : $this->replaceHeaders($headers, $index);
821: }
822:
823: /**
824: * @param array<int,array{string,string}>|null $headers
825: * @param array<string,int[]> $index
826: * @return static
827: */
828: private function replaceHeaders(?array $headers, array $index)
829: {
830: $clone = clone $this;
831: $clone->Headers = $headers ?? $this->getIndexHeaders($index);
832: $clone->Index = $clone->filterIndex($index);
833: $clone->Items = $clone->doGetHeaders(false);
834: $clone->IsParser = false;
835: return $clone;
836: }
837:
838: /**
839: * @param array<string,int[]> $index
840: * @return array<int,array{string,string}>
841: */
842: private function getIndexHeaders(array $index): array
843: {
844: foreach ($index as $headerKeys) {
845: foreach ($headerKeys as $k) {
846: $headers[$k] = true;
847: }
848: }
849: return array_intersect_key($this->Headers, $headers ?? []);
850: }
851:
852: /**
853: * @template T
854: *
855: * @param array<string,T> $index
856: * @return array<string,T>
857: */
858: private function filterIndex(array $index): array
859: {
860: // [RFC9110] Section 7.2: "A user agent that sends Host SHOULD send it
861: // as the first field in the header section of a request"
862: return isset($index['host'])
863: ? ['host' => $index['host']] + $index
864: : $index;
865: }
866:
867: /**
868: * @param array<int,array{string,string}> $headers
869: * @return array<int,array{string,string}>
870: */
871: private function filterHeaders(array $headers): array
872: {
873: $host = [];
874: foreach ($headers as $k => $header) {
875: if (Str::lower($header[0]) === 'host') {
876: $host[$k] = $header;
877: }
878: }
879: return $host + $headers;
880: }
881:
882: /**
883: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
884: * @param array<int,array{string,string}>|null $headers
885: * @param array<string,int[]>|null $index
886: * @param-out array<int,array{string,string}> $headers
887: * @param-out array<string,int[]> $index
888: * @return array<string,string[]>
889: */
890: protected function getItemsArray(
891: $items,
892: ?array &$headers = null,
893: ?array &$index = null
894: ): array {
895: $headers = [];
896: $index = [];
897: $k = -1;
898: foreach ($this->getItems($items) as $key => $values) {
899: $lower = Str::lower($key);
900: foreach ($values as $value) {
901: $headers[++$k] = [$key, $value];
902: $index[$lower][] = $k;
903: $array[$lower][] = $value;
904: }
905: }
906: return $array ?? [];
907: }
908:
909: /**
910: * @param Arrayable<string,string[]|string>|iterable<string,string[]|string> $items
911: * @return iterable<string,non-empty-array<string>>
912: */
913: protected function getItems($items): iterable
914: {
915: if ($items instanceof self) {
916: $items = $items->generateHeaders();
917: } elseif ($items instanceof Arrayable) {
918: $items = $items->toArray();
919: }
920: // @phpstan-ignore argument.type
921: yield from $this->filterItems($items);
922: }
923:
924: /**
925: * @param iterable<string,string[]|string> $items
926: * @return iterable<string,non-empty-array<string>>
927: */
928: private function filterItems(iterable $items): iterable
929: {
930: foreach ($items as $key => $value) {
931: $values = (array) $value;
932: if (!$values) {
933: throw new InvalidArgumentException(
934: sprintf('No values given for header: %s', $key),
935: );
936: }
937: $key = $this->filterName($key);
938: $filtered = [];
939: foreach ($values as $value) {
940: $filtered[] = $this->filterValue($value);
941: }
942: yield $key => $filtered;
943: }
944: }
945:
946: private function filterName(string $name): string
947: {
948: if (!Regex::match(self::HTTP_FIELD_NAME_REGEX, $name)) {
949: throw new InvalidArgumentException(
950: sprintf('Invalid header name: %s', $name),
951: );
952: }
953: return $name;
954: }
955:
956: private function filterValue(string $value): string
957: {
958: $value = Regex::replace('/\r\n\h++/', ' ', trim($value, " \t"));
959: if (!Regex::match(self::HTTP_FIELD_VALUE_REGEX, $value)) {
960: throw new InvalidArgumentException(
961: sprintf('Invalid header value: %s', $value),
962: );
963: }
964: return $value;
965: }
966:
967: /**
968: * @param string[] $a
969: * @param string[] $b
970: */
971: protected function compareItems($a, $b): int
972: {
973: return $a <=> $b;
974: }
975:
976: /**
977: * @return iterable<string,string>
978: */
979: protected function generateHeaders(): iterable
980: {
981: foreach ($this->Headers as [$key, $value]) {
982: yield $key => $value;
983: }
984: }
985: }
986: