icanboogie / http
Provides an API to handle HTTP requests.
Installs: 6 941
Dependents: 3
Suggesters: 0
Security: 0
Stars: 5
Watchers: 3
Forks: 0
Open Issues: 0
Requires
- php: >=7.2
- ext-mbstring: *
- icanboogie/datetime: ^1.2
- icanboogie/event: ^4.0
- icanboogie/prototype: ^5.0
Requires (Dev)
- ext-gd: *
- phpunit/phpunit: ^8.5
README
The icanboogie/http package provides a foundation to handle HTTP requests, with representations for requests, request files, responses, and headers. The package also lay the foundation of
The following example is an overview of request processing:
<?php namespace ICanBoogie\HTTP; // The request is usually created from the $_SERVER super global. $request = Request::from($_SERVER); /* @var ResponderProvider $responder_provider */ // The Responder Provider matches a request with a Responder $responder = $responder_provider->responder_for_request($request); // The Responder responds to the request with a Response, it might also throw an exception. $response = $responder->respond($request); // The response is sent to the client. $response();
Installation
composer require icanboogie/http
Request
A request is represented by a Request instance. The initial request is usually created from
the $_SERVER
array, while sub requests are created from arrays of Request::OPTION_*
or
RequestOptions::OPTION_*
options.
<?php namespace ICanBoogie\HTTP; $initial_request = Request::from($_SERVER); # a custom request in the same environment $request = Request::from('path/to/file.html', $_SERVER); # a request created from scratch $request = Request::from([ Request::OPTION_PATH => 'path/to/file.html', Request::OPTION_IS_LOCAL => true, // or OPTION_IP => '::1' Request::OPTION_METHOD => RequestMethod::METHOD_POST, Request::OPTION_HEADERS => [ 'Cache-Control' => 'no-cache' ] ]);
Safe and idempotent requests
Safe methods are HTTP methods that don't modify resources.
For instance, using GET
or HEAD
on a resource URL, should NEVER change the resource.
The is_safe
property may be used to check if a request is safe or not.
<?php use ICanBoogie\HTTP\Request; Request::from([ Request::OPTION_METHOD => Request::METHOD_GET ])->is_safe; // true Request::from([ Request::OPTION_METHOD => Request::METHOD_POST ])->is_safe; // false Request::from([ Request::OPTION_METHOD => Request::METHOD_DELETE ])->is_safe; // false
An idempotent HTTP method is an HTTP method that can be called many times without different outcomes. It would not matter if the method is called only once, or ten times over. The result should be the same.
The is_idempotent
property may be used to check if a request is idempotent or not.
<?php use ICanBoogie\HTTP\Request; Request::from([ Request::OPTION_METHOD => Request::METHOD_GET ])->is_idempotent; // true Request::from([ Request::OPTION_METHOD => Request::METHOD_POST ])->is_idempotent; // false Request::from([ Request::OPTION_METHOD => Request::METHOD_DELETE ])->is_idempotent; // true
A request with changed properties
Requests are for the most part immutable, the with()
method creates an instance copy with changed
properties.
<?php namespace ICanBoogie\HTTP; $request = Request::from($_SERVER)->with([ Request::OPTION_METHOD => RequestMethod::METHOD_HEAD => true, Request::OPTION_IS_XHR => true ]);
Request parameters
Whether they're sent as part of the query string, the POST body, or the path info, parameters sent
along a request are collected in arrays. The query_params
, request_params
, and path_params
properties give you access to these parameters.
You can access each type of parameter as follows:
<?php namespace ICanBoogie\HTTP; /* @var $request Request */ $id = $request->query_params['id']; $method = $request->request_params['method']; $info = $request->path_params['info'];
All the request parameters are also available through the params
property, which merges the
query, request and path parameters:
<?php namespace ICanBoogie\HTTP; /* @var $request Request */ $id = $request->params['id']; $method = $request->params['method']; $info = $request->params['info'];
Used as an array, the Request instance provides these parameters as well, but returns null
when a parameter is not defined:
<?php namespace ICanBoogie\HTTP; /* @var $request Request */ $id = $request['id']; $method = $request['method']; $info = $request['info']; var_dump($request['undefined']); // null
Of course, the request is also an iterator:
<?php namespace ICanBoogie\HTTP; /* @var $request Request */ foreach ($request as $parameter => $value) { echo "$parameter: $value\n"; }
Request files
Files associated with a request are collected in a FileList instance. The initial request
created with $_SERVER
obtain its files from $_FILES
. For custom requests, files are defined
using OPTION_FILES
.
<?php namespace ICanBoogie\HTTP; $request = Request::from($_SERVER); # or $request = Request::from([ Request::OPTION_FILES => [ 'uploaded' => [ FileOptions::OPTION_PATHNAME => '/path/to/my/example.zip' ] ] ]); # $files = $request->files; // instanceof FileList $file = $files['uploaded']; // instanceof File $file = $files['undefined']; // null
File represents uploaded files, and pretend uploaded files, with a single API.
The is_uploaded
property helps you set them apart.
The is_valid
property is a simple way to check if a file is valid. The move()
method
lets you move the file out of the temporary folder or around the filesystem.
<?php namespace ICanBoogie\HTTP; /* @var $file File */ echo $file->name; // example.zip echo $file->unsuffixed_name; // example echo $file->extension; // .zip echo $file->size; // 1234 echo $file->type; // application/zip echo $file->is_uploaded; // false if ($file->is_valid) { $file->move('/path/to/repository/' . $file->name, File::MOVE_OVERWRITE); }
The match()
method is used to check if a file matches a MIME type, a MIME class, or a file
extension:
<?php namespace ICanBoogie\HTTP; /* @var $file File */ echo $file->match('application/zip'); // true echo $file->match('application'); // true echo $file->match('.zip'); // true echo $file->match('image/png'); // false echo $file->match('image'); // false echo $file->match('.png'); // false
The method also handles sets, and returns true
if there is any match:
<?php echo $file->match([ '.png', 'application/zip' ]); // true echo $file->match([ '.png', '.zip' ]); // true echo $file->match([ 'image/png', '.zip' ]); // true echo $file->match([ 'image/png', 'text/plain' ]); // false
File instances can be converted into arrays with the to_array()
method:
<?php $file->to_array(); /* [ 'name' => 'example.zip', 'unsuffixed_name' => 'example', 'extension' => '.zip', 'type' => 'application/zip', 'size' => 1234, 'pathname' => '/path/to/my/example.zip', 'error' => null, 'error_message' => null ] */
Request context
Because requests may be nested, the request context offers a safe place where you can store the state of your application that is relative to a request. For instance, the context can store the relative site, page, route, dispatcher…
The following example demonstrates how to store a value in a request context:
<?php namespace ICanBoogie\HTTP; /** @var Request $request */ /** @var \ICanBoogie\Routing\Route $route */ $request->context->add($route); // … $route = $request->conntext->find(Route::class); # or, if the value is required $route = $request->conntext->get(Route::class);
Obtaining a response
A response is the result of a Responder's respond()
method. An exception is thrown when a
response can't be provided; for example, NotFound.
<?php namespace ICanBoogie\HTTP; /* @var $request Request */ /* @var ResponderProvider $responder_provider */ // The Responder Provider matches a request with a Responder $responder = $responder_provider->responder_for_request($request); // The Responder responds to the request with a Response, it might also throw an exception. $response = $responder->respond($request); // The response is sent to the client. $response();
Response
The response to a request is represented by a Response instance. The response body can
either be null
, a string (or Stringable
), or a Closure
.
Note: Contrary to Request instances, Response instances or completely mutable.
<?php namespace ICanBoogie\HTTP; $response = new Response('<!DOCTYPE html><html><body><h1>Hello world!</h1></body></html>', Response::STATUS_OK, [ Headers::HEADER_CONTENT_TYPE => 'text/html', Headers::HEADER_CACHE_CONTROL => 'public, max-age=3600', ]);
The header and body are sent by invoking the response:
<?php namespace ICanBoogie\HTTP; /* @var $response Response */ $response();
Response status
The response status is represented by a Status instance. It may be defined as an HTTP response
code such as 200
, an array such as [ 200, "Ok" ]
, or a string such as "200 Ok"
.
<?php namespace ICanBoogie\HTTP; $response = new Response; echo $response->status; // 200 Ok echo $response->status->code; // 200 echo $response->status->message; // Ok $response->status->is_valid; // true $response->status = Response::STATUS_NOT_FOUND; echo $response->status->code; // 404 echo $response->status->message; // Not Found $response->status->is_valid; // false $response->status->is_client_error; // true $response->status->is_not_found; // true
Streaming the response body
When a large response body needs to be streamed, it is recommended to use a closure as a response body instead of a huge string that would consume a lot of memory.
<?php namespace ICanBoogie\HTTP; $records = $app->models->order('created_at DESC'); $output = function() use ($records) { $out = fopen('php://output', 'w'); foreach ($records as $record) { fputcsv($out, [ $record->title, $record->created_at ]); } fclose($out); }; $response = new Response($output, Response::STATUS_OK, [ 'Content-Type' => 'text/csv' ]);
About the Content-Length
header field
Before v2.3.2 the Content-Length
header field was added automatically when it was computable,
for instance when the body was a string or an instance implementing __toString()
.
Starting v2.3.2, this is no longer the case, and the header field has to be defined when required.
This was decided to prevent a bug with Apache+FastCGI+DEFLATE where the Content-Length
field
wasn't adjusted although the body was compressed. Also, in most cases it is not such a good idea
to define that field for generated content because it prevents the response from being sent as
compressed chunks.
Redirect response
A redirect response may be created using a RedirectResponse instance.
<?php namespace ICanBoogie\HTTP; $response = new RedirectResponse('/to/redirect/location'); $response->status->code; // 302 $response->status->is_redirect; // true
Delivering a file
A file may be delivered using a FileResponse instance. Cache control and range requests
are handled automatically; you only have to provide the pathname of the file, or a SplFileInfo
instance, and a request.
<?php namespace ICanBoogie\HTTP; /* @var $request Request */ $response = new FileResponse("/absolute/path/to/my/file", $request); $response();
The OPTION_FILENAME
option may be used to force downloading. Of course, utf-8 strings are
supported:
<?php namespace ICanBoogie\HTTP; /* @var $request Request */ $response = new FileResponse("/absolute/path/to/my/file", $request, [ FileResponse::OPTION_FILENAME => "Vidéo d'un été à la mer.mp4" ]); $response();
The following options are also available:
-
OPTION_ETAG
: Specifies theETag
header field of the response. If it is not defined, the SHA-384 of the file is used instead. -
OPTION_EXPIRES
: Specifies the expiration date as aDateTime
instance or a relative date such as+3 month
, which maps to theExpires
header field. Themax-age
directive of theCache-Control
header field is computed from the current time. If it is not definedDEFAULT_EXPIRES
is used instead ("+1 month"). -
OPTION_MIME
: Specifies the MIME of the file, which maps to theContent-Type
header field. If it is not defined the MIME is guessed usingfinfo::file()
.
The following properties are available:
-
modified_time
: Returns the last modified timestamp of the file. -
is_modified
: Whether the file has been modified since the last response. The value is computed using the request header fieldsIf-None-Match
andIf-Modified-Since
, and the propertiesmodified_time
andetag
.
Headers
Here is an overview of header usage. Details are available in the Headers documentation.
<?php namespace ICanBoogie\HTTP; $headers = new Headers(); $headers->cache_control = 'public, max-age=3600, no-transform'; $headers->cache_control->no_transform = false; $headers->cache_control->max_age = 7200; echo $headers->cache_control; // public, max-age=7200 $headers->content_type = 'text/plain'; $headers->content_type->type = 'application/xml'; $headers->content_type->charset = 'utf-8'; echo $headers->content_type; // application/xml; charset=utf-8 $headers->content_length = 123; $headers->content_disposition->type = 'attachment'; $headers->content_disposition->filename = 'été.jpg'; echo $headers->content_disposition; // attachment; filename="ete.jpg"; filename*=UTF-8''%C3%A9t%C3%A9.jpg $headers->etag = "ABC123"; $headers->date = 'now'; $headers->expires = '+1 hour'; $headers->if_modified_since = '-1 hour'; $headers->if_unmodified_since = '-1 hour'; $headers->last_modified = '2022-01-01'; $headers->retry_after = '+1 hour'; $headers->retry_after = 123; $headers->location = 'to/the/moon'; $headers['X-My-Header'] = 'Some value'; echo $headers['X-My-Header']; // 'Some value';
Exceptions
The HTTP package defines the following exceptions:
- ClientError: thrown when a client error occurs.
- NotFound: thrown when a resource is not found. For instance, this exception is thrown by the dispatcher when it fails to resolve a request into a response.
- AuthenticationRequired: thrown when the authentication of the client is required. Implements SecurityError.
- PermissionRequired: thrown when the client lacks a required permission. Implements SecurityError.
- MethodNotAllowed: thrown when an HTTP method is not supported.
- ServerError: throw when a server error occurs.
- ServiceUnavailable: thrown when a server is currently unavailable (because it is overloaded or down for maintenance).
- ForceRedirect: thrown when a redirect is absolutely required.
- StatusCodeNotValid: thrown when an HTTP status code is not valid.
Exceptions defined by the package implement the ICanBoogie\HTTP\Exception
interface.
Using this interface, one can easily catch HTTP-related exceptions:
<?php try { // … } catch (\ICanBoogie\HTTP\Exception $e) { // HTTP exception types } catch (\Exception $e) { // Other exception types }
Continuous Integration
The project is continuously tested by GitHub actions.
Code of Conduct
This project adheres to a Contributor Code of Conduct. By participating in this project and its community, you're expected to uphold this code.
Contributing
See CONTRIBUTING for details.
License
icanboogie/http is released under the BSD-3-Clause.