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