# Interfaces and Dependency Injection in PHP

> How I use interface definitions and dependency injection in PHP to build loosely coupled classes, illustrated with practical examples.

- Published: 2016-07-03
- Category: Languages
- Tags: PHP, OOP
- Reading time: 3 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/interfaces-and-dependency-injection-in-php/
- Language: en-US
- Author: Muhammet Şafak

---
Imagine you're writing a class that needs to send email. If you hard-code `new SwiftMailer()` inside it, that class becomes tightly coupled to SwiftMailer. The day comes when you need to swap out the library, or you want to avoid sending real emails during testing — and suddenly you have to reach back into that class and change it.

Interfaces and dependency injection are two concepts used together to solve exactly this problem. Both were the tools I leaned on when [putting SOLID into practice in PHP](/en/blog/putting-solid-into-practice-in-php-single-responsibility-principle/); that post was about the single responsibility principle, this one is about the mechanism that makes it possible. I've been applying them more deliberately in my own projects over the past few months, and this post is a write-up of what I've learned.

## What is an interface?

An **interface** is a contract that defines what a class can do — not how it does it. Any class that implements the interface is required to provide all the methods declared in it.

```php
<?php

interface MailerInterface
{
    public function gonder(string $alici, string $konu, string $icerik): bool;
}
```

Let's write two different classes that implement this interface:

```php
<?php

class SmtpMailer implements MailerInterface
{
    public function gonder(string $alici, string $konu, string $icerik): bool
    {
        // Send email via a real SMTP connection
        return mail($alici, $konu, $icerik);
    }
}

class LogMailer implements MailerInterface
{
    public function gonder(string $alici, string $konu, string $icerik): bool
    {
        // Write to a log file instead of sending a real email
        file_put_contents('mail.log', "[{$alici}] {$konu}\n", FILE_APPEND);
        return true;
    }
}
```

Both classes satisfy `MailerInterface`. From the outside, they are interchangeable: both expose a `gonder()` method.

## What is dependency injection?

**Dependency injection** means that instead of a class creating the objects it needs internally, those objects are passed in from the outside. Constructor injection is the most common approach.

```php
<?php

class UserNotifier
{
    private $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function kayitOlduBildir(string $email): bool
    {
        return $this->mailer->gonder(
            $email,
            'Kaydınız tamamlandı',
            'Sisteme başarıyla kaydoldunuz. Hoşgeldiniz!'
        );
    }
}
```

`UserNotifier` no longer knows about `SmtpMailer`. It only knows about `MailerInterface`. Which concrete implementation is used gets decided from the outside:

```php
<?php

// Production: real email
$notifier = new UserNotifier(new SmtpMailer());
$notifier->kayitOlduBildir('kullanici@example.com');

// Development: log only
$notifier = new UserNotifier(new LogMailer());
$notifier->kayitOlduBildir('test@example.com');
```

`UserNotifier` never changes. Its behavior is shaped entirely by the object injected into it.

This pattern can feel overly indirect at first. But think about it: when you need to move from SwiftMailer to another library, you never touch `UserNotifier`. You write a new `SmtpMailer` class, PHP verifies it satisfies the interface, and you're done. When you test `UserNotifier`, you don't need to connect to a real mail server. Those are the two problems it solves.

## When you write tests, the difference becomes obvious

When you want to test `UserNotifier`, you don't want to send real emails. Thanks to the interface, you can write a dedicated test implementation:

```php
<?php

class FakeMailer implements MailerInterface
{
    public array $gonderilen = [];

    public function gonder(string $alici, string $konu, string $icerik): bool
    {
        $this->gonderilen[] = compact('alici', 'konu', 'icerik');
        return true;
    }
}

// Test code
$fakeMailer = new FakeMailer();
$notifier   = new UserNotifier($fakeMailer);

$notifier->kayitOlduBildir('test@example.com');

assert(count($fakeMailer->gonderilen) === 1);
assert($fakeMailer->gonderilen[0]['alici'] === 'test@example.com');
```

No real SMTP connection, tests run fast, and you can verify exactly what was sent.

Without the interface, writing this test would require either modifying `SmtpMailer` at test time or adding conditional logic inside `UserNotifier`. Both are bad options.

## You don't need interfaces everywhere

If a class is only ever going to have one implementation and there's no plan to change it, adding an interface creates unnecessary complexity. The situations where interfaces earn their keep are:

- Things that will have multiple implementations (payments, email, storage, etc.).
- Dependencies where you'll want to substitute a fake/mock during testing.
- Behaviors expected to be externally configurable.

Adding an interface to every class is not a requirement — using them in the right places is what matters.

When I was starting out, my mistake was adding an interface to everything. `UserRepository` had a `UserRepositoryInterface`. `OrderService` had an `OrderServiceInterface`. After a while the project had twice as many files as real classes, most interfaces had a single implementation, and none of them ever changed. Letting go of that unnecessary abstraction took some time.

## The Laravel container makes this easy

In Laravel, I use the service container to bind an interface to a concrete class:

```php
// Inside AppServiceProvider or a dedicated provider
$this->app->bind(MailerInterface::class, SmtpMailer::class);
```

Now whenever `UserNotifier` is resolved through the container, Laravel automatically satisfies the `MailerInterface` dependency with `SmtpMailer`. You can also bind a different implementation based on the environment:

```php
if (app()->environment('production')) {
    $this->app->bind(MailerInterface::class, SmtpMailer::class);
} else {
    $this->app->bind(MailerInterface::class, LogMailer::class);
}
```

This registration happens once. From that point on, no matter where you resolve `UserNotifier`, the correct mailer is injected automatically.

Using these two concepts together feels a bit abstract at first. But once you put them into practice and see how much more flexible the codebase becomes, the logic clicks into place.
