Skip to main content
Contracts

Contracts

Contracts are interfaces that describe what a service does. Laravel publishes contracts for core services such as the cache, queue, filesystem, and mailer. Depending on an interface instead of a concrete class lets you replace implementations and test behavior in isolation.

Using a contract

namespace App\Services;

use Illuminate\Contracts\Cache\Repository as Cache;

class DashboardStats
{
    public function __construct(private Cache $cache)
    {
    }

    public function load(): array
    {
        return $this->cache->remember('dashboard.stats', 600, function () {
            return [
                'users' => User::count(),
                'orders' => Order::count(),
            ];
        });
    }
}

The container resolves Illuminate\Contracts\Cache\Repository to the configured cache store. Swapping the cache driver requires no change to this class.

Defining your own contract

namespace App\Contracts;

interface PaymentGateway
{
    public function charge(int $amount, string $currency): PaymentResult;

    public function refund(string $transactionId, int $amount): PaymentResult;
}
namespace App\Services;

use App\Contracts\PaymentGateway;

class StripeGateway implements PaymentGateway
{
    public function charge(int $amount, string $currency): PaymentResult
    {
        // call Stripe
    }

    public function refund(string $transactionId, int $amount): PaymentResult
    {
        // call Stripe
    }
}

Binding the interface

use App\Contracts\PaymentGateway;
use App\Services\StripeGateway;

public function register(): void
{
    $this->app->bind(PaymentGateway::class, StripeGateway::class);
}

Controllers and jobs can now type-hint the interface. Tests can bind a fake implementation without touching Stripe.

class FakeGateway implements PaymentGateway
{
    public array $charges = [];

    public function charge(int $amount, string $currency): PaymentResult
    {
        $this->charges[] = compact('amount', 'currency');

        return PaymentResult::approved('test-'.$amount);
    }

    public function refund(string $transactionId, int $amount): PaymentResult
    {
        return PaymentResult::approved($transactionId);
    }
}

Contextual binding

Two classes may need different implementations of the same contract. Contextual binding selects one based on the consumer.

$this->app->when(ReportExporter::class)
    ->needs(PaymentGateway::class)
    ->give(function () {
        return new SandboxGateway();
    });

Contracts versus facades

A contract is a type you inject; a facade is a static proxy to a container binding. Both resolve the same services. Prefer contracts at architectural boundaries where replacement matters, and facades for short, local use of framework utilities.

Practical guidance

  • Define contracts around behavior your application owns, not around every class.
  • Keep interfaces small; one or two methods are easier to fake and implement.
  • Register bindings in a service provider, not in controllers.
  • Let tests bind fakes through the container rather than using conditionals in production code.