<?php
/**
* Part of the Fleck HTTP Component
*
* @author Dan Horrigan <hide@address.com>
* @copyright 2012 Dan Horrigan
* @license http://opensource.org/licenses/mit-license.php MIT License
* @version 1.0-dev
*/
namespace Fleck\Http;
use RuntimeException;
use InvalidArgumentException;
use OutOfBoundsException;
/**
* The Response class allows you to easily create an HTTP response which you
* can then send to the browser, or use however you like.
*/
class Response
{
/**
* @var array All valid HTTP response codes.
* @static
*/
protected static $statusCodes = [
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status',
208 => 'Already Reported',
226 => 'IM Used',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
306 => 'Reserved',
307 => 'Temporary Redirect',
308 => 'Permanent Redirect',
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
418 => 'I\'m a celestial teapot',
422 => 'Unprocessable Entity',
423 => 'Locked',
424 => 'Failed Dependency',
425 => 'Reserved for WebDAV advanced collections expired proposal',
426 => 'Upgrade Required',
428 => 'Precondition Required',
429 => 'Too Many Requests',
431 => 'Request Header Fields Too Large',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates (Experimental)',
507 => 'Insufficient Storage',
508 => 'Loop Detected',
510 => 'Not Extended',
511 => 'Network Authentication Required',
];
/**
* @var Headers The Response headers.
*/
public $headers = null;
/**
* @var string The content of the Response.
*/
protected $content = '';
/**
* @var int The HTTP status code of the Response.
*/
protected $status = 200;
/**
* @var string The Content-Type (mime-type) of the Response.
*/
protected $contentType = null;
/**
* @var string The charset that the Response is using.
*/
protected $charset = 'utf-8';
/**
* @var bool Whether the headers have been sent.
*/
protected $headersSent = false;
/**
* @var bool Whether to treat this Response as an attachment.
*/
protected $isAttachment = false;
/**
* @var string The attachment source file.
*/
protected $attachmentSourceFile = null;
/**
* @var string The attachment filename to send to the browser.
*/
protected $attachmentFilename = null;
/**
* Constructs the Response object based on the given argument. The $arg
* argument can either be an array of configuration options or a string
* containing the Responses content.
*
* When sending a config array, the only valid options are options that
* have a set*() method.
*
* @param string|array Either the content string or an options array.
* @return void
* @throws OutOfBoundsException
* @throws InvalidArgumentException
*/
public function __construct($arg = null)
{
if (is_array($arg)) {
foreach ($arg as $key => $value) {
$setterName = 'set'.ucfirst($key);
if (method_exists($this, $setterName)) {
$this->$setterName($value);
} else {
throw new OutOfBoundsException(sprintf('"%s" is not a valid property.', $key));
}
}
} elseif (is_string($arg)) {
$this->content = $arg;
} elseif ($arg !== null) {
throw new InvalidArgumentException('Array or String expected.');
}
if ($this->headers === null) {
$this->setHeaders(new Headers());
}
}
/**
* Sets the headers.
*
* @param array $headers The headers to set.
* @param bool $replaceExisting Whether to replace individual headers, or append.
* @return Fleck\Http\Response The Response object.
* @throws InvalidArgumentException
*/
public function setHeaders($headers)
{
if (is_array($headers)) {
$this->headers = new Headers($headers);
} elseif ($headers instanceof Headers) {
$this->headers = $headers;
} else {
throw new InvalidArgumentException('setHeaders must be sent an array or an instance of Fleck\Http\Headers.');
}
return $this;
}
/**
* Sends all of the registered headers to the browser. Builds any missing, but
* necessary, headers (e.g. Content-Type) prior to sending them.
*
* @return Fleck\Http\Response The Response object.
* @throws RuntimeException
*/
public function sendHeaders()
{
if ($this->headersSent or headers_sent()) {
throw new RuntimeException('The HTTP headers have already been sent.');
}
http_response_code($this->getStatus());
if ($this->contentType === null and ! $this->isAttachment) {
$this->contentType = 'text/html';
} elseif ($this->isAttachment) {
if ($this->attachmentSourceFile === null) {
throw new RuntimeException('You must set the attachment Source File when the Request is set as an Attachment.');
}
$finfo = finfo_open(FILEINFO_MIME);
$mimeType = finfo_file($finfo, $this->attachmentSourceFile);
finfo_close($finfo);
if ($mimeType === false) {
$mimeType = 'application/octet-stream';
}
$this->headers->set('Content-Type', $mimeType);
if ($this->attachmentFilename === null) {
$this->attachmentFilename = basename($this->attachmentSourceFile);
}
$this->headers->set('Content-Disposition', 'attachment; filename="'.$this->attachmentFilename.'"');
}
if (! $this->headers->has('Content-Type')) {
$this->headers->set('Content-Type', $this->contentType.'; charset='.$this->charset);
}
foreach ($this->headers as $name => $values) {
foreach ($values as $value) {
header(sprintf('%s: %s', $name, $value));
}
}
$this->headersSent = true;
return $this;
}
/**
* Gets the Response's content.
*
* @return string The Response content.
*/
public function getContent()
{
return $this->content;
}
/**
* Sets the Response's content.
*
* @param string The content of the Response.
* @return Fleck\Http\Response The Fleck\Http\Response object.
*/
public function setContent($content = '')
{
$this->content = $content;
return $this;
}
/**
* Sends the Response content to the browser either via echo or readfile()
* depending on if the response is an attachment.
*
* @return Fleck\Http\Response The Fleck\Http\Response object.
*/
public function sendContent()
{
if ($this->isAttachment) {
if ( ! $this->headersSent) {
throw new RuntimeException('You must send the headers before sending the content when the Response is an attachment.');
}
readfile($this->attachmentSourceFile);
} else {
echo $this->getContent();
}
return $this;
}
/**
* Gets the Response's Status code. You can optionally have it returned
* as the full status string (e.g. "200 OK").
*
* @param bool Whether to return the full status string.
* @return string|int
*/
public function getStatus($asString = false)
{
if ($asString) {
return $this->status.' '.self::$statusCodes[$this->status];
}
return $this->status;
}
/**
* Sets the Response's status code to the given code.
*
* @param int The status code to set.
* @return Fleck\Http\Response The Fleck\Http\Response object.
* @throws OutOfBoundsException
*/
public function setStatus($status)
{
if (! array_key_exists($status, self::$statusCodes)) {
throw new OutOfBoundsException(sprintf('%d is not a valid HTTP status code.', $status));
}
$this->status = $status;
return $this;
}
/**
* Sets whether to treat this Response as an attachment (i.e. download it
* as a file).
*
* @param bool Whether to treat the Response as an attachment.
* @return Fleck\Http\Response The Fleck\Http\Response object.
*/
public function setAttachment($isAttachment = true)
{
$this->isAttachment = $isAttachment;
return $this;
}
/**
* Sets the attachment source file name. This is the file that is to be
* streamed to the browser.
*
* @param string The attachment source file name.
* @return Fleck\Http\Response The Fleck\Http\Response object.
* @throws InvalidArgumentException
*/
public function setAttachmentSourceFile($file)
{
if (! is_file($file) or ! is_readable($file)) {
throw new InvalidArgumentException(sprintf('"%s" is not a readable file.', $file));
}
$this->attachmentSourceFile = $file;
return $this;
}
/**
* Set the attachement file name that will be sent to the browser.
*
* @param string The attachment file name.
* @return Fleck\Http\Response The Fleck\Http\Response object.
*/
public function setAttachmentFilename($filename)
{
$this->attachmentFilename = $filename;
return $this;
}
/**
* A shortcut for sending the headers then the content.
* This is the same as calling $r->sendHeaders()->sendContent();
*
* @return Fleck\Http\Response The Fleck\Http\Response object.
*/
public function send()
{
return $this->sendHeaders()->sendContent();
}
/**
* Magic method for when the Response is cast as a string.
*
* @return string The Response content.
*/
public function __toString()
{
return $this->getContent();
}
}