Controllers
Controllers keep route files readable by grouping related request handling in one class. A typical index action can be as small as this:
class PostController extends Controller { Future<Object?> index(Request request) async { return {'posts': await posts.all()}; }}
final posts = PostController();Route.get('/posts', posts.index);Controllers live in lib/app/http/controllers. Use a closure for a tiny route;
move to a controller when the handler has a name, dependencies, middleware, or
siblings such as store, show, and destroy.
Writing Controllers
Basic controllers
Generate a controller, then add methods that take a Request and return
anything a route may return:
maat make:controller UserControllerimport 'package:maat/maat.dart';
class PostController extends Controller { /// Show a single post. Future<Object?> show(Request request, String id) async { return {'post': await posts.find(id)}; }}Register a route that points at the method:
final posts = PostController();
Route.get('/posts/{id}', posts.show);When a request matches the route, the method runs and its return value is normalised into a response exactly as for a closure (see Responses). Route parameters arrive as positional String arguments after the request.
Controllers are not required to extend Controller. Doing so gives you controller middleware, described below, and the Laravel shape everyone recognises.
Single-action controllers
A controller that handles one action defines a call method. Dart lets an object with call stand in for a function, so you pass the controller itself as the handler:
class ProvisionServer extends Controller { /// Provision a new web server. Future<Object?> call(Request request) async { // ... return Response.noContent(); }}Route.post('/server', ProvisionServer());call may accept route parameters like any handler: call(Request request, String id). Generate one with the --invokable option:
maat make:controller ProvisionServer --invokableController Middleware
You may assign middleware to a controller’s routes in your route files:
Route.get('/profile', profile.show).middleware(['auth']);Or declare it on the controller by overriding middleware(). This is Laravel’s HasMiddleware: a list of the same things a route accepts, plus ControllerMiddleware to limit an entry to some actions. Actions are named by their method tear-off, so a typo is a compile error rather than a silent no-op:
class UserController extends ResourceController { @override List<Object> middleware() => [ 'auth', ControllerMiddleware('log', only: [index]), ControllerMiddleware('subscribed', except: [store, update]), ];
// ...}only restricts the middleware to the listed actions; except applies it to every action but those. An entry without ControllerMiddleware applies to all of them.
Where controller middleware applies
Dart has no reflection, so the router can only honour middleware() when it can see the controller behind a handler. That is true in three places:
// 1. Resource controllers: every generated route knows its action.Route.resource('users', UserController());
// 2. Single-action controllers passed as the handler.Route.post('/server', ProvisionServer());
// 3. A controller group, Laravel's Route::controller().final orders = OrderController();Route.controller(orders).group(() { Route.get('/orders/{id}', orders.show); Route.post('/orders', orders.store);});A bare tear-off registered on its own, Route.get('/orders', orders.index), carries no reference to the controller and therefore gets no controller middleware. Wrap such routes in a Route.controller(...) group when you need it.
Middleware order for a route is: group middleware, then controller middleware, then middleware chained on the route itself. Route.controller() is a group builder like Route.prefix(), so it composes:
Route.prefix('admin').middleware(['auth']).controller(orders).group(() { Route.get('/orders', orders.index).middleware(['throttle:60,1']);});Resource Controllers
A resource controller extends ResourceController and overrides the actions it exposes. Any action you leave out responds 404.
maat make:controller PhotoController --apiclass PhotoController extends ResourceController { @override Future<Object?> index(Request request) async => photos.all();
@override Future<Object?> store(Request request) async { final data = await request.validate({'title': 'required'}); return Response.json(await photos.create(data), status: 201); }
@override Future<Object?> show(Request request, String id) async => photos.find(id);}One line registers every route:
Route.resource('photos', PhotoController());| Verb | URI | Action | Route Name |
|---|---|---|---|
| GET | /photos |
index | photos.index |
| POST | /photos |
store | photos.store |
| GET | /photos/{id} |
show | photos.show |
| PUT, PATCH | /photos/{id} |
update | photos.update |
| DELETE | /photos/{id} |
destroy | photos.destroy |
There are no create and edit actions: they render HTML forms, which belong to the Full edition. Route.resource is therefore the same thing as Laravel’s Route::apiResource.
Partial resource routes
Route.resource('photos', PhotoController(), only: ['index', 'show']);Route.resource('photos', PhotoController(), except: ['destroy']);Named routes and URLs
Resource routes are named <resource>.<action>, so the route() helper works as usual:
route('photos.show', {'id': 3}); // /photos/3Dependency Injection & Controllers
Laravel builds controllers through the container and injects constructor arguments by type. Maat has no reflection, so you construct controllers yourself and pass their dependencies explicitly. Register the controller in a provider when it has dependencies worth sharing:
class AppServiceProvider extends ServiceProvider { @override void register() { this.app.singleton<PhotoController>( (c) => PhotoController(c.make<PhotoRepository>()), ); }}Route.resource('photos', Application.current.make<PhotoController>());Because the controller is a plain object, tests may construct it with fakes and call its methods directly, without the router.
Not Supported
Nested resources (photos.comments), create/edit actions, ->names() and ->parameters() customisation, and controller resolution by class name ([PhotoController::class, 'index']). Nested resources change every action’s parameter list, and class-name resolution needs reflection.