Passed
Push — master ( fca16e...a09e23 )
by Alexander
04:10
created

Headers::set()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 22
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 14
nc 8
nop 3
dl 0
loc 22
rs 8.8333
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * Lenevor Framework
5
 *
6
 * LICENSE
7
 *
8
 * This source file is subject to the new BSD license that is bundled
9
 * with this package in the file license.md.
10
 * It is also available through the world-wide-web at this URL:
11
 * https://lenevor.com/license
12
 * If you did not receive a copy of the license and are unable to
13
 * obtain it through the world-wide-web, please send an email
14
 * to [email protected] so we can send you a copy immediately.
15
 *
16
 * @package     Lenevor
17
 * @subpackage  Base
18
 * @link        https://lenevor.com
19
 * @copyright   Copyright (c) 2019 - 2023 Alexander Campo <[email protected]>
20
 * @license     https://opensource.org/licenses/BSD-3-Clause New BSD license or see https://lenevor.com/license or see /license.md
21
 */
22
23
namespace Syscodes\Components\Http\Loaders;
24
25
use DateTime;
26
use Countable;
27
use Traversable;
28
use ArrayIterator;
29
use RuntimeException;
30
use DateTimeInterface;
31
use IteratorAggregate;
32
33
/**
34
 * Headers class is a container for HTTP headers.
35
 */
36
class Headers implements IteratorAggregate, Countable
37
{
38
	protected const STRING_UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ';
39
	protected const STRING_LOWER = '-abcdefghijklmnopqrstuvwxyz';
40
41
	/**
42
	 * An array of HTTP headers.
43
	 * 
44
	 * @var array $herders
45
	 */
46
	protected $headers = [];
47
	
48
	/**
49
	 * Specifies the directives for the caching mechanisms in both
50
	 * the requests and the responses.
51
	 * 
52
	 * @var array $cacheControl
53
	 */
54
	protected $cacheControl = [];
55
	
56
	/**
57
	 * Constructor. The Headers class instance.
58
	 * 
59
	 * @param  array  $headers
60
	 * 
61
	 * @return void
62
	 */
63
	public function __construct(array $headers = [])
64
	{
65
		foreach ($headers as $key => $values) {
66
			$this->set($key, $values);
67
		}
68
	}
69
	
70
	/**
71
	 * Returns all the headers.
72
	 * 
73
	 * @param  string|null  $key  The name of the headers
74
	 * 
75
	 * @return array
76
	 */
77
	public function all(string $key = null): array
78
	{
79
		if (null !== $key) {
80
			return $this->headers[strtr($key, self::STRING_UPPER, self::STRING_LOWER)] ?? [];
81
		}
82
83
		return $this->headers;
84
	}
85
	
86
	/**
87
	 * Returns the parameter keys.
88
	 * 
89
	 * @return array An array of parameter keys
90
	 */
91
	public function keys(): array
92
	{
93
		return array_keys($this->all());
94
	}
95
	
96
	/**
97
	 * Replaces the current HTTP headers by a new set.
98
	 * 
99
	 * @param  array  $headers
100
	 * 
101
	 * @return void
102
	 */
103
	public function replace(array $headers = []): void
104
	{
105
		$this->headers = [];
106
		$this->add($headers);
107
	}
108
	
109
	/**
110
	 * Adds multiple header to the queue.
111
	 * 
112
	 * @param  array  $headers  The header name
113
	 * 
114
	 * @return mixed
115
	 */
116
	public function add(array $headers = [])
117
	{
118
		foreach ($headers as $key => $values) {
119
			$this->set($key, $values);
120
		}
121
	}
122
	
123
	/**
124
	 * Gets a header value by name.
125
	 *
126
	 * @param  string  $key  The header name, or null for all headers
127
	 * @param  string|null  $default  The default value
128
	 *
129
	 * @return mixed
130
	 */
131
	public function get(string $key, string $default = null): ?string
132
	{
133
		$headers = $this->all($key);
134
		
135
		if ( ! $headers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $headers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
introduced by
$headers is an empty array, thus ! $headers is always true.
Loading history...
136
			return $default;
137
		}
138
		
139
		if (null === $headers[0]) {
140
			return null;
141
		}
142
		
143
		return (string) $headers[0];
144
	}
145
146
	/**
147
	 * Sets a header by name.
148
	 * 
149
	 * @param  string  $key  The header name
150
	 * @param  string|string[]|null  $values  The value or an array of values
151
	 * @param  bool  $replace  If you want to replace the value exists by the header, 
152
	 * 					       it is not overwritten / overwritten when it is false
153
	 *
154
	 * @return void
155
	 */
156
	public function set(string $key, $values, bool $replace = true): void
157
	{
158
		$key = strtr($key, self::STRING_UPPER, self::STRING_LOWER);
159
160
		if (is_array($values)) {
161
			$values = array_values($values);
162
163
			if (true === $replace || ! isset($this->headers[$key])) {
164
				$this->headers[$key] = $values;
165
			} else {
166
				$this->headers[$key] = array_merge($this->headers[$key], $values);
167
			}
168
		} else {
169
			if (true === $replace || ! isset($this->headers[$key])) {
170
				$this->headers[$key] = [$values];
171
			} else {
172
				$this->headers[$key][] = $values;
173
			}
174
		}
175
		
176
		if ('cache-control' === $key) {
177
			$this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key]));
178
		}
179
	}
180
181
	/**
182
	 * Returns true if the HTTP header is defined.
183
	 * 
184
	 * @param  string  $key  The HTTP header
185
	 * 
186
	 * @return bool  true if the parameter exists, false otherwise
187
	 */
188
	public function has(string $key): bool
189
	{
190
		return array_key_exists(strtr($key, self::STRING_UPPER, self::STRING_LOWER), $this->all());
191
	}
192
193
	/**
194
	 * Removes a header.
195
	 * 
196
	 * @param  string  $name  The header name
197
	 * 
198
	 * @return mixed
199
	 */
200
	public function remove(string $key)
201
	{
202
		$key = strtr($key, self::STRING_UPPER, self::STRING_LOWER);
203
204
		unset($this->headers[$key]);
205
206
		if ('cache-control' === $key) {
207
			$this->cacheControl = [];
208
		}
209
	}
210
	
211
	/**
212
	 * Returns the HTTP header value converted to a date.
213
	 * 
214
	 * @param  string  $key
215
	 * @param  DateTime|null  $default
216
	 * 
217
	 * @throws \RuntimeException When the HTTP header is not parseable
218
	 */
219
	public function getDate(string $key, DateTime $default = null): ?DateTimeInterface
220
	{
221
		if (null === $value = $this->get($key)) {
0 ignored issues
show
introduced by
The condition null === $value = $this->get($key) is always true.
Loading history...
Bug introduced by
Are you sure the assignment to $value is correct as $this->get($key) targeting Syscodes\Components\Http\Loaders\Headers::get() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
222
			return $default;
223
		}
224
		
225
		if (false === $date = DateTime::createFromFormat(DATE_RFC2822, $value)) {
226
			throw new RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value));
227
		}
228
		
229
		return $date;
230
	}
231
	
232
	/**
233
	 * Returns an iterator for headers.
234
	 * 
235
	 * @return \ArrayIterator An \ArrayIterator instance
236
	 */
237
	public function getIterator(): Traversable
238
	{
239
		return new ArrayIterator($this->headers);
240
	}
241
	
242
	/**
243
	 * Returns the number of headers.
244
	 * 
245
	 * @return int The number of headers
246
	 */
247
	public function count(): int
248
	{
249
		return count($this->headers);
250
	}
251
	
252
	/**
253
	 * Parses a Cache-Control HTTP header.
254
	 * 
255
	 * @param string $header The value of the Cache-Control HTTP header
256
	 * 
257
	 * @return array An array representing the attribute values
258
	 */
259
	protected function parseCacheControl($header): array
260
	{
261
		$cacheControl = [];
262
		
263
		preg_match_all('~([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?~', $header, $matches, PREG_SET_ORDER);
264
		
265
		foreach ($matches as $match) {
266
			$cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true);
267
		}
268
		
269
		return $cacheControl;
270
	}
271
	
272
	/**
273
	 * Magic method.
274
	 * 
275
	 * Returns the headers as a string.
276
	 * 
277
	 * @return string The headers
278
	 */
279
	public function __toString(): string
280
	{
281
		if ( ! $headers = $this->all()) {
282
			return '';
283
		}
284
		
285
		ksort($headers);
286
		
287
		$max     = max(array_map('strlen', array_keys($headers))) + 1;
288
		$content = '';
289
		
290
		foreach ($headers as $name => $values) {
291
			$name = ucwords($name, '-');
292
			
293
			foreach ($values as $value) {
294
				$content .= sprintf("%-{$max}s %s\r\n", $name.':', $value);
295
			}
296
		}
297
298
		return $content;
299
	}
300
}