first commit

This commit is contained in:
2025-03-12 17:06:23 +01:00
commit 2241f7131f
13185 changed files with 1692479 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
# Change Log
## Unreleased
## 1.7.1 - 2018-03-36
### Fixed
- #36: Failure evaluating code: is_resource($handle) (string assertions are deprecated in PHP 7.2)
## 1.7 - 2017-02-09
### Changed
- #30: Make sure we rewind streams
## 1.6.2 - 2017-01-02
### Fixed
- #29: Request not using CURLOPT_POSTFIELDS have content-length set to
### Changed
- Use binary mode to create response body stream.
## 1.6.1 - 2016-11-11
### Fixed
- #27: ErrorPlugin and sendAsyncRequest() incompatibility
## 1.6 - 2016-09-12
### Changed
- `Client::sendRequest` now throws `Http\Client\Exception\NetworkException` on network errors.
- `\UnexpectedValueException` replaced with `Http\Client\Exception\RequestException` in
`Client::sendRequest` and `Client::sendAsyncRequest`
## 1.5.1 - 2016-08-29
### Fixed
- #26: Combining CurlClient with StopwatchPlugin causes Promise onRejected handler to never be
invoked.
## 1.5 - 2016-08-03
### Changed
- Request body can be send with any method except GET, HEAD and TRACE.
- #25: Make discovery a hard dependency.
## 1.4.2 - 2016-06-14
### Added
- #23: "php-http/async-client-implementation" added to "provide" section.
## 1.4.1 - 2016-05-30
### Fixed
- #22: Cannot create the client using `HttpClientDiscovery`.
## 1.4 - 2016-03-30
### Changed
- #20: Minimize memory usage when reading large response body.
## 1.3 - 2016-03-14
### Fixed
- #18: Invalid "Expect" header.
### Removed
- #13: Remove HeaderParser.
## 1.2 - 2016-03-09
### Added
- #16: Make sure discovery can find the curl client
### Fixed
- #15: "Out of memory" sending large files.
## 1.1.0 - 2016-01-29
### Changed
- Switch to php-http/message 1.0.
## 1.0.0 - 2016-01-28
First stable release.
## 0.7.0 - 2016-01-26
### Changed
- Migrate from `php-http/discovery` and `php-http/utils` to `php-http/message`.
## 0.6.0 - 2016-01-12
### Changed
- Root namespace changed from `Http\Curl` to `Http\Client\Curl`.
- Main client class name renamed from `CurlHttpClient` to `Client`.
- Minimum required [php-http/discovery](https://packagist.org/packages/php-http/discovery)
version changed to 0.5.
## 0.5.0 - 2015-12-18
### Changed
- Compatibility with php-http/httplug 1.0 beta
- Switch to php-http/discovery 0.4
## 0.4.0 - 2015-12-16
### Changed
- Switch to php-http/message-factory 1.0
## 0.3.1 - 2015-12-14
### Changed
- Requirements fixed.
## 0.3.0 - 2015-11-24
### Changed
- Use cURL constants as options keys.
## 0.2.0 - 2015-11-17
### Added
- HttpAsyncClient support.
## 0.1.0 - 2015-11-11
### Added
- Initial release

View File

@@ -0,0 +1,19 @@
Copyright (c) 2015 PHP HTTP Team <team@php-http.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,44 @@
# Curl client for PHP HTTP
[![Latest Version](https://img.shields.io/github/release/php-http/curl-client.svg?style=flat-square)](https://github.com/php-http/curl-client/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/travis/php-http/curl-client.svg?style=flat-square)](https://travis-ci.org/php-http/curl-client)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/curl-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/curl-client)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/curl-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/curl-client)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/curl-client.svg?style=flat-square)](https://packagist.org/packages/php-http/curl-client)
The cURL client use the cURL PHP extension which must be activated in your `php.ini`.
## Install
Via Composer
``` bash
$ composer require php-http/curl-client
```
## Documentation
Please see the [official documentation](http://docs.php-http.org/en/latest/clients/curl-client.html).
## Testing
``` bash
$ composer test
```
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details.
## Security
If you discover any security related issues, please contact us at
[security@php-http.org](mailto:security@php-http.org).
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

View File

@@ -0,0 +1,50 @@
{
"name": "php-http/curl-client",
"description": "cURL client for PHP-HTTP",
"license": "MIT",
"keywords": ["http", "curl"],
"homepage": "http://php-http.org",
"authors": [
{
"name": "Михаил Красильников",
"email": "m.krasilnikov@yandex.ru"
}
],
"prefer-stable": true,
"minimum-stability": "beta",
"config": {
"bin-dir": "vendor/bin"
},
"require": {
"php": "^5.5 || ^7.0",
"ext-curl": "*",
"php-http/httplug": "^1.0",
"php-http/message-factory": "^1.0.2",
"php-http/message": "^1.2",
"php-http/discovery": "^1.0"
},
"require-dev": {
"guzzlehttp/psr7": "^1.0",
"php-http/client-integration-tests": "^0.6",
"phpunit/phpunit": "^4.8.27",
"zendframework/zend-diactoros": "^1.0"
},
"autoload": {
"psr-4": {
"Http\\Client\\Curl\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Http\\Client\\Curl\\Tests\\": "tests/"
}
},
"provide": {
"php-http/client-implementation": "1.0",
"php-http/async-client-implementation": "1.0"
},
"scripts": {
"test": "vendor/bin/phpunit",
"test-ci": "vendor/bin/phpunit --coverage-clover build/coverage.xml"
}
}

View File

@@ -0,0 +1,242 @@
{
"version": "1.0",
"name": "php-http/curl-client",
"bindings": {
"98239b8b-103b-4f47-94c7-4cba49a05a1f": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Client\\Curl\\Client",
"type": "Http\\Client\\HttpAsyncClient"
},
"a6a79968-2aa5-427c-bbe1-a581d9a48321": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Client\\Curl\\Client",
"type": "Http\\Client\\HttpClient"
}
},
"config": {
"bootstrap-file": "vendor/autoload.php"
},
"packages": {
"clue/stream-filter": {
"install-path": "vendor/clue/stream-filter",
"installer": "composer",
"env": "dev"
},
"doctrine/instantiator": {
"install-path": "vendor/doctrine/instantiator",
"installer": "composer",
"env": "dev"
},
"guzzlehttp/psr7": {
"install-path": "vendor/guzzlehttp/psr7",
"installer": "composer",
"env": "dev"
},
"justinrainbow/json-schema": {
"install-path": "vendor/justinrainbow/json-schema",
"installer": "composer",
"env": "dev"
},
"paragonie/random_compat": {
"install-path": "vendor/paragonie/random_compat",
"installer": "composer",
"env": "dev"
},
"php-http/adapter-integration-tests": {
"install-path": "vendor/php-http/adapter-integration-tests",
"installer": "composer",
"env": "dev"
},
"php-http/discovery": {
"install-path": "vendor/php-http/discovery",
"installer": "composer",
"env": "dev"
},
"php-http/httplug": {
"install-path": "vendor/php-http/httplug",
"installer": "composer"
},
"php-http/message": {
"install-path": "vendor/php-http/message",
"installer": "composer",
"env": "dev"
},
"php-http/message-factory": {
"install-path": "vendor/php-http/message-factory",
"installer": "composer"
},
"php-http/promise": {
"install-path": "vendor/php-http/promise",
"installer": "composer"
},
"phpdocumentor/reflection-docblock": {
"install-path": "vendor/phpdocumentor/reflection-docblock",
"installer": "composer",
"env": "dev"
},
"phpspec/prophecy": {
"install-path": "vendor/phpspec/prophecy",
"installer": "composer",
"env": "dev"
},
"phpunit/php-code-coverage": {
"install-path": "vendor/phpunit/php-code-coverage",
"installer": "composer",
"env": "dev"
},
"phpunit/php-file-iterator": {
"install-path": "vendor/phpunit/php-file-iterator",
"installer": "composer",
"env": "dev"
},
"phpunit/php-text-template": {
"install-path": "vendor/phpunit/php-text-template",
"installer": "composer",
"env": "dev"
},
"phpunit/php-timer": {
"install-path": "vendor/phpunit/php-timer",
"installer": "composer",
"env": "dev"
},
"phpunit/php-token-stream": {
"install-path": "vendor/phpunit/php-token-stream",
"installer": "composer",
"env": "dev"
},
"phpunit/phpunit": {
"install-path": "vendor/phpunit/phpunit",
"installer": "composer",
"env": "dev"
},
"phpunit/phpunit-mock-objects": {
"install-path": "vendor/phpunit/phpunit-mock-objects",
"installer": "composer",
"env": "dev"
},
"psr/http-message": {
"install-path": "vendor/psr/http-message",
"installer": "composer"
},
"psr/log": {
"install-path": "vendor/psr/log",
"installer": "composer",
"env": "dev"
},
"puli/composer-plugin": {
"install-path": "vendor/puli/composer-plugin",
"installer": "composer",
"env": "dev"
},
"puli/discovery": {
"install-path": "vendor/puli/discovery",
"installer": "composer",
"env": "dev"
},
"puli/repository": {
"install-path": "vendor/puli/repository",
"installer": "composer",
"env": "dev"
},
"puli/url-generator": {
"install-path": "vendor/puli/url-generator",
"installer": "composer",
"env": "dev"
},
"ramsey/uuid": {
"install-path": "vendor/ramsey/uuid",
"installer": "composer",
"env": "dev"
},
"sebastian/comparator": {
"install-path": "vendor/sebastian/comparator",
"installer": "composer",
"env": "dev"
},
"sebastian/diff": {
"install-path": "vendor/sebastian/diff",
"installer": "composer",
"env": "dev"
},
"sebastian/environment": {
"install-path": "vendor/sebastian/environment",
"installer": "composer",
"env": "dev"
},
"sebastian/exporter": {
"install-path": "vendor/sebastian/exporter",
"installer": "composer",
"env": "dev"
},
"sebastian/global-state": {
"install-path": "vendor/sebastian/global-state",
"installer": "composer",
"env": "dev"
},
"sebastian/recursion-context": {
"install-path": "vendor/sebastian/recursion-context",
"installer": "composer",
"env": "dev"
},
"sebastian/version": {
"install-path": "vendor/sebastian/version",
"installer": "composer",
"env": "dev"
},
"seld/jsonlint": {
"install-path": "vendor/seld/jsonlint",
"installer": "composer",
"env": "dev"
},
"symfony/filesystem": {
"install-path": "vendor/symfony/filesystem",
"installer": "composer",
"env": "dev"
},
"symfony/process": {
"install-path": "vendor/symfony/process",
"installer": "composer",
"env": "dev"
},
"symfony/yaml": {
"install-path": "vendor/symfony/yaml",
"installer": "composer",
"env": "dev"
},
"th3n3rd/cartesian-product": {
"install-path": "vendor/th3n3rd/cartesian-product",
"installer": "composer",
"env": "dev"
},
"webmozart/assert": {
"install-path": "vendor/webmozart/assert",
"installer": "composer",
"env": "dev"
},
"webmozart/expression": {
"install-path": "vendor/webmozart/expression",
"installer": "composer",
"env": "dev"
},
"webmozart/glob": {
"install-path": "vendor/webmozart/glob",
"installer": "composer",
"env": "dev"
},
"webmozart/json": {
"install-path": "vendor/webmozart/json",
"installer": "composer",
"env": "dev"
},
"webmozart/path-util": {
"install-path": "vendor/webmozart/path-util",
"installer": "composer",
"env": "dev"
},
"zendframework/zend-diactoros": {
"install-path": "vendor/zendframework/zend-diactoros",
"installer": "composer",
"env": "dev"
}
}
}

View File

@@ -0,0 +1,372 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\StreamFactoryDiscovery;
use Http\Message\MessageFactory;
use Http\Message\StreamFactory;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* PSR-7 compatible cURL based HTTP client.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
* @author Blake Williams <github@shabbyrobe.org>
*
* @api
*
* @since 1.0
*/
class Client implements HttpClient, HttpAsyncClient
{
/**
* cURL options.
*
* @var array
*/
private $options;
/**
* PSR-7 message factory.
*
* @var MessageFactory
*/
private $messageFactory;
/**
* PSR-7 stream factory.
*
* @var StreamFactory
*/
private $streamFactory;
/**
* cURL synchronous requests handle.
*
* @var resource|null
*/
private $handle = null;
/**
* Simultaneous requests runner.
*
* @var MultiRunner|null
*/
private $multiRunner = null;
/**
* Create new client.
*
* @param MessageFactory|null $messageFactory HTTP Message factory
* @param StreamFactory|null $streamFactory HTTP Stream factory
* @param array $options cURL options (see http://php.net/curl_setopt)
*
* @throws \Http\Discovery\Exception\NotFoundException If factory discovery failed
*
* @since 1.0
*/
public function __construct(
MessageFactory $messageFactory = null,
StreamFactory $streamFactory = null,
array $options = []
) {
$this->messageFactory = $messageFactory ?: MessageFactoryDiscovery::find();
$this->streamFactory = $streamFactory ?: StreamFactoryDiscovery::find();
$this->options = $options;
}
/**
* Release resources if still active.
*/
public function __destruct()
{
if (is_resource($this->handle)) {
curl_close($this->handle);
}
}
/**
* Sends a PSR-7 request.
*
* @param RequestInterface $request
*
* @return ResponseInterface
*
* @throws \Http\Client\Exception\NetworkException In case of network problems
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException If creating the body stream fails
*
* @since 1.6 \UnexpectedValueException replaced with RequestException
* @since 1.6 Throw NetworkException on network errors
* @since 1.0
*/
public function sendRequest(RequestInterface $request)
{
$responseBuilder = $this->createResponseBuilder();
$options = $this->createCurlOptions($request, $responseBuilder);
if (is_resource($this->handle)) {
curl_reset($this->handle);
} else {
$this->handle = curl_init();
}
curl_setopt_array($this->handle, $options);
curl_exec($this->handle);
$errno = curl_errno($this->handle);
switch ($errno) {
case CURLE_OK:
// All OK, no actions needed.
break;
case CURLE_COULDNT_RESOLVE_PROXY:
case CURLE_COULDNT_RESOLVE_HOST:
case CURLE_COULDNT_CONNECT:
case CURLE_OPERATION_TIMEOUTED:
case CURLE_SSL_CONNECT_ERROR:
throw new Exception\NetworkException(curl_error($this->handle), $request);
default:
throw new Exception\RequestException(curl_error($this->handle), $request);
}
$response = $responseBuilder->getResponse();
$response->getBody()->seek(0);
return $response;
}
/**
* Sends a PSR-7 request in an asynchronous way.
*
* @param RequestInterface $request
*
* @return Promise
*
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException If creating the body stream fails
*
* @since 1.6 \UnexpectedValueException replaced with RequestException
* @since 1.0
*/
public function sendAsyncRequest(RequestInterface $request)
{
if (!$this->multiRunner instanceof MultiRunner) {
$this->multiRunner = new MultiRunner();
}
$handle = curl_init();
$responseBuilder = $this->createResponseBuilder();
$options = $this->createCurlOptions($request, $responseBuilder);
curl_setopt_array($handle, $options);
$core = new PromiseCore($request, $handle, $responseBuilder);
$promise = new CurlPromise($core, $this->multiRunner);
$this->multiRunner->add($core);
return $promise;
}
/**
* Generates cURL options.
*
* @param RequestInterface $request
* @param ResponseBuilder $responseBuilder
*
* @throws \Http\Client\Exception\RequestException On invalid request
* @throws \InvalidArgumentException For invalid header names or values
* @throws \RuntimeException if can not read body
*
* @return array
*/
private function createCurlOptions(RequestInterface $request, ResponseBuilder $responseBuilder)
{
$options = $this->options;
$options[CURLOPT_HEADER] = false;
$options[CURLOPT_RETURNTRANSFER] = false;
$options[CURLOPT_FOLLOWLOCATION] = false;
try {
$options[CURLOPT_HTTP_VERSION]
= $this->getProtocolVersion($request->getProtocolVersion());
} catch (\UnexpectedValueException $e) {
throw new Exception\RequestException($e->getMessage(), $request);
}
$options[CURLOPT_URL] = (string) $request->getUri();
$options = $this->addRequestBodyOptions($request, $options);
$options[CURLOPT_HTTPHEADER] = $this->createHeaders($request, $options);
if ($request->getUri()->getUserInfo()) {
$options[CURLOPT_USERPWD] = $request->getUri()->getUserInfo();
}
$options[CURLOPT_HEADERFUNCTION] = function ($ch, $data) use ($responseBuilder) {
$str = trim($data);
if ('' !== $str) {
if (strpos(strtolower($str), 'http/') === 0) {
$responseBuilder->setStatus($str)->getResponse();
} else {
$responseBuilder->addHeader($str);
}
}
return strlen($data);
};
$options[CURLOPT_WRITEFUNCTION] = function ($ch, $data) use ($responseBuilder) {
return $responseBuilder->getResponse()->getBody()->write($data);
};
return $options;
}
/**
* Return cURL constant for specified HTTP version.
*
* @param string $requestVersion
*
* @throws \UnexpectedValueException if unsupported version requested
*
* @return int
*/
private function getProtocolVersion($requestVersion)
{
switch ($requestVersion) {
case '1.0':
return CURL_HTTP_VERSION_1_0;
case '1.1':
return CURL_HTTP_VERSION_1_1;
case '2.0':
if (defined('CURL_HTTP_VERSION_2_0')) {
return CURL_HTTP_VERSION_2_0;
}
throw new \UnexpectedValueException('libcurl 7.33 needed for HTTP 2.0 support');
}
return CURL_HTTP_VERSION_NONE;
}
/**
* Add request body related cURL options.
*
* @param RequestInterface $request
* @param array $options
*
* @return array
*/
private function addRequestBodyOptions(RequestInterface $request, array $options)
{
/*
* Some HTTP methods cannot have payload:
*
* - GET — cURL will automatically change method to PUT or POST if we set CURLOPT_UPLOAD or
* CURLOPT_POSTFIELDS.
* - HEAD — cURL treats HEAD as GET request with a same restrictions.
* - TRACE — According to RFC7231: a client MUST NOT send a message body in a TRACE request.
*/
if (!in_array($request->getMethod(), ['GET', 'HEAD', 'TRACE'], true)) {
$body = $request->getBody();
$bodySize = $body->getSize();
if ($bodySize !== 0) {
if ($body->isSeekable()) {
$body->rewind();
}
// Message has non empty body.
if (null === $bodySize || $bodySize > 1024 * 1024) {
// Avoid full loading large or unknown size body into memory
$options[CURLOPT_UPLOAD] = true;
if (null !== $bodySize) {
$options[CURLOPT_INFILESIZE] = $bodySize;
}
$options[CURLOPT_READFUNCTION] = function ($ch, $fd, $length) use ($body) {
return $body->read($length);
};
} else {
// Small body can be loaded into memory
$options[CURLOPT_POSTFIELDS] = (string) $body;
}
}
}
if ($request->getMethod() === 'HEAD') {
// This will set HTTP method to "HEAD".
$options[CURLOPT_NOBODY] = true;
} elseif ($request->getMethod() !== 'GET') {
// GET is a default method. Other methods should be specified explicitly.
$options[CURLOPT_CUSTOMREQUEST] = $request->getMethod();
}
return $options;
}
/**
* Create headers array for CURLOPT_HTTPHEADER.
*
* @param RequestInterface $request
* @param array $options cURL options
*
* @return string[]
*/
private function createHeaders(RequestInterface $request, array $options)
{
$curlHeaders = [];
$headers = $request->getHeaders();
foreach ($headers as $name => $values) {
$header = strtolower($name);
if ('expect' === $header) {
// curl-client does not support "Expect-Continue", so dropping "expect" headers
continue;
}
if ('content-length' === $header) {
if (array_key_exists(CURLOPT_POSTFIELDS, $options)) {
// Small body content length can be calculated here.
$values = [strlen($options[CURLOPT_POSTFIELDS])];
} elseif (!array_key_exists(CURLOPT_READFUNCTION, $options)) {
// Else if there is no body, forcing "Content-length" to 0
$values = [0];
}
}
foreach ($values as $value) {
$curlHeaders[] = $name.': '.$value;
}
}
/*
* curl-client does not support "Expect-Continue", but cURL adds "Expect" header by default.
* We can not suppress it, but we can set it to empty.
*/
$curlHeaders[] = 'Expect:';
return $curlHeaders;
}
/**
* Create new ResponseBuilder instance.
*
* @return ResponseBuilder
*
* @throws \RuntimeException If creating the stream from $body fails
*/
private function createResponseBuilder()
{
try {
$body = $this->streamFactory->createStream(fopen('php://temp', 'w+b'));
} catch (\InvalidArgumentException $e) {
throw new \RuntimeException('Can not create "php://temp" stream.');
}
$response = $this->messageFactory->createResponse(200, null, [], $body);
return new ResponseBuilder($response);
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Http\Client\Curl;
use Http\Promise\Promise;
/**
* Promise represents a response that may not be available yet, but will be resolved at some point
* in future. It acts like a proxy to the actual response.
*
* This interface is an extension of the promises/a+ specification https://promisesaplus.com/
* Value is replaced by an object where its class implement a Psr\Http\Message\RequestInterface.
* Reason is replaced by an object where its class implement a Http\Client\Exception.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class CurlPromise implements Promise
{
/**
* Shared promise core.
*
* @var PromiseCore
*/
private $core;
/**
* Requests runner.
*
* @var MultiRunner
*/
private $runner;
/**
* Create new promise.
*
* @param PromiseCore $core Shared promise core
* @param MultiRunner $runner Simultaneous requests runner
*/
public function __construct(PromiseCore $core, MultiRunner $runner)
{
$this->core = $core;
$this->runner = $runner;
}
/**
* Add behavior for when the promise is resolved or rejected.
*
* If you do not care about one of the cases, you can set the corresponding callable to null
* The callback will be called when the response or exception arrived and never more than once.
*
* @param callable $onFulfilled Called when a response will be available
* @param callable $onRejected Called when an error happens.
*
* You must always return the Response in the interface or throw an Exception
*
* @return Promise Always returns a new promise which is resolved with value of the executed
* callback (onFulfilled / onRejected)
*/
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
if ($onFulfilled) {
$this->core->addOnFulfilled($onFulfilled);
}
if ($onRejected) {
$this->core->addOnRejected($onRejected);
}
return new self($this->core, $this->runner);
}
/**
* Get the state of the promise, one of PENDING, FULFILLED or REJECTED.
*
* @return string
*/
public function getState()
{
return $this->core->getState();
}
/**
* Wait for the promise to be fulfilled or rejected.
*
* When this method returns, the request has been resolved and the appropriate callable has terminated.
*
* When called with the unwrap option
*
* @param bool $unwrap Whether to return resolved value / throw reason or not
*
* @return \Psr\Http\Message\ResponseInterface|null Resolved value, null if $unwrap is set to false
*
* @throws \Http\Client\Exception The rejection reason
*/
public function wait($unwrap = true)
{
$this->runner->wait($this->core);
if ($unwrap) {
if ($this->core->getState() === self::REJECTED) {
throw $this->core->getException();
}
return $this->core->getResponse();
}
return null;
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception\RequestException;
/**
* Simultaneous requests runner.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class MultiRunner
{
/**
* cURL multi handle.
*
* @var resource|null
*/
private $multiHandle = null;
/**
* Awaiting cores.
*
* @var PromiseCore[]
*/
private $cores = [];
/**
* Release resources if still active.
*/
public function __destruct()
{
if (is_resource($this->multiHandle)) {
curl_multi_close($this->multiHandle);
}
}
/**
* Add promise to runner.
*
* @param PromiseCore $core
*/
public function add(PromiseCore $core)
{
foreach ($this->cores as $existed) {
if ($existed === $core) {
return;
}
}
$this->cores[] = $core;
if (null === $this->multiHandle) {
$this->multiHandle = curl_multi_init();
}
curl_multi_add_handle($this->multiHandle, $core->getHandle());
}
/**
* Remove promise from runner.
*
* @param PromiseCore $core
*/
public function remove(PromiseCore $core)
{
foreach ($this->cores as $index => $existed) {
if ($existed === $core) {
curl_multi_remove_handle($this->multiHandle, $core->getHandle());
unset($this->cores[$index]);
return;
}
}
}
/**
* Wait for request(s) to be completed.
*
* @param PromiseCore|null $targetCore
*/
public function wait(PromiseCore $targetCore = null)
{
do {
$status = curl_multi_exec($this->multiHandle, $active);
$info = curl_multi_info_read($this->multiHandle);
if (false !== $info) {
$core = $this->findCoreByHandle($info['handle']);
if (null === $core) {
// We have no promise for this handle. Drop it.
curl_multi_remove_handle($this->multiHandle, $info['handle']);
continue;
}
if (CURLE_OK === $info['result']) {
$core->fulfill();
} else {
$error = curl_error($core->getHandle());
$core->reject(new RequestException($error, $core->getRequest()));
}
$this->remove($core);
// This is a promise we are waited for. So exiting wait().
if ($core === $targetCore) {
return;
}
}
} while ($status === CURLM_CALL_MULTI_PERFORM || $active);
}
/**
* Find core by handle.
*
* @param resource $handle
*
* @return PromiseCore|null
*/
private function findCoreByHandle($handle)
{
foreach ($this->cores as $core) {
if ($core->getHandle() === $handle) {
return $core;
}
}
return null;
}
}

View File

@@ -0,0 +1,241 @@
<?php
namespace Http\Client\Curl;
use Http\Client\Exception;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Shared promises core.
*
* @license http://opensource.org/licenses/MIT MIT
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*/
class PromiseCore
{
/**
* HTTP request.
*
* @var RequestInterface
*/
private $request;
/**
* cURL handle.
*
* @var resource
*/
private $handle;
/**
* Response builder.
*
* @var ResponseBuilder
*/
private $responseBuilder;
/**
* Promise state.
*
* @var string
*/
private $state;
/**
* Exception.
*
* @var Exception|null
*/
private $exception = null;
/**
* Functions to call when a response will be available.
*
* @var callable[]
*/
private $onFulfilled = [];
/**
* Functions to call when an error happens.
*
* @var callable[]
*/
private $onRejected = [];
/**
* Create shared core.
*
* @param RequestInterface $request HTTP request.
* @param resource $handle cURL handle.
* @param ResponseBuilder $responseBuilder Response builder.
*
* @throws \InvalidArgumentException If $handle is not a cURL resource.
*/
public function __construct(
RequestInterface $request,
$handle,
ResponseBuilder $responseBuilder
) {
if (!is_resource($handle)) {
throw new \InvalidArgumentException(
sprintf(
'Parameter $handle expected to be a cURL resource, %s given',
gettype($handle)
)
);
}
if (get_resource_type($handle) !== 'curl') {
throw new \InvalidArgumentException(
sprintf(
'Parameter $handle expected to be a cURL resource, %s resource given',
get_resource_type($handle)
)
);
}
$this->request = $request;
$this->handle = $handle;
$this->responseBuilder = $responseBuilder;
$this->state = Promise::PENDING;
}
/**
* Add on fulfilled callback.
*
* @param callable $callback
*/
public function addOnFulfilled(callable $callback)
{
if ($this->getState() === Promise::PENDING) {
$this->onFulfilled[] = $callback;
} elseif ($this->getState() === Promise::FULFILLED) {
$response = call_user_func($callback, $this->responseBuilder->getResponse());
if ($response instanceof ResponseInterface) {
$this->responseBuilder->setResponse($response);
}
}
}
/**
* Add on rejected callback.
*
* @param callable $callback
*/
public function addOnRejected(callable $callback)
{
if ($this->getState() === Promise::PENDING) {
$this->onRejected[] = $callback;
} elseif ($this->getState() === Promise::REJECTED) {
$this->exception = call_user_func($callback, $this->exception);
}
}
/**
* Return cURL handle.
*
* @return resource
*/
public function getHandle()
{
return $this->handle;
}
/**
* Get the state of the promise, one of PENDING, FULFILLED or REJECTED.
*
* @return string
*/
public function getState()
{
return $this->state;
}
/**
* Return request.
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->request;
}
/**
* Return the value of the promise (fulfilled).
*
* @return ResponseInterface Response Object only when the Promise is fulfilled
*/
public function getResponse()
{
return $this->responseBuilder->getResponse();
}
/**
* Get the reason why the promise was rejected.
*
* If the exception is an instance of Http\Client\Exception\HttpException it will contain
* the response object with the status code and the http reason.
*
* @return Exception Exception Object only when the Promise is rejected
*
* @throws \LogicException When the promise is not rejected
*/
public function getException()
{
if (null === $this->exception) {
throw new \LogicException('Promise is not rejected');
}
return $this->exception;
}
/**
* Fulfill promise.
*/
public function fulfill()
{
$this->state = Promise::FULFILLED;
$response = $this->responseBuilder->getResponse();
try {
$response->getBody()->seek(0);
} catch (\RuntimeException $e) {
$exception = new Exception\TransferException($e->getMessage(), $e->getCode(), $e);
$this->reject($exception);
return;
}
while (count($this->onFulfilled) > 0) {
$callback = array_shift($this->onFulfilled);
$response = call_user_func($callback, $response);
}
if ($response instanceof ResponseInterface) {
$this->responseBuilder->setResponse($response);
}
}
/**
* Reject promise.
*
* @param Exception $exception Reject reason
*/
public function reject(Exception $exception)
{
$this->exception = $exception;
$this->state = Promise::REJECTED;
while (count($this->onRejected) > 0) {
$callback = array_shift($this->onRejected);
try {
$exception = call_user_func($callback, $this->exception);
$this->exception = $exception;
} catch (Exception $exception) {
$this->exception = $exception;
}
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Http\Client\Curl;
use Http\Message\Builder\ResponseBuilder as OriginalResponseBuilder;
use Psr\Http\Message\ResponseInterface;
/**
* Extended response builder.
*/
class ResponseBuilder extends OriginalResponseBuilder
{
/**
* Replace response with a new instance.
*
* @param ResponseInterface $response
*/
public function setResponse(ResponseInterface $response)
{
$this->response = $response;
}
}

View File

@@ -0,0 +1,307 @@
# Change Log
## 1.13.0 - 2020-11-27
- Support discovering PSR-17 factories of `slim/psr7` package https://github.com/php-http/discovery/pull/192
## 1.12.0 - 2020-09-22
- Support discovering HttpClient of `php-http/guzzle7-adapter` https://github.com/php-http/discovery/pull/189
## 1.11.0 - 2020-09-22
- Use correct method name to find Uri Factory in PSR17 https://github.com/php-http/discovery/pull/181
## 1.10.0 - 2020-09-04
- Discover PSR-18 implementation of phalcon
## 1.9.1 - 2020-07-13
### Fixed
- Support PHP 7.4 and 8.0
## 1.9.0 - 2020-07-02
### Added
- Support discovering PSR-18 factories of `guzzlehttp/guzzle` 7+
## 1.8.0 - 2020-06-14
### Added
- Support discovering PSR-17 factories of `guzzlehttp/psr7` package
- Support discovering PSR-17 factories of `laminas/laminas-diactoros` package
- `ClassDiscovery::getStrategies()` to retrieve the list of current strategies.
### Fixed
- Ignore exception during discovery when Symfony HttplugClient checks if HTTPlug is available.
## 1.7.4 - 2020-01-03
### Fixed
- Improve conditions on Symfony's async HTTPlug client.
## 1.7.3 - 2019-12-27
### Fixed
- Enough conditions to only use Symfony HTTP client if all needed components are available.
## 1.7.2 - 2019-12-27
### Fixed
- Allow a condition to specify an interface and not just classes.
## 1.7.1 - 2019-12-26
### Fixed
- Better conditions to see if Symfony's HTTP clients are available.
## 1.7.0 - 2019-06-30
### Added
- Dropped support for PHP < 7.1
- Support for `symfony/http-client`
## 1.6.1 - 2019-02-23
### Fixed
- MockClientStrategy also provides the mock client when requesting an async client
## 1.6.0 - 2019-01-23
### Added
- Support for PSR-17 factories
- Support for PSR-18 clients
## 1.5.2 - 2018-12-31
Corrected mistakes in 1.5.1. The different between 1.5.2 and 1.5.0 is that
we removed some PHP 7 code.
https://github.com/php-http/discovery/compare/1.5.0...1.5.2
## 1.5.1 - 2018-12-31
This version added new features by mistake. These are reverted in 1.5.2.
Do not use 1.5.1.
### Fixed
- Removed PHP 7 code
## 1.5.0 - 2018-12-30
### Added
- Support for `nyholm/psr7` version 1.0.
- `ClassDiscovery::safeClassExists` which will help Magento users.
- Support for HTTPlug 2.0
- Support for Buzz 1.0
- Better error message when nothing found by introducing a new exception: `NoCandidateFoundException`.
### Fixed
- Fixed condition evaluation, it should stop after first invalid condition.
## 1.4.0 - 2018-02-06
### Added
- Discovery support for nyholm/psr7
## 1.3.0 - 2017-08-03
### Added
- Discovery support for CakePHP adapter
- Discovery support for Zend adapter
- Discovery support for Artax adapter
## 1.2.1 - 2017-03-02
### Fixed
- Fixed minor issue with `MockClientStrategy`, also added more tests.
## 1.2.0 - 2017-02-12
### Added
- MockClientStrategy class.
## 1.1.1 - 2016-11-27
### Changed
- Made exception messages clearer. `StrategyUnavailableException` is no longer the previous exception to `DiscoveryFailedException`.
- `CommonClassesStrategy` is using `self` instead of `static`. Using `static` makes no sense when `CommonClassesStrategy` is final.
## 1.1.0 - 2016-10-20
### Added
- Discovery support for Slim Framework factories
## 1.0.0 - 2016-07-18
### Added
- Added back `Http\Discovery\NotFoundException` to preserve BC with 0.8 version. You may upgrade from 0.8.x and 0.9.x to 1.0.0 without any BC breaks.
- Added interface `Http\Discovery\Exception` which is implemented by all our exceptions
### Changed
- Puli strategy renamed to Puli Beta strategy to prevent incompatibility with a future Puli stable
### Deprecated
- For BC reasons, the old `Http\Discovery\NotFoundException` (extending the new exception) will be thrown until version 2.0
## 0.9.1 - 2016-06-28
### Changed
- Dropping PHP 5.4 support because we use the ::class constant.
## 0.9.0 - 2016-06-25
### Added
- Discovery strategies to find classes
### Changed
- [Puli](http://puli.io) made optional
- Improved exceptions
- **[BC] `NotFoundException` moved to `Http\Discovery\Exception\NotFoundException`**
## 0.8.0 - 2016-02-11
### Changed
- Puli composer plugin must be installed separately
## 0.7.0 - 2016-01-15
### Added
- Temporary puli.phar (Beta 10) executable
### Changed
- Updated HTTPlug dependencies
- Updated Puli dependencies
- Local configuration to make tests passing
### Removed
- Puli CLI dependency
## 0.6.4 - 2016-01-07
### Fixed
- Puli [not working](https://twitter.com/PuliPHP/status/685132540588507137) with the latest json-schema
## 0.6.3 - 2016-01-04
### Changed
- Adjust Puli dependencies
## 0.6.2 - 2016-01-04
### Changed
- Make Puli CLI a requirement
## 0.6.1 - 2016-01-03
### Changed
- More flexible Puli requirement
## 0.6.0 - 2015-12-30
### Changed
- Use [Puli](http://puli.io) for discovery
- Improved exception messages
## 0.5.0 - 2015-12-25
### Changed
- Updated message factory dependency (php-http/message)
## 0.4.0 - 2015-12-17
### Added
- Array condition evaluation in the Class Discovery
### Removed
- Message factories (moved to php-http/utils)
## 0.3.0 - 2015-11-18
### Added
- HTTP Async Client Discovery
- Stream factories
### Changed
- Discoveries and Factories are final
- Message and Uri factories have the type in their names
- Diactoros Message factory uses Stream factory internally
### Fixed
- Improved docblocks for API documentation generation
## 0.2.0 - 2015-10-31
### Changed
- Renamed AdapterDiscovery to ClientDiscovery
## 0.1.1 - 2015-06-13
### Fixed
- Bad HTTP Adapter class name for Guzzle 5
## 0.1.0 - 2015-06-12
### Added
- Initial release

View File

@@ -0,0 +1,19 @@
Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,46 @@
# HTTPlug Discovery
[![Latest Version](https://img.shields.io/github/release/php-http/discovery.svg?style=flat-square)](https://github.com/php-http/discovery/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/travis/php-http/discovery/master.svg?style=flat-square)](https://travis-ci.org/php-http/discovery)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/discovery.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/discovery)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/discovery.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/discovery)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/discovery.svg?style=flat-square)](https://packagist.org/packages/php-http/discovery)
**Finds installed HTTPlug implementations and PSR-7 message factories.**
## Install
Via Composer
``` bash
$ composer require php-http/discovery
```
## Documentation
Please see the [official documentation](http://php-http.readthedocs.org/en/latest/discovery.html).
## Testing
``` bash
$ composer test
```
## Contributing
Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).
## Security
If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org).
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

View File

@@ -0,0 +1,51 @@
{
"name": "php-http/discovery",
"description": "Finds installed HTTPlug implementations and PSR-7 message factories",
"license": "MIT",
"keywords": ["http", "discovery", "client", "adapter", "message", "factory", "psr7"],
"homepage": "http://php-http.org",
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"graham-campbell/phpspec-skip-example-extension": "^5.0",
"php-http/httplug": "^1.0 || ^2.0",
"php-http/message-factory": "^1.0",
"phpspec/phpspec": "^5.1 || ^6.1",
"puli/composer-plugin": "1.0.0-beta10"
},
"suggest": {
"puli/composer-plugin": "Sets up Puli which is recommended for Discovery to work. Check http://docs.php-http.org/en/latest/discovery.html for more details.",
"php-http/message": "Allow to use Guzzle, Diactoros or Slim Framework factories"
},
"autoload": {
"psr-4": {
"Http\\Discovery\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"spec\\Http\\Discovery\\": "spec/"
}
},
"scripts": {
"test": "vendor/bin/phpspec run",
"test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml"
},
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"conflict": {
"nyholm/psr7": "<1.0"
},
"prefer-stable": true,
"minimum-stability": "beta"
}

View File

@@ -0,0 +1,246 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\ClassInstantiationFailedException;
use Http\Discovery\Exception\DiscoveryFailedException;
use Http\Discovery\Exception\NoCandidateFoundException;
use Http\Discovery\Exception\StrategyUnavailableException;
/**
* Registry that based find results on class existence.
*
* @author David de Boer <david@ddeboer.nl>
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
abstract class ClassDiscovery
{
/**
* A list of strategies to find classes.
*
* @var array
*/
private static $strategies = [
Strategy\PuliBetaStrategy::class,
Strategy\CommonClassesStrategy::class,
Strategy\CommonPsr17ClassesStrategy::class,
];
/**
* Discovery cache to make the second time we use discovery faster.
*
* @var array
*/
private static $cache = [];
/**
* Finds a class.
*
* @param string $type
*
* @return string|\Closure
*
* @throws DiscoveryFailedException
*/
protected static function findOneByType($type)
{
// Look in the cache
if (null !== ($class = self::getFromCache($type))) {
return $class;
}
$exceptions = [];
foreach (self::$strategies as $strategy) {
try {
$candidates = call_user_func($strategy.'::getCandidates', $type);
} catch (StrategyUnavailableException $e) {
$exceptions[] = $e;
continue;
}
foreach ($candidates as $candidate) {
if (isset($candidate['condition'])) {
if (!self::evaluateCondition($candidate['condition'])) {
continue;
}
}
// save the result for later use
self::storeInCache($type, $candidate);
return $candidate['class'];
}
$exceptions[] = new NoCandidateFoundException($strategy, $candidates);
}
throw DiscoveryFailedException::create($exceptions);
}
/**
* Get a value from cache.
*
* @param string $type
*
* @return string|null
*/
private static function getFromCache($type)
{
if (!isset(self::$cache[$type])) {
return;
}
$candidate = self::$cache[$type];
if (isset($candidate['condition'])) {
if (!self::evaluateCondition($candidate['condition'])) {
return;
}
}
return $candidate['class'];
}
/**
* Store a value in cache.
*
* @param string $type
* @param string $class
*/
private static function storeInCache($type, $class)
{
self::$cache[$type] = $class;
}
/**
* Set new strategies and clear the cache.
*
* @param array $strategies string array of fully qualified class name to a DiscoveryStrategy
*/
public static function setStrategies(array $strategies)
{
self::$strategies = $strategies;
self::clearCache();
}
/**
* Returns the currently configured discovery strategies as fully qualified class names.
*
* @return string[]
*/
public static function getStrategies(): iterable
{
return self::$strategies;
}
/**
* Append a strategy at the end of the strategy queue.
*
* @param string $strategy Fully qualified class name to a DiscoveryStrategy
*/
public static function appendStrategy($strategy)
{
self::$strategies[] = $strategy;
self::clearCache();
}
/**
* Prepend a strategy at the beginning of the strategy queue.
*
* @param string $strategy Fully qualified class name to a DiscoveryStrategy
*/
public static function prependStrategy($strategy)
{
array_unshift(self::$strategies, $strategy);
self::clearCache();
}
/**
* Clear the cache.
*/
public static function clearCache()
{
self::$cache = [];
}
/**
* Evaluates conditions to boolean.
*
* @param mixed $condition
*
* @return bool
*/
protected static function evaluateCondition($condition)
{
if (is_string($condition)) {
// Should be extended for functions, extensions???
return self::safeClassExists($condition);
}
if (is_callable($condition)) {
return (bool) $condition();
}
if (is_bool($condition)) {
return $condition;
}
if (is_array($condition)) {
foreach ($condition as $c) {
if (false === static::evaluateCondition($c)) {
// Immediately stop execution if the condition is false
return false;
}
}
return true;
}
return false;
}
/**
* Get an instance of the $class.
*
* @param string|\Closure $class A FQCN of a class or a closure that instantiate the class.
*
* @return object
*
* @throws ClassInstantiationFailedException
*/
protected static function instantiateClass($class)
{
try {
if (is_string($class)) {
return new $class();
}
if (is_callable($class)) {
return $class();
}
} catch (\Exception $e) {
throw new ClassInstantiationFailedException('Unexpected exception when instantiating class.', 0, $e);
}
throw new ClassInstantiationFailedException('Could not instantiate class because parameter is neither a callable nor a string');
}
/**
* We want to do a "safe" version of PHP's "class_exists" because Magento has a bug
* (or they call it a "feature"). Magento is throwing an exception if you do class_exists()
* on a class that ends with "Factory" and if that file does not exits.
*
* This function will catch all potential exceptions and make sure it returns a boolean.
*
* @param string $class
* @param bool $autoload
*
* @return bool
*/
public static function safeClassExists($class)
{
try {
return class_exists($class) || interface_exists($class);
} catch (\Exception $e) {
return false;
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Http\Discovery;
/**
* An interface implemented by all discovery related exceptions.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface Exception
{
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* Thrown when a class fails to instantiate.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class ClassInstantiationFailedException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* Thrown when all discovery strategies fails to find a resource.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class DiscoveryFailedException extends \Exception implements Exception
{
/**
* @var \Exception[]
*/
private $exceptions;
/**
* @param string $message
* @param \Exception[] $exceptions
*/
public function __construct($message, array $exceptions = [])
{
$this->exceptions = $exceptions;
parent::__construct($message);
}
/**
* @param \Exception[] $exceptions
*/
public static function create($exceptions)
{
$message = 'Could not find resource using any discovery strategy. Find more information at http://docs.php-http.org/en/latest/discovery.html#common-errors';
foreach ($exceptions as $e) {
$message .= "\n - ".$e->getMessage();
}
$message .= "\n\n";
return new self($message, $exceptions);
}
/**
* @return \Exception[]
*/
public function getExceptions()
{
return $this->exceptions;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* When we have used a strategy but no candidates provided by that strategy could be used.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class NoCandidateFoundException extends \Exception implements Exception
{
/**
* @param string $strategy
*/
public function __construct($strategy, array $candidates)
{
$classes = array_map(
function ($a) {
return $a['class'];
},
$candidates
);
$message = sprintf(
'No valid candidate found using strategy "%s". We tested the following candidates: %s.',
$strategy,
implode(', ', array_map([$this, 'stringify'], $classes))
);
parent::__construct($message);
}
private function stringify($mixed)
{
if (is_string($mixed)) {
return $mixed;
}
if (is_array($mixed) && 2 === count($mixed)) {
return sprintf('%s::%s', $this->stringify($mixed[0]), $mixed[1]);
}
return is_object($mixed) ? get_class($mixed) : gettype($mixed);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* Thrown when a discovery does not find any matches.
*
* @final do NOT extend this class, not final for BC reasons
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
/*final */class NotFoundException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Http\Discovery\Exception;
/**
* Thrown when we can't use Puli for discovery.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class PuliUnavailableException extends StrategyUnavailableException
{
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Http\Discovery\Exception;
use Http\Discovery\Exception;
/**
* This exception is thrown when we cannot use a discovery strategy. This is *not* thrown when
* the discovery fails to find a class.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class StrategyUnavailableException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Http\Discovery;
use Http\Client\HttpAsyncClient;
use Http\Discovery\Exception\DiscoveryFailedException;
/**
* Finds an HTTP Asynchronous Client.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
final class HttpAsyncClientDiscovery extends ClassDiscovery
{
/**
* Finds an HTTP Async Client.
*
* @return HttpAsyncClient
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$asyncClient = static::findOneByType(HttpAsyncClient::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException('No HTTPlug async clients found. Make sure to install a package providing "php-http/async-client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e);
}
return static::instantiateClass($asyncClient);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Http\Discovery;
use Http\Client\HttpClient;
use Http\Discovery\Exception\DiscoveryFailedException;
/**
* Finds an HTTP Client.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class HttpClientDiscovery extends ClassDiscovery
{
/**
* Finds an HTTP Client.
*
* @return HttpClient
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$client = static::findOneByType(HttpClient::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException('No HTTPlug clients found. Make sure to install a package providing "php-http/client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e);
}
return static::instantiateClass($client);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Http\Message\MessageFactory;
/**
* Finds a Message Factory.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery.
*/
final class MessageFactoryDiscovery extends ClassDiscovery
{
/**
* Finds a Message Factory.
*
* @return MessageFactory
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$messageFactory = static::findOneByType(MessageFactory::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException('No message factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e);
}
return static::instantiateClass($messageFactory);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Discovery;
/**
* Thrown when a discovery does not find any matches.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since since version 1.0, and will be removed in 2.0. Use {@link \Http\Discovery\Exception\NotFoundException} instead.
*/
final class NotFoundException extends \Http\Discovery\Exception\NotFoundException
{
}

View File

@@ -0,0 +1,136 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
/**
* Finds PSR-17 factories.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class Psr17FactoryDiscovery extends ClassDiscovery
{
private static function createException($type, Exception $e)
{
return new \Http\Discovery\Exception\NotFoundException(
'No PSR-17 '.$type.' found. Install a package from this list: https://packagist.org/providers/psr/http-factory-implementation',
0,
$e
);
}
/**
* @return RequestFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findRequestFactory()
{
try {
$messageFactory = static::findOneByType(RequestFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('request factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return ResponseFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findResponseFactory()
{
try {
$messageFactory = static::findOneByType(ResponseFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('response factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return ServerRequestFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findServerRequestFactory()
{
try {
$messageFactory = static::findOneByType(ServerRequestFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('server request factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return StreamFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findStreamFactory()
{
try {
$messageFactory = static::findOneByType(StreamFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('stream factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return UploadedFileFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findUploadedFileFactory()
{
try {
$messageFactory = static::findOneByType(UploadedFileFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('uploaded file factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return UriFactoryInterface
*
* @throws Exception\NotFoundException
*/
public static function findUriFactory()
{
try {
$messageFactory = static::findOneByType(UriFactoryInterface::class);
} catch (DiscoveryFailedException $e) {
throw self::createException('url factory', $e);
}
return static::instantiateClass($messageFactory);
}
/**
* @return UriFactoryInterface
*
* @throws Exception\NotFoundException
*
* @deprecated This will be removed in 2.0. Consider using the findUriFactory() method.
*/
public static function findUrlFactory()
{
return static::findUriFactory();
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Psr\Http\Client\ClientInterface;
/**
* Finds a PSR-18 HTTP Client.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class Psr18ClientDiscovery extends ClassDiscovery
{
/**
* Finds a PSR-18 HTTP Client.
*
* @return ClientInterface
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$client = static::findOneByType(ClientInterface::class);
} catch (DiscoveryFailedException $e) {
throw new \Http\Discovery\Exception\NotFoundException('No PSR-18 clients found. Make sure to install a package providing "psr/http-client-implementation". Example: "php-http/guzzle6-adapter".', 0, $e);
}
return static::instantiateClass($client);
}
}

View File

@@ -0,0 +1,181 @@
<?php
namespace Http\Discovery\Strategy;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Psr7\Request as GuzzleRequest;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Discovery\Exception\NotFoundException;
use Http\Discovery\MessageFactoryDiscovery;
use Http\Discovery\Psr17FactoryDiscovery;
use Http\Message\RequestFactory;
use Psr\Http\Message\RequestFactoryInterface as Psr17RequestFactory;
use Http\Message\MessageFactory;
use Http\Message\MessageFactory\GuzzleMessageFactory;
use Http\Message\StreamFactory;
use Http\Message\StreamFactory\GuzzleStreamFactory;
use Http\Message\UriFactory;
use Http\Message\UriFactory\GuzzleUriFactory;
use Http\Message\MessageFactory\DiactorosMessageFactory;
use Http\Message\StreamFactory\DiactorosStreamFactory;
use Http\Message\UriFactory\DiactorosUriFactory;
use Psr\Http\Client\ClientInterface as Psr18Client;
use Zend\Diactoros\Request as DiactorosRequest;
use Http\Message\MessageFactory\SlimMessageFactory;
use Http\Message\StreamFactory\SlimStreamFactory;
use Http\Message\UriFactory\SlimUriFactory;
use Slim\Http\Request as SlimRequest;
use GuzzleHttp\Client as GuzzleHttp;
use Http\Adapter\Guzzle7\Client as Guzzle7;
use Http\Adapter\Guzzle6\Client as Guzzle6;
use Http\Adapter\Guzzle5\Client as Guzzle5;
use Http\Client\Curl\Client as Curl;
use Http\Client\Socket\Client as Socket;
use Http\Adapter\React\Client as React;
use Http\Adapter\Buzz\Client as Buzz;
use Http\Adapter\Cake\Client as Cake;
use Http\Adapter\Zend\Client as Zend;
use Http\Adapter\Artax\Client as Artax;
use Symfony\Component\HttpClient\HttplugClient as SymfonyHttplug;
use Symfony\Component\HttpClient\Psr18Client as SymfonyPsr18;
use Nyholm\Psr7\Factory\HttplugFactory as NyholmHttplugFactory;
/**
* @internal
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class CommonClassesStrategy implements DiscoveryStrategy
{
/**
* @var array
*/
private static $classes = [
MessageFactory::class => [
['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]],
['class' => GuzzleMessageFactory::class, 'condition' => [GuzzleRequest::class, GuzzleMessageFactory::class]],
['class' => DiactorosMessageFactory::class, 'condition' => [DiactorosRequest::class, DiactorosMessageFactory::class]],
['class' => SlimMessageFactory::class, 'condition' => [SlimRequest::class, SlimMessageFactory::class]],
],
StreamFactory::class => [
['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]],
['class' => GuzzleStreamFactory::class, 'condition' => [GuzzleRequest::class, GuzzleStreamFactory::class]],
['class' => DiactorosStreamFactory::class, 'condition' => [DiactorosRequest::class, DiactorosStreamFactory::class]],
['class' => SlimStreamFactory::class, 'condition' => [SlimRequest::class, SlimStreamFactory::class]],
],
UriFactory::class => [
['class' => NyholmHttplugFactory::class, 'condition' => [NyholmHttplugFactory::class]],
['class' => GuzzleUriFactory::class, 'condition' => [GuzzleRequest::class, GuzzleUriFactory::class]],
['class' => DiactorosUriFactory::class, 'condition' => [DiactorosRequest::class, DiactorosUriFactory::class]],
['class' => SlimUriFactory::class, 'condition' => [SlimRequest::class, SlimUriFactory::class]],
],
HttpAsyncClient::class => [
['class' => SymfonyHttplug::class, 'condition' => [SymfonyHttplug::class, Promise::class, RequestFactory::class, [self::class, 'isPsr17FactoryInstalled']]],
['class' => Guzzle7::class, 'condition' => Guzzle7::class],
['class' => Guzzle6::class, 'condition' => Guzzle6::class],
['class' => Curl::class, 'condition' => Curl::class],
['class' => React::class, 'condition' => React::class],
],
HttpClient::class => [
['class' => SymfonyHttplug::class, 'condition' => [SymfonyHttplug::class, RequestFactory::class, [self::class, 'isPsr17FactoryInstalled']]],
['class' => Guzzle7::class, 'condition' => Guzzle7::class],
['class' => Guzzle6::class, 'condition' => Guzzle6::class],
['class' => Guzzle5::class, 'condition' => Guzzle5::class],
['class' => Curl::class, 'condition' => Curl::class],
['class' => Socket::class, 'condition' => Socket::class],
['class' => Buzz::class, 'condition' => Buzz::class],
['class' => React::class, 'condition' => React::class],
['class' => Cake::class, 'condition' => Cake::class],
['class' => Zend::class, 'condition' => Zend::class],
['class' => Artax::class, 'condition' => Artax::class],
[
'class' => [self::class, 'buzzInstantiate'],
'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class],
],
],
Psr18Client::class => [
[
'class' => [self::class, 'symfonyPsr18Instantiate'],
'condition' => [SymfonyPsr18::class, Psr17RequestFactory::class],
],
[
'class' => GuzzleHttp::class,
'condition' => [self::class, 'isGuzzleImplementingPsr18'],
],
[
'class' => [self::class, 'buzzInstantiate'],
'condition' => [\Buzz\Client\FileGetContents::class, \Buzz\Message\ResponseBuilder::class],
],
],
];
/**
* {@inheritdoc}
*/
public static function getCandidates($type)
{
if (Psr18Client::class === $type) {
return self::getPsr18Candidates();
}
return self::$classes[$type] ?? [];
}
/**
* @return array The return value is always an array with zero or more elements. Each
* element is an array with two keys ['class' => string, 'condition' => mixed].
*/
private static function getPsr18Candidates()
{
$candidates = self::$classes[Psr18Client::class];
// HTTPlug 2.0 clients implements PSR18Client too.
foreach (self::$classes[HttpClient::class] as $c) {
try {
if (is_subclass_of($c['class'], Psr18Client::class)) {
$candidates[] = $c;
}
} catch (\Throwable $e) {
trigger_error(sprintf('Got exception "%s (%s)" while checking if a PSR-18 Client is available', get_class($e), $e->getMessage()), E_USER_WARNING);
}
}
return $candidates;
}
public static function buzzInstantiate()
{
return new \Buzz\Client\FileGetContents(MessageFactoryDiscovery::find());
}
public static function symfonyPsr18Instantiate()
{
return new SymfonyPsr18(null, Psr17FactoryDiscovery::findResponseFactory(), Psr17FactoryDiscovery::findStreamFactory());
}
public static function isGuzzleImplementingPsr18()
{
return defined('GuzzleHttp\ClientInterface::MAJOR_VERSION');
}
/**
* Can be used as a condition.
*
* @return bool
*/
public static function isPsr17FactoryInstalled()
{
try {
Psr17FactoryDiscovery::findResponseFactory();
} catch (NotFoundException $e) {
return false;
} catch (\Throwable $e) {
trigger_error(sprintf('Got exception "%s (%s)" while checking if a PSR-17 ResponseFactory is available', get_class($e), $e->getMessage()), E_USER_WARNING);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Http\Discovery\Strategy;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
/**
* @internal
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
final class CommonPsr17ClassesStrategy implements DiscoveryStrategy
{
/**
* @var array
*/
private static $classes = [
RequestFactoryInterface::class => [
'Phalcon\Http\Message\RequestFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\RequestFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Http\Factory\Diactoros\RequestFactory',
'Http\Factory\Guzzle\RequestFactory',
'Http\Factory\Slim\RequestFactory',
'Laminas\Diactoros\RequestFactory',
'Slim\Psr7\Factory\RequestFactory',
],
ResponseFactoryInterface::class => [
'Phalcon\Http\Message\ResponseFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\ResponseFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Http\Factory\Diactoros\ResponseFactory',
'Http\Factory\Guzzle\ResponseFactory',
'Http\Factory\Slim\ResponseFactory',
'Laminas\Diactoros\ResponseFactory',
'Slim\Psr7\Factory\ResponseFactory',
],
ServerRequestFactoryInterface::class => [
'Phalcon\Http\Message\ServerRequestFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\ServerRequestFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Http\Factory\Diactoros\ServerRequestFactory',
'Http\Factory\Guzzle\ServerRequestFactory',
'Http\Factory\Slim\ServerRequestFactory',
'Laminas\Diactoros\ServerRequestFactory',
'Slim\Psr7\Factory\ServerRequestFactory',
],
StreamFactoryInterface::class => [
'Phalcon\Http\Message\StreamFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\StreamFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Http\Factory\Diactoros\StreamFactory',
'Http\Factory\Guzzle\StreamFactory',
'Http\Factory\Slim\StreamFactory',
'Laminas\Diactoros\StreamFactory',
'Slim\Psr7\Factory\StreamFactory',
],
UploadedFileFactoryInterface::class => [
'Phalcon\Http\Message\UploadedFileFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\UploadedFileFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Http\Factory\Diactoros\UploadedFileFactory',
'Http\Factory\Guzzle\UploadedFileFactory',
'Http\Factory\Slim\UploadedFileFactory',
'Laminas\Diactoros\UploadedFileFactory',
'Slim\Psr7\Factory\UploadedFileFactory',
],
UriFactoryInterface::class => [
'Phalcon\Http\Message\UriFactory',
'Nyholm\Psr7\Factory\Psr17Factory',
'Zend\Diactoros\UriFactory',
'GuzzleHttp\Psr7\HttpFactory',
'Http\Factory\Diactoros\UriFactory',
'Http\Factory\Guzzle\UriFactory',
'Http\Factory\Slim\UriFactory',
'Laminas\Diactoros\UriFactory',
'Slim\Psr7\Factory\UriFactory',
],
];
/**
* {@inheritdoc}
*/
public static function getCandidates($type)
{
$candidates = [];
if (isset(self::$classes[$type])) {
foreach (self::$classes[$type] as $class) {
$candidates[] = ['class' => $class, 'condition' => [$class]];
}
}
return $candidates;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Http\Discovery\Strategy;
use Http\Discovery\Exception\StrategyUnavailableException;
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
interface DiscoveryStrategy
{
/**
* Find a resource of a specific type.
*
* @param string $type
*
* @return array The return value is always an array with zero or more elements. Each
* element is an array with two keys ['class' => string, 'condition' => mixed].
*
* @throws StrategyUnavailableException if we cannot use this strategy.
*/
public static function getCandidates($type);
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Http\Discovery\Strategy;
use Http\Client\HttpAsyncClient;
use Http\Client\HttpClient;
use Http\Mock\Client as Mock;
/**
* Find the Mock client.
*
* @author Sam Rapaport <me@samrapdev.com>
*/
final class MockClientStrategy implements DiscoveryStrategy
{
/**
* {@inheritdoc}
*/
public static function getCandidates($type)
{
if (is_a(HttpClient::class, $type, true) || is_a(HttpAsyncClient::class, $type, true)) {
return [['class' => Mock::class, 'condition' => Mock::class]];
}
return [];
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Http\Discovery\Strategy;
use Http\Discovery\ClassDiscovery;
use Http\Discovery\Exception\PuliUnavailableException;
use Puli\Discovery\Api\Discovery;
use Puli\GeneratedPuliFactory;
/**
* Find candidates using Puli.
*
* @internal
* @final
*
* @author David de Boer <david@ddeboer.nl>
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class PuliBetaStrategy implements DiscoveryStrategy
{
/**
* @var GeneratedPuliFactory
*/
protected static $puliFactory;
/**
* @var Discovery
*/
protected static $puliDiscovery;
/**
* @return GeneratedPuliFactory
*
* @throws PuliUnavailableException
*/
private static function getPuliFactory()
{
if (null === self::$puliFactory) {
if (!defined('PULI_FACTORY_CLASS')) {
throw new PuliUnavailableException('Puli Factory is not available');
}
$puliFactoryClass = PULI_FACTORY_CLASS;
if (!ClassDiscovery::safeClassExists($puliFactoryClass)) {
throw new PuliUnavailableException('Puli Factory class does not exist');
}
self::$puliFactory = new $puliFactoryClass();
}
return self::$puliFactory;
}
/**
* Returns the Puli discovery layer.
*
* @return Discovery
*
* @throws PuliUnavailableException
*/
private static function getPuliDiscovery()
{
if (!isset(self::$puliDiscovery)) {
$factory = self::getPuliFactory();
$repository = $factory->createRepository();
self::$puliDiscovery = $factory->createDiscovery($repository);
}
return self::$puliDiscovery;
}
/**
* {@inheritdoc}
*/
public static function getCandidates($type)
{
$returnData = [];
$bindings = self::getPuliDiscovery()->findBindings($type);
foreach ($bindings as $binding) {
$condition = true;
if ($binding->hasParameterValue('depends')) {
$condition = $binding->getParameterValue('depends');
}
$returnData[] = ['class' => $binding->getClassName(), 'condition' => $condition];
}
return $returnData;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Http\Message\StreamFactory;
/**
* Finds a Stream Factory.
*
* @author Михаил Красильников <m.krasilnikov@yandex.ru>
*
* @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery.
*/
final class StreamFactoryDiscovery extends ClassDiscovery
{
/**
* Finds a Stream Factory.
*
* @return StreamFactory
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$streamFactory = static::findOneByType(StreamFactory::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException('No stream factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e);
}
return static::instantiateClass($streamFactory);
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Http\Discovery;
use Http\Discovery\Exception\DiscoveryFailedException;
use Http\Message\UriFactory;
/**
* Finds a URI Factory.
*
* @author David de Boer <david@ddeboer.nl>
*
* @deprecated This will be removed in 2.0. Consider using Psr17FactoryDiscovery.
*/
final class UriFactoryDiscovery extends ClassDiscovery
{
/**
* Finds a URI Factory.
*
* @return UriFactory
*
* @throws Exception\NotFoundException
*/
public static function find()
{
try {
$uriFactory = static::findOneByType(UriFactory::class);
} catch (DiscoveryFailedException $e) {
throw new NotFoundException('No uri factories found. To use Guzzle, Diactoros or Slim Framework factories install php-http/message and the chosen message implementation.', 0, $e);
}
return static::instantiateClass($uriFactory);
}
}

View File

@@ -0,0 +1,72 @@
# Change Log
## 1.1.0 - 2016-08-31
- Added HttpFulfilledPromise and HttpRejectedPromise which respect the HttpAsyncClient interface
## 1.0.0 - 2016-01-26
### Removed
- Stability configuration from composer
## 1.0.0-RC1 - 2016-01-12
### Changed
- Updated package files
- Updated promise dependency to RC1
## 1.0.0-beta - 2015-12-17
### Added
- Puli configuration and binding types
### Changed
- Exception concept
## 1.0.0-alpha3 - 2015-12-13
### Changed
- Async client does not throw exceptions
### Removed
- Promise interface moved to its own repository: [php-http/promise](https://github.com/php-http/promise)
## 1.0.0-alpha2 - 2015-11-16
### Added
- Async client and Promise interface
## 1.0.0-alpha - 2015-10-26
### Added
- Better domain exceptions.
### Changed
- Purpose of the library: general HTTP CLient abstraction.
### Removed
- Request options: they should be configured at construction time.
- Multiple request sending: should be done asynchronously using Async Client.
- `getName` method
## 0.1.0 - 2015-06-03
### Added
- Initial release

View File

@@ -0,0 +1,20 @@
Copyright (c) 2014-2015 Eric GELOEN <geloen.eric@gmail.com>
Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,57 @@
# HTTPlug
[![Latest Version](https://img.shields.io/github/release/php-http/httplug.svg?style=flat-square)](https://github.com/php-http/httplug/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/travis/php-http/httplug.svg?style=flat-square)](https://travis-ci.org/php-http/httplug)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/httplug.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/httplug)
[![Quality Score](https://img.shields.io/scrutinizer/g/php-http/httplug.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/httplug)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/httplug.svg?style=flat-square)](https://packagist.org/packages/php-http/httplug)
[![Slack Status](http://slack.httplug.io/badge.svg)](http://slack.httplug.io)
[![Email](https://img.shields.io/badge/email-team@httplug.io-blue.svg?style=flat-square)](mailto:team@httplug.io)
**HTTPlug, the HTTP client abstraction for PHP.**
## Install
Via Composer
``` bash
$ composer require php-http/httplug
```
## Intro
This is the contract package for HTTP Client.
Use it to create HTTP Clients which are interoperable and compatible with [PSR-7](http://www.php-fig.org/psr/psr-7/).
This library is the official successor of the [ivory http adapter](https://github.com/egeloen/ivory-http-adapter).
## Documentation
Please see the [official documentation](http://docs.php-http.org).
## Testing
``` bash
$ composer test
```
## Contributing
Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html).
## Security
If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org).
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

View File

@@ -0,0 +1,40 @@
{
"name": "php-http/httplug",
"description": "HTTPlug, the HTTP client abstraction for PHP",
"license": "MIT",
"keywords": ["http", "client"],
"homepage": "http://httplug.io",
"authors": [
{
"name": "Eric GELOEN",
"email": "geloen.eric@gmail.com"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": ">=5.4",
"psr/http-message": "^1.0",
"php-http/promise": "^1.0"
},
"require-dev": {
"phpspec/phpspec": "^2.4",
"henrikbjorn/phpspec-code-coverage" : "^1.0"
},
"autoload": {
"psr-4": {
"Http\\Client\\": "src/"
}
},
"scripts": {
"test": "vendor/bin/phpspec run",
"test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml"
},
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
}
}

View File

@@ -0,0 +1,12 @@
{
"version": "1.0",
"name": "php-http/httplug",
"binding-types": {
"Http\\Client\\HttpAsyncClient": {
"description": "Async HTTP Client"
},
"Http\\Client\\HttpClient": {
"description": "HTTP Client"
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Http\Client;
/**
* Every HTTP Client related Exception must implement this interface.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface Exception
{
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Thrown when a response was received but the request itself failed.
*
* In addition to the request, this exception always provides access to the response object.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class HttpException extends RequestException
{
/**
* @var ResponseInterface
*/
protected $response;
/**
* @param string $message
* @param RequestInterface $request
* @param ResponseInterface $response
* @param \Exception|null $previous
*/
public function __construct(
$message,
RequestInterface $request,
ResponseInterface $response,
\Exception $previous = null
) {
parent::__construct($message, $request, $previous);
$this->response = $response;
$this->code = $response->getStatusCode();
}
/**
* Returns the response.
*
* @return ResponseInterface
*/
public function getResponse()
{
return $this->response;
}
/**
* Factory method to create a new exception with a normalized error message.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param \Exception|null $previous
*
* @return HttpException
*/
public static function create(
RequestInterface $request,
ResponseInterface $response,
\Exception $previous = null
) {
$message = sprintf(
'[url] %s [http method] %s [status code] %s [reason phrase] %s',
$request->getRequestTarget(),
$request->getMethod(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
return new self($message, $request, $response, $previous);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Exception;
/**
* Thrown when the request cannot be completed because of network issues.
*
* There is no response object as this exception is thrown when no response has been received.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class NetworkException extends RequestException
{
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Http\Client\Exception;
use Psr\Http\Message\RequestInterface;
/**
* Exception for when a request failed, providing access to the failed request.
*
* This could be due to an invalid request, or one of the extending exceptions
* for network errors or HTTP error responses.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class RequestException extends TransferException
{
/**
* @var RequestInterface
*/
private $request;
/**
* @param string $message
* @param RequestInterface $request
* @param \Exception|null $previous
*/
public function __construct($message, RequestInterface $request, \Exception $previous = null)
{
$this->request = $request;
parent::__construct($message, 0, $previous);
}
/**
* Returns the request.
*
* @return RequestInterface
*/
public function getRequest()
{
return $this->request;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Http\Client\Exception;
use Http\Client\Exception;
/**
* Base exception for transfer related exceptions.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class TransferException extends \RuntimeException implements Exception
{
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Http\Client;
use Http\Promise\Promise;
use Psr\Http\Message\RequestInterface;
/**
* Sends a PSR-7 Request in an asynchronous way by returning a Promise.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
interface HttpAsyncClient
{
/**
* Sends a PSR-7 request in an asynchronous way.
*
* Exceptions related to processing the request are available from the returned Promise.
*
* @param RequestInterface $request
*
* @return Promise Resolves a PSR-7 Response or fails with an Http\Client\Exception.
*
* @throws \Exception If processing the request is impossible (eg. bad configuration).
*/
public function sendAsyncRequest(RequestInterface $request);
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Http\Client;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Sends a PSR-7 Request and returns a PSR-7 response.
*
* @author GeLo <geloen.eric@gmail.com>
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
* @author David Buchmann <mail@davidbu.ch>
*/
interface HttpClient
{
/**
* Sends a PSR-7 request.
*
* @param RequestInterface $request
*
* @return ResponseInterface
*
* @throws \Http\Client\Exception If an error happens during processing the request.
* @throws \Exception If processing the request is impossible (eg. bad configuration).
*/
public function sendRequest(RequestInterface $request);
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Http\Client\Promise;
use Http\Client\Exception;
use Http\Promise\Promise;
use Psr\Http\Message\ResponseInterface;
final class HttpFulfilledPromise implements Promise
{
/**
* @var ResponseInterface
*/
private $response;
/**
* @param ResponseInterface $response
*/
public function __construct(ResponseInterface $response)
{
$this->response = $response;
}
/**
* {@inheritdoc}
*/
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
if (null === $onFulfilled) {
return $this;
}
try {
return new self($onFulfilled($this->response));
} catch (Exception $e) {
return new HttpRejectedPromise($e);
}
}
/**
* {@inheritdoc}
*/
public function getState()
{
return Promise::FULFILLED;
}
/**
* {@inheritdoc}
*/
public function wait($unwrap = true)
{
if ($unwrap) {
return $this->response;
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Http\Client\Promise;
use Http\Client\Exception;
use Http\Promise\Promise;
final class HttpRejectedPromise implements Promise
{
/**
* @var Exception
*/
private $exception;
/**
* @param Exception $exception
*/
public function __construct(Exception $exception)
{
$this->exception = $exception;
}
/**
* {@inheritdoc}
*/
public function then(callable $onFulfilled = null, callable $onRejected = null)
{
if (null === $onRejected) {
return $this;
}
try {
return new HttpFulfilledPromise($onRejected($this->exception));
} catch (Exception $e) {
return new self($e);
}
}
/**
* {@inheritdoc}
*/
public function getState()
{
return Promise::REJECTED;
}
/**
* {@inheritdoc}
*/
public function wait($unwrap = true)
{
if ($unwrap) {
throw $this->exception;
}
}
}

View File

@@ -0,0 +1,65 @@
# Change Log
## 1.0.2 - 2015-12-19
### Added
- Request and Response factory binding types to Puli
## 1.0.1 - 2015-12-17
### Added
- Puli configuration and binding types
## 1.0.0 - 2015-12-15
### Added
- Response Factory in order to be reused in Message and Server Message factories
- Request Factory
### Changed
- Message Factory extends Request and Response factories
## 1.0.0-RC1 - 2015-12-14
### Added
- CS check
### Changed
- RuntimeException is thrown when the StreamFactory cannot write to the underlying stream
## 0.3.0 - 2015-11-16
### Removed
- Client Context Factory
- Factory Awares and Templates
## 0.2.0 - 2015-11-16
### Changed
- Reordered the parameters when creating a message to have the protocol last,
as its the least likely to need to be changed.
## 0.1.0 - 2015-06-01
### Added
- Initial release
### Changed
- Helpers are renamed to templates

View File

@@ -0,0 +1,19 @@
Copyright (c) 2015 PHP HTTP Team <team@php-http.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,36 @@
# PSR-7 Message Factory
[![Latest Version](https://img.shields.io/github/release/php-http/message-factory.svg?style=flat-square)](https://github.com/php-http/message-factory/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/message-factory.svg?style=flat-square)](https://packagist.org/packages/php-http/message-factory)
**Factory interfaces for PSR-7 HTTP Message.**
## Install
Via Composer
``` bash
$ composer require php-http/message-factory
```
## Documentation
Please see the [official documentation](http://php-http.readthedocs.org/en/latest/message-factory/).
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) and [CONDUCT](CONDUCT.md) for details.
## Security
If you discover any security related issues, please contact us at [security@php-http.org](mailto:security@php-http.org).
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

View File

@@ -0,0 +1,27 @@
{
"name": "php-http/message-factory",
"description": "Factory interfaces for PSR-7 HTTP Message",
"license": "MIT",
"keywords": ["http", "factory", "message", "stream", "uri"],
"homepage": "http://php-http.org",
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": ">=5.4",
"psr/http-message": "^1.0"
},
"autoload": {
"psr-4": {
"Http\\Message\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
}
}

View File

@@ -0,0 +1,43 @@
{
"version": "1.0",
"binding-types": {
"Http\\Message\\MessageFactory": {
"description": "PSR-7 Message Factory",
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
},
"Http\\Message\\RequestFactory": {
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
},
"Http\\Message\\ResponseFactory": {
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
},
"Http\\Message\\StreamFactory": {
"description": "PSR-7 Stream Factory",
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
},
"Http\\Message\\UriFactory": {
"description": "PSR-7 URI Factory",
"parameters": {
"depends": {
"description": "Optional class dependency which can be checked by consumers"
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Http\Message;
/**
* Factory for PSR-7 Request and Response.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface MessageFactory extends RequestFactory, ResponseFactory
{
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Http\Message;
use Psr\Http\Message\UriInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
/**
* Factory for PSR-7 Request.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface RequestFactory
{
/**
* Creates a new PSR-7 request.
*
* @param string $method
* @param string|UriInterface $uri
* @param array $headers
* @param resource|string|StreamInterface|null $body
* @param string $protocolVersion
*
* @return RequestInterface
*/
public function createRequest(
$method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
);
}

View File

@@ -0,0 +1,35 @@
<?php
namespace Http\Message;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
/**
* Factory for PSR-7 Response.
*
* This factory contract can be reused in Message and Server Message factories.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface ResponseFactory
{
/**
* Creates a new PSR-7 response.
*
* @param int $statusCode
* @param string|null $reasonPhrase
* @param array $headers
* @param resource|string|StreamInterface|null $body
* @param string $protocolVersion
*
* @return ResponseInterface
*/
public function createResponse(
$statusCode = 200,
$reasonPhrase = null,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
);
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Http\Message;
use Psr\Http\Message\StreamInterface;
/**
* Factory for PSR-7 Stream.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface StreamFactory
{
/**
* Creates a new PSR-7 stream.
*
* @param string|resource|StreamInterface|null $body
*
* @return StreamInterface
*
* @throws \InvalidArgumentException If the stream body is invalid.
* @throws \RuntimeException If creating the stream from $body fails.
*/
public function createStream($body = null);
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Http\Message;
use Psr\Http\Message\UriInterface;
/**
* Factory for PSR-7 URI.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface UriFactory
{
/**
* Creates an PSR-7 URI.
*
* @param string|UriInterface $uri
*
* @return UriInterface
*
* @throws \InvalidArgumentException If the $uri argument can not be converted into a valid URI.
*/
public function createUri($uri);
}

View File

@@ -0,0 +1,224 @@
# Change Log
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [1.10.0] - 2020-11-11
- Added support for PHP 8.0.
## [1.9.1] - 2020-10-13
- Improved detection of binary stream to not consider newlines, carriage return or tabs as binary.
## [1.9.0] - 2020-08-17
- Omitted binary body in FullHttpMessageFormatter and CurlCommandFormatter.
`[binary stream omitted]` will be shown instead.
### Added
- New Header authentication method for arbitrary header authentication.
## [1.8.0] - 2019-08-05
### Changed
- Raised minimum PHP version to 7.1
### Fixed
- Fatal error on `CurlCommandFormatter` when body is larger than `escapeshellarg` allowed length.
- Do not read stream in message formatter if stream is not seekable.
## [1.7.2] - 2018-10-30
### Fixed
- FilteredStream uses `@trigger_error` instead of throwing exceptions to not
break careless users. You still need to fix your stream code to respect
`isSeekable`. Seeking does not work as expected, and we will add exceptions
in version 2.
## [1.7.1] - 2018-10-29
### Fixed
- FilteredStream is not actually seekable
## [1.7.0] - 2018-08-15
### Fixed
- Fix CurlCommandFormatter for binary request payloads
- Fix QueryParam authentication to assemble proper URL regardless of PHP `arg_separator.output` directive
- Do not pass `null` parameters to `Clue\StreamFilter\fun`
### Changed
- Dropped tests on HHVM
## [1.6.0] - 2017-07-05
### Added
- CookieUtil::parseDate to create a date from cookie date string
### Fixed
- Fix curl command of CurlFormatter when there is an user-agent header
## [1.5.0] - 2017-02-14
### Added
- Check for empty string in Stream factories
- Cookie::createWithoutValidation Static constructor to create a cookie. Will not perform any attribute validation during instantiation.
- Cookie::isValid Method to check if cookie attributes are valid.
### Fixed
- FilteredStream::getSize returns null because the contents size is unknown.
- Stream factories does not rewinds streams. The previous behavior was not coherent between factories and inputs.
### Deprecated
- FilteredStream::getReadFilter The read filter is internal and should never be used by consuming code.
- FilteredStream::getWriteFilter We did not implement writing to the streams at all. And if we do, the filter is an internal information and should not be used by consuming code.
## [1.4.1] - 2016-12-16
### Fixed
- Cookie::matchPath Cookie with root path (`/`) will not match sub path (e.g. `/cookie`).
## [1.4.0] - 2016-10-20
### Added
- Message, stream and URI factories for [Slim Framework](https://github.com/slimphp/Slim)
- BufferedStream that allow you to decorate a non-seekable stream with a seekable one.
- cUrlFormatter to be able to redo the request with a cURL command
## [1.3.1] - 2016-07-15
### Fixed
- FullHttpMessageFormatter will not read from streams that you cannot rewind (non-seekable)
- FullHttpMessageFormatter will not read from the stream if $maxBodyLength is zero
- FullHttpMessageFormatter rewinds streams after they are read
## [1.3.0] - 2016-07-14
### Added
- FullHttpMessageFormatter to include headers and body in the formatted message
### Fixed
- #41: Response builder broke header value
## [1.2.0] - 2016-03-29
### Added
- The RequestMatcher is built after the Symfony RequestMatcher and separates
scheme, host and path expressions and provides an option to filter on the
method
- New RequestConditional authentication method using request matchers
- Add automatic basic auth info detection based on the URL
### Changed
- Improved ResponseBuilder
### Deprecated
- RegexRequestMatcher, use RequestMatcher instead
- Matching authenitcation method, use RequestConditional instead
## [1.1.0] - 2016-02-25
### Added
- Add a request matcher interface and regex implementation
- Add a callback request matcher implementation
- Add a ResponseBuilder, to create PSR7 Response from a string
### Fixed
- Fix casting string on a FilteredStream not filtering the output
## [1.0.0] - 2016-01-27
## [0.2.0] - 2015-12-29
### Added
- Autoregistration of stream filters using Composer autoload
- Cookie
- [Apigen](http://www.apigen.org/) configuration
## [0.1.2] - 2015-12-26
### Added
- Request and response factory bindings
### Fixed
- Chunk filter namespace in Dechunk stream
## [0.1.1] - 2015-12-25
### Added
- Formatter
## 0.1.0 - 2015-12-24
### Added
- Authentication
- Encoding
- Message decorator
- Message factory (Guzzle, Diactoros)
[Unreleased]: https://github.com/php-http/message/compare/1.10.0...HEAD
[1.10.0]: https://github.com/php-http/message/compare/1.9.1...1.10.0
[1.9.1]: https://github.com/php-http/message/compare/1.9.0...1.9.1
[1.9.0]: https://github.com/php-http/message/compare/1.8.0...1.9.0
[1.8.0]: https://github.com/php-http/message/compare/1.7.2...1.8.0
[1.7.2]: https://github.com/php-http/message/compare/v1.7.1...1.7.2
[1.7.1]: https://github.com/php-http/message/compare/1.7.0...v1.7.1
[1.7.0]: https://github.com/php-http/message/compare/1.6.0...1.7.0
[1.6.0]: https://github.com/php-http/message/compare/1.5.0...1.6.0
[1.5.0]: https://github.com/php-http/message/compare/v1.4.1...1.5.0
[1.4.1]: https://github.com/php-http/message/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/php-http/message/compare/v1.3.1...v1.4.0
[1.3.1]: https://github.com/php-http/message/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/php-http/message/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/php-http/message/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/php-http/message/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/php-http/message/compare/0.2.0...v1.0.0
[0.2.0]: https://github.com/php-http/message/compare/v0.1.2...0.2.0
[0.1.2]: https://github.com/php-http/message/compare/v0.1.1...v0.1.2
[0.1.1]: https://github.com/php-http/message/compare/v0.1.0...v0.1.1

View File

@@ -0,0 +1,19 @@
Copyright (c) 2015-2016 PHP HTTP Team <team@php-http.org>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,50 @@
# HTTP Message
[![Latest Version](https://img.shields.io/github/release/php-http/message.svg?style=flat-square)](https://github.com/php-http/message/releases)
![GitHub Workflow Status](https://img.shields.io/github/workflow/status/php-http/message/CI?style=flat-square)
[![Total Downloads](https://img.shields.io/packagist/dt/php-http/message.svg?style=flat-square)](https://packagist.org/packages/php-http/message)
**HTTP Message related tools.**
## Install
Via Composer
``` bash
$ composer require php-http/message
```
## Intro
This package contains various PSR-7 tools which might be useful in an HTTP workflow:
- Authentication method implementations
- Various Stream encoding tools
- Message decorators
- Message factory implementations for Guzzle PSR-7 and Diactoros
- Cookie implementation
- Request matchers
## Documentation
Please see the [official documentation](http://docs.php-http.org/en/latest/message.html).
## Testing
``` bash
$ composer test
```
## Credits
Thanks to [Cuzzle](https://github.com/namshi/cuzzle) for inpiration for the `CurlCommandFormatter`.
## License
The MIT License (MIT). Please see [License File](LICENSE) for more information.

View File

@@ -0,0 +1,6 @@
source:
- src/
destination: build/api/
templateTheme: bootstrap

View File

@@ -0,0 +1,65 @@
{
"name": "php-http/message",
"description": "HTTP Message related tools",
"keywords": [
"message",
"http",
"psr-7"
],
"homepage": "http://php-http.org",
"license": "MIT",
"authors": [
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com"
}
],
"require": {
"php": "^7.1 || ^8.0",
"clue/stream-filter": "^1.5",
"php-http/message-factory": "^1.0.2",
"psr/http-message": "^1.0"
},
"provide": {
"php-http/message-factory-implementation": "1.0"
},
"require-dev": {
"ext-zlib": "*",
"ergebnis/composer-normalize": "^2.6",
"guzzlehttp/psr7": "^1.0",
"phpspec/phpspec": "^5.1 || ^6.3",
"slim/slim": "^3.0",
"zendframework/zend-diactoros": "^1.0"
},
"suggest": {
"ext-zlib": "Used with compressor/decompressor streams",
"guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories",
"slim/slim": "Used with Slim Framework PSR-7 implementation",
"zendframework/zend-diactoros": "Used with Diactoros Factories"
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "1.10-dev"
}
},
"autoload": {
"psr-4": {
"Http\\Message\\": "src/"
},
"files": [
"src/filters.php"
]
},
"autoload-dev": {
"psr-4": {
"spec\\Http\\Message\\": "spec/"
}
},
"scripts": {
"test": "vendor/bin/phpspec run",
"test-ci": "vendor/bin/phpspec run -c phpspec.ci.yml"
}
}

View File

@@ -0,0 +1,111 @@
{
"version": "1.0",
"name": "php-http/message",
"bindings": {
"064d003d-78a1-48c4-8f3b-1f92ff25da69": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory",
"type": "Http\\Message\\MessageFactory",
"parameters": {
"depends": "Zend\\Diactoros\\Request"
}
},
"0836751e-6558-4d1b-8993-4a52012947c3": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\SlimMessageFactory",
"type": "Http\\Message\\ResponseFactory"
},
"1d127622-dc61-4bfa-b9da-d221548d72c3": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\SlimMessageFactory",
"type": "Http\\Message\\RequestFactory"
},
"2438c2d0-0658-441f-8855-ddaf0f87d54d": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory",
"type": "Http\\Message\\MessageFactory",
"parameters": {
"depends": "GuzzleHttp\\Psr7\\Request"
}
},
"253aa08c-d705-46e7-b1d2-e28c97eef792": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory",
"type": "Http\\Message\\RequestFactory",
"parameters": {
"depends": "GuzzleHttp\\Psr7\\Request"
}
},
"273a34f9-62f4-4ba1-9801-b1284d49ff89": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\StreamFactory\\GuzzleStreamFactory",
"type": "Http\\Message\\StreamFactory",
"parameters": {
"depends": "GuzzleHttp\\Psr7\\Stream"
}
},
"304b83db-b594-4d83-ae75-1f633adf92f7": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\UriFactory\\GuzzleUriFactory",
"type": "Http\\Message\\UriFactory",
"parameters": {
"depends": "GuzzleHttp\\Psr7\\Uri"
}
},
"3f4bc1cd-aa95-4702-9fa7-65408e471691": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\UriFactory\\DiactorosUriFactory",
"type": "Http\\Message\\UriFactory",
"parameters": {
"depends": "Zend\\Diactoros\\Uri"
}
},
"4672a6ee-ad9e-4109-a5d1-b7d46f26c7a1": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\SlimMessageFactory",
"type": "Http\\Message\\MessageFactory"
},
"6234e947-d3bd-43eb-97d5-7f9e22e6bb1b": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory",
"type": "Http\\Message\\ResponseFactory",
"parameters": {
"depends": "Zend\\Diactoros\\Response"
}
},
"6a9ad6ce-d82c-470f-8e30-60f21d9d95bf": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\UriFactory\\SlimUriFactory",
"type": "Http\\Message\\UriFactory"
},
"72c2afa0-ea56-4d03-adb6-a9f241a8a734": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\StreamFactory\\SlimStreamFactory",
"type": "Http\\Message\\StreamFactory"
},
"95c1be8f-39fe-4abd-8351-92cb14379a75": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\StreamFactory\\DiactorosStreamFactory",
"type": "Http\\Message\\StreamFactory",
"parameters": {
"depends": "Zend\\Diactoros\\Stream"
}
},
"a018af27-7590-4dcf-83a1-497f95604cd6": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\GuzzleMessageFactory",
"type": "Http\\Message\\ResponseFactory",
"parameters": {
"depends": "GuzzleHttp\\Psr7\\Response"
}
},
"c07955b1-de46-43db-923b-d07fae9382cb": {
"_class": "Puli\\Discovery\\Binding\\ClassBinding",
"class": "Http\\Message\\MessageFactory\\DiactorosMessageFactory",
"type": "Http\\Message\\RequestFactory",
"parameters": {
"depends": "Zend\\Diactoros\\Request"
}
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace Http\Message;
use Psr\Http\Message\RequestInterface;
/**
* Add authentication information to a PSR-7 Request.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface Authentication
{
/**
* Alter the request to add the authentication credentials.
*
* To do that, the implementation might use pre-stored credentials or do
* separate HTTP requests to obtain a valid token.
*
* @param RequestInterface $request The request without authentication information
*
* @return RequestInterface The request with added authentication information
*/
public function authenticate(RequestInterface $request);
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request using Basic Auth based on credentials in the URI.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class AutoBasicAuth implements Authentication
{
/**
* Whether user info should be removed from the URI.
*
* @var bool
*/
private $shouldRemoveUserInfo;
/**
* @param bool|true $shouldRremoveUserInfo
*/
public function __construct($shouldRremoveUserInfo = true)
{
$this->shouldRemoveUserInfo = (bool) $shouldRremoveUserInfo;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
$uri = $request->getUri();
$userInfo = $uri->getUserInfo();
if (!empty($userInfo)) {
if ($this->shouldRemoveUserInfo) {
$request = $request->withUri($uri->withUserInfo(''));
}
$request = $request->withHeader('Authorization', sprintf('Basic %s', base64_encode($userInfo)));
}
return $request;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request using Basic Auth.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class BasicAuth implements Authentication
{
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $password;
/**
* @param string $username
* @param string $password
*/
public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
$header = sprintf('Basic %s', base64_encode(sprintf('%s:%s', $this->username, $this->password)));
return $request->withHeader('Authorization', $header);
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request using a token.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class Bearer implements Authentication
{
/**
* @var string
*/
private $token;
/**
* @param string $token
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
$header = sprintf('Bearer %s', $this->token);
return $request->withHeader('Authorization', $header);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request with a multiple authentication methods.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class Chain implements Authentication
{
/**
* @var Authentication[]
*/
private $authenticationChain = [];
/**
* @param Authentication[] $authenticationChain
*/
public function __construct(array $authenticationChain = [])
{
foreach ($authenticationChain as $authentication) {
if (!$authentication instanceof Authentication) {
throw new \InvalidArgumentException(
'Members of the authentication chain must be of type Http\Message\Authentication'
);
}
}
$this->authenticationChain = $authenticationChain;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
foreach ($this->authenticationChain as $authentication) {
$request = $authentication->authenticate($request);
}
return $request;
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
class Header implements Authentication
{
/**
* @var string
*/
private $name;
/**
* @var string|array
*/
private $value;
public function __construct(string $name, $value)
{
$this->name = $name;
$this->value = $value;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
return $request->withHeader($this->name, $this->value);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Http\Message\RequestMatcher\CallbackRequestMatcher;
use Psr\Http\Message\RequestInterface;
@trigger_error('The '.__NAMESPACE__.'\Matching class is deprecated since version 1.2 and will be removed in 2.0. Use Http\Message\Authentication\RequestConditional instead.', E_USER_DEPRECATED);
/**
* Authenticate a PSR-7 Request if the request is matching.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @deprecated since since version 1.2, and will be removed in 2.0. Use {@link RequestConditional} instead.
*/
final class Matching implements Authentication
{
/**
* @var Authentication
*/
private $authentication;
/**
* @var CallbackRequestMatcher
*/
private $matcher;
public function __construct(Authentication $authentication, callable $matcher = null)
{
if (is_null($matcher)) {
$matcher = function () {
return true;
};
}
$this->authentication = $authentication;
$this->matcher = new CallbackRequestMatcher($matcher);
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
if ($this->matcher->matches($request)) {
return $this->authentication->authenticate($request);
}
return $request;
}
/**
* Creates a matching authentication for an URL.
*
* @param string $url
*
* @return self
*/
public static function createUrlMatcher(Authentication $authentication, $url)
{
$matcher = function (RequestInterface $request) use ($url) {
return preg_match($url, $request->getRequestTarget());
};
return new static($authentication, $matcher);
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request by adding parameters to its query.
*
* Note: Although in some cases it can be useful, we do not recommend using query parameters for authentication.
* Credentials in the URL is generally unsafe as they are not encrypted, anyone can see them.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class QueryParam implements Authentication
{
/**
* @var array
*/
private $params = [];
public function __construct(array $params)
{
$this->params = $params;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
$uri = $request->getUri();
$query = $uri->getQuery();
$params = [];
parse_str($query, $params);
$params = array_merge($params, $this->params);
$query = http_build_query($params, null, '&');
$uri = $uri->withQuery($query);
return $request->withUri($uri);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Http\Message\RequestMatcher;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request if the request is matching the given request matcher.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class RequestConditional implements Authentication
{
/**
* @var RequestMatcher
*/
private $requestMatcher;
/**
* @var Authentication
*/
private $authentication;
public function __construct(RequestMatcher $requestMatcher, Authentication $authentication)
{
$this->requestMatcher = $requestMatcher;
$this->authentication = $authentication;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
if ($this->requestMatcher->matches($request)) {
return $this->authentication->authenticate($request);
}
return $request;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Http\Message\Authentication;
use Http\Message\Authentication;
use Psr\Http\Message\RequestInterface;
/**
* Authenticate a PSR-7 Request using WSSE.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class Wsse implements Authentication
{
/**
* @var string
*/
private $username;
/**
* @var string
*/
private $password;
/**
* @param string $username
* @param string $password
*/
public function __construct($username, $password)
{
$this->username = $username;
$this->password = $password;
}
/**
* {@inheritdoc}
*/
public function authenticate(RequestInterface $request)
{
// TODO: generate better nonce?
$nonce = substr(md5(uniqid(uniqid().'_', true)), 0, 16);
$created = date('c');
$digest = base64_encode(sha1(base64_decode($nonce).$created.$this->password, true));
$wsse = sprintf(
'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"',
$this->username,
$digest,
$nonce,
$created
);
return $request
->withHeader('Authorization', 'WSSE profile="UsernameToken"')
->withHeader('X-WSSE', $wsse)
;
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace Http\Message\Builder;
use Psr\Http\Message\ResponseInterface;
/**
* Fills response object with values.
*/
class ResponseBuilder
{
/**
* The response to be built.
*
* @var ResponseInterface
*/
protected $response;
/**
* Create builder for the given response.
*/
public function __construct(ResponseInterface $response)
{
$this->response = $response;
}
/**
* Return response.
*
* @return ResponseInterface
*/
public function getResponse()
{
return $this->response;
}
/**
* Add headers represented by an array of header lines.
*
* @param string[] $headers response headers as array of header lines
*
* @return $this
*
* @throws \UnexpectedValueException for invalid header values
* @throws \InvalidArgumentException for invalid status code arguments
*/
public function setHeadersFromArray(array $headers)
{
$status = array_shift($headers);
$this->setStatus($status);
foreach ($headers as $headerLine) {
$headerLine = trim($headerLine);
if ('' === $headerLine) {
continue;
}
$this->addHeader($headerLine);
}
return $this;
}
/**
* Add headers represented by a single string.
*
* @param string $headers response headers as single string
*
* @return $this
*
* @throws \InvalidArgumentException if $headers is not a string on object with __toString()
* @throws \UnexpectedValueException for invalid header values
*/
public function setHeadersFromString($headers)
{
if (!(is_string($headers)
|| (is_object($headers) && method_exists($headers, '__toString')))
) {
throw new \InvalidArgumentException(
sprintf(
'%s expects parameter 1 to be a string, %s given',
__METHOD__,
is_object($headers) ? get_class($headers) : gettype($headers)
)
);
}
$this->setHeadersFromArray(explode("\r\n", $headers));
return $this;
}
/**
* Set response status from a status string.
*
* @param string $statusLine response status as a string
*
* @return $this
*
* @throws \InvalidArgumentException for invalid status line
*/
public function setStatus($statusLine)
{
$parts = explode(' ', $statusLine, 3);
if (count($parts) < 2 || 0 !== strpos(strtolower($parts[0]), 'http/')) {
throw new \InvalidArgumentException(
sprintf('"%s" is not a valid HTTP status line', $statusLine)
);
}
$reasonPhrase = count($parts) > 2 ? $parts[2] : '';
$this->response = $this->response
->withStatus((int) $parts[1], $reasonPhrase)
->withProtocolVersion(substr($parts[0], 5));
return $this;
}
/**
* Add header represented by a string.
*
* @param string $headerLine response header as a string
*
* @return $this
*
* @throws \InvalidArgumentException for invalid header names or values
*/
public function addHeader($headerLine)
{
$parts = explode(':', $headerLine, 2);
if (2 !== count($parts)) {
throw new \InvalidArgumentException(
sprintf('"%s" is not a valid HTTP header line', $headerLine)
);
}
$name = trim($parts[0]);
$value = trim($parts[1]);
if ($this->response->hasHeader($name)) {
$this->response = $this->response->withAddedHeader($name, $value);
} else {
$this->response = $this->response->withHeader($name, $value);
}
return $this;
}
}

View File

@@ -0,0 +1,524 @@
<?php
namespace Http\Message;
/**
* Cookie Value Object.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*
* @see http://tools.ietf.org/search/rfc6265
*/
final class Cookie
{
/**
* @var string
*/
private $name;
/**
* @var string|null
*/
private $value;
/**
* @var int|null
*/
private $maxAge;
/**
* @var string|null
*/
private $domain;
/**
* @var string
*/
private $path;
/**
* @var bool
*/
private $secure;
/**
* @var bool
*/
private $httpOnly;
/**
* Expires attribute is HTTP 1.0 only and should be avoided.
*
* @var \DateTime|null
*/
private $expires;
/**
* @param string $name
* @param string|null $value
* @param int|null $maxAge
* @param string|null $domain
* @param string|null $path
* @param bool $secure
* @param bool $httpOnly
* @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided.
*
* @throws \InvalidArgumentException if name, value or max age is not valid
*/
public function __construct(
$name,
$value = null,
$maxAge = null,
$domain = null,
$path = null,
$secure = false,
$httpOnly = false,
\DateTime $expires = null
) {
$this->validateName($name);
$this->validateValue($value);
$this->validateMaxAge($maxAge);
$this->name = $name;
$this->value = $value;
$this->maxAge = $maxAge;
$this->expires = $expires;
$this->domain = $this->normalizeDomain($domain);
$this->path = $this->normalizePath($path);
$this->secure = (bool) $secure;
$this->httpOnly = (bool) $httpOnly;
}
/**
* Creates a new cookie without any attribute validation.
*
* @param string $name
* @param string|null $value
* @param int $maxAge
* @param string|null $domain
* @param string|null $path
* @param bool $secure
* @param bool $httpOnly
* @param \DateTime|null $expires Expires attribute is HTTP 1.0 only and should be avoided.
*/
public static function createWithoutValidation(
$name,
$value = null,
$maxAge = null,
$domain = null,
$path = null,
$secure = false,
$httpOnly = false,
\DateTime $expires = null
) {
$cookie = new self('name', null, null, $domain, $path, $secure, $httpOnly, $expires);
$cookie->name = $name;
$cookie->value = $value;
$cookie->maxAge = $maxAge;
return $cookie;
}
/**
* Returns the name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Returns the value.
*
* @return string|null
*/
public function getValue()
{
return $this->value;
}
/**
* Checks if there is a value.
*
* @return bool
*/
public function hasValue()
{
return isset($this->value);
}
/**
* Sets the value.
*
* @param string|null $value
*
* @return Cookie
*/
public function withValue($value)
{
$this->validateValue($value);
$new = clone $this;
$new->value = $value;
return $new;
}
/**
* Returns the max age.
*
* @return int|null
*/
public function getMaxAge()
{
return $this->maxAge;
}
/**
* Checks if there is a max age.
*
* @return bool
*/
public function hasMaxAge()
{
return isset($this->maxAge);
}
/**
* Sets the max age.
*
* @param int|null $maxAge
*
* @return Cookie
*/
public function withMaxAge($maxAge)
{
$this->validateMaxAge($maxAge);
$new = clone $this;
$new->maxAge = $maxAge;
return $new;
}
/**
* Returns the expiration time.
*
* @return \DateTime|null
*/
public function getExpires()
{
return $this->expires;
}
/**
* Checks if there is an expiration time.
*
* @return bool
*/
public function hasExpires()
{
return isset($this->expires);
}
/**
* Sets the expires.
*
* @return Cookie
*/
public function withExpires(\DateTime $expires = null)
{
$new = clone $this;
$new->expires = $expires;
return $new;
}
/**
* Checks if the cookie is expired.
*
* @return bool
*/
public function isExpired()
{
return isset($this->expires) and $this->expires < new \DateTime();
}
/**
* Returns the domain.
*
* @return string|null
*/
public function getDomain()
{
return $this->domain;
}
/**
* Checks if there is a domain.
*
* @return bool
*/
public function hasDomain()
{
return isset($this->domain);
}
/**
* Sets the domain.
*
* @param string|null $domain
*
* @return Cookie
*/
public function withDomain($domain)
{
$new = clone $this;
$new->domain = $this->normalizeDomain($domain);
return $new;
}
/**
* Checks whether this cookie is meant for this domain.
*
* @see http://tools.ietf.org/html/rfc6265#section-5.1.3
*
* @param string $domain
*
* @return bool
*/
public function matchDomain($domain)
{
// Domain is not set or exact match
if (!$this->hasDomain() || 0 === strcasecmp($domain, $this->domain)) {
return true;
}
// Domain is not an IP address
if (filter_var($domain, FILTER_VALIDATE_IP)) {
return false;
}
return (bool) preg_match(sprintf('/\b%s$/i', preg_quote($this->domain)), $domain);
}
/**
* Returns the path.
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* Sets the path.
*
* @param string|null $path
*
* @return Cookie
*/
public function withPath($path)
{
$new = clone $this;
$new->path = $this->normalizePath($path);
return $new;
}
/**
* Checks whether this cookie is meant for this path.
*
* @see http://tools.ietf.org/html/rfc6265#section-5.1.4
*
* @param string $path
*
* @return bool
*/
public function matchPath($path)
{
return $this->path === $path || (0 === strpos($path, rtrim($this->path, '/').'/'));
}
/**
* Checks whether this cookie may only be sent over HTTPS.
*
* @return bool
*/
public function isSecure()
{
return $this->secure;
}
/**
* Sets whether this cookie should only be sent over HTTPS.
*
* @param bool $secure
*
* @return Cookie
*/
public function withSecure($secure)
{
$new = clone $this;
$new->secure = (bool) $secure;
return $new;
}
/**
* Check whether this cookie may not be accessed through Javascript.
*
* @return bool
*/
public function isHttpOnly()
{
return $this->httpOnly;
}
/**
* Sets whether this cookie may not be accessed through Javascript.
*
* @param bool $httpOnly
*
* @return Cookie
*/
public function withHttpOnly($httpOnly)
{
$new = clone $this;
$new->httpOnly = (bool) $httpOnly;
return $new;
}
/**
* Checks if this cookie represents the same cookie as $cookie.
*
* This does not compare the values, only name, domain and path.
*
* @param Cookie $cookie
*
* @return bool
*/
public function match(self $cookie)
{
return $this->name === $cookie->name && $this->domain === $cookie->domain and $this->path === $cookie->path;
}
/**
* Validates cookie attributes.
*
* @return bool
*/
public function isValid()
{
try {
$this->validateName($this->name);
$this->validateValue($this->value);
$this->validateMaxAge($this->maxAge);
} catch (\InvalidArgumentException $e) {
return false;
}
return true;
}
/**
* Validates the name attribute.
*
* @see http://tools.ietf.org/search/rfc2616#section-2.2
*
* @param string $name
*
* @throws \InvalidArgumentException if the name is empty or contains invalid characters
*/
private function validateName($name)
{
if (strlen($name) < 1) {
throw new \InvalidArgumentException('The name cannot be empty');
}
// Name attribute is a token as per spec in RFC 2616
if (preg_match('/[\x00-\x20\x22\x28-\x29\x2C\x2F\x3A-\x40\x5B-\x5D\x7B\x7D\x7F]/', $name)) {
throw new \InvalidArgumentException(sprintf('The cookie name "%s" contains invalid characters.', $name));
}
}
/**
* Validates a value.
*
* @see http://tools.ietf.org/html/rfc6265#section-4.1.1
*
* @param string|null $value
*
* @throws \InvalidArgumentException if the value contains invalid characters
*/
private function validateValue($value)
{
if (isset($value)) {
if (preg_match('/[^\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]/', $value)) {
throw new \InvalidArgumentException(sprintf('The cookie value "%s" contains invalid characters.', $value));
}
}
}
/**
* Validates a Max-Age attribute.
*
* @param int|null $maxAge
*
* @throws \InvalidArgumentException if the Max-Age is not an empty or integer value
*/
private function validateMaxAge($maxAge)
{
if (isset($maxAge)) {
if (!is_int($maxAge)) {
throw new \InvalidArgumentException('Max-Age must be integer');
}
}
}
/**
* Remove the leading '.' and lowercase the domain as per spec in RFC 6265.
*
* @see http://tools.ietf.org/html/rfc6265#section-4.1.2.3
* @see http://tools.ietf.org/html/rfc6265#section-5.1.3
* @see http://tools.ietf.org/html/rfc6265#section-5.2.3
*
* @param string|null $domain
*
* @return string
*/
private function normalizeDomain($domain)
{
if (isset($domain)) {
$domain = ltrim(strtolower($domain), '.');
}
return $domain;
}
/**
* Processes path as per spec in RFC 6265.
*
* @see http://tools.ietf.org/html/rfc6265#section-5.1.4
* @see http://tools.ietf.org/html/rfc6265#section-5.2.4
*
* @param string|null $path
*
* @return string
*/
private function normalizePath($path)
{
$path = rtrim($path, '/');
if (empty($path) or '/' !== substr($path, 0, 1)) {
$path = '/';
}
return $path;
}
}

View File

@@ -0,0 +1,210 @@
<?php
namespace Http\Message;
/**
* Cookie Jar holds a set of Cookies.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
final class CookieJar implements \Countable, \IteratorAggregate
{
/**
* @var \SplObjectStorage
*/
protected $cookies;
public function __construct()
{
$this->cookies = new \SplObjectStorage();
}
/**
* Checks if there is a cookie.
*
* @return bool
*/
public function hasCookie(Cookie $cookie)
{
return $this->cookies->contains($cookie);
}
/**
* Adds a cookie.
*/
public function addCookie(Cookie $cookie)
{
if (!$this->hasCookie($cookie)) {
$cookies = $this->getMatchingCookies($cookie);
foreach ($cookies as $matchingCookie) {
if ($cookie->getValue() !== $matchingCookie->getValue() || $cookie->getMaxAge() > $matchingCookie->getMaxAge()) {
$this->removeCookie($matchingCookie);
continue;
}
}
if ($cookie->hasValue()) {
$this->cookies->attach($cookie);
}
}
}
/**
* Removes a cookie.
*/
public function removeCookie(Cookie $cookie)
{
$this->cookies->detach($cookie);
}
/**
* Returns the cookies.
*
* @return Cookie[]
*/
public function getCookies()
{
$match = function ($matchCookie) {
return true;
};
return $this->findMatchingCookies($match);
}
/**
* Returns all matching cookies.
*
* @return Cookie[]
*/
public function getMatchingCookies(Cookie $cookie)
{
$match = function ($matchCookie) use ($cookie) {
return $matchCookie->match($cookie);
};
return $this->findMatchingCookies($match);
}
/**
* Finds matching cookies based on a callable.
*
* @return Cookie[]
*/
protected function findMatchingCookies(callable $match)
{
$cookies = [];
foreach ($this->cookies as $cookie) {
if ($match($cookie)) {
$cookies[] = $cookie;
}
}
return $cookies;
}
/**
* Checks if there are cookies.
*
* @return bool
*/
public function hasCookies()
{
return $this->cookies->count() > 0;
}
/**
* Sets the cookies and removes any previous one.
*
* @param Cookie[] $cookies
*/
public function setCookies(array $cookies)
{
$this->clear();
$this->addCookies($cookies);
}
/**
* Adds some cookies.
*
* @param Cookie[] $cookies
*/
public function addCookies(array $cookies)
{
foreach ($cookies as $cookie) {
$this->addCookie($cookie);
}
}
/**
* Removes some cookies.
*
* @param Cookie[] $cookies
*/
public function removeCookies(array $cookies)
{
foreach ($cookies as $cookie) {
$this->removeCookie($cookie);
}
}
/**
* Removes cookies which match the given parameters.
*
* Null means that parameter should not be matched
*
* @param string|null $name
* @param string|null $domain
* @param string|null $path
*/
public function removeMatchingCookies($name = null, $domain = null, $path = null)
{
$match = function ($cookie) use ($name, $domain, $path) {
$match = true;
if (isset($name)) {
$match = $match && ($cookie->getName() === $name);
}
if (isset($domain)) {
$match = $match && $cookie->matchDomain($domain);
}
if (isset($path)) {
$match = $match && $cookie->matchPath($path);
}
return $match;
};
$cookies = $this->findMatchingCookies($match);
$this->removeCookies($cookies);
}
/**
* Removes all cookies.
*/
public function clear()
{
$this->cookies = new \SplObjectStorage();
}
/**
* {@inheritdoc}
*/
public function count()
{
return $this->cookies->count();
}
/**
* {@inheritdoc}
*/
public function getIterator()
{
return clone $this->cookies;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Http\Message;
use Http\Message\Exception\UnexpectedValueException;
final class CookieUtil
{
/**
* Handles dates as defined by RFC 2616 section 3.3.1, and also some other
* non-standard, but common formats.
*
* @var array
*/
private static $dateFormats = [
'D, d M y H:i:s T',
'D, d M Y H:i:s T',
'D, d-M-y H:i:s T',
'D, d-M-Y H:i:s T',
'D, d-m-y H:i:s T',
'D, d-m-Y H:i:s T',
'D M j G:i:s Y',
'D M d H:i:s Y T',
];
/**
* @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/BrowserKit/Cookie.php
*
* @param string $dateValue
*
* @return \DateTime
*
* @throws UnexpectedValueException if we cannot parse the cookie date string
*/
public static function parseDate($dateValue)
{
foreach (self::$dateFormats as $dateFormat) {
if (false !== $date = \DateTime::createFromFormat($dateFormat, $dateValue, new \DateTimeZone('GMT'))) {
return $date;
}
}
// attempt a fallback for unusual formatting
if (false !== $date = date_create($dateValue, new \DateTimeZone('GMT'))) {
return $date;
}
throw new UnexpectedValueException(sprintf(
'Unparseable cookie date string "%s"',
$dateValue
));
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace Http\Message\Decorator;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait MessageDecorator
{
/**
* @var MessageInterface
*/
private $message;
/**
* Returns the decorated message.
*
* Since the underlying Message is immutable as well
* exposing it is not an issue, because it's state cannot be altered
*
* @return MessageInterface
*/
public function getMessage()
{
return $this->message;
}
/**
* {@inheritdoc}
*/
public function getProtocolVersion()
{
return $this->message->getProtocolVersion();
}
/**
* {@inheritdoc}
*/
public function withProtocolVersion($version)
{
$new = clone $this;
$new->message = $this->message->withProtocolVersion($version);
return $new;
}
/**
* {@inheritdoc}
*/
public function getHeaders()
{
return $this->message->getHeaders();
}
/**
* {@inheritdoc}
*/
public function hasHeader($header)
{
return $this->message->hasHeader($header);
}
/**
* {@inheritdoc}
*/
public function getHeader($header)
{
return $this->message->getHeader($header);
}
/**
* {@inheritdoc}
*/
public function getHeaderLine($header)
{
return $this->message->getHeaderLine($header);
}
/**
* {@inheritdoc}
*/
public function withHeader($header, $value)
{
$new = clone $this;
$new->message = $this->message->withHeader($header, $value);
return $new;
}
/**
* {@inheritdoc}
*/
public function withAddedHeader($header, $value)
{
$new = clone $this;
$new->message = $this->message->withAddedHeader($header, $value);
return $new;
}
/**
* {@inheritdoc}
*/
public function withoutHeader($header)
{
$new = clone $this;
$new->message = $this->message->withoutHeader($header);
return $new;
}
/**
* {@inheritdoc}
*/
public function getBody()
{
return $this->message->getBody();
}
/**
* {@inheritdoc}
*/
public function withBody(StreamInterface $body)
{
$new = clone $this;
$new->message = $this->message->withBody($body);
return $new;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Http\Message\Decorator;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
/**
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait RequestDecorator
{
use MessageDecorator {
getMessage as getRequest;
}
/**
* Exchanges the underlying request with another.
*
* @return self
*/
public function withRequest(RequestInterface $request)
{
$new = clone $this;
$new->message = $request;
return $new;
}
/**
* {@inheritdoc}
*/
public function getRequestTarget()
{
return $this->message->getRequestTarget();
}
/**
* {@inheritdoc}
*/
public function withRequestTarget($requestTarget)
{
$new = clone $this;
$new->message = $this->message->withRequestTarget($requestTarget);
return $new;
}
/**
* {@inheritdoc}
*/
public function getMethod()
{
return $this->message->getMethod();
}
/**
* {@inheritdoc}
*/
public function withMethod($method)
{
$new = clone $this;
$new->message = $this->message->withMethod($method);
return $new;
}
/**
* {@inheritdoc}
*/
public function getUri()
{
return $this->message->getUri();
}
/**
* {@inheritdoc}
*/
public function withUri(UriInterface $uri, $preserveHost = false)
{
$new = clone $this;
$new->message = $this->message->withUri($uri, $preserveHost);
return $new;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Http\Message\Decorator;
use Psr\Http\Message\ResponseInterface;
/**
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait ResponseDecorator
{
use MessageDecorator {
getMessage as getResponse;
}
/**
* Exchanges the underlying response with another.
*
* @return self
*/
public function withResponse(ResponseInterface $response)
{
$new = clone $this;
$new->message = $response;
return $new;
}
/**
* {@inheritdoc}
*/
public function getStatusCode()
{
return $this->message->getStatusCode();
}
/**
* {@inheritdoc}
*/
public function withStatus($code, $reasonPhrase = '')
{
$new = clone $this;
$new->message = $this->message->withStatus($code, $reasonPhrase);
return $new;
}
/**
* {@inheritdoc}
*/
public function getReasonPhrase()
{
return $this->message->getReasonPhrase();
}
}

View File

@@ -0,0 +1,138 @@
<?php
namespace Http\Message\Decorator;
use Psr\Http\Message\StreamInterface;
/**
* Decorates a stream.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
trait StreamDecorator
{
/**
* @var StreamInterface
*/
protected $stream;
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->stream->__toString();
}
/**
* {@inheritdoc}
*/
public function close()
{
$this->stream->close();
}
/**
* {@inheritdoc}
*/
public function detach()
{
return $this->stream->detach();
}
/**
* {@inheritdoc}
*/
public function getSize()
{
return $this->stream->getSize();
}
/**
* {@inheritdoc}
*/
public function tell()
{
return $this->stream->tell();
}
/**
* {@inheritdoc}
*/
public function eof()
{
return $this->stream->eof();
}
/**
* {@inheritdoc}
*/
public function isSeekable()
{
return $this->stream->isSeekable();
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET)
{
$this->stream->seek($offset, $whence);
}
/**
* {@inheritdoc}
*/
public function rewind()
{
$this->stream->rewind();
}
/**
* {@inheritdoc}
*/
public function isWritable()
{
return $this->stream->isWritable();
}
/**
* {@inheritdoc}
*/
public function write($string)
{
return $this->stream->write($string);
}
/**
* {@inheritdoc}
*/
public function isReadable()
{
return $this->stream->isReadable();
}
/**
* {@inheritdoc}
*/
public function read($length)
{
return $this->stream->read($length);
}
/**
* {@inheritdoc}
*/
public function getContents()
{
return $this->stream->getContents();
}
/**
* {@inheritdoc}
*/
public function getMetadata($key = null)
{
return $this->stream->getMetadata($key);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Http\Message\Encoding;
/**
* Transform a regular stream into a chunked one.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class ChunkStream extends FilteredStream
{
/**
* {@inheritdoc}
*/
protected function readFilter()
{
return 'chunk';
}
/**
* {@inheritdoc}
*/
protected function writeFilter()
{
return 'dechunk';
}
/**
* {@inheritdoc}
*/
protected function fill()
{
parent::fill();
if ($this->stream->eof()) {
$this->buffer .= "0\r\n\r\n";
}
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream compress (RFC 1950).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class CompressStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => 15, 'level' => $level]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15]);
}
/**
* {@inheritdoc}
*/
protected function readFilter()
{
return 'zlib.deflate';
}
/**
* {@inheritdoc}
*/
protected function writeFilter()
{
return 'zlib.inflate';
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Http\Message\Encoding;
/**
* Decorate a stream which is chunked.
*
* Allow to decode a chunked stream
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class DechunkStream extends FilteredStream
{
/**
* {@inheritdoc}
*/
protected function readFilter()
{
return 'dechunk';
}
/**
* {@inheritdoc}
*/
protected function writeFilter()
{
return 'chunk';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream decompress (RFC 1950).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class DecompressStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => 15]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 15, 'level' => $level]);
}
/**
* {@inheritdoc}
*/
protected function readFilter()
{
return 'zlib.inflate';
}
/**
* {@inheritdoc}
*/
protected function writeFilter()
{
return 'zlib.deflate';
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream deflate (RFC 1951).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class DeflateStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
parent::__construct($stream, ['window' => -15, 'level' => $level]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15]);
}
/**
* {@inheritdoc}
*/
protected function readFilter()
{
return 'zlib.deflate';
}
/**
* {@inheritdoc}
*/
protected function writeFilter()
{
return 'zlib.inflate';
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Http\Message\Encoding\Filter;
/**
* Userland implementation of the chunk stream filter.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class Chunk extends \php_user_filter
{
/**
* {@inheritdoc}
*/
public function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$lenbucket = stream_bucket_new($this->stream, dechex($bucket->datalen)."\r\n");
stream_bucket_append($out, $lenbucket);
$consumed += $bucket->datalen;
stream_bucket_append($out, $bucket);
$lenbucket = stream_bucket_new($this->stream, "\r\n");
stream_bucket_append($out, $lenbucket);
}
return PSFS_PASS_ON;
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Http\Message\Decorator\StreamDecorator;
use Psr\Http\Message\StreamInterface;
/**
* A filtered stream has a filter for filtering output and a filter for filtering input made to a underlying stream.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
abstract class FilteredStream implements StreamInterface
{
const BUFFER_SIZE = 8192;
use StreamDecorator {
rewind as private doRewind;
seek as private doSeek;
}
/**
* @var callable
*/
protected $readFilterCallback;
/**
* @var resource
*
* @deprecated since version 1.5, will be removed in 2.0
*/
protected $readFilter;
/**
* @var callable
*
* @deprecated since version 1.5, will be removed in 2.0
*/
protected $writeFilterCallback;
/**
* @var resource
*
* @deprecated since version 1.5, will be removed in 2.0
*/
protected $writeFilter;
/**
* Internal buffer.
*
* @var string
*/
protected $buffer = '';
/**
* @param mixed|null $readFilterOptions
* @param mixed|null $writeFilterOptions deprecated since 1.5, will be removed in 2.0
*/
public function __construct(StreamInterface $stream, $readFilterOptions = null, $writeFilterOptions = null)
{
if (null !== $readFilterOptions) {
$this->readFilterCallback = Filter\fun($this->readFilter(), $readFilterOptions);
} else {
$this->readFilterCallback = Filter\fun($this->readFilter());
}
if (null !== $writeFilterOptions) {
$this->writeFilterCallback = Filter\fun($this->writeFilter(), $writeFilterOptions);
@trigger_error('The $writeFilterOptions argument is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED);
} else {
$this->writeFilterCallback = Filter\fun($this->writeFilter());
}
$this->stream = $stream;
}
/**
* {@inheritdoc}
*/
public function read($length)
{
if (strlen($this->buffer) >= $length) {
$read = substr($this->buffer, 0, $length);
$this->buffer = substr($this->buffer, $length);
return $read;
}
if ($this->stream->eof()) {
$buffer = $this->buffer;
$this->buffer = '';
return $buffer;
}
$read = $this->buffer;
$this->buffer = '';
$this->fill();
return $read.$this->read($length - strlen($read));
}
/**
* {@inheritdoc}
*/
public function eof()
{
return $this->stream->eof() && '' === $this->buffer;
}
/**
* Buffer is filled by reading underlying stream.
*
* Callback is reading once more even if the stream is ended.
* This allow to get last data in the PHP buffer otherwise this
* bug is present : https://bugs.php.net/bug.php?id=48725
*/
protected function fill()
{
$readFilterCallback = $this->readFilterCallback;
$this->buffer .= $readFilterCallback($this->stream->read(self::BUFFER_SIZE));
if ($this->stream->eof()) {
$this->buffer .= $readFilterCallback();
}
}
/**
* {@inheritdoc}
*/
public function getContents()
{
$buffer = '';
while (!$this->eof()) {
$buf = $this->read(self::BUFFER_SIZE);
// Using a loose equality here to match on '' and false.
if (null == $buf) {
break;
}
$buffer .= $buf;
}
return $buffer;
}
/**
* Always returns null because we can't tell the size of a stream when we filter.
*/
public function getSize()
{
return null;
}
/**
* {@inheritdoc}
*/
public function __toString()
{
return $this->getContents();
}
/**
* Filtered streams are not seekable.
*
* We would need to buffer and process everything to allow seeking.
*/
public function isSeekable()
{
return false;
}
/**
* {@inheritdoc}
*/
public function rewind()
{
@trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED);
$this->doRewind();
}
/**
* {@inheritdoc}
*/
public function seek($offset, $whence = SEEK_SET)
{
@trigger_error('Filtered streams are not seekable. This method will start raising an exception in the next major version', E_USER_DEPRECATED);
$this->doSeek($offset, $whence);
}
/**
* Returns the read filter name.
*
* @return string
*
* @deprecated since version 1.5, will be removed in 2.0
*/
public function getReadFilter()
{
@trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED);
return $this->readFilter();
}
/**
* Returns the write filter name.
*
* @return string
*/
abstract protected function readFilter();
/**
* Returns the write filter name.
*
* @return string
*
* @deprecated since version 1.5, will be removed in 2.0
*/
public function getWriteFilter()
{
@trigger_error('The '.__CLASS__.'::'.__METHOD__.' method is deprecated since version 1.5 and will be removed in 2.0.', E_USER_DEPRECATED);
return $this->writeFilter();
}
/**
* Returns the write filter name.
*
* @return string
*/
abstract protected function writeFilter();
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream for decoding from gzip format (RFC 1952).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class GzipDecodeStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => 31]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31, 'level' => $level]);
}
/**
* {@inheritdoc}
*/
protected function readFilter()
{
return 'zlib.inflate';
}
/**
* {@inheritdoc}
*/
protected function writeFilter()
{
return 'zlib.deflate';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream for encoding to gzip format (RFC 1952).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class GzipEncodeStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => 31, 'level' => $level]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => 31]);
}
/**
* {@inheritdoc}
*/
protected function readFilter()
{
return 'zlib.deflate';
}
/**
* {@inheritdoc}
*/
protected function writeFilter()
{
return 'zlib.inflate';
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Http\Message\Encoding;
use Clue\StreamFilter as Filter;
use Psr\Http\Message\StreamInterface;
/**
* Stream inflate (RFC 1951).
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
*/
class InflateStream extends FilteredStream
{
/**
* @param int $level
*/
public function __construct(StreamInterface $stream, $level = -1)
{
if (!extension_loaded('zlib')) {
throw new \RuntimeException('The zlib extension must be enabled to use this stream');
}
parent::__construct($stream, ['window' => -15]);
// @deprecated will be removed in 2.0
$this->writeFilterCallback = Filter\fun($this->writeFilter(), ['window' => -15, 'level' => $level]);
}
/**
* {@inheritdoc}
*/
protected function readFilter()
{
return 'zlib.inflate';
}
/**
* {@inheritdoc}
*/
protected function writeFilter()
{
return 'zlib.deflate';
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Http\Message;
/**
* An interface implemented by all HTTP message related exceptions.
*/
interface Exception
{
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Http\Message\Exception;
use Http\Message\Exception;
final class UnexpectedValueException extends \UnexpectedValueException implements Exception
{
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Http\Message;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Formats a request and/or a response as a string.
*
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
interface Formatter
{
/**
* Formats a request.
*
* @return string
*/
public function formatRequest(RequestInterface $request);
/**
* Formats a response.
*
* @return string
*/
public function formatResponse(ResponseInterface $response);
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Http\Message\Formatter;
use Http\Message\Formatter;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* A formatter that prints a cURL command for HTTP requests.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class CurlCommandFormatter implements Formatter
{
/**
* {@inheritdoc}
*/
public function formatRequest(RequestInterface $request)
{
$command = sprintf('curl %s', escapeshellarg((string) $request->getUri()->withFragment('')));
if ('1.0' === $request->getProtocolVersion()) {
$command .= ' --http1.0';
} elseif ('2.0' === $request->getProtocolVersion()) {
$command .= ' --http2';
}
$method = strtoupper($request->getMethod());
if ('HEAD' === $method) {
$command .= ' --head';
} elseif ('GET' !== $method) {
$command .= ' --request '.$method;
}
$command .= $this->getHeadersAsCommandOptions($request);
$body = $request->getBody();
if ($body->getSize() > 0) {
// escapeshellarg argument max length on Windows, but longer body in curl command would be impractical anyways
if ($body->getSize() > 8192) {
$data = '[too long stream omitted]';
} elseif ($body->isSeekable()) {
$data = $body->__toString();
$body->rewind();
// all non-printable ASCII characters and <DEL> except for \t, \r, \n
if (preg_match('/([\x00-\x09\x0C\x0E-\x1F\x7F])/', $data)) {
$data = '[binary stream omitted]';
}
} else {
$data = '[non-seekable stream omitted]';
}
$escapedData = @escapeshellarg($data);
if (empty($escapedData)) {
$escapedData = 'We couldn\'t not escape the data properly';
}
$command .= sprintf(' --data %s', $escapedData);
}
return $command;
}
/**
* {@inheritdoc}
*/
public function formatResponse(ResponseInterface $response)
{
return '';
}
/**
* @return string
*/
private function getHeadersAsCommandOptions(RequestInterface $request)
{
$command = '';
foreach ($request->getHeaders() as $name => $values) {
if ('host' === strtolower($name) && $values[0] === $request->getUri()->getHost()) {
continue;
}
if ('user-agent' === strtolower($name)) {
$command .= sprintf(' -A %s', escapeshellarg($values[0]));
continue;
}
$command .= sprintf(' -H %s', escapeshellarg($name.': '.$request->getHeaderLine($name)));
}
return $command;
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Http\Message\Formatter;
use Http\Message\Formatter;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* A formatter that prints the complete HTTP message.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class FullHttpMessageFormatter implements Formatter
{
/**
* The maximum length of the body.
*
* @var int|null
*/
private $maxBodyLength;
/**
* @param int|null $maxBodyLength
*/
public function __construct($maxBodyLength = 1000)
{
$this->maxBodyLength = $maxBodyLength;
}
/**
* {@inheritdoc}
*/
public function formatRequest(RequestInterface $request)
{
$message = sprintf(
"%s %s HTTP/%s\n",
$request->getMethod(),
$request->getRequestTarget(),
$request->getProtocolVersion()
);
foreach ($request->getHeaders() as $name => $values) {
$message .= $name.': '.implode(', ', $values)."\n";
}
return $this->addBody($request, $message);
}
/**
* {@inheritdoc}
*/
public function formatResponse(ResponseInterface $response)
{
$message = sprintf(
"HTTP/%s %s %s\n",
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
foreach ($response->getHeaders() as $name => $values) {
$message .= $name.': '.implode(', ', $values)."\n";
}
return $this->addBody($response, $message);
}
/**
* Add the message body if the stream is seekable.
*
* @param string $message
*
* @return string
*/
private function addBody(MessageInterface $request, $message)
{
$message .= "\n";
$stream = $request->getBody();
if (!$stream->isSeekable() || 0 === $this->maxBodyLength) {
// Do not read the stream
return $message;
}
$data = $stream->__toString();
$stream->rewind();
// all non-printable ASCII characters and <DEL> except for \t, \r, \n
if (preg_match('/([\x00-\x09\x0C\x0E-\x1F\x7F])/', $data)) {
return $message.'[binary stream omitted]';
}
if (null === $this->maxBodyLength) {
return $message.$data;
}
return $message.mb_substr($data, 0, $this->maxBodyLength);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Http\Message\Formatter;
use Http\Message\Formatter;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Normalize a request or a response into a string or an array.
*
* @author Joel Wurtz <joel.wurtz@gmail.com>
* @author Márk Sági-Kazár <mark.sagikazar@gmail.com>
*/
class SimpleFormatter implements Formatter
{
/**
* {@inheritdoc}
*/
public function formatRequest(RequestInterface $request)
{
return sprintf(
'%s %s %s',
$request->getMethod(),
$request->getUri()->__toString(),
$request->getProtocolVersion()
);
}
/**
* {@inheritdoc}
*/
public function formatResponse(ResponseInterface $response)
{
return sprintf(
'%s %s %s',
$response->getStatusCode(),
$response->getReasonPhrase(),
$response->getProtocolVersion()
);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Http\Message\MessageFactory;
use Http\Message\MessageFactory;
use Http\Message\StreamFactory\DiactorosStreamFactory;
use Zend\Diactoros\Request;
use Zend\Diactoros\Response;
/**
* Creates Diactoros messages.
*
* @author GeLo <geloen.eric@gmail.com>
*
* @deprecated This will be removed in php-http/message2.0. Consider using the official Diactoros PSR-17 factory
*/
final class DiactorosMessageFactory implements MessageFactory
{
/**
* @var DiactorosStreamFactory
*/
private $streamFactory;
public function __construct()
{
$this->streamFactory = new DiactorosStreamFactory();
}
/**
* {@inheritdoc}
*/
public function createRequest(
$method,
$uri,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
return (new Request(
$uri,
$method,
$this->streamFactory->createStream($body),
$headers
))->withProtocolVersion($protocolVersion);
}
/**
* {@inheritdoc}
*/
public function createResponse(
$statusCode = 200,
$reasonPhrase = null,
array $headers = [],
$body = null,
$protocolVersion = '1.1'
) {
return (new Response(
$this->streamFactory->createStream($body),
$statusCode,
$headers
))->withProtocolVersion($protocolVersion);
}
}

Some files were not shown because too many files have changed in this diff Show More