jasny / http-message
PSR-7 implementation for handling HTTP requests
Requires
- php: >=5.6.0
- jasny/php-functions: ^2.2|^3.0|^4.0
- psr/http-message: ^1.0
Requires (Dev)
- jasny/php-code-quality: 2.1.*
- mikey179/vfsstream: ^1.6
Provides
- psr/http-message-implementation: ~1.0.0
- dev-master
- v2.x-dev
- v1.3.8
- v1.3.7
- v1.3.6
- v1.3.5
- v1.3.4
- v1.3.3
- v1.3.2
- v1.3.1
- v1.3.0
- v1.2.3
- v1.2.2
- v1.2.1
- v1.2.0
- v1.1.0
- v1.0.5
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- dev-integration-tests
- dev-force-stale
- dev-reparse-on-null
- dev-global-environment-interface
- dev-scrutinizer-patch-2
- dev-fix-ungroup-uploaded-files-uri
- dev-fix-revive-response
- dev-parsedbody-charset
- dev-global-env-fix
- dev-interface-naming
- dev-stream-eof
- dev-emit
- dev-headers-fix
- dev-response
- dev-globalstream
- dev-scrutinizer-patch-1
This package is auto-updated.
Last update: 2022-06-26 13:47:08 UTC
README
This library provides an abstraction around PHPs various superglobals as well as controlling the HTTP response. This practice helps reduce coupling to the superglobals by consumers, and encourages and promotes the ability to test request consumers.
The library only implements those PSR-7 interfaces focussed on handling a received HTTP request. If you want to send HTTP request to other webservices, I recommend using Guzzle.
Why this library?
Jasny HTTP Message is a no-nonsence implementation, that can be used with any framework or library.
The focus of the library is to behave as expected, without unwanted and unexpected side effects. A good example of this is how parsing the body is implemented.
Using the library in it's basic form is kept as simple as possible. You only to deal with a subset of all available classes, unless you need to customize.
When using PSR-7, outputing directly using echo
and header()
isn't permitted. Instead you need to use the Response
object. Using superglobals like $_GET
and $_POST
also won't work, instead you need to use the ServerRequest
object.
If you, your team or your project isn't ready for this paradigm shift, this library allows you to ease into using PSR-7.
It can be used as an abstraction layer over the normal input/output methods and variables like echo
, header()
,
$_GET
, $_POST
, etc.
Installation
composer require jasny/http-message
Documentation
The library implements the following PSR-7 interfaces
ServerRequest
implementsPsr\Http\Message\ServerRequestInterface
Response
implementsPsr\Http\Message\ResponseInterface
Stream
implementsPsr\Http\Message\StreamInterface
Uri
implementsPsr\Http\Message\UriInterface
it defines one interface
ServerRequest
The ServerRequest
class represents an HTTP request as received by the webserver and processed by PHP.
For the full documentation about the ServerRequest
, please see
PSR-7 RequestInterface
and
PSR-7 ServerRequestInterface
.
To create a ServerRequest
object with the $_SERVER
, $_COOKIE
, $_GET
, $_POST
and $_FILES
superglobals and
with php://input
as input stream, use the withGlobalEnvironment()
method.
$request = (new Jasny\HttpMessage\ServerRequest())->withGlobalEnvironment();
Binding to global environment
By using withGlobalEnvironment(true)
, the ServerRequest
links the superglobals by reference. If you modify these
variables, the changes will be reflected in the ServerRequest
object. Vise versa, using withQueryParams()
will
change $_GET
, withServerParams
changes $_SERVER
, etc.
use Jasny\HttpMessage\ServerRequest; // $_GET is not affected $requestByVal = (new ServerRequest())->withGlobalEnvironment(); $requestByVal = $request->withQueryParams(['foo' => 1]); var_dump($_GET); // array(0) { } // $_GET is affected $requestByRef = (new ServerRequest())->withGlobalEnvironment(true); $requestByRef = $request->withQueryParams(['foo' => 1]); var_dump($_GET); // array(1) { ["foo"]=> int(1) }
Parsed body
The getParsedBody()
method can do a number of things.
If withParsedBody($data)
has been called explicitly, the provided data will always be returned regardless of headers
or other request properties.
If $_POST
was copied from the global environment and the content type is multipart/form-data
or
application/x-www-form-urlencoded
, than the post data is used.
If the request has body content and the content-type is application/json
, application/xml
or text/xml
than the
body content is parsed. For XML this will result in a SimpleXmlElement
.
The body is also parsed for application/x-www-form-urlencoded
if $_POST
isn't copied. However multipart/form-data
is never manually parsed, so in that case if $_POST
isn't copied an exception is thrown.
In case the content type is unknown, getParsedBody()
will simply return null. If the body does have content, but no
content type header has been set, a warning is triggered.
If the headers or body content changes, the body will be reparsed upon calling getParsedBody()
. However this only
happends if the parsed body hasn't been explictly set using withParsedBody()
.
Response
The Response
class allows you to create the outgoing HTTP response.
For the full documentation about the Response
class, please see
PSR-7 ResponseInterface
.
By default a Response
object will stream to php://temp
and simply hold a list of all set headers.
$response = new Jasny\HttpMessage\Response();
Emit
The response object holds all the output, including headers and body content. To send it to the client (in other words
output it), use the emit()
method.
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\Response; $request = (new ServerRequest())->withGlobalEnvironment(); $response = $router->handle($request, new Response()); $response->emit();
The emit()
method will create an Emitter
object. If needed you can create your own class that implements
EmitterInterface
and pass it as $response->emit(new CustomEmitter())
.
The emitter can also be used directly without using the emit()
method of the response. This is also useful if you're
unsure if the router / middleware / controller will return a Jasny/HttpMessage/Response
or migth return some other
PSR-7 ResponseInterface
implementation.
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\Response; use Jasny\HttpMessage\Emitter; $request = (new ServerRequest())->withGlobalEnvironment(); $response = $router->handle($request, new Response()); $emitter = new Emitter(); $emitter->emit($response);
Binding to global environment
To create a Response
object which uses the header()
method and
with php://output
as output stream, use the withGlobalEnvironment(true)
method.
$request = (new Response())->withGlobalEnvironment(true); $request->withHeader('Content-Type', 'text/plain'); // Does `header("Content-Type: text/plain")` $request->getBody()->write('hello world'); // Outputs "hello world"
Uri
The Uri
class is meant to represent URIs according to RFC 3986. It allows you
to get and change any specific part of an uri.
For the full documentation about the Uri
class, please see
PSR-7 UriInterface
.
When creating an Uri you can pass the URL as string or pass the URL in parts as associative array. For the URL parts
see the parse_url
function.
The Jasny\HttpMessage\Uri
object only supports the http
and https
schemes.
$uri = new Jasny\HttpMessage\Uri("http://www.example.com/foo");
Stream
The Stream
class is a wrapper around php streams implementing the
PSR-7 StreamInterface
.
$input = new Jasny\HttpMessage\Stream(); $input->write(json_encode(['foo' => 'bar', 'color' => 'red']));
Creating a stream
By default it will create a stream using a php://temp
. You may pass a stream resource when creating a stream to use
a different kind of handle.
$handle = fopen('php://memory', 'r+'); $stream = new Jasny\HttpMessage\Stream($handle);
Alternatively you may use Stream::open($uri, $mode)
to create a stream with a specific handle.
$stream = Jasny\HttpMessage\Stream::open('php://memory', 'r+');
Cloning the stream
When cloning a stream, the handle is recreated. This means that for php://temp
and php://memory
, you'll get a stream
without any content. Clearing the body of a response can typically be done by cloning the stream.
$newResponse = $response->withBody(clone $response->getBody());
This behaviour is not specified in PSR-7 and cloning streams may not work with other PSR-7 implementations.
DerivedAttribute
You can set arbitrary attributes for a ServerRequest
using the withAttribute()
method. To get an attribute use the
getAttribute()
method.
An attribute can be set to any static value, or it can be derived from other values of a ServerRequest
object, like a
header or query parameter. The easiest way to create a derived attribute is to use a
Closure
.
use Jasny\HttpMessage\ServerRequest; $request = (new ServerRequest())->withAttribute('accept_json', function(ServerRequest $request) { $accept = $request->getHeaderLine('Accept'); return strpos($accept, 'application/json') !== false || strpos($accept, '*/*') !== false; });
You can create more sophisticated derived attributes by creating a class that implements the DerivedAttributeInterface
interface. When implementing that interface, implement __invoke(ServerRequest $request)
.
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\DerivedAttributeInterface; class DetectBot implements DerivedAttributeInterface { public static $identifiers = [ 'google' => 'googlebot', 'yahoo' => 'yahoobot', 'magpie' => 'magpie-crawler' ]; protected $detect = []; public function __construct(array $detect) { $this->detect = $detect; } public function __invoke(ServerRequest $request) { $useragent = $request->getHeaderLine('User-Agent'); $detected = false; foreach ($this->detect as $bot) { $identifier = static::$identifiers[$bot]; $detected = $detected || stripos($useragent, $bot) !== false; } return $detected; } } $request = (new ServerRequest()) ->withAttribute('is_friendly_bot', new DetectBot(['google', 'yahoo'])) ->withAttribute('is_annoying_bot', new DetectBot(['magpie']));
Remember that a ServerRequest
method is immutability, so withAttribute()
will create a new object.
This library comes with a number of derived attributes, which may be used.
ClientIp
Get the client IP. By default only $_SERVER['REMOTE_ADDR']
is returned.
use Jasny\HttpMessage\ServerRequest; $request = (new ServerRequest())->withGlobalEnvironment(); $request->getAttribute('client_ip'); // always returns $_SERVER['REMOTE_ADDR']
You can specificy an IP or CIDR address for trusted proxies. When used, addresses send as HTTP header through
X-Forwarded-For
, Client-Ip
or Forwarded
are taken into consideration.
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\DerivedAttribute\ClientIp; $request = (new ServerRequest()) ->withGlobalEnvironment() ->withAttribute('client_ip', new ClientIp(['trusted_proxy => '10.0.0.0/24']); $ip = $request->getAttribute('client_ip'); // for a request from the internal network, use the `X-Forwarded-For` header
Note: If more than one of these headers are set, a RuntimeException
is thrown. This prevents a user injecting a
Client-Ip
address to fake his ip, where your proxy is setting the X-Forwarded-For
header. To make sure this
exception doesn't occur, remove all unexpected forward headers.
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\DerivedAttribute\ClientIp; $request = (new ServerRequest()) ->withGlobalEnvironment() ->withoutHeader('Client-Ip') ->withoutHeader('Forwarded') ->withAttribute('client_ip', new ClientIp(['trusted_proxy' => '10.0.0.0/24']);
IsXhr
Test is the request with made using AJAX.
All modern browsers set the X-Requested-With
header to XMLHttpRequest
when making an AJAX request. This derived
attribute simply checks that header.
use Jasny\HttpMessage\ServerRequest; $request = (new ServerRequest())->withGlobalEnvironment(); $isXhr = $request->getAttribute('is_xhr'); // true or false
LocalReferer
Return the path of the Referer
header, but only if the referer's scheme, host and port matches request's scheme, host
and port.
use Jasny\HttpMessage\ServerRequest; $request = (new ServerRequest())->withGlobalEnvironment(); $back = $request->getAttribute('local_referer') ?: '/'; // Referer Uri path, defaults to `/` for no or external referer
It is possible to disable the check on scheme and/or port if needed.
use Jasny\HttpMessage\ServerRequest; use Jasny\HttpMessage\DerivedAttribute\LocalReferer; $request = (new ServerRequest()) ->withGlobalEnvironment() ->withAttribute('local_referer', new LocalReferer(['checkScheme' => false, 'checkPort' => false]));
Testing
When testing code that is fully PSR-7 compatible, create a ServerRequest
with specific headers, parameters and data
and a default Response
.
$request = (new ServerRequest()) ->withMethod('GET') ->withUri('/foo') ->withQueryParams(['page' => 1]);
PSR-7 compatible code MUST NOT access superglobals directly and also MUST NOT output headers and data directly.
Testing legacy code
This library allows you to test code that isn't fully PSR-7 compatible. It might access the superglobals directly and/or
output using echo
and headers()
.
// Start output buffering, so the output isn't send directly ob_start(); // Create server request that is bound to the global enviroment. $baseRequest = (new ServerRequest())->withGlobalEnvironment(true); // Modifying the bound request, modifies the superglobals. $request = $baseRequest ->withServerParams(['REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/foo']) ->withQueryParams(['page' => 1]); // Create response that is bound to the global enviroment. $baseResponse = (new Response())->withGlobalEnvironment(true); // Some PSR-7 compatible router handles the request. The code uses `header` and `echo` to output. $router->handle($request, $baseResponse); // Disconnect the global environment, copy the data and headers $response = $response->withoutGlobalEnvironment(); // Refiving the base request and response, restores the global environment. Also clean the output buffer. $baseRequest = $baseRequest->revive(); $baseResponse = $baseResponse->revive()->withBody(new OutputBufferStream()); // Assert response ... // Ready for next request :)
Stale and revive
Using this technique allows you to start using PSR-7 without having to rewrite your whole code base. Instead you can refactor your code bit by bit.
When doing $copy = $object->with..()
, the $copy
is now bound to the global environment, while $object
has turned
stale.
Stale means that the object was bound to the global environment, but no longer reflects the current state. The state of the global environment has been copied to the object (think of it as frozen in time). Changes in the global environment do not affect stale objects. It is not possible to modify a stale object.
Note that the Stream
is a resource that is not cloned by with...
methods. This is also true when the Response
is
bound to the output stream. So outputting does affect stale response objects.
In some cases, you do want to continue with a stale object. For example when catching an error in middleware. In that
case you need to call revive()
. This methods restores the global environment to the state of the stale object.
function errorHandlerMiddleware(ServerRequestInterface $request, ResponseInterface $response, $next) { try { $newResponse = $next($request, $response); } catch (Throwable $error) { // If the next middleware or controller has done something like set the response status, the response is stale. if ($request instanceof Jasny\HttpMessage\ServerRequest) { $request = $request->revive(); } if ($response instanceof Jasny\HttpMessage\Response) { $response = $response->revive(); } $newResponse = handleError($request, $response, $error); } return $newResponse; }
Codeception
If you're using Codeception, the Jasny Codeception module migt be interresting. It uses the Jasny Router to handle PSR-7 server requests.