Polymorphism
What Is Polymorphism?
In simple terms: Polymorphism means "many forms". In programming, it means that different objects can be used in the same way, while each one behaves in its own way. A checkout page asks every shipping method "how much do you cost?" — standard delivery, express delivery, and store pickup each answer differently, but the checkout code asks them all the same question.
Technically: Polymorphism lets code work with a general type (an interface or a parent class) while the actual objects are of various specific types. When a method is called, PHP runs the implementation belonging to the object's actual class (dynamic dispatch). In PHP, polymorphism is achieved through interfaces and inheritance (abstract or regular parent classes).
Why It Matters
Polymorphism lets you add new behavior without changing existing code. The code that uses the general type stays the same; new variants are added as new classes. This principle is known as the Open/Closed Principle: software should be open for extension but closed for modification.
The Problem: Type-Based Conditionals
Without polymorphism, code often checks "what kind of thing is this?" and branches:
<?php
function shippingCost(string $method, float $orderTotal, float $weightKg): float
{
switch ($method) {
case 'standard':
return $orderTotal >= 999 ? 0 : 60;
case 'express':
return 120 + 20 * ceil($weightKg);
case 'pickup':
return 0;
default:
throw new InvalidArgumentException("Unknown method: $method");
}
}
echo shippingCost('express', 850, 2.4) . "\n";
Output
180
This works for a while. But the same switch usually appears in several places — cost calculation, delivery estimates, labels, tracking links. Adding a new shipping method ("same-day delivery") then means finding and changing every switch, and forgetting one causes bugs.
The Polymorphic Solution
Each shipping method becomes a class implementing a shared interface:
<?php
interface ShippingMethod
{
public function name(): string;
public function cost(float $orderTotal, float $weightKg): float;
}
final class StandardShipping implements ShippingMethod
{
public function name(): string
{
return 'Standard';
}
public function cost(float $orderTotal, float $weightKg): float
{
return $orderTotal >= 999 ? 0.0 : 60.0;
}
}
final class ExpressShipping implements ShippingMethod
{
public function name(): string
{
return 'Express';
}
public function cost(float $orderTotal, float $weightKg): float
{
return 120 + 20 * ceil($weightKg);
}
}
final class StorePickup implements ShippingMethod
{
public function name(): string
{
return 'Store pickup';
}
public function cost(float $orderTotal, float $weightKg): float
{
return 0.0;
}
}
function printShippingOptions(array $methods, float $orderTotal, float $weightKg): void
{
printf("Order ₹%.2f, %.1f kg\n", $orderTotal, $weightKg);
foreach ($methods as $method) {
printf(" %-13s ₹%7.2f\n", $method->name(), $method->cost($orderTotal, $weightKg));
}
}
$methods = [new StandardShipping(), new ExpressShipping(), new StorePickup()];
printShippingOptions($methods, 850, 2.4);
printShippingOptions($methods, 1500, 0.8);
Output
Order ₹850.00, 2.4 kg
Standard ₹ 60.00
Express ₹ 180.00
Store pickup ₹ 0.00
Order ₹1500.00, 0.8 kg
Standard ₹ 0.00
Express ₹ 140.00
Store pickup ₹ 0.00
How It Works
ShippingMethoddefines the common contract: every method has a name and can calculate a cost.- Each class contains only its own rules — free standard shipping above ₹999, express pricing per started kilogram, free pickup.
printShippingOptions()treats all methods the same way. It never asks which method it has; it simply callsname()andcost(), and each object answers in its own way.- There is no
switchstatement and no list of method names to keep in sync.
Adding a New Variant
Same-day delivery requires one new class and no changes to existing code:
final class SameDayDelivery implements ShippingMethod
{
public function name(): string
{
return 'Same day';
}
public function cost(float $orderTotal, float $weightKg): float
{
return 249.0;
}
}
$methods[] = new SameDayDelivery();
Everything that works with ShippingMethod — the options list, the checkout, the invoice — supports the new method automatically.
Polymorphism Through Inheritance
Polymorphism also works with parent classes. Code written for the parent type works with every child:
<?php
abstract class Employee
{
public function __construct(protected string $name, protected float $baseSalary) {}
abstract public function monthlyPay(): float;
public function payslip(): string
{
return sprintf('%-6s ₹%s', $this->name, number_format($this->monthlyPay(), 2));
}
}
final class FullTimeEmployee extends Employee
{
public function monthlyPay(): float
{
return $this->baseSalary;
}
}
final class Intern extends Employee
{
public function monthlyPay(): float
{
return $this->baseSalary * 0.5;
}
}
final class Contractor extends Employee
{
public function __construct(string $name, private float $hourlyRate, private int $hours)
{
parent::__construct($name, 0);
}
public function monthlyPay(): float
{
return $this->hourlyRate * $this->hours;
}
}
$team = [
new FullTimeEmployee('Asha', 85000),
new Intern('Bala', 20000),
new Contractor('Charu', 1200, 64),
];
foreach ($team as $employee) {
echo $employee->payslip() . "\n";
}
$total = array_sum(array_map(fn(Employee $e): float => $e->monthlyPay(), $team));
echo "Total: ₹" . number_format($total, 2) . "\n";
Output
Asha ₹85,000.00
Bala ₹10,000.00
Charu ₹76,800.00
Total: ₹171,800.00
payslip() and the total calculation depend only on the Employee type. Each subclass decides how its pay is calculated.
The Rule That Makes Polymorphism Safe
Polymorphism only works if every implementation keeps the promise of the general type. This is the Liskov Substitution Principle: any object of a subtype must be usable wherever the base type is expected, without surprising the caller.
Examples of broken promises:
- A
ShippingMethodwhosecost()returns a negative number or throws an exception for normal orders. - A subclass that overrides a method to do nothing when callers rely on its effect.
- A "read-only" implementation of a repository interface that throws on
save().
PHP enforces compatible signatures (see Method Overriding), but keeping the behavioral contract is the developer's responsibility. Document the expectations of an interface (for example, "returns the cost in rupees, never negative").
Choosing the Implementation
Somewhere, the program must decide which object to use — typically based on user input or configuration. That decision is made once, often in a small factory or map, and the rest of the code works with the interface:
<?php
function shippingMethodFromCode(string $code): ShippingMethod
{
return match ($code) {
'standard' => new StandardShipping(),
'express' => new ExpressShipping(),
'pickup' => new StorePickup(),
default => throw new InvalidArgumentException("Unknown shipping method: $code"),
};
}
$selected = shippingMethodFromCode('express'); // e.g. from the checkout form
echo $selected->name() . ': ₹' . $selected->cost(850, 2.4) . "\n";
Output
Express: ₹180
The match here is the only place that knows about all the classes. Compare this with the earlier switch, which mixed selection with the cost rules and had to be repeated for every operation. In frameworks, a service container or configuration often takes over this role.
Common Mistakes
Checking Types With instanceof Everywhere
Effect: if ($method instanceof ExpressShipping) { ... } scattered through the code defeats polymorphism.
Fix: Move the varying behavior into a method of the interface.
Interfaces That Do Not Fit All Implementations
Effect: Some classes implement methods by throwing "not supported" exceptions.
Fix: Split the interface into smaller ones that every implementation can honor.
Variants That Break the Contract
Effect: Callers receive unexpected results from some implementations.
Fix: Define and document the contract, and test every implementation against it.
Over-Engineering
Effect: A class hierarchy for logic that will never vary.
Fix: Introduce polymorphism when there are (or will clearly be) several variants of a behavior.
Best Practices
- Replace repeated type-based
switch/matchlogic with polymorphic classes. - Program against interfaces or abstract types, not concrete classes.
- Keep each variant's rules in its own class.
- Select the concrete implementation in one place (factory, map, or container).
- Make sure every implementation honors the documented contract of its type.
Official reference: https://www.php.net/manual/en/language.oop5.interfaces.php