
PSR-7 in the examples
- Transfer
- Tutorial
The PSR-7 standard has been successfully completed. The finishing touches were added this week. And now version 0.6.0 of the http-message package is ready to use. Try to follow this standard in your applications.
I still hear remarks about both too simplistic and too complicated presentation. That is why this post was written - to demonstrate the use of published recommendations and to show at the same time their simplicity and the completeness and reliability that they provide.
To begin with, I will briefly talk about what regulates this standard.
HTTP is a fairly simple protocol. That is why it has been used successfully for many years. Messages in it have the following structure:
Headers are key – value pairs. Keys are case sensitive. Values are strings. One title can have several meanings. In this case, the values are usually represented by a comma separated list.
The message body is a string. Although it is usually treated by the server and client as a stream to reduce memory consumption and reduce processing load. This is extremely important when large datasets are transferred, and especially when files are transferred. For example, PHP “out of the box” represents the incoming request body as a php: // input stream and uses the output buffer (also, formally, the stream) to return the response.
A message line is a place that distinguishes an HTTP request from a response.
The message line at the request (hereinafter referred to as the request line) has the following format:
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 (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 transfers the data for authorization only with the first request to connect to the HTTP server. Then, the relative (or absolute) path to the resource (URI without authorization data) is sent as the request target. Thus, authorization data is transmitted only for a connection request (CONNECT method), which is usually performed when working with a proxy server. The "*" character 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:
The “scheme” in the http request will be either http or https. “Path” is also an understandable part for everyone. But what is “authority”?
“Authority” always contains a host, which can be a domain name or an IP address. The port is optional and is necessary only if it is not standard for a given circuit (or if the circuit is unknown). User information is presented as
where password is optional. In fact, in existing specifications, it is recommended that you do not use a password in the URI at all. It is better to force a password from the client.
A query string is a set of key-value pairs separated by ampersands:
Depending on the implementation language, it can also model lists or arrays:
PHP converts this string into a two-dimensional array:
So, if the flexibility in forming the goal 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:
“VERSION”, as mentioned earlier, is usually 1.0, or more often 1.1. “Status” is a number from 100 to 599 inclusive; “Reason” is a status explanation standard for each status.
So this was a quick overview of HTTP messages. Let's now see how the PSR-7 models them.
The names of the message headers are initially case insensitive. Unfortunately, most languages and libraries bring them to the same register. As an example, PHP stores them in the $ _SERVER array in upper case, with the HTTP_ prefix, and substituting _for - (this is in accordance with the Common Gateway Interface (CGI) specification).
PSR-7 simplifies access to headers by providing an object-oriented layer above them
All of 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:
When the structure is defined, users will know exactly what they will receive and can process the 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 the 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.
If you just need to update the value, you can simply override it:
If you want to add another value to an existing header, you can do the following:
Or even remove the title:
As stated 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 are going to use all available memory on the current process. Most implementations of the HTTP messages that I looked at forget about this or try to change the behavior after the fact (yes, even ZF2 sins this!). If you want to read more about the benefits of this approach, read Michael Dowling's article. He wrote on his blog about the use of streams in PSR-7 last summer.
So, the message body in the PSR-7 is modeled as a stream .
“But it's too complicated for 80% of cases where you can get by with the lines!” - the most frequent argument among those who criticize this implementation of message body processing. Ok, let's look at the following:
This example, and all subsequent examples of working with HTTP messages in this post, will use the phly / http library, written by me, which reflects the development of PSR-7. In this case, Stream implements StreamableInterface.
Essentially, you get a thin, object-oriented interface for interacting with the message body, which allows you to add information to it, read it and much more. Want to change the message? Create a new message body:
My opinion is that despite the fact that the presentation of the message body as a stream seems complicated, in fact, the implementation and use are quite simple and understandable.
The benefit of using StreamableInterface in the 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 calling the read () or getContents () method returns the content of the message (Drupal, in particular, uses this template). Or Iterator, an implementation of which uses any Traversable to return or merge content. The point is that such an interface gives you wide scope for implementing a variety of templates for working with the message body. And it doesn’t just limit it to lines 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 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 become large enough (in this case they are written to a temporary file on disk). The method can be quite effective.
So far, we have been looking at features common to any message. Now I am going to focus on the answers in particular.
The answer has a status code and an explanatory phrase:
It is easy to remember. Now, what if we ourselves form the answer?
An 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 ():
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.
The queries contain the following:
The latter is a bit difficult to model. Probably in 99% of cases, we will see a standard URI as the request target. But this does not negate the fact that it is necessary to provide for other types of request goals. 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 the URI information in the request to establish a connection with the HTTP client).
Let's get the method and URI from the request:
$ uri in this case will be an instance of UriInterface, and will allow you to use a URI:
Just like HTTP messages, URIs are represented as value objects, and changing any part of the URI changes its value, mutating methods return a new instance:
Since changing a URI means creating a new instance, then if you want the change to be reflected in your request, you need to report the changes to the request object; and, as with any message, if you need to change a method or URI in a specific instance, you should use the following methods:
Server requests have slightly different tasks than standard HTTP request messages. Native to PHP Server API (SAPI) provides us with a set of usual functions for PHP developers:
Arguments from the query string, data from the request body and cookies can be obtained from different parts of the request, but it would be convenient if this were 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 base RequestInterface, which describes functions for working with similar data:
Imagine that you are writing an API and want to accept requests in JSON format; doing this might look like this:
The example above demonstrates several features. First, it shows the extraction of the header from the request, and the branching logic based on that header. Secondly, it shows the formation of the request object in the event of an error (the emit () function is hypothetical, it takes the request object and gives the headers and body of the request). Finally, the example demonstrates getting the request body, deserializing it, and embedding it again in the request.
Another feature of server requests is attributes. They are designed to store values that are obtained from the current request. A common use case is storing routing results (dividing the URI into key / value pairs).
Working with attributes consists of the following methods:
As an example, let's look at Aura Router with our request instance:
Экземпляр запроса в данном случае используется для упорядочивания данных и передачи маршруту. Затем результаты маршрутизации используются для создания экземпляра ответа.
Теперь после быстрой экскурсии по различным компонентам PSR-7, давайте вернёмся к конкретным примерам использования.
Для меня главным создателем стандарта PSR-7 является Майкл Доулинг, автор популярного HTTP клиента Guzzle. Поэтому совершенно очевидно, что PSR-7 принесёт улучшения HTTP клиентам. Давайте обсудим как.
Во-первых, это означает, что разработчики будут иметь уникальный интерфейс сообщений для выполнения запросов; они могут отправлять объект запроса по стандарту PSR-7 клиенту и получать обратно объект ответа по тому же стандарту.
Due to the fact that messages and URIs are modeled as value objects, this also means that developers can create basic query instances and URIs and create separate requests and URIs from them:
What PSR-7 offers is a standard way of interacting with requests that you send by the client and responses that you receive. By implementing value objects, we open up the possibility of some interesting use cases with the aim of simplifying the “reset request” template - changing a request always results in a new instance, allowing us to have a base instance with a known state that we can always expand.
I will not dwell on this for a long time, because already did this in an article . The main idea, in short, is as follows:
The function accepts two HTTP messages, and performs some conversions 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 me this term in the mail!):
In the lambda link you make one in the other:
And finally, there is a method promoted by Rack and WSGI, in which each link is an object and goes to the exit:
The use of the intermediate link is that it implements the relationship between the request and the response and follows the standard: a predictable pattern with predictable behavior. This is a great way to write web components that can be reused.
One thing that frameworks have been offering for many years is ... an abstraction layer over HTTP messages. The goal of PSR-7 is to provide a common set of interfaces for frameworks so that they 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 intend to use in the framework:
This is just the intermediate link we described above; the one and only difference is that it uses HTTP implementation specific for this framework. What if it instead supports PSR-7?
Most implementations of HTTP messages in frameworks are designed in such a way that you can change the state of a message at any time. Sometimes this may not be entirely true, especially if we assume 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 messages. This makes the implementation more explicit and easy to track in your code (both 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 that they want to pass on to their passing customers:
The above code clearly shows that we are changing the state of the application.
Using value objects makes it easier for one particular practice: subquery allocation or the implementation of the Hierarchical MVC (HMVC). In this case, you can create new queries based on the current one, without informing the application about it, and being sure that the state of the application will not change.
In general, for most frameworks, using PSR-7 messages will translate into portable abstraction over HTTP messages. This will make it possible to implement a universal intermediate link. Adapting messages, however, will require minor changes. Developers need to update the code responsible for monitoring the status of the application.
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).
The PSR-7 specification is not yet complete. But what I outlined above will not undergo significant changes without a vote. You can familiarize yourself with the specification in more detail at the link:
I also recommend that you read the “explanatory note”, as it describes ideas, 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 through composer . These are 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 a PSR-7 based intermediate, I suggest the following options:
I see the future of development with the PSR-7. And I believe that it will give rise to a completely new generation of PHP applications.
I still hear remarks about both too simplistic and too complicated presentation. That is why this post was written - to demonstrate the use of published recommendations and to show at the same time their simplicity and the completeness and reliability that they provide.
To begin with, I will briefly talk about what regulates this standard.
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:
Header: value
Another-Header: value
Message body
Headers are key – value pairs. Keys are case sensitive. Values are strings. One title can have several meanings. In this case, the values are usually represented by a comma separated list.
The message body is a string. Although it is usually treated by the server and client as a stream to reduce memory consumption and reduce processing load. This is extremely important when large datasets are transferred, and especially when files are transferred. For example, PHP “out of the box” represents the incoming request body as a php: // input stream and uses the output buffer (also, formally, the stream) to return the response.
A message line is a place that distinguishes an HTTP request from a response.
The message line at the request (hereinafter referred to as the request line) 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 (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:
- In standard form, which is the path and query string (if provided), i.e. URI (Universal Resource Identifier).
- In absolute form, which is an absolute URI.
- In the authorization form, which is part of the URI necessary for authorization (user information, if provided; host; and port, if not standard).
- In the form *, that is, a string consisting of the character "*".
Usually, the client transfers the data for authorization only with the first request to connect to the HTTP server. Then, the relative (or absolute) path to the resource (URI without authorization data) is sent as the request target. Thus, authorization data is transmitted only for a connection request (CONNECT method), which is usually performed when working with a proxy server. The "*" character 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:
://[/][?]
The “scheme” in the http request will be either http or https. “Path” is also an understandable 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 is necessary only if it is not standard for a given circuit (or if the circuit is unknown). User information is presented as
user[:pass]
where password is optional. In fact, in existing specifications, it is recommended that you do not use a password in the URI at all. It is better to force a password from the client.
A 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 into a two-dimensional array:
[
'sort' => [
'ASC',
'date'
],
'filter' => [
'product' => 'name'
],
]
So, if the flexibility in forming the goal 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 [ ]
“VERSION”, as mentioned earlier, is usually 1.0, or more often 1.1. “Status” is a number from 100 to 599 inclusive; “Reason” is a status explanation 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
The names of the message headers are initially case insensitive. Unfortunately, most languages and libraries bring them to the same register. As an example, PHP stores them in the $ _SERVER array in upper case, with the HTTP_ prefix, and substituting _for - (this is in accordance 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 of 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 the 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 the 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 existing header, you can do the following:
$message = $message->withAddedHeader('X-Foo', 'bar');
Or even remove the title:
$message = $message->withoutHeader('X-Foo');
Message body
As stated 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 are going to use all available memory on the current process. Most implementations of the HTTP messages that I looked at forget about this or try to change the behavior after the fact (yes, even ZF2 sins this!). If you want to read more about the benefits of this approach, read Michael Dowling's article. He wrote on his blog about the use of streams in PSR-7 last summer.
So, the message body in the PSR-7 is modeled as a stream .
“But it's too complicated for 80% of cases where you can get by with the lines!” - the most frequent argument among those who criticize this implementation of message body processing. Ok, let's look at 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, which reflects the development of PSR-7. In this case, Stream implements StreamableInterface.
Essentially, you get a thin, object-oriented interface for interacting with the message body, which allows you to add information to it, read it and much 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 complicated, in fact, the implementation and use are quite simple and understandable.
The benefit of using StreamableInterface in the 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 calling the read () or getContents () method returns the content of the message (Drupal, in particular, uses this template). Or Iterator, an implementation of which uses any Traversable to return or merge content. The point is that such an interface gives you wide scope for implementing a variety of templates for working with the message body. And it doesn’t just limit it to lines 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 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 become large enough (in this case they are written to a temporary file on disk). The method can be quite effective.
The answers
So far, we have been looking at features common to any message. Now I am going to focus on the answers in particular.
The answer has a status code and an explanatory phrase:
$status = $response->getStatusCode();
$reason = $response->getReasonPhrase();
It is easy to remember. Now, what if we ourselves form the answer?
An 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.
Inquiries
The queries contain the following:
- Method.
- URI / purpose of the request.
The latter is a bit difficult to model. Probably in 99% of cases, we will see a standard URI as the request target. But this does not negate the fact that it is necessary to provide for other types of request goals. Thus, the query interface does the following:
- Creates an instance of a UriInterface that models the request URI.
- Provides two methods for the request target: getRequestTarget (), which returns the request target and evaluates it if not provided (using the proposed URI to return to the original form or to return "/" if the URI is not provided or does not contain a path); and withRequestTarget () to create a new instance for the specific purpose of the request.
This will further allow us to address requests with an arbitrary purpose of the request, when necessary (for example, using the URI information 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 will allow you to use a 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]
Just like HTTP messages, URIs are represented as value objects, and changing any part of the URI changes its value, mutating methods return a new instance:
$uri = $uri
->withScheme('http')
->withHost('example.com')
->withPath('/foo/bar')
->withQuery('?baz=bat');
Since changing a URI means creating a new instance, then if you want the change to be reflected in your request, you need to report the changes to the request object; and, as with any message, if you need to change a method or URI in a specific 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 PHP Server API (SAPI) provides us with a set of usual functions for PHP developers:
- Deserialization of arguments in the query string ($ _GET).
- Deserialization of encoded data transmitted by the POST method ($ _POST).
- Cookie deserialization ($ _COOKIE).
- Display and processing of downloaded files ($ _FILES).
- Encapsulation of CGI / SAPI parameters ($ _SERVER).
Arguments from the query string, data from the request body and cookies can be obtained from different parts of the request, but it would be convenient if this were implemented for us. There are times when we may need to work with these values:
- For the API, the data can be in XML or JSON format and can be transmitted not only by the POST method. That is, we must decrypt the data and then embed it in the request again.
- Many frameworks now encrypt cookies, and this means that they need to be decrypted and embedded in the request.
So, PSR-7 provides a special interface, ServerRequestInterface, which extends the base RequestInterface, which describes functions for 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 request, and the branching logic based on that header. Secondly, it shows the formation of the request object in the event of an error (the emit () function is hypothetical, it takes the request object and gives the headers and body of the request). Finally, the example demonstrates getting the request body, deserializing it, and embedding it again in the request.
Attributes
Another feature of server requests is attributes. They are designed to store values that are obtained from the current request. A common use case is storing routing results (dividing the URI into key / value pairs).
Working with attributes consists of the following methods:
- getAttribute ($ name, $ default = null) to get a specific attribute and return the default value if the attribute is not found.
- getAttributes () retrieves all attributes.
- withAttribute ($ name, $ value) to return a new instance of ServerRequestInterface that contains this attribute.
- withoutAttribute (($ name) to return an instance of ServerRequestInterface without the specified attribute.
As an example, let's look at Aura Router with our request 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);
}
Экземпляр запроса в данном случае используется для упорядочивания данных и передачи маршруту. Затем результаты маршрутизации используются для создания экземпляра ответа.
Варианты использования
Теперь после быстрой экскурсии по различным компонентам PSR-7, давайте вернёмся к конкретным примерам использования.
Клиенты
Для меня главным создателем стандарта PSR-7 является Майкл Доулинг, автор популярного HTTP клиента Guzzle. Поэтому совершенно очевидно, что PSR-7 принесёт улучшения HTTP клиентам. Давайте обсудим как.
Во-первых, это означает, что разработчики будут иметь уникальный интерфейс сообщений для выполнения запросов; они могут отправлять объект запроса по стандарту PSR-7 клиенту и получать обратно объект ответа по тому же стандарту.
$response = $client->send($request);
Due to the fact that messages and URIs are modeled as value objects, this also means that developers can create basic query 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 PSR-7 offers is a standard way of interacting with requests that you send by the client and responses that you receive. By implementing value objects, we open up the possibility of some interesting use cases with the aim of simplifying the “reset request” template - changing a request always results in a new instance, allowing us to have a base instance with a known state that we can always expand.
Connecting link
I will not dwell on this for a long time, because already did this in an article . The main idea, in short, is as follows:
function (
ServerRequestInterface $request,
ResponseInterface $response,
callable $next = null
) {
}
The function accepts two HTTP messages, and performs some conversions 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 me this term 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);
And finally, there is a method promoted by Rack and WSGI, in which each link is an object and goes 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 the intermediate link is that it implements the relationship between the request and the response and follows the standard: a predictable pattern with predictable behavior. This is a great way to write web components that can be reused.
Frameworks
One thing that frameworks have been offering for many years is ... an abstraction layer over HTTP messages. The goal of PSR-7 is to provide a common set of interfaces for frameworks so that they 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 intend to use in the framework:
use Zend\Http\RequestInterface;
use Zend\Http\ResponseInterface;
interface DispatchableInterface
{
public function dispatch(
RequestInterface $request,
ResponseInterface $response
);
}
This is just the intermediate link we described above; the one and only difference is that it uses HTTP implementation specific for this framework. What if it instead supports PSR-7?
Most implementations of HTTP messages in frameworks are designed in such a way that you can change the state of a message at any time. Sometimes this may not be entirely true, especially if we assume 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 messages. This makes the implementation more explicit and easy to track in your code (both 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 that they want to pass on to their passing customers:
// Внутри контроллера
$request = $request->withAttribute('foo', 'bar');
$response = $response->withHeader('X-Foo', 'bar');
$event = $this->getEvent();
$event->setRequest($request)
->setResponse($response);
The above code clearly shows that we are changing the state of the application.
Using value objects makes it easier for one particular practice: subquery allocation or the implementation of the Hierarchical MVC (HMVC). In this case, you can create new queries based on the current one, without informing the application about it, and being sure that the state of the application will not change.
In general, for most frameworks, using PSR-7 messages will translate into portable abstraction over HTTP messages. This will make it possible to implement a universal intermediate link. Adapting messages, however, will require minor changes. Developers need to update the code responsible for monitoring the status of the application.
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).
The PSR-7 specification is not yet complete. But what I outlined above will not undergo significant changes without a vote. You can familiarize yourself with the specification in more detail at the link:
I also recommend that you read the “explanatory note”, as it describes ideas, 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 through composer . These are 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 a PSR-7 based intermediate, I suggest the following options:
- phly / conduit , a Connect library ported from Sencha that uses phly / http and psr / http-message at its core.
- Stacker , StackPHP is a similar implementation written by Larry Garfield.
I see the future of development with the PSR-7. And I believe that it will give rise to a completely new generation of PHP applications.