Skip to content

Ui5App ​

Introduction ​

A Ui5App is a frontend artifact in LaravelUi5 that encapsulates a reusable UI5 app developed outside the Laravel application. It bridges a deployed dist folder with backend metadata, versioning, and registry integration.

Each app is integrated into the Laravel app as part of a Ui5Module, and can later be discovered, versioned, and served through the LaravelUi5 registry.

πŸ“ Note A module can contain either a UI5 app or a UI5 library β€” but never both. The Module is a namespace-keyed container; the App is its single root artifact. See Modules.

Conceptual Overview ​

LaravelUi5 supports importing fully built UI5 apps and wrapping them as native Laravel modules. This integration includes:

  • A Ui5App is a first-class backend representation of a deployed UI5 application.
  • It is located inside a dedicated LaravelUi5 module (e.g. ui5/Offers/) that must not mix in libraries.
  • Each app implements Ui5AppInterface and is registered via a module in config/ui5.php.
  • The app metadata is extracted directly from the frontend build (e.g. ui5.yaml, manifest.json, index.html, i18n.properties).
  • A dedicated ServiceProvider is generated to bootstrap the module and register it with Laravel providers.
  • A minimal composer.json is created, allowing the module to be easily promoted into a standalone Composer package if needed.

How to Generate ​

You can generate an Ui5App class using:

bash
php artisan ui5:app Offers --create --vendor="Acme GmbH"

This assumes a source folder exists in one of the following forms:

plaintext
../ui5-offers/                ← LaravelUi5 naming convention
../com.acme.offers/      ← SAP Easy UI5 convention

The generator runs npm run build for you by default. Pass --no-build to skip it (useful when you've just built and want to re-import only).

⚠️ Patch the lib dependencies after yo easy-ui5 The Easy UI5 generator scaffolds a minimal source project declaring only sap.ui.core and sap.m under sap.ui5.dependencies.libs. If your app renders a Dashboard β€” or any other LaravelUi5 server-emitted control that materialises a Card, Chart, or grid container β€” add sap.f to that map. Core's Dashboard control walks vocabulary entries like sap.f.GridContainer, sap.f.Card, and sap.f.cards.Header that all live in the sap.f library. If the dashboard also embeds integration cards, add sap.ui.integration as well. The same libraries must also be declared in ui5.yaml (and the sibling ui5-dist.yaml / ui5-coverage.yaml files) under framework.libraries; manifest.json alone fails at runtime with failed to load 'library.js'. Both declarations are required: manifest.json drives the application-side load contract; the ui5*.yaml files drive build-time resolution. Treat the patch as the first thing you do after each fresh yo easy-ui5 scaffold.

Options ​

OptionDescription
--createScaffold new app module (fails if it already exists)
--refreshUpdate an existing app module (fails if it does not exist)
--no-buildSkip the UI5 build before importing assets (build runs by default)
--vendor=Required on --create. The vendor name of the module
--php-ns-prefix=Required on --create. PHP namespace prefix, e.g. Acme
--js-ns-prefix=Required on --create. JS namespace prefix, e.g. com.acme
--package-prefix=Required on --create. Composer package prefix, e.g. acme

⚠️ Pass --create or --refresh explicitly. Without either, the command only reports whether the module exists, names the flag to use, and exits with an error.

⚠️ The four identity options have no defaults. They name your vendor, so the generator will not guess them: a --create without them aborts and tells you what to pass. The prefixes end up in your Composer package name, your PHP namespace and β€” through the JS namespace β€” in every manifest.json and every URL the app serves. That makes them expensive to change later, which is why the friction sits here, at the one moment where it costs a command line.

plannedThe generator still falls back to our own vendor names β€” that is what is being removed.

Identity coordinates survive --refresh ​

A UI5 app's PHP namespace and version are identity coordinates β€” the same @{ver} segment that drives the routing URL (/ui5/app/{ns}@{ver}/…) is also pinned in the dev vhost proxy and built-asset paths. Both live on the App leaf (OffersApp.php), which --refresh never rewrites, so they survive by construction rather than by parsing. The generator still reads the PHP namespace back out of the leaf β€” the regenerated base must carry the same namespace declaration, and it is the source-registration key β€” and aborts if it can't be parsed rather than clobber. Source-derived fields (title, description, bootstrap attributes, head script / CSS) refresh into the base after the rebuild.

ui5:app --create writes the SAP-convention 1.0.0 as the initial version. A version bump after that is a deliberate, manual act (and a breaking, cache-busting one β€” bump only when you mean it).

resources/ui5/ is generated β€” edit the source project ​

Everything under the module's resources/ui5/ is output. --refresh rebuilds the UI5 project and re-imports dist/ wholesale, manifest.json included. That is the point: a new route, a new dependency, a changed sap.ui5 section lands in the host module because it was declared in the app, and no one has to remember a second place.

The cost of that guarantee is that the copy is not a place to write:

A hand-edit in ui5/<Name>/resources/ui5/manifest.json survives exactly until the next --refresh.

It will not error, and it will not warn. The file is simply overwritten with what the source produced, and the edit is gone. The same holds for i18n/*.properties, Component-preload.js, and every other imported asset.

So make the change where it is authored, in the UI5 project:

ui5-offers/webapp/manifest.json     ← here
ui5/Offers/resources/ui5/manifest.json   ← not here (generated)

then ui5:app <Name> --refresh to deliver it. It runs npm run build at first so your dist is always uptodate. If a value seems to belong only to the host and not to the app, it is a sign it belongs in the Laravel-side manifest fragment (<Name>Manifest.php) instead β€” that file is src/, it is yours, and --refresh does not touch it.

Two things that are not exceptions to this rule, because they are not hand-edits:

  • Identity coordinates (PHP namespace, version) live on the App leaf, which --refresh never rewrites, as described above.
  • OData dataSources / models are injected into the served manifest at runtime by Core. You never write them into either copy.

Output ​

Upon successful execution, the following artifacts are generated inside the Laravel project:

plaintext
ui5/Offers/
β”œβ”€β”€ composer.json               ← Composer package file
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ OffersApp.php           ← App leaf β€” yours, written once, never regenerated
β”‚   β”œβ”€β”€ OffersAppBase.php       ← Framework-owned base β€” rewritten on every `--refresh`
β”‚   β”œβ”€β”€ OffersManifest.php      ← Laravel-side manifest fragment
β”‚   β”œβ”€β”€ OffersModule.php        ← Module wrapper (the container)
β”‚   └── OffersServiceProvider.php  ← ServiceProvider class
└── resources/ui5/              ← UI5 assets (preloads, i18n, etc.)

The build assets are copied from the dist/ folder.

Metadata Resolution ​

LaravelUi5 reads app metadata from the following locations in the source folder:

Source fileExtracted data
ui5.yamlJS namespace, UI5 framework version
package.jsonThe build script the generator runs
webapp/manifest.jsonUI5 namespace (sap.app.id), sap.ui5 section
webapp/index.htmlBootstrap data-sap-ui-* attributes, inline JS/CSS
webapp/i18n/i18n.propertiesappTitle, appDescription
dist/i18n/*.propertiesAll files copied into Laravel resources/ui5/i18n/

Metadata is read from the authoring webapp/; the assets that land in resources/ui5/ (manifest.json, Component-preload.js, the i18n bundles) are copied from dist/ β€” which is why ui5:app runs npm run build first unless you pass --no-build.

Class Structure ​

The App is a base/leaf pair ​

ui5:app does not scaffold one App class but two, split along a single line: who owns the file.

FileOwnerWrittenCarries
OffersAppBase.phpFrameworkon --create and every --refresheverything derived from the UI5 source build
OffersApp.phpYouon --create only, never againidentity, wiring, and everything you author

The split exists because a --refresh has to re-sync source-derived state after a UI5 rebuild, and the old single mixed-ownership App class made that write reach hand-authored code as well. Now the refreshing write cannot reach your class at all: it targets a different file.

⚠️ Never hand-edit OffersAppBase.php. It is regenerated from the UI5 source on every --refresh, silently and without warning β€” the same contract as resources/ui5/. To customise a base member, override it in the leaf, where plain inheritance keeps it through every refresh.

OffersAppBase.php β€” framework-owned ​

php
abstract class OffersAppBase extends AbstractUi5App
{
    public const string TITLE       = 'Offers';
    public const string DESCRIPTION = 'Customer offer catalog and authoring tools.';

    public function getUi5BootstrapAttributes(): array { /* from index.html */ }
    public function getAdditionalHeadScript(): ?string { /* inline <script> */ }
    public function getAdditionalInlineCss(): ?string  { /* inline <style> */ }
}

It extends AbstractUi5App (which extends LaravelUi5\OData\ODataService) and implements Ui5AppInterface. Every UI5 app is therefore also a backend OData service: the same artifact serves both /ui5/app/{ns}@{ver}/… and /odata/{ns}@{ver}/….

The members it declares are exactly the ones that change when the UI5 project changes:

MemberRe-synced from
TITLE / DESCRIPTIONi18n.properties (appTitle, appDescription)
getUi5BootstrapAttributes()the sap-ui-bootstrap script tag in index.html
getAdditionalHeadScript()inline JS in index.html
getAdditionalInlineCss()inline CSS in index.html

That is the whole base. Add a sap.* library, change the bootstrap, retitle the app β€” one --refresh and the host module agrees again, with nothing of yours at risk.

OffersApp.php β€” the leaf, yours ​

php
class OffersApp extends OffersAppBase
{
    public const string NAMESPACE = 'com.acme.offers';
    public const string VERSION   = '1.0.0';

    public function getResourceNamespaces(): array
    {
        return ['com.laravelui5.core'];
    }

    public function getLaravelUiManifest(): string
    {
        return OffersManifest::class;
    }

    public function getVendor(): string
    {
        return 'Acme GmbH';
    }
}

Identity is declared as class constants β€” NAMESPACE and VERSION here on the leaf, TITLE and DESCRIPTION on the base. All four getters (getNamespace(), getVersion(), getTitle(), getDescription()) come from the HasArtifactIdentity trait used by AbstractUi5App. The constants are intrinsic identity; the methods are the app's runtime contribution.

MemberPurpose
NAMESPACEthe UI5 namespace β€” the {ns} of every route, and the module's namespace via getArtifactRoot()
VERSIONthe @{ver} cache-busting coordinate; bump deliberately
getResourceNamespaces()non-sap.* libraries the app needs (e.g. com.laravelui5.core), wired server-side because the LaravelUi5 manifest is server-generated
getLaravelUiManifest()FQCN of the Laravel-shaped manifest fragment (actions, settings, …)
getVendor()vendor string, surfaced via VendorTaggedInterface

This is also where everything you author later belongs, and where it is safe: an #[Access] attribute gating the app, a configure(EdmBuilderInterface $builder) body registering entity sets, or an override of any base member.

Methods provided further up β€” getModule(), getSource(), getManifestPath(), getType() (returns ArtifactType::Application), and the identity getters β€” are not re-declared in either generated class. Override them only when you have a specific reason.

Migrating a pre-split app ​

An app scaffolded before the split is a monolith leaf with no base. Running --refresh on it is non-destructive: the base is created from the current source, the existing leaf is left byte-for-byte untouched, and the command prints a short manual of what to do. Until you act, the leaf's own definitions harmlessly shadow the base and nothing breaks. To finish:

  1. make OffersApp extend OffersAppBase instead of AbstractUi5App;
  2. delete the now-shadowed source-derived members from the leaf β€” TITLE, DESCRIPTION, getUi5BootstrapAttributes(), getAdditionalHeadScript(), getAdditionalInlineCss();
  3. keep everything else: NAMESPACE, VERSION, getResourceNamespaces(), getLaravelUiManifest(), getVendor(), and anything you authored.

OffersModule.php ​

Implements Ui5ModuleInterface via AbstractUi5Module. The Module is the namespace-keyed container β€” it owns the App as its root artifact, derives its namespace from the App's NAMESPACE constant (getArtifactRoot()->getNamespace()), and exposes the eight getCards() / getTiles() / getCharts() / getActions() / getResources() / getDashboards() / getReports() / getDialogs() getters for sub-artifacts.

The Module is not itself an artifact β€” it implements neither Ui5ArtifactInterface nor declares getType().

OffersServiceProvider.php ​

Standard Laravel ServiceProvider for registering classes and services scoped to the module (the per-app …Manifest binding, action handlers, providers, …).

OffersManifest.php ​

Extends AbstractManifest; returns the per-module Laravel-side manifest fragment (actions, settings, resources, reports, …). Returned by OffersApp::getLaravelUiManifest() and injected into the served manifest.json under laravel.ui5.

Module Integration ​

Each app is part of a domain-specific module. Modules are registered as a class-only array in your application's config/ui5.php β€” the registry derives every namespace from the module's root artifact (i.e. from OffersApp::NAMESPACE):

php
// config/ui5.php
return [
    'modules' => [
        \Acme\Offers\OffersModule::class,
    ],
];

No keyed slugs; the namespace is single-sourced from the App class.

Best Practices ​

  • Use CamelCase names for apps (e.g. Offers, BudgetPlanner).
  • Always run npm run build before executing ui5:app β€” or let ui5:app build for you (default).
  • Provide meaningful values for appTitle and appDescription in i18n.properties.
  • Register your app module in config/ui5.php right after generation.
  • Always specify --vendor.
  • Avoid mixing libraries and apps in the same module.
  • Never hand-edit <Name>AppBase.php β€” write in the leaf <Name>App.php, and override a base member there if you need to.
  • Treat the @{ver} coordinate as identity β€” bump the VERSION constant only when you mean to break cache and pin a new release.