PSR-17 w praktyce: HTTP Factories + first look na PHP 8.5 | MasterPHP

15.08.2025

PSR-17 w praktyce: HTTP Factories

PSR-17 standaryzuje tworzenie obiektów HTTP z PSR-7: requestów, response’ów, URI, strumieni i uploadowanych plików. Zamiast „lepić” obiekty ręcznie, korzystamy z fabryk — a reszta ekosystemu rozumie ten sam kontrakt.

Po co są fabryki z PSR-17?

  • Tworzysz obiekty PSR-7 w jednolity sposób (request, response, stream, uri, uploaded file),
  • łatwiej testujesz (podmieniasz fabryki w DI),
  • bezbłędnie zachowujesz niezmienność obiektów PSR-7,
  • masz kompatybilny most do PSR-18 (klienci HTTP).

Jak to wygląda u mnie

W kursie używam RequestFactory, która składa obiekt requestu z menedżerów: HeaderManager, AttributesManager, ParsedBodyManager, QueryParamsManager, UploadedFilesManager, a także z Stream oraz UploadedFileFactory i budowniczego URI. To czysty PSR-17 w praktyce: z zmiennych superglobalnych powstają obiekty zgodne z PSR-7.

<?php

declare(strict_types=1);

namespace DJWeb\Framework\Http\Request\Psr17;

use DJWeb\Framework\Http\HeaderManager;
use DJWeb\Framework\Http\Request\AttributesManager;
use DJWeb\Framework\Http\Request\ParsedBody;
use DJWeb\Framework\Http\Request\ParsedBodyManager;
use DJWeb\Framework\Http\Request\Psr7\Request;
use DJWeb\Framework\Http\Request\QueryParamsManager;
use DJWeb\Framework\Http\Request\UploadedFilesManager;
use DJWeb\Framework\Http\Stream;
use DJWeb\Framework\Http\UploadedFile\UploadedFileFactory;
use DJWeb\Framework\Http\Uri\UriBuilder;
use DJWeb\Framework\Http\UriManager;
use Psr\Http\Message\ServerRequestFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Http\Message\UriInterface;

class RequestFactory implements ServerRequestFactoryInterface
{
    /**
     * @param string $method
     * @param $uri
     * @param array<string, mixed> $serverParams
     *
     * @return Request
     */
    public function createServerRequest(string $method, $uri, array $serverParams = []): Request
    {
        $uri = $uri instanceof UriInterface ? $uri : new UriManager($uri);
        return new Request(
            $method,
            $uri,
            new Stream('php://temp'),
            new HeaderManager([]),
            $serverParams,
            [],
            new QueryParamsManager([]),
            new UploadedFilesManager([]),
            new ParsedBodyManager(null),
            new AttributesManager()
        );
    }

    public function createFromGlobals(): Request
    {
        return new Request(
            ...$this->getRequestConstructorParams()
        );
    }

    /**
     * @return array<int, mixed>
     *
     * @throws \JsonException
     */
    public function getRequestConstructorParams(): array
    {
        return [
            $_SERVER['REQUEST_METHOD'] ?? 'GET',
            new UriBuilder()->createUriFromGlobals(),
            new Stream('php://input'),
            new HeaderManager($this->getAllHeaders()),
            $_SERVER,
            $_COOKIE,
            new QueryParamsManager($_GET),
            new UploadedFilesManager($this->parseFiles($_FILES)),
            new ParsedBodyManager(new ParsedBody()->get()),
            new AttributesManager(),
        ];
    }

    /**
     * @param array<string, mixed> $files
     *
     * @return array<string, UploadedFileInterface>
     */
    private function parseFiles(array $files): array
    {
        $parsedFiles = [];
        $files = array_filter($files, static fn (mixed $file) => is_array($file));
        foreach ($files as $key => $file) {
            $parsedFiles[$key] = UploadedFileFactory::createUploadedFile(
                $file['name'],
                $file['size'],
                $file['error'],
                $file['tmp_name'],
                $file['type'] ?? null
            );
        }
        return $parsedFiles;
    }

    /**
     * @return array<string, string>
     */
    private function getAllHeaders(): array
    {
        $headers = [];
        foreach ($_SERVER as $name => $value) {
            if (str_starts_with($name, 'HTTP_')) {
                $headers[
                    str_replace(
                        ' ',
                        '-',
                        ucwords(
                            strtolower(str_replace('_', ' ', substr($name, 5)))
                        )
                    )
                ] = $value;
            }
        }
        return $headers;
    }
}

Realny akcent PHP 8.5 w tej fabryce

W metodzie getAllHeaders mamy przykład zagnieżdżenia bardzo wielu funkcji. PHP 8.5 wprowadza nową składnię Pipe operator (|>) w normalizacji nagłówków – zamiast kaskady zagnieżdżonych funkcji, mam czytelny potok transformacji (strip prefix → zamień podkreślenia → ucwords → spacje na „-”. To ogromny wzrost czytelności przy mapowaniu $_SERVER na header keys.

To jest dokładnie ten kierunek, w którym 8.5 robi różnicę: kod fabryki jest krótszy, czytelniejszy i łatwiej go utrzymać, bez łamania kontraktów PSR-7/17. Poniżej alternatywna wersja funkcji getAllHeaders wykorzystująca pełną moc nadchodzącego PHP 8.5

<?php

    private function getAllHeaders(): array
    {
        return array_map(
            callback: static fn(string $item) => substr($item, 5)
                    |> static fn(string $item) => str_replace('_', ' ', $item)
                    |> ucwords(...)
                    |> static fn(string $item) => str_replace(' ', '-', $item),
            array: array_filter(
                array: $_SERVER,
                callback: static fn(mixed $value, string $key) => str_starts_with($key, 'HTTP_'),
            )
        );
    }

Co dalej uprości 8.5 i okolice?

  • clone_with – upraszcza tworzenie „zmodyfikowanych klonów”. W warstwie PSR-7 ułatwi implementacje metod with* po stronie Request/Response, a fabryki będą miały mniej „klejenia” wstępnego.
  • Nowe URL Parsing API (RFC 3986 w core) – fabryka URI przestaje utrzymywać własny parser; Obecnie w kursie opieram się na składni PHP 8.4 lecz już niebawem kurs dostanie aktualizację do PHP 8.5 gdzie zmian jak pokazana powyzej będzie dużo.

Co konkretnie dostarcza PSR-17 (w skrócie)?

  • RequestFactorycreateRequest(method, uri),
  • ServerRequestFactorycreateServerRequest(method, uri, serverParams),
  • ResponseFactorycreateResponse(code, reason),
  • StreamFactorycreateStream(...),
  • UploadedFileFactorycreateUploadedFile(...),
  • UriFactorycreateUri(string).

Wzorce projektowe, które tu grają

Przede wszystkim Factory Method + drobny Builder/Fluent po stronie PSR-7, a także Adapter (np. przy strumieniach/URI).

📖 Oficjalna specyfikacja PSR-17: https://www.php-fig.org/psr/psr-17/

👉 To tylko fragment pełnego kursu MasterPHP!

W kursie znajdziesz kompletną implementację PSR-17, wraz z wariantami pod PHP 8.5:

  • Request/ServerRequest/Response/Stream/Uri/UploadedFile Factories,
  • mapowanie superglobali do obiektów (bez „lepienia” w miejscu),
  • użycie nowego URL API i clone_with do uproszczeń.
Sprawdź pełny kurs