Home PHP PHP Interfaces vs Abstract Classes: When to Use Each (With Real Examples)

PHP Interfaces vs Abstract Classes: When to Use Each (With Real Examples)

In Plain English 🔥
Think of an interface like a job contract — it says 'anyone hired for this role MUST be able to do these tasks,' but it doesn't tell you HOW to do them. An abstract class is more like a franchise manual — it gives you some recipes already written out, but leaves a few blanks you must fill in yourself. The contract enforces capability; the manual gives you a head start plus enforces a few rules.
⚡ Quick Answer
Think of an interface like a job contract — it says 'anyone hired for this role MUST be able to do these tasks,' but it doesn't tell you HOW to do them. An abstract class is more like a franchise manual — it gives you some recipes already written out, but leaves a few blanks you must fill in yourself. The contract enforces capability; the manual gives you a head start plus enforces a few rules.

Every non-trivial PHP application eventually hits the same wall: you have multiple classes that need to behave consistently, but they each work differently under the hood. A PayPal payment processor and a Stripe payment processor both need to charge a card and issue a refund — but the code for each is completely different. Without a shared contract, one developer writes processPayment(), another writes makeCharge(), and your billing page breaks at 2am on a Friday. Interfaces and abstract classes are PHP's answer to that chaos.

The real problem they solve isn't just 'code organisation' — it's enforcing a contract at the language level so PHP itself throws an error the moment a class breaks the agreement. That's vastly better than discovering a broken method signature in production. Abstract classes take it a step further by letting you bake in shared behaviour so you're not copy-pasting the same code across every implementation.

By the end of this article you'll know exactly when to reach for an interface vs an abstract class, how to combine them for maximum flexibility, and you'll have seen a complete payment gateway example you can adapt to your own projects. You'll also know the three mistakes that trip up developers who've been writing PHP for years.

Interfaces: Enforcing a Contract Without Writing Any Logic

An interface is a pure contract. It lists method signatures — name, parameters, return type — and every class that implements it must provide a concrete body for every single one. No exceptions, no partial compliance. PHP will throw a fatal error if you miss even one method.

The power here is polymorphism. Once you type-hint against an interface, you genuinely don't care what class is behind it. Your InvoiceService can accept anything that implements PaymentGatewayInterface — Stripe today, a mock object in your test suite tomorrow, a new provider next quarter. Nothing in InvoiceService changes.

Interfaces also support multiple implementation, meaning one class can implement several interfaces simultaneously. A StripeGateway can implement both PaymentGatewayInterface and RefundableInterface. That's something abstract classes can never give you, because PHP only allows single class inheritance.

Use an interface when: you want to define WHAT something must do, you need multiple unrelated classes to share a common type, or you want to write code that works against a guarantee rather than a concrete implementation.

PaymentGatewayInterface.php · PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
<?php

// The interface defines the CONTRACT — every payment gateway must
// implement ALL of these methods, with exactly these signatures.
interface PaymentGatewayInterface
{
    // Charge a customer a given amount in cents; return a transaction ID.
    public function charge(int $amountInCents, string $currency, string $paymentToken): string;

    // Refund a previous transaction by its ID; return true on success.
    public function refund(string $transactionId): bool;

    // Retrieve the human-readable name of this gateway (e.g. "Stripe").
    public function getGatewayName(): string;
}

// ---------------------------------------------------------------
// Concrete implementation #1: Stripe
// ---------------------------------------------------------------
class StripeGateway implements PaymentGatewayInterface
{
    public function charge(int $amountInCents, string $currency, string $paymentToken): string
    {
        // In a real app you'd call the Stripe SDK here.
        // We're simulating a successful charge and returning a fake transaction ID.
        $transactionId = 'stripe_txn_' . uniqid();
        echo "[Stripe] Charged {$amountInCents} {$currency}. Transaction: {$transactionId}\n";
        return $transactionId;
    }

    public function refund(string $transactionId): bool
    {
        echo "[Stripe] Refunded transaction: {$transactionId}\n";
        return true; // Simulate a successful refund.
    }

    public function getGatewayName(): string
    {
        return 'Stripe';
    }
}

// ---------------------------------------------------------------
// Concrete implementation #2: PayPal
// ---------------------------------------------------------------
class PayPalGateway implements PaymentGatewayInterface
{
    public function charge(int $amountInCents, string $currency, string $paymentToken): string
    {
        $transactionId = 'paypal_txn_' . uniqid();
        echo "[PayPal] Charged {$amountInCents} {$currency}. Transaction: {$transactionId}\n";
        return $transactionId;
    }

    public function refund(string $transactionId): bool
    {
        echo "[PayPal] Refunded transaction: {$transactionId}\n";
        return true;
    }

    public function getGatewayName(): string
    {
        return 'PayPal';
    }
}

// ---------------------------------------------------------------
// This service type-hints against the INTERFACE, not a concrete class.
// Swap Stripe for PayPal (or a mock) and this code never changes.
// ---------------------------------------------------------------
class InvoiceService
{
    // We accept ANYTHING that honours the PaymentGatewayInterface contract.
    public function __construct(private PaymentGatewayInterface $gateway) {}

    public function billCustomer(int $amountInCents, string $currency, string $token): void
    {
        $txnId = $this->gateway->charge($amountInCents, $currency, $token);
        echo "Invoice created for transaction: {$txnId} via " . $this->gateway->getGatewayName() . "\n";
    }
}

// ---------------------------------------------------------------
// Wiring it together
// ---------------------------------------------------------------
$stripeService = new InvoiceService(new StripeGateway());
$stripeService->billCustomer(4999, 'USD', 'tok_visa_test');

$paypalService = new InvoiceService(new PayPalGateway());
$paypalService->billCustomer(1999, 'GBP', 'paypal_token_abc');
▶ Output
[Stripe] Charged 4999 USD. Transaction: stripe_txn_6651a3f4b2c10
Invoice created for transaction: stripe_txn_6651a3f4b2c10 via Stripe
[PayPal] Charged 1999 GBP. Transaction: paypal_txn_6651a3f4b2c11
Invoice created for transaction: paypal_txn_6651a3f4b2c11 via PayPal
⚠️
Pro Tip: Type-hint against interfaces, not concrete classesIf `InvoiceService` type-hinted `StripeGateway` directly, you'd have to rewrite it for every new provider and you couldn't swap in a mock during testing. Type-hinting the interface means your tests can pass in a `FakePaymentGateway` that never touches the network — and the real service code stays untouched.

Abstract Classes: Shared Behaviour With Enforced Gaps

An abstract class sits between a regular class and an interface. You can write concrete methods that all child classes inherit for free, AND you can declare abstract methods that each child class must implement on its own. It's the best tool when you have a group of related classes that share some real logic, but differ in specific steps.

A classic example: every notification type (Email, SMS, Slack) needs to log that it was sent and validate the recipient — but each one sends the message completely differently. You'd put logDispatch() and validateRecipient() in the abstract class as concrete methods, and declare send() as abstract, forcing each child to implement its own delivery mechanism.

The key constraint is that a class can only extend ONE abstract class. That's not a bug — it reflects the 'is-a' relationship. An EmailNotification IS-A Notification. If you find yourself wanting to extend two abstract classes at once, that's a design smell telling you to reach for an interface instead.

Use an abstract class when: subclasses share real, non-trivial implementation that you'd otherwise copy-paste, there's a genuine 'is-a' parent-child relationship, and you want to enforce a template method pattern — where the skeleton of an algorithm lives in the parent.

NotificationAbstract.php · PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
<?php

// The abstract class provides the SHARED skeleton.
// It cannot be instantiated directly — you must extend it.
abstract class Notification
{
    // Concrete shared property — all notifications have a recipient.
    protected string $recipientAddress;
    protected string $messageBody;

    public function __construct(string $recipientAddress, string $messageBody)
    {
        $this->recipientAddress = $recipientAddress;
        $this->messageBody = $messageBody;
    }

    // CONCRETE method — shared logic every child inherits for free.
    // No child class needs to rewrite this.
    protected function logDispatch(string $channelName): void
    {
        $timestamp = date('Y-m-d H:i:s');
        echo "[{$timestamp}] [{$channelName}] Notification dispatched to: {$this->recipientAddress}\n";
    }

    // CONCRETE method — shared validation all channels must pass through.
    protected function validateRecipient(): void
    {
        if (empty(trim($this->recipientAddress))) {
            // Throwing here means no child class can accidentally skip validation.
            throw new InvalidArgumentException('Recipient address cannot be empty.');
        }
    }

    // ABSTRACT method — each channel delivers differently; the parent
    // declares the requirement but provides no body.
    abstract public function send(): void;

    // TEMPLATE METHOD pattern: the public entry point calls shared logic
    // in a fixed order, then calls the abstract send() that each child defines.
    // This guarantees: validate → send → log, every single time.
    final public function dispatch(): void
    {
        $this->validateRecipient(); // Always validate first.
        $this->send();              // Each child handles its own delivery.
        $this->logDispatch(static::class); // Always log after sending.
    }
}

// ---------------------------------------------------------------
// Child class #1: Email delivery
// ---------------------------------------------------------------
class EmailNotification extends Notification
{
    // Only send() needs to be implemented — the rest comes from the parent.
    public function send(): void
    {
        // In production: use PHPMailer, Symfony Mailer, etc.
        echo "[Email] Sending to {$this->recipientAddress}: \"{$this->messageBody}\"\n";
    }
}

// ---------------------------------------------------------------
// Child class #2: SMS delivery
// ---------------------------------------------------------------
class SmsNotification extends Notification
{
    public function send(): void
    {
        // In production: call Twilio or AWS SNS here.
        $shortMessage = substr($this->messageBody, 0, 160); // SMS has a 160-char limit.
        echo "[SMS] Sending to {$this->recipientAddress}: \"{$shortMessage}\"\n";
    }
}

// ---------------------------------------------------------------
// Usage — dispatch() handles the full pipeline for both.
// ---------------------------------------------------------------
$emailAlert = new EmailNotification('alice@example.com', 'Your order has shipped!');
$emailAlert->dispatch();

echo "---\n";

$smsAlert = new SmsNotification('+447911123456', 'Your delivery arrives today between 2-4pm.');
$smsAlert->dispatch();
▶ Output
[Email] Sending to alice@example.com: "Your order has shipped!"
[2024-07-15 14:32:01] [EmailNotification] Notification dispatched to: alice@example.com
---
[SMS] Sending to +447911123456: "Your delivery arrives today between 2-4pm."
[2024-07-15 14:32:01] [SmsNotification] Notification dispatched to: +447911123456
🔥
The Template Method Pattern in disguiseThe `dispatch()` method marked `final` is the Template Method pattern. The parent controls the SEQUENCE (validate → send → log) and child classes only fill in the `send()` step. Marking it `final` means no child can override the sequence and accidentally skip logging or validation. It's a subtle but powerful design move.

Combining Both: The Most Flexible Architecture in PHP

Here's the move senior engineers make that juniors often miss: use an interface to define the public contract for the outside world, and use an abstract class to provide a reusable base for common implementations. They're not competing tools — they're teammates.

The pattern works like this: your PaymentGatewayInterface defines what every gateway MUST do. Then you create an AbstractPaymentGateway that implements the interface and handles cross-cutting concerns shared by all real implementations — things like retry logic, logging failed charges, or formatting currency. Concrete gateways then extend the abstract class and only implement the bits that are truly provider-specific.

This also future-proofs your codebase. Need a completely custom gateway that doesn't fit the abstract class structure? No problem — implement the interface directly. The abstract class is a convenience, not a cage.

This three-layer structure (interface → abstract class → concrete class) is the backbone of every serious PHP framework. Laravel's filesystem, queue, and cache systems all use exactly this pattern. Once you see it, you'll spot it everywhere.

CombinedGatewayPattern.php · PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
<?php

// LAYER 1: The Interface — defines the public contract for all gateways.
// External code (controllers, services) will only ever know about this.
interface PaymentGatewayInterface
{
    public function charge(int $amountInCents, string $currency, string $paymentToken): string;
    public function refund(string $transactionId): bool;
    public function getGatewayName(): string;
}

// LAYER 2: The Abstract Class — implements the interface and provides
// shared behaviour that ALL real gateways benefit from.
abstract class AbstractPaymentGateway implements PaymentGatewayInterface
{
    private array $chargeLog = [];

    // Shared method: log every charge attempt regardless of the provider.
    // No concrete gateway needs to rewrite this.
    protected function recordCharge(string $transactionId, int $amountInCents): void
    {
        $this->chargeLog[] = [
            'txn'    => $transactionId,
            'amount' => $amountInCents,
            'time'   => time(),
        ];
    }

    // Shared utility: format cents into a readable currency string.
    protected function formatAmount(int $amountInCents, string $currency): string
    {
        return number_format($amountInCents / 100, 2) . ' ' . strtoupper($currency);
    }

    // Shared method available to all gateways — retrieve the full charge history.
    public function getChargeLog(): array
    {
        return $this->chargeLog;
    }

    // charge() and refund() are still abstract here — each provider implements them.
    // getGatewayName() is also left abstract — only the concrete class knows its name.
    abstract public function charge(int $amountInCents, string $currency, string $paymentToken): string;
    abstract public function refund(string $transactionId): bool;
    abstract public function getGatewayName(): string;
}

// LAYER 3: Concrete class — only deals with Stripe-specific API logic.
// Gets formatAmount() and recordCharge() and getChargeLog() for free.
class StripeGateway extends AbstractPaymentGateway
{
    public function charge(int $amountInCents, string $currency, string $paymentToken): string
    {
        // Use the inherited helper to format a readable amount for the log.
        $readable = $this->formatAmount($amountInCents, $currency);
        $transactionId = 'stripe_' . bin2hex(random_bytes(6));

        echo "[Stripe] Charged {$readable} using token '{$paymentToken}'. TXN: {$transactionId}\n";

        // Inherited from AbstractPaymentGateway — logs the charge automatically.
        $this->recordCharge($transactionId, $amountInCents);

        return $transactionId;
    }

    public function refund(string $transactionId): bool
    {
        echo "[Stripe] Refunding: {$transactionId}\n";
        return true;
    }

    public function getGatewayName(): string
    {
        return 'Stripe';
    }
}

// A one-off gateway that skips the abstract class entirely — it implements
// the interface directly because it has zero shared logic with others.
class CryptoGateway implements PaymentGatewayInterface
{
    public function charge(int $amountInCents, string $currency, string $paymentToken): string
    {
        $transactionId = 'crypto_' . bin2hex(random_bytes(6));
        echo "[Crypto] Charged {$amountInCents} cents worth of BTC. TXN: {$transactionId}\n";
        return $transactionId;
    }

    public function refund(string $transactionId): bool
    {
        // Blockchain transactions are irreversible — we can't refund.
        echo "[Crypto] Refunds not supported for: {$transactionId}\n";
        return false;
    }

    public function getGatewayName(): string
    {
        return 'Crypto';
    }
}

// ---------------------------------------------------------------
// USAGE: both gateways honour PaymentGatewayInterface.
// InvoiceService doesn't care how they work internally.
// ---------------------------------------------------------------
function processOrder(PaymentGatewayInterface $gateway, int $amountInCents): void
{
    $txnId = $gateway->charge($amountInCents, 'USD', 'tok_test_' . rand(1000,9999));
    echo "Order processed via " . $gateway->getGatewayName() . ". TXN: {$txnId}\n";
}

$stripe = new StripeGateway();
processOrder($stripe, 7999);

echo "---\n";

$crypto = new CryptoGateway();
processOrder($crypto, 24999);

echo "---\n";
// StripeGateway gets getChargeLog() from the abstract class; CryptoGateway doesn't.
echo "Stripe charge log: " . print_r($stripe->getChargeLog(), true);
▶ Output
[Stripe] Charged 79.99 USD using token 'tok_test_4821'. TXN: stripe_a3f9e12c4b01
Order processed via Stripe. TXN: stripe_a3f9e12c4b01
---
[Crypto] Charged 24999 cents worth of BTC. TXN: crypto_b71c3a8f9d22
Order processed via Crypto. TXN: crypto_b71c3a8f9d22
---
Stripe charge log: Array
(
[0] => Array
(
[txn] => stripe_a3f9e12c4b01
[amount] => 7999
[time] => 1721051521
)
)
🔥
Interview Gold: Why use both?If an interviewer asks 'why define an interface if you already have an abstract class that implements it?', the answer is: the interface allows classes that CAN'T or SHOULDN'T extend your abstract class (like CryptoGateway above) to still be used anywhere a PaymentGatewayInterface is expected. The interface is the contract; the abstract class is a convenience layer on top of it.
Feature / AspectInterfaceAbstract Class
Can contain method bodiesNo (PHP 8 allows default interface methods? No — only constants)Yes — mix of concrete and abstract methods
Can contain propertiesNo (only constants)Yes — any visibility
Multiple inheritanceA class can implement many interfacesA class can only extend one abstract class
Constructor allowedNoYes
Instantiate directlyNo — fatal errorNo — fatal error
Keyword to useimplementsextends
Best forDefining a contract for unrelated classesShared base for closely related classes
Relationship typeCan-do / Has-capabilityIs-a / Parent-child
PHP version requirementPHP 5+PHP 5+
Access modifiers on methodsAlways publicpublic, protected (not private)

🎯 Key Takeaways

  • Interfaces define the WHAT (contract), abstract classes define the WHAT plus some of the HOW (shared implementation) — they solve different problems and are most powerful when used together.
  • Type-hint against interfaces in your service classes, not concrete implementations — this is the single change that makes your code both testable and swappable without rewriting core logic.
  • A class can implement as many interfaces as it needs, but can only extend one abstract class — when you need cross-cutting capability on unrelated classes, always reach for an interface.
  • The Template Method pattern (a final method in an abstract class that calls abstract sub-steps) guarantees execution order — validate, execute, log — so no subclass can accidentally skip critical steps.

⚠ Common Mistakes to Avoid

  • Mistake 1: Declaring properties inside an interface — PHP throws a fatal error ('Interfaces may not include properties') because interfaces are contracts, not blueprints. If you need a shared property, move it to an abstract class or a trait. Interfaces only support class constants (defined with const).
  • Mistake 2: Forgetting to implement ALL interface methods in a concrete class — PHP throws 'Class X contains 1 abstract method and must therefore be declared abstract or implement the remaining methods.' The fix is simple: either implement every method, or declare the partial class itself as abstract if you intend it to be extended further.
  • Mistake 3: Using an abstract class when an interface is the right tool — developers reach for an abstract class out of habit, tightly coupling classes that have no genuine 'is-a' relationship. This blocks multiple-implementation and makes testing harder (you can't mock a class as easily). Ask yourself: do these classes share real, non-trivial implementation? If the answer is no, use an interface and keep your options open.

Interview Questions on This Topic

  • QWhat's the practical difference between an interface and an abstract class in PHP, and how do you decide which one to use for a new feature?
  • QCan a PHP class implement multiple interfaces? Can it extend multiple abstract classes? Why does this asymmetry exist, and what design problem does it prevent?
  • QIf you have an abstract class that already implements an interface, do all concrete subclasses automatically satisfy that interface — and what happens if an abstract method in the class matches an interface method that's NOT yet implemented?

Frequently Asked Questions

Can a PHP abstract class implement an interface?

Yes — and this is actually a powerful pattern. An abstract class can implement an interface without providing method bodies for all of the interface's methods; it can leave some as abstract, and concrete subclasses are then required to implement them. This lets you distribute the implementation responsibility across multiple layers of your hierarchy.

Can you have a constructor in a PHP interface?

No, PHP interfaces cannot define constructors. If you need to enforce how objects are constructed, consider a factory interface (e.g. createFromArray(): static) or an abstract class, which does support constructors. Trying to define __construct in an interface in older PHP versions causes a fatal error, though technically PHP 8 allows it in interfaces — but it's considered bad practice and rarely useful.

What happens if I don't implement an abstract method in a child class?

PHP throws a fatal error at class load time, not just when you call the method: 'Class ChildClass contains 1 abstract method and must therefore be declared abstract or implement the remaining methods.' Your only two options are to implement the method or declare the child class abstract itself, which just pushes the requirement down to the next concrete subclass.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousInheritance in PHPNext →Traits in PHP
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged