Skip to content

Service Provider Reference

SdkServiceProvider is the whole of the SDK's bootstrapping — one file, auto-discovered, no publishing step. It is worth reading once as a map: almost everything it does is a binding you can replace, and the few things it does unconditionally are the ones that make the SDK the SDK.

The rule that governs the file: every service is bound to an interface, and every default is overridable by rebinding. If you find yourself editing SDK code to change behaviour, you have almost certainly missed a binding.

What register() does

It merges its config into Core's namespace

php
$this->mergeConfigFrom(__DIR__ . '/../config.php', 'ui5');

The SDK contributes navigation_service, system_actor_id, export, context, discovery, shell, intents and help on top of the keys Core owns. The file is deliberately not publishable: you override individual keys in your own config/ui5.php, which wins on merge, rather than forking a file that then drifts from the package. A composer update ships improved defaults without a diff dance. See Configuration Reference.

It registers twelve commands

Only when running in console. Artisan commands lists them. One is a deliberate override: the SDK's ui5:slot replaces Core's, because the SDK boots after Core and Symfony keys commands by name — last registration wins.

It binds the services

Grouped by what they do rather than by the order they appear:

InterfaceDefaultNotes
AbilityResolverInterfaceAbilityResolverThe authorization engine
SettingsReaderInterface · SettingsWriterInterfaceSettingsReader · SettingsWriterMarked @internal — drive settings through the context
ActorParameterReaderInterface · …WriterInterfacethe defaultsThe reader is request-scoped so its memo spans a dashboard's several resolves
SyncServiceInterfaceSyncServiceThe seven-worker pipeline
NavigationServiceInterfaceconfig('ui5.navigation_service')Swap to CachedNavigationService in production
ContextServiceInterfaceContextServiceAssembles context.json
PartnerResolverInterfaceDefaultPartnerResolverConnect your user model
SystemActorResolverInterfaceConfigSystemActorResolverReads ui5.system_actor_id; the system actor
TenantResolverInterfaceDefaultTenantResolverTenancy
ImpersonationServiceInterfaceImpersonationServiceOver sdk_delegations
IntentDispatcherInterfaceIntentDispatcherIntent dispatch
LoginProviderInterfaceEloquentLoginProvider — via bindIfLogin
AddressProviderInterfacea closure that throwsAddresses
AddressFormatterRegistrywith DefaultAddressFormatterRegister country formatters on it
MarkdownEnvironmentFactoryInterfaceMarkdownEnvironmentFactoryRebind to change the CommonMark extensions
ReadAuthorizerInterfaceODataReadAuthorizerRebinds the OData engine's default; the read gate

Three of those defaults say something about the SDK's stance, and they are the three worth knowing:

  • bindIf on the login provider — the SDK yields to a host binding, always.
  • A throwing closure for the address provider — there is no honest default, so it fails loud. You can use the same idiom yourself: bind any port to a closure that throws, and the surfaces that depend on it fail with a domain message instead of doing the wrong thing quietly.
  • bind, not singleton, for the read authorizer — it reads the current actor per request.

It rebinds two Core classes

php
ActionDispatchController::class SdkActionDispatchController::class
ArtifactBehaviorScaffolder::class SdkArtifactBehaviorScaffolder::class

No route surgery is needed for the first: Laravel resolves route controllers through the container, so swapping the binding swaps the dispatcher. This is how the SDK adds the typed action path — business transaction, business context, the seal — while a legacy Core handler still flows through Core's path untouched. The second is why ui5:action generates a typed handler while every other generator is unchanged.

It aliases the registry three ways

IntentRegistryInterface, AbilityIndexInterface and ScopeRegistryInterface all resolve to the active registry, because SdkRegistry and CachedRegistry both implement them. One object, three contracts, so a consumer depends on the narrow interface it needs rather than on the registry.

It self-registers seven intents

The SDK appends its own intent triples to ui5.intents, so you get them without wiring: weave.navigate and weave.open, partner.impersonate, navigation, artifact-open, value-help-open, and session logout. Your own intents go in ui5.intents in your config — the merge keeps both. It appends its Settings and Weave context contributors to ui5.context the same way.

This is the standard Laravel idiom for "defaults the host may extend, plus wiring I must always contribute."

It tags one vetoer

LaunchTileVetoer is tagged into Core's dashboard veto chain, so a Launchpad tile whose app the actor cannot open is not shown. Any veto hides, so it composes with a vetoer of your own without either knowing the other. Core stays stateless — it runs the chain and knows nothing about abilities.

What boot() does

Registers three modules as infrastructure — Launchpad, Partners, Settings. These are the shipped apps; they arrive without being listed in your ui5.modules.

Loads the migrations from the package. php artisan migrate applies them; there is nothing to publish. See the database schema.

Extends the parameter pipeline to insert the actor source:

Request → Actor → Composition → Setting

Core's pipeline stops at three sources and leaves the actor seat empty, because Core knows no actors. The SDK fills it with before(Composition). See actor slot values.

Feeds Core's dashboard group collector from the #[Contribute] attributes — but only the first time a dashboard transformer is resolved, so a request that renders no dashboard pays no reflection walk.

Adds the ui5 view namespace and the @includeIfSdk Blade directive. The directive is how a host's own bootstrap template re-adds the SDK's shell fragment: it renders the included view only when the current artifact's manifest actually implements the shell-fragment contract, and silently does nothing when no UI5 context is bound. See the bootstrap mechanism.

Registers the routes — the artifact-scoped ones on config('ui5.middleware'), the global help ones on a plain ['web', 'auth'] stack. The request edge explains why those two cannot share a stack.

Overriding a default

Your own provider, in register():

php
public function register(): void
{
    $this->app->bind(TenantResolverInterface::class, AcmeTenantResolver::class);
    $this->app->bind(LoginProviderInterface::class, LdapLoginProvider::class);
}

Registry calls that need other services go in boot():

php
public function boot(): void
{
    $this->app->make(AddressFormatterRegistry::class)->register(new AustrianAddressFormatter());
}

Laravel boots your application providers after the package's, so your binding wins — except where the SDK used bindIf, in which case it never registers a default at all if you got there first. Either way the outcome is the same.

The two things you cannot rebind

The migrations and the three shipped modules load unconditionally. Everything else in the file is either a config key you can redeclare or a binding you can replace.