Skip to content

Controllers and Middleware

Precious Lyson edited this page Nov 13, 2026 · 3 revisions

The Machinjiri framework provides two foundational abstract classes that power the HTTP layer of your application:

Class Purpose
AbstractController Base class for all application controllers. Provides request/response handling, view rendering, validation, session management, CSRF protection, and file uploads.
AbstractMiddleware Base class for all middleware. Provides a standard handle() contract plus helpers for detecting AJAX/JSON requests and accessing route parameters.

Both classes live in Mlangeni\Machinjiri\Core\Artisans\Base and are designed to be extended — never instantiated directly.

The CLI generators (make:controller, make:model, make:middleware) scaffold these classes for you automatically.


Beginner Guide

What is a Controller?

A controller is the class that receives an HTTP request, does something (fetch data, validate input, save records), and returns a response (HTML view, JSON, or redirect).

In Machinjiri, every controller extends AbstractController, which gives you ready-to-use helpers for the most common tasks.

What is Middleware?

Middleware is a layer that sits between the request and the controller. It can inspect, modify, or reject a request before it reaches your controller — and can also act on the response after it leaves.

Common uses: authentication, logging, CORS, rate limiting, maintenance mode.

Creating Your First Controller

Use the built-in Artisan command:

php artisan make:controller UserController

This generates app/Controllers/UserController.php:

<?php

namespace App\Controllers;

use Mlangeni\Machinjiri\Core\Artisans\Base\AbstractController;
use Mlangeni\Machinjiri\Core\Http\HttpRequest;
use Mlangeni\Machinjiri\Core\Http\HttpResponse;

class UserController extends AbstractController
{
    public function index(HttpRequest $request, HttpResponse $response)
    {
        return $this->view('welcome');
    }
}

What's happening here?

  • The class extends AbstractController — this is mandatory.
  • The index() method is an action (a route handler).
  • $this->view('welcome') renders the welcome view and returns the HTML string.

Register a route in your routing file:

$router->get('/users', [UserController::class, 'index']);

Visit /users — you'll see the welcome page.

Creating Your First Middleware

php artisan make:middleware AuthMiddleware

Generated file: app/Middleware/AuthMiddleware.php

<?php

namespace App\Middleware;

use Mlangeni\Machinjiri\Core\Artisans\Base\AbstractMiddleware;
use Mlangeni\Machinjiri\Core\Http\HttpRequest;
use Mlangeni\Machinjiri\Core\Http\HttpResponse;

class AuthMiddleware extends AbstractMiddleware
{
    public function handle(
        HttpRequest $request,
        HttpResponse $response,
        callable $next,
        array $params = []
    ) {
        // Your middleware logic here.
        return $next($params);
    }
}

The golden rule: middleware must return $next($params) (or its result) to continue the pipeline. If you don't call $next, the request stops here.

A simple authentication example:

public function handle($request, $response, callable $next, array $params = [])
{
    if (!$this->session()->has('user_id')) {
        return $this->redirect('/login');
    }

    return $next($params);
}

Intermediate Guide

Controller Helpers Reference

All helpers below are protected — available inside any controller extending AbstractController.

Rendering Views

// Render a view (returns HTML string)
return $this->view('users.index', ['users' => $users]);

// Dot notation resolves to resources/views/users/index.php
// Namespaced views also work: 'admin::dashboard'

JSON Responses

return $this->json(['status' => 'ok'], 200);
return $this->json(['error' => 'Not found'], 404);

Redirects

// Redirect to a URL
return $this->redirect('/dashboard');

// Redirect back to the referer (or '/' if none)
return $this->back();

// Redirect to a named route with parameters
return $this->redirectToRoute('user.show', ['id' => 42]);

Note: redirect() in the current implementation calls ->send() internally. For clean testable code, you may prefer to return the HttpResponse and let the kernel send it.

Assets

// Generate a versioned asset URL
$url = $this->asset('css/app.css');

// Output a <link> tag directly
$this->style('css/app.css', ['media' => 'screen']);

// Output a <script> tag directly
$this->script('js/app.js', ['defer' => true]);

Global View Data

// Share data with every view rendered in this request
$this->share('appName', 'Machinjiri');
$this->share(['user' => $user, 'theme' => 'dark']);

Aborting Requests

// Throw an HTTP exception (404)
$this->abort(404, 'User not found');

// Conditional abort
$this->abortIf(!$user, 404, 'User not found');

Working with Views

Rendering is straightforward:

public function show(HttpRequest $request)
{
    $user = User::find($request->input('id'));

    return $this->view('users.show', [
        'user'    => $user,
        'title'   => 'User Profile',
        'isAdmin' => $user->role === 'admin',
    ]);
}

The view() method returns a string. To send it as a response, return it directly from your action — the kernel will wrap it in an HttpResponse.


Handling Forms & Validation

There are two validation approaches: simple rules and FormRequest classes.

Approach 1: Simple Rules

public function store(HttpRequest $request)
{
    $data = $this->validate([
        'name'     => 'required|string|min:3|max:255',
        'email'    => 'required|email|unique:users,email',
        'password' => 'required|min:8',
    ], [
        'name.required' => 'Please tell us your name.',
        'email.email'   => 'That does not look like a valid email.',
    ]);

    // $data contains only validated fields
    User::create($data);

    $this->flash('success', 'Account created!');
    return $this->redirect('/login');
}

Rule syntax: ruleName or ruleName:param1,param2. Separate multiple rules with |.

Examples:

Rule string Meaning
required Field must be present and non-empty
email Must be a valid email
min:3 At least 3 characters
max:255 At most 255 characters
unique:users,email No existing row in users.email
`required string

What happens on failure?

validate() automatically:

  1. Stores errors in form_errors session key.
  2. Stores submitted input in old_input session key.
  3. For AJAX/JSON requests → returns 422 JSON with { errors: {...} } and exits.
  4. For normal requests → redirects back to the referer and exits.

You never need to handle validation failure manually.

Reading errors in a view

<?php $errors = $this->errors(); ?>

<?php if ($errors->has('email')): ?>
    <span class="error"><?= $errors->first('email') ?></span>
<?php endif; ?>

Approach 2: FormRequest Classes

For complex forms, create a dedicated FormRequest subclass. This keeps controllers thin and validation logic reusable.

public function store(HttpRequest $request)
{
    $data = $this->validateWith(RegisterRequest::class);
    User::create($data);
    return $this->redirect('/login');
}

validateWith() resolves your FormRequest from the container (injecting request, response, session, CSRF token, and filesystem), runs it, and — on failure — responds with errors (AJAX JSON or redirect-back) automatically.


File Uploads

public function uploadAvatar(HttpRequest $request)
{
    $upload = $this->upload('local', 'avatars');

    // The FileUpload object handles the actual file
    $path = $upload->save($request->file('avatar'));

    return $this->json(['path' => $path]);
}
  • 'local' → local filesystem disk
  • 'ftp' → remote FTP disk
  • 'avatars' → subdirectory inside the disk root

Flash Messages & Old Input

Flash messages live for one request only.

// Set a flash message
$this->flash('success', 'Profile updated.');
$this->flash('errors', ['Something went wrong.']);

// Read and consume it (returns null if not present)
$message = $this->getFlash('success', 'Default message');

Preserving form input

// Store all request input for the next request
$this->withInput();

// Store a custom array
$this->withInput(['name' => 'Jane', 'email' => 'jane@example.com']);

// Read it back in the view
$oldName = $this->old('name', '');

A typical failed-validation flow does this automatically — you get old input for free.


Middleware Helpers

AbstractMiddleware provides:

// Detect AJAX requests (X-Requested-With: XMLHttpRequest)
if ($this->isAjax()) { ... }

// Detect JSON-accepting requests (Accept: application/json)
if ($this->expectsJson()) { ... }

// Get route parameters (requires router integration)
$params = $this->getRouteParams($request);

// Call the next middleware in the chain
return $this->next($next, $params);

Example: Logging Middleware

public function handle($request, $response, callable $next, array $params = [])
{
    $start = microtime(true);
    $result = $next($params);
    $duration = round((microtime(true) - $start) * 1000, 2);

    error_log(sprintf('[%s] %s took %dms', $request->getMethod(), $request->getPath(), $duration));

    return $result;
}

Example: JSON Guard

public function handle($request, $response, callable $next, array $params = [])
{
    if (!$this->expectsJson()) {
        return $response->setStatusCode(406)->setJsonBody(['error' => 'JSON required']);
    }

    return $next($params);
}

Advanced Guide

Dependency Injection in Controllers

The constructor of AbstractController accepts six optional dependencies and falls back to the container if they're not provided:

Dependency Type
$request HttpRequest
$response HttpResponse
$session Session
$cookie Cookie
$csrfToken CSRFToken
$fileSystem FileSystemManager

This means your child controller can either:

  1. Do nothing — the container supplies everything.
  2. Add your own dependencies — and call parent::__construct().
class OrderController extends AbstractController
{
    public function __construct(
        private OrderService $orders,
        ?HttpRequest $request = null,
        ?HttpResponse $response = null,
        ?Session $session = null,
        ?Cookie $cookie = null,
        ?CSRFToken $csrfToken = null,
        ?FileSystemManager $fileSystem = null
    ) {
        parent::__construct($request, $response, $session, $cookie, $csrfToken, $fileSystem);
    }
}

The router can also inject request/response at runtime via:

$controller->setRequestResponse($request, $response);

This is useful for tests where you construct the controller yourself.


Custom FormRequest Validation

A FormRequest should encapsulate:

  • The validation rules
  • The custom messages
  • The validation execution and failure behavior
class RegisterRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'     => 'required|min:3',
            'email'    => 'required|email|unique:users,email',
            'password' => 'required|min:8|confirmed',
        ];
    }

    public function messages(): array
    {
        return [
            'email.unique' => 'This email is already registered.',
        ];
    }
}

Then in the controller:

$data = $this->validateWith(RegisterRequest::class);

validateWith() will:

  1. Assert the class extends FormRequest.
  2. Resolve it via Container::resolve() with the current request context.
  3. Run $formRequest->validate().
  4. On failure: respondWithErrors() for AJAX, redirectBack() otherwise — and exit.
  5. On success: return $formRequest->validated().

⚠️ Important: validateWith() and validate() both call exit on failure. This guarantees no code after validation runs when validation fails — but it also means you cannot catch validation failures with try/catch. Treat validation as a terminal operation.


Middleware Pipeline & Execution Flow

The middleware contract is:

abstract public function handle(
    HttpRequest  $request,
    HttpResponse $response,
    callable     $next,
    array        $params = []
);

The $next callable accepts the route parameters array and returns the next middleware's result (ultimately, the controller's return value).

Request → Middleware A → Middleware B → Controller
                            ↓
Response ← Middleware A ← Middleware B ← (return value)

Key behaviors to understand:

  1. Short-circuiting: If a middleware returns without calling $next, the pipeline stops and its return value is sent as the response.
  2. Response mutation: A middleware can call $next(), then modify the result before returning it.
  3. Parameter passing: $params flows through the pipeline unchanged unless a middleware alters it.
  4. Exception propagation: Throwing MachinjiriException from middleware aborts the request (use $this->abort() from controllers, or throw directly from middleware).

Authentication Example

public function handle($request, $response, callable $next, array $params = [])
{
    $userId = $request->session()->get('user_id');

    if (!$userId) {
        if ($this->expectsJson()) {
            return $response->setStatusCode(401)
                            ->setJsonBody(['error' => 'Unauthenticated']);
        }
        return $response->setStatusCode(302)
                        ->setHeader('Location', '/login');
    }

    // Attach the user to the params so downstream can use it
    $params['auth_user_id'] = $userId;

    return $next($params);
}

CORS Example

public function handle($request, $response, callable $next, array $params = [])
{
    $result = $next($params);

    $result->setHeader('Access-Control-Allow-Origin', '*');
    $result->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    $result->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

    return $result;
}

CSRF Protection Deep Dive

The controller exposes two CSRF helpers:

// Generate the hidden input field (safe for HTML embedding)
echo $this->csrfField();
// <input type="hidden" name="_token" value="...">

// Verify a token from the request (form field OR X-CSRF-TOKEN header)
if (!$this->verifyCsrf()) {
    $this->abort(419, 'CSRF token mismatch');
}

In a form

<form method="POST" action="/users">
    <?= $this->csrfField() ?>
    <input name="name" value="<?= $this->old('name') ?>">
    <button type="submit">Create</button>
</form>

In AJAX

Send the token in the X-CSRF-TOKEN header:

fetch('/users', {
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({ name: 'Jane' }),
});

Best practice: wrap verification in middleware

Rather than verifying CSRF in every controller action, create a VerifyCsrfToken middleware that checks all POST/PUT/PATCH/DELETE requests. Controllers then only call csrfField() when rendering forms.


AJAX & JSON Responses

Both validate() and validateWith() automatically detect AJAX/JSON requests and return structured JSON on failure:

{
    "errors": {
        "email": ["The email field is required."],
        "password": ["The password must be at least 8 characters."]
    }
}

Detection uses:

  • $request->isAjax() — checks X-Requested-With: XMLHttpRequest
  • $request->expectsJson() — checks Accept: application/json

You can exploit the same detection in your own controllers:

public function destroy(HttpRequest $request, int $id)
{
    User::find($id)->delete();

    if ($request->isAjax() || $request->expectsJson()) {
        return $this->json(['deleted' => true]);
    }

    $this->flash('success', 'User deleted.');
    return $this->redirect('/users');
}

Middleware Termination Hooks

The terminate() method on AbstractMiddleware is an optional hook that runs after the response has been sent to the client.

public function terminate(HttpRequest $request, HttpResponse $response): void
{
    // Runs after the client has received the response
    $this->logger->info('Request completed', [
        'status' => $response->getStatusCode(),
        'path'   => $request->getPath(),
    ]);
}

Why use it? Termination hooks are ideal for work that shouldn't delay the response:

  • Writing access logs to disk
  • Sending analytics events
  • Cleaning up temporary files
  • Flushing buffers

The default implementation is a no-op — override it only when needed.

Note: Your application kernel is responsible for invoking terminate() on each middleware after sending the response. If your kernel doesn't call it yet, termination hooks won't fire.


Complete API Reference

AbstractController — Public/Protected Methods

Method Signature Description
__construct (?HttpRequest, ?HttpResponse, ?Session, ?Cookie, ?CSRFToken, ?FileSystemManager) Injects dependencies (falls back to container)
setRequestResponse (HttpRequest, HttpResponse): void Runtime injection (used by router/tests)
view (string, array = []): string Render a view to string
json (mixed, int = 200): HttpResponse Build a JSON response
redirect (string, int = 302): void Redirect and send immediately
back (int = 302): HttpResponse Redirect to referer
redirectToRoute (string, array = [], int = 302): HttpResponse Redirect by route name
asset (string): string Versioned asset URL
style (string, array = []): void Output <link> tag
script (string, array = []): void Output <script> tag
share `(string array, mixed = null): void`
session (): Session Get the session instance
flash (string, mixed): void Set flash message
getFlash (string, mixed = null): mixed Read & clear flash message
withInput (?array = null): void Preserve input for next request
old (string, mixed = null): mixed Read preserved input
errors (): FormErrorBag Read validation errors
csrfField (): string Hidden CSRF input tag
verifyCsrf (): bool Validate CSRF token from request
validateWith (string): array Validate via FormRequest class
validate (array, array = []): array Validate with simple rules
upload (string = 'local', string = 'uploads'): FileUpload Get upload handler
abort (int, string = ''): void Throw HTTP exception
abortIf (bool, int, string = ''): void Conditional abort

AbstractMiddleware — Public/Protected Methods

Method Signature Description
handle (HttpRequest, HttpResponse, callable, array = []) Abstract — must implement
terminate (HttpRequest, HttpResponse): void Optional post-response hook
isAjax (): bool Detect AJAX requests
expectsJson (): bool Detect JSON-accepting requests
getRouteParams (HttpRequest): array Fetch route params
next (callable, array = []) Invoke next middleware

Best Practices

Controllers

  1. Keep controllers thin. Delegate business logic to services or models. A controller action should ideally be 5–15 lines.
  2. Use FormRequest for anything non-trivial. It keeps validation rules reusable and testable.
  3. Return responses; don't echo. Returning from an action lets the kernel handle output, headers, and testing.
  4. Use abort() for expected error conditions (e.g., 404) rather than returning manual error responses — it centralizes error handling.
  5. Prefer redirectToRoute() over hardcoded URLs — it survives URL changes.
  6. Always include csrfField() in state-changing forms.

Middleware

  1. Always return $next($params) unless you intend to short-circuit.
  2. Don't do heavy work before $next(). The request hasn't been handled yet; latency-sensitive tasks belong after $next() (or in terminate()).
  3. One concern per middleware. Don't combine auth + logging + CORS. Compose them in the pipeline.
  4. Use terminate() for post-response work (logging, analytics) to avoid delaying the client.
  5. Order matters. Auth middleware should run before anything that assumes a user context. CORS middleware should run first so it can attach headers even to error responses.

General

  1. Never call session()->start() manually in a controller — the kernel handles it.
  2. Beware of exit in validate() / validateWith(). These are terminal by design. Don't wrap them in try/catch expecting to intercept failures.
  3. Use expectsJson() and isAjax() together when detecting API clients — isAjax() alone misses non-browser clients that send Accept: application/json but not X-Requested-With.

Troubleshooting

"Cannot instantiate abstract class AbstractController"

You're trying to use the base class directly. Every controller must extend it:

class MyController extends AbstractController { ... }

Validation "silently" aborts my controller

That's expected. validate() and validateWith() call exit on failure. If you need custom failure behavior, either:

  • Handle validation manually using FormValidator directly, or
  • Override the failure behavior in your FormRequest.

redirect() returns null

Because redirect() calls ->send() internally and returns nothing, return $this->redirect(...) returns null. If you need the response object (e.g., for testing), construct it directly or refactor redirect() to return instead of send.

Middleware doesn't run

Check:

  1. The middleware is registered in the route or globally.
  2. It actually calls $next($params).
  3. No earlier middleware short-circuited the pipeline.

CSRF verification always fails

  • Ensure csrfField() is present in the form and that the form uses method="POST".
  • For AJAX, send the X-CSRF-TOKEN header.
  • Confirm the session cookie is being sent (same origin, SameSite compatible).
  • Check that the token in the session hasn't expired or been regenerated.

old() returns null

old() reads from the old_input session key, which is populated by withInput() or automatically by failed validation. If you call old() outside a redirect flow, it will be empty. Call withInput() before redirecting.

Middleware terminate() never fires

Your kernel must invoke terminate() on each middleware after sending the response. If it doesn't, implement it in your kernel's dispatch loop:

foreach ($middlewareChain as $mw) {
    $mw->terminate($request, $response);
}

Summary Cheat Sheet

// ─── Controllers ─────────────────────────────────────────────
return $this->view('posts.index', ['posts' => $posts]);
return $this->json(['ok' => true]);
return $this->redirect('/home');
return $this->back();
return $this->redirectToRoute('post.show', ['id' => 1]);

$this->flash('success', 'Saved!');
$msg = $this->getFlash('success');

$this->withInput();
$name = $this->old('name');

$errors = $this->errors();

$data = $this->validate(['email' => 'required|email']);
$data = $this->validateWith(CreatePostRequest::class);

echo $this->csrfField();
$ok = $this->verifyCsrf();

$upload = $this->upload('local', 'avatars');

$this->abort(403, 'Forbidden');
$this->abortIf(!$user, 404);

// ─── Middleware ──────────────────────────────────────────────
public function handle($request, $response, callable $next, array $params = [])
{
    if ($this->isAjax()) { /* ... */ }
    if ($this->expectsJson()) { /* ... */ }

    $routeParams = $this->getRouteParams($request);

    return $next($params);          // continue
    return $this->next($next, $params); // equivalent
}

public function terminate($request, $response): void { /* post-response */ }

Clone this wiki locally