-
Notifications
You must be signed in to change notification settings - Fork 2
Model
Pair\Core\Model is the base class for Pair MVC models.
It provides:
- framework/application access (
$app) - DB access (
Databasesingleton) - reusable pagination + ordering helpers
- common item listing/counting for
ActiveRecord - internal error list helpers
final public function __construct()Constructor workflow:
- injects
Application::getInstance()into$app - injects
Database::getInstance()into$db - executes
_init()hook insidetry/catch
_init() is optional and meant for model setup:
protected function _init(): void {}Important: exceptions thrown by _init() are swallowed in current implementation.
For this reason _init() should be used for lightweight setup, not for critical validation that must stop the request.
Typical override:
protected function _init(): void
{
// Initializes default runtime state for later queries.
$this->defaultStatus = 'active';
}__get(string $name): mixed__set(string $name, mixed $value): void__call(string $name, array $arguments): void
Behavior:
- unknown property access throws
ExceptionwithErrorCodes::PROPERTY_NOT_FOUND - unknown method calls throw
ExceptionwithErrorCodes::METHOD_NOT_FOUND -
__set()allows injecting optional runtime dependencies (for examplepagination)
addError(string $message): voidgetLastError(): array|boolgetErrors(): array
Although the signature currently says array|bool, the implementation effectively returns the latest error message string or false.
Usage:
// Registers a human-readable error for the controller/view layer.
$model->addError('Invalid filter');
// Returns all tracked errors in insertion order.
$all = $model->getErrors();
// Returns the latest message or false if no errors were stored.
$last = $model->getLastError();Legacy helper that:
- validates class is an
ActiveRecordsubclass - sets
$this->pagination->count = $class::countAllObjects() - builds SQL with
ORDER BYandLIMIT - returns
Collection
Requires a non-null pagination object (Pair\Html\Pagination) with start and limit.
In modern Pair modules this object is usually injected automatically by View::__construct().
Default implementation returns [].
Override in child models to expose sortable columns.
Builds ORDER BY + LIMIT using:
- router sort value (
Router::getInstance()->order) - provided options or
getOrderOptions() -
pagination->startandpagination->limit
This is the method that keeps your model aligned with sortable table headers rendered by View::sortable().
Example:
protected function getOrderOptions(): array
{
return [
1 => '`createdAt` DESC',
2 => '`total` DESC',
3 => '`customerName` ASC',
];
}Generic item fetch for ActiveRecord classes.
Behavior:
- invalid class returns empty
Collection - query source is
$optionalQueryif provided, otherwisegetQuery($class) - if query is
Query, bindings are extracted and SQL generated -
ORDER/LIMITfromgetOrderLimitSql()are appended only when$optionalQueryisnull
This is the main method you usually call from a view. In the common Pair flow:
- the view creates and injects pagination into the model
-
getItems()loads the current page of rows -
countItems()loads the total for the pagination bar
Example in a view:
protected function render(): void
{
// Loads the current page using the model default query.
$orders = $this->model->getItems(\App\Orm\Order::class);
// Loads the total count for pagination.
$this->pagination->count = $this->model->countItems(\App\Orm\Order::class);
$this->assign('orders', $orders);
}Count helper aligned with query type:
-
Query-> uses optimized$query->count() -
string-> wraps SQL inSELECT COUNT(1) FROM (...) - fallback ->
0
Use it together with getItems() whenever the page can paginate or sort a dataset.
Default list query:
return Query::table($class::TABLE_NAME);Override this in child models to define module-specific default datasets.
This is usually the best place to centralize list filters shared by the whole module.
Example:
protected function getQuery(string $class): Query|string
{
return Query::table($class::TABLE_NAME)
->where('deleted', '=', 0)
->where('tenantId', '=', $this->app->currentUser->tenantId);
}<?php
namespace App\Modules\orders;
use Pair\Core\Model;
use Pair\Orm\Query;
class OrdersModel extends Model {
protected function getOrderOptions(): array
{
// Maps View::sortable() order values to SQL fragments.
return [
1 => '`createdAt` DESC',
2 => '`total` DESC',
3 => '`customerName` ASC',
];
}
protected function getQuery(string $class): Query|string
{
// Defines the default dataset used by getItems()/countItems().
return Query::table($class::TABLE_NAME)
->where('deleted', '=', 0);
}
}Controller/service usage:
// The view usually injects pagination automatically.
$model->pagination = $pagination;
// Loads the current page of rows.
$items = $model->getItems(\App\Orm\Order::class);
// Loads the full count for the pagination bar.
$total = $model->countItems(\App\Orm\Order::class);
$model->pagination->count = $total;// Builds an ad-hoc filtered query.
$query = \Pair\Orm\Query::table('orders')
->where('status', '=', 'paid');
// getItems()/countItems() reuse the bindings from Query automatically.
$rows = $model->getItems(\App\Orm\Order::class, $query);
$total = $model->countItems(\App\Orm\Order::class, $query);If you use a model in a service or CLI script, remember that pagination is not injected automatically.
use Pair\Html\Pagination;
$model = new OrdersModel();
$model->pagination = new Pagination();
$model->pagination->page = 1;
$model->pagination->perPage = 20;
// Loads the first 20 rows using the model default query.
$rows = $model->getItems(\App\Orm\Order::class);These methods are used less often directly, but they explain how Model fits into the Pair MVC stack:
-
__get(string $name): mixedReads internal properties such aspaginationand throws on unknown properties. -
__set(string $name, mixed $value): voidCommonly used byViewto injectpagination. -
__call(string $name, array $arguments): voidThrowsMETHOD_NOT_FOUNDfor undefined methods. -
getActiveRecordObjects(string $class, ?string $orderBy = null, bool $descOrder = false): CollectionLegacy helper that still works well for simple paginated lists. -
getOrderLimitSql(array $orderOptions = []): stringInternal helper for child models that support sortable paginated listings.
-
Modelexpects Pair ORM classes (ActiveRecord,Collection,Query) for best integration. - When using
getItems()with custom$optionalQuery, ordering/limit are not auto-appended. -
getActiveRecordObjects()andgetOrderLimitSql()assumepaginationis configured. - If
_init()throws, the exception is silently swallowed by the constructor.
See also: Controller, ActiveRecord, Query, Pagination, Database.