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).
Let's create a request:
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->method;
// Grab the URI:
$uri = $request->uri;
// Grab the protocol version, eg 1.1:
$protocolVersion = $request->protocolVersion;
// Grab the headers (you can add new headers to the returned hash table)
$headers = $request->headers;
// Set a header
$request->headers->add('Foo', 'bar');
// Get the body (could be null)
$body = $request->body;
// Set the body
$request->body = new StringBody('foo');
// Grab any special metadata properties (a custom hash table that you can define)
$properties = $request->properties;
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')
->build();
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'])
->build();
You can also set any request properties:
$request = new RequestBuilder()->withProperty('routeVars', ['id' => 123])
->build();
If you'd like to use a different request target type besides origin form, you may:
use Aphiria\Net\Http\RequestTargetType;
$request = new RequestBuilder()->withRequestTargetType(RequestTargetType::AbsoluteForm)
->build();
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 are an extension of HashTable
, and also provide the following methods:
getFirst(string $name): mixed
tryGetFirst(string $name, mixed &$value): bool
Note: Header names that are passed into the methods in
Headers
are automatically normalized to Train-Case. In other words,foo_bar
will becomeFoo-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->scheme; // "https"
$uri->user; // "dave"
$uri->password; // "abc123"
$uri->host; // "example.com"
$uri->port; // 443
$uri->path; // "/foo"
$uri->queryString; // "bar=baz"
$uri->fragment; // "hash"
$uri->getAuthority(); // "//dave:abc123@example.com:443"
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::queryString
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;
// Assume the request contained the header "Cookie: userid=123"
$cookies = new RequestParser()->parseCookies($request);
echo $cookies->get('userid'); // "123"
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
. Aphiria provides an easy way 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\RequestTargetType;
$request = new Request(
'GET',
new Uri('https://example.com/foo?bar'),
requestTargetType: RequestTargetType::AuthorityForm
);
The following request target types may be used:
RequestTargetType::AbsoluteForm
RequestTargetType::AsteriskForm
RequestTargetType::AuthorityForm
RequestTargetType::OriginForm
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 properties to get the boundary and the list of MultipartBodyPart
objects that make up the body:
$multipartBody->boundary; // string
$multipartBody->parts; // MultipartBodyPart[]
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 properties:
$multipartBodyPart->body; // ?IBody
$multipartBodyPart->headers; // Headers
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->parts as $multipartBodyPart) {
$bodyStream = $multipartBodyPart->body->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
$actualMimeType = new RequestParser()->getActualMimeType($multipartBodyPart);
To get the MIME type that was specified by the client, call
$clientMimeType = 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->boundary}");
// 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);