ICanBoogie/HTTP v2.4.0
  • Namespace
  • Class

Namespaces

  • ICanBoogie
    • Exception
    • HTTP
      • Dispatcher
      • Headers
      • Request

Classes

  • ICanBoogie\Exception\RescueEvent
  • ICanBoogie\HTTP\CallableDispatcher
  • ICanBoogie\HTTP\Dispatcher
  • ICanBoogie\HTTP\Dispatcher\BeforeDispatchEvent
  • ICanBoogie\HTTP\Dispatcher\DispatchEvent
  • ICanBoogie\HTTP\File
  • ICanBoogie\HTTP\FileInfo
  • ICanBoogie\HTTP\FileList
  • ICanBoogie\HTTP\Headers
  • ICanBoogie\HTTP\Headers\CacheControl
  • ICanBoogie\HTTP\Headers\ContentDisposition
  • ICanBoogie\HTTP\Headers\ContentType
  • ICanBoogie\HTTP\Headers\Date
  • ICanBoogie\HTTP\Headers\Header
  • ICanBoogie\HTTP\Headers\HeaderParameter
  • ICanBoogie\HTTP\Helpers
  • ICanBoogie\HTTP\RedirectResponse
  • ICanBoogie\HTTP\Request
  • ICanBoogie\HTTP\Request\Context
  • ICanBoogie\HTTP\Response
  • ICanBoogie\HTTP\Status
  • ICanBoogie\HTTP\WeightedDispatcher

Interfaces

  • ICanBoogie\HTTP\DispatcherInterface
  • ICanBoogie\HTTP\Exception

Exceptions

  • ICanBoogie\HTTP\DispatcherNotDefined
  • ICanBoogie\HTTP\ForceRedirect
  • ICanBoogie\HTTP\MethodNotSupported
  • ICanBoogie\HTTP\NotFound
  • ICanBoogie\HTTP\ServiceUnavailable
  • ICanBoogie\HTTP\StatusCodeNotValid
  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\Accessor\AccessorTrait;
 15 
 16 /**
 17  * A response to a HTTP request.
 18  *
 19  * @property Status $status
 20  * @property mixed $body The body of the response.
 21  *
 22  * @property int $ttl Adjusts the `s-maxage` part of the `Cache-Control` header field definition according to the `Age` header field definition.
 23  * @property int $age Shortcut to the `Age` header field definition.
 24  * @property Headers\CacheControl $cache_control Shortcut to the `Cache-Control` header field definition.
 25  * @property int $content_length Shortcut to the `Content-Length` header field definition.
 26  * @property Headers\ContentType $content_type Shortcut to the `Content-Type` header field definition.
 27  * @property Headers\Date $date Shortcut to the `Date` header field definition.
 28  * @property string $etag Shortcut to the `Etag` header field definition.
 29  * @property Headers\Date $expires Shortcut to the `Expires` header field definition.
 30  * @property Headers\Date $last_modified Shortcut to the `Last-Modified` header field definition.
 31  * @property string $location Shortcut to the `Location` header field definition.
 32  *
 33  * @property-read boolean $is_cacheable {@link get_is_cacheable()}
 34  * @property-read boolean $is_fresh {@link get_is_fresh()}
 35  * @property-read boolean $is_private {@link get_is_private()}
 36  * @property-read boolean $is_validateable {@link get_is_validateable()}
 37  *
 38  * @see http://tools.ietf.org/html/rfc2616
 39  */
 40 class Response
 41 {
 42     use AccessorTrait;
 43 
 44     /**
 45      * Response headers.
 46      *
 47      * @var Headers
 48      */
 49     public $headers;
 50 
 51     /**
 52      * The HTTP protocol version (1.0 or 1.1), defaults to '1.0'
 53      *
 54      * @var string
 55      */
 56     public $version = '1.0';
 57 
 58     /**
 59      * Initializes the {@link $body}, {@link $header}, {@link $date} and {@link $status}
 60      * properties.
 61      *
 62      * @param mixed $body The body of the response.
 63      * @param int|Status $status The status code of the response.
 64      * @param Headers|array $headers The initial header fields of the response.
 65      */
 66     public function __construct($body = null, $status = 200, $headers = [])
 67     {
 68         if (is_array($headers))
 69         {
 70             $headers = new Headers($headers);
 71         }
 72         else if (!($headers instanceof Headers))
 73         {
 74             throw new \InvalidArgumentException("\$headers must be an array or a ICanBoogie\\HTTP\\Headers instance. Given: " . gettype($headers));
 75         }
 76 
 77         $this->headers = $headers;
 78 
 79         if ($this->date->is_empty)
 80         {
 81             $this->date = 'now';
 82         }
 83 
 84         $this->set_status($status);
 85 
 86         if ($body !== null)
 87         {
 88             $this->set_body($body);
 89         }
 90     }
 91 
 92     /**
 93      * Clones the {@link $headers} and {@link $status} properties.
 94      */
 95     public function __clone()
 96     {
 97         $this->headers = clone $this->headers;
 98         $this->status = clone $this->status;
 99     }
100 
101     /**
102      * Renders the response as an HTTP string.
103      *
104      * @return string
105      */
106     public function __toString()
107     {
108         try
109         {
110             $header = clone $this->headers;
111             $body = $this->body;
112 
113             $this->finalize($header, $body);
114 
115             ob_start();
116 
117             $this->send_body($body);
118 
119             $body = ob_get_clean();
120 
121             return "HTTP/{$this->version} {$this->status}\r\n"
122             . $header
123             . "\r\n"
124             . $body;
125         }
126         catch (\Exception $e)
127         {
128             return $e->getMessage();
129         }
130     }
131 
132     /**
133      * Issues the HTTP response.
134      *
135      * {@link finalize()} is invoked to finalize the headers (a cloned actually)
136      * and the body. {@link send_headers} is invoked to send the headers and {@link send_body()}
137      *is invoked to send the body, if the body is not `null`.
138      *
139      * The body is not send in the following instances:
140      *
141      * - The finalized body is `null`.
142      * - The status is not ok.
143      */
144     public function __invoke()
145     {
146         $headers = clone $this->headers;
147         $body = $this->body;
148 
149         $this->finalize($headers, $body);
150         $this->send_headers($headers);
151 
152         if ($body === null)
153         {
154             return;
155         }
156 
157         $this->send_body($body);
158     }
159 
160     /**
161      * Finalize the body.
162      *
163      * Subclasses might want to override this method if they wish to alter the header or the body
164      * before the response is sent or transformed into a string.
165      *
166      * @param Headers $headers Reference to the final header.
167      * @param mixed $body Reference to the final body.
168      */
169     protected function finalize(Headers &$headers, &$body)
170     {
171         if ($body instanceof \Closure || !method_exists($body, '__toString'))
172         {
173             return;
174         }
175 
176         $body = (string) $body;
177     }
178 
179     /**
180      * Sends response headers.
181      *
182      * @param Headers $headers
183      *
184      * @return bool `true` is the headers were sent, `false` otherwise.
185      */
186     // @codeCoverageIgnoreStart
187     protected function send_headers(Headers $headers)
188     {
189         if (headers_sent($file, $line))
190         {
191             trigger_error(\ICanBoogie\format
192             (
193                 "Cannot modify header information because it was already sent. Output started at !at.", [
194 
195                     'at' => $file . ':' . $line
196 
197                 ]
198             ));
199 
200             return false;
201         }
202 
203         header_remove('Pragma');
204         header_remove('X-Powered-By');
205 
206         header("HTTP/{$this->version} {$this->status}");
207 
208         $headers();
209 
210         return true;
211     }
212     // @codeCoverageIgnoreEnd
213 
214     /**
215      * Sends response body.
216      *
217      * @param mixed $body
218      */
219     protected function send_body($body)
220     {
221         if ($body instanceof \Closure)
222         {
223             $body($this);
224 
225             return;
226         }
227 
228         echo $body;
229     }
230 
231     /**
232      * Status of the HTTP response.
233      *
234      * @var Status
235      */
236     private $status;
237 
238     /**
239      * Sets response status code and optionally status message.
240      *
241      * @param int|Status $status HTTP status code or HTTP status code and HTTP status message.
242      */
243     protected function set_status($status)
244     {
245         $this->status = Status::from($status);
246     }
247 
248     /**
249      * Returns the response status.
250      *
251      * @return Status
252      */
253     protected function get_status()
254     {
255         return $this->status;
256     }
257 
258     /**
259      * The response body.
260      *
261      * @var mixed
262      *
263      * @see set_body(), get_body()
264      */
265     private $body;
266 
267     /**
268      * Sets the response body.
269      *
270      * The body can be any data type that can be converted into a string. This includes numeric
271      * and objects implementing the {@link __toString()} method.
272      *
273      * @param string|\Closure|null $body
274      *
275      * @throws \UnexpectedValueException when the body cannot be converted to a string.
276      */
277     protected function set_body($body)
278     {
279         $this->assert_body_is_valid($body);
280 
281         $this->body = $body;
282     }
283 
284     /**
285      * Asserts that the a body is valid.
286      *
287      * @param $body
288      *
289      * @throws \UnexpectedValueException if the specified body doesn't meet the requirements.
290      */
291     protected function assert_body_is_valid($body)
292     {
293         if ($body === null
294         || $body instanceof \Closure
295         || is_numeric($body)
296         || is_string($body)
297         || (is_object($body) && method_exists($body, '__toString')))
298         {
299             return;
300         }
301 
302         throw new \UnexpectedValueException(\ICanBoogie\format
303         (
304             'The Response body must be a string, an object implementing the __toString() method or be callable, %type given. !value', array
305             (
306                 'type' => gettype($body),
307                 'value' => $body
308             )
309         ));
310     }
311 
312     /**
313      * Returns the response body.
314      *
315      * @return string
316      */
317     protected function get_body()
318     {
319         return $this->body;
320     }
321 
322     /**
323      * Sets the value of the `Location` header field.
324      *
325      * @param string|null $url
326      */
327     protected function set_location($url)
328     {
329         if ($url !== null && !$url)
330         {
331             throw new \InvalidArgumentException('Cannot redirect to an empty URL.');
332         }
333 
334         $this->headers['Location'] = $url;
335     }
336 
337     /**
338      * Returns the value of the `Location` header field.
339      *
340      * @return string
341      */
342     protected function get_location()
343     {
344         return $this->headers['Location'];
345     }
346 
347     /**
348      * Sets the value of the `Content-Type` header field.
349      *
350      * @param string $content_type
351      */
352     protected function set_content_type($content_type)
353     {
354         $this->headers['Content-Type'] = $content_type;
355     }
356 
357     /**
358      * Returns the value of the `Content-Type` header field.
359      *
360      * @return Headers\ContentType
361      */
362     protected function get_content_type()
363     {
364         return $this->headers['Content-Type'];
365     }
366 
367     /**
368      * Sets the value of the `Content-Length` header field.
369      *
370      * @param int $length
371      */
372     protected function set_content_length($length)
373     {
374         $this->headers['Content-Length'] = $length;
375     }
376 
377     /**
378      * Returns the value of the `Content-Length` header field.
379      *
380      * @return int
381      */
382     protected function get_content_length()
383     {
384         return $this->headers['Content-Length'];
385     }
386 
387     /**
388      * Sets the value of the `Date` header field.
389      *
390      * @param mixed $time
391      */
392     protected function set_date($time)
393     {
394         $this->headers['Date'] = $time;
395     }
396 
397     /**
398      * Returns the value of the `Date` header field.
399      *
400      * @return Headers\Date
401      */
402     protected function get_date()
403     {
404         return $this->headers['Date'];
405     }
406 
407     /**
408      * Sets the value of the `Age` header field.
409      *
410      * @param int $age
411      */
412     protected function set_age($age)
413     {
414         $this->headers['Age'] = $age;
415     }
416 
417     /**
418      * Returns the age of the response.
419      *
420      * @return int
421      */
422     protected function get_age()
423     {
424         $age = $this->headers['Age'];
425 
426         if ($age)
427         {
428             return $age;
429         }
430 
431         if (!$this->date->is_empty)
432         {
433             return max(0, time() - $this->date->utc->timestamp);
434         }
435 
436         return null;
437     }
438 
439     /**
440      * Sets the value of the `Last-Modified` header field.
441      *
442      * @param mixed $time
443      */
444     protected function set_last_modified($time)
445     {
446         $this->headers['Last-Modified'] = $time;
447     }
448 
449     /**
450      * Returns the value of the `Last-Modified` header field.
451      *
452      * @return Headers\Date
453      */
454     protected function get_last_modified()
455     {
456         return $this->headers['Last-Modified'];
457     }
458 
459     /**
460      * Sets the value of the `Expires` header field.
461      *
462      * The method also calls the {@link session_cache_expire()} function.
463      *
464      * @param mixed $time
465      */
466     protected function set_expires($time)
467     {
468         $this->headers['Expires'] = $time;
469 
470         session_cache_expire($time); // TODO-20120831: Is this required now that we have an awesome response system ?
471     }
472 
473     /**
474      * Returns the value of the `Expires` header field.
475      *
476      * @return Headers\Date
477      */
478     protected function get_expires()
479     {
480         return $this->headers['Expires'];
481     }
482 
483     /**
484      * Sets the value of the `Etag` header field.
485      *
486      * @param string $value
487      */
488     protected function set_etag($value)
489     {
490         $this->headers['Etag'] = $value;
491     }
492 
493     /**
494      * Returns the value of the `Etag` header field.
495      *
496      * @return string
497      */
498     protected function get_etag()
499     {
500         return $this->headers['Etag'];
501     }
502 
503     /**
504      * Sets the directives of the `Cache-Control` header field.
505      *
506      * @param string $cache_directives
507      */
508     protected function set_cache_control($cache_directives)
509     {
510         $this->headers['Cache-Control'] = $cache_directives;
511     }
512 
513     /**
514      * Returns the `Cache-Control` header field.
515      *
516      * @return Headers\CacheControl
517      */
518     protected function get_cache_control()
519     {
520         return $this->headers['Cache-Control'];
521     }
522 
523     /**
524      * Sets the response's time-to-live for shared caches.
525      *
526      * This method adjusts the Cache-Control/s-maxage directive.
527      *
528      * @param int $seconds The number of seconds.
529      */
530     protected function set_ttl($seconds)
531     {
532         $this->cache_control->s_maxage = $this->age + $seconds;
533     }
534 
535     /**
536      * Returns the response's time-to-live in seconds.
537      *
538      * When the responses TTL is <= 0, the response may not be served from cache without first
539      * re-validating with the origin.
540      *
541      * @return int|null The number of seconds to live, or `null` is no freshness information
542      * is present.
543      */
544     protected function get_ttl()
545     {
546         $max_age = $this->cache_control->max_age;
547 
548         if ($max_age)
549         {
550             return $max_age - $this->age;
551         }
552 
553         return null;
554     }
555 
556     /**
557      * Checks that the response includes header fields that can be used to validate the response
558      * with the origin server using a conditional GET request.
559      *
560      * @return boolean
561      */
562     protected function get_is_validateable()
563     {
564         return !$this->headers['Last-Modified']->is_empty || $this->headers['ETag'];
565     }
566 
567     /**
568      * Checks that the response is worth caching under any circumstance.
569      *
570      * Responses marked _private_ with an explicit `Cache-Control` directive are considered
571      * not cacheable.
572      *
573      * Responses with neither a freshness lifetime (Expires, max-age) nor cache validator
574      * (`Last-Modified`, `ETag`) are considered not cacheable.
575      *
576      * @return boolean
577      */
578     protected function get_is_cacheable()
579     {
580         if (!$this->status->is_cacheable || $this->cache_control->no_store || $this->cache_control->cacheable == 'private')
581         {
582             return false;
583         }
584 
585         return $this->is_validateable || $this->is_fresh;
586     }
587 
588     /**
589      * Checks if the response is fresh.
590      *
591      * @return boolean
592      */
593     protected function get_is_fresh()
594     {
595         return $this->ttl > 0;
596     }
597 }
598 
ICanBoogie/HTTP v2.4.0 API documentation generated by ApiGen