diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8631b5b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/.idea
+/vendor
+composer.phar
+composer.lock
+.php_cs.cache
+.DS_Store
+Thumbs.db
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..15b0e2e
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,26 @@
+language: php
+
+sudo: required
+
+php:
+ - 5.6
+ - 7.0
+ - 7.1
+
+install:
+ - composer self-update
+ - travis_retry composer install --no-interaction --prefer-source --dev
+
+before_script:
+ - composer dump-autoload
+
+script:
+ - php -S 127.0.0.1:8000 tests/server.php > /dev/null 2>&1 &
+ - mkdir -p build/logs
+ - vendor/bin/phpunit --bootstrap ./tests/bootstrap.php --configuration ./phpunit.xml --coverage-clover build/logs/clover.xml
+
+after_script:
+ - php vendor/bin/coveralls -v
+
+after_success:
+ - coveralls
\ No newline at end of file
diff --git a/README.md b/README.md
index b875602..26e1f0d 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,245 @@
-# http
-Neutrino Http Client
+Neutrino : Http Client component
+==============================================
+[![Build Status](https://travis-ci.org/pn-neutrino/http.svg?branch=master)](https://travis-ci.org/pn-neutrino/http) [![Coverage Status](https://coveralls.io/repos/github/pn-neutrino/http/badge.svg?branch=master)](https://coveralls.io/github/pn-neutrino/http)
+
+Http Client library using Curl or HttpStream.
+
+# Provider
+
+## Curl
+require curl extension.
+
+#### How use :
+```php
+use \Neutrino\Http\Provider\Curl as HttpCurl;
+use \Neutrino\Http\Method;
+
+$curl = new HttpCurl;
+
+$response = $curl
+ ->get('http://www.google.com', ['foo' => 'bar'], ['Accept' => 'text/plain'])
+ ->send();
+
+$response->code; // HTTP Status Code
+```
+
+### Curl\Streaming
+Curl\Stream allows you to work with large queries, by recovers content part by part.
+
+#### How use :
+```php
+use \Neutrino\Http\Provider\Curl\Streaming as HttpCurlStream;
+use \Neutrino\Http\Method;
+
+$curl = new HttpCurlStream;
+
+$response = $curl
+ ->get('http://www.google.com')
+ ->on(HttpCurlStream::EVENT_START, function (HttpCurlStream $curl) {
+ // Start to download response body
+ // Header are fully loaded when the event are raised
+ })
+ ->on(HttpCurlStream::EVENT_PROGRESS, function (HttpCurlStream $curl, $content) {
+ // Download progress
+ // $content contain the response part
+ })
+ ->send();
+```
+
+Transfer huge data, without overloading the php memory :
+```php
+$curl
+ ->get('http://www.google.com')
+ ->on(HttpCurlStream::EVENT_START, function (HttpCurlStream $curl) {
+ if ($curl->response->header->has('Content-Length')) {
+ header('Content-Length: ' . $curl->response->header->get('Content-Length'));
+ }
+ })
+ ->on(HttpCurlStream::EVENT_PROGRESS, function (HttpCurlStream $curl, $content) {
+ echo $content;
+ ob_flush();
+ flush();
+ // => Direct echo contents & flush the output (free memory)
+ })
+ ->send();
+```
+
+Download huge file, without overloading the php memory :
+```php
+$resource = fopen($path, 'w');
+
+$curl
+ ->get('http://www.google.com')
+ ->on(HttpCurlStream::EVENT_PROGRESS, function (HttpCurlStream $curl, $content) use ($resource) {
+ fwrite($resource, $content, strlen($content));
+ })
+ ->send();
+
+fclose($resource);
+```
+
+## StreamContext
+StreamContext make HTTP call via the php wrapper.
+
+This require you have "allow_url_fopen" configuration value set to '1'.
+
+#### How use :
+```php
+use \Neutrino\Http\Provider\StreamContext as HttpStreamCtx;
+use \Neutrino\Http\Method;
+
+$streamCtx = new HttpStreamCtx;
+
+$response = $streamCtx
+ ->get('http://www.google.com', ['foo' => 'bar'], ['Accept' => 'text/plain'])
+ ->send();
+
+$response->code; // HTTP Status Code
+```
+
+### StreamContext\Streaming
+Such as Curl\Streaming, StreamContext\Streaming allows you to work with large queries, by recovers content part by part.
+
+#### How use :
+```php
+use \Neutrino\Http\Provider\StreamContext\Streaming as HttpStreamCtxStreaming;
+use \Neutrino\Http\Method;
+
+$streamCtx = new HttpStreamCtxStreaming;
+
+$response = $streamCtx
+ ->get('http://www.google.com')
+ ->on(HttpStreamCtxStreaming::EVENT_START, function (HttpStreamCtxStreaming $streamCtx) {
+ // Start to download response body
+ // Header are fully loaded when the event are raised
+ })
+ ->on(HttpStreamCtxStreaming::EVENT_PROGRESS, function (HttpStreamCtxStreaming $streamCtx, $content) {
+ // Download progress
+ // $content contain the response part
+ })
+ ->send();
+```
+
+# Auth
+Authentication is a request component.
+
+## Auth\Basic
+Auth\Basic provides the elements to configure a call with an Basic Authorization.
+
+#### How use :
+```php
+use \Neutrino\Http\Auth\Basic as AuthBasic;
+use \Neutrino\Http\Provider\StreamContext as HttpStreamCtx;
+use \Neutrino\Http\Method;
+
+$streamCtx = new HttpStreamCtx;
+
+$response = $streamCtx
+ ->get('http://www.google.com')
+ ->setAuth(new AuthBasic('user', 'pass'))
+ ->send();
+```
+
+## Auth\Curl
+Specific for Curl provider.
+
+Auth\Curl provides the elements to build a call with Curl Auth.
+
+#### How use :
+```php
+use \Neutrino\Http\Auth\Curl as AuthCurl;
+use \Neutrino\Http\Provider\Curl as HttpCurl;
+use \Neutrino\Http\Method;
+
+$curl = new HttpCurl;
+
+$response = $curl
+ ->get('http://www.google.com')
+ ->setAuth(new AuthCurl(CURLAUTH_BASIC | CURLAUTH_DIGEST, 'user', 'pass'))
+ ->send();
+```
+
+## Custom Auth Component
+You can easily make your own Auth Component :
+
+```php
+namespace MyLib\Http\Auth;
+
+use Neutrino\Http\Request;
+use Neutrino\Http\Contract\Request\Component;
+
+class Hmac implements Component
+{
+ private $id;
+ private $value;
+
+ public function __construct($id, $value)
+ {
+ $this->id = $id;
+ $this->value = $value;
+ }
+
+ public function build(Request $request)
+ {
+ $date = date('D, d M Y H:i:s', time());
+ $signature = urlencode(base64_encode(hash_hmac('sha1', "date: $date", $this->value, true)));
+
+ $request
+ ->setHeader('Date', $date)
+ ->setHeader('Authorization', 'Signature keyId="' . $this->id . '",algorithm="hmac-sha1",signature="' . $signature . '"');
+ }
+}
+```
+```php
+use \MyLib\Http\Auth\Hmac as AuthHmac;
+use \Neutrino\Http\Provider\Curl as HttpCurl;
+use \Neutrino\Http\Method;
+
+$curl = new HttpCurl;
+
+$response = $curl
+ ->get('http://www.google.com')
+ ->setAuth(new AuthHmac('key_id', 'key_value'))
+ ->send();
+```
+
+# Response
+## Basic
+```php
+$response->code; // HTTP Status Code
+$response->status; // HTTP Status Message
+$response->header; // Response Headers
+$response->body; // Response Body
+```
+## Provider Info
+```php
+$response->errorCode; // Provider Error Code
+$response->error; // Provider Error Message
+$response->providerDatas; // All Provider Information (if available)
+```
+
+## Parse
+
+```php
+use \Neutrino\Http\Parser;
+
+// Json Body => Object
+$jsonObject = $response->parse(Parser\Json::class)->data;
+
+// Xml Body => SimpleXMLElement
+$xmlElement = $response->parse(Parser\Xml::class)->data;
+
+// Xml Body => array
+$xmlArray = $response->parse(Parser\XmlArray::class)->data;
+
+// Other exemple : (PHP7)
+$response->parse(new class implements Parser\Parserize
+{
+ public function parse($body)
+ {
+ return unserialize($body);
+ }
+});
+
+$response->data; // Unserialized body
+```
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..5251a0f
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,51 @@
+{
+ "name": "neutrino/http",
+ "description": "The Neutrino Http package.",
+ "keywords": [
+ "neutrino",
+ "nucleon",
+ "http",
+ "http client",
+ "api",
+ "json",
+ "requests",
+ "rest",
+ "restful",
+ "web service",
+ "xml",
+ "curl",
+ "stream",
+ "stream context",
+ "streaming"
+ ],
+ "license": "MIT",
+ "homepage": "https://phalcon-nucleon.github.io",
+ "support": {
+ "issues": "https://github.com/pn-neutrino/http/issues",
+ "source": "https://github.com/pn-neutrino/http"
+ },
+ "authors": [
+ {
+ "name": "Ark4ne (Guillaume Allegret)",
+ "email": "gui.allegret@gmail.com"
+ }
+ ],
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~5.7",
+ "satooshi/php-coveralls": "~1.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Neutrino\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Test\\": "tests/Test"
+ }
+ },
+ "minimum-stability": "dev"
+}
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..d063f2f
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,21 @@
+
+
+ * $headers = [
+ * 'X-Foo' => 'bar',
+ * 'Content-Type' => 'application/json',
+ * ];
+ *
+ * $curl->addMultiple($headers);
+ *
+ *
+ * @param array $fields
+ * @param bool $merge
+ *
+ * @return $this
+ */
+ public function setHeaders(array $fields, $merge = false)
+ {
+ if ($merge) {
+ $this->headers = array_merge($this->headers, $fields);
+ } else {
+ $this->headers = $fields;
+ }
+
+ return $this;
+ }
+
+ /**
+ * @return array
+ */
+ public function getHeaders()
+ {
+ return $this->headers;
+ }
+
+ /**
+ * Return the header builded for the HTTP Request
+ *
+ * @return array
+ */
+ public function build()
+ {
+ $headers = [];
+
+ foreach ($this->headers as $name => $value) {
+ $headers[] = $name . ': ' . $value;
+ }
+
+ return $headers;
+ }
+
+ /**
+ * Count elements of an object
+ * @link http://php.net/manual/en/countable.count.php
+ * @return int The custom count as an integer.
+ *
+ * The return value is cast to an integer.
+ * @since 5.1.0
+ */
+ public function count()
+ {
+ return count($this->headers);
+ }
+
+ public function parse($raw)
+ {
+ if(is_string($raw)){
+ $raw = array_filter(explode("\r\n", $raw));
+ }
+
+ foreach ($raw as $header) {
+ if (preg_match('%^HTTP/(\d(?:\.\d)?)\s+(\d{3})\s?+(.+)?$%i', $header, $status)) {
+ $this->version = $status[1];
+ $this->code = intval($status[2]);
+ $this->status = isset($status[3]) ? $status[3] : '';
+ } else {
+ $field = explode(':', $header, 2);
+
+ $this->set(trim($field[0]), isset($field[1]) ? trim($field[1]) : null);
+ }
+ }
+ }
+}
diff --git a/src/Http/Parser/Json.php b/src/Http/Parser/Json.php
new file mode 100644
index 0000000..02f0467
--- /dev/null
+++ b/src/Http/Parser/Json.php
@@ -0,0 +1,13 @@
+setOption(CURLOPT_TIMEOUT, $timeout);
+ }
+
+ /**
+ * Definie le timeout de connexion de la requete
+ * Applique l'option CURL 'CURLOPT_CONNECTTIMEOUT'
+ *
+ * @param int $timeout
+ *
+ * @return $this
+ */
+ public function setConnectTimeout($timeout)
+ {
+ return $this->setOption(CURLOPT_CONNECTTIMEOUT, $timeout);
+ }
+
+ /**
+ * @return \Neutrino\Http\Response
+ * @throws \Exception
+ */
+ protected function makeCall()
+ {
+ try {
+ $ch = curl_init();
+
+ $this->curlOptions($ch);
+
+ $this->curlExec($ch);
+
+ $this->curlInfos($ch);
+
+ if ($this->response->errorCode) {
+ throw new HttpException($this->response->error, $this->response->errorCode);
+ }
+
+ return $this->response;
+ } finally {
+ if (isset($ch) && is_resource($ch)) {
+ curl_close($ch);
+ }
+ }
+ }
+
+ protected function curlOptions($ch)
+ {
+ $method = $this->method;
+
+ if ($method === Method::HEAD) {
+ curl_setopt($ch, CURLOPT_NOBODY, true);
+ }
+
+ // Default Options
+ curl_setopt_array($ch,
+ [
+ CURLOPT_URL => $this->uri->build(),
+ CURLOPT_CUSTOMREQUEST => $method,
+ CURLOPT_AUTOREFERER => true,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_MAXREDIRS => 20,
+ CURLOPT_HEADER => false,
+ CURLOPT_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
+ CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTP | CURLPROTO_HTTPS,
+ CURLOPT_CONNECTTIMEOUT => 30,
+ CURLOPT_TIMEOUT => 30,
+ CURLOPT_HEADERFUNCTION => [$this, 'curlHeaderFunction'],
+ ]);
+
+ curl_setopt_array($ch, $this->options);
+ }
+
+ /**
+ * @param resource $ch
+ *
+ * @return void
+ */
+ protected function curlExec($ch)
+ {
+ $result = curl_exec($ch);
+
+ $this->response->body = $result;
+ }
+
+ protected function curlHeaderFunction($ch, $raw)
+ {
+ if ($this->response->code === null) {
+ $this->response->code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ }
+
+ $this->response->header->parse($raw);
+
+ return strlen($raw);
+ }
+
+ public function curlInfos($ch)
+ {
+ if ($this->response->code === null) {
+ $this->response->code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ }
+
+ if (($errno = curl_errno($ch)) !== 0) {
+ $this->response->errorCode = curl_errno($ch);
+ $this->response->error = curl_error($ch);
+ }
+
+ $this->response->providerDatas = curl_getinfo($ch);
+ }
+
+ /**
+ * Construit les parametres de la requete.
+ * HTTP [POST, PUT, PATCH] : Applique l'option CURL "CURLOPT_POSTFIELDS"
+ * HTTP [...] : Contruit l'url de la requete
+ *
+ * @return $this
+ */
+ protected function buildParams()
+ {
+ if ($this->isPostMethod()) {
+ if ($this->isJsonRequest()) {
+ return $this
+ ->setOption(CURLOPT_POSTFIELDS, json_encode($this->params))
+ ->setHeader('Content-Type', 'application/json');
+ }
+
+ return $this
+ ->setOption(CURLOPT_POSTFIELDS, http_build_query($this->params))
+ ->setHeader('Content-Type', 'application/x-www-form-urlencoded');
+ }
+
+ return $this->buildUrl();
+ }
+
+ /**
+ * Construit les cookies de la requete
+ * Applique l'option CURL 'CURLOPT_COOKIE'
+ *
+ * @return $this
+ */
+ protected function buildCookies()
+ {
+ if (!empty($this->cookies)) {
+ return $this->setOption(CURLOPT_COOKIE, $this->getCookies(true));
+ }
+
+ return $this;
+ }
+
+ /**
+ * Construit les headers de la requete.
+ * Applique l'option CURL "CURLOPT_HTTPHEADER"
+ *
+ * @return $this
+ */
+ protected function buildHeaders()
+ {
+ if (!empty($this->header->getHeaders())) {
+ return $this->setOption(CURLOPT_HTTPHEADER, $this->header->build());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Definie le proxy de la requete
+ * Applique les options CURL : CURLOPT_PROXY, CURLOPT_PROXYPORT, CURLOPT_PROXYUSERPWD
+ *
+ * @return $this
+ */
+ protected function buildProxy()
+ {
+ if (isset($this->proxy['host'])) {
+ $this
+ ->setOption(CURLOPT_PROXY, $this->proxy['host'])
+ ->setOption(CURLOPT_PROXYPORT, $this->proxy['port']);
+
+ if (isset($this->proxy['access'])) {
+ $this->setOption(CURLOPT_PROXYUSERPWD, $this->proxy['access']);
+ }
+ }
+
+ return $this;
+ }
+}
diff --git a/src/Http/Provider/Curl/Streaming.php b/src/Http/Provider/Curl/Streaming.php
new file mode 100644
index 0000000..0024144
--- /dev/null
+++ b/src/Http/Provider/Curl/Streaming.php
@@ -0,0 +1,60 @@
+hasStarted) {
+ $this->hasStarted = true;
+
+ $this->emitter->fire(self::EVENT_START, [$this]);
+ }
+
+ $length = strlen($content);
+
+ $this->emitter->fire(self::EVENT_PROGRESS, [$this, $content]);
+
+ return $length;
+ }
+
+ public function send()
+ {
+ parent::send();
+
+ $this->emitter->fire(self::EVENT_FINISH, [$this]);
+ }
+
+ protected function curlOptions($ch)
+ {
+ parent::curlOptions($ch);
+
+ curl_setopt_array($ch,
+ [
+ CURLOPT_RETURNTRANSFER => false,
+ CURLOPT_WRITEFUNCTION => [$this, 'curlWriteFunction'],
+ ]);
+
+ if (isset($this->bufferSize)) {
+ curl_setopt($ch, CURLOPT_BUFFERSIZE, $this->bufferSize);
+ }
+ }
+}
diff --git a/src/Http/Provider/Exception.php b/src/Http/Provider/Exception.php
new file mode 100644
index 0000000..b84796c
--- /dev/null
+++ b/src/Http/Provider/Exception.php
@@ -0,0 +1,8 @@
+setOption('timeout', $timeout);
+ }
+
+ /**
+ * @param $errno
+ * @param $errstr
+ *
+ * @throws HttpException
+ */
+ protected function errorHandler($errno, $errstr)
+ {
+ $this->response->error = $errstr;
+ $this->response->errorCode = $errno;
+
+ throw new HttpException($errstr, $errno);
+ }
+
+ protected function makeCall()
+ {
+ try {
+ $context = stream_context_create();
+
+ $this->streamContextOptions($context);
+
+ set_error_handler([$this, 'errorHandler']);
+
+ $content = $this->streamContextExec($context);
+
+ restore_error_handler();
+
+ $this->response->body = $content;
+
+ return $this->response;
+ } finally {
+ $context = null;
+ }
+ }
+
+ protected function streamContextOptions($context)
+ {
+ stream_context_set_option($context, ['http' => array_merge([
+ 'follow_location' => 1,
+ 'max_redirects' => 20,
+ 'timeout' => 30,
+ 'ignore_errors' => true,
+ ], $this->options, ['method' => $this->method])]);
+ }
+
+ protected function streamContextExec($context)
+ {
+ if ($this->method !== Method::HEAD) {
+ $content = file_get_contents($this->uri->build(), false, $context);
+
+ $this->streamContextParseHeader($http_response_header);
+
+ return $content;
+ }
+
+ try {
+ $handler = fopen($this->uri->build(), 'r', null, $context);
+
+ $this->streamContextParseHeader($http_response_header);
+
+ return '';
+ } finally {
+ if (isset($handler) && is_resource($handler)) {
+ fclose($handler);
+ }
+ }
+ }
+
+ protected function streamContextParseHeader($headers)
+ {
+ $this->response->header->parse($headers);
+
+ $this->response->code = $this->response->header->code;
+ }
+
+ /**
+ * Construit les parametres de la requete.
+ *
+ * @return $this
+ */
+ protected function buildParams()
+ {
+ if ($this->isPostMethod()) {
+ if ($this->isJsonRequest()) {
+ return $this
+ ->setHeader('Content-Type', 'application/json')
+ ->setOption('content', json_encode($this->params));
+ }
+
+ return $this
+ ->setHeader('Content-Type', 'application/x-www-form-urlencoded')
+ ->setOption('content', http_build_query($this->params));
+ }
+
+ return $this->buildUrl();
+ }
+
+ /**
+ * Construit les headers de la requete.
+ *
+ * @return $this
+ */
+ protected function buildHeaders()
+ {
+ $headers = $this->header->build();
+
+ return $this->setOption('header', implode(PHP_EOL, $headers));
+ }
+
+ /**
+ * Construit le proxy de la requete
+ *
+ * @return $this
+ */
+ protected function buildProxy()
+ {
+ if (isset($this->proxy['host'])) {
+ $uri = new Uri([
+ 'scheme' => 'tcp',
+ 'host' => $this->proxy['host'],
+ 'port' => isset($this->proxy['port']) ? $this->proxy['port'] : 80
+ ]);
+
+ if (isset($this->proxy['access'])) {
+ $uri->user = $this->proxy['access'];
+ }
+
+ $this->setOption('proxy', $uri->build());
+ }
+
+ return $this;
+ }
+
+ /**
+ * Construit les cookies de la requete
+ *
+ * @return $this
+ */
+ protected function buildCookies()
+ {
+ if (!empty($this->cookies)) {
+ return $this->setHeader('Cookie', $this->getCookies(true));
+ }
+
+ return $this;
+ }
+}
diff --git a/src/Http/Provider/StreamContext/Streaming.php b/src/Http/Provider/StreamContext/Streaming.php
new file mode 100644
index 0000000..c7a37b5
--- /dev/null
+++ b/src/Http/Provider/StreamContext/Streaming.php
@@ -0,0 +1,44 @@
+getEmitter();
+
+ try {
+ $handler = fopen($this->uri->build(), 'r', null, $context);
+
+ $this->streamContextParseHeader($http_response_header);
+
+ $this->response->providerDatas = stream_get_meta_data($handler);
+
+ $emitter->fire(self::EVENT_START, [$this]);
+
+ $buffer = $this->bufferSize ? $this->bufferSize : 4096;
+
+ while (!feof($handler)) {
+ $emitter->fire(self::EVENT_PROGRESS, [$this, stream_get_contents($handler, $buffer)]);
+ }
+
+ $this->response->providerDatas = stream_get_meta_data($handler);
+
+ $emitter->fire(self::EVENT_FINISH, [$this]);
+
+ return true;
+ } finally {
+ if (isset($handler) && is_resource($handler)) {
+ fclose($handler);
+ }
+ }
+ }
+
+}
diff --git a/src/Http/Request.php b/src/Http/Request.php
new file mode 100644
index 0000000..4b135f1
--- /dev/null
+++ b/src/Http/Request.php
@@ -0,0 +1,606 @@
+header = new Header();
+ }
+
+ /**
+ * Retour l'url de la requete, construite avec les parametres si la method HTTP n'est pas POST, PUT, PATCH
+ *
+ * @return Uri
+ */
+ public function getUri()
+ {
+ if ($this->isPostMethod()) {
+ return $this->uri;
+ }
+
+ return $this->buildUrl()->uri;
+ }
+
+ /**
+ * Definie l'url de la requete
+ *
+ * @param string|Uri $uri
+ *
+ * @return $this
+ */
+ public function setUri($uri)
+ {
+ $this->uri = new Uri($uri);
+
+ return $this;
+ }
+
+ /**
+ * Retourne la method HTTP de la requete
+ *
+ * @return string
+ */
+ public function getMethod()
+ {
+ return $this->method;
+ }
+
+ /**
+ * Definie la method HTTP de la requete
+ *
+ * @param string $method
+ *
+ * @return $this
+ */
+ public function setMethod($method)
+ {
+ $this->method = $method;
+
+ return $this;
+ }
+
+ /**
+ * Renvoie si l'on fais un appel HTTP basée sur le POST
+ *
+ * @return bool
+ */
+ protected function isPostMethod()
+ {
+ $method = $this->getMethod();
+
+ return $method == 'POST' || $method == 'PUT' || $method == 'PATCH';
+ }
+
+ /**
+ * Est-ce que l'on doit envoyer un body "json" contenant les parametres de la requete
+ *
+ * @return bool
+ */
+ public function isJsonRequest()
+ {
+ return $this->jsonRequest;
+ }
+
+ /**
+ * Definie si l'on doit envoyer un body "json" contenant les parametres de la requete
+ *
+ * @param bool $jsonRequest
+ *
+ * @return $this
+ */
+ public function setJsonRequest($jsonRequest)
+ {
+ $this->jsonRequest = $jsonRequest;
+
+ return $this;
+ }
+
+ /**
+ * Retourne les parametres de la requete
+ *
+ * @return array
+ */
+ public function getParams()
+ {
+ return $this->params;
+ }
+
+ /**
+ * Definie, ou ajoute, des parametres de la requete
+ *
+ * @param array $parameters
+ * @param bool $merge Est-ce que l'on ajout les parametres aux parametres existant, ou les ecrases
+ *
+ * @return $this
+ */
+ public function setParams($parameters, $merge = false)
+ {
+ if ($merge) {
+ $this->params = array_merge($this->params, $parameters);
+ } else {
+ $this->params = $parameters;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Ajout un parametre à la requete
+ *
+ * @param string $name
+ * @param string|array $value
+ *
+ * @return $this
+ */
+ public function setParam($name, $value)
+ {
+ $this->params[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Construit l'url de la requete, Si la method HTTP n'est pas [POST, PUT, PATCH]
+ *
+ * @return $this
+ */
+ protected function buildUrl()
+ {
+ if ($this->isPostMethod()) {
+ return $this;
+ }
+
+ return $this->extendUrl($this->params);
+ }
+
+ /**
+ * Ajout des parametres en GET à l'url
+ *
+ * @param array $parameters
+ *
+ * @return $this
+ */
+ public function extendUrl(array $parameters = [])
+ {
+ if (!empty($parameters)) {
+ $this->uri->extendQuery($parameters);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Definie, ou ajoute, des headers à la requete
+ *
+ * @param array $headers
+ * @param bool $merge Est-ce que l'on ajout les parametres aux parametres existant, ou les ecrases
+ *
+ * @return $this
+ */
+ public function setHeaders($headers, $merge = false)
+ {
+ $this->header->setHeaders($headers, $merge);
+
+ return $this;
+ }
+
+ /**
+ * Ajout un header à la requete
+ *
+ * @param string $name
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setHeader($name, $value)
+ {
+ $this->header->set($name, $value);
+
+ return $this;
+ }
+
+ /**
+ * Retourne les informations de proxy
+ *
+ * @return array
+ */
+ public function getProxy()
+ {
+ return $this->proxy;
+ }
+
+ /**
+ * Definie les informations de proxy
+ *
+ * @param string $host
+ * @param int $port
+ * @param string $access
+ *
+ * @return $this
+ */
+ public function setProxy($host, $port = 8080, $access = null)
+ {
+ $this->proxy = [
+ 'host' => $host,
+ 'port' => $port,
+ 'access' => $access,
+ ];
+
+ return $this;
+ }
+
+ /**
+ * Retourne les informations d'authentification
+ *
+ * @return Component|null
+ */
+ public function getAuth()
+ {
+ return $this->auth;
+ }
+
+ /**
+ * Definie les informations d'authentification
+ *
+ * @param \Neutrino\Http\Contract\Request\Component $authComponent
+ *
+ * @return $this
+ */
+ public function setAuth(Component $authComponent)
+ {
+ $this->auth = $authComponent;
+
+ return $this;
+ }
+
+ /**
+ * Construit les informations d'authentification de la requete
+ *
+ * @return $this
+ */
+ protected function buildAuth()
+ {
+ if (isset($this->auth)) {
+ $this->auth->build($this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Retourne les cookies
+ *
+ * @param bool $format Retourne les cookies formatés
+ *
+ * @return array|string
+ */
+ public function getCookies($format = false)
+ {
+ if ($format) {
+ return implode(';', $this->cookies);
+ }
+
+ return $this->cookies;
+ }
+
+ /**
+ * @param array $cookies
+ * @param bool $merge Est-ce que l'on ajout les $cookies aux $cookies existant, ou les ecrases
+ *
+ * @return $this
+ */
+ public function setCookies($cookies, $merge = false)
+ {
+ if ($merge) {
+ $this->cookies = array_merge($this->cookies, $cookies);
+ } else {
+ $this->cookies = $cookies;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Ajoute un cookie a la requete
+ *
+ * @param null|string $key
+ * @param string $value
+ *
+ * @return $this
+ * @throws \InvalidArgumentException
+ */
+ public function setCookie($key, $value)
+ {
+ if (is_null($key)) {
+ $this->cookies[] = $value;
+ } else {
+ $this->cookies[$key] = $value;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Retourne les options
+ *
+ * @return array
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * Definie, ou ajoute, des options
+ *
+ * @param array $options
+ * @param bool $merge Est-ce que l'on ajoute les options aux options existantes, ou les ecrases
+ *
+ * @return $this
+ */
+ public function setOptions($options, $merge = false)
+ {
+ if ($merge) {
+ $this->options = array_merge($this->options, $options);
+ } else {
+ $this->options = $options;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Ajout une option CURL
+ *
+ * @param string $name
+ * @param string $value
+ *
+ * @return $this
+ */
+ public function setOption($name, $value)
+ {
+ $this->options[$name] = $value;
+
+ return $this;
+ }
+
+ /**
+ * @return \Neutrino\Http\Response
+ */
+ public function send()
+ {
+ $this
+ ->buildParams()
+ ->buildAuth()
+ ->buildProxy()
+ ->buildCookies()
+ ->buildHeaders();
+
+ $this->response = new Response();
+
+ return $this->makeCall();
+ }
+
+ /**
+ * @param string $method
+ * @param string $uri
+ * @param array $params
+ * @param array $headers
+ *
+ * @return $this
+ */
+ public function request($method, $uri, array $params = [], array $headers = [])
+ {
+ $this
+ ->setMethod($method)
+ ->setUri($uri);
+
+ if (!empty($params)) {
+ $this->setParams($params, true);
+ }
+ if (!empty($headers)) {
+ $this->setHeaders($headers, true);
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param string $uri
+ * @param array $params
+ * @param array $headers
+ *
+ * @return $this
+ */
+ public function get($uri, array $params = [], array $headers = [])
+ {
+ return $this->request(Method::GET, $uri, $params, $headers);
+ }
+
+ /**
+ * @param string $uri
+ * @param array $params
+ * @param array $headers
+ *
+ * @return $this
+ */
+ public function head($uri, array $params = [], array $headers = [])
+ {
+ return $this->request(Method::HEAD, $uri, $params, $headers);
+ }
+
+ /**
+ * @param string $uri
+ * @param array $params
+ * @param array $headers
+ *
+ * @return $this
+ */
+ public function delete($uri, array $params = [], array $headers = [])
+ {
+ return $this->request(Method::DELETE, $uri, $params, $headers);
+ }
+
+ /**
+ * @param string $uri
+ * @param array $params
+ * @param array $headers
+ * @param bool $json
+ *
+ * @return $this
+ */
+ public function post($uri, array $params = [], array $headers = [], $json = false)
+ {
+ return $this->request(Method::POST, $uri, $params, $headers)->setJsonRequest($json);
+ }
+
+ /**
+ * @param string $uri
+ * @param array $params
+ * @param array $headers
+ * @param bool $json
+ *
+ * @return $this
+ */
+ public function put($uri, array $params = [], array $headers = [], $json = false)
+ {
+ return $this->request(Method::PUT, $uri, $params, $headers)->setJsonRequest($json);
+ }
+
+ /**
+ * @param string $uri
+ * @param array $params
+ * @param array $headers
+ * @param bool $json
+ *
+ * @return $this
+ */
+ public function patch($uri, array $params = [], array $headers = [], $json = false)
+ {
+ return $this->request(Method::PATCH, $uri, $params, $headers)->setJsonRequest($json);
+ }
+
+ /**
+ * Definie le timeout de la requete
+ *
+ * @param int $timeout
+ *
+ * @return $this
+ */
+ abstract public function setTimeout($timeout);
+
+ /**
+ * @return \Neutrino\Http\Response
+ */
+ abstract protected function makeCall();
+
+ /**
+ * Construit les parametres de la requete.
+ *
+ * @return $this
+ */
+ abstract protected function buildParams();
+
+ /**
+ * Construit les headers de la requete.
+ *
+ * @return $this
+ */
+ abstract protected function buildHeaders();
+
+ /**
+ * Construit le proxy de la requete
+ *
+ * @return $this
+ */
+ abstract protected function buildProxy();
+
+ /**
+ * Construit les cookies de la requete
+ *
+ * @return $this
+ */
+ abstract protected function buildCookies();
+}
diff --git a/src/Http/Response.php b/src/Http/Response.php
new file mode 100644
index 0000000..3a2359f
--- /dev/null
+++ b/src/Http/Response.php
@@ -0,0 +1,93 @@
+header = $header === null ? new Header() : $header;
+ }
+
+ /**
+ * Check if the HTTP Response is valid (2xx)
+ *
+ * @return bool
+ */
+ public function isOk()
+ {
+ return $this->code >= 200 && $this->code < 300;
+ }
+
+ /**
+ * Check if an HTTP error append
+ *
+ * @return bool
+ */
+ public function isFail()
+ {
+ return $this->code < 200 || $this->code >= 300;
+ }
+
+ /**
+ * Check if an CURL error append
+ *
+ * @return bool
+ */
+ public function isError()
+ {
+ return isset($this->errorCode) && ($this->errorCode !== null || $this->errorCode !== 0);
+ }
+
+ /**
+ * @param Parserize|string $parser
+ *
+ * @return $this
+ * @throws \RuntimeException
+ */
+ public function parse($parser)
+ {
+ if (is_string($parser)) {
+ $parser = new $parser;
+ }
+
+ if ($parser instanceof Parserize) {
+ $this->data = $parser->parse($this->body);
+
+ return $this;
+ }
+
+ throw new \RuntimeException(__METHOD__ . ': $parserize must implement ' . Parserize::class);
+ }
+}
diff --git a/src/Http/Standard/Method.php b/src/Http/Standard/Method.php
new file mode 100644
index 0000000..e04c7a3
--- /dev/null
+++ b/src/Http/Standard/Method.php
@@ -0,0 +1,16 @@
+ 'Continue',
+ self::SWITCHING_PROTOCOLS => 'Switching Protocols',
+
+ // Success 2xx
+ self::OK => 'OK',
+ self::CREATED => 'Created',
+ self::ACCEPTED => 'Accepted',
+ self::NON_AUTHORITATIVE_INFORMATION => 'Non-Authoritative Information',
+ self::NO_CONTENT => 'No Content',
+ self::RESET_CONTENT => 'Reset Content',
+ self::PARTIAL_CONTENT => 'Partial Content',
+ self::MULTI_STATUS => 'Multi-Status',
+ self::ALREADY_REPORTED => 'Already Reported',
+ self::IM_USED => 'IM Used',
+
+ // Redirection 3xx
+ self::MULTIPLE_CHOICES => 'Multiple Choices',
+ self::MOVED_PERMANENTLY => 'Moved Permanently',
+ self::FOUND => 'Found', // 1.1
+ self::SEE_OTHER => 'See Other',
+ self::NOT_MODIFIED => 'Not Modified',
+ self::USE_PROXY => 'Use Proxy',
+ // 306 is deprecated but reserved
+ self::TEMPORARY_REDIRECT => 'Temporary Redirect',
+ self::PERMANENT_REDIRECT => 'Permanent Redirect',
+
+ // Client Error 4xx
+ self::BAD_REQUEST => 'Bad Request',
+ self::BAD_UNAUTHORIZED => 'Unauthorized',
+ self::PAYMENT_REQUIRED => 'Payment Required',
+ self::FORBIDDEN => 'Forbidden',
+ self::NOT_FOUND => 'Not Found',
+ self::METHOD_NOT_ALLOWED => 'Method Not Allowed',
+ self::NOT_ACCEPTABLE => 'Not Acceptable',
+ self::PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required',
+ self::REQUEST_TIMEOUT => 'Request Timeout',
+ self::CONFLICT => 'Conflict',
+ self::GONE => 'Gone',
+ self::LENGTH_REQUIRED => 'Length Required',
+ self::PRECONDITION_FAILED => 'Precondition Failed',
+ self::REQUEST_ENTITY_TOO_LARGE => 'Request Entity Too Large',
+ self::REQUEST_URI_TOO_LONG => 'Request-URI Too Long',
+ self::UNSUPPORTED_MEDIA_TYPE => 'Unsupported Media Type',
+ self::REQUEST_RANGE_NOT_SATISFIABLE => 'Requested Range Not Satisfiable',
+ self::EXPECTATION_FAILED => 'Expectation Failed',
+ self::I_AM_A_TEAPOT => "I'm a teapot",
+ self::UNPROCESSABLE_ENTITY => 'Unprocessable Entity',
+ self::LOCKED => 'Locked',
+ self::FAILED_DEPENDENCY => 'Failed Dependency',
+ self::UPDATE_REQUIRED => 'Upgrade Required',
+ self::PRECONDITION_REQUIRED => 'Precondition Required',
+ self::TOO_MANY_REQUESTS => 'Too Many Requests',
+ self::REQUEST_HEADER_FIELDS_TOO_LARGE => 'Request Header Fields Too Large',
+
+ // Server Error 5xx
+ self::INTERNAL_SERVER_ERROR => 'Internal Server Error',
+ self::NOT_IMPLEMENTED => 'Not Implemented',
+ self::BAD_GATEWAY => 'Bad Gateway',
+ self::SERVICE_UNAVAILABLE => 'Service Unavailable',
+ self::GATEWAY_TIMEOUT => 'Gateway Timeout',
+ self::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported',
+ self::VARIANT_ALSO_NEGOTIATES => 'Variant Also Negotiates',
+ self::INSUFFICIENT_STORAGE => 'Insufficient Storage',
+ self::LOOP_DETECTED => 'Loop Detected',
+ self::BANDWIDTH_LIMIT_EXCEED => 'Bandwidth Limit Exceeded',
+ self::NOT_EXTENDED => 'Not Extended',
+ self::NETWORK_AUTHENTICATION_REQUIRED => 'Network Authentication Required',
+ ];
+
+ /**
+ * Get response message
+ *
+ * @param int $code
+ *
+ * @return string
+ */
+ public static function message($code)
+ {
+ return (isset(self::$messages[$code])) ? self::$messages[$code] : '';
+ }
+}
diff --git a/src/Http/Uri.php b/src/Http/Uri.php
new file mode 100644
index 0000000..cfcfd7e
--- /dev/null
+++ b/src/Http/Uri.php
@@ -0,0 +1,160 @@
+parts = parse_url($uri);
+ if (!empty($this->parts['query'])) {
+ $query = [];
+ parse_str($this->parts['query'], $query);
+ $this->parts['query'] = $query;
+ }
+ return;
+ }
+ if (is_array($uri)) {
+ $this->parts = $uri;
+ return;
+ }
+ if ($uri instanceof self) {
+ $this->parts = $uri->parts;
+ }
+ }
+
+ public function __toString()
+ {
+ return $this->build();
+ }
+
+ public function __unset($name)
+ {
+ unset($this->parts[$name]);
+ }
+
+ public function __set($name, $value)
+ {
+ $this->parts[$name] = $value;
+ }
+
+ public function __get($name)
+ {
+ return $this->parts[$name];
+ }
+
+ public function __isset($name)
+ {
+ return isset($this->parts[$name]);
+ }
+
+ public function build()
+ {
+ $uri = '';
+ $parts = $this->parts;
+ if (!empty($parts['scheme'])) {
+ $uri .= $parts['scheme'] . ':';
+ if (!empty($parts['host'])) {
+ $uri .= '//';
+ if (!empty($parts['user'])) {
+ $uri .= $parts['user'];
+ if (!empty($parts['pass'])) {
+ $uri .= ':' . $parts['pass'];
+ }
+ $uri .= '@';
+ }
+ $uri .= $parts['host'];
+
+ if (!empty($parts['port'])) {
+ $uri .= ':' . $parts['port'];
+ }
+ }
+ }
+ if (!empty($parts['path'])) {
+ $uri .= $parts['path'];
+ }
+ if (!empty($parts['query'])) {
+ $uri .= '?' . $this->buildQuery($parts['query']);
+ }
+ if (!empty($parts['fragment'])) {
+ $uri .= '#' . $parts['fragment'];
+ }
+ return $uri;
+ }
+
+ public function buildQuery($query)
+ {
+ return (is_array($query) ? http_build_query($query) : $query);
+ }
+
+ public function resolve($uri)
+ {
+ $newUri = new self($this);
+ $newUri->extend($uri);
+ return $newUri;
+ }
+
+ public function extend($uri)
+ {
+ if (!$uri instanceof self) {
+ $uri = new self($uri);
+ }
+ $this->parts = array_merge(
+ $this->parts,
+ array_diff_key($uri->parts, array_flip(['query', 'path']))
+ );
+ if (!empty($uri->parts['query'])) {
+ $this->extendQuery($uri->parts['query']);
+ }
+ if (!empty($uri->parts['path'])) {
+ $this->extendPath($uri->parts['path']);
+ }
+ return $this;
+ }
+
+ public function extendQuery(array $params = null)
+ {
+ $query = empty($this->parts['query']) ? [] : $this->parts['query'];
+ $params = empty($params) ? [] : $params;
+ $this->parts['query'] = array_merge($query, $params);
+ return $this;
+ }
+
+ public function extendPath($path)
+ {
+ if (empty($path)) {
+ return $this;
+ }
+ if (!strncmp($path, '/', 1)) {
+ $this->parts['path'] = $path;
+ return $this;
+ }
+ if (empty($this->parts['path'])) {
+ $this->parts['path'] = '/' . $path;
+ return $this;
+ }
+ $this->parts['path'] = substr($this->parts['path'], 0, strrpos($this->parts['path'], '/') + 1) . $path;
+ return $this;
+ }
+}
diff --git a/tests/Test/Auth/CurlTest.php b/tests/Test/Auth/CurlTest.php
new file mode 100644
index 0000000..e3ce8c3
--- /dev/null
+++ b/tests/Test/Auth/CurlTest.php
@@ -0,0 +1,68 @@
+ [CURLAUTH_ANY, 'user', 'pass'],
+ 'ANYSAFE' => [CURLAUTH_ANYSAFE, 'user', 'pass'],
+ 'BASIC' => [CURLAUTH_BASIC, 'user', 'pass'],
+ 'DIGEST' => [CURLAUTH_DIGEST, 'user', 'pass'],
+ 'NTLM' => [CURLAUTH_NTLM, 'user', 'pass'],
+ //'NTLM_WB' => [CURLAUTH_NTLM_WB, 'user', 'pass'],
+ //'NEGOTIATE' => [CURLAUTH_NEGOTIATE, 'user', 'pass'],
+ 'GSSNEGOTIATE' => [CURLAUTH_GSSNEGOTIATE, 'user', 'pass'],
+
+ 'BASIC|DIGEST' => [CURLAUTH_BASIC | CURLAUTH_DIGEST, 'user', 'pass'],
+ 'BASIC|DIGEST|GSSNEGOTIATE' => [CURLAUTH_BASIC | CURLAUTH_DIGEST | CURLAUTH_GSSNEGOTIATE, 'user', 'pass'],
+ ];
+ }
+
+ /**
+ * @dataProvider dataProvider
+ *
+ * @param $type
+ */
+ public function testType($type)
+ {
+ $this->assertInstanceOf(Auth\Curl::class, new Auth\Curl($type, '', ''));
+ }
+
+ /**
+ * @dataProvider dataProvider
+ *
+ * @param $type
+ */
+ public function testBuild($type, $user, $pass)
+ {
+ $request = new Curl();
+
+ $auth = new Auth\Curl($type, $user, $pass);
+
+ $auth->build($request);
+
+ $options = $request->getOptions();
+
+ $this->assertArrayHasKey(CURLOPT_HTTPAUTH, $options);
+ $this->assertArrayHasKey(CURLOPT_USERPWD, $options);
+
+ $this->assertEquals($type, $options[CURLOPT_HTTPAUTH]);
+ $this->assertEquals($user . ':' . $pass, $options[CURLOPT_USERPWD]);
+ }
+
+ /**
+ * @expectedException \Neutrino\Http\Auth\Exception
+ */
+ public function testWrongType()
+ {
+ new Auth\Curl(100000, '', '');
+ }
+}
diff --git a/tests/Test/Event/EmitterTest.php b/tests/Test/Event/EmitterTest.php
new file mode 100644
index 0000000..465b160
--- /dev/null
+++ b/tests/Test/Event/EmitterTest.php
@@ -0,0 +1,45 @@
+assertTrue($emitter->attach('test', $closure));
+
+ $emitter->fire('test');
+ $this->assertEquals(1, $watcher);
+
+ $this->assertTrue($emitter->attach('test', $closure));
+
+ $emitter->fire('test');
+ $this->assertEquals(3, $watcher);
+
+ $this->assertTrue($emitter->detach('test', $closure));
+
+ $emitter->fire('test');
+ $this->assertEquals(4, $watcher);
+
+ $this->assertTrue($emitter->detach('test', $closure));
+ $this->assertFalse($emitter->detach('test', $closure));
+
+ $emitter->fire('test');
+ $this->assertEquals(4, $watcher);
+
+ $this->assertTrue($emitter->clear('test'));
+ $this->assertFalse($emitter->clear('test'));
+ $this->assertFalse($emitter->detach('test', $closure));
+ }
+}
\ No newline at end of file
diff --git a/tests/Test/HeaderTest.php b/tests/Test/HeaderTest.php
new file mode 100644
index 0000000..892aee7
--- /dev/null
+++ b/tests/Test/HeaderTest.php
@@ -0,0 +1,136 @@
+assertEquals($header, $header->set('test', 'test'));
+
+ $header->set('test', 'value');
+ $header->set('test_1', 'value_1');
+
+ $this->assertEquals(['test' => 'value', 'test_1' => 'value_1'], $header->getHeaders());
+
+ $this->assertTrue($header->has('test'));
+ $this->assertTrue($header->has('test_1'));
+ $this->assertFalse($header->has('test_2'));
+
+ $this->assertEquals('value', $header->get('test'));
+ $this->assertEquals('value_1', $header->get('test_1'));
+ $this->assertEquals(null, $header->get('test_2'));
+ $this->assertEquals('value', $header->get('test', 'default'));
+ $this->assertEquals('default', $header->get('test_2', 'default'));
+
+ $this->assertEquals(2, $header->count());
+ $this->assertCount(2, $header);
+
+ $this->assertEquals($header, $header->remove('test'));
+
+ $this->assertEquals(null, $header->get('test'));
+
+ $this->assertEquals(1, $header->count());
+ $this->assertCount(1, $header);
+
+ $this->assertEquals($header, $header->setHeaders([
+ 'test' => 'value',
+ 'test_2' => 'value_2'
+ ], true));
+
+ $this->assertEquals('value_2', $header->get('test_2', 'default'));
+
+ $this->assertEquals(3, $header->count());
+ $this->assertCount(3, $header);
+
+ $this->assertEquals($header, $header->setHeaders([
+ 'test' => 'value',
+ 'test_2' => 'value_2'
+ ]));
+
+ $this->assertEquals(2, $header->count());
+ $this->assertCount(2, $header);
+ }
+
+ public function testBuild()
+ {
+ $header = new Header();
+
+ $header->set('test', 'test');
+ $header->set('test', 'value');
+ $header->set('test_1', 'value_1');
+
+ $this->assertEquals([
+ 'test: value',
+ 'test_1: value_1',
+ ], $header->build());
+ }
+
+ public function dataParse()
+ {
+ return [
+ [null, null, null, [], ""],
+ [null, null, null, ['Date' => 'Thu, 27 Apr 2017 13:42:19 GMT'], PHP_EOL . "Date: Thu, 27 Apr 2017 13:42:19 GMT" . PHP_EOL],
+ ['1.1', 200, 'OK', [], "HTTP/1.1 200 OK"],
+ ['1.1', 200, 'Success', [], "HTTP/1.1 200 Success"],
+ ['1.1', 302, 'Redirect', [], "HTTP/1.1 302 Redirect"],
+ ['1.1', 418, 'I\'m a teapot', [], "HTTP/1.1 418 I'm a teapot"],
+ ['1.1', 526, 'Whoops', [], "HTTP/1.1 526 Whoops"],
+ [null, null, null, [
+ 'Date' => 'Thu, 27 Apr 2017 13:42:19 GMT',
+ 'X-Powered-By' => 'PHP/7.0.10',
+ 'Content-Length' => '5524',
+ 'Server' => 'Apache/2.4.23 (Win64) PHP/7.0.10',
+ ], [
+ "Date: Thu, 27 Apr 2017 13:42:19 GMT",
+ "X-Powered-By: PHP/7.0.10\r\n Content-Length: 5524",
+ "Server: Apache/2.4.23 (Win64) PHP/7.0.10",
+ ]],
+ ['1.1', 200, 'OK', [
+ 'Date' => 'Thu, 27 Apr 2017 13:42:19 GMT',
+ 'Server' => 'Apache/2.4.23 (Win64) PHP/7.0.10',
+ 'X-Powered-By' => 'PHP/7.0.10',
+ 'Content-Length' => '5524',
+ 'Content-Type' => 'text/html; charset=UTF-8',
+ ], implode("\r\n", [
+ "HTTP/1.1 200 OK",
+ 'Date: Thu, 27 Apr 2017 13:42:19 GMT',
+ 'Server: Apache/2.4.23 (Win64) PHP/7.0.10',
+ 'X-Powered-By: PHP/7.0.10',
+ 'Content-Length: 5524',
+ 'Content-Type: text/html; charset=UTF-8'
+ ]),
+ ]
+ ];
+ }
+
+ /**
+ * @dataProvider dataParse
+ *
+ * @param $expectedCode
+ * @param $expectedStatus
+ * @param $expectedHeaders
+ * @param $raw
+ */
+ public function testParse($expectedVersion, $expectedCode, $expectedStatus, $expectedHeaders, $raw)
+ {
+ $header = new Header();
+
+ if (is_array($raw)) {
+ foreach ($raw as $item) {
+ $header->parse($item);
+ }
+ } else {
+ $header->parse($raw);
+ }
+
+ $this->assertEquals($expectedVersion, $header->version);
+ $this->assertEquals($expectedCode, $header->code);
+ $this->assertEquals($expectedStatus, $header->status);
+ $this->assertEquals($expectedHeaders, $header->getHeaders());
+ }
+}
diff --git a/tests/Test/Parser/JsonTest.php b/tests/Test/Parser/JsonTest.php
new file mode 100644
index 0000000..fc5fffc
--- /dev/null
+++ b/tests/Test/Parser/JsonTest.php
@@ -0,0 +1,21 @@
+assertEquals((object)['data' => 'test'], $parser->parse(json_encode(['data' => 'test'])));
+ }
+}
diff --git a/tests/Test/Parser/XmlArrayTest.php b/tests/Test/Parser/XmlArrayTest.php
new file mode 100644
index 0000000..4104ee3
--- /dev/null
+++ b/tests/Test/Parser/XmlArrayTest.php
@@ -0,0 +1,29 @@
+
+