Controllers
A controller is a plain class whose public methods answer routes. Extend
Pop\Controller\AbstractController, give it a method per action, and name the pair in the route table.
A Controller and Its Actions#
The smallest controller that a controller/action route can dispatch:
<?php
namespace App\Http\Controller;
use Pop\Controller\AbstractController;
class StatusController extends AbstractController
{
public function index(): void
{
echo 'OK';
}
}
<?php
return [
'routes' => [
'/status' => [
'controller' => 'App\Http\Controller\StatusController',
'action' => 'index'
]
]
];
Extending AbstractController is what makes the router treat the class as a dispatchable rather than as a
plain callable. The router tests is_subclass_of($dispatchable, 'Pop\Dispatch\AbstractDispatcher'), and
AbstractController extends AbstractDispatcher — so a controller is constructed once and then dispatched
through dispatch($action, $params), which is the method that decides which of its own methods to call.
That controller receives nothing except its route parameters. Pop\Controller\AbstractController declares no
constructor and no application() method of its own; a controller that extends nothing else gets
new StatusController() and that's all. That's everything the router needs. Whether it is everything the
rest of an application needs is a separate question, and one the scaffolded base controller answers.
Getting the Application, Request and Response#
The framework stops at the application deliberately: a controller might answer a URL or a console command, so rather than guess, it hands over neither a request nor a console. You declare the environment by adding a trait.
Declaring Pop\Dispatch\HttpTrait is what changes that. The trait supplies a constructor taking the
application, a Pop\Http\Server\Request and a Pop\Http\Server\Response, the last two defaulted, so a
controller declaring it has all three without writing a constructor of its own.
<?php
namespace App\Http\Controller;
use Pop\Controller\AbstractController;
use Pop\Dispatch\HttpTrait;
class UsersController extends AbstractController
{
use HttpTrait;
public function index(): void
{
$users = $this->application()->getService('users');
$this->response->setBody(json_encode($users->getAll()));
$this->response->send(200, ['Content-Type' => 'application/json']);
}
public function edit(string $id): void
{
if (!$this->request->isPost()) {
$this->response->setBody('');
$this->response->send(405, ['Allow' => 'POST']);
return;
}
}
}
Each trait brings a different constructor and a different set of accessors:
| Trait | Constructor | Available afterward |
|---|---|---|
Pop\Dispatch\HttpTrait |
__construct(?Application $application = null, Request $request = new Request(new Uri()), Response $response = new Response()) |
application(), request(), response() |
Pop\Dispatch\ConsoleTrait |
__construct(?Application $application = null, Console $console = new Console(120)) |
application(), console() |
Both forms work — $this->request() and $this->request are the same object, and the properties are what
the scaffolded base controller uses.
The setBody('') in edit() above is not decoration. A Pop\Http\Server\Response starts with no body object
at all, and send() renders the body unconditionally, so sending a bodiless 405 or 204 without setting a body
first fails with Call to a member function render() on null. Set an empty string when the status is the whole
answer.
Trait detection happens on the controller/action route path — see Routing for how
that form differs from a shorthand string target. To construct a controller with something other than the
application, see Dispatchable Parameters.
Actions and Route Parameters#
Route parameters arrive positionally, in the order the pattern declares them, and they are always strings. An
action whose route has one required parameter takes one argument; one whose parameter is optional needs a
default value in the signature, because the router passes nothing at all rather than passing null.
| Route | Action signature |
|---|---|
/users |
index() |
/users/:id |
edit(string $id) |
/reports[/:year] |
reports(?string $year = null) |
/tags/:names* |
tags(array $names) |
users edit <id> [--force] |
edit(string $id, array $options = []) |
A console route's options are the exception to "positional": every [--option] the command matched arrives
together in one trailing array, after the positional parameters.
The Default Action#
AbstractDispatcher::dispatch() looks for a method matching the route's action. When it does not find one, it
falls back to the controller's default action rather than failing, and the default default is error:
$controller = new App\Http\Controller\UsersController();
$controller->setDefaultAction('notFound');
echo $controller->getDefaultAction(); // notFound
That fallback is why every scaffolded controller has an error() method: a route naming an action the class
doesn't define dispatches error() instead, so a '*' catch-all and a typo both render the same page.
A dynamic route is the one case where the action can be absent entirely; the router substitutes index for it
before dispatching.
Keep helper methods out of the action namespace. The action lookup is method_exists() and the call is
made from inside the class, so any method a route names is reachable — the route table is the access
boundary, not visibility.
Console Controllers#
A console controller is the same class shape with the other trait. $this->console is a
Pop\Console\Console instance, and the options array is the trailing parameter:
<?php
namespace App\Console\Controller;
use Pop\Controller\AbstractController;
use Pop\Dispatch\ConsoleTrait;
class UsersController extends AbstractController
{
use ConsoleTrait;
public function index(array $options = []): void
{
$limit = $options['limit'] ?? 25;
$this->console->write('Listing ' . $limit . ' users');
$this->console->send();
}
}
Output, colors, prompts and generated help are covered in Building Console Applications.
Maintenance Mode#
Pop\Controller\AbstractController implements Pop\Dispatch\MaintenanceInterface and uses
Pop\Dispatch\MaintenanceTrait, so every controller already participates in maintenance mode. When
MAINTENANCE_MODE is on and the request carries no secret, run() calls the controller's maintenance action
instead of the routed one — maintenance by default, changeable with setMaintenanceAction().
class HealthController extends Pop\Controller\AbstractController
{
use Pop\Dispatch\HttpTrait;
protected bool $bypassMaintenance = true;
public function index(): void
{
$this->response->setBody('OK');
$this->response->send(200, ['Content-Type' => 'text/plain']);
}
public function maintenance(): void
{
$this->response->setBody('');
$this->response->send(503);
}
}
Setting $bypassMaintenance to true — or calling setBypassMaintenance() — exempts a controller entirely,
which is what a health check or a status endpoint wants. A controller with neither a maintenance() method
nor a changed maintenance action throws Pop\Dispatch\Exception when maintenance mode is on, so define one on
the base controller and let it be inherited.
Controllers expose more than this page covers — MaintenanceTrait's full surface, and the dispatcher's own
accessors — see the popphp README.
See Also#
- Routing — how a route names a controller and action, and the shorthand target forms
- Requests & Responses — what
$this->requestand$this->responsecan do - Views & Templates — rendering the view a controller prepares
- Models — where the logic an action calls into belongs
- Services — defining what
getService()resolves - popphp README — the full controller and dispatcher API