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.
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.