PSR-7 w praktyce: HTTP Message Interfaces w PHP | MasterPHP

19.09.2025

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.
Sprawdź pełny kurs