1 <?php
2
3 /*
4 * This file is part of the ICanBoogie package.
5 *
6 * (c) Olivier Laviale <olivier.laviale@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12 namespace ICanBoogie\HTTP\Headers;
13
14 use ICanBoogie\OffsetNotDefined;
15 use ICanBoogie\PropertyNotDefined;
16
17 /**
18 * Base class for header fields.
19 *
20 * Classes that extend the class and support attributes must defined them during construct:
21 *
22 * <pre>
23 * <?php
24 *
25 * namespace ICanBoogie\HTTP\Headers;
26 *
27 * class ContentDisposition extends Header
28 * {
29 * public function __construct($value=null, array $attributes=[])
30 * {
31 * $this->parameters['filename'] = new HeaderParameter('filename');
32 *
33 * parent::__construct($value, $attributes);
34 * }
35 * }
36 * </pre>
37 *
38 * Magic properties are automatically mapped to parameters. The value of a parameter is accessed
39 * through its corresponding property:
40 *
41 * <pre>
42 * <?php
43 *
44 * $cd = new ContentDisposition;
45 * $cd->filename = "Statistics.csv";
46 * echo $cd->filename;
47 * // "Statistics.csv"
48 * </pre>
49 *
50 * The instance of the parameter itself is accessed using the header as an array:
51 *
52 * <pre>
53 * <?php
54 *
55 * $cd = new ContentDisposition;
56 * $cd['filename']->value = "Statistics.csv";
57 * $cd['filename']->language = "en";
58 * </pre>
59 *
60 * An alias to the {@link $value} property can be defined by using the `VALUE_ALIAS` constant. The
61 * following code defines `type` as an alias:
62 *
63 * <pre>
64 * <?php
65 *
66 * class ContentDisposition extends Header
67 * {
68 * const VALUE_ALIAS = 'type';
69 * }
70 * </pre>
71 */
72 abstract class Header implements \ArrayAccess
73 {
74 const VALUE_ALIAS = null;
75
76 /**
77 * The value of the header.
78 *
79 * @var string
80 */
81 public $value;
82
83 /**
84 * The parameters supported by the header.
85 *
86 * @var HeaderParameter[]
87 */
88 protected $parameters = [];
89
90 /**
91 * Creates a {@link Header} instance from the provided source.
92 *
93 * @param string|Header $source The source to create the instance from. If the source is
94 * an instance of {@link Header} it is returned as is.
95 *
96 * @return Header
97 */
98 static public function from($source)
99 {
100 if ($source instanceof self)
101 {
102 return $source;
103 }
104
105 if ($source === null)
106 {
107 return new static;
108 }
109
110 list($value, $parameters) = static::parse($source);
111
112 return new static($value, $parameters);
113 }
114
115 /**
116 * Parse the provided source and extract its value and parameters.
117 *
118 * @param string $source The source to create the instance from.
119 *
120 * @throws \InvalidArgumentException if `$source` is not a string nor an object implementing
121 * `__toString()`.
122 *
123 * @return array
124 */
125 static protected function parse($source)
126 {
127 if (is_object($source) && method_exists($source, '__toString'))
128 {
129 $source = (string) $source;
130 }
131
132 if (!is_string($source))
133 {
134 throw new \InvalidArgumentException(\ICanBoogie\format
135 (
136 "%var must be a string or an object implementing __toString(). Given: !data", [
137
138 'var' => 'source',
139 'data' => $source
140
141 ]
142 ));
143 }
144
145 $value_end = strpos($source, ';');
146 $parameters = [];
147
148 if ($value_end !== false)
149 {
150 $value = substr($source, 0, $value_end);
151 $attributes = trim(substr($source, $value_end + 1));
152
153 if ($attributes)
154 {
155 $a = explode(';', $attributes);
156 $a = array_map('trim', $a);
157
158 foreach ($a as $attribute)
159 {
160 $parameter = HeaderParameter::from($attribute);
161 $parameters[$parameter->attribute] = $parameter;
162 }
163 }
164 }
165 else
166 {
167 $value = $source;
168 }
169
170 return [ $value, $parameters ];
171 }
172
173 /**
174 * Checks if a parameter exists.
175 *
176 * @param string $attribute
177 *
178 * @return bool
179 */
180 public function offsetExists($attribute)
181 {
182 return isset($this->parameters[$attribute]);
183 }
184
185 /**
186 * Sets the value of a parameter to `null`.
187 *
188 * @param string $attribute
189 */
190 public function offsetUnset($attribute)
191 {
192 $this->parameters[$attribute]->value = null;
193 }
194
195 /**
196 * Sets the value of a parameter.
197 *
198 * If the value is an instance of {@link HeaderParameter} then the parameter is replaced,
199 * otherwise the value of the current parameter is updated and its language is set to `null`.
200 *
201 * @param string $attribute
202 * @param mixed $value
203 *
204 * @throws OffsetNotDefined in attempt to access a parameter that is not defined.
205 */
206 public function offsetSet($attribute, $value)
207 {
208 if (!$this->offsetExists($attribute))
209 {
210 throw new OffsetNotDefined([ $attribute, $this ]);
211 }
212
213 if ($value instanceof HeaderParameter)
214 {
215 $this->parameters[$attribute] = $value;
216 }
217 else
218 {
219 $this->parameters[$attribute]->value = $value;
220 $this->parameters[$attribute]->language = null;
221 }
222 }
223
224 /**
225 * Returns a {@link HeaderParameter} instance.
226 *
227 * @param string $attribute
228 *
229 * @return HeaderParameter
230 *
231 * @throws OffsetNotDefined in attempt to access a parameter that is not defined.
232 */
233 public function offsetGet($attribute)
234 {
235 if (!$this->offsetExists($attribute))
236 {
237 throw new OffsetNotDefined([ $attribute, $this ]);
238 }
239
240 return $this->parameters[$attribute];
241 }
242
243 /**
244 * Initializes the {@link $name}, {@link $value} and {@link $parameters} properties.
245 *
246 * To enable future extensions, unrecognized parameters are ignored. Supported parameters must
247 * be defined by a child class before it calls its parent.
248 *
249 * @param string $value
250 * @param array $parameters
251 */
252 public function __construct($value=null, array $parameters=[])
253 {
254 $this->value = $value;
255
256 $parameters = array_intersect_key($parameters, $this->parameters);
257
258 foreach ($parameters as $attribute => $value)
259 {
260 $this[$attribute] = $value;
261 }
262 }
263
264 /**
265 * Returns the value of a defined parameter.
266 *
267 * The method also handles the alias of the {@link $value} property.
268 *
269 * @param string $property
270 *
271 * @throws PropertyNotDefined in attempt to access a parameter that is not defined.
272 *
273 * @return mixed
274 */
275 public function __get($property)
276 {
277 if ($property === static::VALUE_ALIAS)
278 {
279 return $this->value;
280 }
281
282 if ($this->offsetExists($property))
283 {
284 return $this[$property]->value;
285 }
286
287 throw new PropertyNotDefined([ $property, $this ]);
288 }
289
290 /**
291 * Sets the value of a supported parameter.
292 *
293 * The method also handles the alias of the {@link $value} property.
294 *
295 * @param string $property
296 * @param mixed $value
297 *
298 * @throws PropertyNotDefined in attempt to access a parameter that is not defined.
299 */
300 public function __set($property, $value)
301 {
302 if ($property === static::VALUE_ALIAS)
303 {
304 $this->value = $value;
305
306 return;
307 }
308
309 if ($this->offsetExists($property))
310 {
311 $this[$property]->value = $value;
312
313 return;
314 }
315
316 throw new PropertyNotDefined([ $property, $this ]);
317 }
318
319 /**
320 * Unsets the matching parameter.
321 *
322 * @param string $property
323 *
324 * @throws PropertyNotDefined in attempt to access a parameter that is not defined.
325 */
326 public function __unset($property)
327 {
328 if (!isset($this->parameters[$property]))
329 {
330 return;
331 }
332
333 unset($this[$property]);
334 }
335
336 /**
337 * Renders the instance's value and parameters into a string.
338 *
339 * @return string
340 */
341 public function __toString()
342 {
343 $value = $this->value;
344
345 if (!$value && $value !== 0)
346 {
347 return '';
348 }
349
350 foreach ($this->parameters as $attribute)
351 {
352 $rendered_attribute = $attribute->render();
353
354 if (!$rendered_attribute)
355 {
356 continue;
357 }
358
359 $value .= '; ' . $rendered_attribute;
360 }
361
362 return $value;
363 }
364 }
365