Skip to content
Viames Marino edited this page Mar 26, 2026 · 4 revisions

Pair framework: View class

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.

Naming convention and layout resolution

Pair determines the module name and the default layout from the View class name.

Module 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

Default layout name

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 file path

Layout files are expected inside the module folder in:

  • layouts/<layout>.php

The file path is resolved as:

<modulePath>/<scriptPath>/<layout>.php

Where:

  • modulePath is detected by reflection (folder of the view file);
  • scriptPath defaults to layouts/;
  • layout is computed from the class name or passed to display().

Lifecycle

1) Construction

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, moduleUrl and the default layout;
  • creates a Pagination object and attaches it both to the view and the model:
    • $this->pagination
    • $this->model->pagination
  • sets pagination->perPage from Options::get('pagination_pages')
  • sets pagination->page from Router::getInstance()->getPage()
  • calls the optional _init() hook (exceptions are ignored).

2) _init() hook

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');
}

3) render() (required)

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);
}

4) display() (renders and includes the layout)

final public function display(?string $name = null): void

display() does two things:

  1. executes render() (inside a try/catch);
  2. 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.php

If 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.)


Assigning variables to layouts

assign($name, $val): void

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.


__get(string $name): mixed

When a layout reads $this->something:

  1. Pair first looks in assigned variables (assign()),
  2. then in real class properties,
  3. otherwise throws AppException (ErrorCodes::PROPERTY_NOT_FOUND).

This makes layouts predictable and helps detect typos early.


__set(string $name, mixed $value): void

Sets a property on the view at runtime.

This can be used to inject extra data without creating explicit setters.

$this->customFlag = true;

__call($name, $arguments): void

If a layout or controller calls a missing method, an AppException is thrown (ErrorCodes::METHOD_NOT_FOUND).


Session state helpers

Pair uses session “state variables” through the Application object.

setState($name, $value): void

// Persists the filter across requests.
$this->setState('filters.users.search', 'john');

getState($name): mixed

// Restores the same filter later.
$search = $this->getState('filters.users.search');

Use these when you want filters and UI preferences to persist across requests.


Translation helpers

Views provide shortcuts for translated strings.

lang(string $key, string|array|null $vars = null, bool $warning = true): string

Returns a translated string.

// Simple translation key.
$title = $this->lang('USERS');

// Translation with placeholders.
$title = $this->lang('WELCOME_USER', ['name' => 'Marino']);

_(string $key, $vars = null): void

Prints a translated string directly.

// Prints the translated string immediately.
$this->_('SAVE');

Loading an object by id from the URL

getObjectRequestedById(string $class, ?int $pos = null): ?ActiveRecord

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);

Pagination helpers

Each view owns a Pagination object ($this->pagination) created in the constructor. The same object is also injected into the model ($this->model->pagination).

getPaginationBar(): string

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; ?>

printPaginationBar(): void

Shortcut that prints the pagination bar:

// Shortcut version when you want to print directly.
$this->printPaginationBar();

mustUsePagination(array $itemsToShow): bool

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();
}

“No data” helper

noData(?string $customMessage = null): void

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.');

Sortable table headers

sortable(string $title, int $ascOrder, int $descOrder): void

Prints a column header as a link that toggles sorting direction using Router::getOrderUrl().

Behavior:

  • if $title is all uppercase, Pair translates it automatically via lang();
  • 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 to 0;
  • 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()).


Minimal complete example

View class

<?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);
    }

}

Layout file: layouts/default.php

<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; ?>

Quick reference of high-use methods

  • assign($name, $val): void
  • display(?string $name = null): void
  • lang(string $key, string|array|null $vars = null, bool $warning = true): string
  • getPaginationBar(): string
  • mustUsePagination(array $itemsToShow): bool
  • sortable(string $title, int $ascOrder, int $descOrder): void
  • setState($name, $value): void, getState($name): mixed
  • noData(?string $customMessage = null): void

Secondary methods worth knowing

These methods are not the main reason you extend View, but they explain how layouts stay safe and predictable:

  • __get(string $name): mixed Resolves assigned variables first, then real properties, and throws on typos.
  • __set(string $name, mixed $value): void Lets you inject runtime flags or helpers without dedicated setters.
  • __call($name, $arguments): void Throws METHOD_NOT_FOUND when a layout calls a missing method.
  • _init(): void Optional pre-render hook; exceptions are swallowed.
  • display(?string $name = null): void Catches render exceptions, shows a modal error, then redirects.

Practical pattern: filters + pagination persistence

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.

Clone this wiki locally