Polymorphism

La Lakshmi Priya Updated 19 Oct 2026
6 min read ·Lesson 137 of 143

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

  1. ShippingMethod defines the common contract: every method has a name and can calculate a cost.
  2. Each class contains only its own rules — free standard shipping above ₹999, express pricing per started kilogram, free pickup.
  3. printShippingOptions() treats all methods the same way. It never asks which method it has; it simply calls name() and cost(), and each object answers in its own way.
  4. There is no switch statement 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 ShippingMethod whose cost() 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/match logic 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

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning
Ask about this post
AI Ask about this post

Ask questions about Polymorphism and get answers drawn from it.

Signed-in readers only.