• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

voku / httpful / 25187974306

30 Apr 2026 08:34PM UTC coverage: 90.483% (+0.6%) from 89.902%
25187974306

push

github

web-flow
Merge pull request #26 from voku/php8

[+]: PHP 8.0+ rework

368 of 407 new or added lines in 7 files covered. (90.42%)

1 existing line in 1 file now uncovered.

2548 of 2816 relevant lines covered (90.48%)

49.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.52
/src/Httpful/Headers.php
1
<?php
2

3
/** @noinspection MagicMethodsValidityInspection */
4
/** @noinspection PhpMissingParentConstructorInspection */
5

6
declare(strict_types=1);
7

8
namespace Httpful;
9

10
use Httpful\Exception\ResponseHeaderException;
11

12
/**
13
 * @implements \ArrayAccess<string, array<int, string>>
14
 * @implements \Iterator<string, array<int, string>>
15
 */
16
class Headers implements \ArrayAccess, \Countable, \Iterator
17
{
18
    /**
19
     * @var array<string, array<int, string>> data storage with lowercase keys
20
     *
21
     * @see offsetSet()
22
     * @see offsetExists()
23
     * @see offsetUnset()
24
     * @see offsetGet()
25
     * @see count()
26
     * @see current()
27
     * @see next()
28
     * @see key()
29
     */
30
    private $data = [];
31

32
    /**
33
     * @var array<string, string> case-sensitive keys
34
     *
35
     * @see offsetSet()
36
     * @see offsetUnset()
37
     * @see key()
38
     */
39
    private $keys = [];
40

41
    /**
42
     * Allow creating either an empty Array, or convert an existing Array to a
43
     * Case-Insensitive Array.  (Caution: Data may be lost when converting Case-
44
     * Sensitive Arrays to Case-Insensitive Arrays)
45
     *
46
     * @param array<string, array<int, float|int|string>|float|int|string>|null $initial (optional) Existing Array to convert
47
     */
48
    public function __construct(?array $initial = null)
49
    {
50
        if ($initial !== null) {
442✔
51
            foreach ($initial as $key => $value) {
363✔
52
                if (!\is_array($value)) {
361✔
53
                    $value = [$value];
8✔
54
                }
55

56
                $this->forceSet($key, $value);
361✔
57
            }
58
        }
59
    }
60

61
    /**
62
     * @see https://secure.php.net/manual/en/countable.count.php
63
     *
64
     * @return int the number of elements stored in the array
65
     */
66
    #[\ReturnTypeWillChange]
67
    public function count()
68
    {
69
        return (int) \count($this->data);
2✔
70
    }
71

72
    /**
73
     * @see https://secure.php.net/manual/en/iterator.current.php
74
     *
75
     * @return array<int, string>|false data at the current position
76
     */
77
    #[\ReturnTypeWillChange]
78
    public function current()
79
    {
80
        return \current($this->data);
131✔
81
    }
82

83
    /**
84
     * @see https://secure.php.net/manual/en/iterator.key.php
85
     *
86
     * @return string|null case-sensitive key at current position
87
     */
88
    #[\ReturnTypeWillChange]
89
    public function key()
90
    {
91
        $key = \key($this->data);
131✔
92

93
        if ($key === null) {
131✔
NEW
94
            return null;
×
95
        }
96

97
        return $this->keys[$key] ?? $key;
131✔
98
    }
99

100
    /**
101
     * @see https://secure.php.net/manual/en/iterator.next.php
102
     *
103
     * @return void
104
     */
105
    #[\ReturnTypeWillChange]
106
    public function next()
107
    {
108
        \next($this->data);
131✔
109
    }
110

111
    /**
112
     * @see https://secure.php.net/manual/en/iterator.rewind.php
113
     *
114
     * @return void
115
     */
116
    #[\ReturnTypeWillChange]
117
    public function rewind()
118
    {
119
        \reset($this->data);
348✔
120
    }
121

122
    /**
123
     * @see https://secure.php.net/manual/en/iterator.valid.php
124
     *
125
     * @return bool if the current position is valid
126
     */
127
    #[\ReturnTypeWillChange]
128
    public function valid()
129
    {
130
        return \key($this->data) !== null;
348✔
131
    }
132

133
    /**
134
     * @param string                                 $offset the offset to store the data at (case-insensitive)
135
     * @param array<int, float|int|string>|float|int|string $value  the data to store at the specified offset
136
     *
137
     * @return void
138
     */
139
    public function forceSet($offset, $value)
140
    {
141
        $value = $this->_validateAndTrimHeader($offset, $value);
366✔
142

143
        $this->offsetSetForce($offset, $value);
363✔
144
    }
145

146
    /**
147
     * @param string $offset
148
     *
149
     * @return void
150
     */
151
    public function forceUnset($offset)
152
    {
153
        $this->offsetUnsetForce($offset);
326✔
154
    }
155

156
    /**
157
     * @param string $string
158
     *
159
     * @return Headers
160
     */
161
    public static function fromString($string): self
162
    {
163
        // init
164
        $parsed_headers = [];
87✔
165

166
        $headers = \preg_split("/[\r\n]+/", $string, -1, \PREG_SPLIT_NO_EMPTY);
87✔
167
        if ($headers === false) {
87✔
168
            return new self($parsed_headers);
×
169
        }
170

171
        $headersCount = \count($headers);
87✔
172
        for ($i = 1; $i < $headersCount; ++$i) {
87✔
173
            $header = $headers[$i];
68✔
174

175
            if (\strpos($header, ':') === false) {
68✔
176
                continue;
6✔
177
            }
178

179
            list($key, $raw_value) = \explode(':', $header, 2);
68✔
180
            $key = \trim($key);
68✔
181
            $value = \trim($raw_value);
68✔
182

183
            if (\array_key_exists($key, $parsed_headers)) {
68✔
184
                $parsed_headers[$key][] = $value;
17✔
185
            } else {
186
                $parsed_headers[$key][] = $value;
68✔
187
            }
188
        }
189

190
        return new self($parsed_headers);
87✔
191
    }
192

193
    /**
194
     * Checks if the offset exists in data storage. The index is looked up with
195
     * the lowercase version of the provided offset.
196
     *
197
     * @see https://secure.php.net/manual/en/arrayaccess.offsetexists.php
198
     *
199
     * @param string $offset Offset to check
200
     *
201
     * @return bool if the offset exists
202
     */
203
    #[\ReturnTypeWillChange]
204
    public function offsetExists($offset)
205
    {
206
        return (bool) \array_key_exists(\strtolower($offset), $this->data);
229✔
207
    }
208

209
    /**
210
     * Return the stored data at the provided offset. The offset is converted to
211
     * lowercase and the lookup is done on the data store directly.
212
     *
213
     * @see https://secure.php.net/manual/en/arrayaccess.offsetget.php
214
     *
215
     * @param string $offset offset to lookup
216
     *
217
     * @return array<int, string>|null the data stored at the offset
218
     */
219
    #[\ReturnTypeWillChange]
220
    public function offsetGet($offset)
221
    {
222
        $offsetLower = \strtolower($offset);
95✔
223

224
        return $this->data[$offsetLower] ?? null;
95✔
225
    }
226

227
    /**
228
     * @param string             $offset
229
     * @param array<int, string> $value
230
     *
231
     * @throws ResponseHeaderException
232
     *
233
     * @return void
234
     */
235
    #[\ReturnTypeWillChange]
236
    public function offsetSet($offset, $value)
237
    {
238
        throw new ResponseHeaderException('Headers are read-only.');
1✔
239
    }
240

241
    /**
242
     * @param string $offset
243
     *
244
     * @throws ResponseHeaderException
245
     *
246
     * @return void
247
     */
248
    #[\ReturnTypeWillChange]
249
    public function offsetUnset($offset)
250
    {
251
        throw new ResponseHeaderException('Headers are read-only.');
1✔
252
    }
253

254
    /**
255
     * @return array<string, array<int, string>>
256
     */
257
    public function toArray(): array
258
    {
259
        // init
260
        $return = [];
345✔
261

262
        $that = clone $this;
345✔
263

264
        foreach ($that as $key => $value) {
345✔
265
            foreach ($value as $keyInner => $valueInner) {
36✔
266
                $value[$keyInner] = \trim($valueInner, " \t");
36✔
267
            }
268

269
            $return[$key] = $value;
36✔
270
        }
271

272
        return $return;
345✔
273
    }
274

275
    /**
276
     * Make sure the header complies with RFC 7230.
277
     *
278
     * Header names must be a non-empty string consisting of token characters.
279
     *
280
     * Header values must be strings consisting of visible characters with all optional
281
     * leading and trailing whitespace stripped. This method will always strip such
282
     * optional whitespace. Note that the method does not allow folding whitespace within
283
     * the values as this was deprecated for almost all instances by the RFC.
284
     *
285
     * header-field = field-name ":" OWS field-value OWS
286
     * field-name   = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
287
     *              / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) )
288
     * OWS          = *( SP / HTAB )
289
     * field-value  = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] )
290
     *
291
     * @see https://tools.ietf.org/html/rfc7230#section-3.2.4
292
     *
293
     * @param mixed $header
294
     * @param mixed $values
295
     *
296
     * @return string[]
297
     */
298
    private function _validateAndTrimHeader($header, $values): array
299
    {
300
        if (
301
            !\is_string($header)
366✔
302
            ||
303
            \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header) !== 1
366✔
304
        ) {
305
            throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string: ' . \print_r($header, true));
2✔
306
        }
307

308
        if (!\is_array($values)) {
365✔
309
            // This is simple, just one value.
310
            if (
311
                (!\is_numeric($values) && !\is_string($values))
101✔
312
                ||
313
                \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values) !== 1
101✔
314
            ) {
315
                throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings: ' . \print_r($header, true));
×
316
            }
317

318
            return [\trim((string) $values, " \t")];
101✔
319
        }
320

321
        if (empty($values)) {
363✔
322
            throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.');
1✔
323
        }
324

325
        // Assert Non empty array
326
        $returnValues = [];
362✔
327
        foreach ($values as $v) {
362✔
328
            if (
329
                (!\is_numeric($v) && !\is_string($v))
362✔
330
                ||
331
                \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v) !== 1
362✔
332
            ) {
333
                throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings: ' . \print_r($v, true));
1✔
334
            }
335

336
            $returnValues[] = \trim((string) $v, " \t");
361✔
337
        }
338

339
        return $returnValues;
361✔
340
    }
341

342
    /**
343
     * Set data at a specified offset. Converts the offset to lowercase, and
344
     * stores the case-sensitive offset and the data at the lowercase indexes in
345
     * $this->keys and @this->data.
346
     *
347
     * @see https://secure.php.net/manual/en/arrayaccess.offsetset.php
348
     *
349
     * @param string $offset the offset to store the data at (case-insensitive)
350
     * @param array<int, string> $value  the data to store at the specified offset
351
     *
352
     * @return void
353
     */
354
    private function offsetSetForce($offset, $value)
355
    {
356
        $offsetlower = \strtolower($offset);
363✔
357
        $this->data[$offsetlower] = $value;
363✔
358
        $this->keys[$offsetlower] = $offset;
363✔
359
    }
360

361
    /**
362
     * Unsets the specified offset. Converts the provided offset to lowercase,
363
     * and unsets the case-sensitive key, as well as the stored data.
364
     *
365
     * @see https://secure.php.net/manual/en/arrayaccess.offsetunset.php
366
     *
367
     * @param string $offset the offset to unset
368
     *
369
     * @return void
370
     */
371
    private function offsetUnsetForce($offset)
372
    {
373
        $offsetLower = \strtolower($offset);
326✔
374

375
        unset($this->data[$offsetLower], $this->keys[$offsetLower]);
326✔
376
    }
377
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc