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]);
?>
Related Topics
For more PHP object-oriented programming topics:
- PHP Classes and Objects - Basic OOP fundamentals
- PHP Inheritance - Class inheritance patterns
- PHP Interfaces and Traits - Contracts and mixins
- PHP Magic Methods - Special method behaviors
- PHP Design Patterns - Common design patterns
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.