The basics
Controllers
A controller groups the actions that handle related requests in one class: a UserController can show, store and
update your users.
Writing controllers#
Basic controllers#
make:controller writes a new controller in app/:
node oriel make:controller UserControllerA controller has any number of methods, each of which can answer a route. This one shows a user's profile:
import { view } from '@orieljs/framework'import { User } from '../../Models/User'import { Controller } from './Controller'export class UserController extends Controller { async show(id: string) { return view('user.profile', { user: await User.findOrFail(id) }) }}Point a route at a method by naming the controller and the method together. TypeScript checks that the method exists:
Route.get('/user/{id}', [UserController, 'show'])When a request matches the route, a new UserController is made and its show() called with the route's
parameters, in the order they appear in the URI. What the method returns becomes the response, as a closure's does.
Every controller make:controller writes extends Controller from app/, which a new
app has:
import { Controller as BaseController } from '@orieljs/framework'/** The base controller every generated controller extends: `await this.authorize('update', post)` comes from it. */export abstract class Controller extends BaseController {}The framework's Controller gives your controllers authorize() (see authorizing actions). A
controller doesn't have to extend it, but methods all your controllers share can go in your base class.
Single action controllers#
A controller for one complex action can be a class of its own, with a single __invoke() method:
import { Controller } from './Controller'export class ProvisionServerController extends Controller { async __invoke() { // ... }}Its route names only the class:
Route.post('/server', ProvisionServerController)--invokable (or -i) writes one:
node oriel make:controller ProvisionServer --invokablemake:controller adds the Controller suffix to a name that lacks it, so this writes
ProvisionServerController.ts in app/.
Controller middleware#
Give a controller's routes their middleware in your route files, on the route or on a group:
Route.get('/profile', [UserController, 'show']).middleware('auth')Resource controllers#
A resource is something your users create, read, update and delete, such as a photo. --resource (or -r) writes a
controller with a method for each of those actions:
node oriel make:controller PhotoController --resourceimport { Controller } from './Controller'export class PhotoController extends Controller { async index() { // } async create() { // } async store() { // } async show(photo: unknown) { // } async edit(photo: unknown) { // } async update(photo: unknown) { // } async destroy(photo: unknown) { // }}Route.resource() then registers a route for each method:
Route.resource('photos', PhotoController)node oriel route:list shows the routes it made.
Actions handled by resource controllers#
| Verb | URI | Action | Route name |
|---|---|---|---|
GET | /photos | index | photos.index |
GET | /photos/ | create | photos.create |
POST | /photos | store | photos.store |
GET | /photos/ | show | photos.show |
GET | /photos/ | edit | photos.edit |
PUT, PATCH | /photos/ | update | photos.update |
DELETE | /photos/ | destroy | photos.destroy |
Each GET route answers HEAD too. The parameter is the resource's name in the singular, so once your app has a
Photo model, {photo} is bound to it and a missing photo is a 404.
Specifying the resource model#
--model (or -m) types the methods with your model, and writes the resource controller even without --resource:
node oriel make:controller PhotoController --model=Photoshow(), edit(), update() and destroy() then take photo: Photo, and each method has a comment saying what it
is for. When app/ has no Photo yet, make:model Photo runs first.
Generating form requests#
--requests (or -R), with --model, also writes a form request for store() and one for update(), and
validates with them:
node oriel make:controller PhotoController --model=Photo --requestsThis writes app/ and app/, and store()
starts with const data = await StorePhotoRequest.validate(). Validation covers form requests.
Partial resource routes#
A resource route can register only some of its actions. List them in only, or the ones to leave out in except:
Route.resource('photos', PhotoController, { only: ['index', 'show'] })Route.resource('photos', PhotoController, { except: ['create', 'store', 'update', 'destroy'] })API resource routes#
An API has no pages with forms, so Route.apiResource() leaves out create and edit:
Route.apiResource('photos', PhotoController)--api writes a controller without those two methods, and with --model, typed with the model:
node oriel make:controller PhotoController --apinode oriel make:controller PhotoController --api --model=PhotoNested resources#
A resource that belongs to another, such as a photo's comments, is named with a dot:
Route.resource('photos.comments', PhotoCommentController)Its URIs hold both parameters, /photos/, and its routes are named photos.comments.index,
photos.comments.show and so on. The actions receive the parameters in that order:
import { Controller } from './Controller'export class PhotoCommentController extends Controller { async show(photo: string, comment: string) { return { photo, comment } }}Each parameter is bound to its model by its name, on its own, once your app has Photo and Comment models.
Naming resource routes#
names replaces the names of some actions' routes:
Route.resource('photos', PhotoController, { names: { create: 'photos.build' } })Naming resource route parameters#
A resource's parameter is its name in the singular, with dashes turned into underscores: blog-posts gives
{blog_post}. parameters names it yourself:
Route.resource('users', AdminUserController, { parameters: { users: 'admin_user' } })The show route's URI is then /users/.
Supplementing resource controllers#
Routes of your own can sit beside a resource's. Oriel matches a fixed segment before a parameter, so
/photos/ reaches popular() rather than show(), wherever you define it:
Route.get('/photos/popular', [PhotoController, 'popular'])Route.resource('photos', PhotoController)A resource can also sit in a group, which gives it a prefix, a name prefix or middleware:
Route.middleware('auth').resource('photos', PhotoController).
Dependency injection and controllers#
Constructor injection#
The service container makes a new controller for each request, with no constructor arguments. Take the
services a controller needs with inject(), in a field:
import { inject } from '@orieljs/framework'import { ConfigRepository } from '@orieljs/framework/contracts'import { Podcast } from '../../Models/Podcast'import { Controller } from './Controller'export class PodcastController extends Controller { readonly #config = inject(ConfigRepository) async index() { return Podcast.latest().limit(this.#config.get('podcasts.per_page', 20)).get() }}Method injection#
An action receives the route's parameters and nothing else. Read the request with request():
import { redirect, request } from '@orieljs/framework'import { Controller } from './Controller'export class UserController extends Controller { async store() { const name = request().string('name').toString() // Store the user... return redirect('/users') }}A route parameter still arrives as the method's argument:
Route.put('/user/{id}', [UserController, 'update'])import { redirect, request } from '@orieljs/framework'import { User } from '../../Models/User'import { Controller } from './Controller'export class UserController extends Controller { async update(id: string) { const user = await User.findOrFail(id) await user.update({ name: request().string('name').toString() }) return redirect('/users') }}Authorizing actions#
this.authorize() checks an ability for the signed-in user, through your gates and policies. When the user may not,
it throws, and the request ends with a 403, or the status the policy chose:
import { redirect, request } from '@orieljs/framework'import type { Post } from '../../Models/Post'import { Controller } from './Controller'export class PostController extends Controller { async update(post: Post) { await this.authorize('update', post) await post.update({ title: request().string('title').toString() }) return redirect().route('posts.show', post) }}this.authorizeForUser(user, 'update', post) checks the ability for another user. Gates and policies are covered on the
Authorization page.
Generating controllers#
make:controller takes these options. Given several, --model wins over --invokable, and --invokable over
--resource:
| Option | What it writes |
|---|---|
| (none) | An empty controller |
-i, --invokable | A controller with an __invoke() method |
-r, --resource | The seven resource methods; the parameter is named after the controller, without its Controller suffix |
--api | The five API resource methods, without create and edit |
-m, --model=Photo | The resource methods typed with the model (with --api, the five); writes the model when it doesn't exist |
-R, --requests | With --model: the Store and Update form requests, and their validate() calls |
--force | Overwrites a controller that exists |
A name with slashes writes into a subfolder: make:controller Settings/ writes
ProfileController.ts in app/, importing ../.