sabre/http

The sabre/http library provides utilities for dealing with http requests and responses.

7.0.3 2023-08-17 13:10 UTC

README

This library provides a toolkit to make working with the HTTP protocol easier.

Most PHP scripts run within an 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

Build Status

release minimum PHP version
master PHP 7.4
7.0 PHP 7.4
6.0 PHP 7.4
5.1 PHP 7.1
4.2 PHP 5.4
3.0 PHP 5.4

Installation

Make sure you have composer installed. In your project directory, create, or edit a composer.json file, and make sure it contains something like this:

{
    "require" : {
        "sabre/http" : "~5.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 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 the scope of just sabre/dav.

Although completely independently developed, this library has a LOT of overlap with Symfony's HttpFoundation.

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.

Getting started

First and foremost, this library wraps the superglobals. The easiest way to instantiate a request object is as follows:

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 its interface:

function handleRequest(HTTP\RequestInterface $request) {

    // Do something with this request :)

}

A response object you can just create as such:

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:

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 an instance of Request so you would have to override the factory method as well,
  3. By controlling the instantiation 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 to add new behavior where you need it. sabre/http provides helper classes to quickly do this.

Example:

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 unit test.

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:

$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, 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, but it provides a simple and lightweight API for making the occasional API call.

Usage

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 events using sabre/event. beforeRequest, afterRequest and error.

$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 an 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, but this provides a much nicer API to handle this.

Sample usage:

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.

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 your $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 APIs

Request

/**
 * 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 encoded as
 * ISO-8859-1, it will convert it to UTF-8.
 *
 * If the path is outside 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 its 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 its 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 an HTTP header.
 *
 * The case-sensitivity 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 an HTTP header.
 *
 * The specified header name must be treated as case-insensitive.
 * 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, 1.1 or 2.0.
 *
 * @param string $version
 * @return void
 */
function setHttpVersion($version);

/**
 * Returns the HTTP version.
 *
 * @return string
 */
function getHttpVersion();

Response

/**
 * 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 its 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 its 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 an HTTP header.
 *
 * The case-sensitivity 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 an HTTP header.
 *
 * The specified header name must be treated as case-insensitive.
 * 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, 1.1 or 2.0.
 *
 * @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. Drop us a line for commercial services or enterprise support.