diff --git a/build/logs/clover.xml b/build/logs/clover.xml new file mode 100644 index 0000000..be5d2d5 --- /dev/null +++ b/build/logs/clover.xml @@ -0,0 +1,1033 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/composer.json b/composer.json index 905b3f6..566205c 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ } ], "require": { - "php": "^5.5 || ^7.0" + "php": "^5.5 || ^7.0", + "psr/http-message": "^1.0" }, "autoload": { "psr-4": { @@ -22,7 +23,7 @@ }, "autoload-dev": { "psr-4": { - "Meek\\Http\\Tests\\": "tests/" + "Meek\\Http\\": "tests/" } } } diff --git a/composer.lock b/composer.lock index 85fdabb..c131715 100644 --- a/composer.lock +++ b/composer.lock @@ -4,22 +4,72 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "e4ff31f88f296def832bcba893987421", - "content-hash": "43eeb7252a4843aeb62e9437eccbb9fd", - "packages": [], + "hash": "816ebaa9f67650dd19dc156d64aea6cc", + "content-hash": "5101cd47e42b08eebfa86ecf5291954b", + "packages": [ + { + "name": "psr/http-message", + "version": "1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "reference": "85d63699f0dbedb190bbd4b0d2b9dc707ea4c298", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2015-05-04 20:22:00" + } + ], "packages-dev": [ { "name": "codeclimate/php-test-reporter", - "version": "v0.3.0", + "version": "v0.3.2", "source": { "type": "git", "url": "https://github.com/codeclimate/php-test-reporter.git", - "reference": "770e5a274608505e9c59f4699ca26f6840acbfa4" + "reference": "3a2d3ebdc1df5acf372458c15041af240a6fc016" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeclimate/php-test-reporter/zipball/770e5a274608505e9c59f4699ca26f6840acbfa4", - "reference": "770e5a274608505e9c59f4699ca26f6840acbfa4", + "url": "https://api.github.com/repos/codeclimate/php-test-reporter/zipball/3a2d3ebdc1df5acf372458c15041af240a6fc016", + "reference": "3a2d3ebdc1df5acf372458c15041af240a6fc016", "shasum": "" }, "require": { @@ -38,7 +88,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "0.2.x-dev" + "dev-master": "0.3.x-dev" } }, "autoload": { @@ -64,7 +114,7 @@ "codeclimate", "coverage" ], - "time": "2016-02-16 14:55:26" + "time": "2016-04-19 16:54:33" }, { "name": "doctrine/instantiator", diff --git a/example/FictionalRouter.php b/example/FictionalRouter.php deleted file mode 100644 index fc52531..0000000 --- a/example/FictionalRouter.php +++ /dev/null @@ -1,14 +0,0 @@ -getBody()->write($body); + + return $response->prepare(get_current_request())->send(); +} diff --git a/example/index.php b/example/index.php index 391c9d2..630535e 100644 --- a/example/index.php +++ b/example/index.php @@ -6,69 +6,15 @@ // include external libraries require_once '../vendor/autoload.php'; -require_once 'FictionalRouter.php'; - -// use statements -use Meek\Http\Request; -use Meek\Http\Session\PdoStorageDriver; -use Meek\Http\Session; -use Meek\Http\Response; -use Meek\Http\RedirectedResponse; -use Meek\Http\Exception as HttpException; - -// get the current request -$request = Request::createFromGlobals(); - -// manipulate the current URI -$uri = $request->getUri(); -$uri->setPath(substr($uri->getPath(), strlen('/meek-http'))); - -// initialize a session -$dbh = new PDO('mysql:host=localhost;dbname=test', 'root', '', [ - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8' -]); -$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - -$handler = new PdoStorageDriver($dbh); -$session = new Session($handler); - -$request->setSession($session); -$request->session->start(); - -// setup and initalize our application's routes -$router = new FictionalRouter(); - -// basic usage -$router->map('GET', '/', function ($request) { - $username = $request->session->get('username', 'World'); - - return new Response(sprintf('Hello, %s!', $username)); -}); - -// working with headers and redirections -$router->map('GET', '/login', function ($request) { - $username = $request->server->get('PHP_AUTH_USER'); - $password = $request->server->get('PHP_AUTH_PW'); - - if ($username === 'admin' && $password === 'password') { - $request->session->set('username', $username); - return new RedirectedResponse('/meek-http/account'); - } - - return new Response('Please sign in.', 401, [ - 'WWW-Authenticate' => sprintf('Basic realm=%s', 'site_login') - ]); -}); - -// working with JSON responses -$router->map('GET', '/api/v1', function () { - $user = new stdClass(); - $user->name = 'Nathan'; - $user->dob = '24-02-1991'; - - return new Meek\Http\JsonResponse($user); -}); - -// save session to store and dispatch request -$request->session->save(); -$router->dispatch($request); +require_once 'functions.php'; + +// dispatch a response based on the current request +send_response(' + + + + + + + +'); diff --git a/src/Collections/FileList.php b/src/Collections/FileList.php deleted file mode 100644 index 71ee029..0000000 --- a/src/Collections/FileList.php +++ /dev/null @@ -1,10 +0,0 @@ -all() as $key => $value) { - if (static::hasPrefix($key, static::$headerPrefix)) { - $headers[substr($key, strlen(static::$headerPrefix))] = $value; - } else if (in_array($key, static::$nonPrefixedHeaders)) { - $headers[$key] = $value; - } - } - - return $headers; - } - - protected static function hasPrefix($string, $prefix) - { - return strpos($string, $prefix) === 0; - } -} diff --git a/src/Cookie.php b/src/Cookie.php index b8a766f..018b392 100644 --- a/src/Cookie.php +++ b/src/Cookie.php @@ -6,56 +6,127 @@ use DateTime; use DateTimeInterface; +/** + * A simple class for working with HTTP "cookies". + * + * @see https://tools.ietf.org/html/rfc6265 + * @version 0.1.0 + * @author Nathan Bishop (nbish11) + * @copyright 2016 Nathan Bishop + * @license MIT + */ class Cookie { + /** + * [$name description] + * + * @var string + */ protected $name; + + /** + * [$value description] + * + * @var string + */ protected $value; + + /** + * [$expire description] + * + * @var integer + */ protected $expire; + + /** + * [$path description] + * + * @var string + */ protected $path; + + /** + * [$domain description] + * + * @var string + */ protected $domain; + + /** + * [$secure description] + * + * @var boolean + */ protected $secure; + + /** + * [$httpOnly description] + * + * @var boolean + */ protected $httpOnly; - - public function __construct( - $name, - $value = '', - $expire = 0, - $path = '', - $domain = '', - $secure = false, - $httpOnly = true - ) { + + /** + * [__construct description] + * + * @param string $name [description] + * @param string $value [description] + * @param integer $expire [description] + * @param string $path [description] + * @param string $domain [description] + */ + public function __construct($name, $value = '', $expire = 0, $path = '', $domain = '') + { $this->setName($name); $this->setValue($value); $this->setExpire($expire); $this->setPath($path); $this->setDomain($domain); - $this->setSecure($secure); - $this->setHttpOnly($httpOnly); + $this->setSecure(false); + $this->setHttpOnly(true); } + /** + * [setName description] + * + * @param string $name [description] + * + * @return self + */ public function setName($name) { + if (empty($name)) { + throw new InvalidArgumentException('The cookie name cannot be empty.'); + } + if (preg_match("/[=,; \t\r\n\013\014]/", $name)) { throw new InvalidArgumentException( sprintf('The cookie name "%s" contains invalid characters.', $name) ); } - if (empty($name)) { - throw new InvalidArgumentException('The cookie name cannot be empty.'); - } - $this->name = $name; return $this; } + /** + * [getName description] + * + * @return string [description] + */ public function getName() { return $this->name; } + /** + * [setValue description] + * + * @param string $value [description] + * + * @return self + */ public function setValue($value) { $this->value = $value; @@ -63,21 +134,35 @@ public function setValue($value) return $this; } + /** + * [getValue description] + * + * @return string [description] + */ public function getValue() { return $this->value; } + /** + * [setExpire description] + * + * @param string|DateTimeInterface|integer $expire [description] + * + * @return self + */ public function setExpire($expire) { - if ($expire instanceof DateTime || $expire instanceof DateTimeInterface) { + if (is_string($expire)) { + $expire = new DateTime($expire); + } + + if ($expire instanceof DateTimeInterface) { $expire = $expire->format('U'); - } else if (!is_numeric($expire)) { - $expire = strtotime($expire); + } - if ($expire === false || $expire === -1) { - throw new InvalidArgumentException('The cookie expiration time is not valid.'); - } + if ($expire === -1 || !is_integer($expire)) { + throw new InvalidArgumentException('The cookie expiration time is not valid.'); } $this->expire = $expire; @@ -85,11 +170,23 @@ public function setExpire($expire) return $this; } + /** + * [getExpire description] + * + * @return integer [description] + */ public function getExpire() { return $this->expire; } + /** + * [setPath description] + * + * @param string $path [description] + * + * @return self + */ public function setPath($path) { $this->path = empty($path) ? '/' : $path; @@ -97,11 +194,23 @@ public function setPath($path) return $this; } + /** + * [getPath description] + * + * @return string [description] + */ public function getPath() { return $this->path; } + /** + * [setDomain description] + * + * @param string $domain [description] + * + * @return self + */ public function setDomain($domain) { $this->domain = $domain; @@ -109,11 +218,23 @@ public function setDomain($domain) return $this; } + /** + * [getDomain description] + * + * @return string [description] + */ public function getDomain() { return $this->domain; } + /** + * [setSecure description] + * + * @param boolean $secure [description] + * + * @return self + */ public function setSecure($secure) { $this->secure = (boolean) $secure; @@ -121,6 +242,13 @@ public function setSecure($secure) return $this; } + /** + * [setHttpOnly description] + * + * @param boolean $httpOnly [description] + * + * @return self + */ public function setHttpOnly($httpOnly) { $this->httpOnly = (boolean) $httpOnly; @@ -128,16 +256,31 @@ public function setHttpOnly($httpOnly) return $this; } + /** + * [isSecure description] + * + * @return boolean [description] + */ public function isSecure() { return $this->secure; } + /** + * [isHttpOnly description] + * + * @return boolean [description] + */ public function isHttpOnly() { return $this->httpOnly; } + /** + * [__toString description] + * + * @return string [description] + */ public function __toString() { $output = ''; @@ -146,7 +289,7 @@ public function __toString() $output .= urlencode($this->getName()) . '='; if (empty($this->getValue())) { - $output .= 'deleted; expires=' . gmdate('D, d-M-Y H:i:s T', time() - 31536001); + $output .= 'deleted; expires=' . gmdate('D, d-M-Y H:i:s T', time() + 1); } else { $output .= urlencode($this->getValue()); diff --git a/src/CookieCollection.php b/src/CookieCollection.php deleted file mode 100644 index 3da52a0..0000000 --- a/src/CookieCollection.php +++ /dev/null @@ -1,34 +0,0 @@ -replace($cookies); - } - - public function set($key, $value) - { - if (!$value instanceof Cookie) { - $value = new Cookie($key, $value); - } - - return parent::set($key, $value); - } - - public function replace(array $cookies) - { - $this->clear(); - - foreach ($cookies as $key => $value) { - $this->set($key, $value); - } - - return $this; - } -} diff --git a/src/DataCollection.php b/src/DataCollection.php deleted file mode 100644 index 89da115..0000000 --- a/src/DataCollection.php +++ /dev/null @@ -1,150 +0,0 @@ -data = $data; - } - - public function get($key, $default = null) - { - if ($this->exists($key)) { - return $this->data[$key]; - } - - return $default; - } - - public function set($key, $value) - { - $this->data[$key] = $value; - - return $this; - } - - public function exists($key) - { - return array_key_exists($key, $this->data); - } - - public function remove($key) - { - if ($this->exists($key)) { - unset($this->data[$key]); - } - - return $this; - } - - public function clear() - { - $this->data = []; - - return $this; - } - - public function replace(array $data) - { - $this->data = $data; - - return $this; - } - - public function merge(array $data, $hard = false) - { - if ($hard) { - $this->data = array_replace($this->data, $data); - } else { - $this->data = array_merge($this->data, $data); - } - - return $this; - } - - public function all() - { - return $this->data; - } - - public function keys() - { - return array_keys($this->data); - } - - public function values() - { - return array_values($this->data); - } - - public function map($callback, $userData = null) - { - array_walk_recursive($this->data, $callback, $userdata); - - return $this; - } - - public function isEmpty() - { - return empty($this->data); - } - - public function __get($key) - { - return $this->get($key); - } - - public function __set($key, $value) - { - $this->set($key, $value); - } - - public function __isset($key) - { - return $this->exists($key); - } - - public function __unset($key) - { - $this->remove($key); - } - - public function offsetGet($key) - { - return $this->get($key); - } - - public function offsetSet($key, $value) - { - $this->set($key, $value); - } - - public function offsetExists($key) - { - return $this->exists($key); - } - - public function offsetUnset($key) - { - $this->remove($key); - } - - public function getIterator() - { - return new ArrayIterator($this->data); - } - - public function count() - { - return count($this->data); - } -} diff --git a/src/FileResponse.php b/src/FileResponse.php deleted file mode 100644 index fcbec74..0000000 --- a/src/FileResponse.php +++ /dev/null @@ -1,52 +0,0 @@ -setPath($path); - } - - public function open() - { - - } - - public function close() - { - - } - - public function setPath($path) - { - if (!file_exists($path)) { - throw new FileNotFoundException(''); - } - - $this->path = $path; - $this->headers['Content-Type'] = finfo_file(finfo_open(FILEINFO_MIME_TYPE), $path); - $this->headers['Content-Length'] = filesize($path); - $this->headers['Content-Disposition'] = 'attachment; filename="' . basename($path) . '"'; - - return $this; - } - - public function send() - { - parent::send(); - readfile($this->path); - - return $this; - } -} diff --git a/src/HeaderCollection.php b/src/HeaderCollection.php deleted file mode 100644 index e4f0be8..0000000 --- a/src/HeaderCollection.php +++ /dev/null @@ -1,131 +0,0 @@ -setNormalization($normalization); - $this->replace($headers); - } - - public function get($key, $default = null) - { - $key = $this->normalize($key); - - return parent::get($key, $default); - } - - public function set($key, $value) - { - $key = $this->normalize($key); - - return parent::set($key, $value); - } - - public function exists($key) - { - $key = $this->normalize($key); - - return parent::exists($key); - } - - public function remove($key) - { - $key = $this->normalize($key); - - return parent::remove($key); - } - - public function replace(array $headers) - { - $this->clear(); - - foreach ($headers as $key => $value) { - $this->set($key, $value); - } - - return $this; - } - - public function getNormalization() - { - return $this->normalization; - } - - public function setNormalization($normalization) - { - $this->normalization = (integer) $normalization; - - return $this; - } - - public function send() - { - if (!headers_sent()) { - foreach ($this->all() as $key => $value) { - header(sprintf('%s: %s', $key, $value)); - } - } - - return $this; - } - - public function __toString() - { - $output = ''; - - foreach ($this->all() as $key => $value) { - $output .= sprintf("%s: %s\r\n", $key, $value); - } - - return $output; - } - - public function __invoke() - { - return $this->send(); - } - - protected function normalize($key) - { - if ($this->normalization & static::NORMALIZE_TRIM) { - $key = trim($key); - } - - if ($this->normalization & static::NORMALIZE_DELIMITERS) { - $key = str_replace([' ', '_'], '-', $key); - } - - if ($this->normalization & static::NORMALIZE_CASE) { - $key = strtolower($key); - } - - if ($this->normalization & static::NORMALIZE_CANONICAL) { - $key = static::canonicalize($key); - } - - return $key; - } - - protected static function canonicalize($key) - { - return implode('-', array_map('ucfirst', explode('-', $key))); - } -} diff --git a/src/Message.php b/src/Message.php new file mode 100644 index 0000000..84bb98b --- /dev/null +++ b/src/Message.php @@ -0,0 +1,225 @@ + + * @copyright Copyright (c) 2016, Nathan Bishop + * @package Meek\Http + * @license MIT + */ +trait Message +{ + /** + * The protocol version used by the request/response. + * + * @var string + */ + private $version = '1.1'; + + /** + * The headers used by the request/response, but with the + * header names formatted to lower case. + * + * @var array + */ + private $headers = []; + + /** + * Mapping of normalized header names to original, user + * provided header names; where key is the normalized header + * name and value was the original header name. + * + * @var array + */ + private $originalHeaderNames = []; + + /** + * The request/response body. + * + * @var PsrHttpStream + */ + private $body; + + /** + * {@inheritdoc} + */ + public function getProtocolVersion() + { + return $this->version; + } + + /** + * {@inheritdoc} + */ + public function withProtocolVersion($version) + { + if (!in_array($version, ['1.0', '1.1', '2'], true)) { + throw new InvalidArgumentException('A valid protocol version was not provided.'); + } + + $instance = clone $this; + $instance->version = $version; + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * {@inheritdoc} + */ + public function hasHeader($name) + { + return array_key_exists(strtolower($name), $this->originalHeaderNames); + } + + /** + * {@inheritdoc} + */ + public function getHeader($name) + { + if (!$this->hasHeader($name)) { + return []; + } + + $original = $this->originalHeaderNames[strtolower($name)]; + $value = $this->headers[$original]; + + return is_array($value) ? $value : [$value]; + } + + /** + * {@inheritdoc} + */ + public function getHeaderLine($name) + { + return implode(', ', $this->getHeader($name)); + } + + /** + * {@inheritdoc} + */ + public function withHeader($name, $value) + { + if (is_string($value)) { + $value = [$value]; + } + + if (!is_array($value) || count(array_filter($value, 'is_string')) !== count($value)) { + throw new InvalidArgumentException('Header value must be a string or an array of strings.'); + } + + $normalized = strtolower($name); + $instance = clone $this; + + // remove the original header name and set to the new one + if ($instance->hasHeader($name)) { + unset($instance->headers[$instance->originalHeaderNames[$normalized]]); + } + + $instance->headers[$name] = $value; + $instance->originalHeaderNames[$normalized] = $name; + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function withAddedHeader($name, $value) + { + if (is_string($value)) { + $value = [$value]; + } + + if (!is_array($value) || count(array_filter($value, 'is_string')) !== count($value)) { + throw new InvalidArgumentException('Header value must be a string or an array of strings.'); + } + + if (!$this->hasHeader($name)) { + return $this->withHeader($name, $value); + } + + $original = $this->originalHeaderNames[strtolower($name)]; + $instance = clone $this; + $instance->headers[$original] = array_merge($this->headers[$original], $value); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function withoutHeader($name) + { + if (!$this->hasHeader($name)) { + return clone $this; + } + + $name = strtolower($name); + $instance = clone $this; + unset($instance->headers[$this->originalHeaderNames[$name]]); + unset($instance->originalHeaderNames[$name]); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getBody() + { + return $this->body; + } + + /** + * {@inheritdoc} + */ + public function withBody(PsrHttpStream $body) + { + $instance = clone $this; + $instance->body = $body; + + return $this; + } + + /** + * Allows for bulk-setting the headers without worring about + * immutability. Should only be used during first time class + * instantiation. + * + * @param array $headers An associative array of the message's + * headers. Each key must be a header + * name, and each value can either be a + * string for the header or an array of + * strings for the header. + */ + private function setHeaders(array $headers) + { + $this->headers = []; + $this->originalHeaderNames = []; + + foreach ($headers as $name => $values) { + if (!is_array($values)) { + $values = [$values]; + } + + foreach ($values as $value) { + $this->headers[$name][] = $value; + $this->originalHeaderNames[strtolower($name)] = $name; + } + } + } +} diff --git a/src/Request.php b/src/Request.php index c41605b..5fe94f7 100644 --- a/src/Request.php +++ b/src/Request.php @@ -2,6 +2,9 @@ namespace Meek\Http; +use Meek\Http\Message; +use Psr\Http\Message\RequestInterface as PsrHttpRequest; +use Psr\Http\Message\UriInterface as PsrHttpUri; use Meek\Http\DataCollection; use Meek\Http\CookieCollection; use Meek\Http\Collections\ServerData as ServerDataCollection; @@ -9,245 +12,95 @@ use Meek\Http\Collections\FileList as FileListCollection; use Meek\Http\Uri; use Meek\Http\Session; +use InvalidArgumentException; -class Request +class Request implements PsrHttpRequest { - private $get; - private $post; - private $cookies; - public $server; - private $headers; - private $files; - private $body; - private $attributes; - private $uri; - public $session; + use Message; - /** - * [__construct description] - * @param array $get [description] - * @param array $post [description] - * @param array $cookies [description] - * @param array $server [description] - * @param array $files [description] - * @param string $body [description] - */ - public function __construct( - array $get = [], - array $post = [], - array $cookies = [], - array $server = [], - array $files = [], - $body = null - ) { - $this->get = new DataCollection($get); - $this->post = new DataCollection($post); - $this->cookies = new CookieCollection($cookies); - $this->server = new ServerDataCollection($server); - $this->headers = new HeaderCollection($this->server->getHeaders()); - $this->files = new FileListCollection($files); - $this->body = $body; - $this->attributes = new DataCollection(); - } + protected $uri; + protected $method; + protected $protocol; + protected $requestTarget; - /** - * [create description] - * @param string $uri [description] - * @param string $method [description] - * @param array $cookies [description] - * @param array $files [description] - * @param array $server [description] - * @param string $body [description] - * @return [type] [description] - */ - public static function create( - $uri, - $method = 'GET', - array $cookies = [], - array $files = [], - array $server = [], - $body = null - ) { - - } - - /** - * [createFromGlobals description] - * @return Request [description] - */ - public static function createFromGlobals() + public function __construct($uri, $method = 'GET', $headers = [], $body = '', $protocol = '1.1') { - return new static($_GET, $_POST, $_COOKIE, $_SERVER, $_FILES, null); - } + $this->uri = $uri instanceof $uri ? $uri : new Uri($uri); + $this->method = $method; + $this->setHeaders($headers); + $this->body = $body instanceof PsrHttpStream ? $body : new Stream('php://temp', 'w+'); + $this->protocol = $protocol; - /** - * [getUri description] - * @return Uri [description] - */ - public function getUri() - { - // cache URI - if (is_null($this->uri)) { - $this->uri = Uri::createFromRequest(); - } + // set request target + $path = $this->uri->getPath(); - return $this->uri; - } - - /** - * [getMethod description] - * @return string [description] - */ - public function getMethod() - { - return array_key_exists('REQUEST_METHOD', $_SERVER) ? $_SERVER['REQUEST_METHOD'] : 'GET'; - } - - /** - * [getParam description] - * @param string $key [description] - * @param mixed $default [description] - * @return mixed [description] - */ - public function getParam($key, $default = null) - { - // check query parameters - if (array_key_exists($key, $this->get)) { - return $this->get[$key]; - - // check request - } else if (array_key_exists($key, $this->post)) { - return $this->post[$key]; - - // finally, check user set data - } else if (array_key_exists($key, $this->attributes)) { - return $this->attributes[$key]; + if (empty($path)) { + $path = '/'; } - return $default; - } + $query = $this->uri->getQuery(); - /** - * [setSession description] - * @param Session $session [description] - */ - public function setSession(Session $session) - { - $this->session = $session; - - return $this; - } - - /** - * [getQueryParams description] - * @return DataCollection [description] - */ - public function getQueryParams() - { - return $this->get; - } + if (!empty($query)) { + $path = $path . '?' . $query; + } - /** - * [getRequestParams description] - * @return DataCollection [description] - */ - public function getRequestParams() - { - return $this->post; + $this->requestTarget = $path; } /** - * [getCookies description] - * @return CookieCollection [description] + * {@inheritdoc} */ - public function getCookies() + public function getRequestTarget() { - return $this->cookies; + return $this->requestTarget; } /** - * [getServer description] - * @return ServerCollection [description] + * {@inheritdoc} */ - public function getServer() + public function withRequestTarget($requestTarget) { - return $this->server; - } + $request = clone $this; + $request->requestTarget = $requestTarget; - /** - * [getHeaders description] - * @return HeaderCollection [description] - */ - public function getHeaders() - { - return $this->headers; + return $request; } /** - * [getFiles description] - * @return FileCollection [description] + * {@inheritdoc} */ - public function getFiles() + public function getMethod() { - return $this->files; + return $this->method; } /** - * [getBody description] - * @return [type] [description] + * {@inheritdoc} */ - public function getBody() + public function withMethod($method) { - // cache body - if (is_null($this->body)) { - $this->body = file_get_contents('php://input'); - } - - return $this->body; - } + $request = clone $this; + $request->method = $method; - /** - * [__get description] - * @param [type] $key [description] - * @return [type] [description] - */ - public function __get($key) - { - return $this->__isset($key) ? $this->attributes[$key] : null; + return $request; } /** - * [__set description] - * @param [type] $key [description] - * @param [type] $value [description] + * {@inheritdoc} */ - public function __set($key, $value) + public function getUri() { - if ($key === null) { - throw new InvalidArgumentException('A key was not provided.'); - } - - $this->attributes[$key] = $value; + return $this->uri; } /** - * [__isset description] - * @param [type] $key [description] - * @return boolean [description] + * {@inheritdoc} */ - public function __isset($key) + public function withUri(PsrHttpUri $uri, $preserveHost = false) { - return array_key_exists($key, $this->attributes); - } + $request = clone $this; + $request->$uri = $uri; - /** - * [__unset description] - * @param [type] $key [description] - */ - public function __unset($key) - { - if ($this->__isset($key)) { - unset($this->attributes[$key]); - } + return $request; } } diff --git a/src/Response.php b/src/Response.php index 2a2980a..4141d3d 100644 --- a/src/Response.php +++ b/src/Response.php @@ -2,158 +2,276 @@ namespace Meek\Http; -use Meek\Http\HeaderCollection; +use Psr\Http\Message\ResponseInterface as PsrHttpResponse; +use Psr\Http\Message\StreamInterface as PsrHttpStream; +use Meek\Http\Message; +use Meek\Http\Stream; use Meek\Http\Status; + use Meek\Http\Request; use InvalidArgumentException; +use RuntimeException; +use Psr\Http\Message\ServerRequestInterface as PsrServerRequest; -class Response +class Response implements PsrHttpResponse { - protected $status; - private $sent = false; - private $body; - public $headers; - protected $cookies; - private $protocol = '1.1'; - private $charset = 'utf-8'; - public $session; - - public function __construct($body = '', $status = 200, array $headers = []) - { - $this->headers = new HeaderCollection($headers); - $this->setBody($body); + use Message; + + /** + * [$status description] + * + * @var [type] + */ + private $status; + + /** + * [$charset description] + * + * @var [type] + */ + private $charset; + + /** + * [__construct description] + * + * @param string $body [description] + * @param integer $status [description] + * @param array $headers [description] + */ + public function __construct(PsrHttpStream $body = null, $status = 200, array $headers = []) + { + $this->body = $body ?: new Stream('php://temp', 'wb+'); $this->setStatus($status); + $this->setHeaders($headers); } - public function setBody($body, $contentType = 'text/html') + /** + * {@inheritdoc} + */ + public function getStatusCode() { - if ($body === null) { - throw new InvalidArgumentException('Please provide a body!'); - } - - $this->headers['Content-Type'] = "$contentType; charset=$this->charset"; - $this->body = (string) $body; - - return $this; + return $this->status->getCode(); } - public function setStatus($code, $message = null) + /** + * {@inheritdoc} + */ + public function withStatus($code, $reasonPhrase = '') { - if (!($code instanceof Status)) { - $code = new Status($code, $message); - } else if ($message !== null) { - $code->setMessage($message); - } + $instance = clone $this; + $instance->setStatus($code, $reasonPhrase); - $this->status = $code; - - return $this; + return $instance; } - public function setCharset($charset) + /** + * {@inheritdoc} + */ + public function getReasonPhrase() { - $this->charset = $charset; - - return $this; + return $this->status->getMessage(); } - public function setProtocol($protocol) + /** + * https://github.com/symfony/http-foundation/blob/master/Response.php#L250 + * @param PsrServerRequest $request [description] + * @return self + */ + public function prepare(PsrServerRequest $request) { - $this->protocol = $protocol; + $response = clone $this; - return $this; - } + if ($response->status->isInformational() || in_array($response->getStatusCode(), [204, 304])) { + $response = $respone->withBody(new Stream('php://temp')) + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length'); + } else { + // set content type based on request, if we haven't provided one + if (!$response->hasHeader('Content-Type')) { - public function getStatus() - { - return $this->status; - } + } - public function getBody() - { - return $this->body; - } + // fix content type + $charset = $response->charset ?: 'UTF-8'; + if (!$response->hasHeader('Content-Type')) { + $response = $response->withHeader('Content-Type', sprintf('text/html; charset=%s', $charset)); + } elseif (stripos($response->getHeaderLine('Content-Type'), 'text/') === 0 && + stripos($response->getHeaderLine('Content-Type'), 'charset') === false + ) { + $value = sprintf('%s; charset=%s', $response->getHeaderLine('Content-Type'), $charset); + $response = $response->withHeader('Content-Type', $value); + } - public function prepare(Request $request) - { - return $this; + // fix content length + if ($response->hasHeader('Transfer-Encoding')) { + $response = $response->withoutHeader('Content-Length'); + } + + if (!$response->hasHeader('Content-Length')) { + $response->withHeader('Content-Length', (string) $this->getBody()->getSize()); + } + + // fix HEAD requests + if ($request->getMethod() === 'HEAD') { + // make sure content length is specified + if (!$response->hasHeader('Content-Length')) { + $response = $response->withHeader('Content-Length', $response->getBody()->getSize()); + } + + // body should be empty + $response = $respone->withBody(new Stream('php://temp')); + } + } + + // fix protocol + if ($request->getProtocolVersion() !== '1.0') { + $response = $response->withProtocolVersion('1.1'); + } + + // check if we need to send extra expire info headers + if ($response->getProtocolVersion() === '1.0' && + in_array('no-cache', $response->getHeader('Cache-Control')) + ) { + $response = $response->withAddedHeader('Pragma', 'no-cache') + ->withAddedHeader('Expires', -1); + } + + return $response; } + /** + * [send description] + */ public function send() { - $this->sendHeaders(); - $this->sendBody(); - - if (function_exists('fastcgi_finish_request')) { - fastcgi_finish_request(); - } else if ('cli' !== PHP_SAPI) { - static::closeOutputBuffers(0, true); + if (headers_sent()) { + throw new RuntimeException('Headers have already been sent.'); } - return $this; + $this->sendStatusLine(); + $this->sendHeaders(); + static::flush(); + $this->sendBody(); } + /** + * [__toString description] + * + * @return string [description] + */ public function __toString() { $output = ''; $output .= $this->getHttpStatusLine() . "\r\n"; - $output .= (string) $this->headers; + + foreach (array_keys($this->getHeaders()) as $header) { + $output .= sprintf("%s: %s\r\n", static::normalize($header), $this->getHeaderLine($header)); + } + $output .= "\r\n"; $output .= $this->body; return $output; } + /** + * @see self::send + */ public function __invoke() { $this->send(); } + /** + * [__clone description] + */ public function __clone() { - $this->headers = clone $this->headers; + $this->status = clone $this->status; + $this->body = clone $this->body; } - // borrowed from Symfony's Http component. - public static function closeOutputBuffers($targetLevel, $flush) + protected function getHttpStatusLine() { - $status = ob_get_status(true); - $level = count($status); - // PHP_OUTPUT_HANDLER_* are not defined on HHVM 3.3 - $flags = defined('PHP_OUTPUT_HANDLER_REMOVABLE') ? PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? PHP_OUTPUT_HANDLER_FLUSHABLE : PHP_OUTPUT_HANDLER_CLEANABLE) : -1; - while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || $flags === ($s['flags'] & $flags) : $s['del'])) { - if ($flush) { - ob_end_flush(); - } else { - ob_end_clean(); - } - } + return sprintf('HTTP/%s %s', $this->getProtocolVersion(), $this->status); } - protected function getHttpStatusLine() + /** + * [sendStatusLine description] + */ + protected function sendStatusLine() { - return sprintf('HTTP/%s %s', $this->protocol, $this->status); + header($this->getHttpStatusLine()); } + /** + * [sendHeaders description] + */ protected function sendHeaders() { - header($this->getHttpStatusLine()); + if (!$this->hasHeader('Content-Length')) { + $this->setHeaders([ + 'Content-Length' => $this->getBody()->getSize() + ]); + } + + foreach (array_keys($this->getHeaders()) as $header) { + header(sprintf('%s: %s', static::normalize($header), $this->getHeaderLine($header))); + } + } + + /** + * [sendBody description] + */ + protected function sendBody() + { + echo $this->getBody(); + } - $this->headers->send(); + /** + * [setStatus description] + * + * @param [type] $code [description] + * @param [type] $message [description] + */ + private function setStatus($code, $message = null) + { + if (!($code instanceof Status)) { + $code = new Status($code, $message); + } - $this->sendCookies(); + if ($message !== null) { + $code->setMessage($message); + } - return $this; + $this->status = $code; } - protected function sendCookies() + /** + * [flush description] + * + * @param [type] $level [description] + */ + private static function flush($level = null) { - //$this->cookies->send(); + if ($level === null) { + $level = ob_get_level(); + } + + while (ob_get_level() > $level) { + ob_end_flush(); + } } - protected function sendBody() + /** + * [normalize description] + * + * @param [type] $header [description] + * @return [type] [description] + */ + private static function normalize($header) { - echo $this->body; + return str_replace(' ', '-', ucwords(strtolower(str_replace('-', ' ', $header)))); } } diff --git a/src/JsonResponse.php b/src/Response/Json.php similarity index 100% rename from src/JsonResponse.php rename to src/Response/Json.php diff --git a/src/RedirectedResponse.php b/src/Response/Redirect.php similarity index 100% rename from src/RedirectedResponse.php rename to src/Response/Redirect.php diff --git a/src/ServerRequest.php b/src/ServerRequest.php new file mode 100644 index 0000000..591dcb5 --- /dev/null +++ b/src/ServerRequest.php @@ -0,0 +1,226 @@ +server = $server; + $this->query = []; + $this->post = []; + $this->cookies = []; + $this->files = []; + $this->attributes = []; + + parent::__construct($uri, $method, $headers, $body, $protocol); + } + + /** + * {@inheritdoc} + */ + public function getServerParams() + { + return $this->server; + } + + /** + * {@inheritdoc} + */ + public function getCookieParams() + { + return $this->cookies; + } + + /** + * {@inheritdoc} + */ + public function withCookieParams(array $cookies) + { + $request = clone $this; + + array_merge($request->cookies, $cookies); + + return $request; + } + + /** + * {@inheritdoc} + */ + public function getQueryParams() + { + return $this->query; + } + + /** + * {@inheritdoc} + */ + public function withQueryParams(array $query) + { + $request = clone $this; + + array_merge($request->query, $query); + + return $request; + } + + /** + * {@inheritdoc} + */ + public function getUploadedFiles() + { + return $this->files; + } + + /** + * {@inheritdoc} + */ + public function withUploadedFiles(array $files) + { + $request = clone $this; + + array_merge($request->files, $files); + + return $request; + } + + /** + * {@inheritdoc} + */ + public function getParsedBody() + { + return $this->post; + } + + /** + * {@inheritdoc} + */ + public function withParsedBody($data) + { + $request = clone $this; + + $request->data = $data; + + return $request; + } + + /** + * {@inheritdoc} + */ + public function getAttributes() + { + $this->attributes; + } + + /** + * {@inheritdoc} + */ + public function getAttribute($name, $default = null) + { + return self::arrayGet($this->attributes, $name, $default); + } + + /** + * {@inheritdoc} + */ + public function withAttribute($name, $value) + { + $request = clone $this; + $request->attributes[$name] = $value; + + return $request; + } + + /** + * {@inheritdoc} + */ + public function withoutAttribute($name) + { + $request = clone $this; + + unset($request->attributes[$name]); + + return $request; + } + + /** + * [createFromGlobals description] + * + * @return self + */ + public static function createFromGlobals() + { + $method = self::arrayGet($_SERVER, 'REQUEST_METHOD', 'GET'); + $headers = function_exists('getallheaders') ? getallheaders() : []; + $uri = Uri::createFromRequest(); + $body = new Stream('php://input', 'r+'); + $protocol = str_replace('HTTP/', '', self::arrayGet($_SERVER, 'SERVER_PROTOCOL', '1.1')); + $request = new static($uri, $method, $headers, $body, $protocol, $_SERVER); + + return $request->withCookieParams($_COOKIE) + ->withQueryParams($_GET) + ->withParsedBody(!empty($_POST) ? $_POST : null) + ->withUploadedFiles($_FILES); + } + + private static function arrayGet(array $data, $key, $default = null) + { + return array_key_exists($key, $data) ? $data[$key] : $default; + } +} diff --git a/src/Status.php b/src/Status.php index a266d14..4275268 100644 --- a/src/Status.php +++ b/src/Status.php @@ -108,7 +108,7 @@ public function setCode($code) */ public function getMessage() { - return $this->message; + return (string) $this->message; } /** diff --git a/src/Stream.php b/src/Stream.php new file mode 100644 index 0000000..5dd3277 --- /dev/null +++ b/src/Stream.php @@ -0,0 +1,277 @@ +open($resource, $mode); + } + + /** + * [open description] + * + * @param string|resource $resource [description] + * @param string $mode [description] + */ + public function open($resource, $mode = 'r') + { + if (is_string($resource)) { + $resource = @fopen($resource, $mode); + } + + if (!is_resource($resource) || get_resource_type($resource) !== 'stream') { + throw new InvalidArgumentException('A valid resource URI or a stream resource was not provided.'); + } + + $this->resource = $resource; + } + + /** + * {@inheritdoc} + */ + public function close() + { + if (is_resource($this->resource)) { + $reource = $this->detach(); + fclose($resource); + } + } + + /** + * {@inheritdoc} + */ + public function detach() + { + $resource = $this->resource; + $this->resource = null; + + return $resource; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + if (!is_resource($this->resource)) { + return null; + } + + return fstat($this->resource)['size']; + } + + /** + * {@inheritdoc} + */ + public function tell() + { + $this->requireResource(); + + $result = ftell($this->resource); + + if (!is_integer($result)) { + throw new RuntimeException('An unknown error has occured while trying to tell.'); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function eof() + { + if (!is_resource($this->resource)) { + return true; + } + + return feof($this->resource); + } + + /** + * {@inheritdoc} + */ + public function isSeekable() + { + if (!is_resource($this->resource)) { + return false; + } + + return stream_get_meta_data($this->resource)['seekable']; + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + $this->requireResource(); + + if (!$this->isSeekable()) { + throw new RuntimeException('The resource is not seekable'); + } + + $result = fseek($this->resource, $offset, $whence); + + if ($result !== 0) { + throw new RuntimeException('An unknown error has occured while trying to seek within the stream.'); + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + return $this->seek(0); + } + + /** + * {@inheritdoc} + */ + public function isWritable() + { + if (!is_resource($this->resource)) { + return false; + } + + $mode = stream_get_meta_data($this->resource)['mode']; + + // maybe preg_match?? + // return preg_match('/[xwca\+]{1}/', $mode); + return strstr($mode, 'x') || strstr($mode, 'w') + || strstr($mode, 'c') || strstr($mode, 'a') || strstr($mode, '+'); + } + + /** + * {@inheritdoc} + */ + public function write($string) + { + $this->requireResource(); + + if (!$this->isWritable()) { + throw new RuntimeException('The stream is not writeable.'); + } + + $result = fwrite($this->resource, $string); + + if ($result === false) { + throw new RuntimeException('An unknown error has occured while trying to write to the stream.'); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function isReadable() + { + if (!is_resource($this->resource)) { + return false; + } + + $mode = stream_get_meta_data($this->resource)['mode']; + + return strstr($mode, 'r') || strstr($mode, '+'); + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + $this->requireResource(); + $this->requireReadable(); + + $result = fread($this->resource, $length); + + if ($result === false) { + throw new RuntimeException('An unknown error has occured while trying to read from the stream.'); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + $this->requireReadable(); + + $result = stream_get_contents($this->resource); + + if ($result === false) { + throw new RuntimeException('An unknown error has occured while attempting to get the stream contents.'); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + $metadata = stream_get_meta_data($this->resource); + + if ($key === null) { + return $metadata; + } + + return array_key_exists($key, $metadata) ? $metadata[$key] : null; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + if (!$this->isReadable()) { + return ''; + } + + try { + $this->rewind(); + return $this->getContents(); + } catch (RuntimeException $e) { + return ''; + } + } + + /** + * [requireResource description] + */ + private function requireResource() + { + if (!is_resource($this->resource)) { + throw new RuntimeException('The resource has either been detached or closed.'); + } + } + + /** + * [requireReadable description] + */ + private function requireReadable() + { + if (!$this->isReadable()) { + throw new RuntimeException('The stream is not readable.'); + } + } +} diff --git a/src/UploadedFile.php b/src/UploadedFile.php new file mode 100644 index 0000000..a98e4a6 --- /dev/null +++ b/src/UploadedFile.php @@ -0,0 +1,136 @@ + - * @copyright Copyright (c) 2016, Nathan Bishop - * @package Meek\Http - * @version 0.8.7 - * @license MIT - */ -class Uri +use Psr\Http\Message\UriInterface as PsrHttpUri; + + /** + *A class for manipulating URI's. + * + * @version 0.1.0 + * @author Nathan Bishop (nbish11) + * @copyright 2016 Nathan Bishop + * @license MIT + */ +class Uri implements PsrHttpUri { - private $scheme; - private $userInfo; - private $host; - private $port; - private $path; - private $query; - private $fragment; - - public function __construct($uri = null) - { - if (!is_array($uri)) { - $uri = static::parse((string) $uri); - } - - $parsedUri = array_fill_keys([ - 'scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment' - ], ''); - - $uri = array_merge($parsedUri, $uri); - - $this->setScheme($uri['scheme']); - $this->setUserInfo($uri['user'], $uri['pass']); - $this->setHost($uri['host']); - $this->setPort($uri['port'] === '' ? null : (integer) $uri['port']); - $this->setPath($uri['path']); - $this->setQuery($uri['query']); - $this->setFragment($uri['fragment']); - } - + protected $scheme; + protected $userInfo; + protected $host; + protected $port; + protected $path; + protected $query; + protected $fragment; + + protected static $allowedSchemes = []; + + /** + * {@inheritdoc} + */ public function getScheme() { return (string) $this->scheme; } + /** + * {@inheritdoc} + */ public function getAuthority() { $authority = ''; @@ -64,16 +52,25 @@ public function getAuthority() return $authority; } + /** + * {@inheritdoc} + */ public function getUserInfo() { return (string) $this->userInfo; } + /** + * {@inheritdoc} + */ public function getHost() { return (string) $this->host; } + /** + * {@inheritdoc} + */ public function getPort() { // If a port is present, and it is non-standard for the current scheme, @@ -84,96 +81,217 @@ public function getPort() return null; } - return $this->port; + return (integer) $this->port; } + /** + * {@inheritdoc} + */ public function getPath() { return (string) $this->path; } + /** + * {@inheritdoc} + */ public function getQuery() { return (string) $this->query; } + /** + * {@inheritdoc} + */ public function getFragment() { return (string) $this->fragment; } - public function setScheme($scheme) + /** + * {@inheritdoc} + */ + public function withScheme($scheme) { - $this->scheme = $scheme; + $instance = clone $this; + $instance->scheme = $scheme; - return $this; + return $instance; } - public function setUserInfo($user, $pass = null) + /** + * {@inheritdoc} + */ + public function withUserInfo($user, $pass = null) { + $instance = clone $this; + if ($pass) { $user = $user . ':' . $pass; } - $this->userInfo = $user; + $instance->userInfo = $user; - return $this; + return $instance; } - public function setHost($host) + /** + * {@inheritdoc} + */ + public function withHost($host) { - $this->host = $host; + $instance = clone $this; + $instance->host = $host; - return $this; + return $instance; } - public function setPort($port) + /** + * {@inheritdoc} + */ + public function withPort($port) { - $this->port = $port; + $instance = clone $this; + $instance->port = $port; - return $this; + return $instance; } - public function setPath($path) + /** + * {@inheritdoc} + */ + public function withPath($path) { - $this->path = $path; + $instance = clone $this; + $instance->path = $path; - return $this; + return $instance; } - public function setQuery($query) + /** + * {@inheritdoc} + */ + public function withQuery($query) { - $this->query = $query; + $instance = clone $this; + $instance->query = $query; - return $this; + return $instance; } - public function setFragment($fragment) + /** + * {@inheritdoc} + */ + public function withFragment($fragment) { - $this->fragment = $fragment; + $instance = clone $this; + $instance->fragment = $fragment; - return $this; + return $instance; } - public static function createFromRequest() + /** + * {@inheritdoc} + */ + public function __toString() { - return new static([ - 'scheme' => $_SERVER['REQUEST_SCHEME'], - 'host' => $_SERVER['SERVER_NAME'], - 'port' => $_SERVER['SERVER_PORT'], - 'user' => isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : '', - 'pass' => isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '', - 'path' => strstr($_SERVER['REQUEST_URI'], '?', true) ?: $_SERVER['REQUEST_URI'], - 'query' => $_SERVER['QUERY_STRING'] + return static::build([ + 'scheme' => $this->getScheme(), + 'authority' => $this->getAuthority(), + 'path' => $this->getPath(), + 'query' => $this->getQuery(), + 'fragment' => $this->getFragment() + ]); + } + + /** + * [createFromRequest description] + * + * @param array $server [description] + * @return self [description] + */ + public static function createFromRequest(array $server) + { + return static::createFromArray([ + 'scheme' => $server['REQUEST_SCHEME'], + 'host' => $server['SERVER_NAME'], + 'port' => $server['SERVER_PORT'], + 'user' => isset($server['PHP_AUTH_USER']) ? $server['PHP_AUTH_USER'] : '', + 'pass' => isset($server['PHP_AUTH_PW']) ? $server['PHP_AUTH_PW'] : '', + 'path' => strstr($server['REQUEST_URI'], '?', true) ?: $server['REQUEST_URI'], + 'query' => $server['QUERY_STRING'] ]); } + /** + * [createFromString description] + * + * @param string $uri [description] + * @return self [description] + */ + public static function createFromString($uri) + { + return static::createFromArray(static::parse($uri)); + } + + /** + * [createFromArray description] + * + * @param array $uri [description] + * @return self [description] + */ + public static function createFromArray(array $uri) + { + $uri += [ + 'scheme' => '', + 'user' => '', + 'pass' => '', + 'host' => '', + 'port' => null, + 'query' => '', + 'fragment' => '' + ]; + + return (new static()) + ->withScheme($uri['scheme']) + ->withUserInfo($uri['user'], $uri['pass']) + ->withHost($uri['host']) + ->withPort($uri['port']) + ->withPath($uri['path']) + ->withQuery($uri['query']) + ->withFragment($uri['fragment']); + } + + /** + * [parse description] + * + * @param string $uri [description] + * @return array [description] + */ protected static function parse($uri) { - return parse_url($uri); + $uri = (string) $uri; + + if (!is_string($uri) || empty($uri)) { + throw new InvalidArgumentException('Invalid uri.'); + } + + $uri = parse_url((string) $uri); + + if ($uri === false) { + throw new InvalidArgumentException('Malformed uri.'); + } + + return $uri; } - // http://tools.ietf.org/html/rfc3986#section-5.3 + /** + * [compile description] + * + * @link http://tools.ietf.org/html/rfc3986#section-5.3 + * @param array $parts [description] + * @return string [description] + */ protected static function compile(array $parts) { $result = ''; @@ -203,6 +321,12 @@ protected static function compile(array $parts) return $result; } + /** + * [build description] + * + * @param array $parsedUri [description] + * @return string [description] + */ protected static function build(array $parsedUri) { $uri = ''; @@ -240,15 +364,4 @@ protected static function build(array $parsedUri) return $uri; } - - public function __toString() - { - return static::build([ - 'scheme' => $this->getScheme(), - 'authority' => $this->getAuthority(), - 'path' => $this->getPath(), - 'query' => $this->getQuery(), - 'fragment' => $this->getFragment() - ]); - } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Collections/FileListTest.php b/tests/Collections/FileListTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Collections/ServerDataTest.php b/tests/Collections/ServerDataTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/CookieCollectionTest.php b/tests/CookieCollectionTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/CookieTest.php b/tests/CookieTest.php index e69de29..965e156 100644 --- a/tests/CookieTest.php +++ b/tests/CookieTest.php @@ -0,0 +1,8 @@ +message = $this->getMockForTrait(Message::class); + } + + /** + * @covers Meek\Http\Message::getProtocolVersion + */ + public function testHasDefaultProtocol() + { + $this->assertEquals('1.1', $this->message->getProtocolVersion()); + } + + /** + * @covers Meek\Http\Message::withProtocolVersion + * @dataProvider invalidProtocolVersions + * @expectedException InvalidArgumentException + */ + public function testThrowsErrorWithIncorrectProtocolVersionOrTypes($invalidVersion) + { + $message = $this->message->withProtocolVersion($invalidVersion); + } + + /** + * @covers Meek\Http\Message::getProtocolVersion + * @covers Meek\Http\Message::withProtocolVersion + */ + public function testChangingProtocolKeepsMessageImmuttable() + { + $message = $this->message->withProtocolVersion('1.0'); + + $this->assertNotSame($this->message, $message); + $this->assertEquals('1.1', $this->message->getProtocolVersion()); + $this->assertEquals('1.0', $message->getProtocolVersion()); + } + + /** + * @covers Meek\Http\Message::getHeaders + */ + public function testDefaultsToNoHeaders() + { + $this->assertEmpty($this->message->getHeaders()); + } + + /** + * @covers Meek\Http\Message::hasHeader + * @dataProvider differentCases + */ + public function testHeaderExists($name) + { + $message = $this->message->withHeader('Foo', 'bar'); + + $this->assertTrue($message->hasHeader($name)); + } + + /** + * @covers Meek\Http\Message::hasHeader + * @dataProvider differentCases + */ + public function testHeaderDoesNotExist($name) + { + $this->assertFalse($this->message->hasHeader($name)); + } + + /** + * @covers Meek\Http\Message::getHeader + * @dataProvider differentCases + */ + public function testRetrievingANonExistantHeaderReturnsAnEmptyArray($name) + { + $value = $this->message->getHeader($name); + + $this->assertInternalType('array', $value); + $this->assertEmpty($value); + } + + /** + * @covers Meek\Http\Message::getHeader + * @dataProvider differentCases + */ + public function testRetrievingHeaderReturnsCorrectValue($name) + { + $message = $this->message->withHeader('Foo', 'bar'); + $value = $message->getHeader($name); + + $this->assertInternalType('array', $value); + $this->assertCount(1, $value); + $this->assertEquals('bar', $value[0]); + } + + /** + * @covers Meek\Http\Message::getHeader + * @dataProvider differentCases + */ + public function testRetrievingHeaderReturnsCorrectValues($name) + { + $message = $this->message + ->withHeader('Foo', 'bar') + ->withAddedHeader('Foo', 'baz'); + $value = $message->getHeader($name); + + $this->assertCount(2, $value); + $this->assertEquals('bar', $value[0]); + $this->assertEquals('baz', $value[1]); + } + + /** + * @covers Meek\Http\Message::getHeaderLine + * @dataProvider differentCases + */ + public function testBuildsAnEmptyValueIfHeaderDoesNotExist($name) + { + $value = $this->message->getHeaderLine($name); + + $this->assertInternalType('string', $value); + $this->assertEmpty($value); + } + + /** + * @covers Meek\Http\Message::getHeaderLine + * @dataProvider differentCases + */ + public function testBuildsValueIfOnlyOneValue($name) + { + $message = $this->message->withHeader('Foo', 'bar'); + $value = $message->getHeaderLine($name); + + $this->assertInternalType('string', $value); + $this->assertEquals('bar', $value); + } + + /** + * @covers Meek\Http\Message::getHeaderLine + * @dataProvider differentCases + */ + public function testBuildsCommaSeparatedListIfMoreThanOneValue($name) + { + $message = $this->message + ->withHeader('Foo', 'bar') + ->withAddedHeader('Foo', 'baz'); + $value = $message->getHeaderLine($name); + + $this->assertEquals('bar, baz', $value); + } + + /** + * @covers Meek\Http\Message::withHeader + * @dataProvider invalidDataTypes + * @expectedException InvalidArgumentException + */ + public function testThowsErrorWhenChangingHeaderWithInvalidType($invalidType) + { + $message = $this->message->withHeader('foo', $invalidType); + } + + /** + * @covers Meek\Http\Message::withHeader + */ + public function testAddingANewHeaderKeepsTheMessageImmutable() + { + $message = $this->message->withHeader('Foo', 'bar'); + + $this->assertNotSame($this->message, $message); + $this->assertEquals('bar', $message->getHeaderLine('foo')); + } + + /** + * @covers Meek\Http\Message::withHeader + */ + public function testAddingtoAnExistingHeaderKeepsTheMessageImmutable() + { + $message = $this->message + ->withHeader('Foo', 'bar') + ->withAddedHeader('Foo', 'baz'); + + $this->assertNotSame($this->message, $message); + $this->assertEquals('bar, baz', $message->getHeaderLine('foo')); + } + + /** + * @covers Meek\Http\Message::withHeader + */ + public function testChangingAnExistingHeaderChangesTheOriginalHeaderName() + { + $message = $this->message + ->withHeader('Foo', 'bar') + ->withHeader('FOO', 'baz'); + $headers = $message->getHeaders(); + + $this->assertCount(1, $headers); + $this->assertArrayHasKey('FOO', $headers); + } + + /** + * @covers Meek\Http\Message::withAddedHeader + * @dataProvider invalidDataTypes + * @expectedException InvalidArgumentException + */ + public function testThrowsErrorWhenAddingHeadersWithInvalidTypes($invalidType) + { + $message = $this->message->withAddedHeader('foo', $invalidType); + } + + /** + * @covers Meek\Http\Message::withAddedHeader + */ + public function testAddingANewHeaderKeepsTheMessageImmutableWithAddedHeader() + { + $message = $this->message->withAddedHeader('Foo', 'bar'); + + $this->assertNotSame($this->message, $message); + $this->assertEquals('bar', $message->getHeaderLine('foo')); + } + + /** + * @covers Meek\Http\Message::withHeader + */ + public function testAddingtoAnExistingHeaderKeepsTheMessageImmutableWithAddedHeader() + { + $message = $this->message + ->withAddedHeader('Foo', 'bar') + ->withAddedHeader('Foo', 'baz'); + + $this->assertNotSame($this->message, $message); + $this->assertEquals('bar, baz', $message->getHeaderLine('foo')); + } + + /** + * @covers Meek\Http\Message::withHeader + */ + public function testCanAddMultipleValues() + { + $message = $this->message->withHeader('Foo', ['bar', 'baz']); + + $this->assertEquals('bar, baz', $message->getHeaderLine('foo')); + } + + /** + * @covers Meek\Http\Message::withAddedHeader + */ + public function testDoesNotModifyOriginalHeaderName() + { + $message = $this->message + ->withHeader('Foo', 'bar') + ->withAddedHeader('FOO', 'baz'); + $headers = $message->getHeaders(); + + $this->assertArrayHasKey('Foo', $headers); + } + + /** + * @covers Meek\Http\Message::withAddedHeader + */ + public function testCanAddMultipleValuesWithHeader() + { + $message = $this->message + ->withHeader('Foo', 'test') + ->withAddedHeader('Foo', ['bar', 'baz']); + + $this->assertEquals('test, bar, baz', $message->getHeaderLine('foo')); + } + + /** + * @covers Meek\Http\Message::withoutHeader + */ + public function testMessageIsImmutableIfNoHeadersWereRemoved() + { + $message = $this->message->withoutHeader('Foo'); + + $this->assertNotSame($this->message, $message); + } + + /** + * @covers Meek\Http\Message::withoutHeader + * @dataProvider differentCases + */ + public function testRemovingAHeaderKeepsTheMessageImmutable() + { + $message1 = $this->message->withHeader('Foo', 'bar'); + $message2 = $message1->withoutHeader('Foo'); + + $this->assertNotSame($message1, $message2); + $this->assertEmpty($message2->getHeaderLine('foo')); + } + + /** + * @covers Meek\Http\Message::setHeaders + */ + public function testSettingHeadersCanBeMuttable() + { + $headers = [ + 'Host' => 'www.example.com', + 'Cache-Control' => ['no-cache', 'private'] + ]; + + $setHeaders = new \ReflectionMethod($this->message, 'setHeaders'); + $setHeaders->setAccessible(true); + $setHeaders->invoke($this->message, $headers); + + $headers['Host'] = ['www.example.com']; + $this->assertEquals($headers, $this->message->getHeaders()); + } + + public function invalidProtocolVersions() + { + return [ + 'an empty string' => [''], + 'a boolean data type' => [true], + 'an array data type' => [[]], + 'an object' => [new \stdClass], + 'a float data type' => [1.1], + 'an integer data type' => [2], + 'an alpabetic string' => ['hello'] + ]; + } + + public function invalidDataTypes() + { + return [ + 'a boolean' => [true], + 'an object' => [new \stdClass], + 'a float data type' => [3.14159], + 'an integer' => [4], + 'an array of booleans' => [[true]], + 'an array of arrays' => [[[]]], + 'an array of objects' => [[new \stdClass]], + 'an array of floats' => [[3.14159]], + 'an array of integers' => [[4]] + ]; + } + + public function differentCases() + { + return [ + 'lowercase' => ['foo'], + 'uppercase' => ['FOO'], + 'mixedcase' => ['FoO'] + ]; + } +} diff --git a/tests/RedirectedResponseTest.php b/tests/RedirectedResponseTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/RequestTest.php b/tests/RequestTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Session/PdoStorageDriverTest.php b/tests/Session/PdoStorageDriverTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Session/StorageHandlerTest.php b/tests/Session/StorageHandlerTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/SessionTest.php b/tests/SessionTest.php deleted file mode 100644 index e69de29..0000000 diff --git a/tests/StatusTest.php b/tests/StatusTest.php index 37a61d7..cd347c0 100644 --- a/tests/StatusTest.php +++ b/tests/StatusTest.php @@ -1,11 +1,8 @@