📜 ⬆️ ⬇️

PSR-7 in examples

The PSR-7 standard has been successfully completed. The final touches were added this week. And now version 0.6.0 of the http-message package is ready for use. Try to follow this standard in your applications.

I still hear remarks both about too simplified, and about too complicated presentation. That is why this post was written - to demonstrate the use of published recommendations and to show both their simplicity, and the completeness and reliability that they provide.

To begin with, I will briefly talk about what this standard regulates.

HTTP messages


HTTP is a fairly simple protocol. That is why it has been used successfully for many years. Messages in it have the following structure:
<message line> Header: value Another-Header: value Message body 

Headers are key – value pairs. Keys are case sensitive. Values ​​are strings. A single header can have multiple meanings. In this case, the values ​​are usually represented by a comma-separated list.
The message body is a string. Although it is usually handled by the server and client as a stream, to reduce memory consumption and reduce the processing load. This is extremely important when large data sets are transferred, and especially when files are transferred. For example, PHP "out of the box" represents the incoming request body as a stream php: // input and uses the output buffer (also, formally, a stream) to return a response.
A message line is a place that distinguishes an HTTP request from a response.
The message string of the request (hereinafter referred to as the query string) has the following format:
 METHOD request-target HTTP/VERSION 

Where the method (“METHOD”) determines the type of request: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, and so on, the protocol version (“VERSION”) is usually 1.0 or 1.1 (more often 1.1 for modern web clients). And on the purpose of the request (“request-target”), we dwell in more detail.
The purpose of the request can be represented as follows:

Usually, the client sends the data for the HTTP authorization only with the first request to connect to the HTTP server. Then, just a relative (or absolute) path to the resource (URI without data for authorization) is sent as the target of the request. Thus, data for authorization is transmitted only for a connection request (CONNECT method), which is usually performed when working with a proxy server. The “*” symbol is used with the OPTIONS method to get general information about the web server.
In short, there are many uses for the purpose of a query.
Now, to completely confuse you, consider the URI. We see the following:
 <scheme>://<authority>[/<path>][?<query string>] 

The “scheme” in the http request will be either http or https. “Path” is also a clear part for everyone. But what is “authority”?
 [user-info@]host[:port] 

“Authority” always contains a host, which can be a domain name or an IP address. The port is optional and only necessary if it is not standard for this scheme (or if the scheme is unknown). User information is presented as
 user[:pass] 

where the password is optional. In fact, in existing specifications, it is recommended not to use a password in the URI at all. It is better to force a password from the client.
The query string is a set of key-value pairs separated by ampersands:
 ?foo=bar&baz&quz=1 

Depending on the implementation language, it can also model lists or arrays:
 ?sort[]=ASC&sort[]=date&filter[product]=name 

PHP converts this string to a two-dimensional array:
 [ 'sort' => [ 'ASC', 'date' ], 'filter' => [ 'product' => 'name' ], ] 

So, if the flexibility in shaping the purpose of the request would not be enough for us, the URI would provide its own.
Fortunately, HTTP server responses are simpler. The response line is as follows:
 HTTP/VERSION <status>[ <reason>] 

“VERSION”, as mentioned earlier, is usually 1.0 or, more often, 1.1. “Status” is a number from 100 to 599 inclusive; “Reason” - an explanation of the status, standard for each status.
So this was a quick overview of HTTP messages. Let's now see how the PSR-7 models them.
')

Message Headers


Message header names are case-insensitive. Unfortunately, most languages ​​and libraries bring them to one register. As an example, PHP stores them in the $ _SERVER array in uppercase, with the HTTP_ prefix, and substituting _for - (this is for compliance with the Common Gateway Interface (CGI) specification).
PSR-7 simplifies access to headers by providing an object-oriented layer above them.
 //  null,   : $header = $message->getHeader('Accept'); // ,   : if (! $message->hasHeader('Accept')) { } //     , //   : $values = $message->getHeaderLines('X-Foo'); 

All the above logic does not depend on how the header is specified; accept, ACCEEPT, or even aCCePt will be valid header names and return the same result.
PSR-7 assumes that parsing all the headers will return the structure as an array:
 /* Returns the following structure: [ 'Header' => [ 'value1' 'value2' ] ] */ foreach ($message->getAllHeaders() as $header => $values) { } 

When the structure is defined, users will know exactly what they will receive and can process headers in a convenient way for them - regardless of implementation.
But what if you want to add headers to the message — for example, to create a request and send it to the HTTP client?
Messages in PSR-7 are modeled as value objects ; this means that any change in state is, in fact, a different meaning. Thus, defining a new header will result in a new message object.
 $new = $message->withHeader('Location', 'http://example.com'); 

If you just need to update the value, you can simply override it:
 $message = $message->withHeader('Location', 'http://example.com'); 

If you want to add another value to an already existing header, you can do the following:
 $message = $message->withAddedHeader('X-Foo', 'bar'); 

Or even delete the title:
 $message = $message->withoutHeader('X-Foo'); 


Message body


As mentioned above, the message body is usually treated as a stream to improve performance. This is especially important when you transfer files using HTTP. Unless you intend to use all available memory for the current process. Most implementations of HTTP messages that I have reviewed forget about this or try to change the behavior in fact (yes, even ZF2 is sinning!). If you want to read more about the benefits of this approach, read the article by Michael Dowling. He wrote on his blog about using threads in the PSR-7 last summer.
So, the message body in PSR-7 is modeled as a stream .
“But this is too difficult for 80% of cases where you can get by with strings!” Is the most frequent argument among those that criticize this implementation of processing the message body. Well, let's consider the following:
 $body = new Stream('php://temp'); $body->write('Here is the content for my message!'); 

This example, and all subsequent examples of working with HTTP messages in this post will use the phly / http library written by me and reflecting the development of PSR-7. In this case, the Stream implements StreamableInterface.
Essentially, you get a subtle, object-oriented interface for interacting with the message body, which allows you to add information to it, read it, and more. Want to change the message? Create a new message body:
 $message = $message->withBody(new Stream('php://temp')); 

My opinion is that, despite the fact that the presentation of the message body as a stream seems to be difficult, in reality, implementation and use are quite simple and understandable.
The benefit of using a StreamableInterface in PSR-7 is that it provides flexibility that simplifies the implementation of various design patterns. For example, you can implement a “callback” function, which, when you call the read () method or getContents (), returns the content of the message (Drupal, in particular, uses this template). Or "Iterator", the implementation of which uses any "Traversable" to return or merge content. The point is that such an interface gives you a wide scope for implementing a variety of templates for working with the message body. And does not limit just strings or files.
StreamableInterface offers a set of methods that are most often used when working with the body of an HTTP message. This does not mean that it provides for absolutely everything, but covers a large set of potentially necessary operations.
Personally, I like to use php: // temp streams, because they are in memory until they are large enough (in this case, they are written to a temporary file on disk). The method can be quite effective.

Answers


So far, we have considered functions common to any message. Now I am going to dwell on the answers in particular.
The response has a status code and an explanatory phrase:
 $status = $response->getStatusCode(); $reason = $response->getReasonPhrase(); 

It's easy to remember. Now, what if we form the answer ourselves?
Explanatory phrase is considered optional (but at the same time standard for each status code). For it, the interface provides a response-specific mutator withStatus ():
 $response = $response->withStatus(418, "I'm a teapot"); 

Again, messages are modeled as value objects; changing any value will result in a new instance that must be tied to the response or request. But in most cases, you will simply reassign the current instance.

Requests


Requests include the following:

The latter is a bit tricky to model. Probably in 99% of cases, we will see the standard URI as the target of the request. But this does not negate the fact that it is necessary to envisage other types of query objectives. Thus, the query interface does the following:

This will further allow us to address requests with an arbitrary purpose of the request, when necessary (for example, using information about the URI in the request to establish a connection with the HTTP client).
Let's get the method and URI from the request:
 $method = $request->getMethod(); $uri = $request->getUri(); 

$ uri in this case will be an instance of UriInterface, and allow you to use the URI:
 //  URI: $scheme = $uri->getScheme(); $userInfo = $uri->getUserInfo(); $host = $uri->getHost(); $port = $uri->getPort(); $path = $uri->getPath(); $query = $uri->getQuery(); //   $authority = $uri->getAuthority(); // [user-info@]host[:port] 

In the same way as HTTP messages, URIs are represented as value objects, and changing any part of a URI changes its value, mutated methods return a new instance:
 $uri = $uri ->withScheme('http') ->withHost('example.com') ->withPath('/foo/bar') ->withQuery('?baz=bat'); 

Since changing the URI means creating a new instance, if you want the change to be reflected in your request, you need to report these changes to the request object; and, as with any message, if you need to change a method or a URI in a particular instance, you should use the following methods:
 $request = $request ->withMethod('POST') ->withUri($uri->withPath('/api/user')); 


Server requests


Server requests have slightly different tasks than standard HTTP request messages. Native to the PHP Server API (SAPI) provides us with a set of common, as for PHP developers, functions:

Arguments from the request string, data from the request body and cookies can be obtained from different parts of the request, but it would be convenient if this was implemented for us. There are times when we may need to work with these values:

So, PSR-7 provides a special interface, ServerRequestInterface, which extends the basic RequestInterface, which describes the functions of working with similar data:
 $query = $request->getQueryParams(); $body = $request->getBodyParams(); $cookies = $request->getCookieParams(); $files = $request->getFileParams(); $server = $request->getServerParams(); 

Imagine that you are writing an API and want to accept requests in JSON format; doing this might look like this:
 $accept = $request->getHeader('Accept'); if (! $accept || ! preg_match('#^application/([^+\s]+\+)?json#', $accept)) { $response->getBody()->write(json_encode([ 'status' => 405, 'detail' => 'This API can only provide JSON representations', ])); emit($response ->withStatus(405, 'Not Acceptable') ->withHeader('Content-Type', 'application/problem+json') ); exit(); } $body = (string) $request->getBody(); $request = $request ->withBodyParams(json_decode($body)); 

The example above demonstrates several features. First, it shows the extraction of the header from the query, and the branching of logic based on this header. Secondly, it shows the formation of the request object in the event of an error (the emit () function is hypothetical, it accepts the request object and gives the headers and the request body). Finally, the example demonstrates getting the request body, deserializing it, and inserting it back into the request.

Attributes


Another feature of server requests is attributes. They are designed to store values ​​that are obtained from the current query. A common use case is storing routing results (splitting a URI into key / value pairs).
Working with attributes consists of the following methods:

As an example, let's take Aura Router with our query instance:
 use Aura\Router\Generator; use Aura\Router\RouteCollection; use Aura\Router\RouteFactory; use Aura\Router\Router; $router = new Router( new RouteCollection(new RouteFactory()), new Generator() ); $path = $request->getUri()->getPath(); $route = $router->match($path, $request->getServerParams()); foreach ($route->params as $param => $value) { $request = $request->withAttribute($param, $value); } 

The query instance in this case is used to organize the data and transfer it to the route. Routing results are then used to create an instance of the response.

Use cases


Now, after a quick tour of the various components of the PSR-7, let's go back to the specific use cases.

Customers


For me, the main creator of the PSR-7 standard is Michael Dowling , the author of the popular HTTP client Guzzle . Therefore, it is clear that PSR-7 will bring HTTP improvements to clients. Let's discuss how.
Firstly, this means that developers will have a unique messaging interface for executing requests; they can send the request object using the PSR-7 standard to the client and receive the response object back using the same standard.
 $response = $client->send($request); 

Because messages and URIs are modeled as value objects, it also means that developers can create basic request instances and URIs and create separate requests and URIs from them:
 $baseUri = new Uri('https://api.example.com'); $baseRequest = (new Request()) ->withUri($baseUri) ->withHeader('Authorization', $apiToken); while ($action = $queue->dequeue()) { //   !   // URI      . $request = $baseRequest ->withMethod($action->method) ->withUri($baseUri->withPath($action->path)); //  URI! foreach ($action->headers as $header => $value) { //      ,    //     ! $request = $request->withHeader($header, $value); } $response = $client->send($request); $status = $response->getStatusCode(); if (! in_array($status, range(200, 204))) { //  ! break; } //  ! $data->enqueue(json_decode((string) $response->getBody())); } 

What the PSR-7 offers is a standard way to interact with the requests that you send to the client and the answers you receive. By implementing value objects, we open up the possibility of some interesting use cases with an eye to simplifying the “reset request” pattern — changing the request always results in a new instance, allowing us to have a basic instance with a known state, which we can always expand.

Connecting link


I will not dwell on this for a long time, because already did it in the article . The basic idea, briefly, is this:
 function ( ServerRequestInterface $request, ResponseInterface $response, callable $next = null ) { } 

The function accepts two HTTP messages, and performs some transformations with them (which may include delegation to the next available link). Typically, these links return a response object.
Another option that is often used is lambda expressions (thanks to Larry Garfield , who sent this term to me in the mail!):
 /* response = */ function (ServerRequestInterface $request) { /* ... */ return $response; } 

In the lambda link, you make one in the other:
 $inner = function (ServerRequestInterface $request) { /* ... */ return $response; }; $outer = function (ServerRequestInterface $request) use ($inner) { /* ... */ $response = $inner($request); /* ... */ return $response; }; $response = $outer($request); 

Finally, there is a method promoted by Rack and WSGI, in which each link is an object and passes to the exit:
 class Command { private $wrapped; public function __construct(callable $wrapped) { $this->wrapped = $wrapped; } public function __invoke( ServerRequestInterface $request, ResponseInterface $response ) { //   $new = $request->withAttribute('foo', 'bar'); //  ,   : $result = ($this->wrapped)($new, $response); // ,      if ($result instanceof ResponseInterface) { $response = $result; } //      return $reponse->withHeader('X-Foo', 'Bar'); } } 

The use of an intermediate link is that it implements the connection between request and response and follows the standard: a predictable pattern with predictable behavior. This is a great method of writing web components that can be reused.

Frameworks


One thing that frameworks have been offering for many years is ... a layer of abstraction over HTTP messages. The purpose of PSR-7 is to provide a common set of interfaces for frameworks so that the latter can use the same abstractions. This will allow developers to write reusable, framework-independent code, or at least that's what I would like to see!
Consider the Zend Framework 2. It defines the Zend \ Stdlib \ DispatchableInterface interface, which is the base for any controller that you are going to use in the framework:
 use Zend\Http\RequestInterface; use Zend\Http\ResponseInterface; interface DispatchableInterface { public function dispatch( RequestInterface $request, ResponseInterface $response ); } 

This is the intermediate we described above; the only difference is that it uses HTTP framework implementations specific to this framework. What if, instead, it will support PSR-7?
Most implementations of HTTP messages in frameworks are built in such a way that you can change the status of a message at any time.Sometimes this may not be entirely true, especially assuming that the state of the message may already be invalid. But this is perhaps the only drawback of this method.
PSR-7 messages are value objects. Thus, you do not need to inform the application in any way about any change in the messages. This makes the implementation more explicit and easy to track in your code (and step by step in the debugger, and using static code analyzers).
As an example, if ZF2 is updated in accordance with PSR-7, developers will not be required to inform MvcEvent of any changes they want to pass on to the following clients:
 //   $request = $request->withAttribute('foo', 'bar'); $response = $response->withHeader('X-Foo', 'bar'); $event = $this->getEvent(); $event->setRequest($request) ->setResponse($response); 

The code above clearly shows that we are changing the state of the application.
Using value objects makes one particular practice simpler: distributing subqueries or implementing Hierarchical MVC (HMVC). In this case, you can create new requests based on the current one without informing the application about it, and being sure that the status of the application will not change.
In general, for most frameworks, using PSR-7 messages will translate to portable abstraction over HTTP messages. This will make it possible to implement a universal intermediate. Adaptation of messages, however, will require minor changes. Developers need to update the code responsible for tracking application state.

Sources


I hope you see the advantage that the PSR-7 standard provides: a unified, complete abstraction over HTTP messages. Further, this abstraction can be used for each part of the HTTP transaction (where you send requests through the HTTP client, or parse the server request).
PSR-7 specification is not yet complete. But what I have outlined above will not undergo significant changes without a vote. More details on the specification can be on the link:

I also recommend that you read the “explanatory note”, as it describes the ideas, the developed solutions and the results of (endless) disputes over two years:

The latest updates are published in the psr / http-message package, which you can install via composer . This is always the latest updated offers.
I created a library, phly / http, which offers a concrete implementation of the proposed interfaces. It can also be installed via composer.
Finally, if you want to experiment with an intermediate based on PSR-7, I suggest the following options:

I see the future of development with PSR-7. And I believe that it will generate a completely new generation of PHP applications.

Source: https://habr.com/ru/post/250343/


All Articles