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.