Advanced HTTP library for PHP. PSR-7/17 native. Framework-agnostic.
composer require phpdot/httpA standalone HTTP library that wraps any PSR-7 implementation with a rich, typed API:
- Request — decorator over
ServerRequestInterfacewith 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
php >= 8.3
psr/http-message ^2.0
psr/http-factory ^1.0
Two PSR interfaces. Nothing else.
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 │
└────────────────────────┘
Wraps any ServerRequestInterface. Implements ServerRequestInterface itself — drop-in compatible.
use PHPdot\Http\Request;
$request = new Request($psrRequest);$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$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 nullFor 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();$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();$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$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: prefetchConfigure 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$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$request->route('id'); // reads from getAttribute('id')$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$request->psr(); // inner ServerRequestInterfaceBuilds PSR-7 responses. Depends on PSR-17 factories.
use PHPdot\Http\ResponseFactory;
$factory = new ResponseFactory($responseFactory, $streamFactory);$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// 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$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
}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');$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"}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- SameSite=None requires Secure
- Partitioned requires Secure
- Cookie names validated per RFC 6265
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'],
);$exception->toProblemDetails();
// [
// 'type' => 'about:blank',
// 'title' => 'Not Found',
// 'status' => 404,
// 'detail' => 'User not found',
// ]| 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 |
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'); // trueComplete 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); // ""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
$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>");
});$app->get('/users/{id}', function ($request, $response, $args) use ($factory) {
$req = new Request($request);
return $factory->json(['id' => $req->integer('id')]);
});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(),
);
}
}
}composer test # PHPUnit (236 tests)
composer analyse # PHPStan level 10
composer cs-fix # PHP-CS-Fixer
composer cs-check # Dry run
composer check # All threeMIT