Overrides

How site templates, routers, and controllers take priority over addon and framework defaults, and how to compose instead of copy.

On this page

The chain

Anything named @alias/..., whether a view, a template, a field or a field type, can be replaced by putting your own file at the same path under @alias in your site. For an addon view such as @admin/login:

site/views/@admin/login/index.latte         ← checked first
phlat/addons/admin/views/login/index.latte  ← addon default

router.php and controller.php use the same chain, and so do the core's views: site/views/@phlat/errors/ before phlat/views/errors/. The first file that exists wins. A plain site view has no chain; it is only site/views/{view}/.

An override replaces, it doesn't merge

If site/views/@admin/login/index.latte exists, it fully replaces the addon's template. None of the addon's markup runs unless the override references it. The same holds for router.php (none of the addon's routes register) and controller.php (none of its data is returned).

A naive override therefore starts as a copy of the whole file, which drifts from the addon's original as it evolves. Compose instead of copying.

Templates

A template can't extend itself. {layout '@admin/login/index.latte'} inside site/views/@admin/login/index.latte resolves back to the override, because site/views/ is checked first. Override at a seam instead: when an addon template pulls in pieces with {include}, place a file of the same name under site/views/@admin/ and the rest keeps running. An override stands in for the addon's file, so its own bare includes are the addon's and a copy keeps working. To reach one of your site's templates from an override, use a relative path such as {layout '../../layout.latte'}.

When building an addon, or a site view meant to be overridden, split it into small partials at the points a caller is likely to customize.

Controllers

An override controller can include the addon's original by path and layer data on top. Page::controller() would resolve back to the override, so include the file directly:

<?php
// site/views/@admin/login/controller.php

return function (Phlat\App $app, Phlat\Page $page, array $context): array {
    $original = (include "{$app->system}/addons/admin/views/login/controller.php")($app, $page, $context);

    return [...$original, 'showSupportLink' => true];
};

Routers

Include the addon's router to register its routes first, then add or replace routes on the same $page->router:

<?php
// site/views/@admin/login/router.php

return function (Phlat\App $app, Phlat\Page $page): void {
    (include "{$app->system}/addons/admin/views/login/router.php")($app, $page);

    $page->router->get('sso', fn() => ...);
};

Full overrides

Replace the whole thing when the change is large: a login page redesigned from scratch, or a router with a different route set. Compose when customizing a seam.

Fields and field types

site/fields/@phlat/password/_index.json replaces the core's password field everywhere it is used, inside the core and in every field that extends it. Inside an override, extending the name it overrides means the shipped original, so a tweak doesn't need a full copy:

// site/fields/@phlat/password/_index.json
{
    "extends": "@phlat/password",
    "label": "Passphrase"
}

site/fieldtypes/@phlat/text/ replaces the folder the core's text type reads its _index.json and its edit.latte, preview.latte and render.latte from. It is the whole folder, so copy the templates you want to keep. It can't replace the PHP class: other types extend the core's, so a second FieldTypeText is a fatal error. For different behaviour, write a type of your own that extends the core's class.