Skip to content

phpdot/http

Repository files navigation

phpdot/http

Advanced HTTP library for PHP. PSR-7/17 native. Framework-agnostic.

Install

composer require phpdot/http

What It Is

A standalone HTTP library that wraps any PSR-7 implementation with a rich, typed API:

  • Request — decorator over ServerRequestInterface with typed input, content negotiation, trusted proxies, method override
  • ResponseFactory — builds JSON, HTML, file downloads, redirects, cache headers, RFC 9457 problem details
  • Cookie — immutable value object per RFC 6265
  • HTTP Exceptions — typed exceptions with RFC 9457 Problem Details
  • IpUtils — IPv4/IPv6 subnet checking for trusted proxy support
  • StatusText — complete RFC 9110 status code mapping

Dependencies

php >= 8.3
psr/http-message ^2.0
psr/http-factory ^1.0

Two PSR interfaces. Nothing else.


Architecture

Request/Response Flow

                    Any PSR-7 Implementation
                    (Nyholm, Guzzle, Laminas)
                              │
                              ▼
                 ┌────────────────────────┐
                 │   ServerRequestInterface │
                 └────────────────────────┘
                              │
                              ▼
              ┌───────────────────────────────┐
              │        PHPdot\Http\Request     │
              │                               │
              │  Decorates PSR-7 request with: │
              │  • Typed input accessors       │
              │  • Content negotiation         │
              │  • Trusted proxy support       │
              │  • Method override             │
              │  • Cookie/file helpers         │
              │  • URL pattern matching        │
              └───────────────────────────────┘
                              │
                      Your Application
                              │
                              ▼
              ┌───────────────────────────────┐
              │   PHPdot\Http\ResponseFactory  │
              │                               │
              │  Builds PSR-7 responses:       │
              │  • json(), html(), text()      │
              │  • download(), file() (Range)  │
              │  • redirect(), noContent()     │
              │  • Cache: ETag, Last-Modified  │
              │  • Cookies, Problem Details    │
              └───────────────────────────────┘
                              │
                              ▼
                 ┌────────────────────────┐
                 │    ResponseInterface     │
                 └────────────────────────┘

Request

Wraps any ServerRequestInterface. Implements ServerRequestInterface itself — drop-in compatible.

use PHPdot\Http\Request;

$request = new Request($psrRequest);

Input — Typed Accessors

$request->query('page', 1);              // query string with default
$request->input('email');                 // parsed body
$request->all();                          // merged query + body (body wins)

$request->only(['email', 'name']);        // pick specific keys
$request->except(['password']);           // exclude keys

$request->has('email');                   // exists and not empty
$request->hasAny(['email', 'phone']);     // at least one exists
$request->filled('name');                 // exists, not empty, not null
$request->missing('token');              // not present at all

Typed — Safe, Never Throws

$request->string('name', '');            // string, default on failure
$request->integer('page', 1);            // int via filter_var
$request->float('price', 0.0);          // float via filter_var
$request->boolean('active', false);      // "true","1","on","yes" → true
$request->array('tags', []);             // array or default
$request->date('created_at');            // DateTimeImmutable or null
$request->enum('color', Color::class);   // BackedEnum or null

Method Override

For HTML forms that only support GET/POST:

$request->method();                       // intended method (_method or X-HTTP-Method-Override)
$request->realMethod();                   // actual HTTP method
$request->isGet();                        // check intended method
$request->isPost();
$request->isPut();
$request->isDelete();

Headers

$request->header('Accept');               // first value or null
$request->headers('Accept');              // all values as array
$request->bearerToken();                  // "Bearer <token>" → token
$request->basicCredentials();             // ['username' => ..., 'password' => ...]
$request->userAgent();
$request->contentType();                  // without parameters
$request->contentLength();

Content Negotiation

$request->accepts('application/json');                   // bool
$request->wantsJson();                                    // Accept contains json
$request->preferredType(['text/html', 'application/json']); // best match or null
$request->preferredLanguage(['en', 'ar', 'fr']);            // best match or null

Client & Connection

$request->ip();                           // trusted-proxy-aware
$request->ips();                          // full X-Forwarded-For chain
$request->scheme();                       // trusted-proxy-aware
$request->host();                         // trusted-proxy-aware
$request->port();                         // trusted-proxy-aware
$request->isSecure();                     // trusted-proxy-aware
$request->isXhr();                        // XMLHttpRequest
$request->isJson();                       // Content-Type contains json
$request->isPrefetch();                   // Purpose/Sec-Purpose: prefetch

Trusted Proxies

Configure once at boot:

Request::setTrustedProxies(
    ['10.0.0.0/8', '172.16.0.0/12'],
    Request::HEADER_X_FORWARDED_ALL,
);

After configuration, ip(), scheme(), host(), port(), isSecure() automatically read from forwarded headers when the request comes through a trusted proxy.

Header constants:

Request::HEADER_X_FORWARDED_FOR   // 0b00001
Request::HEADER_X_FORWARDED_HOST  // 0b00010
Request::HEADER_X_FORWARDED_PORT  // 0b00100
Request::HEADER_X_FORWARDED_PROTO // 0b01000
Request::HEADER_FORWARDED         // 0b10000 (RFC 7239)
Request::HEADER_X_FORWARDED_ALL   // 0b01111

URL

$request->path();                         // /users/42
$request->url();                          // https://example.com/users/42
$request->fullUrl();                      // https://example.com/users/42?page=2
$request->segment(1);                     // "users" (1-indexed)
$request->segments();                     // ["users", "42"]
$request->is('api/*');                    // wildcard pattern matching
$request->is('admin/**');                 // ** matches multiple segments

Route Parameters

$request->route('id');                    // reads from getAttribute('id')

Files & Cookies

$request->file('avatar');                 // UploadedFileInterface or null
$request->hasFile('avatar');              // exists AND no upload error
$request->allFiles();                     // all uploaded files

$request->cookie('session_id');           // cookie value or null
$request->cookies();                      // all cookies
$request->hasCookie('session_id');        // bool

Escape Hatch

$request->psr();                          // inner ServerRequestInterface

ResponseFactory

Builds PSR-7 responses. Depends on PSR-17 factories.

use PHPdot\Http\ResponseFactory;

$factory = new ResponseFactory($responseFactory, $streamFactory);

Basic Responses

$factory->json(['status' => 'ok']);                    // 200, application/json
$factory->json(['user' => $user], 201);                // custom status
$factory->json($data, 200, JSON_PRETTY_PRINT);         // custom options

$factory->html('<h1>Hello</h1>');                      // 200, text/html; charset=UTF-8
$factory->text('plain text');                           // 200, text/plain; charset=UTF-8
$factory->xml('<root/>');                               // 200, application/xml; charset=UTF-8

$factory->redirect('/login');                           // 302 redirect
$factory->redirect('/new-url', 301);                    // permanent redirect

$factory->noContent();                                  // 204
$factory->raw(202);                                     // empty response with status

File Responses

// Force download — UTF-8 filename support (RFC 5987)
$factory->download('/path/to/تقرير.pdf');
// Content-Disposition: attachment; filename="-.pdf"; filename*=UTF-8''%D8%AA%D9%82%D8%B1%D9%8A%D8%B1.pdf

// Inline with Range support (RFC 7233)
$factory->file('/path/to/video.mp4', $request->psr());
// No Range    → 200, full content, Accept-Ranges: bytes
// Range       → 206, partial content, Content-Range header
// Bad Range   → 416, Range Not Satisfiable

Cache Helpers

$response = $factory->withCache($response, maxAge: 3600, public: true, immutable: true);
// Cache-Control: public, max-age=3600, immutable

$response = $factory->withEtag($response, md5($content));
// ETag: "abc123"

$response = $factory->withEtag($response, md5($content), weak: true);
// ETag: W/"abc123"

$response = $factory->withLastModified($response, $date);
// Last-Modified: Thu, 01 Jan 2026 00:00:00 GMT

if ($factory->isNotModified($request->psr(), $response)) {
    return $factory->notModified();  // 304
}

Cookies

use PHPdot\Http\Cookie;

$cookie = Cookie::create('session', $token)
    ->withPath('/')
    ->withSecure()
    ->withHttpOnly()
    ->withSameSite('Lax')
    ->withMaxAge(3600);

$response = $factory->withCookie($response, $cookie);
$response = $factory->withoutCookie($response, 'old_session');

Problem Details (RFC 9457)

$factory->problem(
    status: 422,
    detail: 'Email is already taken',
    extensions: ['field' => 'email'],
);
// Content-Type: application/problem+json
// {"type":"about:blank","title":"Unprocessable Content","status":422,"detail":"Email is already taken","field":"email"}

Cookie

Immutable value object. Builds and parses Set-Cookie headers per RFC 6265.

use PHPdot\Http\Cookie;

$cookie = Cookie::create('session', 'abc123')
    ->withPath('/')
    ->withDomain('.example.com')
    ->withSecure()
    ->withHttpOnly()
    ->withSameSite('Strict')
    ->withMaxAge(86400);

// Serialize
$header = $cookie->toHeaderString();
// session=abc123; Path=/; Domain=.example.com; Max-Age=86400; Secure; HttpOnly; SameSite=Strict

// Parse
$parsed = Cookie::fromHeaderString($header);

// Inspect
$cookie->getName();        // "session"
$cookie->getValue();       // "abc123"
$cookie->isSecure();       // true
$cookie->isHttpOnly();     // true
$cookie->isExpired();      // false

Validation

  • SameSite=None requires Secure
  • Partitioned requires Secure
  • Cookie names validated per RFC 6265

HTTP Exceptions

All extend HttpException. All support RFC 9457 Problem Details.

use PHPdot\Http\Exception\NotFoundException;
use PHPdot\Http\Exception\UnprocessableEntityException;
use PHPdot\Http\Exception\TooManyRequestsException;
use PHPdot\Http\Exception\MethodNotAllowedException;

throw new NotFoundException('User not found');

throw new UnprocessableEntityException(
    errors: ['email' => 'Already taken', 'name' => 'Required'],
    message: 'Validation failed',
);

throw new TooManyRequestsException(
    retryAfter: 60,
    message: 'Rate limit exceeded',
);

throw new MethodNotAllowedException(
    allowedMethods: ['GET', 'POST'],
);

Problem Details

$exception->toProblemDetails();
// [
//     'type' => 'about:blank',
//     'title' => 'Not Found',
//     'status' => 404,
//     'detail' => 'User not found',
// ]

Available Exceptions

Class Status
BadRequestException 400
UnauthorizedException 401
ForbiddenException 403
NotFoundException 404
MethodNotAllowedException 405
RequestTimeoutException 408
ConflictException 409
PayloadTooLargeException 413
UnsupportedMediaTypeException 415
UnprocessableEntityException 422
TooManyRequestsException 429
ServerErrorException 500
BadGatewayException 502
ServiceUnavailableException 503
GatewayTimeoutException 504

IpUtils

Standalone IPv4/IPv6 utility.

use PHPdot\Http\IpUtils;

IpUtils::inRange('10.0.0.5', '10.0.0.0/8');          // true
IpUtils::inRange('::1', '::1/128');                    // true
IpUtils::matches('10.0.0.5', ['10.0.0.0/8', '172.16.0.0/12']); // true
IpUtils::isPrivate('10.0.0.1');                        // true (RFC 1918)
IpUtils::isPrivate('8.8.8.8');                         // false
IpUtils::isIPv4('192.168.1.1');                        // true
IpUtils::isIPv6('::1');                                // true

StatusText

Complete RFC 9110 mapping.

use PHPdot\Http\StatusText;

StatusText::get(200);  // "OK"
StatusText::get(404);  // "Not Found"
StatusText::get(418);  // "I'm a Teapot"
StatusText::get(999);  // ""

Package Structure

src/
├── Request.php                 PSR-7 decorator with rich API
├── ResponseFactory.php         Builds all response types
├── Cookie.php                  Immutable RFC 6265 cookie
├── IpUtils.php                 IPv4/IPv6 subnet checking
├── StatusText.php              RFC 9110 status codes
└── Exception/
    ├── HttpException.php       Base (RFC 9457 Problem Details)
    ├── BadRequestException.php
    ├── UnauthorizedException.php
    ├── ForbiddenException.php
    ├── NotFoundException.php
    ├── MethodNotAllowedException.php
    ├── RequestTimeoutException.php
    ├── ConflictException.php
    ├── PayloadTooLargeException.php
    ├── UnsupportedMediaTypeException.php
    ├── UnprocessableEntityException.php
    ├── TooManyRequestsException.php
    ├── ServerErrorException.php
    ├── BadGatewayException.php
    ├── ServiceUnavailableException.php
    └── GatewayTimeoutException.php

Usage with Frameworks

With phpdot/routing

$router->get('/users/{id:int}', function (ServerRequestInterface $serverRequest, int $id) use ($factory): ResponseInterface {
    $request = new Request($serverRequest);

    if ($request->wantsJson()) {
        return $factory->json(['id' => $id, 'name' => 'Omar']);
    }

    return $factory->html("<h1>User {$id}</h1>");
});

With Slim

$app->get('/users/{id}', function ($request, $response, $args) use ($factory) {
    $req = new Request($request);
    return $factory->json(['id' => $req->integer('id')]);
});

With Any PSR-15 Middleware

final class ApiErrorHandler implements MiddlewareInterface
{
    public function __construct(private ResponseFactory $factory) {}

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        try {
            return $handler->handle($request);
        } catch (HttpException $e) {
            return $this->factory->problem(
                status: $e->getStatusCode(),
                detail: $e->getDetail(),
                extensions: $e->getExtensions(),
            );
        }
    }
}

Development

composer test        # PHPUnit (236 tests)
composer analyse     # PHPStan level 10
composer cs-fix      # PHP-CS-Fixer
composer cs-check    # Dry run
composer check       # All three

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages