Skip to content

LUX Weave ​

Two apps, written by two vendors, shipped a year apart. The second one has something to say about a business object the first one owns. Weave is how it says so — without a class import, without a route, without an edit to the first app.

Three attributes carry it:

AttributeGoes onSays
#[Concept]the module that owns the model"this business object exists, and this is its public name"
#[ConceptEntry]an app"I am where you land when you open one of these"
#[Weave]an app"I have a page about these — put me in the menu"

Everything else — the menu in the hub app, the deep link, the authorization — follows from those three declarations.

A concept is a public name for a business object ​

php
use LaravelUi5\Sdk\Intent\Attributes\Concept;

#[Concept(name: 'acme.customer', model: Customer::class, marketplaceName: 'Customer')]
class SalesModule extends AbstractUi5Module { /* … */ }
  • name is the wire identity, vendor-namespaced. Other packages reference this string — never your model class — so they weave to you without depending on your code.
  • model is the Eloquent model behind it. Its primary key is the concept's instance key. This is a local binding, not part of the identity.
  • marketplaceName is a short English name for publishers — the marketplace, ui5:describe, ui5:concept. Every concept is presentable, so it is required. It is literal text, not a bundle key, and it never reaches a user's screen: where a user sees a concept, they see the Weave label or the title of the app concerned.
  • key overrides the instance key. Set it only for a surrogate or composite key.

The attribute goes on the module, because the owner of the model declares the concept — a UI-less domain library can own one just as well as an app can. A concept name may be declared once in an installation; a second declaration is a build-time error naming the first definer.

The SDK's own concepts are laravelui5.partner (the Partners app) and laravelui5.setting (the Settings app).

The front door ​

One app per concept is where you land when someone opens an instance of it:

php
use LaravelUi5\Sdk\Intent\Attributes\ConceptEntry;

#[ConceptEntry(concept: 'acme.customer', route: 'customer/{0}')]
class SalesApp extends AbstractUi5App { /* … */ }

The route is relative to the app root, and {0} is filled with the instance key at dispatch. There is no label and no icon: a front door is a referential jump, not a menu item.

At most one front door per concept, across the whole installation — a second one is a build-time collision. An app may be the front door of several concepts; the attribute is repeatable for that.

Declaring the front door is also what makes this app the concept's hub: the app that carries the related-menu for everyone weaving to it. That is one declaration, not one per contributor.

One placeholder, two segments

{0} is the only placeholder, but a key containing a slash expands into two path segments. The Settings app uses that: its front door is setting/{0}, and a reference of com.acme.orders/pageSize reaches the two-parameter route setting/{artifactNamespace}/{setting} with no handler change.

The doorways ​

An app that has a page about someone else's concept says so on itself:

php
use LaravelUi5\Sdk\Intent\Attributes\Weave;

#[Weave(
    concept: 'acme.customer',
    route:   'orders/customer/{0}',
    icon:    'sap-icon://sales-order',
)]
class OrdersApp extends AbstractUi5App { /* … */ }

route is again relative to the app root with {0} for the crossing key, icon an icon URI.

The menu text lives in the contributing app's bundle, under a key the concept fixes:

properties
# OrdersApp — i18n/i18n.properties
weave.acme.customer.label=Orders

# i18n/i18n_de.properties
weave.acme.customer.label=Aufträge

The key is weave.<concept>.label, and it is read from the app that declares the #[Weave] — the one that owns the page — not from the hub, which has never seen that app's bundle. The registry resolves it in every language the bundle offers when it is built, and the shell picks the request's language: German, then English, then the base bundle. A key missing from the base bundle stops the build with a LogicException that names the key and the app, the same way a missing route does — so the hub never shows a raw key.

The hub declares nothing. It reads every #[Weave] pointed at its concept and renders each as a menu item. Ship a new contributor and the hub gains a doorway with an empty diff.

Crossing, in the app ​

Two intents do the travelling, and both take the concept by name (Intent Dispatch).

weave.open — jump to a concept's own detail. The caller names the concept and the key. It does not know the hub, its route, or whether it is even installed.

js
LaravelUi5.dispatchIntent({
    name: 'weave.open',
    parameters: { concept: 'laravelui5.partner', refKey: partnerId },
});

weave.navigate — follow an outbound arc. This is what the related-menu fires: from the hub's detail page to a contributor's page about the same instance.

weave.openweave.navigate
Directioninbound, to the concept's own detailoutbound, to a related app
Parametersconcept, refKeyconcept, target, refKey
Which app is gatedthe app providing the front doorthe target app
Fails whenthe concept has no front doorthe target is not one of the concept's doorways

Neither intent lets the client name a URL. The client sends a concept, a target namespace and a key; the server re-resolves the route and answers with a redirect. A forged target is denied because it is not in the concept's outbound list.

The menu is one tag, with no attributes and no controller code:

xml
<mvc:View xmlns:lux="com.laravelui5.sdk" …>
    <uxap:ObjectPageLayout>
        <uxap:headerTitle>
            <uxap:ObjectPageDynamicHeaderTitle>
                <uxap:actions>
                    <lux:Weave/>
                </uxap:actions>
            </uxap:ObjectPageDynamicHeaderTitle>
        </uxap:headerTitle>
    </uxap:ObjectPageLayout>
</mvc:View>

It reads the doorways the server delivered, builds one menu item per entry, and takes the crossing key off the current binding context using the column name the server named — it never guesses id. When there are no doorways it hides itself, so a hub page looks finished either way. Outside the shell — on Core alone — the server sends no doorways and the control renders nothing.

changed in 2.0The control moved from Core's library (`com.laravelui5.core.controls.Weave`) to the SDK's (`com.laravelui5.sdk.Weave`). Change the view's namespace and add `com.laravelui5.sdk` to the app's resource namespaces.

What the server sends ​

The shell's context carries a weave key: the outbound doorways of the concept the current app provides (Context API).

json
"weave": [
  { "concept": "acme.customer", "target": "com.acme.orders", "key": "id",
    "label": "Orders", "icon": "sap-icon://sales-order" }
]
FieldIs
conceptthe concept name both sides agreed on
targetthe contributing app's namespace — what weave.navigate sends back
keythe column the crossing key is read from, resolved from the concept's model
label, iconthe menu entry's text, already in the request's language, and its icon

There is no route in it. The list is already filtered by the actor's #[Access] on each target app, and the same rule is applied again when the intent is dispatched — the pre-filtered list is a convenience, not the gate.

Inspecting the graph ​

bash
php artisan ui5:concept            # every concept, with its inbound and outbound counts
php artisan ui5:concept acme.customer

It reads the booted registry, so it shows what the host will actually answer with.

What fails at build time ​

Weave has no runtime "link not found" state. Everything is decided when the registry is built:

DeclarationError
two #[Concept] with the same nameConcept […] is already defined by […]
#[Concept] whose model is not an Eloquent modelrefused
a second #[ConceptEntry] for one conceptbuild-time collision
#[ConceptEntry] or #[Weave] with an empty routerefused — a doorway must go somewhere
either one naming an unknown conceptDeclare a #[Concept] with that name first

Nothing to migrate ​

The graph is read from attributes when the registry is built. There is no table, no seeder and no ui5:sync step for it. In production it is baked into bootstrap/cache/ui5.php by ui5:cache, so run that on deployment like you already do. Remove a package and its doorways are simply gone; there are no orphans to clean up.

Worked example — the reciprocal pair the SDK ships ​

Two of the SDK's own apps weave to each other, and neither imports a line of the other's code.

A partner's overridden settings → the setting. The Partners app shows, on a person, the settings that have been overridden for them. Each row links to the setting itself:

js
LaravelUi5.dispatchIntent({
    name: 'weave.open',
    parameters: { concept: 'laravelui5.setting', refKey: `${artifactNamespace}/${setting}` },
});

The Settings app's front door is setting/{0}, and because the refKey carries a slash it expands into the two-segment deep link — one placeholder, a two-parameter route.

A setting's User value → the partner. The Settings app lists every stored value of a setting, including the per-user ones. The owner of a User row links back:

js
LaravelUi5.dispatchIntent({
    name: 'weave.open',
    parameters: { concept: 'laravelui5.partner', refKey: partnerId },
});

A person → their settings, in the console. The Settings app also contributes to the partner's Related menu, the outbound direction:

php
#[Weave(concept: 'laravelui5.partner', route: 'partner/{0}', icon: 'sap-icon://settings')]
class SettingsApp extends AbstractUi5App { /* … */ }
properties
# ui5-settings — i18n/i18n.properties
weave.laravelui5.partner.label=Settings

The entry lands on the catalogue narrowed to that person's overrides. The Partners app gained it without a changed line: its detail page renders <lux:Weave/>, and the menu is whatever weaves to its concept.

Neither call names an app, a route or a URL. Each names a concept and a key. Uninstall the other app and the link stops resolving instead of 404-ing into a missing route — and a partner who may not open the target app is refused before anything is rendered.

The other two facets ​

The same idea — name a stable anchor, contribute to it, no edit on the receiving side — carries two more contributions:

Not there yet ​

  • One key per crossing. {0} is the only placeholder; composite crossings are out of scope.
  • No package index. php artisan ui5:describe writes an extra.laravelui5 block into your composer.json — the concepts your package defines, its catalogue category, its tags — and --check fails a CI run when the block has drifted. Nothing reads those blocks yet. Publish them if you want to be listed later; expect no effect today.

See also ​