breadbunch/ory-contracts
Provides PHP contracts for integrating Ory identity and access management services into Laravel applications. It defines reusable interfaces for Ory clients, identity flows, and related services, helping keep implementations decoupled, consistent, and easy to test across projects.
breadbunch/ory-contracts is a contracts-only package: it ships interfaces (not implementations) for talking to Ory (Kratos/Hydra-style identity and auth services). Its job is to let your application depend on abstractions rather than a concrete Ory SDK, so you can swap clients, decorate them, or fake them in tests.
First steps
composer require breadbunch/ory-contracts
vendor/breadbunch/ory-contracts/src/ and read the interfaces before writing any code. There is no service provider magic to discover — the value is in the type hints.BB\Ory\Contracts\... (previously a different prefix — see Gotchas).composer.json. If not, you will need an adapter (your own or a third-party Ory client) that implements these interfaces.App\Providers\AppServiceProvider::register() is fine for a small app; a dedicated OryServiceProvider is better once you have more than two bindings.First use case: inject an interface into a controller or action and let the container resolve it.
use BB\Ory\Contracts\...;
final class LoginController
{
public function __construct(private readonly SomeOryContract $ory) {}
public function __invoke(LoginRequest $request)
{
return $this->ory->...;
}
}
If you get Target [BB\Ory\Contracts\...] is not instantiable, that is the expected error — you have not bound an implementation yet.
Bind interfaces to concrete adapters. Keep exactly one binding per contract, keyed by the interface FQCN, and put the adapter in your own namespace (App\Services\Ory\...) so the vendor package never becomes a transitive dependency of your domain code.
$this->app->bind(
\BB\Ory\Contracts\Identity\IdentityApi::class,
\App\Services\Ory\KratosIdentityApi::class
);
Adapter over the generated Ory client. If you use an OpenAPI-generated client, wrap it: the generated class implements nothing you control, and its method signatures change with Ory versions. Your adapter maps the SDK's arrays/objects into your own DTOs, satisfying the contract while shielding the rest of the app.
Split admin vs. public (self-service) surfaces. Ory typically exposes an admin API and a public/frontend API with different auth requirements. Bind them separately (e.g. two interfaces, two adapters, two HTTP clients with different base URLs and credentials) rather than one god-interface with an $isAdmin flag.
Decorate for cross-cutting concerns. Because consumers only see the interface, you can wrap the real adapter in a logging, retry, or caching decorator and rebind it — no call-site changes:
$this->app->bind(IdentityApi::class, fn ($app) => new LoggingIdentityApi(
new KratosIdentityApi($app['ory.http']),
$app['log']
));
Fake at the interface in tests. This is the main day-to-day payoff. $this->mock(IdentityApi::class) or a hand-written in-memory fake lets you test session/login flows without a running Ory instance. Bind the fake in a test service provider or use $this->instance(...).
Contextual binding for multi-tenant setups. If different tenants hit different Ory projects, use when(...)->needs(...)->give(...) on the consumer class, or resolve the adapter from a small factory keyed by tenant, rather than reading config inside the adapter.
Keep domain services interface-typed. Type-hint contracts in your actions/jobs/listeners, never the adapter class. That keeps queue payloads and constructor signatures stable when you upgrade or replace the implementation.
Config-driven driver selection. Put ory.driver, base URLs, and credentials in config/ory.php, and let the service provider choose the adapter. Avoid env() calls outside config files — it breaks under config:cache.
0.1.1 is a breaking namespace rename. PR #3 moved everything to BB\Ory\Contracts. Any use statement, class_exists()/interface_exists() string, container binding key, config value, or mock target referencing the old prefix will silently fail to resolve. Do a project-wide grep for the old namespace and for hardcoded FQCN strings (bindings and mocks are the ones people forget, because they are strings, not imports).
Re-run the autoloader and clear caches after upgrading. composer dump-autoload, then php artisan config:clear && php artisan cache:clear. Stale cached bindings are a common source of "it works in tinker but not in the app."
Pin sensibly. This is a 0.1.x package, so semver allows breaking changes in minor bumps. Use ^0.1.1 deliberately, and read the changelog on every 0.1.x → 0.2.x jump rather than relying on Composer to protect you.
Contracts ship no behaviour — no default implementation. There is nothing to fall back on. If a binding is missing, you get an "is not instantiable" error at resolution time, not at boot. Consider a composer test/CI smoke test that resolves each contract from the container so missing bindings fail fast.
Mocking interfaces with many methods is verbose. If the interfaces are wide, prefer small hand-written fakes in tests/Support/ over PHPUnit mocks; you only implement what the test exercises. Keep them in one place so an interface change breaks one file, not twenty tests.
Interface changes are breaking for you, not just the package. Adding a method to a contract forces every adapter and fake you own to implement it. Check the diff of the interfaces between versions — a release note mentioning "contracts" changes means work on your side even if the diff looks trivial.
Watch LSP/return-type drift in adapters. Ory API responses are loosely typed; if you declare a concrete return type in your adapter, make sure it matches the interface exactly (nullable, array vs. collection, static vs. self). PHP will throw a fatal on incompatible signatures at class load, which surfaces as a confusing 500 rather than a clear error.
Extension points: your adapter, your decorators, and your service provider bindings. There is no package-side extension API to hook into — if you need different behaviour, decorate rather than fork, so you keep receiving upstream namespace/interface fixes like 0.1.1.
Static analysis pays off here. With larastan/phpstan at a decent level, the namespace rename and any interface signature mismatch show up as analysis errors instead of runtime failures in a login path.
How can I help you explore Laravel packages today?