pagemill / http
A PHP library for helping with HTTP requests and responses
Requires
- php: ^8.2
- pagemill/accept: ^3.0
Requires (Dev)
- phpunit/phpunit: ^11.5
README
A lightweight, focused PHP library for handling HTTP requests and responses. Clean API, no bloat, gets out of your way.
Why PageMill HTTP?
Clean API: Simple, class-based interface for HTTP operations without framework overhead.
Production Ready: 125 tests (unit + functional), 83% coverage, battle-tested in production.
Focused: Does HTTP headers, status codes, and responses well. Nothing else.
Modern PHP: Type hints, PHPDoc everywhere, follows PSR standards (with DealNews conventions).
Installation
composer require pagemill/http
Quick Start
use PageMill\HTTP\Response; use PageMill\HTTP\Request; use PageMill\HTTP\HTTP; // Parse incoming request $request = new Request(); $auth_header = $request->header('Authorization'); // Build response $response = Response::init(); $response->status(HTTP::STATUS_OK); $response->contentType(HTTP::CONTENT_TYPE_JSON); $response->cache(3600); // Cache for 1 hour // Output echo json_encode(['message' => 'Hello, World!']);
Table of Contents
- Use Cases
- Request Handling
- Response Building
- Status Codes
- Redirects
- Cache Control
- CORS
- Error Handling
- Custom Headers
- Content-Type
- API Reference
- Testing
Use Cases
REST API Endpoint
use PageMill\HTTP\Response; use PageMill\HTTP\Request; use PageMill\HTTP\HTTP; $request = new Request(); $response = Response::init(); // Check authentication $api_key = $request->header('X-API-Key'); if (!$api_key || !isValidApiKey($api_key)) { $response->error(HTTP::STATUS_UNAUTHORIZED, 'Invalid API key'); exit; } // Set response headers $response->contentType(HTTP::CONTENT_TYPE_JSON); $response->cache(300); // 5 minutes $response->headers->set('X-API-Version', '1.0'); // Return data echo json_encode([ 'users' => getUserList(), 'count' => getUserCount(), ]);
CORS-Enabled API
$response = Response::init(); // Configure allowed origins $allowed_origins = ['app.example.com', 'admin.example.com']; // Check and set CORS headers if (!$response->accessAllowed($allowed_origins)) { $response->error(HTTP::STATUS_FORBIDDEN); exit; } $response->accessControl($allowed_origins); // Handle preflight if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { $response->status(HTTP::STATUS_NO_CONTENT); exit; }
Request Handling
Parse incoming HTTP headers from $_SERVER or Apache.
use PageMill\HTTP\Request; $request = new Request(); // Get all headers (normalized) $all_headers = $request->headers(); // ['Content-Type' => 'application/json', 'Authorization' => 'Bearer ...'] // Get specific header (case-insensitive) $content_type = $request->header('Content-Type'); $content_type = $request->header('content-type'); // Same result
Headers are normalized: HTTP_CONTENT_TYPE → Content-Type
Response Building
use PageMill\HTTP\Response; use PageMill\HTTP\HTTP; // Get singleton instance $response = Response::init(); // Set status code $response->status(HTTP::STATUS_OK); // 200 $response->status(HTTP::STATUS_NOT_FOUND); // 404 $response->status(HTTP::STATUS_INTERNAL_SERVER_ERROR); // 500 // Set content-type $response->contentType(HTTP::CONTENT_TYPE_JSON); $response->contentType(HTTP::CONTENT_TYPE_HTML, 'utf-8');
Status Codes
All standard HTTP status codes available as constants.
// Success 2xx HTTP::STATUS_OK // 200 HTTP::STATUS_CREATED // 201 HTTP::STATUS_NO_CONTENT // 204 // Redirection 3xx HTTP::STATUS_MOVED_PERMANENTLY // 301 HTTP::STATUS_FOUND // 302 HTTP::STATUS_TEMPORARY_REDIRECT // 307 // Client Error 4xx HTTP::STATUS_BAD_REQUEST // 400 HTTP::STATUS_UNAUTHORIZED // 401 HTTP::STATUS_FORBIDDEN // 403 HTTP::STATUS_NOT_FOUND // 404 HTTP::STATUS_TOO_MANY_REQUESTS // 429 // Server Error 5xx HTTP::STATUS_INTERNAL_SERVER_ERROR // 500 HTTP::STATUS_SERVICE_UNAVAILABLE // 503
Get descriptions:
$description = HTTP::$status_codes[404]; // "Not Found"
Redirects
// 302 temporary redirect (default) $response->redirect('https://example.com'); // 301 permanent redirect $response->redirect( 'https://example.com', HTTP::STATUS_MOVED_PERMANENTLY );
Note: redirect() calls exit() after setting headers.
Cache Control
// Cache for 1 hour $response->cache(3600); // Sends: // Cache-Control: max-age=3600 // Expires: <time + 3600> // Last-Modified: <now> // With custom last-modified $file_time = filemtime('/path/to/file'); $response->cache(3600, $file_time); // Disable caching $response->cache(0); // or $response->cache(HTTP::NOCACHE);
CORS (Cross-Origin Resource Sharing)
$allowed_origins = ['app.example.com', 'admin.example.com']; // Check if origin is allowed if ($response->accessAllowed($allowed_origins)) { // Set CORS headers $response->accessControl($allowed_origins); } // Handle preflight OPTIONS if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { $response->status(HTTP::STATUS_NO_CONTENT); $response->headers->set('Access-Control-Allow-Methods', 'GET, POST, DELETE'); $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization'); exit; }
Subdomain matching: 'example.com' matches www.example.com, api.example.com, etc.
Error Handling
// Basic error $response->error(HTTP::STATUS_NOT_FOUND); // With message $response->error(HTTP::STATUS_FORBIDDEN, 'Access denied'); // With template path $response->error( HTTP::STATUS_INTERNAL_SERVER_ERROR, 'Database error', '/path/to/templates' );
Templates (place in template directory):
404.html,500.html- HTML error pageserror.json- JSON error formaterror.xml- XML error format
Variables:
{{STATUS}}- HTTP status code{{MESSAGE}}- Error message
Example 404.html:
<!DOCTYPE html> <html> <head><title>{{STATUS}} - {{MESSAGE}}</title></head> <body> <h1>{{STATUS}}</h1> <p>{{MESSAGE}}</p> </body> </html>
Custom Headers
$response = Response::init(); $headers = $response->headers; // Set header (replaces existing) $headers->set('X-API-Version', '1.0'); // Add header (allows multiple values) $headers->add('X-Tag', 'production'); $headers->add('X-Tag', 'api'); // Remove header $headers->remove('X-Debug'); // Replace part of value $headers->set('X-Server', 'web-old-001'); $headers->replace('X-Server', 'old', 'new'); // Results in: X-Server: web-new-001 // Get all queued headers $all = $headers->getHeaders();
Content-Type Negotiation
// Predefined types $response->contentType(HTTP::CONTENT_TYPE_JSON); // application/json $response->contentType(HTTP::CONTENT_TYPE_HTML); // text/html $response->contentType(HTTP::CONTENT_TYPE_XML); // text/xml $response->contentType(HTTP::CONTENT_TYPE_CSS); // text/css $response->contentType(HTTP::CONTENT_TYPE_CSV); // text/csv // With charset $response->contentType(HTTP::CONTENT_TYPE_HTML, 'utf-8'); // Custom type $response->contentType('application/pdf');
Available constants:
CONTENT_TYPE_HTML,CONTENT_TYPE_JSON,CONTENT_TYPE_XMLCONTENT_TYPE_CSS,CONTENT_TYPE_CSV,CONTENT_TYPE_PLAINCONTENT_TYPE_IMAGE_GIF,CONTENT_TYPE_IMAGE_PNG,CONTENT_TYPE_IMAGE_JPEG
API Reference
Request
class Request { public function __construct(array $server = null); public function headers(): array; public function header(string $name): ?string; }
Response
class Response { public static function init(): Response; public static function resetInstance(): void; public function error(int $status, ?string $message = null, ?string $path = null): void; public function status(int $status): void; public function redirect(string $to, int $status = HTTP::STATUS_FOUND): void; public function contentType(string $type, ?string $charset = null): void; public function cache(int $ttl, int|string|null $last_modified = null): void; public function accessControl(array $allowed_origins): void; public function accessAllowed(array $allowed_origins): bool; }
Headers
class Headers { public function set(string $name, string $value): void; public function add(string $name, string $value): void; public function replace(string $name, string $search, string $value): void; public function remove(string $name): void; public function removeAll(): void; public function getHeaders(): array; }
Testing
# Run all tests ./vendor/bin/phpunit # With coverage ./vendor/bin/phpunit --coverage-text # Unit tests only ./vendor/bin/phpunit tests/Unit # Functional tests only ./vendor/bin/phpunit tests/Functional
Coverage: 83.46% (227/272 lines)
- Request: 97.37%
- Headers: 95.12%
- Callback: 100%
- Singleton: 100%
- Response: 76.40%
Requirements
- PHP 8.2 or higher
- pagemill/accept ^3.0
Contributing
- Fork the repository
- Create a feature branch
- Write tests for your changes
- Ensure all tests pass
- Submit a pull request
See AGENTS.md for coding guidelines.
License
BSD-3-Clause
Questions? Check the test suite - it demonstrates all features with 125 tests.