<?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 OutOfBoundsException;
use RuntimeException;
use ArrayIterator;
use IteratorAggregate;
use Countable;
use ArrayAccess;
/**
* Encapsulates any Response or Request headers.
*/
class Headers implements IteratorAggregate, Countable, ArrayAccess
{
/**
* @var array Holds all the headers
*/
protected $headers = [];
/**
* Takes a string of properly formatted HTTP headers and parses it
* out and returns a valid Headers object. This can be used to parse
* raw response headers and the resulting object be sent to the
* Response or Request classes.
*
* @static
* @param string The headers string
* @return Headers The new Headers object
* @throws RuntimeException
*/
public static function fromString($string)
{
$headers = new static();
$currentHeader = [];
foreach (explode("\r\n", $string) as $line) {
// Parse a valid header line.
if (preg_match('/^(?P<label>[a-zA-Z0-9_-]+):\s*(?P<value>.*)$/', $line, $match)) {
if ( ! empty($currentHeader)) {
$headers->set($currentHeader['label'], $currentHeader['value'], false);
}
$currentHeader = ['label' => $match['label'], 'value' => $match['value']];
}
// If we are in a header, but there it doesn't match a new header line,
// then it must be a continuation of the current header.
elseif ( ! empty($currentHeader) and preg_match('/^\s+.*$/', $line, $match)) {
$currentHeader['value'] .= trim($line);
}
// A blank line indicates the end of the headers.
elseif (preg_match('/^\s*$/', $line)) {
break;
} else {
throw new RuntimeException(sprintf('"%s" is not a valid header.', $line));
}
}
// Finish up by writing the open header if there is one.
if ( ! empty($currentHeader)) {
$headers->set($currentHeader['label'], $currentHeader['value'], false);
}
return $headers;
}
/**
* Sets up with Headers object with the given headers.
*
* @param array Headers to initialize with.
*/
public function __construct(array $headers = [])
{
if ( ! empty($headers)) {
$this->add($headers);
}
}
/**
* Returns all of the headers.
*
* @return array The headers.
*/
public function all()
{
return $this->headers;
}
/**
* Sets an array of headers.
*
* @param array The headers to add.
* @return Headers
*/
public function add(array $headers)
{
foreach ($headers as $label => $values) {
$this->set($label, $values);
}
return $this;
}
/**
* Sets the given header to the given value. It optionally accepts an array
* of values, which will send multiple headers of the same name with the
* given values when the headers are sent.
*
* @param string $label The name of the header.
* @param mixed $value The value(s) of the header.
* @param bool $replaceExisting Whether to replace the given header, or append.
* @return Headers The Headers object.
*/
public function set($label, $value = null, $replaceExisting = true)
{
$label = $this->cleanLabel($label);
$value = $this->cleanValue($value);
if ($replaceExisting or ! array_key_exists($label, $this->headers)) {
$this->headers[$label] = is_array($value) ? $value : [$value];
} else {
array_push($this->headers[$label], $value);
}
return $this;
}
/**
* Gets the given header if it exists.
*
* @param mixed The Header label.
* @param bool Get only the first header value.
* @return mixeed The header value.
* @throws OutOfBoundsException
*/
public function get($label, $firstOnly = true)
{
$label = $this->cleanLabel($label);
if ( ! array_key_exists($label, $this->headers)) {
throw new OutOfBoundsException(sprintf('%s is not a valid offset.', $label));
}
return $firstOnly ? $this->headers[$label][0] : $this->headers[$label];
}
/**
* Checks if the given header exists.
*
* @param mixed The Header label.
* @return bool Whether it exists.
*/
public function has($label)
{
$label = $this->cleanLabel($label);
return array_key_exists($label, $this->headers);
}
/**
* Removes the given header.
*
* @param mixed The header to remove.
* @return Headers
*/
public function remove($label)
{
$label = $this->cleanLabel($label);
unset($this->headers[$label]);
return $this;
}
/**
* Returns all of the headers in string form.
*
* @return string The header string.
*/
public function __toString()
{
if (count($this->headers) === 0) {
return '';
}
$string = '';
foreach ($this->headers as $label => $values) {
foreach ($values as $value) {
$string .= $label.': '.$value."\r\n";
}
}
return $string;
}
/**
* Returns an ArrayIterator for the headers.
*
* @return ArrayIterator An ArrayIterator instance.
*/
public function getIterator()
{
return new ArrayIterator($this->headers);
}
/**
* Gets the number of headers.
*
* @return int The number of headers.
*/
public function count()
{
return count($this->headers);
}
/**
* Checks if the given header exists.
*
* @param mixed The Header label.
* @return bool Whether it exists.
*/
public function offsetExists($label)
{
return $this->has($label);
}
/**
* Gets the given header if it exists.
*
* @param mixed The Header label.
* @return mixeed The header value.
* @throws OutOfBoundsException
*/
public function offsetGet($label)
{
return $this->get($label);
}
/**
* Sets the given header.
*
* @param string The header label.
* @param mixed The header value.
* @return void
*/
public function offsetSet($label, $value)
{
$this->set($label, $value);
}
/**
* Removes the given header.
*
* @param mixed The header to remove.
* @return void
*/
public function offsetUnset($label)
{
$this->remove($label);
}
/**
* Sanitizes, then Normalizes the given header label.
*
* @param string The header label.
* @return string The clean label.
*/
protected function cleanLabel($label)
{
// Sanitize the label
$label = preg_replace('/[^a-zA-Z0-9_-]/', '', $label);
// Normalize the label
$label = ucwords(strtolower(str_replace(['_', '-'], ' ', $label)));
return str_replace(' ', '-', $label);
}
/**
* Removes all line breaks from the header value.
*
* @param string The header value.
* @return string The clean value.
*/
protected function cleanValue($value)
{
return str_replace(["\r", "\n"], '', $value);
}
}