HTTP
pop-http
is the main HTTP component for the Pop PHP Framework.
It provides a robust set of features to manage the many aspects of HTTP connections, including
client-side and server-side requests, responses, headers and data.
Client
At its core, the client object works with a request object, a handler object and a response object to successfully execute an HTTP request. The request object can have request data. Both the request and response objects can have headers and a body. The response object will have a response code and response message, along with other helper functions to determine if the request yielded a successful response or an error.
NOTE: The constructor of the Pop\Http\Client
class is flexible and can take any of the following parameters in any order:
- A URI string
- A
Pop\Http\Client\Request
object (which contains the URI) - A
Pop\Http\Client\Response
object (not common, as the response object is typically auto-populated) - A
Pop\Http\Auth
object (to assist with authorization) - A handler object that is an instance of
Pop\Http\Client\Handler\HandlerInterface
- An
$options
array
Quickstart
The most basic way to wire up a simple GET
request would be:
use Pop\Http\Client;
$response = Client::get('http://localhost/');
which is also the equivalent to:
use Pop\Http\Client;
$client = new Client('http://localhost/');
$response = $client->get();
or
use Pop\Http\Client;
$client = new Client('http://localhost/', ['method' => 'GET']);
$response = $client->send();
In the examples above, the $response
object returned is a full response object, complete with all of the headers,
data, messaging and content that comes with an HTTP response. If you want to simply access the pertinent content of
the response object, this method can be used:
$content = $response->getParsedResponse();
That method will attempt to auto-negotiate using the Content-Type
header and give an appropriate data response
or object. For example, if the content type of the response was application/json
, then the data returned will
be a PHP array representation of that JSON data.
POST Example
A POST
request can be given some data in the $options
array to send along with the request:
use Pop\Http\Client;
$response = Client::post('http://localhost/post', [
'data' => [
'foo' => 'bar',
'baz' => 123
]
]);
which is also the equivalent to:
use Pop\Http\Client;
$client = new Client('http://localhost/post', [
'data' => [
'foo' => 'bar',
'baz' => 123
]
]);
$response = $client->post();
or
use Pop\Http\Client;
$client = new Client('http://localhost/post', [
'method' => 'POST',
'data' => [
'foo' => 'bar',
'baz' => 123
]
]);
$response = $client->send();
Other Methods
All of the standard HTTP request methods are accessible in the manner outlined above. For example:
use Pop\Http\Client;
$responsePut = Client::put('http://localhost/put', ['data' => ['foo' => 'bar']]);
$responsePatch = Client::patch('http://localhost/patch', ['data' => ['foo' => 'bar']]);
$responseDelete = Client::delete('http://localhost/delete', ['data' => ['foo' => 'bar']]);
Auth
There is an auth header class to assist in wiring up different types of standard authorization headers:
- Basic
- Bearer Token
- API Key
- Digest
Basic
use Pop\Http\Auth;
use Pop\Http\Client;
$response = Client::post('http://localhost/auth', Auth::createBasic('username', 'password'));
Bearer Token
use Pop\Http\Auth;
use Pop\Http\Client;
$response = Client::post('http://localhost/auth', Auth::createBearer('MY_AUTH_TOKEN'));
API Key
use Pop\Http\Auth;
use Pop\Http\Client;
$response = Client::post('http://localhost/auth', Auth::createKey('MY_API_KEY')));
Digest
Digest authorization can be complex and require a number of different parameters. This is a basic example:
use Pop\Http\Auth;
use Pop\Http\Client;
$response = Client::post(
'http://localhost/auth',
Auth::createDigest(
new Auth\Digest('realm', 'username', 'password', '/uri', 'SERVER_NONCE')
)
);
Options
The client object supports an $options
array to pass in general configuration details and data for the request.
Supported keys in the options array are:
base_uri
- the base URI for re-submitting many requests with the same client to different endpoints on the same domainmethod
- the request method (GET, POST, PUT, PATCH, DELETE, etc.)headers
- an array of request headersuser_agent
- the user agent stringquery
- an array of request query data - reserved for only a URL-encoded query stringdata
- an array of request data - can be any request datafiles
- an array of files on disk to be sent with the requesttype
- set the request type (URL-form, JSON, XML or multipart/form)Request::URLENCODED
-application/x-www-form-urlencoded
Request::JSON
-application/json
Request::XML
-application/xml
Request::MULTIPART
-multipart/form-data
auto
- trigger automatic content negotiation and return the parsed content, if possible (boolean)async
- trigger an asynchronous request (boolean)verify_peer
- enforce or disallow verifying the host for SSL connections (boolean)allow_self_signed
- allow or disallow the use of self-signed certificates for SSL connections (boolean)force_custom_method
- for Curl only. Forces the use ofCURLOPT_CUSTOMREQUEST
(boolean)
Here is an example using a base_uri
:
use Pop\Http\Client;
use Pop\Http\Client\Request;
$client = new Client(['base_uri' => 'http://localhost']);
$response1 = $client->get('/page1'); // Will request http://localhost/page1
$response2 = $client->get('/page2'); // Will request http://localhost/page2
$response2 = $client->get('/page3'); // Will request http://localhost/page3
Here is an example to send some JSON data:
use Pop\Http\Client;
use Pop\Http\Client\Request;
$client = new Client('http://localhost/post', [
'data' => [
'foo' => 'bar',
'baz' => 123
],
'type' => Request::JSON // "application/json"
]);
$response = $client->post();
Here is an example to send some files:
use Pop\Http\Client;
use Pop\Http\Client\Request;
$client = new Client('http://localhost/post', [
'method' => 'POST',
'files' => [
'/path/to/file/image1.jpg',
'/path/to/file/image2.jpg',
],
'type' => Request::MULTIPART // "multipart/form-data"
]);
$response = $client->send();
Content Negotiation
In the above examples, the $response
returned is a full response object.
If you want to get the actual response content, as mentioned above, you would call:
$content = $response->getParsedResponse();
The above call will attempt to negotiate the content by the Content-Type
header and return the correctly parsed content.
Automatic Content Negotiation
There are two ways to attempt content negotiation automatically and return the parsed content:
- If you would like to attempt to parse the response content as JSON, regardless of any
Content-Type
header value or absence thereof, you can call thejson()
method to obtain a PHP array representation of the data:
use Pop\Http\Client;
// Returns an array
$data = Client::get('http://localhost/')->json();
- Similarly, if you would prefer to have a
Collection
object populated with the data content returned, you can call thecollect()
method. Internally, this will attempt thejson()
method as well:
use Pop\Http\Client;
// Returns an instance of Pop\Utils\Collection
$data = Client::get('http://localhost/')->collect();
- You can set the
auto
option to true, which is contingent on the server response having the correctContent-Type
header. This will return a PHP array representation of the data:
use Pop\Http\Client;
$client = new Client('http://localhost/', ['auto' => true]);
$data = $client->get(); // Returns an array
If you still need to access the full response object, you can access it by calling:
$clientResponse = $client->getResponse();
Requests
You can have granular control over the configuration of the request object by interacting with it directly.
use Pop\Http\Client;
use Pop\Http\Client\Request;
$request = new Request('http://localhost/', 'POST');
$request->createAsJson();
$request->addHeaders([
'X-Custom-Header: Custom-Value',
]);
$request->setData([
'foo' => 'bar',
'baz' => 123
]);
$client = new Client($request);
$response = $client->send();
There are four ways to configure the request for four different common data types:
- JSON
- XML
- URL-encoded form
- Multipart form
use Pop\Http\Client\Request;
$requestJson = Request::createJson('http://localhost/', 'POST', $data);
$requestXml = Request::createXml('http://localhost/', 'POST', $data);
$requestUrl = Request::createUrlForm('http://localhost/', 'POST', $data);
$requestMulti = Request::createMultipart('http://localhost/', 'POST', $data);
or
$request->createAsJson();
$request->createAsXml();
$request->createAsUrlEncoded();
$request->createAsMultipart();
Each way effectively sets the appropriate Content-Type
header and properly formats the data for that data type.
Rendering Requests
Client requests can be rendered out to a raw string:
use Pop\Http\Client;
$client = new Client('http://localhost:8000/files.php', [
'method' => 'POST',
'data' => [
'foo' => 'bar'
],
'headers' => [
'Authorization' => 'Bearer 123456789',
],
'type' => Request::URLENCODED
]);
echo $client->render();
Which would produce a string like this:
POST /files.php HTTP/1.1
Host: localhost:8000
Authorization: Bearer 123456789
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length: 7
foo=bar
Responses
Upon sending a request, the response object is automatically created and populated with the content from the raw response.
use Pop\Http\Client;
$response = Client::post('http://localhost/post', [
'data' => [
'foo' => 'bar',
'baz' => 123
]
]);
echo $response->getCode(); // 200
echo $response->getMessage(); // OK
var_dump($response->getHeaders()); // An array of HTTP header objects
var_dump($response->hasHeader('Content-Type')); // Boolean result
var_dump($response->getBody()); // A body object than contains the response content
The header and body entities of both requests and responses are actually objects that store all their pertinent data. To access the actual data content, you would have to use methods such as these:
// i.e., 'application/json'
var_dump($response->getHeaderValueAsString('Content-Type'));
// Get actual content of the body object
var_dump($response->getBodyContent());
As mentioned above, using the following method will get the parsed content based on Content-Type
:
var_dump($response->getParsedResponse());
There are a number of helper methods to determine the response's status:
$response->isSuccess(); // Bool on 100/200/300-level responses
$response->isError(); // Bool on 400/500-level responses
$response->isContinue(); // Bool on 100-level response
$response->isOk(); // Bool on 200 response
$response->isCreated(); // Bool on 201 response
$response->isAccepted(); // Bool on 202 response
$response->isNoContent(); // Bool on 204 response
$response->isRedirect(); // Bool on 300-level response
$response->isMovedPermanently(); // Bool on 301 response
$response->isFound(); // Bool on 302 response
$response->isClientError(); // Bool on 400-level response
$response->isBadRequest(); // Bool on 400 response
$response->isUnauthorized(); // Bool on 401 response
$response->isForbidden(); // Bool on 403 response
$response->isNotFound(); // Bool on 404 response
$response->isMethodNotAllowed(); // Bool on 405 response
$response->isNotAcceptable(); // Bool on 406 response
$response->isRequestTimeout(); // Bool on 408 response
$response->isConflict(); // Bool on 409 response
$response->isLengthRequired(); // Bool on 411 response
$response->isUnsupportedMediaType(); // Bool on 415 response
$response->isUnprocessableEntity(); // Bool on 422 response
$response->isTooManyRequests(); // Bool on 429 response
$response->isServerError(); // Bool on 500-level response
$response->isInternalServerError(); // Bool on 500 response
$response->isBadGateway(); // Bool on 502 response
$response->isServiceUnavailable(); // Bool on 503 response
Handlers
You can choose to use a different handler with the client object. The available handlers are:
Pop\Http\Client\Handler\Curl
- uses the PHP curl extension (default)Pop\Http\Client\Handler\Stream
- uses PHP stream functionalityPop\Http\Client\Handler\CurlMulti
- reserved for multiple parallel/concurrent requests at the same time
use Pop\Http\Client;
use Pop\Http\Client\Handler\Stream;
$client = new Client('http://localhost/', new Stream());
or through the setHandler()
method:
use Pop\Http\Client;
use Pop\Http\Client\Handler\Stream;
$client = new Client('http://localhost/');
$client->setHandler(new Stream());
And then you can interact with the handler using the getHandler()
method:
// Example wih a CURL handler
$client->getHandler()->setOption(CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0);
Curl
The handlers allow you to further customize the request by interfacing with each respective handler's settings.
For Curl
, that mainly includes setting additional Curl options needed for the request. (Please Note: Many
of the required Curl options, such as CURLOPT_URL
and CURLOPT_HTTPHEADER
are automatically set based on
the initial configuration of the client and request objects.)
use Pop\Http\Client;
use Pop\Http\Client\Handler\Curl;
$curl = new Curl();
$curl->setOptions([
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0
]);
$client = new Client('http://localhost/');
$client->setHandler($curl);
Stream
For Stream
, that includes setting context options and parameters needed for the request. (Please Note:
Many of the required Stream context options, such as ['http']
, ['http']['method']
and ['http']['header']
are automatically set based on the initial configuration of the client and request objects.)
use Pop\Http\Client;
use Pop\Http\Client\Handler\Stream;
$stream = new Stream();
$stream->setContextOptions([
'http' => [
'protocol_version' => '1.0'
]
]);
$client = new Client('http://localhost/');
$client->setHandler($stream);
Curl Multi-Handler
The Curl Multi-Handler is a special use-case handler that allows for multiple parallel/concurrent requests
to be made at the same time. Each request will get its own Client
object, which will be registered with
the multi-handler object. The simplest way to configure a multi-handler object would be:
use Pop\Http\Client;
// Three GET requests
$multiHandler = Client::createMulti([
'http://localhost/test1.php',
'http://localhost/test2.php',
'http://localhost/test3.php'
]);
or
use Pop\Http\Client;
use Pop\Http\Client\Request;
// Three POST requests
$multiHandler = Client::createMulti([
new Request('http://localhost/test1.php', 'POST'),
new Request('http://localhost/test2.php', 'POST'),
new Request('http://localhost/test3.php', 'POST')
]);
From there, the multi-handler object can send the requests:
$running = null;
do {
$multiHandler->send($running);
} while ($running);
$responses = $multiHandler->getAllResponses();
The $multiHandler->getAllResponses()
method will return an array of all the response objects returned
from each of the requests.
Here is a more verbose way to configure a multi-handler object:
use Pop\Http\Client;
use Pop\Http\Client\Handler\CurlMulti;
$multiHandler = new CurlMulti();
$client1 = new Client('http://localhost/test1.php', $multiHandler);
$client2 = new Client('http://localhost/test2.php', $multiHandler);
$client3 = new Client('http://localhost/test3.php', $multiHandler);
Promises
Promises allow you to stage asynchronous requests within the application. When you initialize a client object and call it asynchronously, it will return a promise object. There are few different ways to achieve this:
use Pop\Http\Client;
$promise = Client::postAsync('http://localhost/');
which is equivalent to:
use Pop\Http\Client;
$client = new Client('http://localhost/');
$promise = $client->postAsync();
or
use Pop\Http\Client;
$client = new Client('http://localhost/', ['method' => 'POST']);
$promise = $client->sendAsync();
or
use Pop\Http\Client;
$client = new Client('http://localhost/', ['method' => 'POST', 'async' => true]);
$promise = $client->send();
The multi-handler supports asynchronous requests as well and will return a promise object:
use Pop\Http\Client;
$multiHandler = Client::createMulti([
'http://localhost/test1.php',
'http://localhost/test2.php',
'http://localhost/test3.php'
]);
$promise = $multiHandler->sendAsync();
Wait
Once you have a promise object, the most basic way to interact with it is to call wait()
, which simply
triggers the request and waits until the request is finished before allowing the application to continue.
Upon completion, the promise will return a response object. Otherwise, it will throw an exception, so it
is best to wrap the call in a try/catch
block:
try {
$response = $promise->wait();
print_r($response->getParsedResponse());
} catch (\Exception $e) {
echo $e->getMessage() . PHP_EOL;
}
If you need something that degrades a little more gracefully and need to suppress the thrown exception,
you can pass false
as the $unwrap
parameter into the wait()
method to prevent the exception from
being thrown:
$response = $promise->wait(false);
if ($response instanceof Response) {
print_r($response->getParsedResponse());
}
Then
You can use the then()
method, along with catch()
and finally()
to assign callbacks to handle
each specific scenario:
then()
- on success callbackcatch()
- on failure callbackfinally()
- callback to run at the end no matter what the result is
Additionally, a cancel callback can be set with the setCancel()
method and will be triggered at any
time the promise is cancelled. Once the promise is configured, the resolve()
method needs to be called
to finish the request.
use Pop\Http\Promise;
use Pop\Http\Client\Response;
$promise->setCancel(function(Promise $promise) {
// Do something upon cancellation
});
$promise->then(function(Response $response) {
// Do something on success
})->catch(function(Response $response)) {
// Do something on failure
})->finally(function(Promise $promise) {
// Do something at the end
});
$promise->resolve();
As a convenience for a simple then()
call, you can pass a $resolve
flag as true
to force the promise
to resolve without having to call the resolve()
method:
use Pop\Http\Client\Response;
// Force resolve
$promise->then(function(Response $response) {
// Do something on success
}, true);
The catch()
and finally()
methods also have the same $resolve
force flag.
Forwarding
You can chain multiple then()
method calls together, which is sometimes called "forwarding" a promise.
The return of the first then()
call needs to be another promise object.
use Pop\Http\Client;
use Pop\Http\Client\Response;
$promise1 = Client::getAsync('http://localhost/test1.php');
$promise2 = Client::getAsync('http://localhost/test2.php');
$promise1->then(function(Response $response) use ($promise2) {
// Do something with the first promise response
return $promise2;
})->then(function(Response $response) {
// Do something with the second promise response
});
$promise1->resolve();
Nesting
Promises can be "nested" together as well, whereas one resolved promise creates and triggers another promise:
use Pop\Http\Client;
use Pop\Http\Client\Response;
$promise = Client::getAsync('http://localhost/test1.php');
$promise->then(function(Response $response) {
$data1 = $response->getParsedResponse();
$promise = Client::getAsync('http://localhost/test2.php')
->then(function(Response $response) use ($data1) {
$data2 = $response->getParsedResponse();
// Do something with both the data results from promise 1 and 2.
}, true);
}, true);
Automatic Content Negotiation
Promises generated by client objects set for automatic content negotiation will return the parsed response content instead of a full response object.
use Pop\Http\Client;
$promise = Client::postAsync('http://localhost/', ['auto' => true]);
$response = $promise->wait(false); // The response will be the parsed content response
Server
The server object and its components provide convenient and robust functionality to manage inbound server requests and outbound responses. At its core, and like the client object, the server object is compromised of a request object and a response object. However, opposite to the client object, the server object's request is typically auto-populated from the incoming request headers and data, while the response object is available to be configured as required to produce and send a response to the calling client.
Within an application, creating a server object will automatically take in the global request data that would come in from an inbound client request. This includes:
- Request data (
$_GET
,$_POST
, etc) - Request headers
- Request body
Request Headers/Data
Headers
$headers = $reqeuest->getHeaders();
if ($request->hasHeader('Content-Type')) {
$contentType = $request->getHeader('Content-Type'); // Header object
var_dump($request->getHeaderValueAsString('Content-Type')); // Header value as string
}
Body
$body = $request->getBody(); // Body object
var_dump($response->getBodyContent()); // Get actual content of the body object
Method
$request->isGet();
$request->isPost();
$request->isPut();
$request->isPatch();
$request->isDelete();
$request->hasFiles();
Data
$queryData = $request->getQuery(); // GET Request
$postData = $request->getPost();
$putData = $request->getPut();
$patchData = $request->getPatch();
$deleteData = $request->getDelete();
$filesData = $request->getFiles();
$serverData = $request->getServer();
$envData = $request->getEnv();
$foo = $request->getQuery('foo'); // GET Request
$foo = $request->getPost('foo');
$foo = $request->getPut('foo');
$foo = $request->getPatch('foo');
$foo = $request->getDelete('foo');
$foo = $request->getFiles('foo');
$foo = $request->getServer('foo');
$foo = $request->getEnv('foo');
If there is general data that has been parsed or raw data, that can be accessed via:
$parsedData = $request->getParsedData();
$rawData = $request->getRawData();
As an example, this curl
command pointing at the following URL with a PHP script can be executed:
curl -i -X POST --header "Authorization: Bearer 1234567890" \
--data "foo=bar&baz=123" "http://localhost/post.php"
with the contents of post.php
being:
use Pop\Http\Server;
$server = new Server();
echo $server->request->getHeader('Authorization')->getValue();
if ($server->request->isPost()) {
print_r($server->request->getPost());
}
Automatically, the server object's request object will be populated with the incoming request data. The example script above will produce:
Bearer 1234567890
Array
(
[foo] => bar
[baz] => 123
)
From an incoming request, you can populate an appropriate response:
$server->response->setCode(200)
->setMessage('OK')
->setVersion('1.1')
->addHeader('Content-Type', 'text/plain')
->setBody('This is the response');
$server->send();
which will produce:
HTTP/1.1 200 OK
Content-Type: text/plain
This is the response
By default, the server object constructor will instantiate new request and response objects, but you can inject your own:
use Pop\Http\Server;
use Pop\Http\Server\Request;
use Pop\Http\Server\Response;
$myRequest = new Request();
$myResponse = new Response();
$server = new Server($myRequest, $myResponse);
Filtering
As an extra layer of protection, you can add filters to the request object to filter incoming data:
use Pop\Http\Server;
use Pop\Http\Server\Reqeust;
$filters = ['strip_tags', 'addslashes'];
$server = new Server(new Request(null, $filters));
And with the following curl command with data that contains tags and a single quote:
curl -i -X POST --data "foo=<script>bad's script</script>" "http://localhost:8000/post.php"
if ($server->request->isPost()) {
print_r($server->request->getPost());
}
the data will be filtered:
Bearer 123456
Array
(
[foo] => bad\'s script
)
Redirects/Forwards
You can redirect to another URL by calling the following method:
use Pop\Http\Server\Response;
Response::redirect('http://www.newlocation.com/');
You can forward a client object's response as the server's response:
use Pop\Http\Server\Response;
Response::forward($clientResponse);
Rendering Responses
Server responses can be rendered out to a raw string:
use Pop\Http\Server;
$server = new Server();
$server->response->setCode(200)
->setMessage('OK')
->setVersion('1.1')
->addHeader('Content-Type', 'text/plain')
->setBody('This is the response');
echo $server;
Which would produce a string like this:
HTTP/1.1 200 OK
Content-Type: text/plain
This is the response
Uploads
Basic file upload
use Pop\Http\Server\Upload;
$upload = new Upload('/path/to/uploads');
$upload->setDefaults();
$upload->upload($_FILES['file_upload']);
// Do something with the newly uploaded file
if ($upload->isSuccess()) {
$file = $upload->getUploadedFile();
} else {
echo $upload->getErrorMessage();
}
The above code creates the upload object, sets the upload path and sets the basic defaults, which includes a max file size of 10MBs, and an array of allowed common file types as well as an array of common disallowed file types.
File upload names and overwrites
By default, the file upload object will not overwrite a file of the same name. In the above
example, if $_FILES['file_upload']['name']
is set to 'my_document.docx' and that file
already exists in the upload path, it will be renamed to my_document_1.docx
.
If you want to enable file overwrites, you can simply do this:
$upload->overwrite(true);
Also, you can give the file a direct name on upload like this:
$upload->upload($_FILES['file_upload'], 'my-custom-filename.docx');
And if you need to check for a duplicate filename first, you can use the checkFilename
method. If the filename exists, it will append a _1
to the end of the filename, or loop
through until it finds a number that doesn't exist yet (_#
). If the filename doesn't
exist yet, it returns the original name.
$filename = $upload->checkFilename('my-custom-filename.docx');
// $filename is set to 'my-custom-filename_1.docx'
$upload->upload($_FILES['file_upload'], $filename);
CLI Conversion
The CLI conversion feature allows you to convert client request objects into valid curl
commands to be used on
the CLI. It also supports converting valid curl
commands into client request objects to be used in a PHP application.
Curl Command to Client Object
use Pop\Http\Client;
$client = Client::fromCurlCommand('curl -i -X POST -d"foo=bar&baz=123" http://localhost/post.php');
$client->send();
Client Object to Curl Command
use Pop\Http\Client;
$client = new Client('http://localhost/post.php', [
'method' => 'POST',
'data' => [
'foo' => 'bar',
'baz' => 123
]
]);
echo $client->toCurlCommand();
curl -i -X POST --data "foo=bar&baz=123" "http://localhost/post.php"