Codeskill

Learn to code, step by step

OOP in practice – interfaces, traits, dependency injection

Putting oOP into practice: interfaces, traits, and a light touch of dependency injection. The intro series covered classes and objects. Here we use those tools to keep code testable and swappable.

Interfaces – contracts between parts

An interface says what a class must do, not how. Useful when you might swap implementations (file storage vs cloud storage, real mail vs fake mail).

<?php
declare(strict_types=1);

namespace AppContracts;

interface MailerInterface
{
    public function send(string $to, string $subject, string $body): bool;
}
<?php
declare(strict_types=1);

namespace AppServices;

use AppContractsMailerInterface;

class LogMailer implements MailerInterface
{
    public function send(string $to, string $subject, string $body): bool
    {
        $line = sprintf("[%s] To: %s | %sn", date('c'), $to, $subject);
        file_put_contents(__DIR__ . '/../../storage/logs/mail.log', $line, FILE_APPEND);
        return true;
    }
}

Code that needs to send mail depends on MailerInterface, not a concrete class. You can plug in a real SMTP mailer later without rewriting the controller.

Traits – shared behaviour, used sparingly

Traits copy methods into a class. Handy for small shared snippets. Do not use them as a substitute for good design – if you need five traits on one class, something else is wrong.

<?php
declare(strict_types=1);

namespace AppTraits;

trait Timestamps
{
    protected function touchTimestamps(array &$row): array
    {
        $now = date('Y-m-d H:i:s');
        if (!isset($row['created_at'])) {
            $row['created_at'] = $now;
        }
        $row['updated_at'] = $now;
        return $row;
    }
}

Dependency injection – pass things in

Dependency injection means giving a class what it needs via the constructor (or setter), instead of creating dependencies inside the class with new.

<?php
declare(strict_types=1);

namespace AppControllers;

use AppContractsMailerInterface;

class ContactController
{
    public function __construct(
        private MailerInterface $mailer
    ) {}

    public function submit(array $input): void
    {
        // validate, then:
        $this->mailer->send(
            'hello@example.com',
            'Contact form',
            $input['message'] ?? ''
        );
    }
}

The controller does not know whether mail goes to a log file or SMTP. Something higher up (often index.php or a tiny container) wires the pieces together:

$mailer = new AppServicesLogMailer();
$controller = new AppControllersContactController($mailer);

When to reach for these tools

  • Interface – two or more implementations, or you want to mock in tests
  • Trait – identical small methods in unrelated classes (use rarely)
  • DI – a class needs a database, mailer, or logger; do not new them inside

You do not need a DI container framework. Constructor injection and a few lines in your front controller are enough for apps this size.

PreviousComposer and autoloading