SimpleResponse

HTTP response builder with HTMX support, redirects, and content negotiation.

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 fluent HTTP response builder for ProcessWire with full HTMX support, redirects, cookies, and content negotiation. Build JSON APIs, send HTML fragments, manage headers, and handle redirects with an elegant chainable interface.

The Response lives at SimpleWire\Response\Response and can be installed standalone or as part of the SimpleWire suite.

Features


  • Fluent Interface: Chainable methods for building responses elegantly
  • Multiple Response Types: JSON, HTML, plain text, XML, files, downloads
  • HTMX Integration: Full support for HTMX response headers, triggers, retargeting, and actions
  • HTTP Headers & Cookies: Simple management of headers, cookies, caching, and CORS
  • Redirects: Full-featured redirect builder with flash data and session support
  • View Integration: Optional view() method when SimpleRender is also installed
  • Content Negotiation: Status codes, content types, and accept header handling

Quick Start


// Via the shorthand function (recommended)
$response = simpleresponse();

// Via the global response() helper
$response = response();

// Via SimpleWire suite facade (if SimpleWire is installed)
$response = simplewire()->response();

// Redirect shorthand
$redirect = redirect('/thank-you');

Basic Usage

<?php
namespace ProcessWire;

// JSON response
response()->json(['status' => 'ok', 'data' => $results])->execute();

// HTML response
response()->html('<p>Hello!</p>')->execute();

// Redirect
redirect('/dashboard')->withFlash('message', 'Saved!');

// With status code
response()->json(['error' => 'Not found'], 404)->execute();

With SimpleRouter Integration

<?php
// /site/templates/api.php
namespace ProcessWire;

$page->route("get:users", function() {
    $users = wire()->users->find("");
    $data = [];
    foreach ($users as $user) {
        $data[] = ['id' => $user->id, 'name' => $user->name];
    }
    return response()->json($data);
});

$page->route("post:users", function() {
    // ... create user
    return response()->json(['id' => $newUser->id], 201);
});

$result = $page->dispatchRoutes();
if ($result !== null) echo $result;

Response Types


JSON

// Basic
response()->json(['message' => 'Success', 'data' => $data])->execute();

// With status code
response()->json(['error' => 'Unauthorized'], 401)->execute();

// Extract a specific node from the array
$payload = ['users' => [...], 'meta' => [...]];
response()->json($payload, 200, 'users')->execute(); // sends only the 'users' node

HTML

response()->html('<div>Saved!</div>')->execute();

// With status code
response()->html('<p>Not Found</p>', 404)->execute();

Plain Text

response()->text('Hello World')->execute();

XML

response()->xml('<root><item>value</item></root>')->execute();

File (inline display)

// Display in browser (PDF, image, etc.)
response()->file('/path/to/document.pdf')->execute();

// With custom headers
response()->file('/path/to/image.jpg', ['Cache-Control' => 'max-age=3600'])->execute();

Download (force download)

// Basic download
response()->download('/path/to/report.pdf')->execute();

// With custom filename
response()->download('/path/to/file.pdf', 'Monthly-Report.pdf')->execute();

// With extra headers
response()->download('/path/to/file.zip', 'archive.zip', ['X-Custom' => 'value'])->execute();

Render (requires SimpleRender)

When the SimpleRender module is installed, render() becomes available as a convenience shorthand — it maps directly to response()->html(render('template', $data)):

// Equivalent to: response()->html(render('products/list', $data))
response()->render('products/list', ['products' => $products])->execute();

// With status code
response()->render('errors/404', [], 404)->execute();

Without SimpleRender, pass the rendered string directly:

response()->html(render('products/list', $data))->execute();
// or
response()->html($page->render())->execute();

JSONP

// For cross-domain requests
response()->withCallback('myCallback', ['data' => 'value'])->execute();
// Output: myCallback({"data":"value"});

// If ?callback=name is present in the query string, that name is used instead.
// The callback name must be a valid JavaScript identifier — an InvalidArgumentException
// is thrown if the value fails validation.

Sending the Response


// execute() sends headers + body, exits if halt flag is set
response()->json($data)->execute();

// send() sets content and marks halt=true, chain execute() to dispatch
response()->send('<p>content</p>', ['X-Custom' => 'value'])->execute();

// output() returns the content string without sending
$html = response()->html($markup)->output();

Headers & Cookies


Setting Headers

// Single header
response()->html($content)->header('X-API-Version', '1.0')->execute();

// Multiple headers at once
response()->json($data)->headers([
    'X-API-Version' => '1.0',
    'X-Rate-Limit'  => '1000',
])->execute();

// Generic with() — adds a header
response()->html($content)->with('X-Custom', 'value')->execute();

CORS

response()->json($data)->allow(['https://example.com', 'https://app.example.com'])->execute();

Cache Control

// Cache-Control string
response()->html($content)->cache('public, max-age=3600')->execute();

// With ETag
response()->html($content)->cache('public', md5($content))->execute();

// With max-age integer (appends max-age to Cache-Control and sets Expires header)
response()->html($content)->cache('public', '', 3600)->execute();
// Produces: Cache-Control: public, max-age=3600  +  Expires: <date>

Cookies

// Set a cookie (60 minutes)
response()->html('Welcome back!')->cookie('user_id', '12345', 60)->execute();

// Full options
response()->html('Logged in')->cookie(
    name: 'session',
    value: 'abc123',
    minutes: 120,
    path: '/',
    domain: 'example.com',
    secure: true,
    httpOnly: true,
    sameSite: 'Strict'
)->execute();

// Remove a cookie
response()->html('Logged out')->withoutCookie('session')->execute();

Status Codes


response()->json($data)->statusCode(201)->execute();      // Created
response()->html($content)->statusCode(404)->execute();   // Not Found
response()->json($err)->statusCode(422)->execute();       // Unprocessable

// Get the status category
$r = response()->statusCode(422);
$category = $r->getStatusCategory(); // HttpStatusCategory::CLIENT_ERROR

Available categories: HttpStatusCategory::INFORMATIONAL, SUCCESS, REDIRECTION, CLIENT_ERROR, SERVER_ERROR.

Redirects


Use the redirect() global function to get a ResponseRedirect instance:

// Basic redirect (fires on object destruction)
redirect('/dashboard');

// Back to previous page
redirect('/')->back();

// Back with form input preserved in session
redirect('/form')->back(withInput: true);

// With flash data
redirect('/dashboard')->withFlash('success', 'Profile updated!');

// Multiple flash items
redirect('/dashboard')->withFlash(['success' => 'Saved', 'count' => '3']);

// Redirect to page by ID
redirect('/')->route(1042);

// Redirect to first page matching template name
redirect('/')->route('contact');

// With query parameters
redirect('/')->route('products', ['sort' => 'price', 'dir' => 'asc']);

// External URL
redirect('/')->away('https://example.com');

// Preserve input (stores POST/GET in session as _flash_input)
redirect('/form')->withInput(['password']); // excludes 'password' field

HTMX Integration


URL Management

// Push URL to browser history
response()->html($content)->pushUrl('/new-url')->execute();

// Replace current URL without adding to history
response()->html($content)->replaceUrl('/updated-url')->execute();

Retargeting & Reswapping

// Change the target element
response()->html('<div>New content</div>')->retarget('#other-element')->execute();

// Change the swap strategy (string)
response()->html('<div>Content</div>')->reswap('beforeend')->execute();

// Using the enum (type-safe)
use SimpleWire\Response\SwapStrategy;
response()->html($content)->reswap(SwapStrategy::BEFORE_END)->execute();

// Available strategies: innerHTML, outerHTML, beforebegin, afterbegin,
//                       beforeend, afterend, delete, none

Triggers

// Simple event trigger
response()->html($form)->addTrigger('formSubmitted')->execute();

// With JSON params
response()->html($content)->addTrigger('showNotification', '{"type":"success"}')->execute();

// Trigger timing
use SimpleWire\Response\TriggerTiming;
response()->html($content)
    ->addTrigger('myEvent', '', TriggerTiming::RECEIVE)  // default: on receive
    ->addTrigger('afterSwap', '', TriggerTiming::SWAP)
    ->addTrigger('afterSettle', '', TriggerTiming::SETTLE)
    ->execute();

HTMX Actions (ResponseAction)

For responses that are purely HTMX control signals (no body), use ResponseAction:

use SimpleWire\Response\ResponseAction;

// Client-side redirect (HTMX handles it, no full page reload)
(new ResponseAction())->actionRedirect('/dashboard')->execute();

// Navigate with target + swap options
(new ResponseAction())->hxLocation('/products', [
    'target' => '#content',
    'swap'   => 'innerHTML',
])->execute();

// Force full page refresh
(new ResponseAction())->actionRefresh()->execute();

// Stop an HTMX polling loop
(new ResponseAction())->actionStopPolling()->execute();

Complete Examples


RESTful API Endpoint

<?php
// /site/templates/api.php
namespace ProcessWire;

$page->route("get:products", function() {
    $products = wire()->pages->find("template=product");
    $data = array_map(fn($p) => [
        'id'    => $p->id,
        'title' => $p->title,
        'price' => $p->price,
    ], $products->getArray());

    return response()
        ->json(['products' => $data, 'count' => count($data)])
        ->cache('public, max-age=300')
        ->allow(['https://example.com']);
});

$page->route("post:products", function() {
    $body = json_decode(file_get_contents('php://input'), true) ?? [];

    if (empty($body['title'])) {
        return response()->json(['error' => 'Title is required'], 400);
    }

    $product = new Page();
    $product->template = 'product';
    $product->parent   = wire()->pages->get('/products/');
    $product->title    = wire()->sanitizer->text($body['title']);
    $product->save();

    return response()->json(['id' => $product->id], 201);
});

$result = $page->dispatchRoutes();
if ($result !== null) echo $result;

HTMX Partial Update

<?php
namespace ProcessWire;

$page->route("post:cart/add/{id<integer>}", function(int $id) {
    $product = wire()->pages->get($id);

    if (!$product->id) {
        return response()->json(['error' => 'Not found'], 404);
    }

    $cart   = wire()->session->cart ?? [];
    $cart[] = $id;
    wire()->session->cart = $cart;

    // Return just the updated cart snippet
    $html = render('partials/cart-item', ['product' => $product]);

    return response()
        ->html($html)
        ->retarget('#cart-list')
        ->reswap('beforeend')
        ->addTrigger('cartUpdated', json_encode(['count' => count($cart)]));
});

Form POST with Redirect

<?php
namespace ProcessWire;

$page->route("post:profile/update", function() {
    $name = wire()->sanitizer->text(wire()->input->post('name'));

    if (empty($name)) {
        return redirect('/profile/edit')
            ->withInput()
            ->withFlash('error', 'Name is required.');
    }

    wire()->user->of(false);
    wire()->user->full_name = $name;
    wire()->user->save();

    return redirect('/profile')->withFlash('success', 'Profile updated!');
});

File Download with Auth Check

<?php
namespace ProcessWire;

$page->route("get:download/{id<integer>}", function(int $id) {
    if (!wire()->user->isLoggedIn()) {
        return response()->json(['error' => 'Unauthorized'], 401);
    }

    $file = wire()->pages->get($id);

    if (!$file->id || !$file->download_file) {
        return response()->html('File not found', 404);
    }

    return response()->download($file->download_file->filename, $file->title . '.pdf');
});

API Reference


Global Functions

simpleresponse(): \SimpleWire\Response\Response   // recommended shorthand
response(): \SimpleWire\Response\Response         // alias
redirect(string $url): \SimpleWire\Response\ResponseRedirect

Enums

SimpleWire\Response\HttpStatusCategory  // INFORMATIONAL, SUCCESS, REDIRECTION, CLIENT_ERROR, SERVER_ERROR
SimpleWire\Response\TriggerTiming       // RECEIVE, SETTLE, SWAP
SimpleWire\Response\SwapStrategy        // INNER_HTML, OUTER_HTML, BEFORE_BEGIN, AFTER_BEGIN,
                                        // BEFORE_END, AFTER_END, DELETE, NONE
SimpleWire\Response\HtmxAction          // LOCATION, REDIRECT, REFRESH, STOP_POLLING

Response Methods

Content

  • html(string $content, int $status = 200): self
  • text(string $content, int $status = 200): self
  • xml(string $content, int $status = 200): self
  • json(mixed $data, int $status = 200, string $node = ''): self
  • file(string $pathToFile, array $headers = []): self
  • download(string $pathToFile, string $filename = '', array $headers = []): self
  • render(string $template, array $data = [], int $status = 200): self (requires SimpleRender)
  • withCallback(string $callback, mixed $data): self
  • content(string $content): self
  • setContent(?string $content): self
  • getContent(): string
  • output(): string

Sending

  • send(string $content = '', array $headers = []): self
  • execute(): void
  • redirect(string $url, int $status = 302): never

Headers

  • header(string $key, string $value): self
  • headers(array $headers): self
  • with(string $key, string $value): self
  • allow(array $whitelist): self
  • cache(string $control = '', string $etag = '', int $maxAge = 0): self
  • setContentType(string $type): self

Cookies

  • cookie(string $name, string $value = '', int $minutes = 0, ...): self
  • withoutCookie(string $name, string $path = '/', string $domain = ''): self

Status

  • statusCode(int $code, ?string $text = null): self
  • getStatusCategory(): HttpStatusCategory

HTMX

  • pushUrl(string $url): self
  • replaceUrl(string $url): self
  • retarget(string $selector): self
  • reswap(SwapStrategy|string $option): self
  • addTrigger(string $event, string $params = '', TriggerTiming|string $timing = TriggerTiming::RECEIVE): self

ResponseAction Methods (extends Response)

  • actionLocation(string $url): self
  • hxLocation(string $path, ?array $options = null): self
  • actionRedirect(string $to): self
  • actionRefresh(): self
  • actionStopPolling(): self

ResponseRedirect Methods (extends Response)

  • back(bool $withInput = false): self
  • withInput(?array $except = null): self
  • route(string|int $location, array $parameters = []): self
  • action(string $classMethod, array $parameters = []): self
  • away(string $url): self
  • withFlash(string|array $key, mixed $value = null): self

License


This module is released under the MIT License.

More modules by WireCodex

  • SimpleRouter

    URL routing with pattern matching and caching for ProcessWire.
  • SimpleClient

    Fluent cURL-based HTTP client with retry, file download, and concurrent pool support.
  • SimpleRequest

    HTTP request abstraction with input handling, validation, and content negotiation.
  • SimpleResponse

    HTTP response builder with HTMX support, redirects, and content negotiation.
  • SimpleQuery

    GraphQL-like query engine for ProcessWire pages with caching, rate limiting, and write support.
  • SimpleQueue

    Background job queue with priority, delayed execution, retry, and LazyCron-based processing.
  • SimpleAsset

    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.

All modules by WireCodex

Install and use modules at your own risk. Always have a site and database backup before installing new modules.