# Nyholm PSR-7 Nyholm PSR-7 is a super lightweight, strict, and fast implementation of the PSR-7 HTTP message interfaces and PSR-17 HTTP factories for PHP. With only around 1,000 lines of code, it is the smallest PSR-7 implementation available while maintaining 100% compliance with the PSR-7 specification and outperforming other implementations like Guzzle, Laminas, and Slim in benchmarks. The library provides all the essential HTTP message abstractions needed for modern PHP applications: Request, Response, ServerRequest, Stream, UploadedFile, and Uri objects. It implements both PSR-7 (HTTP Message interfaces) and PSR-17 (HTTP Factories) standards, making it compatible with any PSR-7 compliant HTTP client or framework. The single `Psr17Factory` class serves as the unified factory for creating all PSR-7 objects. ## Installation Install the library via Composer: ```bash composer require nyholm/psr7 ``` ## Psr17Factory - Create HTTP Objects The `Psr17Factory` is the main entry point for creating all PSR-7 objects. It implements all PSR-17 factory interfaces including `RequestFactoryInterface`, `ResponseFactoryInterface`, `ServerRequestFactoryInterface`, `StreamFactoryInterface`, `UploadedFileFactoryInterface`, and `UriFactoryInterface`. ```php createRequest('GET', 'https://api.example.com/users'); // Create a POST request with headers and body $postRequest = $factory->createRequest('POST', 'https://api.example.com/users'); $postRequest = $postRequest ->withHeader('Content-Type', 'application/json') ->withHeader('Authorization', 'Bearer token123') ->withBody($factory->createStream('{"name": "John", "email": "john@example.com"}')); // Create a response $response = $factory->createResponse(200, 'OK'); $response = $response ->withHeader('Content-Type', 'application/json') ->withBody($factory->createStream('{"status": "success"}')); // Create a URI $uri = $factory->createUri('https://user:pass@example.com:8080/path?query=value#fragment'); // Create a stream from string $stream = $factory->createStream('Hello, World!'); // Create a stream from file $fileStream = $factory->createStreamFromFile('/path/to/file.txt', 'r'); // Create a stream from resource $resource = fopen('php://temp', 'r+'); fwrite($resource, 'data'); rewind($resource); $resourceStream = $factory->createStreamFromResource($resource); // Create a server request $serverRequest = $factory->createServerRequest('GET', '/api/users', $_SERVER); // Create an uploaded file $uploadedFile = $factory->createUploadedFile( $factory->createStream('file contents'), 1024, // size in bytes UPLOAD_ERR_OK, // upload error status 'document.pdf', // client filename 'application/pdf' // client media type ); ``` ## Request - HTTP Client Request The `Request` class represents an outgoing HTTP request. It implements `Psr\Http\Message\RequestInterface` and provides immutable methods for building requests with method, URI, headers, and body. ```php 'application/json'], // Headers array '{"name": "John"}', // Body (string, resource, or StreamInterface) '1.1' // Protocol version ); // Get request properties echo $request->getMethod(); // "POST" echo $request->getUri(); // "https://api.example.com/users" echo $request->getRequestTarget(); // "/users" echo $request->getProtocolVersion(); // "1.1" // Work with headers $request = $request->withHeader('Accept', 'application/json'); $request = $request->withAddedHeader('Accept', 'text/html'); // Adds to existing values echo $request->getHeaderLine('Accept'); // "application/json, text/html" print_r($request->getHeader('Accept')); // ["application/json", "text/html"] $request = $request->withoutHeader('Accept'); // Modify the request (immutable - returns new instance) $newRequest = $request ->withMethod('PUT') ->withUri(new Uri('https://api.example.com/users/123')) ->withHeader('Authorization', 'Bearer abc123') ->withBody(Stream::create('{"name": "Jane"}')); // Check if header exists if ($request->hasHeader('Content-Type')) { echo $request->getHeaderLine('Content-Type'); } ``` ## Response - HTTP Response The `Response` class represents an HTTP response. It implements `Psr\Http\Message\ResponseInterface` with automatic status code reason phrase mapping for all standard HTTP status codes. ```php 'application/json'], // Headers '{"id": 1, "name": "John"}', // Body '1.1', // Protocol version 'Created' // Reason phrase (optional, auto-detected) ); // Get response properties echo $response->getStatusCode(); // 201 echo $response->getReasonPhrase(); // "Created" echo $response->getBody(); // '{"id": 1, "name": "John"}' // Common HTTP responses $okResponse = new Response(200); // "OK" $createdResponse = new Response(201); // "Created" $noContentResponse = new Response(204); // "No Content" $redirectResponse = new Response(302); // "Found" $notFoundResponse = new Response(404); // "Not Found" $serverErrorResponse = new Response(500); // "Internal Server Error" // Modify response (immutable) $response = (new Response()) ->withStatus(200) ->withHeader('Content-Type', 'text/html') ->withHeader('Cache-Control', 'no-cache') ->withBody(Stream::create('Hello!')); // Change status code with custom reason phrase $response = $response->withStatus(418, "I'm a teapot"); echo $response->getReasonPhrase(); // "I'm a teapot" // JSON API response example $jsonResponse = (new Response(200)) ->withHeader('Content-Type', 'application/json') ->withBody(Stream::create(json_encode([ 'status' => 'success', 'data' => ['id' => 1, 'name' => 'John'] ]))); ``` ## ServerRequest - Server-Side Request The `ServerRequest` class represents an incoming HTTP request on the server side. It implements `Psr\Http\Message\ServerRequestInterface` with support for server parameters, cookies, query parameters, uploaded files, and request attributes. ```php 'application/json'], // Headers '{"name": "John"}', // Body '1.1', // Protocol version ['REMOTE_ADDR' => '192.168.1.1', 'SERVER_NAME' => 'example.com'] // Server params ); // Query parameters are automatically parsed from URI print_r($request->getQueryParams()); // ['page' => '1'] // Get server parameters $serverParams = $request->getServerParams(); echo $serverParams['REMOTE_ADDR']; // "192.168.1.1" // Work with cookies $request = $request->withCookieParams([ 'session_id' => 'abc123', 'user_pref' => 'dark_mode' ]); $cookies = $request->getCookieParams(); echo $cookies['session_id']; // "abc123" // Work with query parameters $request = $request->withQueryParams(['page' => '2', 'limit' => '10']); $params = $request->getQueryParams(); echo $params['limit']; // "10" // Work with parsed body (for POST data) $request = $request->withParsedBody([ 'username' => 'john', 'email' => 'john@example.com' ]); $data = $request->getParsedBody(); echo $data['username']; // "john" // Work with uploaded files $uploadedFile = new UploadedFile( Stream::create('file content'), 12, UPLOAD_ERR_OK, 'document.pdf', 'application/pdf' ); $request = $request->withUploadedFiles(['document' => $uploadedFile]); $files = $request->getUploadedFiles(); // Work with attributes (commonly used for routing parameters) $request = $request->withAttribute('user_id', 123); $request = $request->withAttribute('route', 'api.users.show'); $userId = $request->getAttribute('user_id'); // 123 $default = $request->getAttribute('missing', 'none'); // "none" $allAttributes = $request->getAttributes(); // ['user_id' => 123, 'route' => ...] $request = $request->withoutAttribute('route'); ``` ## Stream - Message Body Stream The `Stream` class represents a data stream for HTTP message bodies. It implements `Psr\Http\Message\StreamInterface` and can be created from strings, resources, or existing stream instances. For large strings (200KB+), it uses an optimized copy-on-write mechanism. ```php getContents(); // "Hello, World!" // Create stream from resource $resource = fopen('php://temp', 'r+'); fwrite($resource, 'Resource content'); rewind($resource); $stream = Stream::create($resource); // Create stream from file resource $fileResource = fopen('/path/to/file.txt', 'r'); $stream = new Stream($fileResource); // Read stream contents $stream = Stream::create('Hello, World!'); echo $stream; // "Hello, World!" (via __toString) echo $stream->getSize(); // 13 echo $stream->tell(); // 0 (current position) echo $stream->read(5); // "Hello" echo $stream->tell(); // 5 echo $stream->getContents(); // ", World!" (reads remaining) echo $stream->eof(); // true // Seek and rewind $stream->rewind(); // Go back to start $stream->seek(7); // Move to position 7 echo $stream->read(5); // "World" // Write to stream $stream = Stream::create(''); $stream->write('First line'); $stream->write(' - Second part'); $stream->rewind(); echo $stream->getContents(); // "First line - Second part" // Check stream capabilities $stream = Stream::create('test'); echo $stream->isReadable(); // true echo $stream->isWritable(); // true echo $stream->isSeekable(); // true // Get stream metadata $meta = $stream->getMetadata(); echo $stream->getMetadata('mode'); // "r+" echo $stream->getMetadata('uri'); // "php://memory" // Detach underlying resource $resource = $stream->detach(); // Returns resource, stream becomes unusable // $stream->read(1); // Would throw RuntimeException // Close stream $stream = Stream::create('test'); $stream->close(); // Releases the resource ``` ## Uri - URI Manipulation The `Uri` class represents a URI according to RFC 3986. It implements `Psr\Http\Message\UriInterface` with full support for all URI components including scheme, user info, host, port, path, query, and fragment. ```php getScheme(); // "https" echo $uri->getHost(); // "example.com" echo $uri->getPort(); // 8080 echo $uri->getUserInfo(); // "user:pass" echo $uri->getAuthority(); // "user:pass@example.com:8080" echo $uri->getPath(); // "/path/to/resource" echo $uri->getQuery(); // "foo=bar&baz=qux" echo $uri->getFragment(); // "section" echo (string) $uri; // Full URI string // Standard ports (80 for http, 443 for https) return null $stdUri = new Uri('https://example.com:443/path'); echo $stdUri->getPort(); // null (standard port is suppressed) $nonStdUri = new Uri('https://example.com:8443/path'); echo $nonStdUri->getPort(); // 8443 (non-standard port) // Build URI using fluent interface (immutable) $uri = (new Uri()) ->withScheme('https') ->withHost('api.example.com') ->withPort(8080) ->withPath('/v1/users') ->withQuery('active=true&role=admin') ->withFragment('top'); echo $uri; // "https://api.example.com:8080/v1/users?active=true&role=admin#top" // Modify existing URI $uri = new Uri('https://example.com/users'); $newUri = $uri ->withPath('/users/123') ->withQuery('fields=name,email'); echo $newUri; // "https://example.com/users/123?fields=name,email" // URI with user info $uri = (new Uri('https://example.com')) ->withUserInfo('username', 'password'); echo $uri->getAuthority(); // "username:password@example.com" // Empty URI $emptyUri = new Uri(); echo $emptyUri->getHost(); // "" echo $emptyUri->getPath(); // "" ``` ## UploadedFile - File Upload Handling The `UploadedFile` class represents an uploaded file. It implements `Psr\Http\Message\UploadedFileInterface` and provides methods to access file metadata and move uploaded files to a target location. ```php getSize(); // 1024 echo $uploadedFile->getError(); // 0 (UPLOAD_ERR_OK) echo $uploadedFile->getClientFilename(); // "document.pdf" echo $uploadedFile->getClientMediaType(); // "application/pdf" // Get stream for reading $stream = $uploadedFile->getStream(); $contents = $stream->getContents(); // Move file to target location $uploadedFile->moveTo('/var/uploads/document.pdf'); // After moving, getStream() will throw RuntimeException // Handle upload errors $errorCodes = [ UPLOAD_ERR_OK => 'No error', UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize', UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE in form', UPLOAD_ERR_PARTIAL => 'File only partially uploaded', UPLOAD_ERR_NO_FILE => 'No file uploaded', UPLOAD_ERR_NO_TMP_DIR => 'Missing temp directory', UPLOAD_ERR_CANT_WRITE => 'Failed to write to disk', UPLOAD_ERR_EXTENSION => 'Upload stopped by extension', ]; // Create uploaded file with error $failedUpload = new UploadedFile( Stream::create(''), 0, UPLOAD_ERR_INI_SIZE, 'large-file.zip', 'application/zip' ); if ($failedUpload->getError() !== UPLOAD_ERR_OK) { echo "Upload failed: " . $errorCodes[$failedUpload->getError()]; } // Create from file path (for CLI/testing) $uploadedFile = new UploadedFile( '/tmp/phpXXXXXX', // Temp file path filesize('/tmp/phpXXXXXX'), UPLOAD_ERR_OK, 'upload.jpg', 'image/jpeg' ); ``` ## Integration with HTTP Clients The library integrates with PSR-18 HTTP clients like Buzz, Guzzle, and Symfony HTTP Client. Here is an example showing how to send HTTP requests using nyholm/psr7 with an HTTP client. ```php createRequest('GET', 'https://api.example.com/users'); $response = $client->sendRequest($request); echo $response->getStatusCode(); // 200 echo $response->getBody(); // Response body // Send POST request with JSON body $request = $factory->createRequest('POST', 'https://api.example.com/users') ->withHeader('Content-Type', 'application/json') ->withHeader('Accept', 'application/json') ->withBody($factory->createStream(json_encode([ 'name' => 'John Doe', 'email' => 'john@example.com' ]))); $response = $client->sendRequest($request); $data = json_decode($response->getBody()->getContents(), true); // Send request with authentication $request = $factory->createRequest('GET', 'https://api.example.com/protected') ->withHeader('Authorization', 'Bearer your-api-token'); $response = $client->sendRequest($request); // Handle response if ($response->getStatusCode() === 200) { $contentType = $response->getHeaderLine('Content-Type'); $body = $response->getBody()->getContents(); if (str_contains($contentType, 'application/json')) { $data = json_decode($body, true); } } ``` ## Server Request Creation from Globals For handling incoming HTTP requests in a server context, use the `nyholm/psr7-server` companion package to create ServerRequest objects from PHP superglobals. ```php fromGlobals(); // Access request data $method = $serverRequest->getMethod(); // "POST" $uri = $serverRequest->getUri(); // UriInterface $headers = $serverRequest->getHeaders(); // All headers $body = $serverRequest->getBody(); // StreamInterface $queryParams = $serverRequest->getQueryParams(); // $_GET equivalent $parsedBody = $serverRequest->getParsedBody(); // $_POST equivalent $uploadedFiles = $serverRequest->getUploadedFiles(); // Normalized $_FILES $cookies = $serverRequest->getCookieParams(); // $_COOKIE equivalent $serverParams = $serverRequest->getServerParams(); // $_SERVER // Example: Simple router $path = $serverRequest->getUri()->getPath(); $method = $serverRequest->getMethod(); if ($method === 'GET' && $path === '/api/users') { // Handle GET /api/users } elseif ($method === 'POST' && $path === '/api/users') { $data = json_decode($serverRequest->getBody()->getContents(), true); // Handle POST /api/users } ``` ## Emitting Responses To send HTTP responses to the client, use a response emitter like the one from `laminas/laminas-httphandlerrunner`. ```php createResponse(200) ->withHeader('Content-Type', 'text/plain') ->withBody($factory->createStream('Hello, World!')); $emitter->emit($response); // JSON API response $data = ['status' => 'success', 'data' => ['id' => 1, 'name' => 'John']]; $response = $factory->createResponse(200) ->withHeader('Content-Type', 'application/json') ->withBody($factory->createStream(json_encode($data))); $emitter->emit($response); // Redirect response $response = $factory->createResponse(302) ->withHeader('Location', 'https://example.com/login'); $emitter->emit($response); // Error response $response = $factory->createResponse(404) ->withHeader('Content-Type', 'application/json') ->withBody($factory->createStream(json_encode([ 'error' => 'Not Found', 'message' => 'The requested resource was not found' ]))); $emitter->emit($response); // File download response $fileStream = $factory->createStreamFromFile('/path/to/document.pdf', 'r'); $response = $factory->createResponse(200) ->withHeader('Content-Type', 'application/pdf') ->withHeader('Content-Disposition', 'attachment; filename="document.pdf"') ->withHeader('Content-Length', (string) $fileStream->getSize()) ->withBody($fileStream); $emitter->emit($response); ``` ## Summary Nyholm PSR-7 is ideal for applications requiring PSR-7 compliance with minimal overhead. Common use cases include building RESTful APIs, middleware-based applications, and HTTP client implementations. The library works seamlessly with frameworks like Slim, Mezzio, and any PSR-15 middleware stack. Its strict adherence to PSR-7 and PSR-17 specifications ensures maximum interoperability with the PHP ecosystem. Integration patterns typically involve using `Psr17Factory` as the central factory for all HTTP message creation, combining with `nyholm/psr7-server` for incoming request handling, and using a PSR-18 HTTP client for outgoing requests. The immutable design of all classes promotes functional programming patterns and thread safety. For production deployments, the library's small footprint and high performance make it an excellent choice for microservices and high-throughput applications.