Choose the right design tool - a contract, an algorithm skeleton or horizontal reuse - by knowing the trade-offs of each.
Open this lesson in KodokonPHP gives you four design tools that partly overlap: concrete inheritance, abstract classes, interfaces and traits. The choice comes down to one question: what do you want to share? A contract: interface. A contract plus an implementation skeleton: abstract class. Code shared between classes with no hierarchical link: trait. Concrete inheritance, for its part, is increasingly hard to justify: it couples tightly, and PHP allows only a single parent.
<?php
declare(strict_types=1);
abstract class Exporter
{
public function export(array $rows): string
{
return $this->head() . $this->body($rows);
}
abstract protected function head(): string;
abstract protected function body(
array $rows
): string;
}
final class CsvExporter extends Exporter
{
protected function head(): string
{
return "id;name\n";
}
protected function body(array $rows): string
{
$lines = array_map(
fn (array $r): string => implode(';', $r),
$rows,
);
return implode("\n", $lines);
}
}
$csv = new CsvExporter();
echo $csv->export([[1, 'Ada'], [2, 'Linus']]);The interface is the pure contract: signatures and constants, no state and no implementation. Its decisive advantage over the abstract class: a class can implement several of them, and any existing class can adopt the contract after the fact. The abstract class, by contrast, carries state and shared code, but consumes your single inheritance slot. In practice: type your parameters with interfaces (Cache, not ArrayCache) so you can swap implementations, in tests as well as in production.
<?php
declare(strict_types=1);
interface Cache
{
public function get(string $key): ?string;
public function set(
string $key,
string $value
): void;
}
final class ArrayCache implements Cache
{
private array $items = [];
public function get(string $key): ?string
{
return $this->items[$key] ?? null;
}
public function set(
string $key,
string $value
): void {
$this->items[$key] = $value;
}
}
$cache = new ArrayCache();
$cache->set('lang', 'PHP');
echo $cache->get('lang');Traits solve horizontal sharing: injecting the same methods into classes with no common ancestor. When two traits provide a method with the same name, PHP refuses to choose for you and demands an explicit resolution: insteadof designates the version to keep, as creates an alias to keep the other one accessible. It's verbose by design - the conflict must stay visible in the code.
<?php
declare(strict_types=1);
trait FileLogger
{
public function log(string $message): void
{
echo "[file] $message\n";
}
}
trait AuditLogger
{
public function log(string $message): void
{
echo "[audit] $message\n";
}
}
final class PaymentService
{
use FileLogger, AuditLogger {
FileLogger::log insteadof AuditLogger;
AuditLogger::log as audit;
}
}
$service = new PaymentService();
$service->log('Payment received');
$service->audit('Payment received');log() method. What happens?insteadof