-
Notifications
You must be signed in to change notification settings - Fork 2
Controller
Pair\Core\Controller is the base class for Pair MVC controllers.
It is responsible for:
- loading the model for the current module
- selecting the correct view for the current action
- coordinating request flow, redirects, and UI errors
- keeping controllers thin while models/views do the real work
In Pair, the controller is the orchestration layer. It should decide what happens next, not contain the business logic itself.
final public function __construct()Current execution flow:
- loads
Application,Router, andTranslatorsingletons - derives the controller name from the class name
- resolves the module path via reflection
- tells
Translatorwhich module is active - loads the default model from
model.phpif$modelwas not already set - runs
_init() - if the app is not headless and no view was selected yet, loads the action view or
default
Because the constructor is final, controller setup belongs in _init().
Given OrdersController:
- module folder:
modules/orders/ - default model file:
modules/orders/model.php - default model class:
OrdersModel - view file for action
edit:modules/orders/viewEdit.php - view class for action
edit:OrdersViewEdit
That convention is what makes Pair able to autoload the MVC stack with very little configuration.
Optional setup hook executed after the default model is ready and before the default view is autoloaded.
Use it for:
- access checks
- action-specific model swapping
- preselecting a non-default view
- headless controller setup
Example:
protected function _init(): void
{
// Swaps the model only for export actions.
$this->loadModelForActions('exportModel', ['exportCsv', 'exportXlsx']);
// Forces a custom landing view when the route is /orders/default.
if (($this->router->action ?: 'default') === 'default') {
$this->setView('dashboard');
}
}Loads and stores a view instance for the controller.
Current behavior:
- looks for
view<ViewName>.php - if that file is missing, falls back to
viewDefault.php - instantiates
<ControllerName>View<ViewName> - avoids reloading the same view class twice
Example:
public function createAction(): void
{
// Uses modules/orders/viewEdit.php and OrdersViewEdit.
$this->setView('edit');
}Validates that the current view is a real Pair\Core\View subclass and then calls display().
This is the method the application eventually uses to render HTML output for classic module requests.
Loads a non-default model from the current module folder.
In OrdersController, this call:
$this->loadModel('exportModel');expects:
- file
modules/orders/exportModel.php - class
OrdersExportModel
Use it when one action needs a different query layer or a specialized form/export workflow.
Conditional shortcut around loadModel().
If the current router action is in the provided list, the controller swaps the model automatically.
Example:
protected function _init(): void
{
// Export actions use a specialized model with streaming queries.
$this->loadModelForActions('exportModel', ['exportCsv', 'exportXlsx']);
}Loads an ActiveRecord from the first route parameter (Router::get(0)).
Behavior:
- throws when the id is missing
- instantiates
new $class($itemId) - throws again if the object is not loaded
This is one of the most-used controller helpers for edit/detail actions.
Example:
public function editAction(): void
{
// Reads the id from /orders/edit/{id}.
$order = $this->getObjectRequestedById(\App\Orm\Order::class);
// Continue with the edit flow.
}Converts model/object validation failures into a controller-level exception.
It:
- collects errors from
getErrors() - builds one user-facing message
- logs the failing object class
- throws an exception for the current action flow
Use it after store() or other write operations when you want one consistent failure path.
Shortcut for:
- queueing an error toast
- redirecting the user
It is useful in catch blocks and permission failures.
Protected convenience method for authorization failures.
It redirects to the module root (strtolower($this->name)) with a standard translated error title/message.
class OrdersController extends \Pair\Core\Controller {
protected function _init(): void
{
// Export actions use a specialized model.
$this->loadModelForActions('exportModel', ['exportCsv', 'exportXlsx']);
}
public function editAction(): void
{
try {
// Loads the requested object from /orders/edit/{id}.
$order = $this->getObjectRequestedById(\App\Orm\Order::class);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Applies submitted data.
$order->note = trim($_POST['note'] ?? '');
if ($order->store()) {
// Uses PRG after a successful save.
$this->toastRedirect('Saved', 'Order updated', 'orders/list');
return;
}
// Converts object validation errors into one exception.
$this->raiseError($order);
}
// Exposes the object to the view.
$this->order = $order;
// Renders modules/orders/viewEdit.php.
$this->setView('edit');
} catch (\Throwable $e) {
// Keeps the UI flow consistent on any failure.
$this->redirectWithError($e->getMessage(), 'orders/list');
}
}
}protected function _init(): void
{
// Disables automatic HTML view loading for this controller.
$this->app->headless = true;
}In this mode the constructor skips the default setView(...) call.
These methods are used less often directly, but they explain how the controller fits into the Pair runtime:
-
getView(): ?ViewReturns the currently selected view instance. -
getState(string $name): mixedProxy toApplication::getState()for persistent UI state. -
lang(string $key, string|array|null $vars = null): stringThin translation proxy, especially useful in AJAX flows. -
__get(string $name): mixedExposes controller properties and throws on unknown properties. -
__set(string $name, mixed $value): voidLets actions assign ad-hoc data such as$this->order = $order.
- Default model loading directly includes
model.php, so module naming must stay consistent. -
setView('missing')does not fail immediately ifviewDefault.phpexists; it falls back to the default view file. -
getObjectRequestedById()always reads router position0; if you need another position, theViewhelper is more flexible. - In headless mode (
$app->headless = true), the constructor does not autoload a view. - Most controller helper failures throw generic exceptions, not only
AppException.
See also: Model, View, Router, Application, Translator, ActiveRecord.