Repository navigation
Controllers and Middleware
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.
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.
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.
Use the built-in Artisan command:
php artisan make:controller UserControllerThis 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 thewelcomeview 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.
php artisan make:middleware AuthMiddlewareGenerated 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);
}All helpers below are protected — available inside any controller extending AbstractController.
// 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'return $this->json(['status' => 'ok'], 200);
return $this->json(['error' => 'Not found'], 404);// 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 theHttpResponseand let the kernel send it.
// 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]);// Share data with every view rendered in this request
$this->share('appName', 'Machinjiri');
$this->share(['user' => $user, 'theme' => 'dark']);// Throw an HTTP exception (404)
$this->abort(404, 'User not found');
// Conditional abort
$this->abortIf(!$user, 404, 'User not found');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.
There are two validation approaches: simple rules and FormRequest classes.
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 |
validate() automatically:
- Stores errors in
form_errorssession key. - Stores submitted input in
old_inputsession key. - For AJAX/JSON requests → returns 422 JSON with
{ errors: {...} }and exits. - For normal requests → redirects back to the referer and exits.
You never need to handle validation failure manually.
<?php $errors = $this->errors(); ?>
<?php if ($errors->has('email')): ?>
<span class="error"><?= $errors->first('email') ?></span>
<?php endif; ?>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.
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 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');// 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.
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);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;
}public function handle($request, $response, callable $next, array $params = [])
{
if (!$this->expectsJson()) {
return $response->setStatusCode(406)->setJsonBody(['error' => 'JSON required']);
}
return $next($params);
}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:
- Do nothing — the container supplies everything.
-
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.
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:
- Assert the class extends
FormRequest. - Resolve it via
Container::resolve()with the current request context. - Run
$formRequest->validate(). - On failure:
respondWithErrors()for AJAX,redirectBack()otherwise — andexit. - On success: return
$formRequest->validated().
⚠️ Important:validateWith()andvalidate()both callexiton 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.
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:
-
Short-circuiting: If a middleware returns without calling
$next, the pipeline stops and its return value is sent as the response. -
Response mutation: A middleware can call
$next(), then modify the result before returning it. -
Parameter passing:
$paramsflows through the pipeline unchanged unless a middleware alters it. -
Exception propagation: Throwing
MachinjiriExceptionfrom middleware aborts the request (use$this->abort()from controllers, or throw directly from middleware).
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);
}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;
}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');
}<form method="POST" action="/users">
<?= $this->csrfField() ?>
<input name="name" value="<?= $this->old('name') ?>">
<button type="submit">Create</button>
</form>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' }),
});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.
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()— checksX-Requested-With: XMLHttpRequest -
$request->expectsJson()— checksAccept: 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');
}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.
| 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 |
| 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 |
- Keep controllers thin. Delegate business logic to services or models. A controller action should ideally be 5–15 lines.
- Use FormRequest for anything non-trivial. It keeps validation rules reusable and testable.
-
Return responses; don't
echo. Returning from an action lets the kernel handle output, headers, and testing. -
Use
abort()for expected error conditions (e.g., 404) rather than returning manual error responses — it centralizes error handling. -
Prefer
redirectToRoute()over hardcoded URLs — it survives URL changes. - Always include
csrfField()in state-changing forms.
-
Always return
$next($params)unless you intend to short-circuit. -
Don't do heavy work before
$next(). The request hasn't been handled yet; latency-sensitive tasks belong after$next()(or interminate()). - One concern per middleware. Don't combine auth + logging + CORS. Compose them in the pipeline.
-
Use
terminate()for post-response work (logging, analytics) to avoid delaying the client. - 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.
-
Never call
session()->start()manually in a controller — the kernel handles it. -
Beware of
exitinvalidate()/validateWith(). These are terminal by design. Don't wrap them intry/catchexpecting to intercept failures. -
Use
expectsJson()andisAjax()together when detecting API clients —isAjax()alone misses non-browser clients that sendAccept: application/jsonbut notX-Requested-With.
You're trying to use the base class directly. Every controller must extend it:
class MyController extends AbstractController { ... }That's expected. validate() and validateWith() call exit on failure. If you need custom failure behavior, either:
- Handle validation manually using
FormValidatordirectly, or - Override the failure behavior in your
FormRequest.
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.
Check:
- The middleware is registered in the route or globally.
- It actually calls
$next($params). - No earlier middleware short-circuited the pipeline.
- Ensure
csrfField()is present in the form and that the form usesmethod="POST". - For AJAX, send the
X-CSRF-TOKENheader. - Confirm the session cookie is being sent (same origin,
SameSitecompatible). - Check that the token in the session hasn't expired or been regenerated.
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.
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);
}// ─── 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 */ }Machinjiri Docs
- Home
- Learn
- Build
-
Tutorials
- Routing
- Controllers and Middleware
- Views and Templates
- Database ORM
- Eloquent Database ORM
- Errors and Exceptions
- Forms and Validation
- Http
- Events and Logging
- Services and DI
- Security
- Authentication
- Caching
- File System
- Date and Time
- Debugging
- Queue and Jobs
- Task Scheduling
- Transport (Mail, SMS)
- LDAP
- Notifications
- Webhooks
- UUID Generations
- Terminal
- Helper Function
- Reference