Integration Points
Some things a business application needs, the SDK deliberately does not own: your login screen, the address-validation service you pay for, the spreadsheet library that writes .xlsx. Each of those is a place where your installation meets something the SDK cannot ship — a vendor you licensed, a screen you designed, a table you already had.
The SDK handles all of them the same way, and the shape is worth learning once:
Own a concrete vocabulary. Declare a port. Let the host bind the concrete. No third-party library ever appears in an SDK signature.
What that buys you
A port is an interface plus a DTO the SDK owns. LoginProviderInterface speaks LoginRecord, never your User model. AddressProviderInterface returns NormalizedAddress, never a Google Place. ExportWriterInterface consumes ExportableInterface, never a spreadsheet Worksheet.
Three consequences follow, and they are the reason the idiom is worth the extra interface:
- You can swap the vendor without touching the SDK. Google today, a national register tomorrow; the adapter changes, nothing else does.
- The SDK's
composer.jsonstays small. A licensed SDK, an API key, a 1 MB spreadsheet library — none of them is a dependency oflaravelui5/sdk, because none of them is reachable from an SDK type. - Your code never learns the vendor's shape. Everything downstream of the seam reads the SDK's DTO, so a vendor swap is invisible past the adapter.
Bought, never owned. The library, if there is one, lives behind the writer.
The ports
| Port | Ships a default? | What you bind | Page |
|---|---|---|---|
LoginProviderInterface | Yes, a working one | An adapter over your auth store | Login |
AddressProviderInterface | No — throws until bound | Your licensed validation vendor | Addresses |
AddressFormatterInterface | Yes, good-enough | A country-correct formatter, per country | Addresses |
ExportWriterInterface | Yes, for CSV | A writer per additional format | Export |
TenantResolverInterface | Yes, a single-tenant one | Your operator identity | Tenancy |
Two more host-extension surfaces work by the same instinct but are documented with their own domains: Customizing (you contribute catalog rows in code) and Connecting your user model (you contribute the auth()->user() → Partner mapping).
The three default stances
Not every port can have a default, and the SDK is deliberate about which stance each one takes. Read the stance before you write an adapter — it tells you whether you are replacing something or supplying something that is missing.
A working default. The SDK binds a real implementation and a stock install works with no wiring at all. Login is the example: EloquentLoginProvider reads your users table over the users.partner_id bridge. Bound with bindIf, so your own binding always wins:
// app/Providers/AppServiceProvider.php
public function register(): void
{
$this->app->bind(LoginProviderInterface::class, LdapLoginProvider::class);
}A good-enough default plus a registry. The SDK ships something honest but unambitious, and you add better ones alongside it rather than replacing it. Address formatting works this way: the DefaultAddressFormatter renders a sane Western line order for any country, and you register a country-correct formatter that claims only the countries it knows.
public function boot(): void
{
$this->app->make(AddressFormatterRegistry::class)->register(new AustrianAddressFormatter());
}No default at all. Some things the SDK cannot guess and must not fake. AddressProviderInterface is bound to a closure that throws NoAddressProviderBoundException with a message that says what to do. Resolving it without binding is a loud, immediate failure — never a silent wrong answer.
That last stance is also the opt-out idiom, and you can use it yourself. A host that manages logins entirely outside the SDK binds the port to a closure that throws, so the Partners app's login buttons fail with a domain message instead of writing to a users table that is not the real one:
$this->app->bind(LoginProviderInterface::class, static function () {
throw new NoLoginProviderBoundException();
});Where the binding goes
All of it is ordinary Laravel. Bindings belong in your own service provider's register(); registry calls that need other services belong in boot(). The SDK never scans for adapters and never auto-discovers them — if it is not bound, it is not there.
One port is the exception, because it is reached by a route rather than by a type: the export endpoint needs ExportArtifactResolver in your config/ui5.php resolver chain. See Configuration.