O — Open/Closed Principle: open for extension, closed for modification
Concept
The Open/Closed Principle states: software entities should be open for extension, but closed for modification. Bertrand Meyer introduced the phrase in 1988; Robert Martin gave it the modern interpretation. The idea is that once a class is working and tested, you should be able to add new behavior to your system without touching that class's source code.
The key word is "modification." Every time you open a working class and change its existing logic, you risk breaking callers that depended on the old behavior. OCP says: design your system so that new requirements are handled by adding new code, not by editing existing code.
The mechanism that makes OCP possible is abstraction — specifically, interfaces and polymorphism. Instead of hardcoding behavior, you depend on an abstraction (interface), and new behavior arrives as new implementations of that abstraction. The core class never changes; you just plug in a new variant.
The most direct OCP violation is the type-switch anti-pattern: a method (or chain of if/else blocks) that behaves differently based on the type of some input. Every time a new type is added, you must open and modify this central method.
| Pattern | OCP Stance |
|---|---|
if ($type === 'paypal') {...} elseif ($type === 'stripe') {...} | Closed for extension — each new payment method requires editing |
interface PaymentGateway with StripeGateway, PaypalGateway implementing it | Open for extension — new gateway = new class, zero changes to caller |
switch ($discount->type) inside OrderPricer | Each new discount type requires editing OrderPricer |
interface DiscountStrategy with PercentageDiscount, FlatDiscount, FreeShipping | New discount = new class, OrderPricer never changes |
OCP does not mean you can never change existing code. It means you should design so that the common types of change (adding new variants) don't require touching stable, tested classes. You still refactor, fix bugs, and improve — but new feature additions should flow through extension, not modification.
Code Example
<?php
declare(strict_types=1);
// VIOLATION: Adding a new payment method requires modifying PaymentProcessor
class PaymentProcessor
{
public function charge(Order $order, string $method): void
{
if ($method === 'stripe') {
// Stripe-specific charging logic...
echo "Charging via Stripe: {$order->total}\n";
} elseif ($method === 'paypal') {
// PayPal-specific charging logic...
echo "Charging via PayPal: {$order->total}\n";
} elseif ($method === 'crypto') {
// Must MODIFY this class to add crypto
echo "Charging via Crypto: {$order->total}\n";
}
// Every new payment method = open this file, add an elseif
}
}
// CORRECT: PaymentProcessor is closed to modification, open to extension
interface PaymentGateway
{
public function charge(Order $order): PaymentResult;
public function refund(string $chargeId, float $amount): void;
}
final class StripeGateway implements PaymentGateway
{
public function __construct(private readonly string $apiKey) {}
public function charge(Order $order): PaymentResult
{
// Stripe SDK call here
return new PaymentResult(
chargeId: 'ch_' . bin2hex(random_bytes(8)),
status: 'succeeded',
);
}
public function refund(string $chargeId, float $amount): void
{
// Stripe refund logic
}
}
final class PayPalGateway implements PaymentGateway
{
public function __construct(private readonly string $clientId, private readonly string $secret) {}
public function charge(Order $order): PaymentResult
{
// PayPal SDK call here
return new PaymentResult(
chargeId: 'pp_' . bin2hex(random_bytes(8)),
status: 'completed',
);
}
public function refund(string $chargeId, float $amount): void
{
// PayPal refund logic
}
}
// NEW REQUIREMENT: Add crypto payment — zero changes to PaymentProcessor
final class CryptoGateway implements PaymentGateway
{
public function charge(Order $order): PaymentResult
{
return new PaymentResult(
chargeId: 'btc_' . bin2hex(random_bytes(8)),
status: 'confirmed',
);
}
public function refund(string $chargeId, float $amount): void
{
throw new \RuntimeException('Crypto payments are non-refundable');
}
}
// PaymentProcessor is CLOSED — it never changes when new gateways arrive
final class PaymentProcessor
{
public function __construct(private readonly PaymentGateway $gateway) {}
public function charge(Order $order): PaymentResult
{
$result = $this->gateway->charge($order);
if ($result->status !== 'succeeded' && $result->status !== 'completed' && $result->status !== 'confirmed') {
throw new PaymentFailedException("Payment failed: {$result->status}");
}
return $result;
}
}
// Wiring in Laravel service provider or a factory
$processor = new PaymentProcessor(new StripeGateway(config('services.stripe.key')));
// Switch to crypto? Just swap the gateway. PaymentProcessor untouched.
$processor = new PaymentProcessor(new CryptoGateway());Interview Q&A
Q: How does the Strategy pattern implement OCP?
The Strategy pattern is OCP in action. You define an interface (the strategy) and implement it with concrete classes (the strategies). The class that uses the strategy (the context) is closed to modification because it only knows the interface. New strategies are new classes, not modifications to the context. In a payment system: PaymentGateway is the interface, StripeGateway and PaypalGateway are strategies, and PaymentProcessor is the closed context. Adding a new payment method means adding a new strategy class — the processor never changes.
Q: Does OCP mean you should anticipate every future requirement and make everything extensible?
No — that is the trap that leads to over-engineered abstractions. OCP should be applied reactively, not speculatively. When you see a real, recurring type-switch or if-chain that has been modified twice already to add new cases, that is the signal to refactor toward OCP. Design for the extensions you know are coming or have come twice; do not pre-abstract everything. YAGNI (You Aren't Gonna Need It) is the counterbalance to OCP. A good engineer applies OCP at the points of actual change pressure.
Q: How does OCP apply in a Laravel codebase beyond payment gateways?
Everywhere you see configuration-driven behavior. Laravel's filesystem abstraction (Storage + FilesystemAdapter) is OCP: adding an S3 driver does not change the Storage facade. Same with mail drivers, queue drivers, notification channels, and broadcast drivers. In application code: a ReportGenerator that handles new report formats via a ReportFormatter interface is OCP. An ExportService that switches on file type (if ($format === 'csv')...) is not. The Laravel Illuminate codebase itself is a masterclass in OCP — nearly every feature is extensible through drivers and adapters without modifying the core.