Modern PHP Mastery: From First Steps to Production
Chapter 7: Inheritance, Abstract Classes, and Interfaces
In Chapter 6, we stepped into the object-oriented paradigm. We saw how individual classes encapsulate their own state, guard business rules, and eliminate repetitive boilerplate with constructor property promotion and readonly properties.
However, real-world systems rarely consist of isolated, independent classes. Consider an e-commerce platform that processes payments: you might need to support Stripe, PayPal, local bank transfers, or cash on delivery. Each gateway behaves differently behind the scenes, yet the checkout process needs to treat them all identically: charge an amount, check for success, and record a transaction ID.
If you write separate, disconnected classes for each payment gateway and check their types with lengthy if/else chains in your checkout controller, your codebase quickly becomes brittle and hard to maintain.
To solve this, modern PHP relies on three core architectural pillars:
* Inheritance: Sharing common functionality between parent and child classes.
* Abstract Classes: Providing partial implementations that enforce rules on child classes.
* Interfaces: Defining strict behavioral contracts that enable polymorphism.
In this chapter, we will master each of these concepts, explore their trade-offs, and examine the SOLID design principles that prevent inheritance trees from turning into an unmaintainable mess.
Understanding Class Inheritance (extends)
Inheritance allows a child class to inherit public and protected properties and methods from a parent class using the extends keyword.
Let’s start with a practical logging example:
<?php
declare(strict_types=1);
class BaseLogger
{
public function __construct(
protected string $channelName = 'application'
) {}
public function formatMessage(string $level, string $message): string
{
$timestamp = date('Y-m-d H:i:s');
return "[{$timestamp}] [{$this->channelName}] [{$level}]: {$message}" . PHP_EOL;
}
}
class FileLogger extends BaseLogger
{
public function __construct(
string $channelName,
private string $filePath
) {
// Call the parent class constructor to set up inherited state
parent::__construct($channelName);
}
public function log(string $level, string $message): void
{
$formatted = $this->formatMessage($level, $message);
file_put_contents($this->filePath, $formatted, FILE_APPEND);
}
}
Key Rules of Inheritance
* Single Inheritance Only: PHP does not support multiple class inheritance. A class can extend only one parent class (class Child extends ParentA is valid, but class Child extends ParentA, ParentB is a fatal syntax error).
* The parent:: Keyword: When overriding a method or constructor in a child class, use parent::__construct(...) or parent::methodName(...) to execute the original implementation in the parent.
* The protected Access Modifier: Properties marked private in a parent class are invisible to child classes. If a child class needs access to an inherited property, declare that property as protected.
Overriding Methods and the final Keyword
A child class can override methods defined in its parent to modify or extend their behavior:
<?php
declare(strict_types=1);
class StandardMailer
{
public function send(string $to, string $subject, string $body): bool
{
// Standard delivery logic
return mail($to, $subject, $body);
}
}
class StagingMailer extends StandardMailer
{
// Override the send method to redirect all emails to a safety address
public function send(string $to, string $subject, string $body): bool
{
$safeRecipient = 'dev-team@internal.local';
$modifiedBody = "Original recipient: {$to}\n\n" . $body;
return parent::send($safeRecipient, "[STAGING] " . $subject, $modifiedBody);
}
}
Locking Down Classes and Methods with final
Sometimes you design a class or method whose behavior must never be altered by a subclass. The final keyword prevents overriding:
<?php
declare(strict_types=1);
class SecurityTokenGenerator
{
// No child class can override this specific method
final public function generateSecureHash(string $payload): string
{
return hash_hmac('sha256', $payload, 'system-secret-salt');
}
}
// Or lock the entire class:
final class ImmutableUuid
{
// This class cannot be extended at all: class SubUuid extends ImmutableUuid triggers a fatal error
}
> Design Tip: Making utility classes and value objects final by default is a recognized best practice. It prevents unintended side effects caused by subclasses tampering with critical business logic.
>
Abstract Classes: Blueprints with Partial Implementations
An abstract class (declared with the abstract keyword) is a class that cannot be instantiated directly using new. It exists purely to serve as a base for other classes.
Abstract classes can contain:
* Concrete methods: Methods with full implementations that child classes inherit automatically.
* Abstract methods: Method signatures without a body. Any non-abstract child class extending the parent must implement these methods, adhering strictly to the defined parameter and return types.
<?php
declare(strict_types=1);
abstract class AbstractReportExporter
{
public function __construct(
protected array $records
) {}
// Concrete method: Common across all report formats
public function getRecordCount(): int
{
return count($this->records);
}
// Abstract method: Every export format must implement this differently
abstract public function export(): string;
abstract public function getMimeType(): string;
}
class CsvReportExporter extends AbstractReportExporter
{
public function export(): string
{
if (empty($this->records)) {
return '';
}
$output = fopen('php://temp', 'r+');
fputcsv($output, array_keys($this->records[0])); // Headers
foreach ($this->records as $row) {
fputcsv($output, $row);
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
return $csv;
}
public function getMimeType(): string
{
return 'text/csv';
}
}
Use abstract classes when several related entities share substantial underlying logic, but each subclass has a unique implementation detail that must be enforced at compile time.
Interfaces: The Pure Behavioral Contract
While an abstract class can contain state and concrete logic, an Interface is a pure contract. It declares what a class must do, but provides zero information about how to do it.
An interface contains only public method signatures (and optionally constants). It cannot contain properties or concrete method implementations.
<?php
declare(strict_types=1);
interface CacheDriverInterface
{
public function get(string $key): mixed;
public function set(string $key, mixed $value, int $ttlSeconds = 3600): bool;
public function delete(string $key): bool;
public function clear(): bool;
}
A class binds itself to an interface using the implements keyword:
<?php
declare(strict_types=1);
class RedisCacheDriver implements CacheDriverInterface
{
public function get(string $key): mixed
{
// Redis-specific connection and retrieval logic
return null;
}
public function set(string $key, mixed $value, int $ttlSeconds = 3600): bool
{
// Store into Redis cluster
return true;
}
public function delete(string $key): bool
{
return true;
}
public function clear(): bool
{
return true;
}
}
Multiple Interface Implementation
While a class can extend only one parent class, it can implement multiple interfaces. This is one of the most powerful features in PHP:
<?php
declare(strict_types=1);
interface LoggableInterface
{
public function logActivity(string $action): void;
}
interface ExportableInterface
{
public function toArray(): array;
}
// Implements multiple contracts simultaneously
class UserAuditService implements LoggableInterface, ExportableInterface
{
public function logActivity(string $action): void
{
// Logging implementation
}
public function toArray(): array
{
return ['service' => 'audit', 'status' => 'active'];
}
}
Abstract Class vs. Interface: Which One Should You Use?
Choosing between an abstract class and an interface is one of the most common architecture decisions. Use this comparison table to guide your choice:
| Feature | Interface | Abstract Class |
|---|---|---|
| Multiple Implementation | Yes (implements A, B, C) | No (Can only extend one parent) |
| Properties | No (Constants only) | Yes (private, protected, public) |
| Method Implementations | None (Signatures only) | Both concrete and abstract methods |
| Primary Intent | Defines a behavioral contract ("Can do this") | Defines an identity and shared logic ("Is a kind of this") |
| Coupling | Loosely coupled | Tightly coupled |
Modern Architecture Best Practice:
Favor interfaces for your public API contracts, and optionally back them with an abstract class as a convenience layer if you want to provide common default logic to avoid code duplication.
The Power of Polymorphism
Polymorphism ("many forms") means that different classes implementing the same interface can be used interchangeably throughout your application.
Your controllers or services don't need to know whether they are dealing with a local file storage, an AWS S3 bucket, or an SFTP server. They only need to know that the object satisfies a StorageInterface:
<?php
declare(strict_types=1);
interface StorageInterface
{
public function put(string $path, string $contents): bool;
}
class LocalStorage implements StorageInterface
{
public function put(string $path, string $contents): bool
{
return (bool) file_put_contents('/var/www/uploads/' . $path, $contents);
}
}
class S3Storage implements StorageInterface
{
public function put(string $path, string $contents): bool
{
// Simulating upload to cloud object bucket
return true;
}
}
// Polymorphic consumer function
function storeAvatar(StorageInterface $storage, string $userId, string $imageBinary): bool
{
// The consumer doesn't care which storage engine is running!
return $storage->put("avatars/{$userId}.jpg", $imageBinary);
}
// Both calls are 100% type-safe and valid:
storeAvatar(new LocalStorage(), 'user_101', 'binary_data');
storeAvatar(new S3Storage(), 'user_101', 'binary_data');
If you decide to migrate from local storage to Google Cloud Storage tomorrow, you write a new class implementing StorageInterface. Not a single line of code inside storeAvatar() needs to change.
Practical Example: Building a Polymorphic Payment Processing Engine
Let’s put all these architectural concepts together. We will build a checkout billing engine that defines a strict PaymentGatewayInterface, uses an abstract class to hold transaction auditing logic, and provides concrete implementations for Stripe and BankTransfer.
Create a file named payment_engine.php:
<?php
declare(strict_types=1);
/**
* Immutable Result Object representing the outcome of a charge attempt.
*/
readonly class PaymentResult
{
public function __construct(
public bool $isSuccessful,
public string $transactionId,
public string $gatewayName,
public int $amountChargedInCents,
public string $message
) {}
}
/**
* The Interface: The contract all gateways must satisfy.
*/
interface PaymentGatewayInterface
{
public function charge(int $amountInCents, string $customerReference): PaymentResult;
public function getName(): string;
}
/**
* Abstract Base Gateway: Encapsulates shared auditing and validation logic.
*/
abstract class AbstractPaymentGateway implements PaymentGatewayInterface
{
public function __construct(
protected readonly string $currency = 'PKR'
) {}
/**
* Common guard rule: Ensure payments are always positive.
*/
protected function validateAmount(int $amountInCents): void
{
if ($amountInCents <= 0) {
throw new InvalidArgumentException("Payment charge amount must be greater than zero.");
}
}
/**
* Generates a standardized audit reference code.
*/
protected function generateAuditReference(string $prefix): string
{
return $prefix . '_' . strtoupper(bin2hex(random_bytes(6)));
}
}
/**
* Concrete Gateway 1: Credit Card processing via simulated Stripe.
*/
final class StripePaymentGateway extends AbstractPaymentGateway
{
public function __construct(
private readonly string $secretApiKey,
string $currency = 'PKR'
) {
parent::__construct($currency);
}
public function getName(): string
{
return 'Stripe Credit Card API';
}
public function charge(int $amountInCents, string $customerReference): PaymentResult
{
$this->validateAmount($amountInCents);
// Simulate instant API network authorization
$txId = $this->generateAuditReference('ch_stripe');
return new PaymentResult(
isSuccessful: true,
transactionId: $txId,
gatewayName: $this->getName(),
amountChargedInCents: $amountInCents,
message: "Instant card charge cleared for {$customerReference}."
);
}
}
/**
* Concrete Gateway 2: Direct Interbank Settlement.
*/
final class BankTransferGateway extends AbstractPaymentGateway
{
public function __construct(
private readonly string $ibanNumber,
string $currency = 'PKR'
) {
parent::__construct($currency);
}
public function getName(): string
{
return 'Direct 1Link IBFT Settlement';
}
public function charge(int $amountInCents, string $customerReference): PaymentResult
{
$this->validateAmount($amountInCents);
// Bank transfers require manual voucher settlement
$txId = $this->generateAuditReference('ibft_voucher');
return new PaymentResult(
isSuccessful: true,
transactionId: $txId,
gatewayName: $this->getName(),
amountChargedInCents: $amountInCents,
message: "Payment voucher issued to IBAN {$this->ibanNumber}. Awaiting deposit settlement."
);
}
}
/**
* The Checkout Processor: Decoupled service that only depends on the Interface.
*/
class CheckoutService
{
public function __construct(
private PaymentGatewayInterface $gateway
) {}
public function processOrder(string $customerId, int $orderTotalInCents): PaymentResult
{
// Business logic runs identically regardless of the underlying payment provider
return $this->gateway->charge($orderTotalInCents, $customerId);
}
}
// Simulating two different checkout scenarios
$stripeGateway = new StripePaymentGateway('sk_live_demo_key9901');
$bankGateway = new BankTransferGateway('PK36SCBL0000001123456701');
$orderOne = (new CheckoutService($stripeGateway))->processOrder('CUST-802', 150000); // PKR 1,500.00
$orderTwo = (new CheckoutService($bankGateway))->processOrder('CUST-914', 450000); // PKR 4,500.00
$results = [$orderOne, $orderTwo];
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Polymorphic Payment Engine Demo</title>
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: #0f172a; color: #f8fafc; padding: 2rem; }
.wrapper { max-width: 720px; margin: 0 auto; }
.card { background: #1e293b; border-radius: 10px; padding: 1.5rem; margin-bottom: 1.5rem; border: 1px solid #334155; }
.title { font-size: 1.1rem; font-weight: bold; color: #38bdf8; margin-bottom: 0.5rem; }
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.75rem; font-size: 0.9rem; margin-top: 1rem; }
.badge { display: inline-block; background: #065f46; color: #6ee7b7; padding: 0.2rem 0.6rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; }
.amount { font-size: 1.25rem; font-weight: bold; color: #10b981; }
</style>
</head>
<body>
<div class="wrapper">
<h2>Checkout Payment Settlements</h2>
<p style="color: #94a3b8; margin-bottom: 1.5rem;">Demonstrating polymorphism via <code>PaymentGatewayInterface</code>.</p>
<?php foreach ($results as $item): ?>
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span class="title"><?= htmlspecialchars($item->gatewayName, ENT_QUOTES, 'UTF-8') ?></span>
<span class="badge"><?= $item->isSuccessful ? 'AUTHORIZED' : 'FAILED' ?></span>
</div>
<p style="color: #cbd5e1; margin: 0.5rem 0;"><?= htmlspecialchars($item->message, ENT_QUOTES, 'UTF-8') ?></p>
<div class="grid">
<div>
<span style="color: #64748b;">Transaction Reference:</span><br>
<code><?= htmlspecialchars($item->transactionId, ENT_QUOTES, 'UTF-8') ?></code>
</div>
<div style="text-align: right;">
<span style="color: #64748b;">Settled Sum:</span><br>
<span class="amount">PKR <?= number_format($item->amountChargedInCents / 100, 2) ?></span>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</body>
</html>
Run this file via php -S localhost:8000 and visit http://localhost:8000/payment_engine.php.
Notice how the CheckoutService has zero knowledge of API keys or IBAN vouchers. It simply requires an object that implements PaymentGatewayInterface. That separation of concerns is the hallmark of professional PHP architecture.
Chapter 7 Key Takeaways
* Class inheritance (extends) enables single-parent code reuse. Use parent:: to invoke overridden parent logic, and declare shared attributes as protected.
* Prevent fragile subclass modifications using final: Mark mission-critical classes or sensitive calculation methods final to stop them from being extended or altered.
* Abstract classes (abstract) cannot be instantiated directly; use them when subclasses share common implementation logic but need to enforce specific method definitions on children.
* Interfaces (implements) provide strict, pure contracts without any implementation details. A single class can implement multiple interfaces.
* Program to interfaces, not concrete implementations: Using polymorphism ensures your higher-level application services remain decoupled from third-party APIs and low-level drivers.
In Chapter 8, we will tackle Namespaces, Autoloading, and Composer: organizing large codebases, preventing name collisions, adhering to the PSR-4 autoloading standard, and managing external open-source packages.