PSR-7 w praktyce: HTTP Message Interfaces w PHP
Standard PSR-7 definiuje wspólny sposób reprezentacji żądań (Request
)
i odpowiedzi (Response
) HTTP w PHP. To jeden z najbardziej rozbudowanych standardów PHP-FIG,
obejmujący nie tylko same wiadomości, ale także strumienie, nagłówki oraz zarządzanie URI.
Dlaczego PSR-7?
- Ujednolicony model komunikacji HTTP w ekosystemie PHP,
- Łatwe testowanie aplikacji webowych i API,
- Kompatybilność między frameworkami (np. Symfony, Laravel, Slim),
- Podstawa dla kolejnych standardów takich jak PSR-15 (Middleware) czy PSR-18 (HTTP Client).
Kluczowe komponenty
1. BaseRequest
Klasa bazowa obsługująca żądania HTTP. Zawiera metody do zarządzania metodą żądania, URI, nagłówkami i treścią. Ze względu na bardzo szeroki zakres, w tym artykule pokazujemy tylko przykładowe definicje — pełna implementacja dostępna jest w kursie i publicznie na GitHubie.
Warto podkreślić, że PHP 8.5 wprowadza nowy mechanizm
clone_with,
który znacząco uprości implementację klasy Request
.
Z kolei property hooks z PHP 8.4 nie mogły być tu użyte,
ponieważ zależało nam na pełnej zgodności ze specyfikacją PSR.
<?php
declare(strict_types=1);
namespace DJWeb\Framework\Http\Request\Psr7;
use DJWeb\Framework\Http\HeaderManager;
use DJWeb\Framework\Http\UpdateHostFromUri;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
class BaseRequest implements RequestInterface
{
private string $protocolVersion = '1.1';
public function __construct(
protected string $method,
protected UriInterface $uri,
protected StreamInterface $body,
protected HeaderManager $headerManager
) {
}
public function getProtocolVersion(): string
{
return $this->protocolVersion;
}
public function withProtocolVersion(string $version): static
{
$new = clone $this;
$new->protocolVersion = $version;
return $new;
}
public function getHeaders(): array
{
return $this->headerManager->getHeaders();
}
public function hasHeader(string $name): bool
{
return $this->headerManager->hasHeader($name);
}
public function getHeader(string $name): array
{
return $this->headerManager->getHeader($name);
}
public function getHeaderLine(string $name): string
{
return $this->headerManager->getHeaderLine($name);
}
public function withHeader(string $name, $value): static
{
$new = clone $this;
$new->headerManager = $this->headerManager->withHeader($name, $value);
return $new;
}
public function withAddedHeader(string $name, $value): static
{
$new = clone $this;
$new->headerManager = $this->headerManager->withAddedHeader(
$name,
$value
);
return $new;
}
public function withoutHeader(string $name): static
{
$new = clone $this;
$new->headerManager = $this->headerManager->withoutHeader($name);
return $new;
}
public function getBody(): StreamInterface
{
return $this->body;
}
public function withBody(StreamInterface $body): static
{
$new = clone $this;
$new->body = $body;
return $new;
}
public function getRequestTarget(): string
{
$target = $this->uri->getPath() . '/';
return str_ends_with($target, '/') ? $target : $target . '?' . $this->uri->getQuery();
}
public function withRequestTarget(string $requestTarget): static
{
$new = clone $this;
$new->uri = $new->uri->withPath($requestTarget);
return $new;
}
public function getMethod(): string
{
return $this->method;
}
public function withMethod(string $method): static
{
$new = clone $this;
$new->method = $method;
return $new;
}
public function getUri(): UriInterface
{
return $this->uri;
}
public function withUri(
UriInterface $uri,
bool $preserveHost = false
): static {
$new = clone $this;
$new->uri = $uri;
if (! $preserveHost) {
$new = $new->updateHostFromUri();
}
return $new;
}
private function updateHostFromUri(): static
{
/** @phpstan-ignore-next-line */
return UpdateHostFromUri::update($this, $this->uri);
}
}
2. Response
Klasa Response
reprezentuje odpowiedź HTTP. Udostępnia metody do ustawiania kodu statusu,
nagłówków i treści. PSR-7 zapewnia, że każda odpowiedź jest niezmienna — zamiast modyfikować obiekt,
zwracane są nowe instancje (immutability).
Podobnie jak i Request ta klasa będzie mogła być znacznie uproszczona dzięki clone_with
<?php
declare(strict_types=1);
namespace DJWeb\Framework\Http;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
class Response implements ResponseInterface
{
private string $reasonPhrase = '';
private HeaderManager $headers;
private StreamInterface $body;
/**
* @param array<string, string|array<int, string>> $headers
*/
public function __construct(
array $headers = [],
?StreamInterface $body = null,
private string $version = '1.1',
private int $status = 200,
?string $reason = null
) {
$this->headers = new HeaderManager($headers);
$this->body = $body ?? $this->createDefaultBody();
$this->reasonPhrase = $reason ?? $this->getDefaultReasonPhrase($status);
}
public function getProtocolVersion(): string
{
return $this->version;
}
public function withProtocolVersion(string $version): static
{
$new = clone $this;
$new->version = $version;
return $new;
}
public function getHeaders(): array
{
return $this->headers->getHeaders();
}
public function hasHeader(string $name): bool
{
return $this->headers->hasHeader($name);
}
public function getHeader(string $name): array
{
return $this->headers->getHeader($name);
}
public function getHeaderLine(string $name): string
{
return $this->headers->getHeaderLine($name);
}
public function withHeader(string $name, $value): static
{
$new = clone $this;
$new->headers = $this->headers->withHeader($name, $value);
return $new;
}
public function withAddedHeader(string $name, $value): static
{
$new = clone $this;
$new->headers = $this->headers->withAddedHeader($name, $value);
return $new;
}
public function withoutHeader(string $name): static
{
$new = clone $this;
$new->headers = $this->headers->withoutHeader($name);
return $new;
}
public function getBody(): StreamInterface
{
return $this->body;
}
public function withBody(StreamInterface $body): static
{
$new = clone $this;
$new->body = $body;
return $new;
}
public function getStatusCode(): int
{
return $this->status;
}
public function withStatus(int $code, string $reasonPhrase = ''): static
{
$new = clone $this;
$new->status = $code;
$new->reasonPhrase =
$reasonPhrase ? $reasonPhrase
: $this->getDefaultReasonPhrase($code);
return $new;
}
public function withContent(string $content): ResponseInterface
{
$this->body->write($content);
return $this;
}
/**
* @param array<int|string, mixed> $data
*/
public function withJson(
array $data,
int $status = 200,
): ResponseInterface {
$json = json_encode($data, JSON_THROW_ON_ERROR);
return $this
->withHeader('Content-Type', 'application/json')
->withContent($json)
->withStatus($status);
}
public function getReasonPhrase(): string
{
return $this->reasonPhrase;
}
public function redirect(string $url, int $status = 302): self
{
return $this
->withStatus($status)
->withHeader('Location', $url);
}
private function createDefaultBody(): StreamInterface
{
return new Stream('php://temp', 'r+');
}
private function getDefaultReasonPhrase(int $statusCode): string
{
$phrases = [
200 => 'OK',
201 => 'Created',
204 => 'No Content',
301 => 'Moved Permanently',
302 => 'Found',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Not Found',
405 => 'Method Not Allowed',
500 => 'Internal Server Error',
];
return $phrases[$statusCode] ?? '';
}
}
3. UriManager
Klasa odpowiedzialna za zarządzanie URI, zgodnie ze specyfikacją RFC 3986. W kursie znajdziesz pełną implementację.
Co ciekawe, od PHP 8.5 cała ta logika stanie się zbędna —
dzięki nowemu
URL Parsing API
w core języka dostępna będzie kompletna obsługa URI zgodna z RFC 3986.
Oznacza to, że całą implementację UriManager
będzie można po prostu wyrzucić.
<?php
declare(strict_types=1);
namespace DJWeb\Framework\Http;
use DJWeb\Framework\Http\Helpers\AuthorityBuilder;
use DJWeb\Framework\Http\Helpers\QueryStringEncoder;
use Psr\Http\Message\UriInterface;
class UriManager implements UriInterface
{
private string $scheme = '';
private string $userInfo = '';
private string $host = '';
private ?int $port = null;
private string $path = '';
private string $query = '';
private string $fragment = '';
public function __construct(string $uri = '')
{
$parts = parse_url($uri);
$this->scheme = $parts['scheme'] ?? '';
$this->userInfo = $this->modifyUserInfo(
$parts['user'] ?? '',
$parts['pass'] ?? ''
);
$this->host = $parts['host'] ?? '';
$this->port = isset($parts['port']) ? (int) $parts['port'] : null;
$this->path = $parts['path'] ?? '';
$this->query = $parts['query'] ?? '';
$this->fragment = $parts['fragment'] ?? '';
}
public function __toString(): string
{
return UriStringBuilder::build($this);
}
public function getScheme(): string
{
return $this->scheme;
}
public function getAuthority(): string
{
return AuthorityBuilder::buildAuthority($this);
}
public function getUserInfo(): string
{
return $this->userInfo;
}
public function getHost(): string
{
return $this->host;
}
public function getPort(): ?int
{
return $this->port;
}
public function getPath(): string
{
return $this->path;
}
public function getQuery(): string
{
return $this->query;
}
public function getFragment(): string
{
return $this->fragment;
}
public function withScheme(string $scheme): static
{
$new = clone $this;
$new->scheme = strtolower($scheme);
return $new;
}
public function withUserInfo(string $user, ?string $password = null): static
{
$new = clone $this;
$new->userInfo = $this->modifyUserInfo($user, $password);
return $new;
}
public function withHost(string $host): static
{
$new = clone $this;
$new->host = strtolower($host);
return $new;
}
public function withPort(?int $port): static
{
$new = clone $this;
$new->port = $port;
return $new;
}
public function withPath(string $path): static
{
$new = clone $this;
$path = trim($path, '/');
$exploded = explode('/', $path);
foreach ($exploded as $key => $part) {
$exploded[$key] = rawurlencode($part);
}
$new->path = '/' . implode('/', $exploded);
return $new;
}
public function withQuery(string $query): static
{
$new = clone $this;
$new->query = $this->encodeQueryString($query);
return $new;
}
public function withFragment(string $fragment): static
{
$new = clone $this;
$new->fragment = rawurlencode($fragment);
return $new;
}
private function modifyUserInfo(
string $user,
?string $password = null
): string {
$info = urlencode($user);
if ($password) {
$info .= ':' . urlencode($password);
}
return $info;
}
private function encodeQueryString(string $query): string
{
return QueryStringEncoder::encodeQueryString($query);
}
}
4. HeaderManager
Obsługa nagłówków to świetny przykład rozbicia interfejsu na kilka klas, aby uniknąć duplikacji kodu.
Zarówno Request
, jak i Response
posiadają nagłówki, więc wydzielenie ich do
osobnej klasy (HeaderManager
) pozwala utrzymać czysty i spójny kod.
<?php
declare(strict_types=1);
namespace DJWeb\Framework\Http;
class HeaderManager
{
private HeaderArray $headers;
/**
* @param array<string, string|array<int, string>> $headers
*/
public function __construct(array $headers = [])
{
$this->headers = new HeaderArray($headers);
}
/**
* @return array<array<string>>
*/
public function getHeaders(): array
{
return $this->headers->all();
}
public function hasHeader(string $name): bool
{
return $this->headers->has($name);
}
/**
* @return array<string>
*/
public function getHeader(string $name): array
{
return $this->headers->get($name);
}
public function getHeaderLine(string $name): string
{
return $this->headers->getLine($name);
}
/**
* @param string|array<int, string> $value
*/
public function withHeader(string $name, string|array $value): self
{
$new = new HeaderManager($this->headers->all());
$new->headers->set($name, $value);
return $new;
}
/**
* @param string|array<int, string> $value
*/
public function withAddedHeader(string $name, string|array $value): self
{
$new = new HeaderManager($this->headers->all());
$new->headers->add($name, $value);
return $new;
}
public function withoutHeader(string $name): self
{
$new = new HeaderManager($this->headers->all());
$new->headers->remove($name);
return $new;
}
}
Definicje pozostałych komponentów PSR-7 (m.in. StreamInterface
, szczegółowe metody Request
i Response
)
znajdziesz w kursie i publicznie dostępnym repozytorium GitHub.
Gdzie wykorzystasz PSR-7?
- Budowa własnych frameworków i mikroframeworków,
- Obsługa API REST i GraphQL,
- Middleware zgodne z PSR-15,
- Integracje z bibliotekami HTTP Client (PSR-18).
Podsumowanie
PSR-7 to fundament współczesnych aplikacji webowych w PHP. Choć jego pełna implementacja jest obszerna, w praktyce pozwala budować elastyczne i przenośne systemy komunikacji HTTP.
📖 Oficjalna specyfikacja PSR-7: https://www.php-fig.org/psr/psr-7/
👉 To tylko fragment pełnego kursu MasterPHP!
W kursie znajdziesz kompletną implementację PSR-7 wraz z przykładami:
- pełne klasy Request i Response,
- implementację strumieni i nagłówków,
- obsługę URI zgodną z RFC 3986,
- testy jednostkowe i integracyjne.