0

Polymorphism — one interface, multiple behaviours

Beginner5 min read·eng-12-008
interviewcompare

Concept

Polymorphism — the ability of different types to respond to the same message (method call) in different ways. One interface, many behaviors.

Greek roots: poly = many, morphe = form. Same call, different forms of response.

Types of polymorphism in PHP:

Subtype polymorphism (the most common): A subclass overrides a parent method. At runtime, which render() is called depends on the actual type of the object, not the declared type.

Interface polymorphism: Multiple unrelated classes implement the same interface. Code that type-hints the interface works with any of them.

Ad-hoc polymorphism (less relevant in PHP): Operator overloading or method overloading — not natively supported in PHP (no function signatures based on parameter types).

Duck typing: "If it quacks like a duck, it's a duck." PHP's dynamic typing allows calling any method on any object — no interface required. PHP 8 union types and mixed support this.

Why polymorphism matters:

  • Code that works with an abstraction (NotificationChannel) doesn't need to change when you add a new implementation (TelegramChannel).
  • Eliminates if ($type === 'email') { ... } elseif ($type === 'sms') { ... } — the decision moves into the class hierarchy.
  • Supports the Open/Closed Principle: open for extension, closed for modification.

Real PHP examples: Laravel's notification channels (MailChannel, SlackChannel, SmsChannel), Eloquent relations (HasMany, BelongsTo — same get() method), Laravel Auth guards, payment gateways.

Code Example

php
<?php
// Interface polymorphism — one interface, many implementations
interface PaymentGateway
{
    public function charge(float $amount, string $token): PaymentResult;
    public function refund(string $chargeId, float $amount): void;
}

class StripeGateway implements PaymentGateway
{
    public function charge(float $amount, string $token): PaymentResult
    {
        // Stripe-specific API call
        return new PaymentResult(success: true, transactionId: 'stripe_' . uniqid());
    }
    public function refund(string $chargeId, float $amount): void { /* Stripe refund */ }
}

class PayPalGateway implements PaymentGateway
{
    public function charge(float $amount, string $token): PaymentResult
    {
        // PayPal-specific API call
        return new PaymentResult(success: true, transactionId: 'paypal_' . uniqid());
    }
    public function refund(string $chargeId, float $amount): void { /* PayPal refund */ }
}

// Client code — works with any PaymentGateway (polymorphic)
class CheckoutService
{
    public function __construct(private readonly PaymentGateway $gateway) {}

    public function pay(float $amount, string $token): PaymentResult
    {
        return $this->gateway->charge($amount, $token); // works the same regardless of gateway
    }
}

// Swap implementations without changing CheckoutService
$stripe   = new CheckoutService(new StripeGateway());
$paypal   = new CheckoutService(new PayPalGateway());

// Subtype polymorphism — override in subclass
abstract class Shape
{
    abstract public function area(): float;
    public function describe(): string { return get_class($this) . ' with area ' . $this->area(); }
}

class Circle    extends Shape { public function __construct(private float $r) {} public function area(): float { return M_PI * $this->r ** 2; } }
class Rectangle extends Shape { public function __construct(private float $w, private float $h) {} public function area(): float { return $this->w * $this->h; } }

$shapes = [new Circle(5), new Rectangle(3, 4)];
foreach ($shapes as $shape) {
    echo $shape->describe() . "\n"; // area() resolved to the correct subclass method
}
// Circle with area 78.54...
// Rectangle with area 12