Skip to content

AI blog · DotApp PHP Framework 2.0

Callable strings in DotApp PHP Framework

Routes, middleware hooks, and DotApp::call() share one grammar: Shop:Home@index!. The module name, the class, the method, an optional folder prefix (# middleware, * models), and an optional trailing ! that skips dependency injection. This article is the complete Shop route file with every common form, plus when to keep DI and when to turn it off.

Common mistakes

Wrong Right
Type-hint Renderer $renderer on a method reached with Shop:Home@index!. Trailing ! skips DI. Call Renderer::new() inside the method, or drop the ! and keep the type-hint.
Register application routes in index.php. Register them in the module’s initialize($dotApp).
Shop:AuthGate@check! for middleware. Prefix with #: #Shop:AuthGate@check!.
Shop:Item@get! for a model. Prefix with *: *Shop:Item@get!.
Type-hint services on a hot AJAX handler “just in case”. Use ! on hot paths that only need facades and Renderer::new().

Grammar

String Resolves to DI
Shop:Home@index! Controllers\Home::index Off — no type-hinted services
Shop:Home@index Same method On — reflection injects type-hints
#Shop:AuthGate@check! Middleware\AuthGate::check Off when ! is present
*Shop:Item@get! Models\Item::get Off when ! is present
Closure The function you passed to Router::get Injected, or wrap with NoDI

Namespaces: module and listeners Dotsystems\App\Modules\Shop, controllers ...\Shop\Controllers, middleware ...\Shop\Middleware, models ...\Shop\Models. Methods are public static. There is no $this. Routing verbs and STATIC_ROUTE: How routing works in DotApp PHP Framework. Container bind / resolve: Dependency injection.

With ! vs without

Trailing ! means: do not run DI reflection for this callable. The method should take $request (and any extra args you pass through DotApp::call), then construct what it needs. That is the usual choice for hot paths: list POST, save, toggles, anything you hit on every catalog request.


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

use Dotsystems\App\Parts\Renderer;

class Home extends \Dotsystems\App\Parts\Controller
{
    public static function index($request)
    {
        return Renderer::new()
            ->module('Shop')
            ->setView('home')
            ->setViewVar('title', 'Shop')
            ->renderView();
    }
}
    

Route: Shop:Home@index!. Drop the ! only when you want a type-hinted service from the container:


public static function catalog($request, \Dotsystems\App\Parts\Renderer $renderer)
{
    return $renderer->module(static::modulename())
        ->setView('catalog')
        ->setViewVar('title', 'Catalog')
        ->renderView();
}
    

Route: Shop:Home@catalog (no bang). Bind custom services in initialize($dotApp) with $dotApp->bind / singleton, then type-hint them. Wrong with !: public static function index($request, Renderer $renderer)$renderer is not injected, the signature lies, and the method breaks.

When to skip DI

Skip it (!) when the handler has no type-hinted services, when it only uses facades (DB, Config, Auth, Renderer::new()), and on hot paths where reflection is wasted work. Keep DI when a bound Shop service belongs in the signature and the route string has no trailing !.

DotApp::call()

The same strings work outside routing. Extra arguments are forwarded after the callable.


use Dotsystems\App\DotApp;

DotApp::call('Shop:Home@helper!', $arg1);
DotApp::call('#Shop:AuthGate@check!', $request);
$row = DotApp::call('*Shop:Item@get!', $id);
    

Inside a controller you can also static::call('otherMethod', $request) or static::call('OtherModule:Page@withShell!', $title, $html).

Closures on routes

A closure is a valid route target. The first argument is $request. Default resolution may inject extra type-hints; wrap with NoDI when you want the function exactly as written.


use Dotsystems\App\Parts\NoDI;
use Dotsystems\App\Parts\Response;

Router::get($p . '/ping', function ($request) {
    return 'ok';
}, Router::STATIC_ROUTE);

Router::get($p . '/health', new NoDI(function ($request) {
    return new Response(200, 'ok');
}), Router::STATIC_ROUTE);
    

Prefer a controller string for anything non-trivial so the Shop module stays searchable. Closures are fine for tiny health checks.

Complete module.init.php

Fallbacks, then every callable form. Boot order: How module initialization works in DotApp PHP Framework.


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

use Dotsystems\App\DotApp;
use Dotsystems\App\Parts\Config;
use Dotsystems\App\Parts\NoDI;
use Dotsystems\App\Parts\Renderer;
use Dotsystems\App\Parts\Response;
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');
        $p = Config::module('Shop', 'prefix');

        $dotApp->singleton('shopRenderer', function () {
            return Renderer::new();
        });

        Router::get($p . '/', 'Shop:Home@index!', Router::STATIC_ROUTE);

        Router::get($p . '/catalog', 'Shop:Home@catalog');

        Router::get($p . '/item/{id:i}', 'Shop:Home@item!');

        Router::post($p . '/save', 'Shop:Home@save!')
            ->before('#Shop:AuthGate@check!');

        Router::post($p . '/items/list', 'Shop:Items@list!', Router::STATIC_ROUTE);

        Router::get($p . '/ping', function ($request) {
            return 'ok';
        }, Router::STATIC_ROUTE);

        Router::get($p . '/health', new NoDI(function ($request) {
            return new Response(200, 'ok');
        }), Router::STATIC_ROUTE);

        Router::get($p . '/from-model/{id:i}', function ($request) {
            $id = (int) ($request->matchData()['id'] ?? 0);
            $row = DotApp::call('*Shop:Item@get!', $id);
            if ($row === null) {
                return new Response(404, 'Not found');
            }
            return Renderer::new()
                ->module('Shop')
                ->setView('item')
                ->setViewVar('item', $row)
                ->renderView();
        });
    }

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

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

new Module($dotApp);
    
Line What it demonstrates
Shop:Home@index! Controller, no DI — the default for pages and hot paths.
Shop:Home@catalog Controller with DI (type-hint Renderer or a bound service).
#Shop:AuthGate@check! Module middleware class as a before hook.
*Shop:Item@get! Model via DotApp::call() from a closure.
Closure / NoDI Inline handler; NoDI skips injection entirely.

FAQ

Should every route end with !?

Almost every handler you write for Shop should. Add DI only when you type-hint a bound service and you omit the bang.

Can #Shop:Gate@check! be the main route handler?

It is legal but rare. Middleware classes belong on ->before(). Keep the GET/POST target a controller or a small closure.

Where do I put a route name?

You do not. There are no named routes. Build URLs from Config::module('Shop', 'prefix') concatenation.

Why did my second route never run?

First match wins. Keep exact STATIC_ROUTE paths registered before greedy patterns, and do not hide Shop routes in another module that loaded first.

How do I reuse a controller method?

DotApp::call('Shop:Home@helper!', $arg) or static::call('helper', $request). Keep the bang consistent with the method’s signature.

See also