Lesson 4.4: Interfaces and Abstract Classes

Interfaces and abstract classes are the architects of object-oriented design. They define contracts that specify what classes must do without dictating how they do it. Think of them like building codes for construction—they ensure every building has essential features like fire exits and electrical safety, but architects are free to design buildings in countless creative ways within those requirements.

Imagine you're building a payment processing system for an e-commerce site. You need to accept credit cards, PayPal, Apple Pay, and cryptocurrency. Each payment method works completely differently internally, but they all need to process payments, handle refunds, and validate transactions. Interfaces let you define this common contract while allowing each payment processor to implement the details in its own way.

Abstract classes go a step further by providing partial implementations alongside requirements. They're like house blueprints that include some completed rooms (shared functionality) while leaving others for customization (abstract methods). This combination of shared code and enforced requirements creates powerful, flexible architectures.

Modern PHP frameworks extensively use interfaces and abstract classes. Laravel's contracts define interfaces for caching, queues, and authentication, allowing different implementations to be swapped seamlessly. Understanding these concepts is essential for building professional applications and working with modern PHP ecosystems.

Understanding Interfaces

An interface defines a contract—a set of method signatures that implementing classes must provide. It's like a job description that lists required skills without specifying how someone acquired those skills.

<?php
interface PaymentProcessorInterface {
    public function processPayment($amount, $paymentDetails);
    public function refundPayment($transactionId, $amount);
    public function validatePayment($paymentDetails);
}
?>

Now let's create a credit card processor that implements this interface:

<?php
class CreditCardProcessor implements PaymentProcessorInterface {
    public function processPayment($amount, $paymentDetails) {
        // Credit card specific logic
        if (!$this->validatePayment($paymentDetails)) {
            return ['success' => false, 'error' => 'Invalid card'];
        }

        return [
            'success' => true,
            'transaction_id' => 'CC_' . uniqid(),
            'amount' => $amount
        ];
    }

    public function refundPayment($transactionId, $amount) {
        echo "Credit card refund: $$amount for $transactionId\n";
        return ['success' => true];
    }

    public function validatePayment($paymentDetails) {
        return isset($paymentDetails['card_number']) &&
               isset($paymentDetails['cvv']);
    }
}
?>

Here's a PayPal processor implementing the same interface:

<?php
class PayPalProcessor implements PaymentProcessorInterface {
    public function processPayment($amount, $paymentDetails) {
        if (!$this->validatePayment($paymentDetails)) {
            return ['success' => false, 'error' => 'Invalid PayPal account'];
        }

        return [
            'success' => true,
            'transaction_id' => 'PP_' . uniqid(),
            'amount' => $amount
        ];
    }

    public function refundPayment($transactionId, $amount) {
        echo "PayPal refund: $$amount for $transactionId\n";
        return ['success' => true];
    }

    public function validatePayment($paymentDetails) {
        return isset($paymentDetails['paypal_email']) &&
               filter_var($paymentDetails['paypal_email'], FILTER_VALIDATE_EMAIL);
    }
}
?>

Now here's the magic—both processors can be used interchangeably:

<?php
function processOrder($processor, $amount, $paymentDetails) {
    $result = $processor->processPayment($amount, $paymentDetails);

    if ($result['success']) {
        echo "Payment successful: {$result['transaction_id']}\n";
    } else {
        echo "Payment failed: {$result['error']}\n";
    }
}

$creditCard = new CreditCardProcessor();
$paypal = new PayPalProcessor();

$ccDetails = ['card_number' => '4111111111111111', 'cvv' => '123'];
$paypalDetails = ['paypal_email' => '[email protected]'];

processOrder($creditCard, 100.00, $ccDetails);
processOrder($paypal, 100.00, $paypalDetails);
?>

The interface ensures both payment processors provide the same methods, making them interchangeable in any function that expects a PaymentProcessorInterface.

Multiple Interface Implementation

Unlike inheritance where a class can only extend one parent class, a class can implement multiple interfaces. This provides incredible flexibility for creating classes that fulfill multiple contracts.

<?php
interface Readable {
    public function read();
}

interface Writable {
    public function write($data);
}

interface Deletable {
    public function delete();
}
?>

A class can implement multiple interfaces:

<?php
class FileManager implements Readable, Writable, Deletable {
    private $filename;

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

    public function read() {
        return file_exists($this->filename) ?
               file_get_contents($this->filename) : null;
    }

    public function write($data) {
        return file_put_contents($this->filename, $data) !== false;
    }

    public function delete() {
        return file_exists($this->filename) ? unlink($this->filename) : false;
    }
}
?>

Functions can work with specific capabilities:

<?php
function displayData(Readable $source) {
    $data = $source->read();
    echo "Data: $data\n";
}

function saveData(Writable $destination, $data) {
    if ($destination->write($data)) {
        echo "Data saved successfully\n";
    }
}

$fileManager = new FileManager('data.txt');
saveData($fileManager, "Hello World");
displayData($fileManager);
?>

Understanding Abstract Classes

Abstract classes combine the contract enforcement of interfaces with the code sharing benefits of inheritance. They can contain both abstract methods (which child classes must implement) and concrete methods (which child classes inherit).

<?php
abstract class Document {
    protected $title;
    protected $author;
    protected $content;

    public function __construct($title, $author) {
        $this->title = $title;
        $this->author = $author;
        $this->content = '';
    }

    // Concrete method - shared by all documents
    public function getTitle() {
        return $this->title;
    }

    public function getWordCount() {
        return str_word_count($this->content);
    }

    // Abstract methods - must be implemented by child classes
    abstract public function render();
    abstract public function export($format);
}
?>

Here's a blog post that extends the abstract document:

<?php
class BlogPost extends Document {
    private $tags;

    public function __construct($title, $author) {
        parent::__construct($title, $author);
        $this->tags = [];
    }

    public function addTag($tag) {
        $this->tags[] = $tag;
    }

    public function setContent($content) {
        $this->content = $content;
    }

    public function render() {
        $html = "<article>\n";
        $html .= "<h1>{$this->title}</h1>\n";
        $html .= "<p>By {$this->author}</p>\n";
        $html .= "<div>{$this->content}</div>\n";
        $html .= "</article>";
        return $html;
    }

    public function export($format) {
        if ($format === 'html') {
            return $this->render();
        } elseif ($format === 'text') {
            return "{$this->title}\n{$this->content}";
        }
        return "Unsupported format";
    }
}
?>

And here's a report document:

<?php
class Report extends Document {
    private $department;
    private $data;

    public function __construct($title, $author, $department) {
        parent::__construct($title, $author);
        $this->department = $department;
        $this->data = [];
    }

    public function addData($key, $value) {
        $this->data[$key] = $value;
    }

    public function setContent($content) {
        $this->content = $content;
    }

    public function render() {
        $output = "REPORT: {$this->title}\n";
        $output .= "Department: {$this->department}\n";
        $output .= "Author: {$this->author}\n";
        $output .= "Content: {$this->content}\n";
        return $output;
    }

    public function export($format) {
        if ($format === 'text') {
            return $this->render();
        } elseif ($format === 'csv') {
            $csv = "Key,Value\n";
            foreach ($this->data as $key => $value) {
                $csv .= "$key,$value\n";
            }
            return $csv;
        }
        return "Unsupported format";
    }
}
?>

Both document types can be used polymorphically:

<?php
function processDocument(Document $doc) {
    echo "Processing: " . $doc->getTitle() . "\n";
    echo "Word count: " . $doc->getWordCount() . "\n";
    echo $doc->render() . "\n";
}

$blog = new BlogPost("PHP OOP Guide", "Alice");
$blog->setContent("Learn object-oriented programming with practical examples.");
$blog->addTag("PHP");

$report = new Report("Sales Report", "Bob", "Sales");
$report->setContent("Q1 sales exceeded expectations.");
$report->addData("Revenue", "$1,000,000");

processDocument($blog);
processDocument($report);
?>

Interfaces vs Abstract Classes: When to Use What

Understanding when to use interfaces versus abstract classes is crucial for good object-oriented design.

Use Interfaces When:

<?php
interface CacheInterface {
    public function get($key);
    public function set($key, $value);
    public function delete($key);
}

// Multiple unrelated implementations
class FileCache implements CacheInterface { /* implementation */ }
class RedisCache implements CacheInterface { /* implementation */ }
class MemoryCache implements CacheInterface { /* implementation */ }
?>

Use Abstract Classes When:

<?php
abstract class Animal {
    protected $name;

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

    // Shared functionality
    public function getName() {
        return $this->name;
    }

    // Contract that children must implement
    abstract public function makeSound();
}

class Dog extends Animal {
    public function makeSound() {
        return "Woof!";
    }
}
?>

Real-World Example: Notification System

Let's build a notification system that demonstrates both interfaces and abstract classes working together.

<?php
interface NotificationInterface {
    public function send($recipient, $message);
    public function isDelivered($notificationId);
}

abstract class BaseNotification implements NotificationInterface {
    protected $sender;
    protected $timestamp;

    public function __construct($sender) {
        $this->sender = $sender;
        $this->timestamp = time();
    }

    protected function formatMessage($message) {
        $date = date('Y-m-d H:i:s', $this->timestamp);
        return "[$date] From {$this->sender}: $message";
    }

    protected function logNotification($type, $recipient) {
        echo "Logged: $type notification sent to $recipient\n";
    }
}
?>

Here's an email notification implementation:

<?php
class EmailNotification extends BaseNotification {
    public function send($recipient, $message) {
        $formattedMessage = $this->formatMessage($message);

        // Simulate email sending
        echo "Email sent to $recipient: $formattedMessage\n";
        $this->logNotification('Email', $recipient);

        return 'email_' . uniqid();
    }

    public function isDelivered($notificationId) {
        // Simulate checking email delivery status
        return true;
    }
}
?>

And an SMS notification implementation:

<?php
class SMSNotification extends BaseNotification {
    public function send($recipient, $message) {
        $formattedMessage = $this->formatMessage($message);

        // Simulate SMS sending
        echo "SMS sent to $recipient: $formattedMessage\n";
        $this->logNotification('SMS', $recipient);

        return 'sms_' . uniqid();
    }

    public function isDelivered($notificationId) {
        // Simulate checking SMS delivery status
        return true;
    }
}
?>

The notification manager works with any notification type:

<?php
class NotificationManager {
    private $notifications = [];

    public function addNotifier(NotificationInterface $notifier) {
        $this->notifications[] = $notifier;
    }

    public function sendToAll($recipient, $message) {
        foreach ($this->notifications as $notifier) {
            $id = $notifier->send($recipient, $message);
            echo "Notification ID: $id\n";
        }
    }
}

$manager = new NotificationManager();
$manager->addNotifier(new EmailNotification("System"));
$manager->addNotifier(new SMSNotification("System"));

$manager->sendToAll("[email protected]", "Your order has been shipped!");
?>

Key Interface Design Principles

When designing interfaces, follow these principles for maximum effectiveness:

Keep interfaces focused: Each interface should have a single, clear purpose.

<?php
// Good: Focused interface
interface UserAuthenticator {
    public function authenticate($credentials);
    public function isAuthenticated();
}

// Avoid: Interface trying to do too much
interface UserManager {
    public function authenticate($credentials);
    public function sendEmail($message);
    public function calculateSalary();
    public function generateReport();
}
?>

Use descriptive names: Interface names should clearly indicate their purpose.

<?php
// Good: Clear purpose
interface PaymentProcessor { }
interface DataValidator { }
interface FileUploader { }

// Avoid: Vague names
interface Manager { }
interface Handler { }
interface Service { }
?>

Key Takeaways

Interfaces define pure contracts that specify what methods classes must implement without providing any implementation details. They enable multiple inheritance-like behavior and create flexible, loosely coupled designs.

Abstract classes combine contract enforcement with code sharing, providing both abstract methods that child classes must implement and concrete methods that provide shared functionality. They're perfect for related classes that share common behavior.

Use interfaces when you need to define contracts for unrelated classes or when you want maximum flexibility. Use abstract classes when you have a family of related classes that share significant common functionality.

Both interfaces and abstract classes enable polymorphism—the ability to treat different classes uniformly based on their shared contracts. This creates code that's flexible, maintainable, and easy to extend.

These concepts are fundamental to modern PHP development and are used extensively in frameworks, libraries, and professional applications. Mastering them opens the door to sophisticated architectural patterns and clean, maintainable code.

In the next lesson, we'll explore traits and code reusability, learning how to share functionality across unrelated classes without inheritance.