Zum Inhalt springen

AI blog · DotApp PHP Framework 2.0

How routing works in DotApp PHP Framework

Shop routes are registered in initialize($dotApp), not in index.php. Router::get, post, match, and any attach a callable string or a closure. STATIC_ROUTE is an exact URL; DYNAMIC_ROUTE is pattern matching. Named routes do not exist. JSON endpoints are ordinary Router::get / post handlers that return Response::json. The first matching route wins.

Common mistakes

Wrong Right
Register application URLs in index.php Register them in the module’s initialize().
Read $request->id or a magic property for {id} $request->matchData()['id'].
Type-hint extra services on Shop:Home@index! Trailing ! skips DI. Create Renderer::new() inside the method.
Put a wildcard /shop/{*} above /shop/item/{id:i} First match wins. Register the specific path first.
Invent a name helper to generate URLs Named routes do not exist. Concatenate Config::module('Shop', 'prefix').
Expect a resource dispatcher from the URL Each JSON verb is its own Router::get / post + Response::json.

Verbs and static vs dynamic

Facades: Router:: and Route:: (alias). Verbs: get, post, put, delete, patch, options, head, trace, match, any.


Router::get($path, $callback, $static = false);
Router::post($path, $callback, $static = false);
Router::match(['GET', 'POST'], $path, $callback, $static = false);
Router::any($path, $callback, $static = false);
    
Third argument Meaning
Router::STATIC_ROUTE (true) Exact URL match. Use it for /, /save, /api/items.
Router::DYNAMIC_ROUTE (false, default) Pattern matching for {id:i}, optional segments, wildcards.

The call returns a chain object: ->before(), ->after(), ->middleware() (alias of before), ->throttle(), ->limitExceeded(). If the route does not match the current request, the chain is inert — later before() calls are no-ops for that request.

Path parameters

Pattern Meaning
{param} Required segment
{param?} Optional segment
{param:i} Integer
{param:s} String
{param:l} Letters
{param*} Greedy named remainder
{*} Anonymous greedy remainder
/prefix/* Prefix wildcard

Read matched values from the request. Missing optional params are simply absent from the array:


$id = $request->matchData()['id'] ?? null;
    

Callable strings

String Resolves to
Shop:Home@index! Controllers\Home::index without DI
Shop:Home@index Same method with DI reflection
#Shop:AuthGate@check! Middleware\AuthGate::check
*Shop:Item@get! Models\Item::get

Closures are valid: Router::get('/x', function ($request) { ... }). Grammar: Callable strings in DotApp PHP Framework.

before / after

Returning a Response instance from a before-hook short-circuits the pipeline. Returning null or void continues.


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

Router::before(['POST'], [$p . '/*'], '#Shop:AuthGate@crc!');
    

Global forms: Router::before($callback), Router::before($routePattern, $callback), or Router::before($method, $routePattern, $callback). After-hooks run when the handler finishes without a short-circuit.

Grouping (named routes do not exist)

There is no name table and no URL generator keyed by a route name. Group with one of these:

  1. Prefix concatenation from Config::module('Shop', 'prefix').
  2. Router::onPath('/shop/admin*', function () { ... }).
  3. Middleware::use('is_admin')->group(function () { ... }).

use Dotsystems\App\Parts\Middleware;

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

Router::onPath($p . '/reports*', function () use ($p) {
    Router::get($p . '/reports/sales', 'Shop:Reports@sales!', Router::STATIC_ROUTE);
});
    

JSON endpoints

Register the verb yourself and return Response::json($array, $code = 200). That sets the body and the JSON content type. There is no resource helper that invents getItems / postItems from the path. If the browser posts the same URL with $dotapp().load() or <fo-rm>, still call $request->crcCheck() before you write.


public static function apiItems($request)
{
    $pageNum = (int) ($request->query()['page'] ?? 1);
    if ($pageNum < 1) { $pageNum = 1; }
    $page = \Dotsystems\App\Parts\DB::module('RAW')->q(function ($qb) {
        $qb->select(['id', 'title'])->from('shop_items')->orderBy('id', 'DESC');
    })->paginate(20, $pageNum);
    return Response::json(['status' => 1, 'page' => $page]);
}

public static function apiCreate($request)
{
    if (!$request->crcCheck()) {
        return Response::json(['status' => 0, 'message' => 'Bad request'], 400);
    }
    // insert with DB::module('RAW')->q(...)->execute($ok, $err)
    return Response::json(['status' => 1]);
}
    

First match wins

Once a dynamic route matches the current request, later registrations may receive an inert chain for that request. Keep lists ordered: static exact paths, then constrained params, then greedy {*} / prefix wildcards last. Prefer php dotapper.php --list-routes over Router::hasRoute() when you inspect what is registered.

Complete module.init.php


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

use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\Middleware;
use Dotsystems\App\Parts\Router;

class Module extends \Dotsystems\App\Parts\Module
{
    public function initialize($dotApp)
    {
        Config::module('Shop', 'prefix') ?? Config::module('Shop', 'prefix', '/shop');
        Config::module('Shop', 'itemsPerPage') ?? Config::module('Shop', 'itemsPerPage', 20);
        Config::module('Shop', 'public') ?? Config::module('Shop', 'public', true);

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

        Router::get($p . '/', 'Shop:Home@index!', Router::STATIC_ROUTE);
        Router::get($p . '/item/{id:i}', 'Shop:Home@item!');
        Router::post($p . '/item/save', 'Shop:Home@save!', Router::STATIC_ROUTE)
            ->before('#Shop:AuthGate@check!');
        Router::match(['GET', 'POST'], $p . '/search/{q?}', 'Shop:Search@run!');
        Router::any($p . '/health', 'Shop:Health@ping!', Router::STATIC_ROUTE);

        Router::get($p . '/api/items', 'Shop:Items@index!', Router::STATIC_ROUTE);
        Router::get($p . '/api/items/{id:i}', 'Shop:Items@show!');
        Router::post($p . '/api/items', 'Shop:Items@save!', Router::STATIC_ROUTE);
        Router::post($p . '/api/items/{id:i}', 'Shop:Items@update!');
        Router::post($p . '/api/items/{id:i}/delete', 'Shop:Items@delete!', Router::STATIC_ROUTE);

        Router::onPath($p . '/admin*', function () use ($p) {
            Router::get($p . '/admin', 'Shop:Admin@index!', Router::STATIC_ROUTE)
                ->before('#Shop:AuthGate@check!');
        });

        Middleware::register('shop_staff', function ($request, $next) {
            return $next($request);
        });
        Middleware::use('shop_staff')->group(function () use ($p) {
            Router::get($p . '/staff/orders', 'Shop:Orders@index!', Router::STATIC_ROUTE);
        });
    }

    public function initializeRoutes()
    {
        return ['/shop', '/shop/*'];
    }

    public function initializeCondition($routeMatch)
    {
        return $routeMatch;
    }
}

new Module($dotApp);
    

Official router chapter: Router documentation. Module boot order: How module initialization works in DotApp PHP Framework.

FAQ

Where do I name a route?

Named routes do not exist. Store the prefix in config and concatenate. If two modules share a path, order registrations so the first match is the one you want.

When is STATIC_ROUTE required?

Use it when the path has no parameters. It is the fast exact match. Dynamic patterns ({id:i}, {q?}, {*}) stay on the default DYNAMIC_ROUTE.

When do I drop the trailing !?

Only when the method type-hints services you want injected. Hot paths keep ! and construct Renderer::new() themselves. Details: callable strings and dependency injection.

How do I version a JSON API?

Put the version in the path you register: $p . '/api/v1/items'. Each verb is an explicit Router::get or Router::post that returns Response::json.

What happens on no match?

A dotapp.router.resolve.404 listener can take over. Otherwise the router error handler runs. HTTP 405 is not implemented — unknown verbs fall through the same miss path.

How do I rate-limit login?

Chain ->throttle(['per_minute' => 5, 'per_hour' => 40])->limitExceeded(function ($request) { return new Response(429, 'Too many attempts'); }). Without limitExceeded, a 429 JSON response is sent and the process exits.

See also