You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

747 lines
16 KiB

5 years ago
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