mirror of
https://github.com/OpenXE-org/OpenXE.git
synced 2024-12-27 07:00:29 +01:00
747 lines
16 KiB
Markdown
747 lines
16 KiB
Markdown
|
sabre/http
|
||
|
==========
|
||
|
|
||
|
This library provides a toolkit to make working with the HTTP protocol easier.
|
||
|
|
||
|
Most PHP scripts run within a HTTP request but accessing information about the
|
||
|
HTTP request is cumbersome at least.
|
||
|
|
||
|
There's bad practices, inconsistencies and confusion. This library is
|
||
|
effectively a wrapper around the following PHP constructs:
|
||
|
|
||
|
For Input:
|
||
|
|
||
|
* `$_GET`,
|
||
|
* `$_POST`,
|
||
|
* `$_SERVER`,
|
||
|
* `php://input` or `$HTTP_RAW_POST_DATA`.
|
||
|
|
||
|
For output:
|
||
|
|
||
|
* `php://output` or `echo`,
|
||
|
* `header()`.
|
||
|
|
||
|
What this library provides, is a `Request` object, and a `Response` object.
|
||
|
|
||
|
The objects are extendable and easily mockable.
|
||
|
|
||
|
Build status
|
||
|
------------
|
||
|
|
||
|
| branch | status |
|
||
|
| ------ | ------ |
|
||
|
| master | [![Build Status](https://travis-ci.org/fruux/sabre-http.svg?branch=master)](https://travis-ci.org/fruux/sabre-http) |
|
||
|
| 3.0 | [![Build Status](https://travis-ci.org/fruux/sabre-http.svg?branch=3.0)](https://travis-ci.org/fruux/sabre-http) |
|
||
|
|
||
|
Installation
|
||
|
------------
|
||
|
|
||
|
Make sure you have [composer][1] installed. In your project directory, create,
|
||
|
or edit a `composer.json` file, and make sure it contains something like this:
|
||
|
|
||
|
```json
|
||
|
{
|
||
|
"require" : {
|
||
|
"sabre/http" : "~3.0.0"
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
After that, just hit `composer install` and you should be rolling.
|
||
|
|
||
|
Quick history
|
||
|
-------------
|
||
|
|
||
|
This library came to existence in 2009, as a part of the [`sabre/dav`][2]
|
||
|
project, which uses it heavily.
|
||
|
|
||
|
It got split off into a separate library to make it easier to manage
|
||
|
releases and hopefully giving it use outside of the scope of just `sabre/dav`.
|
||
|
|
||
|
Although completely independently developed, this library has a LOT of
|
||
|
overlap with [Symfony's `HttpFoundation`][3].
|
||
|
|
||
|
Said library does a lot more stuff and is significantly more popular,
|
||
|
so if you are looking for something to fulfill this particular requirement,
|
||
|
I'd recommend also considering [`HttpFoundation`][3].
|
||
|
|
||
|
|
||
|
Getting started
|
||
|
---------------
|
||
|
|
||
|
First and foremost, this library wraps the superglobals. The easiest way to
|
||
|
instantiate a request object is as follows:
|
||
|
|
||
|
```php
|
||
|
use Sabre\HTTP;
|
||
|
|
||
|
include 'vendor/autoload.php';
|
||
|
|
||
|
$request = HTTP\Sapi::getRequest();
|
||
|
```
|
||
|
|
||
|
This line should only happen once in your entire application. Everywhere else
|
||
|
you should pass this request object around using dependency injection.
|
||
|
|
||
|
You should always typehint on it's interface:
|
||
|
|
||
|
```php
|
||
|
function handleRequest(HTTP\RequestInterface $request) {
|
||
|
|
||
|
// Do something with this request :)
|
||
|
|
||
|
}
|
||
|
```
|
||
|
|
||
|
A response object you can just create as such:
|
||
|
|
||
|
```php
|
||
|
use Sabre\HTTP;
|
||
|
|
||
|
include 'vendor/autoload.php';
|
||
|
|
||
|
$response = new HTTP\Response();
|
||
|
$response->setStatus(201); // created !
|
||
|
$response->setHeader('X-Foo', 'bar');
|
||
|
$response->setBody(
|
||
|
'success!'
|
||
|
);
|
||
|
|
||
|
```
|
||
|
|
||
|
After you fully constructed your response, you must call:
|
||
|
|
||
|
```php
|
||
|
HTTP\Sapi::sendResponse($response);
|
||
|
```
|
||
|
|
||
|
This line should generally also appear once in your application (at the very
|
||
|
end).
|
||
|
|
||
|
Decorators
|
||
|
----------
|
||
|
|
||
|
It may be useful to extend the `Request` and `Response` objects in your
|
||
|
application, if you for example would like them to carry a bit more
|
||
|
information about the current request.
|
||
|
|
||
|
For instance, you may want to add an `isLoggedIn` method to the Request
|
||
|
object.
|
||
|
|
||
|
Simply extending Request and Response may pose some problems:
|
||
|
|
||
|
1. You may want to extend the objects with new behaviors differently, in
|
||
|
different subsystems of your application,
|
||
|
2. The `Sapi::getRequest` factory always returns a instance of
|
||
|
`Request` so you would have to override the factory method as well,
|
||
|
3. By controlling the instantation and depend on specific `Request` and
|
||
|
`Response` instances in your library or application, you make it harder to
|
||
|
work with other applications which also use `sabre/http`.
|
||
|
|
||
|
In short: it would be bad design. Instead, it's recommended to use the
|
||
|
[decorator pattern][6] to add new behavior where you need it. `sabre/http`
|
||
|
provides helper classes to quickly do this.
|
||
|
|
||
|
Example:
|
||
|
|
||
|
```php
|
||
|
use Sabre\HTTP;
|
||
|
|
||
|
class MyRequest extends HTTP\RequestDecorator {
|
||
|
|
||
|
function isLoggedIn() {
|
||
|
|
||
|
return true;
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Our application assumes that the true `Request` object was instantiated
|
||
|
somewhere else, by some other subsystem. This could simply be a call like
|
||
|
`$request = Sapi::getRequest()` at the top of your application,
|
||
|
but could also be somewhere in a unittest.
|
||
|
|
||
|
All we know in the current subsystem, is that we received a `$request` and
|
||
|
that it implements `Sabre\HTTP\RequestInterface`. To decorate this object,
|
||
|
all we need to do is:
|
||
|
|
||
|
```php
|
||
|
$request = new MyRequest($request);
|
||
|
```
|
||
|
|
||
|
And that's it, we now have an `isLoggedIn` method, without having to mess
|
||
|
with the core instances.
|
||
|
|
||
|
|
||
|
Client
|
||
|
------
|
||
|
|
||
|
This package also contains a simple wrapper around [cURL][4], which will allow
|
||
|
you to write simple clients, using the `Request` and `Response` objects you're
|
||
|
already familiar with.
|
||
|
|
||
|
It's by no means a replacement for something like [Guzzle][7], but it provides
|
||
|
a simple and lightweight API for making the occasional API call.
|
||
|
|
||
|
### Usage
|
||
|
|
||
|
```php
|
||
|
use Sabre\HTTP;
|
||
|
|
||
|
$request = new HTTP\Request('GET', 'http://example.org/');
|
||
|
$request->setHeader('X-Foo', 'Bar');
|
||
|
|
||
|
$client = new HTTP\Client();
|
||
|
$response = $client->send($request);
|
||
|
|
||
|
echo $response->getBodyAsString();
|
||
|
```
|
||
|
|
||
|
The client emits 3 event using [`sabre/event`][5]. `beforeRequest`,
|
||
|
`afterRequest` and `error`.
|
||
|
|
||
|
```php
|
||
|
$client = new HTTP\Client();
|
||
|
$client->on('beforeRequest', function($request) {
|
||
|
|
||
|
// You could use beforeRequest to for example inject a few extra headers.
|
||
|
// into the Request object.
|
||
|
|
||
|
});
|
||
|
|
||
|
$client->on('afterRequest', function($request, $response) {
|
||
|
|
||
|
// The afterRequest event could be a good time to do some logging, or
|
||
|
// do some rewriting in the response.
|
||
|
|
||
|
});
|
||
|
|
||
|
$client->on('error', function($request, $response, &$retry, $retryCount) {
|
||
|
|
||
|
// The error event is triggered for every response with a HTTP code higher
|
||
|
// than 399.
|
||
|
|
||
|
});
|
||
|
|
||
|
$client->on('error:401', function($request, $response, &$retry, $retryCount) {
|
||
|
|
||
|
// You can also listen for specific error codes. This example shows how
|
||
|
// to inject HTTP authentication headers if a 401 was returned.
|
||
|
|
||
|
if ($retryCount > 1) {
|
||
|
// We're only going to retry exactly once.
|
||
|
}
|
||
|
|
||
|
$request->setHeader('Authorization', 'Basic xxxxxxxxxx');
|
||
|
$retry = true;
|
||
|
|
||
|
});
|
||
|
```
|
||
|
|
||
|
### Asynchronous requests
|
||
|
|
||
|
The `Client` also supports doing asynchronous requests. This is especially handy
|
||
|
if you need to perform a number of requests, that are allowed to be executed
|
||
|
in parallel.
|
||
|
|
||
|
The underlying system for this is simply [cURL's multi request handler][8],
|
||
|
but this provides a much nicer API to handle this.
|
||
|
|
||
|
Sample usage:
|
||
|
|
||
|
```php
|
||
|
|
||
|
use Sabre\HTTP;
|
||
|
|
||
|
$request = new Request('GET', 'http://localhost/');
|
||
|
$client = new Client();
|
||
|
|
||
|
// Executing 1000 requests
|
||
|
for ($i = 0; $i < 1000; $i++) {
|
||
|
$client->sendAsync(
|
||
|
$request,
|
||
|
function(ResponseInterface $response) {
|
||
|
// Success handler
|
||
|
},
|
||
|
function($error) {
|
||
|
// Error handler
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
|
||
|
// Wait for all requests to get a result.
|
||
|
$client->wait();
|
||
|
|
||
|
```
|
||
|
|
||
|
Check out `examples/asyncclient.php` for more information.
|
||
|
|
||
|
Writing a reverse proxy
|
||
|
-----------------------
|
||
|
|
||
|
With all these tools combined, it becomes very easy to write a simple reverse
|
||
|
http proxy.
|
||
|
|
||
|
```php
|
||
|
use
|
||
|
Sabre\HTTP\Sapi,
|
||
|
Sabre\HTTP\Client;
|
||
|
|
||
|
// The url we're proxying to.
|
||
|
$remoteUrl = 'http://example.org/';
|
||
|
|
||
|
// The url we're proxying from. Please note that this must be a relative url,
|
||
|
// and basically acts as the base url.
|
||
|
//
|
||
|
// If youre $remoteUrl doesn't end with a slash, this one probably shouldn't
|
||
|
// either.
|
||
|
$myBaseUrl = '/reverseproxy.php';
|
||
|
// $myBaseUrl = '/~evert/sabre/http/examples/reverseproxy.php/';
|
||
|
|
||
|
$request = Sapi::getRequest();
|
||
|
$request->setBaseUrl($myBaseUrl);
|
||
|
|
||
|
$subRequest = clone $request;
|
||
|
|
||
|
// Removing the Host header.
|
||
|
$subRequest->removeHeader('Host');
|
||
|
|
||
|
// Rewriting the url.
|
||
|
$subRequest->setUrl($remoteUrl . $request->getPath());
|
||
|
|
||
|
$client = new Client();
|
||
|
|
||
|
// Sends the HTTP request to the server
|
||
|
$response = $client->send($subRequest);
|
||
|
|
||
|
// Sends the response back to the client that connected to the proxy.
|
||
|
Sapi::sendResponse($response);
|
||
|
```
|
||
|
|
||
|
The Request and Response API's
|
||
|
------------------------------
|
||
|
|
||
|
### Request
|
||
|
|
||
|
```php
|
||
|
|
||
|
/**
|
||
|
* Creates the request object
|
||
|
*
|
||
|
* @param string $method
|
||
|
* @param string $url
|
||
|
* @param array $headers
|
||
|
* @param resource $body
|
||
|
*/
|
||
|
public function __construct($method = null, $url = null, array $headers = null, $body = null);
|
||
|
|
||
|
/**
|
||
|
* Returns the current HTTP method
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getMethod();
|
||
|
|
||
|
/**
|
||
|
* Sets the HTTP method
|
||
|
*
|
||
|
* @param string $method
|
||
|
* @return void
|
||
|
*/
|
||
|
function setMethod($method);
|
||
|
|
||
|
/**
|
||
|
* Returns the request url.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getUrl();
|
||
|
|
||
|
/**
|
||
|
* Sets the request url.
|
||
|
*
|
||
|
* @param string $url
|
||
|
* @return void
|
||
|
*/
|
||
|
function setUrl($url);
|
||
|
|
||
|
/**
|
||
|
* Returns the absolute url.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getAbsoluteUrl();
|
||
|
|
||
|
/**
|
||
|
* Sets the absolute url.
|
||
|
*
|
||
|
* @param string $url
|
||
|
* @return void
|
||
|
*/
|
||
|
function setAbsoluteUrl($url);
|
||
|
|
||
|
/**
|
||
|
* Returns the current base url.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getBaseUrl();
|
||
|
|
||
|
/**
|
||
|
* Sets a base url.
|
||
|
*
|
||
|
* This url is used for relative path calculations.
|
||
|
*
|
||
|
* The base url should default to /
|
||
|
*
|
||
|
* @param string $url
|
||
|
* @return void
|
||
|
*/
|
||
|
function setBaseUrl($url);
|
||
|
|
||
|
/**
|
||
|
* Returns the relative path.
|
||
|
*
|
||
|
* This is being calculated using the base url. This path will not start
|
||
|
* with a slash, so it will always return something like
|
||
|
* 'example/path.html'.
|
||
|
*
|
||
|
* If the full path is equal to the base url, this method will return an
|
||
|
* empty string.
|
||
|
*
|
||
|
* This method will also urldecode the path, and if the url was incoded as
|
||
|
* ISO-8859-1, it will convert it to UTF-8.
|
||
|
*
|
||
|
* If the path is outside of the base url, a LogicException will be thrown.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getPath();
|
||
|
|
||
|
/**
|
||
|
* Returns the list of query parameters.
|
||
|
*
|
||
|
* This is equivalent to PHP's $_GET superglobal.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
function getQueryParameters();
|
||
|
|
||
|
/**
|
||
|
* Returns the POST data.
|
||
|
*
|
||
|
* This is equivalent to PHP's $_POST superglobal.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
function getPostData();
|
||
|
|
||
|
/**
|
||
|
* Sets the post data.
|
||
|
*
|
||
|
* This is equivalent to PHP's $_POST superglobal.
|
||
|
*
|
||
|
* This would not have been needed, if POST data was accessible as
|
||
|
* php://input, but unfortunately we need to special case it.
|
||
|
*
|
||
|
* @param array $postData
|
||
|
* @return void
|
||
|
*/
|
||
|
function setPostData(array $postData);
|
||
|
|
||
|
/**
|
||
|
* Returns an item from the _SERVER array.
|
||
|
*
|
||
|
* If the value does not exist in the array, null is returned.
|
||
|
*
|
||
|
* @param string $valueName
|
||
|
* @return string|null
|
||
|
*/
|
||
|
function getRawServerValue($valueName);
|
||
|
|
||
|
/**
|
||
|
* Sets the _SERVER array.
|
||
|
*
|
||
|
* @param array $data
|
||
|
* @return void
|
||
|
*/
|
||
|
function setRawServerData(array $data);
|
||
|
|
||
|
/**
|
||
|
* Returns the body as a readable stream resource.
|
||
|
*
|
||
|
* Note that the stream may not be rewindable, and therefore may only be
|
||
|
* read once.
|
||
|
*
|
||
|
* @return resource
|
||
|
*/
|
||
|
function getBodyAsStream();
|
||
|
|
||
|
/**
|
||
|
* Returns the body as a string.
|
||
|
*
|
||
|
* Note that because the underlying data may be based on a stream, this
|
||
|
* method could only work correctly the first time.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getBodyAsString();
|
||
|
|
||
|
/**
|
||
|
* Returns the message body, as it's internal representation.
|
||
|
*
|
||
|
* This could be either a string or a stream.
|
||
|
*
|
||
|
* @return resource|string
|
||
|
*/
|
||
|
function getBody();
|
||
|
|
||
|
/**
|
||
|
* Updates the body resource with a new stream.
|
||
|
*
|
||
|
* @param resource $body
|
||
|
* @return void
|
||
|
*/
|
||
|
function setBody($body);
|
||
|
|
||
|
/**
|
||
|
* Returns all the HTTP headers as an array.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
function getHeaders();
|
||
|
|
||
|
/**
|
||
|
* Returns a specific HTTP header, based on it's name.
|
||
|
*
|
||
|
* The name must be treated as case-insensitive.
|
||
|
*
|
||
|
* If the header does not exist, this method must return null.
|
||
|
*
|
||
|
* @param string $name
|
||
|
* @return string|null
|
||
|
*/
|
||
|
function getHeader($name);
|
||
|
|
||
|
/**
|
||
|
* Updates a HTTP header.
|
||
|
*
|
||
|
* The case-sensitity of the name value must be retained as-is.
|
||
|
*
|
||
|
* @param string $name
|
||
|
* @param string $value
|
||
|
* @return void
|
||
|
*/
|
||
|
function setHeader($name, $value);
|
||
|
|
||
|
/**
|
||
|
* Resets HTTP headers
|
||
|
*
|
||
|
* This method overwrites all existing HTTP headers
|
||
|
*
|
||
|
* @param array $headers
|
||
|
* @return void
|
||
|
*/
|
||
|
function setHeaders(array $headers);
|
||
|
|
||
|
/**
|
||
|
* Adds a new set of HTTP headers.
|
||
|
*
|
||
|
* Any header specified in the array that already exists will be
|
||
|
* overwritten, but any other existing headers will be retained.
|
||
|
*
|
||
|
* @param array $headers
|
||
|
* @return void
|
||
|
*/
|
||
|
function addHeaders(array $headers);
|
||
|
|
||
|
/**
|
||
|
* Removes a HTTP header.
|
||
|
*
|
||
|
* The specified header name must be treated as case-insenstive.
|
||
|
* This method should return true if the header was successfully deleted,
|
||
|
* and false if the header did not exist.
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
function removeHeader($name);
|
||
|
|
||
|
/**
|
||
|
* Sets the HTTP version.
|
||
|
*
|
||
|
* Should be 1.0 or 1.1.
|
||
|
*
|
||
|
* @param string $version
|
||
|
* @return void
|
||
|
*/
|
||
|
function setHttpVersion($version);
|
||
|
|
||
|
/**
|
||
|
* Returns the HTTP version.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getHttpVersion();
|
||
|
```
|
||
|
|
||
|
### Response
|
||
|
|
||
|
```php
|
||
|
/**
|
||
|
* Returns the current HTTP status.
|
||
|
*
|
||
|
* This is the status-code as well as the human readable string.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getStatus();
|
||
|
|
||
|
/**
|
||
|
* Sets the HTTP status code.
|
||
|
*
|
||
|
* This can be either the full HTTP status code with human readable string,
|
||
|
* for example: "403 I can't let you do that, Dave".
|
||
|
*
|
||
|
* Or just the code, in which case the appropriate default message will be
|
||
|
* added.
|
||
|
*
|
||
|
* @param string|int $status
|
||
|
* @throws \InvalidArgumentExeption
|
||
|
* @return void
|
||
|
*/
|
||
|
function setStatus($status);
|
||
|
|
||
|
/**
|
||
|
* Returns the body as a readable stream resource.
|
||
|
*
|
||
|
* Note that the stream may not be rewindable, and therefore may only be
|
||
|
* read once.
|
||
|
*
|
||
|
* @return resource
|
||
|
*/
|
||
|
function getBodyAsStream();
|
||
|
|
||
|
/**
|
||
|
* Returns the body as a string.
|
||
|
*
|
||
|
* Note that because the underlying data may be based on a stream, this
|
||
|
* method could only work correctly the first time.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getBodyAsString();
|
||
|
|
||
|
/**
|
||
|
* Returns the message body, as it's internal representation.
|
||
|
*
|
||
|
* This could be either a string or a stream.
|
||
|
*
|
||
|
* @return resource|string
|
||
|
*/
|
||
|
function getBody();
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Updates the body resource with a new stream.
|
||
|
*
|
||
|
* @param resource $body
|
||
|
* @return void
|
||
|
*/
|
||
|
function setBody($body);
|
||
|
|
||
|
/**
|
||
|
* Returns all the HTTP headers as an array.
|
||
|
*
|
||
|
* @return array
|
||
|
*/
|
||
|
function getHeaders();
|
||
|
|
||
|
/**
|
||
|
* Returns a specific HTTP header, based on it's name.
|
||
|
*
|
||
|
* The name must be treated as case-insensitive.
|
||
|
*
|
||
|
* If the header does not exist, this method must return null.
|
||
|
*
|
||
|
* @param string $name
|
||
|
* @return string|null
|
||
|
*/
|
||
|
function getHeader($name);
|
||
|
|
||
|
/**
|
||
|
* Updates a HTTP header.
|
||
|
*
|
||
|
* The case-sensitity of the name value must be retained as-is.
|
||
|
*
|
||
|
* @param string $name
|
||
|
* @param string $value
|
||
|
* @return void
|
||
|
*/
|
||
|
function setHeader($name, $value);
|
||
|
|
||
|
/**
|
||
|
* Resets HTTP headers
|
||
|
*
|
||
|
* This method overwrites all existing HTTP headers
|
||
|
*
|
||
|
* @param array $headers
|
||
|
* @return void
|
||
|
*/
|
||
|
function setHeaders(array $headers);
|
||
|
|
||
|
/**
|
||
|
* Adds a new set of HTTP headers.
|
||
|
*
|
||
|
* Any header specified in the array that already exists will be
|
||
|
* overwritten, but any other existing headers will be retained.
|
||
|
*
|
||
|
* @param array $headers
|
||
|
* @return void
|
||
|
*/
|
||
|
function addHeaders(array $headers);
|
||
|
|
||
|
/**
|
||
|
* Removes a HTTP header.
|
||
|
*
|
||
|
* The specified header name must be treated as case-insenstive.
|
||
|
* This method should return true if the header was successfully deleted,
|
||
|
* and false if the header did not exist.
|
||
|
*
|
||
|
* @return bool
|
||
|
*/
|
||
|
function removeHeader($name);
|
||
|
|
||
|
/**
|
||
|
* Sets the HTTP version.
|
||
|
*
|
||
|
* Should be 1.0 or 1.1.
|
||
|
*
|
||
|
* @param string $version
|
||
|
* @return void
|
||
|
*/
|
||
|
function setHttpVersion($version);
|
||
|
|
||
|
/**
|
||
|
* Returns the HTTP version.
|
||
|
*
|
||
|
* @return string
|
||
|
*/
|
||
|
function getHttpVersion();
|
||
|
```
|
||
|
|
||
|
Made at fruux
|
||
|
-------------
|
||
|
|
||
|
This library is being developed by [fruux](https://fruux.com/). Drop us a line for commercial services or enterprise support.
|
||
|
|
||
|
[1]: http://getcomposer.org/
|
||
|
[2]: http://sabre.io/
|
||
|
[3]: https://github.com/symfony/HttpFoundation
|
||
|
[4]: http://php.net/curl
|
||
|
[5]: https://github.com/fruux/sabre-event
|
||
|
[6]: http://en.wikipedia.org/wiki/Decorator_pattern
|
||
|
[7]: http://guzzlephp.org/
|
||
|
[8]: http://php.net/curl_multi_init
|