Alpha — v0.1.0. This module is in early testing. The API may change before a stable release. Feedback and bug reports are welcome.
A modern HTTP request handler for ProcessWire that provides an expressive, type-safe API for accessing request data. Handle form inputs, headers, files, cookies, JSON payloads, HTMX requests, and client information with an elegant, fluent interface - all while maintaining ProcessWire's philosophy of simplicity.
The Request lives at SimpleWire\Request\Request and can be installed standalone or as part of the SimpleWire suite.
Features
- Type-Safe HTTP Methods: Enum-based method detection (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS)
- Input Handling: Access and filter request data with sanitization built-in
- Typed Getters: Get input as strings, integers, floats, booleans, or dates
- HTMX Integration: Full support for HTMX headers and request detection
- Content Negotiation: Accept header parsing, JSON detection, content type preferences
- File Uploads: Simple file handling with validation
- Validation System: Built-in validation with 40+ rules
- Authentication: Bearer token extraction from Authorization headers
- Client Information: IP detection, user agent, host information
- Magic Methods: Dynamic property access for convenience
Quick Start
// Via the shorthand function (recommended)
$request = simplerequest();
// Via the global request() helper
$request = request();
// Via the ProcessWire API variable
$request = wire()->simplerequest;
// Via SimpleWire suite facade (if SimpleWire is installed)
$request = simplewire()->request();Basic Usage
<?php
namespace ProcessWire;
// Check request method
if (request()->isPost()) {
// Handle POST request
}
// Get input value
$email = request()->input('email');
// Get typed input
$age = request()->integer('age');
$price = request()->float('price');
$agreed = request()->boolean('terms');
// Check if HTMX request
if (request()->isHtmx()) {
// Return partial HTML
}
// Access headers
$token = request()->bearerToken();
$userAgent = request()->userAgent();
// Get client IP
$ip = request()->ip();With Router Integration
<?php
// /site/templates/api.php
namespace ProcessWire;
// Handle POST request
$page->route("post:users", function() {
// Validate input
$data = request()->validate([
'name' => 'required|min:3|max:50',
'email' => 'required|email',
'age' => 'required|integer|gte:18'
]);
// Get validated data
$name = request()->string('name');
$email = request()->string('email');
$age = request()->integer('age');
// Create user...
header('Content-Type: application/json');
return json_encode(['success' => true]);
});
// Handle JSON payload
$page->route("post:webhook", function() {
$data = request()->json();
// Process webhook data...
header('Content-Type: application/json');
return json_encode(['received' => true]);
});
$result = $page->dispatchRoutes();
if ($result !== null) {
echo $result;
}HTTP Method Detection
Check Request Method
// Individual method checks
if (request()->isGet()) { /* ... */ }
if (request()->isPost()) { /* ... */ }
if (request()->isPut()) { /* ... */ }
if (request()->isDelete()) { /* ... */ }
if (request()->isPatch()) { /* ... */ }
if (request()->isHead()) { /* ... */ }
if (request()->isOptions()) { /* ... */ }
// Generic check with string
if (request()->is('post')) { /* ... */ }
// Using enum (type-safe)
use SimpleWire\Request\HttpMethod;
if (request()->isMethod(HttpMethod::POST)) { /* ... */ }
// Get method as enum
$method = request()->getMethod();
// Returns: HttpMethod enum or nullRequest Type Detection
// AJAX request
if (request()->isAjax()) {
header('Content-Type: application/json');
echo json_encode($data);
}
// HTMX request
if (request()->isHtmx()) {
// Return partial HTML
}
// HTMX boosted request (full page load via HTMX)
if (request()->isBoosted()) {
// Return full page
}
// Combined check
if (request()->is('htmx')) { /* ... */ }
if (request()->is('ajax')) { /* ... */ }Input Handling
Getting Input Values
// Get single input (sanitized automatically)
$name = request()->input('name');
// Get all input as array
$data = request()->input();
// Get with magic methods
$email = request()->email;
$email = request()->email(); // Same as above
// Check if input exists
if (request()->has('email')) {
// Input key exists (even if empty)
}
// Check if any of multiple keys exist
if (request()->hasAny(['email', 'username'])) {
// At least one exists
}
// Check if input exists and is not empty
if (request()->filled('email')) {
// Email exists and has a value
}
// Check if any of multiple keys are filled
if (request()->anyFilled(['email', 'username'])) {
// At least one has a value
}
// Check if input is missing
if (request()->missing('phone')) {
// Phone field not present
}Typed Input Getters
// Get as string (sanitized via ProcessWire)
$name = request()->string('name');
// Returns: ?string
// Get as integer
$age = request()->integer('age');
// Returns: ?int
// Get as float
$price = request()->float('price');
// Returns: ?float
// Get as boolean
$agreed = request()->boolean('terms');
// Returns: bool (false if not present)
// Get as email (sanitized)
$email = request()->email('email');
// Returns: ?string
// Get as URL (sanitized)
$website = request()->urlInput('website');
// Returns: ?string
// Get as date
$birthdate = request()->date('birthdate');
// Returns: ?string (formatted date)
// Get as date with custom format
$date = request()->date('event_date', 'd/m/Y');
// Get as enum
use SimpleWire\Request\HttpMethod;
$method = request()->enum('method', HttpMethod::class);Filtering Input
// Get only specific keys
$credentials = request()->only(['email', 'password']);
// Returns: ['email' => '...', 'password' => '...']
// Get all except specific keys
$userData = request()->except(['_token', 'submit']);
// Returns: All input except specified keys
// Merge with default values
$data = request()->merge(['status' => 'active']);
// Overwrites existing values
// Merge only if missing
$data = request()->mergeIfMissing(['role' => 'user']);
// Only adds if key doesn't existConditional Input
// Execute callback when key exists
request()->whenHas('email', function($email) {
// Process email
}, function() {
// Default when missing
});
// Execute callback when key is filled (non-empty)
request()->whenFilled('search', function($query) {
// Perform search
});Specific Input Sources
// Get from query string ($_GET)
$page = request()->query('page');
$allQuery = request()->query();
// Get from POST data ($_POST)
$name = request()->post('name');
$allPost = request()->post();
// Get cookie value
$sessionId = request()->cookie('session_id');
$allCookies = request()->cookie();
// Get session value
$userId = request()->session('user_id');
$allSession = request()->session();
// Get uploaded file
$file = request()->file('avatar');
$allFiles = request()->file();
// Check if file was uploaded
if (request()->hasFile('avatar')) {
$file = request()->file('avatar');
}Headers
Accessing Headers
// Get single header
$contentType = request()->header('Content-Type');
$userAgent = request()->header('User-Agent');
// Get all headers
$headers = request()->headers();
// Check if header exists
if (request()->hasHeader('Authorization')) {
// Header present
}Bearer Token Authentication
// Extract bearer token from Authorization header
$token = request()->bearerToken();
if ($token) {
// Validate token
$user = authenticateToken($token);
}
// Example Authorization header:
// "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."Content Negotiation
Accept Header Checking
// Check if client accepts JSON
if (request()->acceptsJson()) {
header('Content-Type: application/json');
echo json_encode($data);
}
// Check if client accepts HTML
if (request()->acceptsHtml()) {
echo $page->render();
}
// Check if client accepts XML
if (request()->acceptsXml()) {
header('Content-Type: application/xml');
echo $xml;
}
// Check for specific content types
if (request()->accepts(['application/json', 'text/html'])) {
// Client accepts one of these types
}
// Check if client expects JSON (AJAX + JSON accept or content-type)
if (request()->expectsJson()) {
header('Content-Type: application/json');
echo json_encode($data);
}Content Type Preferences
// Get preferred content type from list
$preferred = request()->prefers(['application/json', 'text/html']);
// Returns: The first matching type or null
// Get preferred format as enum
$format = request()->preferredFormat();
// Returns: AcceptType::HTML, AcceptType::JSON, or AcceptType::XML
// Check if client wants JSON (accepts JSON or sends JSON)
if (request()->wantsJson()) {
header('Content-Type: application/json');
echo json_encode($data);
}JSON & Request Body
JSON Payloads
// Get entire JSON payload as array
$data = request()->json();
// Get specific key from JSON
$name = request()->json('name');
// Get as object instead of array
$data = request()->json('', false);
// Check if request has JSON content type
if (request()->isJson()) {
$data = request()->json();
}File Uploads
Handling Uploaded Files
// Get file array
$file = request()->file('avatar');
// Returns: Array with 'name', 'type', 'tmp_name', 'error', 'size'
// Check if file was uploaded successfully
if (request()->hasFile('avatar')) {
$file = request()->file('avatar');
// Move uploaded file
$filename = wire()->sanitizer->filename($file['name']);
$destination = wire()->config->paths->files . 'avatars/' . $filename;
move_uploaded_file($file['tmp_name'], $destination);
}
// Complete upload with validation
if (request()->isPost() && request()->hasFile('document')) {
$data = request()->validate([
'document' => 'required|file|mimes:pdf,doc,docx|max_size:5120'
]);
$file = request()->file('document');
$filename = wire()->sanitizer->filename($file['name']);
$destination = wire()->config->paths->files . 'uploads/' . $filename;
move_uploaded_file($file['tmp_name'], $destination);
}Validation
Basic Validation
// Validate input — throws InvalidArgumentException on failure
try {
$data = request()->validate([
'email' => 'required|email',
'password' => 'required|min:8',
'age' => 'integer|gte:18'
]);
// $data contains only the validated fields
} catch (\InvalidArgumentException $e) {
$errors = json_decode($e->getMessage(), true);
// Handle errors
}
// Safe validation — returns empty array on failure (no exception)
$data = request()->safe([
'email' => 'required|email',
'password' => 'required|min:8'
]);Custom Error Messages
try {
$data = request()->validate(
[
'email' => 'required|email',
'username' => 'required|min:3|max:20'
],
[
'email.required' => 'Please provide your email address',
'email.email' => 'Please provide a valid email address',
'username.required' => 'Username cannot be empty',
'username.min' => 'Username is too short (minimum 3 characters)',
'username.max' => 'Username is too long (maximum 20 characters)'
]
);
} catch (\InvalidArgumentException $e) {
$errors = json_decode($e->getMessage(), true);
}Available Validation Rules
Required & Presence
required- Field must be present and not emptynullable- Field can be null or empty
String Validations
string- Must be a stringemail- Must be valid email formaturl- Must be valid URL formatalpha- Only letters (a-z, A-Z)alpha_num- Letters and numbers onlyregex:/pattern/- Must match regex pattern
Numeric Validations
numeric- Must be numericinteger- Must be an integerdecimal- Must be a decimal numbergt:value- Greater than valuegte:value- Greater than or equal to valuelt:value- Less than valuelte:value- Less than or equal to value
Size & Length
min:5- Minimum length 5 charactersmax:100- Maximum length 100 charactersbetween:5,100- Length between 5 and 100size:10- Exactly 10 characters
Date Validations
date- Must be valid datedate_format:Y-m-d- Must match specific formatbefore:2024-12-31- Date must be beforeafter:2024-01-01- Date must be after
Boolean
boolean- Must be boolean value (true, false, 0, 1, '0', '1')accepted- Must be yes/on/true/1 (for checkboxes)
Arrays & Options
array- Must be an arrayin:red,blue,green- Must be one of the listed valuesnot_in:admin,root- Cannot be one of the listed values
File Validations
file- Must be an uploaded fileimage- Must be an image filemimes:jpeg,png,pdf- File type must matchmax_size:2048- Maximum file size in KB
Network
ip- Valid IP address (IPv4 or IPv6)ipv4- Valid IPv4 addressipv6- Valid IPv6 address
Other
json- Must be valid JSON string
Validation Examples
// User registration
$data = request()->validate([
'username' => 'required|alpha_num|min:3|max:20',
'email' => 'required|email',
'password' => 'required|min:8',
'age' => 'required|integer|gte:18|lte:100',
'website' => 'nullable|url',
'terms' => 'accepted'
]);
// File upload
$data = request()->validate([
'avatar' => 'required|image|max_size:2048',
'document' => 'required|file|mimes:pdf,doc,docx|max_size:5120'
]);
// Date range
$data = request()->validate([
'start_date' => 'required|date|date_format:Y-m-d',
'end_date' => 'required|date|after:start_date'
]);
// API token
$data = request()->validate([
'api_key' => 'required|regex:/^[a-zA-Z0-9]{32}$/',
'callback_url' => 'required|url'
]);
// Product form
$data = request()->validate([
'name' => 'required|min:3|max:100',
'price' => 'required|decimal|gt:0',
'quantity' => 'required|integer|gte:0',
'category' => 'required|in:electronics,clothing,books',
'description' => 'nullable|min:10'
]);HTMX Integration
HTMX Request Detection
// Check if request is from HTMX
if (request()->isHtmx()) {
// Return partial HTML
}
// Check if boosted (full page via HTMX)
if (request()->isBoosted()) {
// Return full page but know it's HTMX
}Client Information
User Agent & IP Address
// Get user agent
$userAgent = request()->userAgent();
// Get client IP (handles proxies via X-Forwarded-For)
$ip = request()->ip();
// Get all IPs in the chain
$ips = request()->ips();URL & Path Information
// Get request URL
$url = request()->url();
$fullUrl = request()->fullUrl(); // With protocol and host
// Get path only
$path = request()->path();
$decoded = request()->decodedPath();
// Get host
$host = request()->host();
$httpHost = request()->httpHost();
$full = request()->schemeAndHttpHost(); // https://example.com
// Check if secure
if (request()->isSecure()) {
// HTTPS connection
}Complete Examples
User Registration Form
<?php
namespace ProcessWire;
$page->route("post:register", function() {
try {
$data = request()->validate([
'username' => 'required|alpha_num|min:3|max:20',
'email' => 'required|email',
'password' => 'required|min:8',
'password_confirm' => 'required',
'age' => 'required|integer|gte:18',
'terms' => 'accepted'
], [
'username.required' => 'Please choose a username',
'terms.accepted' => 'You must accept the terms of service'
]);
} catch (\InvalidArgumentException $e) {
header('Content-Type: application/json');
http_response_code(422);
return json_encode(['errors' => json_decode($e->getMessage(), true)]);
}
// Check password match
if (request()->string('password') !== request()->string('password_confirm')) {
header('Content-Type: application/json');
http_response_code(422);
return json_encode(['errors' => ['password_confirm' => ['Passwords do not match']]]);
}
// Get sanitized input
$username = wire()->sanitizer->pageName(request()->string('username'));
$email = wire()->sanitizer->email(request()->string('email'));
$password = request()->string('password');
// Create user
$user = new User();
$user->name = $username;
$user->email = $email;
$user->pass = $password;
$user->save();
header('Content-Type: application/json');
http_response_code(201);
return json_encode([
'success' => true,
'user_id' => $user->id,
'message' => 'Registration successful'
]);
});
?>File Upload API
<?php
namespace ProcessWire;
$page->route("post:upload", function() {
// Check authentication
$token = request()->bearerToken();
if (!$token || !validateToken($token)) {
header('Content-Type: application/json');
http_response_code(401);
return json_encode(['error' => 'Unauthorized']);
}
// Validate file
if (!request()->hasFile('document')) {
header('Content-Type: application/json');
http_response_code(400);
return json_encode(['error' => 'No file uploaded']);
}
try {
$data = request()->validate([
'document' => 'required|file|mimes:pdf,doc,docx|max_size:10240',
'title' => 'required|min:3|max:100'
]);
} catch (\InvalidArgumentException $e) {
header('Content-Type: application/json');
http_response_code(422);
return json_encode(['errors' => json_decode($e->getMessage(), true)]);
}
// Process upload
$file = request()->file('document');
$title = request()->string('title');
$filename = wire()->sanitizer->filename($file['name']);
$destination = wire()->config->paths->files . 'uploads/' . $filename;
if (move_uploaded_file($file['tmp_name'], $destination)) {
$p = new Page();
$p->template = 'document';
$p->parent = wire()->pages->get('/documents/');
$p->title = $title;
$p->save();
header('Content-Type: application/json');
http_response_code(201);
return json_encode([
'success' => true,
'document_id' => $p->id,
'filename' => $filename
]);
}
header('Content-Type: application/json');
http_response_code(500);
return json_encode(['error' => 'File upload failed']);
});
?>Content Negotiation API
<?php
namespace ProcessWire;
$page->route("get:products/{id<integer>}", function($id) {
$product = wire()->pages->get($id);
if (!$product->id || $product->template != 'product') {
http_response_code(404);
header('Content-Type: application/json');
return json_encode(['error' => 'Product not found']);
}
$data = [
'id' => $product->id,
'title' => $product->title,
'price' => $product->price,
'description' => $product->description
];
// Content negotiation based on Accept header
if (request()->acceptsJson()) {
header('Content-Type: application/json');
return json_encode($data);
}
if (request()->acceptsXml()) {
header('Content-Type: application/xml');
return arrayToXml($data);
}
// Default to HTML
return $product->render();
});
?>Webhook Handler with JSON
<?php
namespace ProcessWire;
$page->route("post:webhooks/github", function() {
// Verify signature
$signature = request()->header('X-Hub-Signature-256');
$payload = file_get_contents('php://input');
if (!verifyGithubSignature($signature, $payload)) {
header('Content-Type: application/json');
http_response_code(401);
return json_encode(['error' => 'Invalid signature']);
}
// Parse JSON payload
$event = request()->json('action');
$repository = request()->json('repository');
$sender = request()->json('sender');
// Log webhook
wire()->log->save('webhooks', "GitHub {$event} on {$repository['name']} by {$sender['login']}");
// Process based on event
match ($event) {
'opened' => handlePullRequestOpened(request()->json()),
'closed' => handlePullRequestClosed(request()->json()),
default => null
};
header('Content-Type: application/json');
return json_encode(['received' => true]);
});
?>Best Practices
- Always Validate Input: Use the built-in validation system for user data
- Use Typed Getters: Prefer
integer(),string()overinput()for type safety - Sanitize Output: Input is auto-sanitized, but always sanitize for specific contexts
- Check Request Type: Use
isHtmx(),expectsJson()for appropriate responses - Handle File Uploads Safely: Always validate files and use
hasFile()before processing - Use Content Negotiation: Return appropriate format based on Accept headers
- Validate Tokens: Always verify bearer tokens before processing authenticated requests
- Use Custom Error Messages: Provide user-friendly validation messages
- Access PW API via
wire(): Inside route handlers, usewire()->pages,wire()->sanitizer, etc.
Security Considerations
Important Security Notes:
- Input Sanitization: Raw input values are trimmed but not escaped — always use typed getters (
string(),integer(),email(), etc.) or ProcessWire's sanitizer to sanitize for your specific output context - File Uploads: Always validate file types and sizes. Never trust client-provided filenames
- Bearer Tokens: Validate and verify tokens on the server side. Don't trust client headers alone
- IP Detection: Be aware that IPs can be spoofed. Don't rely solely on IP for critical security
- Rate Limiting: Implement rate limiting for public APIs to prevent abuse
- HTTPS Only: Always use HTTPS for sensitive data transmission
- Validation: Server-side validation is mandatory - never trust client-side validation alone
Performance Tips
- Use Specific Getters:
string('key')is faster thaninput('key')for typed values - Validate Only What You Need: Don't validate fields you don't use
- Check Request Type Early: Use
isPost(),isHtmx()early to avoid unnecessary processing - Use
has()Before Access: Check if input exists before processing to avoid errors
Troubleshooting
Input not being captured:
- Check request method matches route method
- Verify form field names match
input()keys - For JSON payloads, use
json()instead ofinput() - Check Content-Type header for JSON requests
Validation not working:
- Ensure input field names match validation rules
- Check rule syntax (use pipe | separator)
- For parameterized rules, use colon:
min:5 - File validation requires actual file upload, not just filename
File uploads failing:
- Check PHP upload limits (
upload_max_filesize,post_max_size) - Verify destination directory exists and is writable
- Use
hasFile()to check upload success before processing - Check file permissions on the destination folder
Headers not accessible:
- Some servers don't expose all headers - check
apache_request_headers()availability - Custom headers may need CORS configuration
- Authorization headers might be stripped by some server configs
Debugging
// Log all input
wire()->log->save('requests', print_r(request()->input(), true));
// Log headers
wire()->log->save('headers', print_r(request()->headers(), true));
// Check request details
wire()->log->save('request-info', sprintf(
'Method: %s, HTMX: %s, AJAX: %s, IP: %s',
request()->getMethod()?->value,
request()->isHtmx() ? 'yes' : 'no',
request()->isAjax() ? 'yes' : 'no',
request()->ip()
));API Reference
Global Functions
simplerequest(): \SimpleWire\Request\Request // recommended shorthand
request(): \SimpleWire\Request\Request // aliasEnums
SimpleWire\Request\HttpMethod // GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
SimpleWire\Request\AcceptType // HTML, JSON, XMLHTTP Methods
getMethod(): ?HttpMethod- Get request method as enumisMethod(HttpMethod|string $method): bool- Check specific methodisGet(): boolisPost(): boolisPut(): boolisDelete(): boolisPatch(): boolisHead(): boolisOptions(): boolisAjax(): boolisHtmx(): boolisBoosted(): boolis(string $key): bool- Generic check
Input Methods
input(string $key = ''): mixedstring(string $key): ?stringinteger(string $key): ?intfloat(string $key): ?floatboolean(string $key): boolemail(string $key): ?stringurlInput(string $key): ?stringarray(string $key): arraydate(string $key, string $format = 'Y-m-d'): ?stringenum(string $key, string $enumClass): mixedhas(array|string $keys): boolhasAny(array $keys): boolfilled(array|string $keys): boolanyFilled(array $keys): boolmissing(string $key): boolonly(array $keys): arrayexcept(array|string $keys): arraymerge(array $input): arraymergeIfMissing(array $input): arraywhenHas(string $key, callable $callback, ?callable $default = null): mixedwhenFilled(string $key, callable $callback, ?callable $default = null): mixed
Specific Input Sources
query(string $key = ''): mixedpost(string $key = ''): mixedcookie(string $key = ''): mixedsession(string $key = ''): mixedfile(string $key = ''): mixedhasFile(string $key): bool
Headers
header(string $key): ?stringheaders(): arrayhasHeader(string $key): boolbearerToken(): ?string
Content Negotiation
accepts(string|array $contentTypes): boolacceptsJson(): boolacceptsHtml(): boolacceptsXml(): boolprefers(array $contentTypes): ?stringpreferredFormat(): AcceptTypewantsJson(): boolexpectsJson(): bool
JSON
json(string $key = '', bool $assoc = true): mixedisJson(): bool
URL & Path
requestUrl(): stringurl(): stringfullUrl(): stringpath(): stringdecodedPath(): stringhost(): stringhttpHost(): stringschemeAndHttpHost(): stringisSecure(): bool
Client Information
ip(): ?stringips(): arrayuserAgent(): ?string
Validation
validate(array $rules, array $messages = []): array- ThrowsInvalidArgumentExceptionon failurevalidated(array $rules, array $messages = []): array- Alias forvalidate()safe(array $rules, array $messages = []): array- Returns empty array on failure
License
This module is released under the MIT License.
More modules by WireCodex
- Added 1 week ago by WireCodex
- Added 3 days ago by WireCodex
- Added 3 days ago by WireCodex
- Added 3 days ago by WireCodex
SimpleQuery
GraphQL-like query engine for ProcessWire pages with caching, rate limiting, and write support.0Added 3 days ago by WireCodexSimpleQueue
Background job queue with priority, delayed execution, retry, and LazyCron-based processing.0Added 3 days ago by WireCodexSimpleAsset
Asset management for ProcessWire. Resolves, groups, and renders CSS/JS assets from CDN sources or local paths with cache-busting, SRI, and inline threshold support.0Added 3 days ago by WireCodex
Install and use modules at your own risk. Always have a site and database backup before installing new modules.