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.