Groups & Middleware

Wrap a set of routes under a common prefix and run shared logic — auth checks, rate limits, JSON-only enforcement — before each handler runs.

A simple group

Pass a prefix pattern and a closure that registers the group's routes:

$App->router->group('/api/*', function() use ($App) {
    $App->router->get ('/api/users',     $listHandler);
    $App->router->post('/api/users',     $createHandler);
    $App->router->get ('/api/users/:id', $showHandler);
});

Adding middleware

Pass middleware as the third argument. Each is an invokable that receives the request and must return true to continue or false to stop the chain:

$requireAuth = function($request) use ($App) {
    $token = $request->headers->authorization ?? '';
    if (!authIsValid($token)) {
        $App->response(['error' => 'Unauthorized'], 'json', 401);
        return false;
    }
    return true;
};

$App->router->group('/api/*', function() use ($App) {
    $App->router->get ('/api/me', $meHandler);
    $App->router->put ('/api/me', $updateHandler);
}, [$requireAuth]);

Multiple middleware run in array order. The first one to return false short-circuits the rest.

Middleware as a class

For anything you'll reuse across routes, write the middleware as a class with an __invoke() method. Drop the file in app/models/ under the App\ namespace — the project's autoloader picks it up automatically, no require needed in routes.php.

Where they live: app/models/ is the conventional home for middleware classes. The App\ namespace is mapped to that directory in composer.json's PSR-4 autoload, so a class named App\SessionTokenMiddleware goes in app/models/SessionTokenMiddleware.php.

Example: session-token auth gate

A short, self-contained gate. Reads a token from the request body (regardless of HTTP verb), validates it via True\Auth, returns true to let the route through or false to stop the chain:

// app/models/SessionTokenMiddleware.php
<?php
namespace App;

class SessionTokenMiddleware
{
    public function __invoke($request)
    {
        $Auth = new \True\Auth;

        $token = $request->{strtolower($request->method)}->token;

        if ($Auth->authenticate(['type' => 'session-token', 'token' => $token])) {
            return true;
        }
        return false;
    }
}

Example: global Mailchimp subscribe handler

A different shape — this one runs on every request looking for a particular form action, processes it if present, and otherwise lets the request continue. Useful for site-wide newsletter signup boxes, contact form fall-throughs, etc.:

// app/models/MailchimpMiddleware.php
<?php
namespace App;

class MailchimpMiddleware
{
    public function __invoke($request)
    {
        $F = new \Truecast\Welder(['action_field' => 'mailchimpsubscribe']);

        if ($F->validate('name=name email=email')) {
            $values = $F->get('object');

            global $App;
            $config = $App->getConfig('mailchimp.ini');

            try {
                $MailChimp = new \DrewM\MailChimp\MailChimp($config->APIKey);
                $listId    = $config->listId;
                [$first, $last] = \True\DataCleaner::splitName($values->name);

                $result = (object) $MailChimp->post("lists/$listId/members", [
                    'email_address' => $values->email,
                    'status'        => 'subscribed',
                    'merge_fields'  => ['FNAME' => $first, 'LNAME' => $last],
                    'tag'           => 'Website Subscriber',
                    'ip_signup'     => $request->ip,
                ]);

                if ($MailChimp->success()) {
                    $App->go("/email-updates-thanks");
                } elseif (in_array($result->title, ['Member Exists', 'Forgotten Email Not Subscribed'], true)) {
                    $this->reSubscribe($MailChimp, $request, $App, $listId, $values->email, $first, $last);
                } else {
                    trigger_error($MailChimp->getLastError(), 256);
                }
            } catch (\Exception $ex) {
                trigger_error($ex->getMessage(), 256);
            }
        }

        return true;
    }

    private function reSubscribe($MailChimp, $request, $App, $listId, $email, $first, $last)
    {
        $hash   = md5($email);
        $result = (object) $MailChimp->put("lists/$listId/members/$hash", [
            'email_address' => $email,
            'status_if_new' => 'subscribed',
            'merge_fields'  => ['FNAME' => $first, 'LNAME' => $last],
            'tag'           => 'Website Subscriber',
            'ip_signup'     => $request->ip,
        ]);

        if ($MailChimp->success()) {
            $App->go("/email-updates-thanks");
        } else {
            trigger_error($MailChimp->getLastError(), 256);
        }
    }
}

Wiring them up

Reference each class by its fully qualified name when registering the group:

// Auth gate around an API endpoint
$App->router->group('/module/api/podcaster/*', function() use ($App) {
    $App->router->any('/module/api/podcaster/:action', BP.'/app/modules/podcaster/controllers/api.php');
}, [ new \App\SessionTokenMiddleware ]);

// Global handler — empty closure, runs on every request
$App->router->group('/*', function() {}, [ new \App\MailchimpMiddleware ]);

Middleware is opt-in. A route registered outside any group runs with no middleware. There's no global "filter every request" hook — even the global Mailchimp pattern above is just a group with an empty route closure and a wide-open /* prefix.