Skip to content

Infrastructure Contributions

Introduction

LaravelUi5 modules can publish static, cross-cutting facts into a dedicated laravel.ui5/infra/{key} node that Core injects into every UI5 app's served manifest.json. A platform-level fact — the logout URL, the slot catalog, a build identifier — is declared once, by its owner, and reads identically from every app's manifest.

This is what the Infrastructure Contributions surface is for. Before it existed, platform-level facts got smuggled into individual modules' fragments (a portal module putting routes.logout in its own routes, even though logout is not the portal's route). The infra node retires that anti-pattern.

📝 The static-fact rule (load-bearing). Contributions must be user-invariant — the same value for every actor, every request. They may read global state (the registry, named Laravel routes, environment values that are constant per deployment). They may not return per-user, per-partner, per-tenant data. Per-actor identity lives on other channels (the principal/shell channel or the SDK's own contribution mechanism); the infra node is platform-cacheable by construction.

What an infra contribution looks like (read-side)

Every UI5 app's manifest carries the infra node under laravel.ui5:

jsonc
// GET /ui5/app/{ns}@{ver}/manifest.json  →  laravel.ui5
{
  // … meta, routes, actions, resources, settings, dashboards, shell …
  "infra": {
    "core": {
      "slots": {
        "date_from": { "type": "Date",   "default": "@first-of-month", "editable": "User", "note": "…" },
        "period":    { "type": "String", "default": "month",           "editable": "User", "note": "…" }
        // … the full slot catalog
      }
    },
    "auth": {
      "logout": "https://your-app.test/logout"
    }
  }
}

Client-side read path: getManifestEntry('laravel.ui5').infra.core.slots, …infra.auth.logout. Every app's manifest carries the same infra node — that's the point.

Contributing infra from your module

A module contributes by implementing Ui5InfrastructureContributorInterface (one capability interface, two methods):

php
use LaravelUi5\Core\Ui5\AbstractUi5Module;
use LaravelUi5\Core\Ui5\Capabilities\Ui5InfrastructureContributorInterface;
use LaravelUi5\Core\Ui5\Contracts\Ui5RegistryInterface;

class BuildModule extends AbstractUi5Module
    implements Ui5InfrastructureContributorInterface
{
    public function getInfrastructureKey(): string
    {
        // The sub-key under laravel.ui5/infra this module publishes to.
        // Keep stable — client code reads `infra.build.…`.
        return 'build';
    }

    public function contribute(Ui5RegistryInterface $registry): array
    {
        // Read global state freely — environment, named routes, the registry.
        // Return user-invariant facts only.
        return [
            'commit'  => env('GIT_COMMIT', 'unknown'),
            'tag'     => env('APP_VERSION', 'dev'),
            'built'   => env('BUILD_TIMESTAMP', ''),
        ];
    }
}

When the manifest is served, Core's AbstractManifest::buildInfra() discovers this module via instanceof, calls contribute($registry), and lands the result under infra.build. No per-module manifest configuration; the contribution appears in every module's manifest, identically.

The four design rules (what the surface promises)

  1. Static facts only. The contract is user-invariant — see the static-fact rule above. Returning per-user data poisons every cached manifest in the system.
  2. The interface is the gate. Implementing Ui5InfrastructureContributorInterface is what makes a module contribute. The Ui5Infrastructure marker (the auto-registration gate) is separate and not required to contribute — though contributors are infrastructure by nature.
  3. Keys are public client contracts. getInfrastructureKey() returns a string that client code reads. Pick it stable; don't derive it from your namespace (a namespace rename should not silently break the client). Two contributors with the same key throw a LogicException at manifest-build time.
  4. infra is Core-built and not aggregatable. A subclass Manifest::contributeFragment() that tries to merge into infra is rejected (LogicException("Manifest key [infra] does not allow contributions.")). The only door into the node is the contributor interface.

When not to use the infra node

Any contribution that needs actor/partner/tenant context doesn't belong here. Common examples:

  • The current user's display name → belongs on the principal/shell channel, not infra (per-user data poisons the cache).
  • A tenant-specific endpoint → belongs on the SDK's own contribution mechanism, which is tenant-aware.
  • A partner-scoped permission flag → same: SDK territory.
  • A per-organization configuration → SDK territory.

Core's infra surface is bounded by design to platform-level, user-invariant facts. That bound is what makes it manifest-cacheable.

Examples in the wild

ContributorKeyUse caseShape
CoreModuleinfra.core.slotsThe global slot catalog — type/default/editability/note per slot — read by Report Control clients that need to describe selection inputsData publication: {slots: {<name>: {type, default, editable, note}}}
AuthModule (host-side)infra.auth.logoutThe platform logout URL — read by ShellBar clientsEndpoint publication: {logout: "https://…"}

These are the two reference shapes — data publication (a catalog or descriptor map) and endpoint publication (a URL or handle). Most contributions will fall into one of those two shapes.

How Core wires it (automatic)

  • LaravelUi5ManifestKeys::INFRA = 'infra' — the manifest key.
  • AbstractManifest::buildInfra() — iterates Ui5RegistryInterface::modules(), filters by instanceof Ui5InfrastructureContributorInterface, calls contribute(), lands result under the contributor's key. Duplicate-key guard throws. Empty result drops the key entirely.
  • getFragment($module) — includes the result under laravel.ui5/infra. Same shape for every consuming module.