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

Example · php
<?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.

Example · php
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.

Example · php
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.

Example · php
// 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

  • Be the first to comment on this lesson.