Contracts (Interfaces)
Program to interfaces, not implementations — the seam that makes code swappable and testable.
Laravel's contracts are a set of interfaces (in Illuminate\Contracts) that define what the framework's core services do, independent of how they do it. Cache\Repository, Queue\Factory, Mail\Mailer — each is an interface with a concrete implementation bound in the container.
Why depend on the interface?
- Swappability — bind a different implementation without touching consumers.
- Testability — inject a fake that satisfies the same contract.
- Honesty — the interface documents exactly the surface you rely on.
// Depend on the contract, not the concrete Repository
use Illuminate\Contracts\Cache\Repository;
public function __construct(private Repository $cache) {}Your own contracts
The real power is defining your own interfaces for domain concepts — PaymentGateway, SmsSender, SearchIndex — then binding the concrete in a provider. Your application code depends on the abstraction; the vendor detail stays at the edge.
Example
<?php
// --- The contract I own ---
namespace App\Contracts;
use App\ValueObjects\Charge;
interface PaymentGateway
{
public function charge(int $cents, string $token): Charge;
public function refund(string $chargeId): void;
}
// --- A real implementation wrapping the vendor SDK ---
namespace App\Services;
use App\Contracts\PaymentGateway;
use App\ValueObjects\Charge;
class StripeGateway implements PaymentGateway
{
public function __construct(private string $secret) {}
public function charge(int $cents, string $token): Charge
{
// ...vendor SDK call stays isolated behind this class...
return new Charge(id: 'ch_123', cents: $cents);
}
public function refund(string $chargeId): void { /* ... */ }
}
// --- A test double satisfying the SAME contract ---
namespace Tests\Fakes;
use App\Contracts\PaymentGateway;
use App\ValueObjects\Charge;
class FakeGateway implements PaymentGateway
{
public array $charges = [];
public function charge(int $cents, string $token): Charge
{
return $this->charges[] = new Charge(id: 'fake_' . count($this->charges), cents: $cents);
}
public function refund(string $chargeId): void {}
}
// Bound in a provider: $this->app->bind(PaymentGateway::class, StripeGateway::class);
// Swapped in a test: $this->app->instance(PaymentGateway::class, new FakeGateway());When to use it
- A developer type-hints Illuminate\Contracts\Cache\Repository in a service class constructor so swapping from Redis to Memcached requires only a container binding change.
- A package defines its own interface (contract) for a notification channel so users can provide custom implementations through the container without modifying the package source.
- An integration test replaces the Queue contract binding with a fake synchronous implementation, ensuring jobs run immediately without a real queue worker.
More examples
Type-hint a contract in a service
Type-hinting Contracts means the container resolves the correct concrete class; swapping implementations requires only a service-provider change.
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Contracts\Cache\Repository;
class OrderService
{
public function __construct(
private Mailer $mailer,
private Repository $cache
) {}
public function complete(Order $order): void
{
$this->cache->forget('orders.pending');
$this->mailer->to($order->user)->send(new OrderCompleteMail($order));
}
}Bind a custom implementation to a contract
Binding the contract to a custom concrete class in a service provider replaces the default implementation app-wide without touching any service class.
use Illuminate\Contracts\Cache\Repository;
// Swap the default Redis driver for a DynamoDB store
$this->app->bind(Repository::class, DynamoDbCacheStore::class);
// Or via a factory closure
$this->app->bind(Repository::class, function ($app) {
return new DynamoDbCacheStore(
config('cache.dynamodb'),
$app->make(LoggerInterface::class)
);
});Define your own contract interface
Defining a custom contract interface and binding it lets any service type-hint SmsSender; switching to a different SMS provider is a one-line change in the provider.
// app/Contracts/SmsSender.php
interface SmsSender
{
public function send(string $to, string $message): bool;
}
// app/Services/TwilioSmsSender.php
class TwilioSmsSender implements SmsSender
{
public function send(string $to, string $message): bool
{
// call Twilio API
return true;
}
}
// Bind in AppServiceProvider
$this->app->bind(SmsSender::class, TwilioSmsSender::class);
Discussion