AI blog · DotApp PHP Framework 2.0
Dependency injection in DotApp PHP Framework
The container is the booted DotApp instance you receive as initialize($dotApp).
Register with $dotApp->bind($key, $fn) (new instance each resolve) or $dotApp->singleton($key, $fn) (once).
Fetch with $dotApp->resolve($key) — that call throws if the key is unbound.
Controller type-hints are filled only when the route has no trailing !.
Hot Shop pages keep the bang and construct facades inside the method.
This article is a complete Shop catalog service: bind in initialize(), inject without !, skip DI with ! and with NoDI.
Common mistakes
| Wrong | Right |
|---|---|
Injector::bind() / Injector::singleton() |
Broken (typo dotAapp()). Use $dotApp->bind / singleton |
Type-hint a service on Shop:Home@index! |
Trailing ! skips DI. Drop the bang, or call Renderer::new() inside |
Assume resolve('missing') returns null |
It throws. Bind first, or wrap in try/catch |
Register application routes in index.php so “DI is global” |
Bind in the module’s initialize($dotApp) |
| Hand-write a second container | One kernel. Facades already wrap the parts you need |
When to inject
A Shop service you own (catalog lookup, pricing) that several controllers share.
Do not inject Renderer on every hot AJAX handler — use Renderer::new() and !.
Callable grammar: Callable strings.
Controllers: Controllers and Response.
Container API
| Call | Returns |
|---|---|
$dotApp->bind($key, $resolver) |
void — new instance every resolve |
$dotApp->singleton($key, $resolver) |
void — created once |
$dotApp->resolve($key) |
object — throws if unbound |
$dotApp->module($name) |
Module DI wrapper — throws if the module is unregistered |
new NoDI($callable) |
Wrapper that skips reflection for a closure on the router |
The framework already registers DotApp::class as a singleton.
Custom facades subclass \Dotsystems\App\Parts\Facade, set $component and $allowedMethods (['*'] is allowed).
A method not in $allowedMethods throws \BadMethodCallException.
Complete Shop catalog bind
File: app/modules/Shop/Libraries/Catalog.php (a class you own — not the Docs catalog).
Then initialize() and two controller styles.
<?php
namespace Dotsystems\App\Modules\Shop\Libraries;
use Dotsystems\App\Parts\DB;
class Catalog
{
public function byId(int $id): ?array
{
$rows = DB::module('RAW')->q(function ($qb) use ($id) {
$qb->raw('SELECT id, title FROM shop_items WHERE id = :id LIMIT 1', ['id' => $id]);
})->all();
return $rows[0] ?? null;
}
}
public function initialize($dotApp)
{
Config::module('Shop', 'prefix') ?? Config::module('Shop', 'prefix', '/shop');
$dotApp->singleton('shop.catalog', function () {
return new \Dotsystems\App\Modules\Shop\Libraries\Catalog();
});
$p = Config::module('Shop', 'prefix');
Router::get($p . '/item/{id:i}', 'Shop:Home@item');
Router::get($p . '/', 'Shop:Home@index!', Router::STATIC_ROUTE);
}
public static function item($request, \Dotsystems\App\Modules\Shop\Libraries\Catalog $catalog)
{
$id = (int) ($request->matchData()['id'] ?? 0);
$row = $catalog->byId($id);
if ($row === null) {
return new \Dotsystems\App\Parts\Response(404, 'Not found');
}
return \Dotsystems\App\Parts\Renderer::new()
->module('Shop')
->setView('item')
->setViewVar('item', $row)
->renderView();
}
public static function index($request)
{
$catalog = \Dotsystems\App\DotApp::dotApp()->resolve('shop.catalog');
// ...
}
Shop:Home@item has no !, so the Catalog type-hint is injected.
Shop:Home@index! skips DI — resolve by key if you still need the service, or construct it.
Closures on the router that must skip reflection: wrap with new \Dotsystems\App\Parts\NoDI(function ($request) { ... }).
Facades wrap DotApp properties
A custom facade subclasses \Dotsystems\App\Parts\Facade.
$component is a property name on the kernel (the same style as built-in parts), not a bind() key.
$allowedMethods is the list of methods you expose; ['*'] allows every method that exists on that object.
A call outside the list throws \BadMethodCallException.
Shop code that only needs a catalog class should stay on bind / resolve / type-hints — do not invent a new property on DotApp.
FAQ
Should every route skip DI?
Hot AJAX and form posts usually yes (! + facades). Shared Shop services on a few GET pages can keep type-hints.
Why is Injector documented in old snippets?
Do not call it. Injector::bind / singleton fail. The kernel methods are the API.
What does $dotApp->module('Shop') return?
A module DI wrapper. It throws if Shop is not registered. It is not Config::module.
Does middleware get type-hints?
Module middleware with #Shop:AuthGate@check! skips DI. Keep check($request).
Middleware.
Must I try/catch resolve()?
If the key might be missing, yes — it throws. Prefer binding in initialize() so a miss is a programmer error.
DotApp::dotApp() vs initialize($dotApp)?
Same instance after boot. DotApp::dotApp() throws if the kernel is not booted.