Controllers and Routing
A controller answers one address family, decides whether the request is a page or a mutation, and gathers what a template needs.
Overview
Every controller extends one base class that does what no controller should repeat. It loads the matching model, exposes the view, collects the template data and routes an operation to its method.
A controller file holds two kinds of method: page methods that build a page, operation methods that change data. Anything else belongs in a helper.
Structure
coremio/controllers/{surface}/{name}.php — surface is admin, website or system.
coremio/models/{surface}/{name}.php, reached as $this->model. Never construct it yourself.
templates/{surface}/{name}/, one file per page served.
coremio/operations/; the controller uses them and their methods become its own.
Startup includes the file by path, resolves the class name in three steps and calls one entry method. A name matching none of the three shapes gives the not-found page, not a class-not-found error.
// 1. the file is included by path, never autoloaded
include CONTROLLER_DIR . $surface . DS . $controller . '.php';
// 2. the class name is tried in this order (separators removed, case-insensitive)
// \WISECP\controllers\{surface}\{name} -> {Name}Controller -> Controller
// 3. the name and the route key are published before construction
Controllers::$cname = $controller; // "widgets"
Controllers::$rkey = $route_key; // the route entry that matched
// 4. params are assigned AFTER the constructor, unless the constructor declares one
$this->controller = new $className();
$this->controller->params = $params;
// 5. entry point: index() wins, main() is the fallback
$content = method_exists($this->controller, 'index')
? $this->controller->index()
: $this->controller->main();
Reference
The Two Kinds of Method
The base class declares neither kind; both are dispatcher conventions. A page method is named after the address segment and returns markup, or an empty string so the caller builds the template. An operation method is named after its operation, takes one argument and produces no page.
// page_{segment}[_{subsegment}[_{subsegment}]] - dashes and dots become underscores
public function page_list(&$links, &$meta, &$breadcrumbs): string;
// one operation method per registered operation name
public function save_widget(Operation $operation): bool;
What the Three Page Arguments Carry
By-reference outputs. The caller passes them to set_predefined_data(), which publishes them under the names below. The breadcrumb key loses its plural.
['controller' => LinkGenerator::admin("widgets")]. The base address the page posts back to; templates and the table component read it.
['title' => 'Widgets']. Must be an array: a locale key shaped as content plus variables makes Language::gc() return a string, and the type declaration throws.
[['link' => string|null, 'title' => string], ...]. A null link appears as the current, unlinked step.
What the Base Class Gives You
public Models $model; // constructed for you, by name
public View $view;
public array $params = []; // URL segments after the controller
public array $data = []; // what the template receives
public array $operations = []; // the registration table
public bool $loggedIn = false;
public bool $loginControl = false;
public static string|int $cname = ''; // controller name
public static string|int $rkey = ''; // matched route key
public static array $rparams = []; // the matched route's params, slug INCLUDED
public function addData($k = '', $v = ''): void;
public function getData($key);
public function takeDatas($which = []): void;
public function set_predefined_data(string $type = 'client', array $meta = [], array $breadcrumbs = [], array $links = []): void;
public function operation(string $name = '', array $operations = []): bool;
public function checkLogin(bool $redirect = true): bool;
public function client_gate(bool $requireComplete = true): ?string;
public function page_404($type = 'admin'): string;
public function page_denied(string $scope = ''): string;
public function CRLink($r_key = '', $params = [], $slang = ''): string|bool;
public function AdminCRLink($r_key = '', $params = [], $slang = ''): string|bool;
public function RouteURI($params = [], $key = null, $slang = '', $admin = false): string;
public function ControllerURI(): string;
protected function localize_lang_links(string $routeKey, array $slugByLang): void;
$this->data. The view extracts the array, so templates read it as a variable.
null.
"admin" gives the panel chrome, "client" the client-area context plus filter:client.predefined_data. Anything else publishes only links, meta, breadcrumb and locale. Call it before building the page.
set_predefined_data() is built from, by keyword: admin-sign-all, admin_info, lang_list, account_info, website_logos. Use it for one block outside a full surface.
loginControl, which makes startup skip the entry method when no session exists.
null means proceed; anything else has already redirected or answered, and you return its value.
website/denied/scope-* phrases.
The Registration Table
Registration maps an operation name a request may send to a small array of properties. Two keys are read, both optional.
public function __construct()
{
parent::__construct();
$this->operations = array_merge($this->operations, [
// 'privileges' => string[] privilege keys, ALL of which the session must hold
// 'method' => string the method to call, when it is not the operation name
'save_widget' => ['privileges' => ['WIDGETS_OPERATION']],
'delete' => ['privileges' => ['WIDGETS_OPERATION'], 'method' => 'delete_widget'],
'view_detail' => ['privileges' => ['WIDGETS_LOOK', 'WIDGETS_OPERATION']],
]);
}
// coremio/classes/Controllers.php reads exactly those two keys:
private function run_operation(string $name = '', array $properties = []): bool;
// $method = $properties["method"] ?? $name;
// $privileges = $properties["privileges"] ?? [];
// if ($privileges && !Admin::isPrivilege($privileges)) throw new Exception(...);
Routing
Addresses are not hardcoded; links come from a route key. Each language has its own route table, so one controller answers a different path per language. A route entry is 'key' => ['pattern', 'controller'], where (?) is one segment: 'services' => ['services/(?)', 'services'].
// Router: instance methods, reached through Router::getInstance()
public static function getInstance(array $routes = []): self;
public function get(string $url): ?array; // ['key' => string, 'controller' => string|callable, 'params' => array] or null
public function add(string $name, string $pattern, string|array|callable $handler): void;
public function generate(string $name, array $params = []): ?string;
public function matchController(array $params): string; // controller name, or '' when the slug resolves to nothing
public function findRoute($controller, $params): bool; // memoized slug lookup in the *_lang tables
public function setRoutes(array $routes): self;
public function setLang($lang): self;
public function getRoutes(): array;
// LinkGenerator: static
public static function admin($route = '', $params = [], $lang = '', $wqs = []): string|bool;
public static function client($route = '', $params = [], $lang = '');
public static function wQS(string|bool|null $url, array|string $params = []): string;
public static function convert_to_link(string $arg): string;
filter:routing.prematch, dynamic patterns, constants, filter:routing.match.
router.php; startup includes one per module directory. The handler may be a callable.
admin("services", ["detail", 5], "", ["tab" => "invoices"]).
Example
namespace WISECP\controllers\admin;
use Controllers;
use Filter;
use Language;
use LinkGenerator;
use WISECP\Operations\AdminWidgets;
class widgets extends Controllers
{
use AdminWidgets; // the operation methods live in this trait
public function __construct()
{
parent::__construct();
$this->checkLogin();
$this->operations = array_merge($this->operations, [
'save_widget' => ['privileges' => ['WIDGETS_OPERATION']],
]);
}
public function main()
{
if ($operation = Filter::init("REQUEST/operation", "route")) return $this->operation($operation);
$page = $this->params[0] ?? 'list';
$links = ['controller' => LinkGenerator::admin("widgets")];
$meta = ['title' => Language::gc("admin/widgets/page-list")];
$breadcrumbs = [
['link' => LinkGenerator::admin("dashboard"), 'title' => Language::gc("admin/index/breadcrumb-name")],
['link' => null, 'title' => Language::gc("admin/widgets/breadcrumb-list")],
];
$method = "page_" . str_replace(["-", "."], "_", $page);
if (method_exists($this, $method) && ($out = $this->$method($links, $meta, $breadcrumbs))) return $out;
// Mind the order: links is the LAST argument here, the FIRST one above.
$this->set_predefined_data("admin", $meta, $breadcrumbs, $links);
return $this->view->chose("admin")->render("widgets/" . $page, $this->data, true);
}
public function page_list(&$links, &$meta, &$breadcrumbs): string
{
$this->addData("rows", $this->model->list(false, [], ['id' => 'DESC'], 0, 25));
$this->addData("clink", $links["controller"]);
return ''; // main() renders templates/admin/widgets/list.php
}
}
<?php /* $rows and $clink arrive from addData(); $meta and $breadcrumb from set_predefined_data() */ ?>
<ul>
<?php foreach ($rows as $row): ?>
<li><a href="<?= $clink ?>/detail/<?= (int) $row['id'] ?>"><?= htmlspecialchars($row['title'] ?? '') ?></a></li>
<?php endforeach; ?>
</ul>
Pitfalls
The dispatcher accepts a request when the name is registered or when a method of that name exists. An unregistered method runs with an empty property array — no privilege check. Registration does not make a method callable; it gives it a privilege.
A page method receives (&$links, &$meta, &$breadcrumbs); set_predefined_data() takes ($type, $meta, $breadcrumbs, $links). Passing them straight through puts the links where the meta belongs, and the page shows an empty title instead of failing.
Routes are translated and the panel directory is configurable. Generate links from a route key; a hardcoded path breaks in another language or installation.
Anything that changes data belongs in an operation, where the privilege check, demo guard and JSON error format apply. A write hidden in a page method has none of those.
Related Articles
Thanks for your feedback!
Our support team is here around the clock for anything you can't find above.