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
Ui5Appis 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
Ui5AppInterfaceand is registered via a module inconfig/ui5.php. - The app metadata is extracted directly from the frontend build (e.g.
ui5.yaml,manifest.json,index.html,i18n.properties). - A dedicated
ServiceProvideris generated to bootstrap the module and register it with Laravel providers. - A
composer.jsonis created β package name, PSR-4 root, the service provider β so the module is a package the host requires from a path repository.
How to Generate β
You can generate an Ui5App class using:
php artisan ui5:app Offers --create --package-prefix=acme --php-ns-prefix=Acme --vendor="Acme GmbH"This assumes a source folder exists next to the host in one of the following forms:
../ui5-offers/ β LaravelUi5 naming convention, always searched
../com.acme.offers/ β SAP Easy UI5 convention, searched with --js-ns-prefix=com.acmeThe 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-ui5The Easy UI5 generator scaffolds a minimal source project declaring onlysap.ui.coreandsap.mundersap.ui5.dependencies.libs. If your app renders a Dashboard β or any other LaravelUi5 server-emitted control that materialises a Card, Chart, or grid container β addsap.fto that map. Core's Dashboard control walks vocabulary entries likesap.f.GridContainer,sap.f.Card, andsap.f.cards.Headerthat all live in thesap.flibrary. If the dashboard also embeds integration cards, addsap.ui.integrationas well. The same libraries must also be declared inui5.yaml(and the siblingui5-dist.yaml/ui5-coverage.yamlfiles) underframework.libraries;manifest.jsonalone fails at runtime withfailed to load 'library.js'. Both declarations are required:manifest.jsondrives the application-side load contract; theui5*.yamlfiles drive build-time resolution. Treat the patch as the first thing you do after each freshyo easy-ui5scaffold.php artisan ui5:doctorchecks both places and names the library and the file that miss it.
Options β
| Option | Description |
|---|---|
--create | Scaffold new app module (fails if it already exists) |
--refresh | Update an existing app module (fails if it does not exist) |
--no-build | Skip the UI5 build before importing assets (build runs by default) |
--package-prefix= | Required on --create. Composer vendor of the package, e.g. acme |
--php-ns-prefix= | Required on --create. PHP namespace prefix, e.g. Acme or Acme\Erp |
--vendor= | Required on --create. The vendor name of the module, e.g. "Acme GmbH" |
--js-ns-prefix= | Optional. Also look for an Easy UI5 source folder, e.g. com.acme β ../com.acme.offers/ |
β οΈ Pass
--createor--refreshexplicitly. Without either, the command only reports whether the module exists, names the flag to use, and exits with an error.
β οΈ The three identity options have no defaults. They name your vendor, so the generator will not guess them: a
changed in 3.0The generator no longer falls back to our own vendor names.--createwithout them aborts before doing anything, names every missing option and prints a complete command line. They end up in source written once β the Composer package name (acme/offers), every PHP namespace (Acme\Offers\β¦), the app's vendor β which makes them expensive to change later, so the friction sits here, at the one moment where it costs a command line. A package prefix Composer would refuse (Acme) or a PHP prefix that is no namespace aborts too.--refreshneeds none of them.The UI5 namespace is not among them: it comes from the source project's
sap.app/id, which is why--js-ns-prefixonly widens the search for the source folder.
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. The namespace lives on the App leaf (OffersApp.php), the version on the module (OffersModule.php) since 7.0; --refresh rewrites neither, so both 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 module's initial version from the source's sap.app/applicationVersion (a placeholder such as ${version} becomes 1.0.0). After that the version is yours: bump it with every release that changes a served file β a built file is kept by browsers for good under its version β and ui5:doctor reports a change you forgot to bump (core.W7).
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.jsonsurvives 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
--refreshnever rewrites, as described above. - OData
dataSources/modelsare 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:
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 file | Extracted data |
|---|---|
ui5.yaml | JS namespace, OpenUI5 version β getUi5Version() (see configuration) |
package.json | The build script the generator runs |
webapp/manifest.json | UI5 namespace (sap.app.id), sap.ui5 section |
webapp/index.html | Bootstrap data-sap-ui-* attributes, inline JS/CSS |
webapp/i18n/i18n.properties | appTitle, appDescription |
dist/i18n/*.properties | All 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.
| File | Owner | Written | Carries |
|---|---|---|---|
OffersAppBase.php | Framework | on --create and every --refresh | everything derived from the UI5 source build |
OffersApp.php | You | on --create only, never again | identity, 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 asresources/ui5/. To customise a base member, override it in the leaf, where plain inheritance keeps it through every refresh.
OffersAppBase.php β framework-owned β
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:
| Member | Re-synced from |
|---|---|
TITLE / DESCRIPTION | i18n.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 β
class OffersApp extends OffersAppBase
{
public const string NAMESPACE = 'com.acme.offers';
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 here on the leaf, TITLE and DESCRIPTION on the base. The three getters (getNamespace(), getTitle(), getDescription()) come from the HasArtifactIdentity trait used by AbstractUi5App. The constants are intrinsic identity; the methods are the app's runtime contribution. changed in 7.0The version is not the app's: it is the module's VERSION, read through getModule()->getVersion().
| Member | Purpose |
|---|---|
NAMESPACE | the UI5 namespace β the {ns} of every route, and the module's namespace via getArtifactRoot() |
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 CoreArtifactType::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:
- make
OffersAppextendOffersAppBaseinstead ofAbstractUi5App; - delete the now-shadowed source-derived members from the leaf β
TITLE,DESCRIPTION,getUi5BootstrapAttributes(),getAdditionalHeadScript(),getAdditionalInlineCss(); - keep everything else:
NAMESPACE,getResourceNamespaces(),getLaravelUiManifest(),getVendor(), and anything you authored β and moveVERSIONto the module (Core 7).
OffersModule.php β
Implements Ui5ModuleInterface via AbstractUi5Module. The Module is the namespace-keyed container β it names the App as its root in ARTIFACT_ROOT, derives its namespace from the App's NAMESPACE constant (getArtifactRoot()->getNamespace()), and names its sub-artifacts in ARTIFACTS β see Ui5Module. changed in 4.0The module used to expose one getter per kind (getCards(), getActions(), β¦).
The Module is not itself an artifact β it implements neither ArtifactInterface nor declares getType(). It carries the version every artifact it holds runs under:
class OffersModule extends AbstractUi5Module
{
public const string ARTIFACT_ROOT = OffersApp::class;
public const string VERSION = '1.0.0';
public const array ARTIFACTS = [
];
}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):
// 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 buildbefore executingui5:appβ or letui5:appbuild for you (default). - Provide meaningful values for
appTitleandappDescriptionini18n.properties. - Register your app module in
config/ui5.phpright after generation. - 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. - Bump the module's
VERSIONwith every release that changes a served file, and only then; runui5:doctorbefore you tag.