Senior 5 min · March 06, 2026

PHP Interfaces vs Abstract Classes — Silent Refund Failure

A missing interface method caused silent refund failures with 200 OK but no processing.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Interfaces define a pure contract: method signatures without implementation
  • Abstract classes mix contract and shared implementation: concrete methods plus abstract ones
  • Use interfaces when unrelated classes need to share a common capability
  • Use abstract classes when related classes share real logic and have an is-a relationship
  • A class can implement many interfaces but extend only one abstract class
  • Name your interfaces after capabilities (e.g., PaymentGatewayInterface) and abstract classes after the base type (e.g., AbstractNotification)
Plain-English First

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.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<?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 classes
If 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.
Production Insight
If a class implements an interface but forgets a method, PHP throws a fatal error at class load time — not when the method is called.
This means a forgotten method in an unused code path goes unnoticed until that code is exercised.
Always test every implementation path, including error handling and refund flows.
Key Takeaway
Interfaces enforce a contract at load time.
Type-hint against interfaces to decouple your code.
The compiler catches missing methods early; don't rely on runtime.

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.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?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 disguise
The 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.
Production Insight
If you mark a method final in an abstract class, subclasses cannot override it.
This is both a blessing and a curse: it guarantees the algorithm's integrity but blocks legitimate customisation.
Rule: only mark final when the method enforces a critical invariant like logging or validation that must always run.
Key Takeaway
Abstract classes give you implementation reuse plus a forced contract.
They are ideal for is-a hierarchies with shared behaviour.
Use final on template methods to lock the algorithm's structure.

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.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<?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.
Production Insight
The three-layer pattern can be overkill for small projects — don't reach for it prematurely.
It shines when you have multiple implementations with common logic and a few edge cases that need the raw interface.
Get the interface right first, then layer the abstract class if you see repeated code across implementations.
Key Takeaway
Interface + abstract class = maximum flexibility.
Interface is the contract; abstract class is the convenience.
Never let the abstract class become a cage — implement the interface directly when needed.

When to Use Interfaces vs Abstract Classes: A Practical Decision Guide

Choosing between an interface and an abstract class often comes down to answering three questions:

  1. Do the classes share a genuine 'is-a' relationship? If you can say 'A StripeGateway IS-A PaymentGateway', an abstract class might fit. But if you're forcing a relationship just to reuse code, you're coupling things that don't belong together.
  2. Do you need to enforce a contract across completely unrelated classes? Interfaces are the only way to make a Mailer and a PaymentGateway both implement a common LoggableInterface — they have zero relationship but both can log.
  3. Is there shared implementation that would be copy-pasted otherwise? If every implementation does the same validation, logging, or formatting, an abstract class saves you from repeating that code. If there's no shared code, an interface is lighter and more flexible.

The rule of thumb: default to interfaces. Reach for an abstract class only when you have proven that multiple implementations share non-trivial logic. Otherwise, you're creating coupling that will hurt later.

decision_guide.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

/**
 * Decision function to demonstrate how to pick.
 * This is not production code — it's a self-documenting skeleton.
 */
function choosePattern(array $classes): string
{
    $hasSharedLogic = false;
    $hasIsARelationship = false;
    $multipleUnrelated = false;

    // Simulate checks (in reality you'd analyse the code)
    if (/* classes share > 2 methods with same body */ true) {
        $hasSharedLogic = true;
    }
    if (/* natural hierarchy exists */ true) {
        $hasIsARelationship = true;
    }
    if (/* more than one unrelated type */ true) {
        $multipleUnrelated = true;
    }

    if ($multipleUnrelated) {
        return 'Use an interface to define a shared capability';
    }
    if ($hasSharedLogic && $hasIsARelationship) {
        return 'Use an abstract class for shared implementation + contract';
    }
    return 'Consider an interface first; extract abstract class later if needed';
}
Output
Use an interface to define a shared capability
Production Insight
A common mistake is using an abstract class for everything 'just in case' you need shared logic later.
This forces all implementations into a rigid hierarchy that blocks future flexibility.
Start with an interface. Add the abstract class as a refactoring step when you see duplication — not before.
Key Takeaway
Default to interfaces.
Add abstract classes only when you have real shared code.
You can always refactor from interface to interface+abstract later.

Real-World Patterns in PHP Frameworks (Laravel, Symfony)

The interface-abstract class combination isn't just academic — it powers every major PHP framework under the hood.

Laravel's Queue System: The Illuminate\Contracts\Queue\Queue interface defines methods like push(), later(), pop(). An abstract class Queue implements this interface and provides shared logic for serializing jobs, firing events, and handling failures. Concrete drivers (DatabaseQueue, RedisQueue, SqsQueue) extend the abstract class and only implement the transport-specific bits.

Symfony's Cache System: The Psr\SimpleCache\CacheInterface (from PSR-16) is the contract. An abstract class AbstractCache implements it and adds shared logic like serialisation and expiration checking. Concrete adapters (FilesystemCache, RedisCache, DoctrineCache) extend the abstract class.

This pattern is so pervasive that you can spot it in the source code of any well-architected PHP library. It gives you a clean contract for the application code and a reusable base for the implementors, while still allowing edge cases to implement the interface directly.

When you design your own packages, follow this pattern: define the interface first in a Contracts namespace, put the abstract implementation in a Concerns or Base namespace, and keep concrete implementations separate. This is what the Pro PHP community calls 'coding to the interface, not the implementation'.

FrameworkPattern.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php
namespace Io\TheCodeforge\Cache;

// Example inspired by Symfony/Laravel
// Step 1: Define the contract
interface CacheInterface
{
    public function get(string $key, mixed $default = null): mixed;
    public function set(string $key, mixed $value, int $ttl = null): bool;
    public function delete(string $key): bool;
}

// Step 2: Abstract base with shared logic
abstract class AbstractCache implements CacheInterface
{
    protected function validateKey(string $key): void
    {
        if (strlen($key) > 64) {
            throw new \InvalidArgumentException('Cache key too long');
        }
    }

    // Concrete classes implement serialization differently
    protected function serialize(mixed $value): string
    {
        return serialize($value);
    }

    protected function unserialize(string $value): mixed
    {
        return unserialize($value);
    }
}

// Step 3: Concrete adapter
class FileSystemCache extends AbstractCache
{
    public function get(string $key, mixed $default = null): mixed
    {
        $this->validateKey($key);
        // ... read from filesystem
        return $default;
    }

    public function set(string $key, mixed $value, int $ttl = null): bool
    {
        $this->validateKey($key);
        // ... write to filesystem
        return true;
    }

    public function delete(string $key): bool
    {
        $this->validateKey($key);
        // ... delete file
        return true;
    }
}

// Edge case: in-memory cache might not need the abstract class
class RuntimeCache implements CacheInterface
{
    private array $store = [];

    public function get(string $key, mixed $default = null): mixed
    {
        return $this->store[$key] ?? $default;
    }

    public function set(string $key, mixed $value, int $ttl = null): bool
    {
        $this->store[$key] = $value;
        return true;
    }

    public function delete(string $key): bool
    {
        unset($this->store[$key]);
        return true;
    }
}
Production Insight
When you extend a framework's abstract base class, you inherit all its assumptions.
If a future version changes the abstract class's internal behaviour, your subclass might break.
Always implement the interface directly in critical paths where you need full control; use the abstract class only when you trust its evolution.
Key Takeaway
Every major PHP framework uses the interface+abstract class pattern.
Learn to recognise it and apply it in your own packages.
But don't treat the abstract class as sacred — implement directly when flexibility matters.
● Production incidentPOST-MORTEMseverity: high

Silent Refund Failure: The Missing Interface Method

Symptom
Refund requests never processed; no error logged; API returned 200 but refund not executed.
Assumption
Because the StripeGateway class implemented PaymentGatewayInterface and the charge method worked, the team assumed all methods were correctly implemented.
Root cause
The developer added a new refund() method to the interface to meet a new business requirement, but the StripeGateway class was not updated. PHP loads the class successfully because the missing method isn't checked until the class is actually used. Since the refund feature was on a separate code path that wasn't triggered in tests, the error only surfaced in production.
Fix
Add integration tests that call every method of the interface on every implementation. Use static analysis tools like PHPStan or Psalm with level max to detect missing interface methods during CI.
Key lesson
  • Interface methods are only checked at class load time - not instantiation time.
  • Always write integration tests covering every method of every interface implementation.
  • Use static analysis to catch missing implementations before deploy.
Production debug guideSymptom-based guide to fixing contract enforcement problems4 entries
Symptom · 01
PHP Fatal error: Class X contains 1 abstract method and must therefore be declared abstract or implement the remaining methods
Fix
This happens when a class extends an abstract class or implements an interface but doesn't implement all abstract methods. Check the class declaration and ensure all method signatures match exactly - including return types and parameter types.
Symptom · 02
Runtime error: Call to undefined method X::methodName()
Fix
A method exists in the interface but is missing in the concrete class. Verify that the class implements all interface methods. Use PHPStan with --level=6 to catch this automatically.
Symptom · 03
Type error: Argument 1 passed to ... must be an instance of InterfaceName, instance of ConcreteClass given
Fix
Check if the concrete class actually implements the interface. If it only extends the abstract class and the abstract class doesn't declare it implements the interface, the concrete class may not satisfy the type hint. Ensure the abstract class explicitly implements the interface.
Symptom · 04
Laravel/Symfony service provider throws BindingResolutionException for an interface
Fix
The container cannot resolve the interface because no concrete implementation is bound. Check service provider bindings (e.g., $this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);).
★ Quick Reference: Interface vs Abstract Class DecisionUse this cheat sheet when designing a new class hierarchy to pick the right tool fast.
Multiple unrelated classes need a common behaviour (e.g., both Logger and Payment need to be Cacheable)
Immediate action
Use an interface. They can share a capability without sharing a type hierarchy.
Commands
interface CacheableInterface { public function getCacheKey(): string; }
class Logger implements CacheableInterface { ... }
Fix now
Implement the interface on each class.
Classes share real code but differ in one or two steps (e.g., different notification channels)+
Immediate action
Use an abstract class with the Template Method pattern.
Commands
abstract class Notification { final public function dispatch(): void { $this->validate(); $this->send(); $this->log(); } abstract protected function send(): void; }
class EmailNotification extends Notification { protected function send(): void { ... } }
Fix now
Extract common code into the abstract class; mark the varying step as abstract.
You need a strict contract but have no shared code yet+
Immediate action
Use an interface only. You can always add an abstract class later.
Commands
interface PaymentGatewayInterface { public function charge(int $amount): string; }
class StripeGateway implements PaymentGatewayInterface { ... }
Fix now
Type-hint against the interface everywhere.
A class needs to both extend an abstract class and implement a second contract+
Immediate action
Use an interface for the second contract. PHP only allows single class inheritance, but multiple interfaces.
Commands
class StripeGateway extends AbstractPaymentGateway implements RefundableInterface { ... }
RefundableInterface defines refund(): bool; StripeGateway implements both.
Fix now
Define the extra capability as an interface and implement it alongside the class hierarchy.
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

1
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.
2
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.
3
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.
4
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.
5
Default to interfaces. Extract an abstract class only after you see real code duplication across implementations.

Common mistakes to avoid

3 patterns
×

Declaring properties inside an interface

Symptom
PHP throws a fatal error: 'Interfaces may not include properties'
Fix
Move the property to an abstract class or a trait. Interfaces only support class constants (defined with const). If you need shared state, design your hierarchy so the interface doesn't require it.
×

Forgetting to implement ALL interface methods in a concrete class

Symptom
PHP throws 'Class X contains 1 abstract method and must therefore be declared abstract or implement the remaining methods.' The error appears at class load time, not just when the method is called.
Fix
Either implement every method listed in the interface, or declare the partial class itself as abstract if you intend it to be extended further. Use static analysis tools to catch this before deploy.
×

Using an abstract class when an interface is the right tool

Symptom
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). Later, you need to add a second abstract method and everything breaks.
Fix
Ask yourself: do these classes share real, non-trivial implementation? If no, use an interface. If you need both, combine them: interface + abstract class that implements it.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the practical difference between an interface and an abstract cl...
Q02SENIOR
Can a PHP class implement multiple interfaces? Can it extend multiple ab...
Q03SENIOR
If you have an abstract class that already implements an interface, do a...
Q04SENIOR
Describe a real scenario where using an abstract class would be a mistak...
Q01 of 04JUNIOR

Explain the practical difference between an interface and an abstract class in PHP. When would you choose one over the other for a new feature?

ANSWER
An interface is a pure contract with method signatures only — it defines 'what' a class must do. An abstract class can have both concrete methods (shared implementation) and abstract methods (obligations for subclasses). Choose an interface when you need unrelated classes to share a common capability (e.g., both a Logger and a PaymentGateway need to be Cacheable). Choose an abstract class when your classes share real, non-trivial logic and have a natural 'is-a' relationship (e.g., EmailNotification is a Notification). Default to interfaces; add abstract classes only when you see actual code duplication.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Can a PHP abstract class implement an interface?
02
Can you have a constructor in a PHP interface?
03
What happens if I don't implement an abstract method in a child class?
04
When should I use a Trait instead of an abstract class?
🔥

That's OOP in PHP. Mark it forged?

5 min read · try the examples if you haven't

Previous
Inheritance in PHP
3 / 7 · OOP in PHP
Next
Traits in PHP