Skip to content

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:sync is mandatory. Runtime authorization reads the database, not the registry. An un-synced install has no grants. Run ui5:sync on every deploy.

What gets synchronized ​

Seven workers run in FK-dependency order:

OrderWorkerSourceTarget table
1ArtifactsWorkerUi5Registry artifactssdk_artifacts
2SlotsWorker#[Slot] declarationssdk_slots
3SettingsWorker#[Setting] declarationssdk_settings
4AbilitiesWorker#[Access], #[Act], See and #[Read] abilitiessdk_abilities
5RolesWorker#[Role] attributessdk_roles
6CustomizingWorker#[CustomizingEntry] catalogseach declared catalog table
7ScopeConformanceWorker#[ContributesScope] contributionsnone (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 ​

bash
php artisan ui5:sync

Preview without writing:

bash
php artisan ui5:sync --dry

A deploy runs the steps in this order:

bash
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 below

The 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.
  • --dry previews 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. If config('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 —

php
#[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:

php
// 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.