Addons
How addons are discovered, loaded, mounted and wired into the app.
What an addon is
A self-contained folder of PHP that can register services, listen to lifecycle events, and offer fields, views, routers and controllers, without changing framework code. The built-in admin is the largest example; clockwork (the profiler) is the simplest.
Where addons live
| Location | Id | Notes |
|---|---|---|
phlat/addons/{name}/ |
phlat/{name} |
Framework built-ins |
site/addons/{name}/ |
site/{name} |
Site-specific |
addons/{creator}/{name}/ |
{creator}/{name} |
Each its own git repo, gitignored at the root |
Enabling an addon
App::boot() loads every addon that has an addons.{id} config entry with enabled not false. The entry also holds the addon's settings, read with $app->addonConfig('phlat/admin', 'theme') ({addon('phlat/admin', 'theme')} in Latte):
{
"addons": {
"phlat/clockwork": { "mount": "__clockwork" },
"site/newsletter": { "enabled": true }
}
}
An addon's alias is the last segment of its id (phlat/admin is admin). It is the only name used in @alias/view refs and {include '@alias/...'}. Two addons with the same alias are a hard error at load.
index.php
If the addon folder has an index.php, it is included once per boot with $app and the addon's $path injected. Other files are not auto-loaded; require them from here.
<?php
// site/addons/newsletter/index.php
use Phlat\App;
use Phlat\Page;
// Pure setup, no I/O
$app->bind('newsletter', new NewsletterService($app));
// Defer anything session, registry or request-specific to a hook
$app->on('app.route:after', function (Page $page) use ($app) {
if ($app->session->isLoggedIn())
return;
$app('newsletter')->trackVisit($page);
});
Services
$app->bind('name', $instance) registers a service; $app->make('name') or $app('name') retrieves it. Binding a name twice throws.
Events
$app->on($event, $handler) observes the request lifecycle. Hooks are observational only: return values are ignored, and a hook that wants to stop a request redirects or throws. See Event Hooks for the list.
Views and mounting
An addon offers views the same way the site does: views/{name}/ with its own router.php and controller.php.
An addon cannot add pages to the site by itself. One that ships a pages/ tree is served only when the site config names a mount slug for it:
{ "addons": { "phlat/admin": { "mount": "admin" } } }
The slug maps to the addon's pages/index.json, and sub-slugs to pages/{sub}/index.json. Every URL still corresponds to a slug the site author chose, and a real page at that slug in data/pages/ wins over the addon's own, so any addon page can be overridden. The built-ins declare their mounts in phlat/config.json.
No I/O at the top level of index.php
index.php runs during addon loading, inside every request's critical path. Calls that do I/O, like $app->session or $app->registry(), belong in an $app->on(...) callback, so they run after the addons have loaded, with the request's context.
Clockwork's index.php once read $app->session->isLoggedIn() at the top level. That calls session_start(), which takes a file lock, so concurrent requests serialized on it and showed up as a ~20ms spike in the profiler's "Addons" timing. Moving the read into an app.route:after listener fixed it.
// Wrong: runs while addons load, before the page is resolved
$loggedIn = $app->session->isLoggedIn();
// Right: runs once the page is routed, with that request's session
$app->on('app.route:after', function () use ($app) {
$loggedIn = $app->session->isLoggedIn();
});