PSR-7
PSR-7 предоставляет объекты HTTP-запросов и ответов, URI, потоки и вспомогательные функции для разбора HTTP-строк. Основные классы находятся в localzet\PSR7, но реализуют интерфейсы Psr\Http\Message из psr/http-message.
В этой реализации важно учитывать различия между объектами: часть with*-методов меняет текущий объект, часть возвращает копию. Нельзя переносить в код предположение о неизменяемости всех HTTP-сообщений.
Установка
Заголовок раздела «Установка»Манифест требует PHP ^8.2, Localzet Server ^4.1 и psr/http-message ^2.0:
composer config repositories.localzet-psr7 vcs https://github.com/localzet/PSR-7composer require localzet/psr7:dev-mainComposer подключает классы через PSR-4 и функции из src/functions_include.php. Отдельный пакет localzet/psr не подменяет зависимость psr/http-message: namespaces у них различаются.
Запрос и ответ
Заголовок раздела «Запрос и ответ»use localzet\PSR7\Request;use localzet\PSR7\Response;
$request = new Request('get', 'https://example.test/items?page=2');echo $request->getMethod(); // GETecho $request->getRequestTarget(); // /items?page=2
$response = new Response(200, ['Content-Type' => 'application/json'], '{"ok":true}');echo $response->getHeaderLine('content-type');echo (string) $response->getBody();Конструктор Request приводит метод к верхнему регистру. Если заголовок Host не задан, он формируется из URI с портом, когда он есть. Request target по умолчанию состоит из пути и query; для пустого пути используется /.
Response получает status, headers и строковое тело. Для известных кодов reason phrase подставляется автоматически, если не задан явно. Создание объекта не отправляет его клиенту — для этого нужен HTTP-сервер или клиентский transport.
Какие изменения затрагивают оригинал
Заголовок раздела «Какие изменения затрагивают оригинал»$original = new Response(200);$modified = $original->withHeader('X-Mode', 'preview');
var_dump($original === $modified); // truewithHeader, withAddedHeader, withoutHeader, withHeaders, withBody и withProtocolVersion меняют текущий объект и возвращают его. В Request это относится также к withMethod, withRequestTarget и withUri.
Напротив, Response::withStatus(), методы Uri::with*() и дополнительные ServerRequest::with*() для query/cookies/body/attributes/uploads возвращают копию, когда меняют значение. Не выводите семантику одного метода из поведения другого.
Если нужно изменить заголовки, сохранив исходный response, явно создайте копию:
$modified = clone $original;$modified->withHeader('X-Mode', 'download');Это поверхностная копия. Уже существующий body-stream остаётся общим объектом: чтение, запись или закрытие потока через одну копию влияет на другую. Для независимого тела создайте новый stream и назначьте его через withBody().
Работа с заголовками
Заголовок раздела «Работа с заголовками»Имена ищутся без учёта регистра; исходное написание имени сохраняется в карте. getHeader() возвращает массив значений, getHeaderLine() соединяет их через , .
$response->withHeader('X-Tag', 'first');$response->withAddedHeader('x-tag', 'second');
$values = $response->getHeader('X-TAG');// ['first', 'second']withHeader() заменяет значения, withAddedHeader() добавляет к ним. Дополнительный метод withHeaders() объединяет переданные значения с существующими; это не полная замена всей карты заголовков. setHeaders() вызывает ту же операцию объединения.
Не используйте getHeaderLine() для выдачи нескольких Set-Cookie: их нужно сохранять отдельными значениями. Перед установкой имени и значения из внешнего ввода валидируйте их; общие setters здесь в основном приводят значения к массиву и обрезают пробелы/табуляции, а не обеспечивают полную проверку HTTP-синтаксиса.
Потоки и позиция чтения
Заголовок раздела «Потоки и позиция чтения»use localzet\PSR7\Stream;
$resource = fopen('php://temp', 'r+');fwrite($resource, 'abcdef');rewind($resource);
$stream = new Stream($resource);echo $stream->read(2); // abecho $stream->getContents(); // cdef$stream->rewind();echo $stream->getContents(); // abcdef$stream->close();getContents() читает с текущей позиции, а не всегда с начала. (string) $stream пытается перейти к началу, если seek поддерживается, и читает содержимое. Конвертация в строку может загрузить всё тело в память.
close() закрывает underlying resource, detach() отдаёт resource вызывающему коду и отсоединяет его. После detach чтение, запись и seek больше недоступны. У сетевого/генерируемого потока может не быть размера или возможности перемотки.
Функция stream_for() имеет строковый параметр в реальной сигнатуре. Не ориентируйтесь на более широкий перечень типов в её старом DocBlock: для resource используйте new Stream($resource), для callback — new PumpStream($callback), для готового потока — withBody($stream).
use localzet\PSR7\Uri;use localzet\PSR7\UriResolver;
$base = new Uri('https://example.test/api/items');$next = UriResolver::resolve($base, new Uri('../users?page=2'));
$changed = $base->withQuery('page=3');Uri разделяет scheme, user info, host, port, path, query и fragment. Методы изменения URI возвращают копию; исходный URI сохраняется. Query передаётся строкой без ?, fragment — без #.
В библиотеке есть helpers parse_query() и build_query(). Они оперируют параметрами, а не проверяют разрешённый hostname или право на исходящее соединение. URI-объект сам по себе не защищает от SSRF; такую проверку должен делать transport/приложение.
Входящий ServerRequest
Заголовок раздела «Входящий ServerRequest»new ServerRequest($httpBuffer) принимает сырую HTTP-строку и использует парсер Localzet Server. Это не конструктор вида (method, uri), как у исходящего Request. Он заполняет server params из $_SERVER, а query/cookies/uploads — из разобранного запроса.
Для явного parsed body используйте результат withParsedBody($data); изменение query-параметров не переписывает query исходного URI. Attributes предназначены для данных middleware, а не для доверия входным HTTP-полям.
UploadedFile предоставляет поток загруженного файла и moveTo(). Имя и media type от клиента — metadata, а не основание доверять расширению или содержимому. Проверяйте ошибку загрузки и назначение пути до перемещения.
Классы, потоки, функции и ошибки
Полный пример
Заголовок раздела «Полный пример»<?php
declare(strict_types=1);
use localzet\PSR7\Request;use localzet\PSR7\Response;use localzet\PSR7\Stream;
require __DIR__ . '/vendor/autoload.php';
$request = new Request('get', 'https://example.test/items?page=2');$response = new Response(200, ['Content-Type' => 'application/json'], '{"ok":true}');$tagged = $response->withHeader('X-Mode', 'preview');$accepted = $response->withStatus(202);
$resource = fopen('php://temp', 'r+');fwrite($resource, 'abcdef');rewind($resource);$stream = new Stream($resource);try { echo json_encode([ 'method' => $request->getMethod(), 'target' => $request->getRequestTarget(), 'same_header_object' => $tagged === $response, 'original_status' => $response->getStatusCode(), 'new_status' => $accepted->getStatusCode(), 'first_bytes' => $stream->read(2), 'remaining_bytes' => $stream->getContents(), ], JSON_THROW_ON_ERROR), PHP_EOL;} finally { $stream->close();}Исходный код · AGPL-3.0