Polymorphism means “many forms”. Different classes respond to the same method call in their own way, and the caller does not need to know which class it has.
A payment example
<?php
interface PaymentMethod
{
public function pay(float $amount): string;
}
class CreditCard implements PaymentMethod
{
public function pay(float $amount): string
{
return "Charged $amount to the credit card";
}
}
class Upi implements PaymentMethod
{
public function pay(float $amount): string
{
return "Requested $amount through UPI";
}
}
class Wallet implements PaymentMethod
{
public function pay(float $amount): string
{
return "Deducted $amount from the wallet";
}
}
function checkout(PaymentMethod $method, float $amount): void
{
echo $method->pay($amount) . "\n";
}
checkout(new CreditCard(), 499);
checkout(new Upi(), 499);
checkout(new Wallet(), 499);
Charged 499 to the credit card
Requested 499 through UPI
Deducted 499 from the wallet
checkout() calls pay() on whatever object it receives. Each class does the right thing.
Without polymorphism
The same logic with if or switch on a type:
<?php
function checkoutOld(string $type, float $amount): string
{
if ($type === "card") {
return "Charged $amount to the credit card";
} elseif ($type === "upi") {
return "Requested $amount through UPI";
} elseif ($type === "wallet") {
return "Deducted $amount from the wallet";
}
throw new InvalidArgumentException("Unknown type");
}
echo checkoutOld("upi", 100);
Requested 100 through UPI
Every new payment method means editing checkoutOld(), and every function that branches on the type. With polymorphism, you add a new class, and existing code does not change.
With an abstract parent class
It works with inheritance too.
<?php
abstract class Employee
{
public function __construct(protected string $name) {}
abstract public function pay(): float;
public function slip(): string
{
return $this->name . ": " . number_format($this->pay(), 2);
}
}
class Salaried extends Employee
{
public function __construct(string $name, private float $monthly)
{
parent::__construct($name);
}
public function pay(): float
{
return $this->monthly;
}
}
class Hourly extends Employee
{
public function __construct(string $name, private float $rate, private int $hours)
{
parent::__construct($name);
}
public function pay(): float
{
return $this->rate * $this->hours;
}
}
$staff = [new Salaried("Ana", 50000), new Hourly("Ben", 400, 120)];
foreach ($staff as $employee) {
echo $employee->slip() . "\n";
}
Ana: 50,000.00
Ben: 48,000.00
The benefit in one line
You can add new kinds of objects without changing the code that uses them.
instanceof is a warning sign
If you find yourself writing if ($x instanceof A) ... elseif ($x instanceof B), the behaviour probably belongs inside the classes, as a method.
Program to an interface
PaymentMethod), not a class (CreditCard). That is what makes the code flexible and easy to test.