pagemill/http

A PHP library for helping with HTTP requests and responses

Maintainers

Package info

github.com/dealnews/pagemill-http

pkg:composer/pagemill/http

Statistics

Installs: 11

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

3.0.0 2026-03-01 22:12 UTC

This package is auto-updated.

Last update: 2026-03-01 22:15:44 UTC


README

A lightweight, focused PHP library for handling HTTP requests and responses. Clean API, no bloat, gets out of your way.

Tests Coverage PHP Version

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

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_TYPEContent-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 pages
  • error.json - JSON error format
  • error.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_XML
  • CONTENT_TYPE_CSS, CONTENT_TYPE_CSV, CONTENT_TYPE_PLAIN
  • CONTENT_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

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for your changes
  4. Ensure all tests pass
  5. 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.