From d74d051311b6bec6ddb9645e1ffb33e15b5df680 Mon Sep 17 00:00:00 2001
From: Thomas Flori <thflori@gmail.com>
Date: Sat, 4 Aug 2018 22:50:47 +0200
Subject: [PATCH] overwrite message implementation from guzzle and extend psr-7

---
 README.md                                    |  29 +-
 src/ChangeableMessageTrait.php               | 124 +++++++
 src/ClientRequest.php                        |  94 +++++
 src/ClientResponse.php                       |   9 +
 src/MessageTrait.php                         | 185 ++++++++++
 src/Psr7Extended/ClientRequestInterface.php  | 129 +++++++
 src/Psr7Extended/ClientResponseInterface.php |  17 +
 src/Psr7Extended/ServerRequestInterface.php  |   7 +
 src/Psr7Extended/ServerResponseInterface.php |  94 +++++
 src/Request.php                              | 145 ++++++++
 src/Response.php                             | 134 ++++++++
 src/ServerRequest.php                        | 340 ++++++++++++++++++-
 src/ServerResponse.php                       |  96 ++++++
 src/functions.php                            |  33 --
 14 files changed, 1400 insertions(+), 36 deletions(-)
 create mode 100644 src/ChangeableMessageTrait.php
 create mode 100644 src/ClientRequest.php
 create mode 100644 src/ClientResponse.php
 create mode 100644 src/MessageTrait.php
 create mode 100644 src/Psr7Extended/ClientRequestInterface.php
 create mode 100644 src/Psr7Extended/ClientResponseInterface.php
 create mode 100644 src/Psr7Extended/ServerRequestInterface.php
 create mode 100644 src/Psr7Extended/ServerResponseInterface.php
 create mode 100644 src/Request.php
 create mode 100644 src/Response.php
 create mode 100644 src/ServerResponse.php
 delete mode 100644 src/functions.php

diff --git a/README.md b/README.md
index 418db67..dac9151 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,29 @@
 # tal
-A guzzle/psr-7 wrapper to add additional functionality like sending responses and managing cookies
+
+This is a clone of guzzle/psr-7 with additional functionality like sending responses and managing cookies. Because they
+changed the access level of the properties to be private I was forced to copy the code instead of extending it.
+
+If you think the code from guzzle has changed a lot and this is outdated - feel free open a pull request.
+
+## PSR-7 Extended
+
+This fork is using an extended and fully compatible version of PSR-7. This extension breaks some statements made on the
+meta document of PSR-7.
+
+In PSR-7 every message should be immutable. That means that you can not change a message after it got created. Instead
+you have to clone it and modify the clone. I do not fully agree that every message has to be immutable. While it is true
+that a request send from a client on the servers side should not be changed because the request already happened. The
+request can sill be changed before it gets sent to the server. The same is valid for the response: the client should
+not change the response but the server can change the response before it gets sent to the client.
+
+Therefore we split the request and response interfaces two:
+
+* `Tal\Psr7Extended\ClientRequestInterface` - the request that will be sent from client to server
+* `Tal\Psr7Extended\ClientResponseInterface` - the response that got sent from server to client
+* `Tal\Psr7Extended\ServerRequestInterface` - the request that got sent from client to server
+* `Tal\Psr7Extended\ServerResponseInterface` - the response that will be sent from server to client
+
+They are fully compatible with the PSR-7 interfaces. That means that the `with*()` methods still exist and can also be
+used to change the client request and server response. But you have to understand that this is not just a philosophical
+question: when you clone the message and modify the properties it means that the object has to be copied in the memory.
+Even if you don't store the old object it has to be collected and removed from the garbage collector.
diff --git a/src/ChangeableMessageTrait.php b/src/ChangeableMessageTrait.php
new file mode 100644
index 0000000..98c9463
--- /dev/null
+++ b/src/ChangeableMessageTrait.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Tal;
+
+use Psr\Http\Message\StreamInterface;
+
+trait ChangeableMessageTrait
+{
+    use MessageTrait;
+
+    /**
+     * Sets the specified HTTP protocol version.
+     *
+     * The version string MUST contain only the HTTP version number (e.g.,
+     * "1.1", "1.0").
+     *
+     * @param string $version HTTP protocol version
+     * @return static
+     */
+    public function setProtocolVersion($version)
+    {
+        $this->protocol = $version;
+        return $this;
+    }
+
+    /**
+     * Sets the provided value replacing the specified header.
+     *
+     * While header names are case-insensitive, the casing of the header will
+     * be preserved by this function, and returned from getHeaders().
+     *
+     * @param string $header Case-insensitive header field name.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function setHeader($header, $value)
+    {
+        if (!is_array($value)) {
+            $value = [$value];
+        }
+
+        $value = $this->trimHeaderValues($value);
+        $normalized = strtolower($header);
+
+        if (isset($this->headerNames[$normalized])) {
+            unset($this->headers[$this->headerNames[$normalized]]);
+        }
+        $this->headerNames[$normalized] = $header;
+        $this->headers[$header] = $value;
+
+        return $this;
+    }
+
+    /**
+     * Adds the specified header appended with the given value.
+     *
+     * Existing values for the specified header will be maintained. The new
+     * value(s) will be appended to the existing list. If the header did not
+     * exist previously, it will be added.
+     *
+     * @param string $header Case-insensitive header field name to add.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function addHeader($header, $value)
+    {
+        if (!is_array($value)) {
+            $value = [$value];
+        }
+
+        $value = $this->trimHeaderValues($value);
+        $normalized = strtolower($header);
+
+        if (isset($this->headerNames[$normalized])) {
+            $header = $this->headerNames[$normalized];
+            $this->headers[$header] = array_merge($this->headers[$header], $value);
+        } else {
+            $this->headerNames[$normalized] = $header;
+            $this->headers[$header] = $value;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Deletes the specified header.
+     *
+     * Header resolution MUST be done without case-sensitivity.
+     *
+     * @param string $header Case-insensitive header field name to remove.
+     * @return static
+     */
+    public function deleteHeader($header)
+    {
+        $normalized = strtolower($header);
+
+        if (!isset($this->headerNames[$normalized])) {
+            return $this;
+        }
+
+        $header = $this->headerNames[$normalized];
+
+        unset($this->headers[$header], $this->headerNames[$normalized]);
+
+        return $this;
+    }
+
+    /**
+     * Sets the specified message body.
+     *
+     * The body MUST be a StreamInterface object.
+     *
+     * @param StreamInterface $body Body.
+     * @return static
+     * @throws \InvalidArgumentException When the body is not valid.
+     */
+    public function setBody(StreamInterface $body)
+    {
+        $this->stream = $body;
+        return $this;
+    }
+}
diff --git a/src/ClientRequest.php b/src/ClientRequest.php
new file mode 100644
index 0000000..18d2c5c
--- /dev/null
+++ b/src/ClientRequest.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace Tal;
+
+use InvalidArgumentException;
+use Psr\Http\Message\UriInterface;
+use Tal\Psr7Extended\ClientRequestInterface;
+
+class ClientRequest extends Request implements ClientRequestInterface
+{
+    use ChangeableMessageTrait;
+
+    /**
+     * Sets the specific request-target.
+     *
+     * If the request needs a non-origin-form request-target — e.g., for
+     * specifying an absolute-form, authority-form, or asterisk-form —
+     * this method may be used to create an instance with the specified
+     * request-target, verbatim.
+     *
+     * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
+     *     request-target forms allowed in request messages)
+     * @param mixed $requestTarget
+     * @return static
+     */
+    public function setRequestTarget($requestTarget)
+    {
+        if (preg_match('#\s#', $requestTarget)) {
+            throw new InvalidArgumentException(
+                'Invalid request target provided; cannot contain whitespace'
+            );
+        }
+
+        $this->requestTarget = $requestTarget;
+        return $this;
+    }
+
+    /**
+     * Sets the provided HTTP method.
+     *
+     * While HTTP method names are typically all uppercase characters, HTTP
+     * method names are case-sensitive and thus implementations SHOULD NOT
+     * modify the given string.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * changed request method.
+     *
+     * @param string $method Case-sensitive method.
+     * @return static
+     * @throws \InvalidArgumentException for invalid HTTP methods.
+     */
+    public function setMethod($method)
+    {
+        $this->method = strtoupper($method);
+        return $this;
+    }
+
+    /**
+     * Sets the provided URI.
+     *
+     * This method MUST update the Host header of the returned request by
+     * default if the URI contains a host component. If the URI does not
+     * contain a host component, any pre-existing Host header MUST be carried
+     * over to the returned request.
+     *
+     * You can opt-in to preserving the original state of the Host header by
+     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+     * `true`, this method interacts with the Host header in the following ways:
+     *
+     * - If the Host header is missing or empty, and the new URI contains
+     *   a host component, this method MUST update the Host header in the returned
+     *   request.
+     * - If the Host header is missing or empty, and the new URI does not contain a
+     *   host component, this method MUST NOT update the Host header in the returned
+     *   request.
+     * - If a Host header is present and non-empty, this method MUST NOT update
+     *   the Host header in the returned request.
+     *
+     * @link http://tools.ietf.org/html/rfc3986#section-4.3
+     * @param UriInterface $uri New request URI to use.
+     * @param bool $preserveHost Preserve the original state of the Host header.
+     * @return static
+     */
+    public function setUri(UriInterface $uri, $preserveHost = false)
+    {
+        $this->uri = $uri;
+
+        if (!$preserveHost) {
+            $this->updateHostFromUri();
+        }
+        return $this;
+    }
+}
diff --git a/src/ClientResponse.php b/src/ClientResponse.php
new file mode 100644
index 0000000..92e5870
--- /dev/null
+++ b/src/ClientResponse.php
@@ -0,0 +1,9 @@
+<?php
+
+namespace Tal;
+
+use Tal\Psr7Extended\ClientResponseInterface;
+
+class ClientResponse extends Response implements ClientResponseInterface
+{
+}
diff --git a/src/MessageTrait.php b/src/MessageTrait.php
new file mode 100644
index 0000000..6f53690
--- /dev/null
+++ b/src/MessageTrait.php
@@ -0,0 +1,185 @@
+<?php
+
+namespace Tal;
+
+use function GuzzleHttp\Psr7\stream_for;
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Trait implementing functionality common to requests and responses.
+ */
+trait MessageTrait
+{
+    /** @var array Map of all registered headers, as original name => array of values */
+    protected $headers = [];
+
+    /** @var array Map of lowercase header name => original name at registration */
+    protected $headerNames  = [];
+
+    /** @var string */
+    protected $protocol = '1.1';
+
+    /** @var StreamInterface */
+    protected $stream;
+
+    public function getProtocolVersion()
+    {
+        return $this->protocol;
+    }
+
+    public function withProtocolVersion($version)
+    {
+        if ($this->protocol === $version) {
+            return $this;
+        }
+
+        $new = clone $this;
+        $new->protocol = $version;
+        return $new;
+    }
+
+    public function getHeaders()
+    {
+        return $this->headers;
+    }
+
+    public function hasHeader($header)
+    {
+        return isset($this->headerNames[strtolower($header)]);
+    }
+
+    public function getHeader($header)
+    {
+        $header = strtolower($header);
+
+        if (!isset($this->headerNames[$header])) {
+            return [];
+        }
+
+        $header = $this->headerNames[$header];
+
+        return $this->headers[$header];
+    }
+
+    public function getHeaderLine($header)
+    {
+        return implode(', ', $this->getHeader($header));
+    }
+
+    public function withHeader($header, $value)
+    {
+        if (!is_array($value)) {
+            $value = [$value];
+        }
+
+        $value = $this->trimHeaderValues($value);
+        $normalized = strtolower($header);
+
+        $new = clone $this;
+        if (isset($new->headerNames[$normalized])) {
+            unset($new->headers[$new->headerNames[$normalized]]);
+        }
+        $new->headerNames[$normalized] = $header;
+        $new->headers[$header] = $value;
+
+        return $new;
+    }
+
+    public function withAddedHeader($header, $value)
+    {
+        if (!is_array($value)) {
+            $value = [$value];
+        }
+
+        $value = $this->trimHeaderValues($value);
+        $normalized = strtolower($header);
+
+        $new = clone $this;
+        if (isset($new->headerNames[$normalized])) {
+            $header = $this->headerNames[$normalized];
+            $new->headers[$header] = array_merge($this->headers[$header], $value);
+        } else {
+            $new->headerNames[$normalized] = $header;
+            $new->headers[$header] = $value;
+        }
+
+        return $new;
+    }
+
+    public function withoutHeader($header)
+    {
+        $normalized = strtolower($header);
+
+        if (!isset($this->headerNames[$normalized])) {
+            return $this;
+        }
+
+        $header = $this->headerNames[$normalized];
+
+        $new = clone $this;
+        unset($new->headers[$header], $new->headerNames[$normalized]);
+
+        return $new;
+    }
+
+    public function getBody()
+    {
+        if (!$this->stream) {
+            $this->stream = stream_for('');
+        }
+
+        return $this->stream;
+    }
+
+    public function withBody(StreamInterface $body)
+    {
+        if ($body === $this->stream) {
+            return $this;
+        }
+
+        $new = clone $this;
+        $new->stream = $body;
+        return $new;
+    }
+
+    protected function setHeaders(array $headers)
+    {
+        $this->headerNames = $this->headers = [];
+        foreach ($headers as $header => $value) {
+            if (!is_array($value)) {
+                $value = [$value];
+            }
+
+            $value = $this->trimHeaderValues($value);
+            $normalized = strtolower($header);
+            if (isset($this->headerNames[$normalized])) {
+                $header = $this->headerNames[$normalized];
+                $this->headers[$header] = array_merge($this->headers[$header], $value);
+            } else {
+                $this->headerNames[$normalized] = $header;
+                $this->headers[$header] = $value;
+            }
+        }
+    }
+
+    /**
+     * Trims whitespace from the header values.
+     *
+     * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field.
+     *
+     * header-field = field-name ":" OWS field-value OWS
+     * OWS          = *( SP / HTAB )
+     *
+     * @param string[] $values Header values
+     *
+     * @return string[] Trimmed header values
+     *
+     * @see https://tools.ietf.org/html/rfc7230#section-3.2.4
+     */
+    protected function trimHeaderValues(array $values)
+    {
+        return array_map(function ($value) {
+            return trim($value, " \t");
+        }, $values);
+    }
+}
diff --git a/src/Psr7Extended/ClientRequestInterface.php b/src/Psr7Extended/ClientRequestInterface.php
new file mode 100644
index 0000000..2324564
--- /dev/null
+++ b/src/Psr7Extended/ClientRequestInterface.php
@@ -0,0 +1,129 @@
+<?php
+
+namespace Tal\Psr7Extended;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UriInterface;
+
+interface ClientRequestInterface extends RequestInterface
+{
+    /**
+     * Sets the specific request-target.
+     *
+     * If the request needs a non-origin-form request-target — e.g., for
+     * specifying an absolute-form, authority-form, or asterisk-form —
+     * this method may be used to create an instance with the specified
+     * request-target, verbatim.
+     *
+     * @link http://tools.ietf.org/html/rfc7230#section-5.3 (for the various
+     *     request-target forms allowed in request messages)
+     * @param mixed $requestTarget
+     * @return static
+     */
+    public function setRequestTarget($requestTarget);
+
+    /**
+     * Sets the provided HTTP method.
+     *
+     * While HTTP method names are typically all uppercase characters, HTTP
+     * method names are case-sensitive and thus implementations SHOULD NOT
+     * modify the given string.
+     *
+     * This method MUST be implemented in such a way as to retain the
+     * immutability of the message, and MUST return an instance that has the
+     * changed request method.
+     *
+     * @param string $method Case-sensitive method.
+     * @return static
+     * @throws \InvalidArgumentException for invalid HTTP methods.
+     */
+    public function setMethod($method);
+
+    /**
+     * Sets the provided URI.
+     *
+     * This method MUST update the Host header of the returned request by
+     * default if the URI contains a host component. If the URI does not
+     * contain a host component, any pre-existing Host header MUST be carried
+     * over to the returned request.
+     *
+     * You can opt-in to preserving the original state of the Host header by
+     * setting `$preserveHost` to `true`. When `$preserveHost` is set to
+     * `true`, this method interacts with the Host header in the following ways:
+     *
+     * - If the Host header is missing or empty, and the new URI contains
+     *   a host component, this method MUST update the Host header in the returned
+     *   request.
+     * - If the Host header is missing or empty, and the new URI does not contain a
+     *   host component, this method MUST NOT update the Host header in the returned
+     *   request.
+     * - If a Host header is present and non-empty, this method MUST NOT update
+     *   the Host header in the returned request.
+     *
+     * @link http://tools.ietf.org/html/rfc3986#section-4.3
+     * @param UriInterface $uri New request URI to use.
+     * @param bool $preserveHost Preserve the original state of the Host header.
+     * @return static
+     */
+    public function setUri(UriInterface $uri, $preserveHost = false);
+
+    /**
+     * Sets the specified HTTP protocol version.
+     *
+     * The version string MUST contain only the HTTP version number (e.g.,
+     * "1.1", "1.0").
+     *
+     * @param string $version HTTP protocol version
+     * @return static
+     */
+    public function setProtocolVersion($version);
+
+    /**
+     * Sets the provided value replacing the specified header.
+     *
+     * While header names are case-insensitive, the casing of the header will
+     * be preserved by this function, and returned from getHeaders().
+     *
+     * @param string $name Case-insensitive header field name.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function setHeader($name, $value);
+
+    /**
+     * Adds the specified header appended with the given value.
+     *
+     * Existing values for the specified header will be maintained. The new
+     * value(s) will be appended to the existing list. If the header did not
+     * exist previously, it will be added.
+     *
+     * @param string $name Case-insensitive header field name to add.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function addHeader($name, $value);
+
+    /**
+     * Deletes the specified header.
+     *
+     * Header resolution MUST be done without case-sensitivity.
+     *
+     * @param string $name Case-insensitive header field name to remove.
+     * @return static
+     */
+    public function deleteHeader($name);
+
+    /**
+     * Sets the specified message body.
+     *
+     * The body MUST be a StreamInterface object.
+     *
+     * @param StreamInterface $body Body.
+     * @return static
+     * @throws \InvalidArgumentException When the body is not valid.
+     */
+    public function setBody(StreamInterface $body);
+}
diff --git a/src/Psr7Extended/ClientResponseInterface.php b/src/Psr7Extended/ClientResponseInterface.php
new file mode 100644
index 0000000..e4ccbe4
--- /dev/null
+++ b/src/Psr7Extended/ClientResponseInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Tal\Psr7Extended;
+
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Interface ClientResponseInterface
+ *
+ * This interface extends the response just to divide the ClientResponseInterface from
+ * the ServerResponseInterface.
+ *
+ * @package Tal\Psr7Extended
+ */
+interface ClientResponseInterface extends ResponseInterface
+{
+}
diff --git a/src/Psr7Extended/ServerRequestInterface.php b/src/Psr7Extended/ServerRequestInterface.php
new file mode 100644
index 0000000..e6a3cdf
--- /dev/null
+++ b/src/Psr7Extended/ServerRequestInterface.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace Tal\Psr7Extended;
+
+interface ServerRequestInterface extends \Psr\Http\Message\ServerRequestInterface
+{
+}
diff --git a/src/Psr7Extended/ServerResponseInterface.php b/src/Psr7Extended/ServerResponseInterface.php
new file mode 100644
index 0000000..5f574ad
--- /dev/null
+++ b/src/Psr7Extended/ServerResponseInterface.php
@@ -0,0 +1,94 @@
+<?php
+
+
+namespace Tal\Psr7Extended;
+
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+
+interface ServerResponseInterface extends ResponseInterface
+{
+    /**
+     * Sets the specified status code and, optionally, reason phrase.
+     *
+     * If no reason phrase is specified, implementations MAY choose to default
+     * to the RFC 7231 or IANA recommended reason phrase for the response's
+     * status code.
+     *
+     * @link http://tools.ietf.org/html/rfc7231#section-6
+     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+     * @param int $code The 3-digit integer result code to set.
+     * @param string $reasonPhrase The reason phrase to use with the
+     *     provided status code; if none is provided, implementations MAY
+     *     use the defaults as suggested in the HTTP specification.
+     * @return static
+     * @throws \InvalidArgumentException For invalid status code arguments.
+     */
+    public function setStatus($code, $reasonPhrase = '');
+
+    /**
+     * Sets the specified HTTP protocol version.
+     *
+     * The version string MUST contain only the HTTP version number (e.g.,
+     * "1.1", "1.0").
+     *
+     * @param string $version HTTP protocol version
+     * @return static
+     */
+    public function setProtocolVersion($version);
+
+    /**
+     * Sets the provided value replacing the specified header.
+     *
+     * While header names are case-insensitive, the casing of the header will
+     * be preserved by this function, and returned from getHeaders().
+     *
+     * @param string $name Case-insensitive header field name.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function setHeader($name, $value);
+
+    /**
+     * Adds the specified header appended with the given value.
+     *
+     * Existing values for the specified header will be maintained. The new
+     * value(s) will be appended to the existing list. If the header did not
+     * exist previously, it will be added.
+     *
+     * @param string $name Case-insensitive header field name to add.
+     * @param string|string[] $value Header value(s).
+     * @return static
+     * @throws \InvalidArgumentException for invalid header names or values.
+     */
+    public function addHeader($name, $value);
+
+    /**
+     * Deletes the specified header.
+     *
+     * Header resolution MUST be done without case-sensitivity.
+     *
+     * @param string $name Case-insensitive header field name to remove.
+     * @return static
+     */
+    public function deleteHeader($name);
+
+    /**
+     * Sets the specified message body.
+     *
+     * The body MUST be a StreamInterface object.
+     *
+     * @param StreamInterface $body Body.
+     * @return static
+     * @throws \InvalidArgumentException When the body is not valid.
+     */
+    public function setBody(StreamInterface $body);
+
+    /**
+     * Sends this response to the client.
+     *
+     * @return static
+     */
+    public function send();
+}
diff --git a/src/Request.php b/src/Request.php
new file mode 100644
index 0000000..1890882
--- /dev/null
+++ b/src/Request.php
@@ -0,0 +1,145 @@
+<?php
+
+namespace Tal;
+
+use function GuzzleHttp\Psr7\stream_for;
+use GuzzleHttp\Psr7\Uri;
+use InvalidArgumentException;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UriInterface;
+
+/**
+ * PSR-7 request implementation.
+ */
+abstract class Request implements RequestInterface
+{
+    use MessageTrait;
+
+    /** @var string */
+    protected $method;
+
+    /** @var null|string */
+    protected $requestTarget;
+
+    /** @var UriInterface */
+    protected $uri;
+
+    /**
+     * @param string                               $method  HTTP method
+     * @param string|UriInterface                  $uri     URI
+     * @param array                                $headers Request headers
+     * @param string|null|resource|StreamInterface $body    Request body
+     * @param string                               $version Protocol version
+     */
+    public function __construct(
+        $method,
+        $uri,
+        array $headers = [],
+        $body = null,
+        $version = '1.1'
+    ) {
+        if (!($uri instanceof UriInterface)) {
+            $uri = new Uri($uri);
+        }
+
+        $this->method = strtoupper($method);
+        $this->uri = $uri;
+        $this->setHeaders($headers);
+        $this->protocol = $version;
+
+        if (!$this->hasHeader('Host')) {
+            $this->updateHostFromUri();
+        }
+
+        if ($body !== '' && $body !== null) {
+            $this->stream = stream_for($body);
+        }
+    }
+
+    public function getRequestTarget()
+    {
+        if ($this->requestTarget !== null) {
+            return $this->requestTarget;
+        }
+
+        $target = $this->uri->getPath();
+        if ($target == '') {
+            $target = '/';
+        }
+        if ($this->uri->getQuery() != '') {
+            $target .= '?' . $this->uri->getQuery();
+        }
+
+        return $target;
+    }
+
+    public function withRequestTarget($requestTarget)
+    {
+        if (preg_match('#\s#', $requestTarget)) {
+            throw new InvalidArgumentException(
+                'Invalid request target provided; cannot contain whitespace'
+            );
+        }
+
+        $new = clone $this;
+        $new->requestTarget = $requestTarget;
+        return $new;
+    }
+
+    public function getMethod()
+    {
+        return $this->method;
+    }
+
+    public function withMethod($method)
+    {
+        $new = clone $this;
+        $new->method = strtoupper($method);
+        return $new;
+    }
+
+    public function getUri()
+    {
+        return $this->uri;
+    }
+
+    public function withUri(UriInterface $uri, $preserveHost = false)
+    {
+        if ($uri === $this->uri) {
+            return $this;
+        }
+
+        $new = clone $this;
+        $new->uri = $uri;
+
+        if (!$preserveHost) {
+            $new->updateHostFromUri();
+        }
+
+        return $new;
+    }
+
+    protected function updateHostFromUri()
+    {
+        $host = $this->uri->getHost();
+
+        if ($host == '') {
+            return;
+        }
+
+        if (($port = $this->uri->getPort()) !== null) {
+            $host .= ':' . $port;
+        }
+
+        if (isset($this->headerNames['host'])) {
+            $header = $this->headerNames['host'];
+        } else {
+            $header = 'Host';
+            $this->headerNames['host'] = 'Host';
+        }
+        // Ensure Host is the first header.
+        // See: http://tools.ietf.org/html/rfc7230#section-5.4
+        $this->headers = [$header => [$host]] + $this->headers;
+    }
+}
diff --git a/src/Response.php b/src/Response.php
new file mode 100644
index 0000000..30774b1
--- /dev/null
+++ b/src/Response.php
@@ -0,0 +1,134 @@
+<?php
+
+namespace Tal;
+
+use function GuzzleHttp\Psr7\stream_for;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * PSR-7 response implementation.
+ */
+abstract class Response implements ResponseInterface
+{
+    use MessageTrait;
+
+    /** @var array Map of standard HTTP status code/reason phrases */
+    protected static $phrases = [
+        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',
+        300 => 'Multiple Choices',
+        301 => 'Moved Permanently',
+        302 => 'Found',
+        303 => 'See Other',
+        304 => 'Not Modified',
+        305 => 'Use Proxy',
+        306 => 'Switch Proxy',
+        307 => 'Temporary 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 Time-out',
+        409 => 'Conflict',
+        410 => 'Gone',
+        411 => 'Length Required',
+        412 => 'Precondition Failed',
+        413 => 'Request Entity Too Large',
+        414 => 'Request-URI Too Large',
+        415 => 'Unsupported Media Type',
+        416 => 'Requested range not satisfiable',
+        417 => 'Expectation Failed',
+        418 => 'I\'m a teapot',
+        422 => 'Unprocessable Entity',
+        423 => 'Locked',
+        424 => 'Failed Dependency',
+        425 => 'Unordered Collection',
+        426 => 'Upgrade Required',
+        428 => 'Precondition Required',
+        429 => 'Too Many Requests',
+        431 => 'Request Header Fields Too Large',
+        451 => 'Unavailable For Legal Reasons',
+        500 => 'Internal Server Error',
+        501 => 'Not Implemented',
+        502 => 'Bad Gateway',
+        503 => 'Service Unavailable',
+        504 => 'Gateway Time-out',
+        505 => 'HTTP Version not supported',
+        506 => 'Variant Also Negotiates',
+        507 => 'Insufficient Storage',
+        508 => 'Loop Detected',
+        511 => 'Network Authentication Required',
+    ];
+
+    /** @var string */
+    protected $reasonPhrase = '';
+
+    /** @var int */
+    protected $statusCode = 200;
+
+    /**
+     * @param int $status Status code
+     * @param array $headers Response headers
+     * @param string|null|resource|StreamInterface $body Response body
+     * @param string $version Protocol version
+     * @param string|null $reason Reason phrase (when empty a default will be used based on the status code)
+     */
+    public function __construct(
+        $status = 200,
+        array $headers = [],
+        $body = null,
+        $version = '1.1',
+        $reason = null
+    ) {
+        $this->statusCode = (int) $status;
+
+        if ($body !== '' && $body !== null) {
+            $this->stream = stream_for($body);
+        }
+
+        $this->setHeaders($headers);
+        if ($reason == '' && isset(self::$phrases[$this->statusCode])) {
+            $this->reasonPhrase = self::$phrases[$this->statusCode];
+        } else {
+            $this->reasonPhrase = (string) $reason;
+        }
+
+        $this->protocol = $version;
+    }
+
+    public function getStatusCode()
+    {
+        return $this->statusCode;
+    }
+
+    public function getReasonPhrase()
+    {
+        return $this->reasonPhrase;
+    }
+
+    public function withStatus($code, $reasonPhrase = '')
+    {
+        $new = clone $this;
+        $new->statusCode = (int) $code;
+        if ($reasonPhrase == '' && isset(self::$phrases[$new->statusCode])) {
+            $reasonPhrase = self::$phrases[$new->statusCode];
+        }
+        $new->reasonPhrase = $reasonPhrase;
+        return $new;
+    }
+}
diff --git a/src/ServerRequest.php b/src/ServerRequest.php
index 910f5d0..dd11648 100644
--- a/src/ServerRequest.php
+++ b/src/ServerRequest.php
@@ -3,10 +3,169 @@
 namespace Tal;
 
 use GuzzleHttp\Psr7\LazyOpenStream;
-use GuzzleHttp\Psr7\ServerRequest as BaseServerRequest;
+use GuzzleHttp\Psr7\UploadedFile;
+use GuzzleHttp\Psr7\Uri;
+use InvalidArgumentException;
+use Psr\Http\Message\UriInterface;
+use Psr\Http\Message\StreamInterface;
+use Psr\Http\Message\UploadedFileInterface;
+use Tal\Psr7Extended\ServerRequestInterface;
 
-class ServerRequest extends BaseServerRequest
+/**
+ * Server-side HTTP request
+ *
+ * Extends the Request definition to add methods for accessing incoming data,
+ * specifically server parameters, cookies, matched path parameters, query
+ * string arguments, body parameters, and upload file information.
+ *
+ * "Attributes" are discovered via decomposing the request (and usually
+ * specifically the URI path), and typically will be injected by the application.
+ *
+ * Requests are considered immutable; all methods that might change state are
+ * implemented such that they retain the internal state of the current
+ * message and return a new instance that contains the changed state.
+ */
+class ServerRequest extends Request implements ServerRequestInterface
 {
+    /**
+     * @var array
+     */
+    protected $attributes = [];
+
+    /**
+     * @var array
+     */
+    protected $cookieParams = [];
+
+    /**
+     * @var null|array|object
+     */
+    protected $parsedBody;
+
+    /**
+     * @var array
+     */
+    protected $queryParams = [];
+
+    /**
+     * @var array
+     */
+    protected $serverParams;
+
+    /**
+     * @var array
+     */
+    protected $uploadedFiles = [];
+
+    /**
+     * @param string                               $method       HTTP method
+     * @param string|UriInterface                  $uri          URI
+     * @param array                                $headers      Request headers
+     * @param string|null|resource|StreamInterface $body         Request body
+     * @param string                               $version      Protocol version
+     * @param array                                $serverParams Typically the $_SERVER superglobal
+     */
+    public function __construct(
+        $method,
+        $uri,
+        array $headers = [],
+        $body = null,
+        $version = '1.1',
+        array $serverParams = []
+    ) {
+        $this->serverParams = $serverParams;
+
+        parent::__construct($method, $uri, $headers, $body, $version);
+    }
+
+    /**
+     * Return an UploadedFile instance array.
+     *
+     * @param array $files A array which respect $_FILES structure
+     * @throws InvalidArgumentException for unrecognized values
+     * @return array
+     */
+    public static function normalizeFiles(array $files)
+    {
+        $normalized = [];
+
+        foreach ($files as $key => $value) {
+            if ($value instanceof UploadedFileInterface) {
+                $normalized[$key] = $value;
+            } elseif (is_array($value) && isset($value['tmp_name'])) {
+                $normalized[$key] = self::createUploadedFileFromSpec($value);
+            } elseif (is_array($value)) {
+                $normalized[$key] = self::normalizeFiles($value);
+                continue;
+            } else {
+                throw new InvalidArgumentException('Invalid value in files specification');
+            }
+        }
+
+        return $normalized;
+    }
+
+    /**
+     * Create and return an UploadedFile instance from a $_FILES specification.
+     *
+     * If the specification represents an array of values, this method will
+     * delegate to normalizeNestedFileSpec() and return that return value.
+     *
+     * @param array $value $_FILES struct
+     * @return array|UploadedFileInterface
+     */
+    protected static function createUploadedFileFromSpec(array $value)
+    {
+        if (is_array($value['tmp_name'])) {
+            return self::normalizeNestedFileSpec($value);
+        }
+
+        return new UploadedFile(
+            $value['tmp_name'],
+            (int) $value['size'],
+            (int) $value['error'],
+            $value['name'],
+            $value['type']
+        );
+    }
+
+    /**
+     * Normalize an array of file specifications.
+     *
+     * Loops through all nested files and returns a normalized array of
+     * UploadedFileInterface instances.
+     *
+     * @param array $files
+     * @return UploadedFileInterface[]
+     */
+    protected static function normalizeNestedFileSpec(array $files = [])
+    {
+        $normalizedFiles = [];
+
+        foreach (array_keys($files['tmp_name']) as $key) {
+            $spec = [
+                'tmp_name' => $files['tmp_name'][$key],
+                'size'     => $files['size'][$key],
+                'error'    => $files['error'][$key],
+                'name'     => $files['name'][$key],
+                'type'     => $files['type'][$key],
+            ];
+            $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec);
+        }
+
+        return $normalizedFiles;
+    }
+
+    /**
+     * Return a ServerRequestInterface populated with superglobals:
+     * $_GET
+     * $_POST
+     * $_COOKIE
+     * $_FILES
+     * $_SERVER
+     *
+     * @return ServerRequestInterface
+     */
     public static function fromGlobals()
     {
         $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET';
@@ -24,6 +183,183 @@ class ServerRequest extends BaseServerRequest
             ->withUploadedFiles(self::normalizeFiles($_FILES));
     }
 
+    /**
+     * Get a Uri populated with values from $_SERVER.
+     *
+     * @return UriInterface
+     */
+    public static function getUriFromGlobals()
+    {
+        $uri = new Uri('');
+
+        $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http');
+
+        $hasPort = false;
+        if (isset($_SERVER['HTTP_HOST'])) {
+            $hostHeaderParts = explode(':', $_SERVER['HTTP_HOST']);
+            $uri = $uri->withHost($hostHeaderParts[0]);
+            if (isset($hostHeaderParts[1])) {
+                $hasPort = true;
+                $uri = $uri->withPort($hostHeaderParts[1]);
+            }
+        } elseif (isset($_SERVER['SERVER_NAME'])) {
+            $uri = $uri->withHost($_SERVER['SERVER_NAME']);
+        } elseif (isset($_SERVER['SERVER_ADDR'])) {
+            $uri = $uri->withHost($_SERVER['SERVER_ADDR']);
+        }
+
+        if (!$hasPort && isset($_SERVER['SERVER_PORT'])) {
+            $uri = $uri->withPort($_SERVER['SERVER_PORT']);
+        }
+
+        $hasQuery = false;
+        if (isset($_SERVER['REQUEST_URI'])) {
+            $requestUriParts = explode('?', $_SERVER['REQUEST_URI']);
+            $uri = $uri->withPath($requestUriParts[0]);
+            if (isset($requestUriParts[1])) {
+                $hasQuery = true;
+                $uri = $uri->withQuery($requestUriParts[1]);
+            }
+        }
+
+        if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) {
+            $uri = $uri->withQuery($_SERVER['QUERY_STRING']);
+        }
+
+        return $uri;
+    }
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getServerParams()
+    {
+        return $this->serverParams;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getUploadedFiles()
+    {
+        return $this->uploadedFiles;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function withUploadedFiles(array $uploadedFiles)
+    {
+        $new = clone $this;
+        $new->uploadedFiles = $uploadedFiles;
+
+        return $new;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getCookieParams()
+    {
+        return $this->cookieParams;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function withCookieParams(array $cookies)
+    {
+        $new = clone $this;
+        $new->cookieParams = $cookies;
+
+        return $new;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getQueryParams()
+    {
+        return $this->queryParams;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function withQueryParams(array $query)
+    {
+        $new = clone $this;
+        $new->queryParams = $query;
+
+        return $new;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getParsedBody()
+    {
+        return $this->parsedBody;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function withParsedBody($data)
+    {
+        $new = clone $this;
+        $new->parsedBody = $data;
+
+        return $new;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttributes()
+    {
+        return $this->attributes;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getAttribute($attribute, $default = null)
+    {
+        if (false === array_key_exists($attribute, $this->attributes)) {
+            return $default;
+        }
+
+        return $this->attributes[$attribute];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function withAttribute($attribute, $value)
+    {
+        $new = clone $this;
+        $new->attributes[$attribute] = $value;
+
+        return $new;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function withoutAttribute($attribute)
+    {
+        if (false === array_key_exists($attribute, $this->attributes)) {
+            return $this;
+        }
+
+        $new = clone $this;
+        unset($new->attributes[$attribute]);
+
+        return $new;
+    }
+
     public function getCookie(string $name, $default = null)
     {
         return $this->getCookieParams()[$name] ?? $default;
diff --git a/src/ServerResponse.php b/src/ServerResponse.php
new file mode 100644
index 0000000..594e19d
--- /dev/null
+++ b/src/ServerResponse.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Tal;
+
+use Tal\Psr7Extended\ServerResponseInterface;
+
+class ServerResponse extends Response implements ServerResponseInterface
+{
+    use ChangeableMessageTrait;
+
+    /**
+     * Sets the specified status code and, optionally, reason phrase.
+     *
+     * If no reason phrase is specified, implementations MAY choose to default
+     * to the RFC 7231 or IANA recommended reason phrase for the response's
+     * status code.
+     *
+     * @link http://tools.ietf.org/html/rfc7231#section-6
+     * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+     * @param int $code The 3-digit integer result code to set.
+     * @param string $reasonPhrase The reason phrase to use with the
+     *     provided status code; if none is provided, implementations MAY
+     *     use the defaults as suggested in the HTTP specification.
+     * @return static
+     * @throws \InvalidArgumentException For invalid status code arguments.
+     */
+    public function setStatus($code, $reasonPhrase = '')
+    {
+        $this->statusCode = (int) $code;
+        if ($reasonPhrase == '' && isset(static::$phrases[$this->statusCode])) {
+            $reasonPhrase = static::$phrases[$this->statusCode];
+        }
+        $this->reasonPhrase = $reasonPhrase;
+        return $this;
+    }
+
+    /**
+     * Sends this response to the client.
+     *
+     * @return static
+     */
+    public function send()
+    {
+        $http_line = sprintf(
+            'HTTP/%s %s %s',
+            $this->getProtocolVersion(),
+            $this->getStatusCode(),
+            $this->getReasonPhrase()
+        );
+        header($http_line, true, $this->getStatusCode());
+
+        foreach ($this->getHeaders() as $name => $values) {
+            if (strtolower($name) !== 'set-cookie') {
+                header(sprintf('%s: %s', $name, implode(',', $values)), false);
+            } else {
+                foreach ($values as $value) {
+                    header(sprintf('%s: %s', $name, $value), false);
+                }
+            }
+        }
+
+        $stream = $this->getBody();
+        if ($stream->isSeekable()) {
+            $stream->rewind();
+        }
+        while (!$stream->eof()) {
+            echo $stream->read(1024 * 8);
+        }
+        return $this;
+    }
+
+    public function setCookie(
+        $name,
+        $value = "",
+        $expire = 0,
+        $path = "",
+        $domain = "",
+        $secure = false,
+        $httponly = false
+    ) {
+        $headerLine = sprintf('%s=%s', $name, urlencode($value));
+        if ($expire) {
+            $headerLine .= '; expires=' . gmdate('D, d M Y H:i:s T', time() + $expire);
+            $headerLine .= '; max-age=' . $expire;
+        }
+        // @todo prepare the header with all options given
+        $this->addHeader('Set-Cookie', $headerLine);
+        return $this;
+    }
+
+    public function deleteCookie($name)
+    {
+        $this->setCookie($name, 'deleted', -1);
+        return $this;
+    }
+}
diff --git a/src/functions.php b/src/functions.php
deleted file mode 100644
index 8c98859..0000000
--- a/src/functions.php
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-use Psr\Http\Message\ResponseInterface;
-
-if (!function_exists('sendResponse')) {
-    function sendResponse(ResponseInterface $response)
-    {
-        $http_line = sprintf('HTTP/%s %s %s',
-            $response->getProtocolVersion(),
-            $response->getStatusCode(),
-            $response->getReasonPhrase()
-        );
-        header($http_line, true, $response->getStatusCode());
-
-        foreach ($response->getHeaders() as $name => $values) {
-            if (strtolower($name) !== 'set-cookie') {
-                header(sprintf('%s: %s', $name, implode(',', $values)), false);
-            } else {
-                foreach ($values as $value) {
-                    header(sprintf('%s: %s', $name, $value), false);
-                }
-            }
-        }
-
-        $stream = $response->getBody();
-        if ($stream->isSeekable()) {
-            $stream->rewind();
-        }
-        while (!$stream->eof()) {
-            echo $stream->read(1024 * 8);
-        }
-    }
-}
-- 
GitLab