Skip to content
24/48h shipping across Italy
Skip to content
Documentation navigation

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 through TemplateValueInterface. 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. TemplateDataComposer names both owners for duplicate roots; no array spread or array_merge is allowed to decide ownership.
  • FrontViewDataProviderRegistry resolves 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 data variable.
  • BaseViewDataAggregator consumes flash data through SessionStore::pull(); rendering code never accesses $_SESSION directly.
  • Template-slot parameters cannot shadow the renderer-owned slot or slotContext variables.

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:

  1. 0.26.x: all new/changed controllers and front providers use declared payloads; existing raw controller arrays remain supported.
  2. 0.27.x: CI inventory may warn on raw arrays in touched controllers; module authors must migrate front-provider contributions.
  3. 0.28.x: raw front-provider contributions are scheduled for rejection. Existing controller arrays remain behind the adapter.
  4. 0.29.0 retains 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 features page root is now settingsFeatures, 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 oneUptimeLocale root.

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.