HTTP стандарты: PSR-7, PSR-15, PSR-17, PSR-18
PSR-7: HTTP Message Interface
PSR-7 определяет интерфейсы для HTTP-сообщений: запросов и ответов. Ключевой принцип -- иммутабельность: методы with*() возвращают новый объект, а не изменяют текущий.
Основные интерфейсы
<?php
declare(strict_types=1);
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
// MessageInterface -- base for requests and responses
// Methods: getHeaders(), getHeader(), getBody(), getProtocolVersion()
// withHeader(), withBody(), withProtocolVersion()
Иммутабельность PSR-7
Все with*() методы возвращают новый экземпляр:
<?php
declare(strict_types=1);
use Psr\Http\Message\ResponseInterface;
function modifyResponse(ResponseInterface $response): ResponseInterface
{
// Each with*() returns NEW object, original is unchanged
$new = $response
->withStatus(200)
->withHeader('Content-Type', 'application/json')
->withHeader('X-Request-Id', 'abc-123');
// $response is still the original, unmodified
// $new is a brand new object with applied changes
return $new;
}
RequestInterface и ServerRequestInterface
<?php
declare(strict_types=1);
use Psr\Http\Message\ServerRequestInterface;
function handleRequest(ServerRequestInterface $request): void
{
// Request line
$method = $request->getMethod(); // 'GET', 'POST', etc.
$uri = $request->getUri(); // UriInterface
$path = $uri->getPath(); // '/api/users'
// Headers
$contentType = $request->getHeaderLine('Content-Type');
$allHeaders = $request->getHeaders(); // array<string, string[]>
// Body
$body = $request->getBody(); // StreamInterface
$content = $body->getContents(); // Raw body string
// Server request specific
$queryParams = $request->getQueryParams(); // $_GET equivalent
$parsedBody = $request->getParsedBody(); // $_POST equivalent
$cookies = $request->getCookieParams(); // $_COOKIE equivalent
$uploaded = $request->getUploadedFiles(); // Uploaded files
// Attributes (added by middleware)
$userId = $request->getAttribute('user_id');
$request = $request->withAttribute('role', 'admin');
}
StreamInterface
Тело HTTP-сообщения представлено как поток (stream), а не как строка. Это позволяет работать с большими телами без загрузки в память:
<?php
declare(strict_types=1);
use Psr\Http\Message\StreamInterface;
function processStream(StreamInterface $stream): string
{
// Check stream capabilities
$stream->isReadable(); // Can we read?
$stream->isWritable(); // Can we write?
$stream->isSeekable(); // Can we seek?
// Read entire contents
$contents = $stream->getContents(); // From current position
$all = (string) $stream; // Full contents (rewinds)
// Metadata
$size = $stream->getSize(); // Size in bytes or null
return $contents;
}
UriInterface
<?php
declare(strict_types=1);
use Psr\Http\Message\UriInterface;
function inspectUri(UriInterface $uri): void
{
$scheme = $uri->getScheme(); // 'https'
$host = $uri->getHost(); // 'api.example.com'
$port = $uri->getPort(); // 443 or null
$path = $uri->getPath(); // '/api/v1/users'
$query = $uri->getQuery(); // 'page=1&limit=10'
$fragment = $uri->getFragment(); // 'section1'
// Immutable modifications
$newUri = $uri
->withScheme('https')
->withHost('api.example.com')
->withPath('/api/v2/orders')
->withQuery('status=active');
echo (string) $newUri;
// https://api.example.com/api/v2/orders?status=active
}
PSR-15: HTTP Handlers и Middleware
PSR-15 определяет два ключевых интерфейса для обработки HTTP-запросов.
RequestHandlerInterface
Обработчик -- конечная точка, которая принимает запрос и возвращает ответ:
<?php
declare(strict_types=1);
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class UserListHandler implements RequestHandlerInterface
{
public function __construct(
private readonly UserRepository $users,
private readonly ResponseFactoryInterface $responseFactory,
private readonly StreamFactoryInterface $streamFactory,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
$page = (int) ($request->getQueryParams()['page'] ?? 1);
$users = $this->users->findPaginated($page);
$body = $this->streamFactory->createStream(
json_encode($users, JSON_THROW_ON_ERROR)
);
return $this->responseFactory
->createResponse(200)
->withHeader('Content-Type', 'application/json')
->withBody($body);
}
}
MiddlewareInterface
Middleware -- промежуточный слой, который может модифицировать запрос/ответ или прервать цепочку:
<?php
declare(strict_types=1);
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
final class AuthenticationMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly TokenValidator $tokenValidator,
private readonly ResponseFactoryInterface $responseFactory,
) {}
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler,
): ResponseInterface {
$token = $request->getHeaderLine('Authorization');
if ($token === '') {
// Short-circuit: return 401 without calling next handler
return $this->responseFactory->createResponse(401);
}
$userId = $this->tokenValidator->validate($token);
if ($userId === null) {
return $this->responseFactory->createResponse(403);
}
// Add user info to request and pass to next handler
$request = $request->withAttribute('user_id', $userId);
return $handler->handle($request);
}
}
Middleware Pipeline
Middleware выстраиваются в цепочку (pipeline):
Request --> [Auth] --> [CORS] --> [Logging] --> [Handler] --> Response
|
Response <-- [Auth] <-- [CORS] <-- [Logging] <-----+
<?php
declare(strict_types=1);
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
// Conceptual pipeline implementation
final class MiddlewarePipeline implements RequestHandlerInterface
{
/** @var MiddlewareInterface[] */
private array $middlewares = [];
public function pipe(MiddlewareInterface $middleware): self
{
$this->middlewares[] = $middleware;
return $this;
}
public function handle(ServerRequestInterface $request): ResponseInterface
{
// Process middlewares in order, last one calls the final handler
$handler = $this->finalHandler;
foreach (array_reverse($this->middlewares) as $middleware) {
$handler = new class($middleware, $handler) implements RequestHandlerInterface {
public function __construct(
private readonly MiddlewareInterface $middleware,
private readonly RequestHandlerInterface $next,
) {}
public function handle(ServerRequestInterface $request): ResponseInterface
{
return $this->middleware->process($request, $this->next);
}
};
}
return $handler->handle($request);
}
}
PSR-17: HTTP Factories
PSR-17 определяет фабрики для создания PSR-7 объектов:
<?php
declare(strict_types=1);
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
final class ApiClient
{
public function __construct(
private readonly RequestFactoryInterface $requestFactory,
private readonly StreamFactoryInterface $streamFactory,
private readonly UriFactoryInterface $uriFactory,
) {}
public function createGetRequest(string $path): RequestInterface
{
$uri = $this->uriFactory->createUri('https://api.example.com' . $path);
return $this->requestFactory
->createRequest('GET', $uri)
->withHeader('Accept', 'application/json');
}
public function createPostRequest(string $path, array $data): RequestInterface
{
$body = $this->streamFactory->createStream(
json_encode($data, JSON_THROW_ON_ERROR),
);
return $this->requestFactory
->createRequest('POST', 'https://api.example.com' . $path)
->withHeader('Content-Type', 'application/json')
->withBody($body);
}
}
PSR-18: HTTP Client
PSR-18 определяет единый интерфейс для отправки HTTP-запросов:
<?php
declare(strict_types=1);
use Psr\Http\Client\ClientInterface;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
// PSR-18 interface:
// interface ClientInterface {
// public function sendRequest(RequestInterface $request): ResponseInterface;
// }
final readonly class PaymentGateway
{
public function __construct(
private ClientInterface $httpClient,
private RequestFactoryInterface $requestFactory,
private StreamFactoryInterface $streamFactory,
) {}
public function charge(int $amountCents, string $currency): PaymentResult
{
$body = $this->streamFactory->createStream(json_encode([
'amount' => $amountCents,
'currency' => $currency,
], JSON_THROW_ON_ERROR));
$request = $this->requestFactory
->createRequest('POST', 'https://payments.example.com/charge')
->withHeader('Content-Type', 'application/json')
->withHeader('Authorization', 'Bearer ' . $this->apiKey)
->withBody($body);
try {
$response = $this->httpClient->sendRequest($request);
} catch (ClientExceptionInterface $e) {
return PaymentResult::failed($e->getMessage());
}
if ($response->getStatusCode() !== 200) {
return PaymentResult::failed('Payment declined');
}
$data = json_decode(
$response->getBody()->getContents(),
true,
flags: JSON_THROW_ON_ERROR,
);
return PaymentResult::success($data['transaction_id']);
}
}
Symfony HttpFoundation vs PSR-7
Symfony использует собственный компонент HttpFoundation, а не PSR-7 напрямую. Но предоставляет бридж:
<?php
declare(strict_types=1);
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
// Symfony Request -> PSR-7 ServerRequestInterface
$symfonyRequest = Request::createFromGlobals();
$psrFactory = new PsrHttpFactory(/* ... */);
$psrRequest = $psrFactory->createRequest($symfonyRequest);
// PSR-7 ResponseInterface -> Symfony Response
$httpFoundationFactory = new HttpFoundationFactory();
$symfonyResponse = $httpFoundationFactory->createResponse($psrResponse);
| Аспект | HttpFoundation | PSR-7 |
|---|---|---|
| Мутабельность | Мутабельный | Иммутабельный |
| Использование | Внутри Symfony | Кросс-фреймворк |
| Middleware | Symfony Events | PSR-15 pipeline |
| Производительность | Чуть быстрее (без клонирования) | Больше объектов |
Совет: В Symfony-проектах используйте HttpFoundation. PSR-7 бридж нужен только при интеграции с PSR-7 совместимыми библиотеками.