Skip to content

Controllers and Requests

Technomantus Corvi edited this page Sep 5, 2026 · 1 revision

Controllers & Requests

Controllers

Every controller extends Controller and gets access to $this->request, plus a set of response helpers.

<?php

class ArticleController extends Controller
{
    public function index(): void
    {
        $this->view('article/index', [
            'title'    => 'Articles',
            'articles' => ArticleModel::all(),
        ]);
    }

    public function store(): void
    {
        $title = $this->request->post('title');

        if (empty($title)) {
            $this->flash('error', 'Title is required.');
            $this->redirect('/article/create');
        }

        ArticleModel::create(['title' => $title]);
        $this->flash('success', 'Article created.');
        $this->redirect('/article');
    }
}

Methods available on Controller

Method Description
$this->view($name, $data) Renders a view inside the layout
$this->redirect($path) Redirects and stops execution
$this->json($data, $status) Returns JSON and stops execution
$this->abort404() Shows the 404 page and stops execution
$this->flash($key, $message) Stores a flash message in the session
$this->request The current Request instance

Protecting a route (requires tanuki_login)

public function dashboard(): void
{
    auth_require(); // redirects to /login if not authenticated
    $user = auth_user();

    $this->view('dashboard/index', ['user' => $user]);
}

See Authentication for the full auth helper reference.

The Request class

$this->request->post('field')        // Value from $_POST
$this->request->get('param')         // Value from $_GET
$this->request->input('field')       // POST first, falls back to GET
$this->request->all()                // All inputs merged (POST wins)
$this->request->only(['a', 'b'])     // Only those fields
$this->request->except(['token'])    // All fields except those
$this->request->has('field')         // bool: exists and not empty?
$this->request->file('avatar')       // $_FILES entry, or null
$this->request->method()             // 'GET', 'POST', 'PUT'…
$this->request->isGet() / isPost() / isPut() / isDelete()
$this->request->isAjax()             // bool
$this->request->ip()                 // Client IP — see note below
$this->request->uri()                // Current path, no query string

A note on ip()

Request::ip() trusts the X-Forwarded-For header, which any client can set — only rely on it behind a trusted reverse proxy that overwrites that header. See Security.

Form repopulation on validation failure

public function store(): void
{
    $title       = $this->request->post('title');
    $description = $this->request->post('description', '');

    if (empty($title)) {
        keep_old(['title' => $title, 'description' => $description]);
        $this->flash('error', 'Title is required.');
        $this->redirect('/article/create');
    }
    // ...
}

In the view:

<input type="text" name="title" value="<?= e(old('title')) ?>">

keep_old() stores the data in the session; old() retrieves it once and view() clears it automatically after rendering, so it only survives a single redirect-and-redisplay cycle.

CSRF protection

public function store(): void
{
    if (!csrf_verify($this->request->post('_token'))) {
        $this->flash('error', 'Your session expired. Please try again.');
        $this->redirect('/article/create');
    }
    // ...
}

In the view:

<form method="POST" action="/article">
    <?= csrf_field() ?>
    <!-- ... -->
</form>

Not enforced globally by the router on purpose — see Security for why.

Clone this wiki locally