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