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\PropertyNotDefined;
15 /**
16 * Representation of the `Cache-Control` header field.
17 *
18 * <pre>
19 * <?php
20 *
21 * use ICanBoogie\HTTP\CacheControlHeader;
22 *
23 * $cc = CacheControl::from('public, max-age=3600');
24 * echo $cc->cacheable; // true
25 * echo $cc->max_age; // 3600
26 *
27 * $cc->cacheable = 'no-cache';
28 * $cc->max_age = null;
29 * $cc->no_store = true;
30 * $cc->must_revalidate = true;
31 * echo $cc; // no-cache, no-store, must-revalidate
32 * </pre>
33 *
34 * @see http://tools.ietf.org/html/rfc2616#section-14.9
35 */
36 class CacheControlHeader
37 {
38 static protected $cacheable_values = array
39 (
40 'private',
41 'public',
42 'no-cache'
43 );
44
45 static protected $booleans = array
46 (
47 'no-store',
48 'no-transform',
49 'only-if-cached',
50 'must-revalidate',
51 'proxy-revalidate'
52 );
53
54 static protected $placeholder = array
55 (
56 'cacheable'
57 );
58
59 static private $default_values = array();
60
61 /**
62 * Returns the default values of the instance.
63 *
64 * @return array[string]mixed
65 */
66 static private function get_default_values()
67 {
68 $class = get_called_class();
69
70 if (isset(self::$default_values[$class]))
71 {
72 return self::$default_values[$class];
73 }
74
75 $reflection = new \ReflectionClass($class);
76 $default_values = array();
77
78 foreach ($reflection->getDefaultProperties() as $property => $default_value)
79 {
80 $property_reflection = new \ReflectionProperty($class, $property);
81
82 if ($property_reflection->isStatic() || !$property_reflection->isPublic())
83 {
84 continue;
85 }
86
87 $default_values[$property] = $default_value;
88 }
89
90 return self::$default_values[$class] = $default_values;
91 }
92
93 /**
94 * Parses the provided cache directive.
95 *
96 * @param string $cache_directive
97 *
98 * @return array Returns an array made of the properties and extensions.
99 */
100 static protected function parse($cache_directive)
101 {
102 $directives = explode(',', $cache_directive);
103 $directives = array_map('trim', $directives);
104
105 $properties = self::get_default_values();
106 $extensions = array();
107
108 foreach ($directives as $value)
109 {
110 if (in_array($value, self::$booleans))
111 {
112 $property = strtr($value, '-', '_');
113 $properties[$property] = true;
114 }
115 if (in_array($value, self::$cacheable_values))
116 {
117 $properties['cacheable'] = $value;
118 }
119 else if (preg_match('#^([^=]+)=(.+)$#', $value, $matches))
120 {
121 list(, $directive, $value) = $matches;
122
123 $property = strtr($directive, '-', '_');
124
125 if (is_numeric($value))
126 {
127 $value = 0 + $value;
128 }
129
130 if (!array_key_exists($property, $properties))
131 {
132 $extensions[$property] = $value;
133
134 continue;
135 }
136
137 $properties[$property] = $value;
138 }
139 }
140
141 return array($properties, $extensions);
142 }
143
144 /**
145 * Create an instance from the provided source.
146 *
147 * @param string $source
148 *
149 * @return \ICanBoogie\HTTP\CacheControlHeader
150 */
151 static public function from($source)
152 {
153 if ($source instanceof self)
154 {
155 return $source;
156 }
157
158 return new static($source);
159 }
160
161 /**
162 * Wheter the request/response is cacheable. The following properties are supported: `public`,
163 * `private` and `no-cache`. The variable may be empty in which case the cacheability of the
164 * request/response is unspecified.
165 *
166 * Scope: request, response.
167 *
168 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.1
169 *
170 * @var string
171 */
172 private $cacheable;
173
174 /**
175 * Wheter the request/response is can be stored.
176 *
177 * Scope: request, response.
178 *
179 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.2
180 *
181 * @var bool
182 */
183 public $no_store = false;
184
185 /**
186 * Indicates that the client is willing to accept a response whose age is no greater than the
187 * specified time in seconds. Unless `max-stale` directive is also included, the client is not
188 * willing to accept a stale response.
189 *
190 * Scope: request.
191 *
192 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
193 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
194 *
195 * @var int
196 */
197 public $max_age;
198
199 /**
200 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
201 *
202 * @var int
203 */
204 public $s_maxage;
205
206 /**
207 * Indicates that the client is willing to accept a response that has exceeded its expiration
208 * time. If max-stale is assigned a value, then the client is willing to accept a response
209 * that has exceeded its expiration time by no more than the specified number of seconds. If
210 * no value is assigned to max-stale, then the client is willing to accept a stale response
211 * of any age.
212 *
213 * Scope: request.
214 *
215 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
216 *
217 * @var string
218 */
219 public $max_stale;
220
221 /**
222 * Indicates that the client is willing to accept a response whose freshness lifetime is no
223 * less than its current age plus the specified time in seconds. That is, the client wants a
224 * response that will still be fresh for at least the specified number of seconds.
225 *
226 * Scope: request.
227 *
228 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
229 *
230 * @var int
231 */
232 public $min_fresh;
233
234 /**
235 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5
236 *
237 * Scope: request, response.
238 *
239 * @var bool
240 */
241 public $no_transform = false;
242
243 /**
244 * Scope: request.
245 *
246 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
247 *
248 * @var bool
249 */
250 public $only_if_cached = false;
251
252 /**
253 * Scope: response.
254 *
255 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
256 *
257 * @var bool
258 */
259 public $must_revalidate = false;
260
261 /**
262 * Scope: response.
263 *
264 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
265 *
266 * @var bool
267 */
268 public $proxy_revalidate = false;
269
270 /**
271 * Scope: request, response.
272 *
273 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.6
274 *
275 * @var string
276 */
277 public $extensions = array();
278
279 /**
280 * If they are defined, the object is initialized with the cache directives.
281 *
282 * @param string $cache_directive Cache directives.
283 */
284 public function __construct($cache_directives=null)
285 {
286 if ($cache_directives)
287 {
288 $this->modify($cache_directives);
289 }
290 }
291
292 public function __get($property)
293 {
294 switch ($property)
295 {
296 case 'cacheable': return $this->cacheable;
297 }
298
299 throw new PropertyNotDefined(array($property, $this));
300 }
301
302 public function __set($property, $value)
303 {
304 switch ($property)
305 {
306 case 'cacheable':
307 {
308 if ($value === false)
309 {
310 $value = 'no-cache';
311 }
312
313 if ($value !== null && !in_array($value, self::$cacheable_values))
314 {
315 throw new \InvalidArgumentException(\ICanBoogie\format
316 (
317 "%var must be one of: public, private, no-cache. Give: %value", array
318 (
319 'var' => 'cacheable',
320 'value' => $value
321 )
322 ));
323 }
324
325 $this->cacheable = $value;
326 }
327 return;
328 }
329
330 throw new PropertyNotDefined(array($property, $this));
331 }
332
333 /**
334 * Returns cache directives.
335 *
336 * @return string
337 */
338 public function __toString()
339 {
340 $cache_directive = '';
341
342 foreach (get_object_vars($this) as $directive => $value)
343 {
344 $directive = strtr($directive, '_', '-');
345
346 if (in_array($directive, self::$booleans))
347 {
348 if (!$value)
349 {
350 continue;
351 }
352
353 $cache_directive .= ', ' . $directive;
354 }
355 else if (in_array($directive, self::$placeholder))
356 {
357 if (!$value)
358 {
359 continue;
360 }
361
362 $cache_directive .= ', ' . $value;
363 }
364 else if (is_array($value))
365 {
366 // TODO: 20120831: extentions
367
368 continue;
369 }
370 else if ($value !== null && $value !== false)
371 {
372 $cache_directive .= ", $directive=$value";
373 }
374 }
375
376 return $cache_directive ? substr($cache_directive, 2) : '';
377 }
378
379 /**
380 * Sets the cache directives, updating the properties of the object.
381 *
382 * Unknown directives are stashed in the {@link $extensions} property.
383 *
384 * @param string $cache_directive
385 */
386 public function modify($cache_directive)
387 {
388 list($properties, $extensions) = static::parse($cache_directive);
389
390 foreach ($properties as $property => $value)
391 {
392 $this->$property = $value;
393 }
394
395 $this->extensions = $extensions;
396 }
397 }