Template Data Contract
This is the authoritative migration and ownership guide for data crossing the
PHP controller/renderer boundary. It applies to PHP Plates HTML rendering and
to ViewModel JSON negotiation from k0smos 0.26.0 onward.
Invariants
- A declared payload is immutable and implements
TemplatePayloadInterface; its owner is stable and appears in collision diagnostics. - Values normalize to
null, scalar values, backed-enum values, or arrays. Presentation value objects opt in throughTemplateValueInterface. Domain entities, repositories, services, containers, DB connections, and arbitrary objects are rejected before Plates or the serializer receives them. - Core shared roots are reserved by
ViewDataSchema. Page and module payloads may not claim them.TemplateDataComposernames both owners for duplicate roots; no array spread orarray_mergeis allowed to decide ownership. FrontViewDataProviderRegistryresolves providers from active runtime modules only. It applies the same reserved-root and duplicate-owner checks before the public SPA receives module data. An inactive module has no provider instance and therefore no payload.- HTML and JSON receive the same normalized shape for a declared payload. JSON
is not wrapped in the legacy HTML-only
datavariable. BaseViewDataAggregatorconsumes flash data throughSessionStore::pull(); rendering code never accesses$_SESSIONdirectly.- Template-slot parameters cannot shadow the renderer-owned
slotorslotContextvariables.
Framework Root Inventory
The executable inventory is ViewDataSchema::definitions(). Every entry
declares its PHP/presentation type, owner, sensitivity (public, internal, or
personal), mutability, and Default/Sober consumers. Tests fail if catalog keys
collide or an entry omits either bundled admin theme.
| Owner | Root variables | Sensitivity / mutability |
|---|---|---|
core.theme |
ver, theme, themeName, themeSide, assetBase, assets |
public, immutable |
core.base |
app, env, request, user, flash |
public/internal/personal; immutable except consumed-on-read flash |
core.render-profile |
features, ui |
internal/public, immutable |
core.public-settings |
social, company, klaro, frontContent, favicons |
public, immutable |
core.feature-flags |
featureFlags |
internal, immutable |
core.module-runtime |
activeModules |
internal, immutable |
core.theme-preference |
themePalette, themeMode |
personal, immutable |
core.navigation |
menu, menuChannel, footerMenu, navigation |
public/internal, immutable |
core.breadcrumbs |
breadcrumbs |
public, immutable |
core.locale |
locale_switcher, locale |
public, immutable |
core.tenant / core.spa |
tenant, initial |
internal, immutable |
core.seo |
seo |
public, immutable |
The favicons value contains ico, png, apple_touch_icon, regular PWA 192/512, and maskable PWA 192/512 URLs. A Media-managed family is all-or-nothing: if its module is inactive or the projected set is incomplete, all values fall back to the shared project icons. URL-only legacy tenants continue to resolve their configured values, with regular PWA icons also serving as their maskable fallback.
Request, user, tenant, locale, navigation, breadcrumb, flash, asset, and SEO
value objects live under src/Rendering/TemplateData. PageTemplateData is the
page-specific envelope when a more specific presentation DTO is not warranted.
Compatibility Boundary And Deprecation Window
LegacyTemplateDataAdapter is the only compatibility boundary. In 0.26.x, a
raw array passed to ViewModel remains a set of direct Plates variables and is
not recursively normalized; this preserves existing templates that have not
yet migrated. A declared payload is strict immediately. A top-level object is
not silently wrapped for HTML and fails with an actionable exception.
The migration window is:
0.26.x: all new/changed controllers and front providers use declared payloads; existing raw controller arrays remain supported.0.27.x: CI inventory may warn on raw arrays in touched controllers; module authors must migrate front-provider contributions.0.28.x: raw front-provider contributions are scheduled for rejection. Existing controller arrays remain behind the adapter.0.29.0retains the compatibility adapter and arbitrary controller arrays. Any later removal requires a separate release note and migration verification.
Module Migration
For a page, replace the raw array without changing the direct variables used by the template:
return (new ViewModel(new PageTemplateData(
'module.example.admin-overview',
['exampleStatus' => $status, 'exampleWritable' => $canWrite],
)))->setTemplate('example/overview', 'admin');
Choose module-specific roots (exampleStatus, blogFeed) rather than generic
core names (user, features, locale). If construction rejects an object,
map it at an application/presentation boundary; never loosen the normalizer.
For front integration, implement FrontViewDataProviderInterface and return a
TemplatePayload whose owner is the provider class. Register the provider only
through the module's FrontViewDataProviderModuleInterface contribution. Do
not place module providers in core DI definitions or instantiate inactive
modules to obtain view data.
Delivered Pilots
- Core admin settings overview: declared page payload; its former ambiguous
featurespage root is nowsettingsFeatures, avoiding the core render- profile root. - Blog homepage feed: declared active-module contribution under
blogFeed. - OneUptime admin overview/settings: declared page payload with scalar/array
values and the module-specific
oneUptimeLocaleroot.
Contract, collision, unsupported-object, legacy compatibility, session, JSON,
active/no-module, and Default/Sober rendering coverage lives in
tests/Unit/Rendering plus the focused Blog and OneUptime tests.