Controllers

Inject data into a view at render time.

On this page

A controller is a PHP file that runs just before a view renders. It receives $app, $page and the context merged so far, and returns an array that is merged into the template's variables. Use one for data every page of a view needs: related pages, navigation, computed values.

Controllers are view-scoped and run only while a page renders. Route handlers and guards fetch their own data.

Location

Colocated with the view's template and router. Optional.

site/views/{view}/controller.php
phlat/views/{view}/controller.php   ← a core view, @phlat/{view}

Structure

Return a callable that returns an array:

<?php

use Phlat\App;
use Phlat\Page;

return function (App $app, Page $page, array $context): array {
    $posts = $app->page('blog')
        ->children('sort=-created');

    return compact('posts');
};

The keys become template variables:

{foreach $posts as $post}
    <a href="{$post->url()}">{$post->title}</a>
{/foreach}

Layers

Page::render() merges two layers, in order:

  1. The global controller, views/controller.php (view name app). It runs for every page of a site view. A page of an @alias view runs that addon's or the core's own instead.
  2. The page's own view controller.

The view controller receives the global result as $context, and its own keys win on collision. Nothing else merges automatically. To use another view's data, call $page->controller('otherView', $context).

Phlat ships no global controller. A site defines whatever its layouts need.

Keeping it thin

A controller that grows past a few lines is usually holding logic that belongs to the page. Move it onto the view's Page class, described below, and let the controller call it:

return fn(App $app, Page $page): array => [
    'minutes' => $page->readingMinutes(),
];

A Page class for the view

Logic that belongs to the page itself, such as navigation or a derived value, can live on a Page subclass bound to the view. Put {view}.php in the view's folder, declaring Phlat\{View}Page:

<?php
// site/views/article/article.php

declare(strict_types=1);

namespace Phlat;

class ArticlePage extends Page
{
    public function readingMinutes(): int
    {
        return max(1, intdiv(str_word_count((string) $this->body), 200));
    }
}

Every page of that view is now an ArticlePage, however it is reached: $app->page(), find(), children() or routing. The namespace is always Phlat. A nested view name is joined, so catalogue/record is CatalogueRecordPage in views/catalogue/record/record.php. The file is looked up where the view is: your site's for a bare view, the core's for @phlat/....

Method names share Page's, so don't reuse root(), next(), prev() and the rest. A view that an addon ships (@alias/...) is always an AddonPage and can't have one. The constructor stays Page's.