HTTP Requests

Basics

Requests are HTTP messages sent by clients to servers. They contain data like the URI that was requested, the HTTP method (eg GET), the headers, and the body (if one was present).

use Aphiria\Net\Http\Request;
use Aphiria\Net\Http\StringBody;
use Aphiria\Net\Uri;

$request = new Request('GET', new Uri('https://example.com'));

// Grab the HTTP method, eg "GET"
$method = $request->getMethod();

// Grab the URI:
$uri = $request->getUri();

// Grab the protocol version, eg 1.1:
$protocolVersion = $request->getProtocolVersion();

// Grab the headers (you can add new headers to the returned hash table)
$headers = $request->getHeaders();

// Set a header
$request->getHeaders()->add('Foo', 'bar');

// Get the body
$body = $request->getBody(); // Could be null

// Set the body
$request->setBody(new StringBody('foo'));

// Grab any special metadata properties (a custom hash table that you can define)
$properties = $request->getProperties();

Creating Requests

Manually creating a request is easy:

use Aphiria\Net\Http\Headers;
use Aphiria\Net\Http\Request;
use Aphiria\Net\Http\StringBody;
use Aphiria\Net\Uri;

$request = new Request(
    'GET',
    new Uri('https://example.com'),
    new Headers(),
    new StringBody('foo')
);

Creating Requests From Superglobals

PHP has superglobal arrays that store information about the requests. They're a mess, architecturally-speaking. Aphiria attempts to insulate developers from the nastiness of superglobals by giving you a simple method to create requests and responses. To create a request, use RequestFactory:

use Aphiria\Net\Http\RequestFactory;

$request = (new RequestFactory)->createRequestFromSuperglobals($_SERVER);

Aphiria reads all the information it needs from the $_SERVER superglobal - it doesn't need the others.

Request Builders

Aphiria comes with a fluent syntax for building your requests, which is somewhat similar to PSR-7. Let's look at a simple example:

use Aphiria\Net\Http\RequestBuilder;

$request = (new RequestBuilder())->withMethod('GET')
    ->withUri('http://example.com')
    ->withHeader('Cookie', 'foo=bar')
    ->build();

You can specify a body of a request:

use Aphiria\Net\Http\StringBody;

$request = (new RequestBuilder())->withMethod('POST')
    ->withUri('http://example.com/users')
    ->withBody(new StringBody('{"name":"Dave"}'))
    ->withHeader('Content-Type', 'application/json');

Note: If you specify a body, you must also specify a content type. You can also pass in null to remove a body from the request.

You can specify multiple headers in one call:

$request = (new RequestBuilder())->withManyHeaders(['Foo' => 'bar', 'Baz' => 'buzz'])
    // ...

You can also set any request properties:

$request = (new RequestBuilder())->withProperty('routeVars', ['id' => 123])
    // ...

If you'd like to use a different request target type besides origin form, you may:

use Aphiria\Net\Http\RequestTargetTypes;

$request = (new RequestBuilder())->withRequestTargetType(RequestTargetTypes::ABSOLUTE_FORM)
    // ...

Note: Keep in mind that each with*() method will return a clone of the request builder.

Aphiria also has a negotiated request builder that can serialize your models to a request body.

use Aphiria\ContentNegotiation\NegotiatedRequestBuilder;

$request = (new NegotiatedRequestBuilder())->withMethod('POST')
    ->withUri('http://example.com/users')
    ->withBody(new User('Dave'))
    ->build();

Note: By default, negotiated request bodies will be serialized to JSON, but you can specify a different default content type, eg new NegotiatedRequestBuilder(defaultContentType: 'text/xml').

Headers

Headers provide metadata about the HTTP message. In Aphiria, they're implemented by Aphiria\Net\Http\Headers, which extends Aphiria\Collections\HashTable. On top of the methods provided by HashTable, they also provide the following methods:

Note: Header names that are passed into the methods in Headers are automatically normalized to Train-Case. In other words, foo_bar will become Foo-Bar.

Bodies

HTTP bodies contain data associated with the HTTP message, and are optional. They're represented by Aphiria\Net\Http\IBody, and provide a few methods to read and write their contents to streams and to strings:

use Aphiria\IO\Streams\Stream;
use Aphiria\Net\Http\StringBody;

$body = new StringBody('foo');

// Read the body as a stream
$stream = $body->readAsStream();

// Read the body as a string
$body->readAsString(); // "foo"
(string)$body; // "foo"

// Write the body to a stream
$streamToWriteTo = new Stream(fopen('php://temp', 'w+b'));
$body->writeToStream($streamToWriteTo);

String Bodies

HTTP bodies are most commonly represented as strings.

use Aphiria\Net\Http\StringBody;

$body = new StringBody('foo');

Stream Bodies

Sometimes, bodies might be too big to hold entirely in memory. This is where StreamBody comes in handy:

use Aphiria\IO\Streams\Stream;
use Aphiria\Net\Http\StreamBody;

$stream = new Stream(fopen('foo.txt', 'r+b'));
$body = new StreamBody($stream);

URIs

A URI identifies a resource, typically over a network. They contain such information as the scheme, host, port, path, query string, and fragment. Aphiria represents them in Aphiria\Net\Uri, and they include the following methods:

use Aphiria\Net\Uri;

$uri = new Uri('https://dave:abc123@example.com:443/foo?bar=baz#hash');
$uri->getScheme(); // "https"
$uri->getAuthority(); // "//dave:abc123@example.com:443"
$uri->getUser(); // "dave"
$uri->getPassword(); // "abc123"
$uri->getHost(); // "example.com"
$uri->getPort(); // 443
$uri->getPath(); // "/foo"
$uri->getQueryString(); // "bar=baz"
$uri->getFragment(); // "hash"

To serialize a URI, just cast it to a string:

(string)$uri; // "https://dave:abc123@example.com:443/foo?bar=baz#hash"

Getting POST Data

In vanilla PHP, you can read URL-encoded form data via the $_POST superglobal. Aphiria gives you a helper to parse the body of form requests into a hash table.

use Aphiria\Net\Http\Formatting\RequestParser;

// Let's assume the raw body is "email=foo%40bar.com"
$formInput = (new RequestParser)->readAsFormInput($request);
echo $formInput->get('email'); // "foo@bar.com"

Getting Query String Data

In vanilla PHP, query string data is read from the $_GET superglobal. In Aphiria, it's stored in the request's URI. Uri::getQueryString() returns the raw query string - to return it as an immutable hash table, use RequestParser:

use Aphiria\Net\Http\Formatting\RequestParser;

// Assume the query string was "?foo=bar"
$queryStringParams = (new RequestParser)->parseQueryString($request);
echo $queryStringParams->get('foo'); // "bar"

JSON Requests

To check if a request is a JSON request, call

use Aphiria\Net\Http\Formatting\RequestParser;

$isJson = (new RequestParser)->isJson($request);

Rather than having to parse a JSON body yourself, you can use RequestParser to do it for you:

use Aphiria\Net\Http\Formatting\RequestParser;

$json = (new RequestParser)->readAsJson($request);

Getting Cookies

Aphiria has a helper to grab cookies from request headers as an immutable hash table:

use Aphiria\Net\Http\Formatting\RequestParser;

$cookies = (new RequestParser)->parseCookies($request);
$cookies->get('userid');

Getting Client IP Address

If you use the RequestFactory to create your request, the client IP address will be added to the request property CLIENT_IP_ADDRESS. To make it easier to grab this value, you can use RequestParser to retrieve it:

use Aphiria\Net\Http\Formatting\RequestParser;

$clientIPAddress = (new RequestParser)->getClientIPAddress($request);

Note: This will take into consideration any trusted proxy header values when determining the original client IP address.

Header Parameters

Some header values are semicolon delimited, eg Content-Type: text/html; charset=utf-8. It's sometimes convenient to grab those key-value pairs:

$contentTypeValues = $requestParser->parseParameters($request, 'Content-Type');
// Keys without values will return null:
echo $contentTypeValues->get('text/html'); // null
echo $contentTypeValues->get('charset'); // "utf-8"

Serializing Requests

You can serialize a request per RFC 7230 by casting it to a string:

echo (string)$request;

By default, this will use origin-form for the request target, but you can override the request type via the constructor:

use Aphiria\Net\Http\RequestTargetTypes;

$request = new Request(
    'GET',
    new Uri('https://example.com/foo?bar'),
    requestTargetType: RequestTargetTypes::AUTHORITY_FORM
);

The following request target types may be used:

Multipart Requests

Multipart requests contain multiple bodies, each with headers. That's actually how file uploads work - each file gets a body with headers indicating the name, type, and size of the file. Aphiria can parse these multipart bodies into a MultipartBody, which extends StreamBody. It contains additional methods to get the boundary and the list of MultipartBodyPart objects that make up the body:

You can check if a request is a multipart request:

use Aphiria\Net\Http\Formatting\RequestParser;

$isMultipart = (new RequestParser)->isMultipart($request);

To parse a request body as a multipart body, call

use Aphiria\Net\Http\Formatting\RequestParser;

$multipartBody = (new RequestParser)->readAsMultipart($request);

Each MultipartBodyPart contains the following methods:

Saving Uploaded Files

To save a multipart body's parts to files in a memory-efficient manner, read each part as a stream and copy it to the destination path:

foreach ($multipartBody->getParts() as $multipartBodyPart) {
    $bodyStream = $multipartBodyPart->getBody()->readAsStream();
    $bodyStream->rewind();
    $bodyStream->copyToStream(new Stream(fopen('path/to/copy/to/' . uniqid(), 'wb')));
}

Getting the MIME Type of the Body

To grab the actual MIME type of an HTTP body, call

(new RequestParser)->getActualMimeType($multipartBodyPart);

To get the MIME type that was specified by the client, call

(new RequestParser)->getClientMimeType($multipartBodyPart);

Creating Multipart Requests

The Net library makes it straightforward to create a multipart request manually. The following example creates a request to upload two images:

use Aphiria\Net\Http\Headers;
use Aphiria\Net\Http\MultipartBody;
use Aphiria\Net\Http\MultipartBodyPart;
use Aphiria\Net\Http\Request;
use Aphiria\Net\Http\StreamBody;
use Aphiria\Net\Uri;

// Build the first image's headers and body
$image1Headers = new Headers();
$image1Headers->add('Content-Disposition', 'form-data; name="image1"; filename="foo.png"');
$image1Headers->add('Content-Type', 'image/png');
$image1Body = new StreamBody(fopen('path/to/foo.png', 'rb'));

// Build the second image's headers and body
$image2Headers = new Headers();
$image2Headers->add('Content-Disposition', 'form-data; name="image2"; filename="bar.png"');
$image2Headers->add('Content-Type', 'image/png');
$image2Body = new StreamBody(fopen('path/to/bar.png', 'rb'));

// Build the request's headers and body
$body = new MultipartBody([
    new MultipartBodyPart($image1Headers, $image1Body),
    new MultipartBodyPart($image2Headers, $image2Body)
]);
$headers = new Headers();
$headers->add('Content-Type', "multipart/form-data; boundary={$body->getBoundary()}");

// Build the request
$request = new Request(
    'POST',
    new Uri('https://example.com'),
    $headers,
    $body
);

Trusted Proxies

If you're using a load balancer or some sort of proxy server, you'll need to add it to the list of trusted proxies. You can also use your proxy to set custom, trusted headers. You may specify them in the factory constructor:

// The client IP will be read from the "X-My-Proxy-Ip" header when using a trusted proxy
$factory = new RequestFactory(['192.168.1.1'], ['HTTP_CLIENT_IP' => 'X-My-Proxy-Ip']);
$request = $factory->createRequestFromSuperglobals($_SERVER);