PHP Polymorphism
Introduction to Polymorphism
Polymorphism is one of the fundamental principles of object-oriented programming that allows objects of different types to be treated as instances of the same type through a common interface. In PHP, polymorphism enables you to write more flexible, maintainable code by allowing different classes to implement the same interface or extend the same base class while providing their own specific implementations.
Understanding polymorphism is essential for creating scalable, extensible applications that can handle new requirements without modifying existing code.
Why Polymorphism Matters
Code Flexibility: Polymorphism allows you to write code that works with different object types without knowing their specific implementation details.
Maintainability: New classes can be added without modifying existing code, adhering to the open/closed principle.
Code Reusability: Common functionality can be defined once and reused across different implementations.
Design Patterns: Many design patterns rely on polymorphism to provide flexible, interchangeable components.
Testing: Polymorphism enables easier unit testing through dependency injection and mock objects.
Types of Polymorphism in PHP
Interface Polymorphism: Multiple classes implementing the same interface with different behaviors.
Inheritance Polymorphism: Subclasses overriding parent class methods to provide specialized behavior.
Method Overriding: Child classes providing specific implementations of parent class methods.
Late Static Binding: Polymorphic behavior with static methods and properties.
Duck Typing: PHP's dynamic nature allows polymorphic behavior based on method availability.
Interface Polymorphism
Common Interfaces with Different Implementations
<?php
/**
* Interface Polymorphism Examples
*
* Different classes implementing the same interface
* to provide polymorphic behavior.
*/
/**
* Common interface for data storage
*/
interface StorageInterface
{
public function save(string $key, $data): bool;
public function load(string $key);
public function exists(string $key): bool;
public function delete(string $key): bool;
public function clear(): bool;
}
/**
* File-based storage implementation
*/
class FileStorage implements StorageInterface
{
private string $directory;
public function __construct(string $directory = 'storage')
{
$this->directory = rtrim($directory, '/');
if (!is_dir($this->directory)) {
mkdir($this->directory, 0755, true);
}
}
public function save(string $key, $data): bool
{
$filename = $this->getFilename($key);
$serializedData = serialize($data);
return file_put_contents($filename, $serializedData, LOCK_EX) !== false;
}
public function load(string $key)
{
$filename = $this->getFilename($key);
if (!file_exists($filename)) {
return null;
}
$data = file_get_contents($filename);
return $data !== false ? unserialize($data) : null;
}
public function exists(string $key): bool
{
return file_exists($this->getFilename($key));
}
public function delete(string $key): bool
{
$filename = $this->getFilename($key);
if (file_exists($filename)) {
return unlink($filename);
}
return true;
}
public function clear(): bool
{
$files = glob($this->directory . '/*.dat');
foreach ($files as $file) {
if (!unlink($file)) {
return false;
}
}
return true;
}
private function getFilename(string $key): string
{
return $this->directory . '/' . md5($key) . '.dat';
}
}
/**
* Memory-based storage implementation
*/
class MemoryStorage implements StorageInterface
{
private array $data = [];
public function save(string $key, $data): bool
{
$this->data[$key] = $data;
return true;
}
public function load(string $key)
{
return $this->data[$key] ?? null;
}
public function exists(string $key): bool
{
return array_key_exists($key, $this->data);
}
public function delete(string $key): bool
{
unset($this->data[$key]);
return true;
}
public function clear(): bool
{
$this->data = [];
return true;
}
}
/**
* Database storage implementation
*/
class DatabaseStorage implements StorageInterface
{
private PDO $pdo;
private string $table;
public function __construct(PDO $pdo, string $table = 'storage')
{
$this->pdo = $pdo;
$this->table = $table;
$this->initializeTable();
}
public function save(string $key, $data): bool
{
$serializedData = serialize($data);
$sql = "INSERT INTO {$this->table} (storage_key, storage_value, created_at)
VALUES (?, ?, NOW())
ON DUPLICATE KEY UPDATE
storage_value = VALUES(storage_value),
updated_at = NOW()";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([$key, $serializedData]);
}
public function load(string $key)
{
$sql = "SELECT storage_value FROM {$this->table} WHERE storage_key = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$key]);
$result = $stmt->fetchColumn();
return $result !== false ? unserialize($result) : null;
}
public function exists(string $key): bool
{
$sql = "SELECT COUNT(*) FROM {$this->table} WHERE storage_key = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$key]);
return $stmt->fetchColumn() > 0;
}
public function delete(string $key): bool
{
$sql = "DELETE FROM {$this->table} WHERE storage_key = ?";
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([$key]);
}
public function clear(): bool
{
$sql = "DELETE FROM {$this->table}";
return $this->pdo->exec($sql) !== false;
}
private function initializeTable(): void
{
$sql = "CREATE TABLE IF NOT EXISTS {$this->table} (
storage_key VARCHAR(255) PRIMARY KEY,
storage_value LONGTEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)";
$this->pdo->exec($sql);
}
}
/**
* Cache manager that works with any storage implementation
*/
class CacheManager
{
private StorageInterface $storage;
private int $defaultTtl;
public function __construct(StorageInterface $storage, int $defaultTtl = 3600)
{
$this->storage = $storage;
$this->defaultTtl = $defaultTtl;
}
public function get(string $key)
{
$cacheKey = $this->getCacheKey($key);
$data = $this->storage->load($cacheKey);
if ($data === null) {
return null;
}
// Check if data has expired
if (isset($data['expires_at']) && $data['expires_at'] < time()) {
$this->storage->delete($cacheKey);
return null;
}
return $data['value'] ?? null;
}
public function set(string $key, $value, int $ttl = null): bool
{
$ttl = $ttl ?? $this->defaultTtl;
$cacheKey = $this->getCacheKey($key);
$data = [
'value' => $value,
'created_at' => time(),
'expires_at' => time() + $ttl
];
return $this->storage->save($cacheKey, $data);
}
public function delete(string $key): bool
{
return $this->storage->delete($this->getCacheKey($key));
}
public function clear(): bool
{
return $this->storage->clear();
}
private function getCacheKey(string $key): string
{
return 'cache:' . $key;
}
}
// Usage examples demonstrating polymorphism
echo "=== Interface Polymorphism Examples ===\n";
// All these storage implementations can be used interchangeably
$storageTypes = [
'file' => new FileStorage('temp_storage'),
'memory' => new MemoryStorage(),
// 'database' => new DatabaseStorage($pdo) // Requires PDO instance
];
foreach ($storageTypes as $type => $storage) {
echo "\nTesting $type storage:\n";
// Same interface, different implementations
$storage->save('user:123', ['name' => 'John Doe', 'email' => '[email protected]']);
$storage->save('config:theme', 'dark');
echo "User data: " . json_encode($storage->load('user:123')) . "\n";
echo "Theme: " . $storage->load('config:theme') . "\n";
echo "User exists: " . ($storage->exists('user:123') ? 'yes' : 'no') . "\n";
// Cache manager works with any storage implementation
$cache = new CacheManager($storage, 60);
$cache->set('cached_data', 'This is cached content');
echo "Cached data: " . $cache->get('cached_data') . "\n";
$storage->clear();
}
/**
* Shape polymorphism example
*/
interface ShapeInterface
{
public function getArea(): float;
public function getPerimeter(): float;
public function draw(): string;
}
class Circle implements ShapeInterface
{
public function __construct(private float $radius) {}
public function getArea(): float
{
return pi() * $this->radius ** 2;
}
public function getPerimeter(): float
{
return 2 * pi() * $this->radius;
}
public function draw(): string
{
return "Drawing a circle with radius {$this->radius}";
}
}
class Rectangle implements ShapeInterface
{
public function __construct(private float $width, private float $height) {}
public function getArea(): float
{
return $this->width * $this->height;
}
public function getPerimeter(): float
{
return 2 * ($this->width + $this->height);
}
public function draw(): string
{
return "Drawing a rectangle {$this->width}x{$this->height}";
}
}
class Triangle implements ShapeInterface
{
public function __construct(private float $a, private float $b, private float $c) {}
public function getArea(): float
{
$s = ($this->a + $this->b + $this->c) / 2;
return sqrt($s * ($s - $this->a) * ($s - $this->b) * ($s - $this->c));
}
public function getPerimeter(): float
{
return $this->a + $this->b + $this->c;
}
public function draw(): string
{
return "Drawing a triangle with sides {$this->a}, {$this->b}, {$this->c}";
}
}
// Polymorphic shape handling
function processShapes(array $shapes): void
{
foreach ($shapes as $shape) {
if ($shape instanceof ShapeInterface) {
echo $shape->draw() . "\n";
echo "Area: " . number_format($shape->getArea(), 2) . "\n";
echo "Perimeter: " . number_format($shape->getPerimeter(), 2) . "\n\n";
}
}
}
$shapes = [
new Circle(5),
new Rectangle(4, 6),
new Triangle(3, 4, 5)
];
echo "\n=== Shape Polymorphism ===\n";
processShapes($shapes);
?>
Method Overriding and Inheritance
Polymorphic Method Behavior
<?php
/**
* Method Overriding and Inheritance Polymorphism
*
* Demonstrating how subclasses can override parent methods
* to provide specialized behavior while maintaining the same interface.
*/
/**
* Base animal class
*/
abstract class Animal
{
protected string $name;
protected int $age;
protected float $weight;
public function __construct(string $name, int $age, float $weight)
{
$this->name = $name;
$this->age = $age;
$this->weight = $weight;
}
/**
* Concrete method that can be overridden
*/
public function eat(float $amount): void
{
echo "{$this->name} is eating {$amount}kg of food\n";
$this->weight += $amount * 0.1; // Default weight gain
}
/**
* Concrete method using template method pattern
*/
final public function sleep(): void
{
echo "{$this->name} is going to sleep\n";
$this->performSleepBehavior();
echo "{$this->name} is now sleeping\n";
}
/**
* Abstract method - must be implemented by subclasses
*/
abstract public function makeSound(): string;
/**
* Abstract method for sleep behavior
*/
abstract protected function performSleepBehavior(): void;
/**
* Concrete method that may be overridden
*/
public function getDescription(): string
{
return "{$this->name} is a {$this->age} year old animal weighing {$this->weight}kg";
}
// Getters
public function getName(): string { return $this->name; }
public function getAge(): int { return $this->age; }
public function getWeight(): float { return $this->weight; }
}
/**
* Dog implementation with specific behaviors
*/
class Dog extends Animal
{
private string $breed;
private bool $isVaccinated;
public function __construct(string $name, int $age, float $weight, string $breed, bool $isVaccinated = true)
{
parent::__construct($name, $age, $weight);
$this->breed = $breed;
$this->isVaccinated = $isVaccinated;
}
/**
* Override abstract method
*/
public function makeSound(): string
{
return "Woof! Woof!";
}
/**
* Override abstract method
*/
protected function performSleepBehavior(): void
{
echo "{$this->name} is turning in circles before lying down\n";
}
/**
* Override eating behavior for dogs
*/
public function eat(float $amount): void
{
echo "{$this->name} the dog is eagerly eating {$amount}kg of dog food\n";
$this->weight += $amount * 0.08; // Dogs gain less weight
}
/**
* Override description to include breed
*/
public function getDescription(): string
{
$baseDescription = parent::getDescription();
$vaccination = $this->isVaccinated ? 'vaccinated' : 'not vaccinated';
return "{$baseDescription} - Breed: {$this->breed} ({$vaccination})";
}
/**
* Dog-specific method
*/
public function bark(): void
{
echo "{$this->name} says: {$this->makeSound()}\n";
}
public function fetch(): void
{
echo "{$this->name} is fetching the ball!\n";
}
}
/**
* Cat implementation with different behaviors
*/
class Cat extends Animal
{
private bool $isIndoor;
private array $favoriteToys;
public function __construct(string $name, int $age, float $weight, bool $isIndoor = true, array $favoriteToys = [])
{
parent::__construct($name, $age, $weight);
$this->isIndoor = $isIndoor;
$this->favoriteToys = $favoriteToys;
}
/**
* Override abstract method
*/
public function makeSound(): string
{
return "Meow";
}
/**
* Override abstract method
*/
protected function performSleepBehavior(): void
{
echo "{$this->name} is finding a warm, sunny spot\n";
}
/**
* Override eating behavior for cats
*/
public function eat(float $amount): void
{
echo "{$this->name} the cat is delicately eating {$amount}kg of cat food\n";
$this->weight += $amount * 0.12; // Cats are more efficient at weight gain
}
/**
* Override description to include indoor status
*/
public function getDescription(): string
{
$baseDescription = parent::getDescription();
$location = $this->isIndoor ? 'indoor' : 'outdoor';
$toys = !empty($this->favoriteToys) ? ' - Favorite toys: ' . implode(', ', $this->favoriteToys) : '';
return "{$baseDescription} - {$location} cat{$toys}";
}
/**
* Cat-specific methods
*/
public function purr(): void
{
echo "{$this->name} is purring contentedly\n";
}
public function climb(): void
{
echo "{$this->name} is climbing up high\n";
}
}
/**
* Bird implementation with flying behavior
*/
class Bird extends Animal
{
private float $wingSpan;
private bool $canFly;
public function __construct(string $name, int $age, float $weight, float $wingSpan, bool $canFly = true)
{
parent::__construct($name, $age, $weight);
$this->wingSpan = $wingSpan;
$this->canFly = $canFly;
}
/**
* Override abstract method
*/
public function makeSound(): string
{
return "Tweet tweet";
}
/**
* Override abstract method
*/
protected function performSleepBehavior(): void
{
echo "{$this->name} is tucking its head under its wing\n";
}
/**
* Override eating behavior for birds
*/
public function eat(float $amount): void
{
echo "{$this->name} the bird is pecking at {$amount}kg of seeds\n";
$this->weight += $amount * 0.15; // Birds have high metabolism
}
/**
* Override description to include flight capability
*/
public function getDescription(): string
{
$baseDescription = parent::getDescription();
$flightStatus = $this->canFly ? 'can fly' : 'flightless';
return "{$baseDescription} - Wingspan: {$this->wingSpan}cm ({$flightStatus})";
}
/**
* Bird-specific methods
*/
public function fly(): void
{
if ($this->canFly) {
echo "{$this->name} is soaring through the sky\n";
} else {
echo "{$this->name} cannot fly\n";
}
}
public function sing(): void
{
echo "{$this->name} sings: {$this->makeSound()}\n";
}
}
// Polymorphic usage examples
echo "\n=== Method Overriding Examples ===\n";
$animals = [
new Dog('Buddy', 3, 25.5, 'Golden Retriever', true),
new Cat('Whiskers', 2, 4.2, true, ['feather wand', 'catnip mouse']),
new Bird('Tweety', 1, 0.5, 15.0, true)
];
// Polymorphic behavior - same method calls, different implementations
foreach ($animals as $animal) {
echo "\n" . str_repeat('-', 40) . "\n";
echo $animal->getDescription() . "\n";
echo "Sound: " . $animal->makeSound() . "\n";
$animal->eat(0.5);
$animal->sleep();
echo "New weight: " . number_format($animal->getWeight(), 1) . "kg\n";
}
/**
* Animal care system demonstrating polymorphism
*/
class AnimalCareSystem
{
private array $animals = [];
public function addAnimal(Animal $animal): void
{
$this->animals[] = $animal;
echo "Added {$animal->getName()} to the care system\n";
}
public function feedAllAnimals(float $amountPerAnimal): void
{
echo "\n=== Feeding Time ===\n";
foreach ($this->animals as $animal) {
$animal->eat($amountPerAnimal);
}
}
public function bedtime(): void
{
echo "\n=== Bedtime ===\n";
foreach ($this->animals as $animal) {
$animal->sleep();
}
}
public function makeAllSounds(): void
{
echo "\n=== Animal Sounds ===\n";
foreach ($this->animals as $animal) {
echo "{$animal->getName()}: {$animal->makeSound()}\n";
}
}
public function getAnimalReport(): string
{
$report = "\n=== Animal Care Report ===\n";
$totalWeight = 0;
foreach ($this->animals as $animal) {
$report .= $animal->getDescription() . "\n";
$totalWeight += $animal->getWeight();
}
$report .= "\nTotal animals: " . count($this->animals) . "\n";
$report .= "Total weight: " . number_format($totalWeight, 1) . "kg\n";
return $report;
}
}
// Usage of polymorphic animal care system
$careSystem = new AnimalCareSystem();
foreach ($animals as $animal) {
$careSystem->addAnimal($animal);
}
$careSystem->makeAllSounds();
$careSystem->feedAllAnimals(0.3);
$careSystem->bedtime();
echo $careSystem->getAnimalReport();
?>
Late Static Binding
Static Polymorphism in PHP
<?php
/**
* Late Static Binding Examples
*
* Demonstrating polymorphic behavior with static methods
* and the 'static' keyword.
*/
/**
* Base model class with static methods
*/
abstract class Model
{
protected static string $table = '';
protected static array $fillable = [];
protected array $attributes = [];
public function __construct(array $attributes = [])
{
$this->fill($attributes);
}
/**
* Static factory method with late static binding
*/
public static function create(array $attributes): static
{
$instance = new static($attributes);
$instance->save();
return $instance;
}
/**
* Static finder method
*/
public static function find(int $id): ?static
{
// Simulate database query
echo "Finding " . static::class . " with ID: $id from table: " . static::$table . "\n";
// Return new instance (simplified)
return new static(['id' => $id]);
}
/**
* Static query builder
*/
public static function where(string $column, $value): static
{
echo "Querying " . static::$table . " where $column = $value\n";
return new static([$column => $value]);
}
/**
* Get table name polymorphically
*/
public static function getTable(): string
{
return static::$table;
}
/**
* Get fillable fields polymorphically
*/
public static function getFillable(): array
{
return static::$fillable;
}
/**
* Instance methods
*/
public function fill(array $attributes): void
{
foreach ($attributes as $key => $value) {
if (in_array($key, static::$fillable) || $key === 'id') {
$this->attributes[$key] = $value;
}
}
}
public function save(): bool
{
$action = isset($this->attributes['id']) ? 'updated' : 'created';
echo static::class . " $action in table: " . static::$table . "\n";
if (!isset($this->attributes['id'])) {
$this->attributes['id'] = rand(1, 1000);
}
return true;
}
public function getAttribute(string $key)
{
return $this->attributes[$key] ?? null;
}
public function setAttribute(string $key, $value): void
{
if (in_array($key, static::$fillable) || $key === 'id') {
$this->attributes[$key] = $value;
}
}
public function toArray(): array
{
return $this->attributes;
}
}
/**
* User model with specific table and fields
*/
class User extends Model
{
protected static string $table = 'users';
protected static array $fillable = ['name', 'email', 'password'];
/**
* User-specific static method
*/
public static function findByEmail(string $email): ?static
{
echo "Finding user by email: $email\n";
return new static(['email' => $email, 'id' => rand(1, 100)]);
}
/**
* User-specific instance method
*/
public function getFullName(): string
{
$firstName = $this->getAttribute('first_name') ?? '';
$lastName = $this->getAttribute('last_name') ?? '';
return trim("$firstName $lastName") ?: $this->getAttribute('name') ?? 'Unknown';
}
}
/**
* Product model with different table and fields
*/
class Product extends Model
{
protected static string $table = 'products';
protected static array $fillable = ['name', 'price', 'description', 'category_id'];
/**
* Product-specific static method
*/
public static function findByCategory(int $categoryId): array
{
echo "Finding products in category: $categoryId\n";
return [
new static(['name' => 'Product 1', 'category_id' => $categoryId]),
new static(['name' => 'Product 2', 'category_id' => $categoryId])
];
}
/**
* Product-specific instance method
*/
public function getFormattedPrice(): string
{
$price = $this->getAttribute('price') ?? 0;
return '$' . number_format($price, 2);
}
}
/**
* Category model
*/
class Category extends Model
{
protected static string $table = 'categories';
protected static array $fillable = ['name', 'description', 'parent_id'];
/**
* Category-specific static method
*/
public static function getRootCategories(): array
{
echo "Finding root categories (parent_id = null)\n";
return [
new static(['name' => 'Electronics', 'id' => 1]),
new static(['name' => 'Clothing', 'id' => 2])
];
}
}
// Late static binding examples
echo "=== Late Static Binding Examples ===\n";
// Static methods demonstrate polymorphism
$models = [User::class, Product::class, Category::class];
foreach ($models as $modelClass) {
echo "\nWorking with $modelClass:\n";
echo "Table: " . $modelClass::getTable() . "\n";
echo "Fillable: " . implode(', ', $modelClass::getFillable()) . "\n";
// Create instance using late static binding
$instance = $modelClass::create(['name' => 'Test Item']);
echo "Created: " . json_encode($instance->toArray()) . "\n";
// Find using late static binding
$found = $modelClass::find(123);
echo "Found: " . json_encode($found->toArray()) . "\n";
}
// Specific model usage
echo "\n=== Model-Specific Usage ===\n";
$user = User::create([
'name' => 'John Doe',
'email' => '[email protected]'
]);
$userByEmail = User::findByEmail('[email protected]');
echo "User full name: " . $userByEmail->getFullName() . "\n";
$product = Product::create([
'name' => 'Laptop',
'price' => 999.99,
'category_id' => 1
]);
echo "Product price: " . $product->getFormattedPrice() . "\n";
$products = Product::findByCategory(1);
echo "Found " . count($products) . " products in category\n";
/**
* Abstract factory with late static binding
*/
abstract class Logger
{
protected static array $instances = [];
/**
* Singleton pattern with late static binding
*/
public static function getInstance(): static
{
$class = static::class;
if (!isset(static::$instances[$class])) {
static::$instances[$class] = new static();
}
return static::$instances[$class];
}
/**
* Factory method for creating loggers
*/
public static function create(string $type): static
{
return match($type) {
'file' => FileLogger::getInstance(),
'database' => DatabaseLogger::getInstance(),
'email' => EmailLogger::getInstance(),
default => throw new InvalidArgumentException("Unknown logger type: $type")
};
}
abstract public function log(string $level, string $message): void;
protected function getLoggerType(): string
{
return substr(static::class, 0, -6); // Remove 'Logger' suffix
}
}
class FileLogger extends Logger
{
public function log(string $level, string $message): void
{
echo "[FILE] [$level] $message\n";
}
}
class DatabaseLogger extends Logger
{
public function log(string $level, string $message): void
{
echo "[DATABASE] [$level] $message\n";
}
}
class EmailLogger extends Logger
{
public function log(string $level, string $message): void
{
echo "[EMAIL] [$level] $message\n";
}
}
// Logger factory with late static binding
echo "\n=== Logger Factory Example ===\n";
$loggerTypes = ['file', 'database', 'email'];
foreach ($loggerTypes as $type) {
$logger = Logger::create($type);
$logger->log('INFO', "Test message from $type logger");
// Verify singleton behavior
$sameLogger = Logger::create($type);
echo "Same instance: " . ($logger === $sameLogger ? 'yes' : 'no') . "\n";
}
?>
Related Topics
For more PHP object-oriented programming topics:
- PHP Interfaces and Traits - Contracts and code reuse
- PHP Abstract Classes - Abstract base classes
- PHP Inheritance - Class inheritance patterns
- PHP Magic Methods - Special method behaviors
- PHP Design Patterns - Common design patterns
Summary
Polymorphism is a fundamental concept that enables flexible, maintainable PHP code:
Interface Polymorphism: Multiple classes implementing the same interface provide consistent behavior with different implementations.
Method Overriding: Subclasses can override parent methods to provide specialized behavior while maintaining the same interface.
Late Static Binding: The static
keyword enables polymorphic behavior with static methods and properties.
Code Flexibility: Polymorphism allows writing code that works with different object types without knowing implementation details.
Design Benefits: Enables design patterns, easier testing, better maintainability, and adherence to SOLID principles.
Best Practices: Use type hints, design clear interfaces, favor composition over inheritance when appropriate, and document expected behaviors.
Mastering polymorphism enables you to create flexible, extensible applications that can adapt to changing requirements while maintaining clean, maintainable code structures.