Controllers and Routing

6 vues Markdown

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

Controller coremio/controllers/{surface}/{name}.php — surface is admin, website or system.
Model coremio/models/{surface}/{name}.php, reached as $this->model. Never construct it yourself.
Templates templates/{surface}/{name}/, one file per page served.
Operations Traits under 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.

coremio/init.php
// 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.

the two signatures
// 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.

$links → $links ['controller' => LinkGenerator::admin("widgets")]. The base address the page posts back to; templates and the table component read it.
$meta → $meta ['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.
$breadcrumbs → $breadcrumb [['link' => string|null, 'title' => string], ...]. A null link appears as the current, unlinked step.

What the Base Class Gives You

coremio/classes/Controllers.php
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;
addData() Adds one named value to $this->data. The view extracts the array, so templates read it as a variable.
getData() Reads back a collected value, or null.
set_predefined_data() Three surfaces. "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.
takeDatas() The blocks 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.
operation() The dispatcher. Call it from your entry method with the requested name; it answers JSON and returns false.
checkLogin() Admin session gate, called in the constructor. It sets loginControl, which makes startup skip the entry method when no session exists.
client_gate() Client-area gate. null means proceed; anything else has already redirected or answered, and you return its value.
page_404() Not-found page, for an address that resolves to no record. Pass the surface, not a message.
page_denied() Refusal page for a client acting on an account that never granted this area. The scope maps to 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.

registration shape and its reader
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'].

coremio/classes/Router.php · LinkGenerator.php
// 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;
Router::get() Resolves an address. Exact patterns first, then filter:routing.prematch, dynamic patterns, constants, filter:routing.match.
Router::findRoute() Not an address resolver: it asks the database whether a slug exists in the current language, for entity routes only. Hits and misses are memoized per language.
Router::matchController() Wraps the lookup above; empty string when the slug does not exist.
Router::add() Registers a route from a module's router.php; startup includes one per module directory. The handler may be a callable.
LinkGenerator::admin() Panel address from a route key. Language is third, query string fourth: admin("services", ["detail", 5], "", ["tab" => "invoices"]).
LinkGenerator::client() The same for a website or client-area address. No query-string argument; add one with the helper below.
LinkGenerator::wQS() Appends a query string, choosing the separator itself. Never concatenate one by hand.

Example

coremio/controllers/admin/widgets.php
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
    }
}
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

An unregistered method is still reachable, and unprotected

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.

The argument order flips between the two calls

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.

Never write an address as a literal

Routes are translated and the panel directory is configurable. Generate links from a route key; a hardcoded path breaks in another language or installation.

A page method must not write

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.

Cet article vous a-t-il été utile ?

Merci pour votre retour !

Besoin d'aide supplémentaire ?

Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.