Sync Pipeline
The sync pipeline projects Core's reflection-based metadata into the SDK's sdk_* database tables. It is the write half of the platform invariant: the registry projects metadata at sync time; the database serves authorization at request time.ui5:sync is the act that moves data from the first to the second.
ui5:syncis mandatory. Runtime authorization reads the database, not the registry. An un-synced install has no grants. Runui5:syncon every deploy.
What gets synchronized
Seven workers run in FK-dependency order:
| Order | Worker | Source | Target table |
|---|---|---|---|
| 1 | ArtifactsWorker | Ui5Registry artifacts | sdk_artifacts |
| 2 | SlotsWorker | #[Slot] declarations | sdk_slots |
| 3 | SettingsWorker | #[Setting] declarations | sdk_settings |
| 4 | AbilitiesWorker | #[Access], #[Act], See and #[Read] abilities | sdk_abilities |
| 5 | RolesWorker | #[Role] attributes | sdk_roles |
| 6 | CustomizingWorker | #[CustomizingEntry] catalogs | each declared catalog table |
| 7 | ScopeConformanceWorker | #[ContributesScope] contributions | none (validation only) |
Artifacts come first because later workers reference them; roles depend on abilities. CustomizingWorker reconciles each code-owned catalog table to its declared set. ScopeConformanceWorker persists nothing: it checks that every contributed value-help scope projects the value help's declared entity type, and aborts the sync on a mismatch. SyncService wires the workers; SyncPipeline runs them.
Invocation
php artisan ui5:syncPreview without writing:
php artisan ui5:sync --dryA deploy runs the steps in this order:
php artisan migrate --force
php artisan ui5:intake # creates the platform-owner organisation; idempotent
php artisan ui5:sync
php artisan ui5:cache # production only, see belowThe command contract
ui5:sync is the one public surface of this domain, and it is Stable (soft-freeze): changes are additive and announced. Its contract:
- Atomic. All workers run inside one database transaction — a failure applies nothing (commit-all or roll-back-all). Partial application is impossible.
- Idempotent + ordered. Each worker reconciles its slice insert → update → delete, keyed on its natural identity. Re-running converges; it never duplicates.
--drypreviews the plan and applies nothing. Dry-run does not branch every write — workers write through the active transaction (so a later worker's FK lookups resolve against rows an earlier worker just staged), and the pipeline rolls the whole transaction back at the end.logDry()is informational only.- Fails loud. On a worker failure the command reports a concise error (which worker, what message — no stack trace) and exits non-zero. A broken sync never passes silently.
- Builds from the live
SdkRegistry. Ifconfig('ui5.registry')is the cached registry, the command reaches past the binding to the live one.
Prerequisite — run ui5:intake first
SettingsWorker stamps each setting's set_by with the installation's system actor, the platform-owner organisation. ui5:intake creates that organisation, so it must run before the first ui5:sync. It is idempotent and keyed on the owner's email. A re-run updates the owner in place, and a database holds exactly one platform owner.
After sync — ui5:cache
In production, ui5:cache bakes the sync-assigned sdk_abilities ids into bootstrap/cache/ui5.php, so it must run after ui5:sync. Run before sync, it fails loud. Like ui5:sync, it builds from the live SdkRegistry whatever is bound (it owns exportToCache()).
Prerequisite — register the morph map
RolesWorker dissolves a scoped role declaration into a stable morph key at sync time. When a role is declared with a model context —
#[Role(role: 'key_account', note: 'Key account manager for one customer', scope: Customer::class)]— the worker resolves Customer::class through Laravel's Relation::morphMap() to the registered alias (e.g. customer) and stores that key, not the FQCN.
This means the host must register its morph map before running ui5:sync. If a scoped model is not in the morph map, the worker falls back to storing the raw FQCN — which works, but silently couples your stored role scopes to a class path that breaks if the class ever moves. Register the morph map in a service provider:
// AppServiceProvider::boot()
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'customer' => \App\Models\Customer::class,
// …every model used as a #[Role] / #[Scoped*] context
]);See Scoped Entity Sets and Role Scope for the read side of the same morph keys.
The pipeline is internal
The Sync API beneath the command — SyncService, SyncPipeline, the seven workers, and the context objects — is internal. There is no third-party worker-registration seam: the SDK owns its sdk_* tables, and you do not inject workers into the pipeline. A host that genuinely needs different sync behavior rebinds SyncServiceInterface wholesale in its service provider. The mechanics above (single transaction, dry-run write-through-rollback, idempotent ordered workers) are stable as behavior; the classes implementing them may be refactored freely as long as the command contract holds.
Related
- The Core Seam — why
ui5:syncis mandatory