A modern, lightweight, fast and type-safe HTTP client for PHP 7.4
Note: I'm still actively working on Wander. Help out if you'd like to, but I'd advise against using it yet.
Making HTTP requests in PHP can be a pain. Either you go full-low-level mode and use streams or curl, or you'll need to use one of the existing choices with array-based configuration, lots of overhead, or missing PSR compatibility. Wander attempts to provide an alternative: It doesn't try to solve every problem under the sun out of the box, but stay simple and extensible. Wander makes some sensible assumptions, but allows you to extend it if those assumptions turn out to be wrong for your use case.
- Simple, discoverable API Wander exposes all request options as chainable methods.
- Fully standards compliant Wander relies on PSR-17 factories, PSR-18 client drivers and PSR-7 requests/responses. Use our implementations of choice (nyholm/psr7) or bring your own.
- Pluggable serialization Request and response bodies are serialized depending on the content type, transparently and automatically. Use a format we don't know yet? Add your own (and submit a PR!).
- Compatible with other solutions As drivers are, essentially, PSR-18 clients, you can swap in any other client library and make it work out of the box. This provides for a smooth migration path.
- Extensive exceptions Wander throws several exceptions, all of which follow a clear inheritance structure. This makes it exceptionally easy to handle errors as coarsely or fine-grained as necessary.
$responseBody = (new Wander())
->patch('https://example.com')
->withQueryParameter('foo', 'bar')
->withBasicAuthorization('User', 'Pass')
->withHeaders([
Header::ACCEPT => MediaType::APPLICATION_JSON,
])
->withoutHeader(Header::USER_AGENT)
->withBody([ 'test' => true ])
->asJson()
->run()
->getBody()
->getContents();
Install using composer:
composer require radiergummi/wander
The following section provides usage several, concrete examples. For a full reference, view the reference section.
Wander has several layers of shorthands built in, which make working with it as simple as possible. To perform a simple GET
request, the following is enough:
$client = new Wander();
$response = $client
->get('https://example.com')
->run();
Wander has several shorthands for common HTTP methods on the Wander
object (GET
, PUT
, POST
, DELETE
and so on).
A slightly longer version of the above example, using the createContext
method the shorthands also use internally:
$client = new Wander();
$response = $client
->createContext(Method::GET, 'https://example.com')
->run();
This context created here wraps around PSR-7 requests and adds a few helper methods, making it possible to chain the method calls. Doing so requires creating request instances, which of course relies on a PSR-17 factory you can swap out for your own. More on that below.
Wander also supports direct handling of request instances:
$client = new Wander();
$request = new Request(Method::GET, 'https://example.com');
$response = $client->request($request);
Wander accepts any kind of data as for the request body. It will be serialized just before dispatching the request, depending on the Content-Type
header set
at that time. This means that you don't have to take care of body serialization.
If you set a PSR-7 StreamInterface
instance as the body, however, Wander will not attempt to modify the stream and use it as-is.
Sending a JSON request:
$client = new Wander();
$response = $client
->post('https://example.com')
->withBody([
'anything' => true
])
->asJson()
->run();
Sending a raw stream:
$client = new Wander();
$response = $client
->post('https://example.com')
->withBody(Stream::create('/home/moritz/large.mp4'))
->run();
As request contexts wrap around request instances, there's also response contexts wrapping around PSR-7 responses, providing additional helpers, for example to get a parsed representation of the response body, if possible.
$client = new Wander();
$response = $client
->get('https://example.com')
->run()
->getParsedBody();
Wander follows an exception hierarchy that represents different classes of errors. In contrary to PSR-18 clients, I firmly believe response status codes from the 400 or 500 range should throw an exception, because you end up checking for them anyway. Exceptions are friends! Especially in thee case of HTTP, where an error could be an expected part of the flow.
The exception tree looks as follows:
WanderException (inherits from \RuntimeException)
├─ ClientException (implements PSR-18 ClientExceptionInterface)
├─ DriverException (implements PSR-18 RequestExceptionInterface)
├─ ConnectionException (implements PSR-18 NetworkExceptionInterface)
├─ SslCertificateException (implements PSR-18 NetworkExceptionInterface)
├─ UnresolvableHostException (implements PSR-18 NetworkExceptionInterface)
└─ ResponseErrorException
├─ ClientErrorException
│ ├─ BadRequestException
│ ├─ UnauthorizedException
│ ├─ PaymentRequiredException
│ ├─ ForbiddenException
│ ├─ NotFoundException
│ ├─ MethodNotAllowedException
│ ├─ NotAcceptableException
│ ├─ ProxyAuthenticationRequiredException
│ ├─ RequestTimeoutException
│ ├─ ConflictException
│ ├─ GoneException
│ ├─ LengthRequiredException
│ ├─ PreconditionFailedException
│ ├─ PayloadTooLargeException
│ ├─ UriTooLongException
│ ├─ UnsupportedMediaTypeException
│ ├─ RequestedRangeNotSatisfyableException
│ ├─ ExpectationFailedException
│ ├─ ImATeapotException
│ ├─ MisdirectedRequestException
│ ├─ UnprocessableEntityException
│ ├─ LockedException
│ ├─ FailedDependencyException
│ ├─ TooEarlyException
│ ├─ UpgradeRequiredException
│ ├─ PreconditionRequiredException
│ ├─ TooManyRequestsException
│ ├─ RequestHeaderFieldsTooLargeException
│ └─ UnavailableForLegalReasonsException
└─ ServerErrorException
├─ InternalServerErrorException
├─ NotImplementedException
├─ BadGatewayException
├─ ServiceUnavailableException
├─ GatewayTimeoutException
├─ HTTPVersionNotSupportedException
├─ VariantAlsoNegotiatesException
├─ InsufficientStorageException
├─ LoopDetectedException
├─ NotExtendedException
└─ NetworkAuthenticationRequiredException
All response error exceptions provide getters for the request and response instance, so you can do stuff like this easily:
try {
$request->run();
} catch (UnauthorizedException | ForbiddenException $e) {
$this->refreshAccessToken();
return $this->retry();
} catch (GoneException $e) {
throw new RecordDeletedExeption(
$e->getRequest()->getUri()->getPath()
);
} catch (BadRequestException $e) {
$responseBody = $e->getResponse()->getBody()->getContents();
$error = json_decode($responseBody, JSON_THROW_ON_ERROR);
$field = $error['field'] ?? null;
if ($field) {
throw new ValidatorException("Failed to validate {$field}");
}
throw new UnknownException($error);
} catch (WanderException $e) {
// Simply catch all others
throw new RuntimeException(
'Server returned an unknown error: ' .
$e->getResponse()->getBody()->getContents()
);
}
This was just one of a myriad of ways to handle errors with these kinds of exceptions!
Request timeouts can be configured on your driver instance:
$driver = new StreamDriver();
$driver->setTimeout(3000); // 3000ms / 3s
$client = new Wander($driver);
Note:
Request timeouts are an optional feature for drivers, indicated by theSupportsTimeoutsInterface
. All default drivers implement this interface, though, so you'll only need to check this if you use another implementation.
By default, drivers will follow redirects. If you want to disable this behavior, configure it on your driver instance:
$driver = new StreamDriver();
$driver->followRedirects(false);
$client = new Wander($driver);
Note:
Redirects are an optional feature for drivers, indicated by theSupportsRedirectsInterface
. All default drivers implement this interface, though, so you'll only need to check this if you use another implementation.
By default, drivers will follow redirect indefinitely. If you want to limit the maximum number of redirects, configure it on your driver instance:
$driver = new StreamDriver();
$driver->setMaximumRedirects(3);
$client = new Wander($driver);
Note:
Redirects are an optional feature for drivers, indicated by theSupportsRedirectsInterface
. All default drivers implement this interface, though, so you'll only need to check this if you use another implementation.
Wander supports transparent body serialization for requests and responses, by passing the data through a serializer class. Out of the box, Wander ships with serializers for plain text, JSON, XML, form data, and multipart bodies. Serializers follow a well-defined interface, so you can easily add you own serializer for any data format:
$client = new Wander();
$client->addSerializer('your/media-type', new CustomSerializer());
The serializer will be invoked for any requests and responses with this media type set as its Content-Type
header.
Drivers are what actually handles dispatching requests and processing responses. They have one, simple responsibility: Transform a request instance into a
response instance. By default, Wander uses a driver that wraps streams, but it also ships with a curl driver. If you need something else, or require a variation
of one of the default drivers, you can either create a new driver implementing the DriverInterface
or extend one of
the defaults.
$driver = new class implements DriverInterface {
public function sendRequest(RequestInterface $request): ResponseInterface
{
// TODO: Implement sendRequest() method.
}
public function setResponseFactory(ResponseFactoryInterface $responseFactory): void
{
// TODO: Implement setResponseFactory() method.
}
};
$client = new Wander($driver);
This reference shows all available methods.
This section describes all methods of the HTTP client itself. When creating a new instance, you can pass several dependencies:
new Wander(
DriverInterface $driver = null,
?RequestFactoryInterface $requestFactory = null,
?ResponseFactoryInterface $responseFactory = null
)
Parameter | Type | Required | Description |
---|---|---|---|
$driver |
DriverInterface |
No | Underlying HTTP client driver. Defaults to curl |
$requestFactory |
RequestFactoryInterface |
No | PSR-17 request factory |
$responseFactory |
ResponseFactoryInterface |
No | PSR-17 response factory |
Creates a new request context for a GET
request.
get(UriInterface|string $uri): Context
Parameter | Type | Required | Description |
---|---|---|---|
$uri |
string or UriInterface |
Yes | URI instance or string to create one from. |
Creates a new request context for a POST
request.
post(UriInterface|string $uri, ?mixed $body = null): Context
Parameter | Type | Required | Description |
---|---|---|---|
$uri |
string or UriInterface |
Yes | URI instance or string to create one from. |
$body |
Any type | No | Data to use as the request body. |
Creates a new request context for a PUT
request.
put(UriInterface|string $uri, ?mixed $body = null): Context
Parameter | Type | Required | Description |
---|---|---|---|
$uri |
string or UriInterface |
Yes | URI instance or string to create one from. |
$body |
Any type | No | Data to use as the request body. |
Creates a new request context for a PATCH
request.
patch(UriInterface|string $uri, ?mixed $body = null): Context
Parameter | Type | Required | Description |
---|---|---|---|
$uri |
string or UriInterface |
Yes | URI instance or string to create one from. |
$body |
Any type | No | Data to use as the request body. |
Creates a new request context for a DELETE
request.
delete(UriInterface|string $uri, ?mixed $body = null): Context
Parameter | Type | Required | Description |
---|---|---|---|
$uri |
string or UriInterface |
Yes | URI instance or string to create one from. |
Creates a new request context for a HEAD
request.
head(UriInterface|string $uri, ?mixed $body = null): Context
Parameter | Type | Required | Description |
---|---|---|---|
$uri |
string or UriInterface |
Yes | URI instance or string to create one from. |
Creates a new request context for a OPTIONS
request.
options(UriInterface|string $uri, ?mixed $body = null): Context
Parameter | Type | Required | Description |
---|---|---|---|
$uri |
string or UriInterface |
Yes | URI instance or string to create one from. |
Allows creation of a new request context for an arbitrary request method.
createContext(string $method, UriInterface|string $uri): Context
Parameter | Type | Required | Description |
---|---|---|---|
$method |
string |
Yes | Any request method, case sensitive. |
$uri |
string or UriInterface |
Yes | URI instance or string to create one from. |
Allows creation of a new request context from an existing request instance.
createContextFromRequest(RequestInterface $request): Context
Parameter | Type | Required | Description |
---|---|---|---|
$request |
RequestInterface |
Yes | Existing request instance to create the context from. |
Dispatches a request instance on the client instances driver and returns the response.
request(RequestInterface $request): ResponseInterface
Parameter | Type | Required | Description |
---|---|---|---|
$request |
RequestInterface |
Yes | Request to dispatch. |
The context object performs transformations on an underlying request instance. In spirit with PSR-7, the request is of course immutable. The context will only keep reference to the current instance. This allows us to chain all method calls and dispatch requests, all without leaving "the chain" even once. We can also add helper methods and keep references to other objects--like the client itself, for example--making it very easy to use and extend. Note that you should rely on the client creating contexts for you; using the constructor manually is discouraged.
new Context(
HttpClientInterface $client,
RequestInterface $request
)
Parameter | Type | Required | Description |
---|---|---|---|
$client |
HttpClientInterface |
Yes | HTTP client instance to dispatch the request with. |
$request |
RequestInterface |
Yes | Request as created by our request factory. |
Replaces the request instance.
Retrieves the request instance.
Replaces the HTTP request method.
Retrieves the HTTP request method.
Replaces the URI instance.
Retrieves the URI instance.
Adds a query string to the URI.
Retrieves the query string from the URI.
Adds multiple query parameters to the URI.
Retrieves all query parameters from the URI as a dictionary.
Adds a query parameter to the URI.
Removes a single query parameter from the URI.
Retrieves a single query parameter from the URI by name.
Adds multiple headers to the request.
Retrieves all request headers as a dictionary. Proxy to the PSR-7 request method.
Adds a given header to the request. Proxy to the PSR-7 request method.
Removes a given header if it is set on the request. Proxy to the PSR-7 request method.
Retrieves an array of all header values. Proxy to the PSR-7 request method.
Retrieves all header values, delimited by a comma, as a single string. Proxy to the PSR-7 request method.
Sets the Authorization
header to the given authentication type and credentials.
Sets the Authorization
header to the type Basic
and encodes the comma-delimited credentials as Base64.
Sets the Authorization
header to the type Bearer
and uses the token for the credentials.
Sets the Content-Type
header.
Retrieves the value of the Content-Type
header if set, returns null
otherwise.
Sets the Content-Type
header to JSON (application/json
).
Sets the Content-Type
header to XML (text/xml
).
Sets the Content-Type
header to plain text (text/plain
).
Sets the (unserialized) body data on the context. This will be serialized according to the Content-Type
header
before dispatching the request, taking care of serialization automatically, so you don't have to.
By passing a Stream instance, this process will be skipped in the body will be set on the request as-is.
Retrieves the current body data.
Checks whether the context has any data in its body.
Dispatches the request to the client instance and creates a response context
All contributions are welcome, but please be aware of a few requirements:
- We use psalm for static analysis and would like to keep the level at at least 2 (but would like to reach 1 in the long run). Any PR with
degraded analysis results will not be accepted. To run psalm, use
composer run static-analysis
. - Unit and integration tests must be supplied with every PR. To run all test suites, use
composer run test
.