1. php
  2. /object oriented
  3. /abstract-classes

PHP Abstract Classes

Introduction to Abstract Classes

Abstract classes in PHP provide a way to define base classes that cannot be instantiated directly but serve as blueprints for derived classes. They allow you to define common functionality while requiring subclasses to implement specific methods, creating a contract that ensures consistent behavior across related classes.

Understanding abstract classes is essential for creating well-structured object-oriented designs that promote code reuse, enforce consistency, and provide clear inheritance hierarchies.

Why Abstract Classes Matter

Design Contracts: Abstract classes define what methods subclasses must implement, ensuring consistent interfaces across related classes.

Code Reuse: Common functionality can be implemented once in the abstract class and inherited by all subclasses.

Template Method Pattern: Abstract classes enable the template method pattern, where the algorithm structure is defined but specific steps are implemented by subclasses.

Polymorphism: Abstract classes provide a common type that can be used polymorphically with different concrete implementations.

Preventing Instantiation: Abstract classes prevent direct instantiation of incomplete or conceptual classes that should only serve as base classes.

Key Concepts

Abstract Classes: Classes declared with the abstract keyword that cannot be instantiated directly.

Abstract Methods: Methods declared without implementation that must be implemented by concrete subclasses.

Concrete Methods: Fully implemented methods in abstract classes that provide common functionality.

Inheritance Hierarchy: Abstract classes form the foundation of inheritance hierarchies with clear contracts and shared behavior.

Basic Abstract Classes

Creating and Using Abstract Classes

<?php
/**
 * Basic Abstract Class Implementation
 * 
 * Abstract classes provide blueprints for related classes
 * with shared functionality and enforced contracts.
 */

/**
 * Abstract Vehicle class defining common vehicle behavior
 */
abstract class Vehicle
{
    protected string $make;
    protected string $model;
    protected int $year;
    protected float $fuelLevel = 0.0;
    protected bool $isRunning = false;
    
    public function __construct(string $make, string $model, int $year)
    {
        $this->make = $make;
        $this->model = $model;
        $this->year = $year;
    }
    
    /**
     * Concrete method - implemented in abstract class
     */
    public function getInfo(): string
    {
        return "{$this->year} {$this->make} {$this->model}";
    }
    
    /**
     * Concrete method with shared logic
     */
    public function refuel(float $amount): void
    {
        if ($amount <= 0) {
            throw new InvalidArgumentException('Fuel amount must be positive');
        }
        
        $this->fuelLevel += $amount;
        echo "Refueled {$this->getInfo()} with {$amount} units\n";
    }
    
    /**
     * Concrete method that uses abstract method
     */
    public function start(): void
    {
        if ($this->fuelLevel <= 0) {
            throw new RuntimeException('Cannot start: no fuel');
        }
        
        if ($this->isRunning) {
            echo "{$this->getInfo()} is already running\n";
            return;
        }
        
        $this->performStartSequence();
        $this->isRunning = true;
        echo "{$this->getInfo()} started successfully\n";
    }
    
    public function stop(): void
    {
        if (!$this->isRunning) {
            echo "{$this->getInfo()} is already stopped\n";
            return;
        }
        
        $this->isRunning = false;
        echo "{$this->getInfo()} stopped\n";
    }
    
    /**
     * Abstract method - must be implemented by subclasses
     */
    abstract protected function performStartSequence(): void;
    
    /**
     * Abstract method - each vehicle type moves differently
     */
    abstract public function move(float $distance): void;
    
    /**
     * Abstract method - different fuel consumption rates
     */
    abstract public function getFuelConsumption(): float;
    
    // Getters
    public function getMake(): string { return $this->make; }
    public function getModel(): string { return $this->model; }
    public function getYear(): int { return $this->year; }
    public function getFuelLevel(): float { return $this->fuelLevel; }
    public function isRunning(): bool { return $this->isRunning; }
}

/**
 * Concrete Car class implementing abstract methods
 */
class Car extends Vehicle
{
    private int $doors;
    private string $transmission;
    
    public function __construct(string $make, string $model, int $year, int $doors = 4, string $transmission = 'automatic')
    {
        parent::__construct($make, $model, $year);
        $this->doors = $doors;
        $this->transmission = $transmission;
    }
    
    /**
     * Implementation of abstract method
     */
    protected function performStartSequence(): void
    {
        echo "Turning ignition key for {$this->getInfo()}\n";
        echo "Engine warming up...\n";
    }
    
    /**
     * Implementation of abstract method
     */
    public function move(float $distance): void
    {
        if (!$this->isRunning) {
            throw new RuntimeException('Cannot move: vehicle not running');
        }
        
        $fuelNeeded = $distance * $this->getFuelConsumption();
        
        if ($this->fuelLevel < $fuelNeeded) {
            throw new RuntimeException('Insufficient fuel for distance');
        }
        
        $this->fuelLevel -= $fuelNeeded;
        echo "Car drove {$distance} km, fuel remaining: {$this->fuelLevel}\n";
    }
    
    /**
     * Implementation of abstract method
     */
    public function getFuelConsumption(): float
    {
        // Cars consume 0.08 fuel units per km
        return 0.08;
    }
    
    /**
     * Car-specific method
     */
    public function openTrunk(): void
    {
        echo "Trunk opened\n";
    }
    
    public function getDoors(): int { return $this->doors; }
    public function getTransmission(): string { return $this->transmission; }
}

/**
 * Concrete Motorcycle class implementing abstract methods
 */
class Motorcycle extends Vehicle
{
    private int $engineCC;
    private bool $hasSidecar;
    
    public function __construct(string $make, string $model, int $year, int $engineCC, bool $hasSidecar = false)
    {
        parent::__construct($make, $model, $year);
        $this->engineCC = $engineCC;
        $this->hasSidecar = $hasSidecar;
    }
    
    /**
     * Implementation of abstract method
     */
    protected function performStartSequence(): void
    {
        echo "Kickstarting {$this->getInfo()}\n";
        echo "Engine revving...\n";
    }
    
    /**
     * Implementation of abstract method
     */
    public function move(float $distance): void
    {
        if (!$this->isRunning) {
            throw new RuntimeException('Cannot move: motorcycle not running');
        }
        
        $fuelNeeded = $distance * $this->getFuelConsumption();
        
        if ($this->fuelLevel < $fuelNeeded) {
            throw new RuntimeException('Insufficient fuel for distance');
        }
        
        $this->fuelLevel -= $fuelNeeded;
        echo "Motorcycle rode {$distance} km, fuel remaining: {$this->fuelLevel}\n";
    }
    
    /**
     * Implementation of abstract method
     */
    public function getFuelConsumption(): float
    {
        // Base consumption: 0.04 fuel units per km
        $consumption = 0.04;
        
        // Larger engines consume more
        if ($this->engineCC > 600) {
            $consumption *= 1.5;
        }
        
        // Sidecar increases consumption
        if ($this->hasSidecar) {
            $consumption *= 1.3;
        }
        
        return $consumption;
    }
    
    /**
     * Motorcycle-specific method
     */
    public function wheelie(): void
    {
        if (!$this->isRunning) {
            throw new RuntimeException('Cannot perform wheelie: motorcycle not running');
        }
        echo "Performing wheelie!\n";
    }
    
    public function getEngineCC(): int { return $this->engineCC; }
    public function hasSidecar(): bool { return $this->hasSidecar; }
}

// Usage examples
echo "=== Abstract Class Examples ===\n";

// Cannot instantiate abstract class
// $vehicle = new Vehicle('Generic', 'Vehicle', 2024); // This would cause an error

// Create concrete vehicles
$car = new Car('Toyota', 'Camry', 2024, 4, 'automatic');
$motorcycle = new Motorcycle('Harley-Davidson', 'Street 750', 2024, 750, false);

// Use inherited methods
echo "Car: " . $car->getInfo() . "\n";
echo "Motorcycle: " . $motorcycle->getInfo() . "\n";

// Refuel vehicles
$car->refuel(50.0);
$motorcycle->refuel(15.0);

// Start and move vehicles
$car->start();
$car->move(100.0);

$motorcycle->start();
$motorcycle->move(80.0);
$motorcycle->wheelie();

// Polymorphic usage
function displayVehicleInfo(Vehicle $vehicle): void
{
    echo "Vehicle Info: " . $vehicle->getInfo() . "\n";
    echo "Fuel Level: " . $vehicle->getFuelLevel() . "\n";
    echo "Fuel Consumption: " . $vehicle->getFuelConsumption() . " units/km\n";
    echo "Running: " . ($vehicle->isRunning() ? 'Yes' : 'No') . "\n\n";
}

displayVehicleInfo($car);
displayVehicleInfo($motorcycle);
?>

Template Method Pattern

Implementing Template Method with Abstract Classes

<?php
/**
 * Template Method Pattern Implementation
 * 
 * The template method pattern defines the skeleton of an algorithm
 * in an abstract class and lets subclasses override specific steps.
 */

/**
 * Abstract data processor defining the processing template
 */
abstract class DataProcessor
{
    protected array $data = [];
    protected array $errors = [];
    
    /**
     * Template method defining the algorithm structure
     * This method cannot be overridden by subclasses
     */
    final public function process(array $rawData): array
    {
        $this->validateInput($rawData);
        $this->loadData($rawData);
        $this->preprocessData();
        $this->transformData();
        $this->validateOutput();
        $this->saveResults();
        
        return $this->getResults();
    }
    
    /**
     * Concrete method with common validation
     */
    protected function validateInput(array $rawData): void
    {
        if (empty($rawData)) {
            throw new InvalidArgumentException('Input data cannot be empty');
        }
        
        echo "Input validation passed\n";
    }
    
    /**
     * Concrete method with common data loading
     */
    protected function loadData(array $rawData): void
    {
        $this->data = $rawData;
        echo "Data loaded: " . count($this->data) . " records\n";
    }
    
    /**
     * Abstract method - preprocessing varies by data type
     */
    abstract protected function preprocessData(): void;
    
    /**
     * Abstract method - transformation logic varies by processor type
     */
    abstract protected function transformData(): void;
    
    /**
     * Hook method - can be overridden but has default implementation
     */
    protected function validateOutput(): void
    {
        if (empty($this->data)) {
            $this->errors[] = 'No data after processing';
        }
        
        echo "Output validation completed\n";
    }
    
    /**
     * Abstract method - saving mechanism varies
     */
    abstract protected function saveResults(): void;
    
    /**
     * Concrete method for result retrieval
     */
    protected function getResults(): array
    {
        return [
            'data' => $this->data,
            'errors' => $this->errors,
            'processed_at' => date('Y-m-d H:i:s')
        ];
    }
}

/**
 * Concrete processor for user data
 */
class UserDataProcessor extends DataProcessor
{
    /**
     * User-specific preprocessing
     */
    protected function preprocessData(): void
    {
        foreach ($this->data as &$record) {
            // Normalize email addresses
            if (isset($record['email'])) {
                $record['email'] = strtolower(trim($record['email']));
            }
            
            // Standardize phone numbers
            if (isset($record['phone'])) {
                $record['phone'] = preg_replace('/[^\d]/', '', $record['phone']);
            }
            
            // Trim names
            if (isset($record['name'])) {
                $record['name'] = trim($record['name']);
            }
        }
        
        echo "User data preprocessed\n";
    }
    
    /**
     * User-specific transformation
     */
    protected function transformData(): void
    {
        foreach ($this->data as &$record) {
            // Add full name if first and last name exist
            if (isset($record['first_name']) && isset($record['last_name'])) {
                $record['full_name'] = $record['first_name'] . ' ' . $record['last_name'];
            }
            
            // Calculate age from birth date
            if (isset($record['birth_date'])) {
                $birthDate = new DateTime($record['birth_date']);
                $today = new DateTime();
                $record['age'] = $today->diff($birthDate)->y;
            }
            
            // Validate email format
            if (isset($record['email']) && !filter_var($record['email'], FILTER_VALIDATE_EMAIL)) {
                $this->errors[] = "Invalid email: {$record['email']}";
            }
        }
        
        echo "User data transformed\n";
    }
    
    /**
     * Override validation for user-specific rules
     */
    protected function validateOutput(): void
    {
        parent::validateOutput();
        
        foreach ($this->data as $record) {
            if (!isset($record['email']) || !isset($record['name'])) {
                $this->errors[] = 'Missing required fields in user record';
            }
        }
        
        echo "User-specific validation completed\n";
    }
    
    /**
     * User-specific saving logic
     */
    protected function saveResults(): void
    {
        // Simulate saving to user database
        echo "Saving " . count($this->data) . " user records to database\n";
        
        foreach ($this->data as $record) {
            if (isset($record['email'])) {
                echo "Saved user: {$record['email']}\n";
            }
        }
    }
}

/**
 * Concrete processor for product data
 */
class ProductDataProcessor extends DataProcessor
{
    /**
     * Product-specific preprocessing
     */
    protected function preprocessData(): void
    {
        foreach ($this->data as &$record) {
            // Normalize product names
            if (isset($record['name'])) {
                $record['name'] = ucwords(strtolower(trim($record['name'])));
            }
            
            // Ensure price is numeric
            if (isset($record['price'])) {
                $record['price'] = (float) $record['price'];
            }
            
            // Normalize SKU
            if (isset($record['sku'])) {
                $record['sku'] = strtoupper(trim($record['sku']));
            }
        }
        
        echo "Product data preprocessed\n";
    }
    
    /**
     * Product-specific transformation
     */
    protected function transformData(): void
    {
        foreach ($this->data as &$record) {
            // Calculate discounted price
            if (isset($record['price']) && isset($record['discount_percent'])) {
                $discountAmount = $record['price'] * ($record['discount_percent'] / 100);
                $record['discounted_price'] = $record['price'] - $discountAmount;
            }
            
            // Generate slug from name
            if (isset($record['name'])) {
                $record['slug'] = strtolower(preg_replace('/[^a-zA-Z0-9]/', '-', $record['name']));
                $record['slug'] = preg_replace('/-+/', '-', trim($record['slug'], '-'));
            }
            
            // Validate price
            if (isset($record['price']) && $record['price'] <= 0) {
                $this->errors[] = "Invalid price for product: {$record['name']}";
            }
        }
        
        echo "Product data transformed\n";
    }
    
    /**
     * Product-specific saving logic
     */
    protected function saveResults(): void
    {
        // Simulate saving to product catalog
        echo "Saving " . count($this->data) . " products to catalog\n";
        
        foreach ($this->data as $record) {
            if (isset($record['sku'])) {
                echo "Saved product: {$record['sku']} - {$record['name']}\n";
            }
        }
    }
}

// Usage examples
echo "\n=== Template Method Pattern Examples ===\n";

// Process user data
$userProcessor = new UserDataProcessor();
$userData = [
    [
        'first_name' => 'john',
        'last_name' => 'doe',
        'email' => ' [email protected] ',
        'phone' => '(555) 123-4567',
        'birth_date' => '1990-01-15'
    ],
    [
        'first_name' => 'jane',
        'last_name' => 'smith',
        'email' => '[email protected]',
        'phone' => '555.987.6543',
        'birth_date' => '1985-05-20'
    ]
];

echo "Processing user data:\n";
$userResults = $userProcessor->process($userData);

echo "\nUser processing results:\n";
print_r($userResults);

// Process product data
$productProcessor = new ProductDataProcessor();
$productData = [
    [
        'name' => ' wireless headphones ',
        'price' => '99.99',
        'sku' => 'wh-001',
        'discount_percent' => 10
    ],
    [
        'name' => 'BLUETOOTH SPEAKER',
        'price' => '49.99',
        'sku' => ' bs-002 ',
        'discount_percent' => 15
    ]
];

echo "\n" . str_repeat('-', 50) . "\n";
echo "Processing product data:\n";
$productResults = $productProcessor->process($productData);

echo "\nProduct processing results:\n";
print_r($productResults);
?>

Advanced Abstract Class Patterns

Abstract Factory Pattern and Complex Hierarchies

<?php
/**
 * Advanced Abstract Class Patterns
 * 
 * Complex inheritance hierarchies and factory patterns
 * using abstract classes for structured design.
 */

/**
 * Abstract notification system
 */
abstract class NotificationChannel
{
    protected array $config;
    protected array $recipients = [];
    
    public function __construct(array $config = [])
    {
        $this->config = $config;
        $this->validateConfig();
    }
    
    /**
     * Template method for sending notifications
     */
    final public function send(string $message, array $recipients = []): bool
    {
        if (!empty($recipients)) {
            $this->setRecipients($recipients);
        }
        
        if (empty($this->recipients)) {
            throw new RuntimeException('No recipients specified');
        }
        
        $formattedMessage = $this->formatMessage($message);
        $this->validateMessage($formattedMessage);
        
        $success = $this->deliverMessage($formattedMessage);
        $this->logDelivery($success);
        
        return $success;
    }
    
    /**
     * Abstract method for configuration validation
     */
    abstract protected function validateConfig(): void;
    
    /**
     * Abstract method for message formatting
     */
    abstract protected function formatMessage(string $message): string;
    
    /**
     * Abstract method for message delivery
     */
    abstract protected function deliverMessage(string $message): bool;
    
    /**
     * Hook method for message validation
     */
    protected function validateMessage(string $message): void
    {
        if (empty($message)) {
            throw new InvalidArgumentException('Message cannot be empty');
        }
    }
    
    /**
     * Concrete method for recipient management
     */
    public function setRecipients(array $recipients): void
    {
        $this->recipients = array_filter($recipients, [$this, 'isValidRecipient']);
    }
    
    /**
     * Abstract method for recipient validation
     */
    abstract protected function isValidRecipient($recipient): bool;
    
    /**
     * Concrete method for logging
     */
    protected function logDelivery(bool $success): void
    {
        $status = $success ? 'SUCCESS' : 'FAILED';
        $timestamp = date('Y-m-d H:i:s');
        $channelType = static::class;
        
        echo "[$timestamp] $channelType: $status\n";
    }
}

/**
 * Email notification implementation
 */
class EmailNotification extends NotificationChannel
{
    protected function validateConfig(): void
    {
        $required = ['smtp_host', 'smtp_port', 'username', 'password'];
        
        foreach ($required as $key) {
            if (!isset($this->config[$key])) {
                throw new InvalidArgumentException("Missing email config: $key");
            }
        }
    }
    
    protected function formatMessage(string $message): string
    {
        $html = "<html><body>";
        $html .= "<h2>Notification</h2>";
        $html .= "<p>" . nl2br(htmlspecialchars($message)) . "</p>";
        $html .= "<p><small>Sent via automated system</small></p>";
        $html .= "</body></html>";
        
        return $html;
    }
    
    protected function deliverMessage(string $message): bool
    {
        // Simulate email sending
        foreach ($this->recipients as $email) {
            echo "Sending email to: $email\n";
            // In real implementation, use PHPMailer or similar
        }
        
        return true;
    }
    
    protected function isValidRecipient($recipient): bool
    {
        return filter_var($recipient, FILTER_VALIDATE_EMAIL) !== false;
    }
    
    protected function validateMessage(string $message): void
    {
        parent::validateMessage($message);
        
        if (strlen($message) > 100000) {
            throw new InvalidArgumentException('Email message too long');
        }
    }
}

/**
 * SMS notification implementation
 */
class SmsNotification extends NotificationChannel
{
    protected function validateConfig(): void
    {
        $required = ['api_key', 'sender_id'];
        
        foreach ($required as $key) {
            if (!isset($this->config[$key])) {
                throw new InvalidArgumentException("Missing SMS config: $key");
            }
        }
    }
    
    protected function formatMessage(string $message): string
    {
        // SMS has character limits
        if (strlen($message) > 160) {
            $message = substr($message, 0, 157) . '...';
        }
        
        return $message;
    }
    
    protected function deliverMessage(string $message): bool
    {
        // Simulate SMS sending
        foreach ($this->recipients as $phone) {
            echo "Sending SMS to: $phone\n";
            // In real implementation, use Twilio or similar API
        }
        
        return true;
    }
    
    protected function isValidRecipient($recipient): bool
    {
        // Basic phone number validation
        return preg_match('/^\+?[1-9]\d{1,14}$/', $recipient);
    }
    
    protected function validateMessage(string $message): void
    {
        parent::validateMessage($message);
        
        // SMS-specific validation
        if (strlen($message) > 160) {
            echo "Warning: SMS message will be truncated\n";
        }
    }
}

/**
 * Push notification implementation
 */
class PushNotification extends NotificationChannel
{
    protected function validateConfig(): void
    {
        $required = ['app_id', 'api_key'];
        
        foreach ($required as $key) {
            if (!isset($this->config[$key])) {
                throw new InvalidArgumentException("Missing push config: $key");
            }
        }
    }
    
    protected function formatMessage(string $message): string
    {
        return json_encode([
            'title' => 'Notification',
            'body' => $message,
            'timestamp' => time(),
            'priority' => 'high'
        ]);
    }
    
    protected function deliverMessage(string $message): bool
    {
        // Simulate push notification
        foreach ($this->recipients as $deviceToken) {
            echo "Sending push notification to device: " . substr($deviceToken, 0, 10) . "...\n";
            // In real implementation, use Firebase Cloud Messaging or similar
        }
        
        return true;
    }
    
    protected function isValidRecipient($recipient): bool
    {
        // Basic device token validation (simplified)
        return strlen($recipient) > 20 && ctype_alnum($recipient);
    }
}

/**
 * Abstract notification factory
 */
abstract class NotificationFactory
{
    /**
     * Factory method for creating notification channels
     */
    abstract public function createChannel(array $config): NotificationChannel;
    
    /**
     * Template method for sending notifications
     */
    public function sendNotification(string $type, string $message, array $recipients, array $config = []): bool
    {
        $channel = $this->createChannel($config);
        return $channel->send($message, $recipients);
    }
}

/**
 * Concrete email factory
 */
class EmailNotificationFactory extends NotificationFactory
{
    public function createChannel(array $config): NotificationChannel
    {
        $defaultConfig = [
            'smtp_host' => 'smtp.example.com',
            'smtp_port' => 587,
            'username' => '[email protected]',
            'password' => 'secret'
        ];
        
        return new EmailNotification(array_merge($defaultConfig, $config));
    }
}

/**
 * Concrete SMS factory
 */
class SmsNotificationFactory extends NotificationFactory
{
    public function createChannel(array $config): NotificationChannel
    {
        $defaultConfig = [
            'api_key' => 'sms_api_key',
            'sender_id' => 'MyApp'
        ];
        
        return new SmsNotification(array_merge($defaultConfig, $config));
    }
}

// Usage examples
echo "\n=== Advanced Abstract Class Examples ===\n";

// Email notifications
$emailFactory = new EmailNotificationFactory();
$emailNotification = $emailFactory->createChannel([]);

$emailRecipients = ['[email protected]', '[email protected]', 'invalid-email'];
$emailNotification->send('Welcome to our service!', $emailRecipients);

// SMS notifications
$smsFactory = new SmsNotificationFactory();
$smsNotification = $smsFactory->createChannel([]);

$smsRecipients = ['+1234567890', '+0987654321', 'invalid-phone'];
$smsNotification->send('Your verification code is: 123456', $smsRecipients);

// Push notifications
$pushNotification = new PushNotification([
    'app_id' => 'myapp123',
    'api_key' => 'push_api_key'
]);

$deviceTokens = ['abcdef1234567890abcdef1234567890', 'fedcba0987654321fedcba0987654321'];
$pushNotification->send('You have a new message!', $deviceTokens);

// Polymorphic usage
function broadcastMessage(string $message, array $channels): void
{
    foreach ($channels as $channel) {
        if ($channel instanceof NotificationChannel) {
            try {
                $channel->send($message, []);
            } catch (Exception $e) {
                echo "Failed to send via " . get_class($channel) . ": " . $e->getMessage() . "\n";
            }
        }
    }
}

// Would need to set recipients first in real usage
echo "\nBroadcasting message across channels:\n";
// broadcastMessage('System maintenance tonight at 2 AM', [$emailNotification, $smsNotification, $pushNotification]);
?>

For more PHP object-oriented programming topics:

Summary

Abstract classes provide powerful tools for creating structured object-oriented designs:

Design Contracts: Define common interfaces while requiring specific implementations from subclasses for consistent behavior.

Code Reuse: Implement shared functionality once in abstract classes and inherit across multiple concrete implementations.

Template Method Pattern: Define algorithm structure in abstract classes while allowing subclasses to customize specific steps.

Inheritance Hierarchies: Create clear, well-structured inheritance trees with abstract base classes defining common behavior.

Factory Patterns: Use abstract classes as foundations for factory patterns that create related object families.

Best Practices: Balance abstract and concrete methods, use final methods to prevent overriding critical logic, and provide clear documentation for abstract method requirements.

Abstract classes enable sophisticated object-oriented designs that promote maintainability, consistency, and proper separation of concerns in PHP applications.