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.

