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:
- The global controller,
views/controller.php(view nameapp). It runs for every page of a site view. A page of an@aliasview runs that addon's or the core's own instead. - 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.