PHP Interfaces vs Abstract Classes — Silent Refund Failure
A missing interface method caused silent refund failures with 200 OK but no processing.
20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- 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)
PHP interfaces and abstract classes are two distinct tools for defining contracts and shared behavior in object-oriented code, but they are not interchangeable despite often being confused. An interface is a pure contract — it declares method signatures that implementing classes must define, with zero implementation logic.
An abstract class sits in the middle: it can provide concrete method implementations while leaving other methods abstract for subclasses to fill in. The critical difference is that a class can implement multiple interfaces but extend only one abstract class, making interfaces the go-to for polymorphic flexibility and abstract classes for sharing state or partial logic across a family of related classes.
In practice, mixing them up leads to silent failures — for example, defining a refund processing interface that requires a method, then using an abstract class to provide default refund logic, only to have a subclass forget to call process()parent::process() or override the method incorrectly. PHP won't warn you because the abstract class's concrete method is optional to override, unlike an interface method which must be implemented.
This is where refunds silently fail: the contract is technically satisfied, but the expected behavior is missing.
Frameworks like Laravel and Symfony rely heavily on this distinction. Laravel's Illuminate\Contracts namespace uses interfaces for its service container bindings (e.g., Cache, Queue), while abstract classes like Illuminate\Support\Manager provide shared boilerplate for driver-based systems.
Symfony's EventSubscriberInterface is a pure contract, whereas AbstractController gives concrete helper methods. The rule of thumb: use interfaces when you need to enforce a capability across unrelated classes (e.g., Refundable), and abstract classes when you have a clear is-a hierarchy with shared logic (e.g., BasePaymentGateway).
Combining both — an interface for the contract and an abstract class for default implementation — gives you the most flexible architecture, letting consumers depend on the interface while inheriting sensible defaults.
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.
Why PHP Interfaces and Abstract Classes Are Not Interchangeable
An interface in PHP is a contract that enforces method signatures without any implementation. An abstract class is a partially implemented blueprint that can define shared logic while leaving some methods abstract. The core mechanic: interfaces solve for type compatibility (what a class can do), while abstract classes solve for code reuse (what a class is).
In practice, a class can implement multiple interfaces but extend only one abstract class. Interfaces cannot define properties or constructors; abstract classes can. This means interfaces are ideal for polymorphic behavior across unrelated classes, while abstract classes are better for sharing state or default behavior in a hierarchy. A common mistake is using an abstract class when an interface would suffice, coupling consumers to a base class they don't need.
Use an interface when you need to guarantee a capability across disparate types — e.g., a Refundable interface for both Order and Subscription. Use an abstract class when you have a clear "is-a" relationship with shared logic — e.g., AbstractPaymentGateway for StripeGateway and PayPalGateway. Choosing wrong leads to brittle inheritance trees and silent failures when a class cannot extend the required abstract class.
RefundProcessor class. When a new GiftCardRefund class needed to extend GiftCardBase (another abstract), it couldn't extend RefundProcessor. The refund method was never called, and money was never returned — no error, no log. The rule: if a capability must be available across unrelated types, use an interface, not an abstract class.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.
<?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');
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 as abstract, forcing each child to implement its own delivery mechanism.send()
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.
<?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();
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.final in an abstract class, subclasses cannot override it.final when the method enforces a critical invariant like logging or validation that must always run.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.
<?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);
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:
- 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.
- Do you need to enforce a contract across completely unrelated classes? Interfaces are the only way to make a
Mailerand aPaymentGatewayboth implement a commonLoggableInterface— they have zero relationship but both can log. - 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.
Here's a quick decision tree for the design phase:
<?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'; }
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(). An abstract class pop()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'.
<?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; } }
Why Type Errors Sneak Past Abstract Classes But Not Interfaces
You've seen it happen. A dev extends an abstract class, overrides a method, and silently changes the return type to something narrower. PHP lets it slide. Interfaces? Not a chance. They enforce the exact signature contract, no exceptions.
Abstract classes give you shared logic but a blind spot: they can't enforce type covariance the way interfaces do. If your base class returns ParentModel, a child can return ChildModel without PHP complaining. That breaks client code expecting ParentModel.
Interfaces force explicit declaration of every parameter and return type. No hidden surprises. When you need absolute type safety across implementations — like in a service container or repository pattern — interfaces are the wall you want. Abstract classes are for code reuse, not contract enforcement.
Here's the hard truth: if you're writing an abstract class with no shared logic, you've already made the wrong choice. Convert it to an interface. Your IDE and your future self will thank you.
// io.thecodeforge — php tutorial abstract class PaymentProcessor { abstract public function charge(): Transaction; } class StripeProcessor extends PaymentProcessor { public function charge(): StripeTransaction { // PHP allows this return new StripeTransaction(); } } // Caller expects Transaction, gets StripeTransaction // Works now, breaks if StripeTransaction adds methods interface PaymentGateway { public function charge(): Transaction; } class StripeGateway implements PaymentGateway { public function charge(): Transaction { // Must return Transaction exactly return new Transaction(); } }
The Hidden Cost of Multiple Inheritance — And Why PHP Chose Interfaces
Java went full abstract class for everything. C++ gives you multiple inheritance straight up. PHP? It cut the knot: classes can extend only one parent, but implement infinite interfaces. That decision wasn't arbitrary — it was survival.
Multiple inheritance breeds diamond problems. Class A defines . Class B overrides it. Class C overrides again. Your class D inherits from B and C. Which save() wins? PHP said: "Not my circus, not my monkeys." Interfaces avoid this because they carry no implementation. No baggage, no ambiguity.save()
This isn't academic. In production, I've seen teams pile five levels of abstract classes to share a Logger trait. Then a new dev adds a method to the middle class, and half the app silently behaves differently. With interfaces, you compose behavior via traits or dependency injection — not inheritance hierarachies.
Trait composition + interface contracts is the PHP way. It gives you reuse without the nightmare. Stop building inheritance skyscrapers. Your cache layer doesn't need a grandparent class.
// io.thecodeforge — php tutorial interface LoggerInterface { public function log(string $message): void; } interface CacheInterface { public function get(string $key): ?string; } class FileLogger implements LoggerInterface { public function log(string $message): void { // write to file } } class RedisCache implements CacheInterface { public function get(string $key): ?string { // return from redis } } // Clean composition via DI, not inheritance class Service { public function __construct( private LoggerInterface $logger, private CacheInterface $cache ) {} } // No diamond. No ambiguity. No abstract class pile.
Silent Refund Failure: The Missing Interface Method
StripeGateway class implemented PaymentGatewayInterface and the charge method worked, the team assumed all methods were correctly implemented.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.- 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.
PHPStan with --level=6 to catch this automatically.$this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);).interface CacheableInterface { public function getCacheKey(): string; }class Logger implements CacheableInterface { ... }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 { ... } }interface PaymentGatewayInterface { public function charge(int $amount): string; }class StripeGateway implements PaymentGatewayInterface { ... }class StripeGateway extends AbstractPaymentGateway implements RefundableInterface { ... }RefundableInterface defines refund(): bool; StripeGateway implements both.| Feature / Aspect | Interface | Abstract Class |
|---|---|---|
| Can contain method bodies | No (PHP 8 allows default interface methods? No — only constants) | Yes — mix of concrete and abstract methods |
| Can contain properties | No (only constants) | Yes — any visibility |
| Multiple inheritance | A class can implement many interfaces | A class can only extend one abstract class |
| Constructor allowed | No | Yes |
| Instantiate directly | No — fatal error | No — fatal error |
| Keyword to use | implements | extends |
| Best for | Defining a contract for unrelated classes | Shared base for closely related classes |
| Relationship type | Can-do / Has-capability | Is-a / Parent-child |
| PHP version requirement | PHP 5+ | PHP 5+ |
| Access modifiers on methods | Always public | public, protected (not private) |
| File | Command / Code | Purpose |
|---|---|---|
| PaymentGatewayInterface.php | interface PaymentGatewayInterface | Interfaces |
| NotificationAbstract.php | abstract class Notification | Abstract Classes |
| CombinedGatewayPattern.php | interface PaymentGatewayInterface | Combining Both |
| decision_guide.php | /** | When to Use Interfaces vs Abstract Classes |
| FrameworkPattern.php | namespace Io\TheCodeforge\Cache; | Real-World Patterns in PHP Frameworks (Laravel, Symfony) |
| TypeSafetyBreach.php | abstract class PaymentProcessor { | Why Type Errors Sneak Past Abstract Classes But Not Interfac |
| DiamondProblem.php | interface LoggerInterface { | The Hidden Cost of Multiple Inheritance |
Key takeaways
final method in an abstract class that calls abstract sub-steps) guarantees execution orderCommon mistakes to avoid
3 patternsDeclaring properties inside an interface
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
Using an abstract class when an interface is the right tool
Interview Questions on This Topic
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?
Can a PHP class implement multiple interfaces? Can it extend multiple abstract classes? Why does this asymmetry exist, and what design problem does it prevent?
If you have an abstract class that already implements an interface, do all concrete subclasses automatically satisfy that interface? What happens if an abstract method in the class matches an interface method that is NOT yet implemented?
Describe a real scenario where using an abstract class would be a mistake and an interface would have been better. How would you refactor it?
DatabaseLogger and FileLogger both extend AbstractLogger because they share a log() method signature, but the actual logging logic is completely different and there's no shared implementation. This creates an artificial is-a relationship. A better design is to define a LoggerInterface with a log() method, and let both classes implement it directly. This gives you flexibility to switch loggers in the container, mock them in tests, and later add an EmailLogger that might need different constructor parameters — something the abstract class would have forced you to add a dummy parameter to.Frequently Asked Questions
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.
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.
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.
Use a Trait when you need to reuse implementation across unrelated classes that don't share an 'is-a' relationship. Traits avoid single inheritance limitations but can introduce conflicts if multiple traits define the same method. Abstract classes are still the right choice when there's a natural parent-child relationship and you want to enforce a contract via abstract methods.
20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.
That's OOP in PHP. Mark it forged?
6 min read · try the examples if you haven't