-
Notifications
You must be signed in to change notification settings - Fork 2
View
The View class is the base abstract class that manages the HTML layout layer in Pair modules.
A Pair view is responsible for:
- preparing data via a Model;
- assigning variables to the layout (
assign()); - rendering a layout file (
layouts/*.php); - providing common helpers for translation, pagination and sorting.
In Pair, a view is usually used together with a module Controller and Model:
- the controller orchestrates the request;
- the model contains business logic and DB access;
- the view renders the final HTML.
Pair determines the module name and the default layout from the View class name.
The module name is the part before View in the class name, lowercased.
Example:
-
UsersViewDefault→ module name:users
The module public URL becomes:
modules/users
The layout name is the part after View in the class name, with the first letter lowercased.
Example:
-
UsersViewDefault→ layout:default -
OrdersViewList→ layout:list
Layout files are expected inside the module folder in:
layouts/<layout>.php
The file path is resolved as:
<modulePath>/<scriptPath>/<layout>.php
Where:
-
modulePathis detected by reflection (folder of the view file); -
scriptPathdefaults tolayouts/; -
layoutis computed from the class name or passed todisplay().
A view is created by passing a Model instance:
// The controller instantiates the view with its model.
$view = new UsersViewDefault($model);At construction time Pair automatically:
- loads singleton objects:
Application::getInstance()Router::getInstance()Translator::getInstance()
- builds
modulePath,moduleUrland the defaultlayout; - creates a
Paginationobject and attaches it both to the view and the model:$this->pagination$this->model->pagination
- sets
pagination->perPagefromOptions::get('pagination_pages') - sets
pagination->pagefromRouter::getInstance()->getPage() - calls the optional
_init()hook (exceptions are ignored).
protected function _init(): void {}Override _init() in child views to run logic before rendering (e.g. set defaults, check permissions, preload shared data).
Example:
protected function _init(): void
{
// Prepares data shared by every layout rendered by this view.
$this->assign('pageTitle', 'Users');
}abstract protected function render(): void;Every view must implement render().
This is where you:
- read the router state (page, filters, id, order, etc.);
- call the model to fetch data;
- assign variables to the layout using
assign().
Example:
protected function render(): void
{
// Reads the current page of users from the model.
$users = $this->model->getItems(\App\Orm\User::class);
// Makes data available to layouts/default.php.
$this->assign('users', $users);
// Prepares pagination HTML for the layout.
$this->assign('paginationBar', $this->getPaginationBar());
}In practice render() is the main method of every Pair view. It is where you compose the page:
- read route state from
Router - load rows or objects from the model
- compute pagination and sorting state
- assign only the variables the layout really needs
Example with filters and pagination:
protected function render(): void
{
// Reads the current search filter from the URL or from persisted state.
$search = \Pair\Core\Router::get('q') ?? (string)($this->getState('users.q') ?? '');
$this->setState('users.q', $search);
// Loads the current page of rows.
$items = $this->model->getItems(\App\Orm\User::class);
// Loads the total number of rows for pagination.
$this->pagination->count = $this->model->countItems(\App\Orm\User::class);
$this->assign('q', $search);
$this->assign('items', $items);
}final public function display(?string $name = null): voiddisplay() does two things:
- executes
render()(inside a try/catch); - includes the layout file
layouts/<layout>.php.
If $name is provided, it is used as the layout name; otherwise Pair uses the default layout derived from the class name.
Example:
// Uses the layout inferred from the class name, usually layouts/default.php.
$view->display(); // uses the default layout
// Renders a different layout file from the same module.
$view->display('edit'); // uses layouts/edit.phpIf the layout file is missing, a CriticalException is thrown.
If render() throws, Pair shows a modal error and redirects:
- if layout is not
default→ redirects back (module-level) - otherwise → redirects to the user default destination
(Modal/redirect helpers are provided by Pair traits used by View.)
Adds a variable to the internal layout variables bag.
// Exposes a simple scalar to the layout.
$this->assign('title', 'Users');
// Exposes a collection or array to the layout.
$this->assign('items', $items);In the layout file you can access assigned variables via $this->title, $this->items, etc.
When a layout reads $this->something:
- Pair first looks in assigned variables (
assign()), - then in real class properties,
- otherwise throws
AppException(ErrorCodes::PROPERTY_NOT_FOUND).
This makes layouts predictable and helps detect typos early.
Sets a property on the view at runtime.
This can be used to inject extra data without creating explicit setters.
$this->customFlag = true;If a layout or controller calls a missing method, an AppException is thrown (ErrorCodes::METHOD_NOT_FOUND).
Pair uses session “state variables” through the Application object.
// Persists the filter across requests.
$this->setState('filters.users.search', 'john');// Restores the same filter later.
$search = $this->getState('filters.users.search');Use these when you want filters and UI preferences to persist across requests.
Views provide shortcuts for translated strings.
Returns a translated string.
// Simple translation key.
$title = $this->lang('USERS');
// Translation with placeholders.
$title = $this->lang('WELCOME_USER', ['name' => 'Marino']);Prints a translated string directly.
// Prints the translated string immediately.
$this->_('SAVE');This helper reads an item id from the URL using Router::get() and returns a loaded ActiveRecord instance.
- If the id is missing → throws
AppException(ErrorCodes::RECORD_NOT_FOUND) - If the record is not found → throws
AppException(ErrorCodes::RECORD_NOT_FOUND)
Example (edit page):
protected function render(): void
{
// Reads the id from the first router param and loads the object.
$user = $this->getObjectRequestedById(\App\Orm\User::class);
$this->assign('user', $user);
}If you need the id from a different router position, pass $pos:
// Reads the id from the second route param instead of the first.
$user = $this->getObjectRequestedById(\App\Orm\User::class, 1);Each view owns a Pagination object ($this->pagination) created in the constructor.
The same object is also injected into the model ($this->model->pagination).
Returns the rendered pagination bar HTML.
If pagination->count is null, the logger emits a warning (because total count is needed to paginate properly).
Typical pattern:
// Loads the current page of rows.
$items = $this->model->getItems(\App\Orm\User::class);
// Sets the total count required by the pagination component.
$this->pagination->count = $this->model->countItems(\App\Orm\User::class);
$this->assign('items', $items);Then in layout:
<?php if ($this->mustUsePagination($this->items)): ?>
<?php // Renders the pagination bar only when needed. ?>
<?= $this->getPaginationBar() ?>
<?php endif; ?>Shortcut that prints the pagination bar:
// Shortcut version when you want to print directly.
$this->printPaginationBar();Returns true if pagination should be displayed.
Pair checks:
- number of items shown is at least
perPage, or - current page is greater than 1.
Example:
if ($this->mustUsePagination($this->items)) {
// Prints pagination only when the current dataset needs it.
$this->printPaginationBar();
}Prints a standardized “No data” message box.
if (!$this->items) {
// Prints the default "no data" alert.
$this->noData();
}With a custom message:
// Prints a custom empty-state message.
$this->noData('No users found for the selected filters.');Prints a column header as a link that toggles sorting direction using Router::getOrderUrl().
Behavior:
- if
$titleis all uppercase, Pair translates it automatically vialang(); - if current order equals
$ascOrder, an “up arrow” icon is shown and the link switches to$descOrder; - if current order equals
$descOrder, a “down arrow” icon is shown and the link resets order to0; - otherwise, the link sets order to
$ascOrder.
Example (in a layout table header):
<th><?php $this->sortable('NAME', 2, 3); ?></th>
<th><?php $this->sortable('EMAIL', 4, 5); ?></th>Then your model can map router order values to SQL order clauses (see Model::getOrderOptions() and Model::getOrderLimitSql()).
<?php
namespace App\Modules\Users;
use Pair\Core\View;
class UsersViewDefault extends View {
protected function render(): void
{
// Loads the current page of users.
$items = $this->model->getItems(\App\Orm\User::class);
// Loads the full count for the pagination bar.
$this->pagination->count = $this->model->countItems(\App\Orm\User::class);
// Makes the rows available to the layout.
$this->assign('items', $items);
}
}<h1><?= $this->lang('USERS') ?></h1>
<?php if (!$this->items): ?>
<?php // Empty state. ?>
<?php $this->noData(); ?>
<?php else: ?>
<table>
<thead>
<tr>
<?php // Sortable table headers. ?>
<th><?php $this->sortable('NAME', 2, 3); ?></th>
<th><?php $this->sortable('EMAIL', 4, 5); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($this->items as $user): ?>
<tr>
<?php // Escapes user data before rendering. ?>
<td><?= htmlspecialchars($user->name) ?></td>
<td><?= htmlspecialchars($user->email) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ($this->mustUsePagination($this->items)): ?>
<?php // Shows pagination only when the current page needs it. ?>
<?= $this->getPaginationBar() ?>
<?php endif; ?>
<?php endif; ?>assign($name, $val): voiddisplay(?string $name = null): voidlang(string $key, string|array|null $vars = null, bool $warning = true): stringgetPaginationBar(): stringmustUsePagination(array $itemsToShow): boolsortable(string $title, int $ascOrder, int $descOrder): void-
setState($name, $value): void,getState($name): mixed noData(?string $customMessage = null): void
These methods are not the main reason you extend View, but they explain how layouts stay safe and predictable:
-
__get(string $name): mixedResolves assigned variables first, then real properties, and throws on typos. -
__set(string $name, mixed $value): voidLets you inject runtime flags or helpers without dedicated setters. -
__call($name, $arguments): voidThrowsMETHOD_NOT_FOUNDwhen a layout calls a missing method. -
_init(): voidOptional pre-render hook; exceptions are swallowed. -
display(?string $name = null): voidCatches render exceptions, shows a modal error, then redirects.
protected function render(): void
{
// Reads the filter from the URL first, then falls back to persisted state.
$search = \Pair\Core\Router::get('q') ?? (string)($this->getState('users.q') ?? '');
// Persists the latest filter for the next request.
$this->setState('users.q', $search);
// Loads the current page.
$items = $this->model->getItems(\App\Orm\User::class);
// Loads the full count for the pagination bar.
$this->pagination->count = $this->model->countItems(\App\Orm\User::class);
$this->assign('q', $search);
$this->assign('items', $items);
}See also: Controller, Model, Form, Pagination, TemplateRenderer.