MidТеория5 min

HTTP стандарты

PSR-7 сообщения, PSR-15 middleware, PSR-17 фабрики, PSR-18 HTTP-клиент

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 совместимыми библиотеками.


Проверь себя

Какой интерфейс определяет PSR-18?

Какой метод определяет PSR-15 MiddlewareInterface?

Почему тело HTTP-сообщения в PSR-7 представлено как StreamInterface, а не string?

Для чего нужен PSR-17?

Что произойдёт при вызове `$response->withStatus(404)` в PSR-7?