Skip to content

AI blog · DotApp PHP Framework 2.0

Middleware in DotApp PHP Framework

DotApp has three middleware shapes. Do not mix their callbacks. A route before / after hook receives the locked $request — returning a Response short-circuits. Named pipeline middleware is Middleware::register then Middleware::use('name')->group(). That callback is function ($request, $next) and must call $next($request) to continue. A module class lives in app/modules/Shop/Middleware/, extends ModuleMiddleware, and is attached as #Shop:AuthGate@check!. This article is a complete Shop admin gate: class, named pipeline, and a per-route login hook. Do not put crcCheck() in a global before-hook if the handler also calls it — the first pass burns the token.

Watch: crcCheck() once per request

A passing crcCheck burns the one-time token

The posted envelope carries a one-time CSRF token. A successful crcCheck() invalidates it. The boolean is not remembered. Call it again on the same request and the second call returns false (used token) — HTTP 400 — even though the first check passed.

Wrong: Router::before(['POST'], ['/shop/*'], '#Shop:AuthGate@crc!') plus if (!$request->crcCheck()) in the save method.
Right: one call. Default: the handler. If you insist on a before-hook, that hook is the only call — handlers must not call crcCheck() again. form() does not re-run CRC. Canonical: Request lifecycle.

Common mistakes

Wrong Right
function ($request, $next) on ->before() Route hooks get ($request) only. Pipeline middleware gets ($request, $next)
Forget $next($request) in a named middleware The group never runs. Return $next($request)
Shop:AuthGate@check! without # #Shop:AuthGate@check!callable strings
Global crcCheck() in middleware, then again in the controller One crcCheck() per request. The first success burns the token. See crcCheck once
Hand-write the middleware class file php dotapper.php --module=Shop --create-middleware=AuthGate
Middleware::use('missing') Throws. register first
Put GET/POST routes only inside module.listeners.php Application routes stay in initialize(). Listeners may attach a global Router::before

Three shapes

Shape Callback Attach
Route hook function ($request) Router::get(...)->before($fn) or Router::before($methods, $paths, $fn)
Named pipeline function ($request, $next) Middleware::register then use()->group()
Module class public static function check($request) ->before('#Shop:AuthGate@check!')

Returning a Response from a before hook stops the route. Middleware::use($name)->group(): if a middleware returns a Response, it is sent and the script exits. Middleware::get($name) throws when the name is unknown. Official DI chapter also covers hooks: Dependency injection documentation.

Complete Shop admin gate

File: app/modules/Shop/Middleware/AuthGate.php.


<?php
namespace Dotsystems\App\Modules\Shop\Middleware;

use Dotsystems\App\Parts\Auth;
use Dotsystems\App\Parts\Response;

class AuthGate extends \Dotsystems\App\Parts\ModuleMiddleware
{
    public static function check($request)
    {
        if (!Auth::isLogged()) {
            return new Response(403, 'Forbidden');
        }
        if (!Auth::can('Shop.admin')) {
            return new Response(403, 'Forbidden');
        }
    }
}
    

In initialize() — named pipeline around admin URLs, plus a per-route login hook. This class does not call crcCheck(). Handlers still do that once.


use Dotsystems\App\Parts\Auth;
use Dotsystems\App\Parts\Middleware;
use Dotsystems\App\Parts\Response;
use Dotsystems\App\Parts\Router;

Middleware::register('is_admin', function ($request, $next) {
    if (!Auth::isLogged() || !Auth::can('Shop.admin')) {
        return new Response(403, 'Forbidden');
    }
    return $next($request);
});

$p = Config::module('Shop', 'prefix');

Middleware::use('is_admin')->group(function () use ($p) {
    Router::get($p . '/admin/users', 'Shop:Admin@users!', Router::STATIC_ROUTE);
    Router::get($p . '/admin/items', 'Shop:Admin@items!', Router::STATIC_ROUTE);
});

Router::get($p . '/account', 'Shop:Account@index!', Router::STATIC_ROUTE)
    ->before('#Shop:AuthGate@check!');
    

Do not add Router::before(['POST'], ['/shop/*'], '#Shop:AuthGate@crc!') if Shop save methods already call crcCheck(). That double call is the used-token 400 — crcCheck once. Auth session test is Auth::isLogged(). Auth::logged() does not exist. Permissions: Authentication and 2FA. Boot order: Module initialization.

Named middleware API

Call Returns
Middleware::register / define / set Chain object
Middleware::use($name) / get($name) Middleware — throws if undefined
->group($cb) $this; a Response from middleware is sent and the script exits
->callAllMiddlewares() Last return value, or the Response
->when($cb) / ->true($cb) / ->false($cb) $this

Other grouping: prefix concatenation from config, or Router::onPath('/shop/admin*', function () { ... }). Rate limits on a route: ->throttle([...]) — without limitExceeded a 429 JSON is sent and the script exits.

FAQ

Is before() an onion stack?

No. before / after are flat hooks. The onion $next stack is the named Middleware:: pipeline.

Can the gate return JSON 401?

Yes: return Response::json(['status' => 0, 'message' => 'Unauthorized'], 401). Use that for public JSON APIs — JSON endpoints.

Does #Shop:AuthGate@check! inject services?

Trailing ! skips DI. Keep the method as check($request) only.

When do I use after()?

Logging or headers after the handler ran. It does not replace a successful controller return.

HTTP 405?

Not implemented as a framework feature. Disallowed methods on the request object are a different path (405 and exit on the request). Do not invent a 405 helper on Router.

Can I crcCheck in a global before-hook?

Only if that is the only crcCheck() on the request. Handlers that also call it will 400. Prefer the handler. See crcCheck once.

May a global Router::before live in listeners?

Yes — module.listeners.php runs first. Use it for auth gates, not for a second CRC. Do not register the catalog of Shop pages there.

See also