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)
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.
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.
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.
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:
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'.
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.
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 save(). 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.
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.
PHP 8.4 Property Hooks on Interfaces
PHP 8.4 introduced property hooks, allowing interfaces to define get and set hooks for properties. This enables interfaces to enforce not just method signatures but also property access patterns. For example, an interface can require that a property be read-only via a get hook, or that setting a property triggers validation. This bridges the gap between abstract classes (which can define property logic) and interfaces (which previously only defined method contracts). However, property hooks in interfaces cannot include backing fields; they only define the hook behavior. Implementing classes must provide the actual property storage. This feature is particularly useful for value objects or DTOs where you want to enforce immutability or validation at the interface level. Example:
```php interface ReadableName { public string $name { get => $this->name; } }
class User implements ReadableName { public string $name { get => $this->name; } public function __construct(string $name) { $this->name = $name; } } ```
This ensures any class implementing ReadableName must expose a readable $name property, but the implementation details (like validation on set) remain flexible.
Static Methods on Interfaces (PHP 8.1)
PHP 8.1 introduced the ability to declare static methods in interfaces. This allows interfaces to define a contract for static methods, which can be used for factory patterns or named constructors. Prior to PHP 8.1, static methods could only be defined in abstract classes or traits. With this feature, you can enforce that implementing classes provide a specific static factory method. For example:
```php interface Factory { public static function create(array $data): self; }
class User implements Factory { public function __construct(private string $name) {}
public static function create(array $data): self { return new self($data['name']); } } ```
This is particularly powerful for dependency injection containers or when you want to standardize object creation across different implementations. However, note that static methods in interfaces are always public and cannot be declared as final or abstract (they are implicitly abstract). Also, you cannot call the static method on the interface itself; you must call it on the implementing class. This feature complements abstract classes by allowing static contracts without the overhead of inheritance.
Abstract Classes vs Interfaces with Default Methods Decision Guide
When choosing between abstract classes and interfaces with default methods (introduced in PHP 8.0), consider the following:
- State and Constructor Logic: If you need to share state (properties) or constructor logic, use an abstract class. Interfaces cannot have properties or constructors.
- Multiple Inheritance: If a class needs to inherit behavior from multiple sources, use interfaces (a class can implement many interfaces but extend only one abstract class).
- Default Implementation: Both can provide default method implementations. In abstract classes, default methods can access private state; in interfaces, default methods can only call other interface methods or use public/protected helpers.
- Evolution: Interfaces with default methods are better for evolving APIs without breaking existing implementations. Abstract classes are more rigid.
- Semantics: Use abstract classes for "is-a" relationships with shared base logic; use interfaces for "can-do" capabilities.
Example decision: If you have a Logger interface with a default method that writes to a file, but you want to allow custom loggers to override it, use an interface. If you have a log()Database abstract class with shared connection logic and a abstract method, use an abstract class.query()
In practice, many frameworks combine both: abstract classes implement interfaces to provide default behavior while still enforcing contracts.
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 { ... }| 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 |
| property-hooks-interface.php | interface ReadableName { | PHP 8.4 Property Hooks on Interfaces |
| static-method-interface.php | interface Factory { | Static Methods on Interfaces (PHP 8.1) |
| decision-guide.php | interface Logger { | Abstract Classes vs Interfaces with Default Methods Decision |
Key takeaways
final method in an abstract class that calls abstract sub-steps) guarantees execution orderInterview 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?
Frequently Asked Questions
20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.
That's OOP in PHP. Mark it forged?
8 min read · try the examples if you haven't