Scaffold a Self-Contained UI5 App
Introduction
The ui5:assemble Artisan command scaffolds a complete, self-contained OpenUI5 application inside your Laravel project — backend (PHP) and frontend (JS/XML) — that boots onto a working dashboard from a single command. No external UI5 toolchain, no build step: every file is owned by your project (the Breeze pattern).
It is the fastest way to see the whole stack working end to end and to learn how the pieces fit. It is a teaching on-ramp, not an app factory: there is one self-contained app, always named Showcase.
📝 Note The self-contained app is great for a quick start and for learning. For real-world, many-app projects, use
ui5:apptogether with a workspace-based UI5 CLI build (PackageStrategy).Showcaseis the constrained, batteries-included subset.
How to Generate
php artisan ui5:assemble launchpad-applaunchpad-app is the blueprint — the plan for what gets assembled. Omit it to pick from the catalog interactively:
php artisan ui5:assemble
# → Which blueprint? ❯ launchpad-app — A working dashboard: 'Sales Overview' …This named → run it; empty → pick from the list pattern is the catalog grammar, and you will meet it again in --seed and ui5:wire. Learn it once, use it everywhere.
There is no {name} argument and there are no prefix/title/vendor options — the app is always ui5/Showcase/, and its namespace + vendor are fixed by the command (you rename post-generation if you want your own — the files are yours).
What it assembles
ui5:assemble launchpad-app produces a working "Sales Overview" launchpad:
Dashboard: 'Sales Overview'
Group: 'This Month'
Tile: 'New Customers'
Tile: 'Open Deals'
Tile: 'Revenue'
Chart: 'Calls / Week' (sparkline)
Group: 'Pipeline'
Chart: 'Pipeline' (funnel)
Chart: 'Revenue Trend'
Card: 'Top Deals' (list)
Group: 'Performance'
Chart: 'Quota Attainment' (gauge)
Chart: 'Revenue Mix' (donut)
Card: 'Account Snapshot' (object)Under the hood it orchestrates the leaf generators — it really runs ui5:tile, ui5:chart and ui5:card (seeded, so they come populated) — and ships a pre-wired module whose getTiles()/getCharts()/getCards()/getDashboards() and the groups' getChildNamespaces() already reference exactly those artifacts. The leaf generators run silently, since their "now register this" hints would describe work that is already done; the command lists every file it wrote instead. The app boots wired.
Output
ui5/
└── Showcase/
├── composer.json
├── src/
│ ├── ShowcaseApp.php # implements Ui5AppInterface directly (not OData)
│ ├── ShowcaseModule.php # pre-wired: tiles, charts, cards, dashboard
│ ├── ShowcaseManifest.php
│ ├── ShowcaseServiceProvider.php
│ ├── Dashboards/
│ │ └── SalesOverview.php
│ ├── Groups/
│ │ ├── ThisMonthGroup.php
│ │ ├── PipelineGroup.php
│ │ └── PerformanceGroup.php
│ ├── Tiles/ # generated by ui5:tile (seeded)
│ │ ├── NewCustomersTile.php + Provider/
│ │ ├── OpenDealsTile.php + Provider/
│ │ └── RevenueTile.php + Provider/
│ ├── Charts/ # generated by ui5:chart (seeded), five charts
│ │ └── …Chart.php + Provider/
│ └── Cards/ # generated by ui5:card (seeded), two cards
│ └── …Card.php + Provider/
└── resources/
├── ui5/cards/… # the two card manifests
└── app/
├── manifest.json
├── Component.js
├── controller/{App.controller.js, Dashboard.controller.js}
├── view/{App.view.xml, Dashboard.view.xml}
└── i18n/{i18n.properties, i18n_en.properties}ShowcaseApp implements Ui5AppInterface directly — it is intentionally not an OData service. That is the floor the data act builds on.
The generated Component.js loads the facade late
The assembled Component.js does not import LaravelUi5 at the top of the module. It reaches for it inside init():
sap.ui.require(["com/laravelui5/core/LaravelUi5"], function (LaravelUi5) {
LaravelUi5.init(that).then(function () {
that.getRouter().initialize();
});
});That is specific to this shape. A self-contained app is served as one bundle, and its Component.js can be evaluated before the Core library's library-preload.js has registered its exports — a module named in sap.ui.define([...]) would then resolve to undefined. sap.ui.require() defers the lookup to runtime, after the preload is in place.
An ordinary app does not need this. A module imported into a project built with the UI5 tooling — the ui5:app path, and every app in the wild — arms the facade with a plain top-level import, which is what the facade page shows. If you graduate a showcase to ui5:app, the import moves to the top and this workaround goes.
Register the app
ui5:assemble writes ui5/Showcase/composer.json — a real Composer package manifest carrying the Showcase\ autoload root and, under extra.laravel.providers, the generated ShowcaseServiceProvider. The host therefore loads the app as a local path package, and repeats neither.
Let Composer wire it, from the host root:
composer config repositories.showcase path ui5/Showcase
composer require example/showcase:@devTwo commands rather than a hand-edit, for a reason worth knowing. You registered our package registry with composer config repositories.pragmatiqu composer …, which writes repositories as a keyed object. Pasting the array form ("repositories": [ … ]) replaces that object and drops the registry entry with it, so the next resolve cannot find laravelui5/core at all. Composer keeps the form consistent, and prepends the path repository so the local app wins.
One entry per app, naming the directory exactly. The wildcard form ("url": "ui5/*") works too, but then the set of loaded apps is whatever happens to sit in the folder — we name them.
Then declare the module in config/ui5.php:
// config/ui5.php
return [
'modules' => [
\Showcase\ShowcaseModule::class,
],
];This one stays manual: the registry merges declared modules with the infrastructure ones collected from packages, and a business module must be listed here. It is the deliberate statement that the app is in scope — a product decision, not wiring.
Nothing to dump
composer require installs the package for you. Reaching for composer dump-autoload instead does nothing: it only regenerates maps for what is already in composer.lock, and the app is not in there yet. The app then boots into UI5 module class … does not exist, which points at the registry when the fault is the autoloader.
Open the dashboard route — the "Sales Overview" launchpad renders, populated.
Seeding the leaves
The dashboard came populated because ui5:assemble ran the leaf generators with --seed. On their own, ui5:tile/ui5:chart/ui5:card are bare by default — they emit a clean shell, never smuggled sample data. --seed swaps in a populated starting point:
php artisan ui5:tile Showcase/Revenue --seed=revenue
php artisan ui5:chart Showcase/RevenueTrend --seed=revenue-trend
php artisan ui5:card Showcase/Kpi --seed=kpi
php artisan ui5:tile Showcase/Foo --seed # pick interactivelySee ui5:tile, ui5:chart, and ui5:card for the per-artifact seed catalogs.
The data act: ui5:wire
A dashboard shows numbers; real apps browse data. ui5:wire lands a master/detail floorplan (a view + controller) for a model into the Showcase app:
php artisan ui5:wire User # default recipe (master-detail)
php artisan ui5:wire User --recipe # pick the recipe interactively
php artisan ui5:wire # prompt for the model, then the recipeThis writes User.view.xml + User.controller.js under resources/app/{view,controller}/. The command does the files; you do the reveal — it prints the exact steps, because lighting up OData is a deliberate edit, not magic:
- In
ShowcaseApp.php, changeimplements Ui5AppInterface→extends AbstractUi5App. - In
configure(EdmBuilderInterface $builder), add$this->discoverModel(\App\Models\User::class);. - Add the client route to
manifest.jsontargeting theUserview.
Then …/$metadata exposes the entity set and the master/detail populates. (The view binds to /{PluralModel} by convention — confirm the set name against $metadata and adjust if needed.)
Best Practices
- Treat
Showcaseas a sandbox to learn in; graduate toui5:app+PackageStrategyfor production, many-app projects. - Keep artifacts bare by default; reach for
--seedonly to see a shape, then build the real provider. - Composition is explicit by design — when you add your own tile/card/chart, follow each generator's printed registration directives (
getTiles()etc. + the group'sgetChildNamespaces()).