Skip to the content
OrielDocs
0.1.0-alpha.10GitHub

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/Http/Controllers:

Terminal
node oriel make:controller UserController

A controller has any number of methods, each of which can answer a route. This one shows a user's profile:

app/Http/Controllers/UserController.ts
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/Http/Controllers/Controller.ts, which a new app has:

app/Http/Controllers/Controller.ts
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:

app/Http/Controllers/ProvisionServerController.ts
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:

Terminal
node oriel make:controller ProvisionServer --invokable

make:controller adds the Controller suffix to a name that lacks it, so this writes ProvisionServerController.ts in app/Http/Controllers.

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:

Terminal
node oriel make:controller PhotoController --resource
app/Http/Controllers/PhotoController.ts
import { 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#

VerbURIActionRoute name
GET/photosindexphotos.index
GET/photos/createcreatephotos.create
POST/photosstorephotos.store
GET/photos/{photo}showphotos.show
GET/photos/{photo}/editeditphotos.edit
PUT, PATCH/photos/{photo}updatephotos.update
DELETE/photos/{photo}destroyphotos.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:

Terminal
node oriel make:controller PhotoController --model=Photo

show(), edit(), update() and destroy() then take photo: Photo, and each method has a comment saying what it is for. When app/Models 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:

Terminal
node oriel make:controller PhotoController --model=Photo --requests

This writes app/Http/Requests/StorePhotoRequest.ts and app/Http/Requests/UpdatePhotoRequest.ts, 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:

Terminal
node oriel make:controller PhotoController --apinode oriel make:controller PhotoController --api --model=Photo

Nested 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/{photo}/comments/{comment}, and its routes are named photos.comments.index, photos.comments.show and so on. The actions receive the parameters in that order:

app/Http/Controllers/PhotoCommentController.ts
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/{admin_user}.

Supplementing resource controllers#

Routes of your own can sit beside a resource's. Oriel matches a fixed segment before a parameter, so /photos/popular 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:

app/Http/Controllers/PodcastController.ts
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():

app/Http/Controllers/UserController.ts
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'])
app/Http/Controllers/UserController.ts
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:

app/Http/Controllers/PostController.ts
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:

OptionWhat it writes
(none)An empty controller
-i, --invokableA controller with an __invoke() method
-r, --resourceThe seven resource methods; the parameter is named after the controller, without its Controller suffix
--apiThe five API resource methods, without create and edit
-m, --model=PhotoThe resource methods typed with the model (with --api, the five); writes the model when it doesn't exist
-R, --requestsWith --model: the Store and Update form requests, and their validate() calls
--forceOverwrites a controller that exists

A name with slashes writes into a subfolder: make:controller Settings/ProfileController writes ProfileController.ts in app/Http/Controllers/Settings, importing ../Controller.

Privacy policy ·

esc