In our previous lesson, we established a visual language for discussing software architecture using UML class diagrams. These diagrams show the static structure of a system—its classes and their relationships. Now, we'll delve into the principles that guide the design of a good structure. The goal isn't just to build things that work, but to build things that are robust, maintainable, and adaptable to change.
This lesson introduces the five SOLID principles of object-oriented design. As an experienced Laravel developer, you have almost certainly applied these principles, perhaps implicitly, as they are deeply embedded in the framework's philosophy. Our objective here is to formalize this knowledge, giving you a precise vocabulary to analyze and articulate design decisions. We will explore what each principle means and see how it manifests in practical PHP and Laravel code.
The SOLID Principles: An Overview
The SOLID acronym represents five principles that, when applied together, make it more likely that a developer will create a system that is easy to maintain and extend over time. They were introduced by Robert C. Martin ("Uncle Bob") and have become a cornerstone of modern object-oriented design.

We will examine each of these principles in detail. For each one, we'll start with a general definition and a clear PHP example, then connect it to the specific patterns and practices you're familiar with in the Laravel ecosystem.
S: Single Responsibility Principle (SRP)
A class should have one and only one reason to change, meaning it should have only one job.
This is arguably the most fundamental and frequently cited principle. It's about cohesion. When a class has multiple responsibilities, changes to one responsibility can inadvertently break another. It also makes the code harder to understand, test, and reuse.
To see a classic violation and refactoring of SRP, please read the following section from an article by DigitalOcean.
SOLID Design Principles Explained: Building Better ...
This section provides a clear, step-by-step example of applying the Single Responsibility Principle. It starts with a class that has two responsibilities (calculating area and formatting output) and refactors it into separate classes, each with a single responsibility.
Please read the section titled Single-Responsibility Principle. Pay close attention to the two stages of refactoring: first, moving the area() calculation into the Shape classes, and second, creating a separate SumCalculatorOutputter class.
This principle directly addresses a common anti-pattern in Laravel development: the "bloated controller." A controller's single responsibility should be to handle an incoming HTTP request and return a response. It shouldn't contain business logic, data validation, or complex data manipulation.
The following video from the popular "Laravel Daily" channel provides an excellent, concise demonstration of applying SRP in a Laravel context.
SOLID Principles in Laravel: 5 Examples (+ New Course!)
This segment shows a classic example of a bloated controller in Laravel and demonstrates how to refactor it by moving responsibilities to more appropriate classes.
Please watch from the explanation of SRP. Notice how the logic is extracted into a Form Request for validation and a Service class for the core business logic, leaving the controller clean and focused.
By delegating validation to a Form Request and business logic to a service (or an action, or a job), the controller adheres to SRP. Each class now has only one reason to change:
- The Controller changes if routing or the HTTP response changes.
- The Form Request changes if validation rules change.
- The Service changes if the business logic for creating an invoice changes.
O: Open/Closed Principle (OCP)
Software entities (e.g., classes, modules) should be open for extension but closed for modification.
This principle means you should be able to add new functionality to your system without changing existing, working code. Modifying existing code is risky; it can introduce bugs into features that were previously stable. OCP encourages designs that achieve new functionality through addition, not modification.
The DigitalOcean article demonstrates this by introducing an interface, which allows new shapes to be added without modifying the AreaCalculator. A common way this is achieved in Laravel is through patterns like Strategy or, more simply, with PHP's traits.
The following article from Martin Joo's blog provides a perfect, Laravel-native example using a Likeable trait.
SOLID Principles with Laravel - Martin Joo
This section explains OCP using two examples. The first, involving a Likeable trait, is a very clear and common pattern in Laravel for extending models.
Please read the first half of the section Open-Closed Principle. This demonstrates how a trait allows you to add "like" functionality to any model (Post, Comment, etc.) without ever modifying the base Model class or the models themselves. You extend them by useing the trait.
By using the Likeable trait, you can give any model the ability to be liked. When a new model ChatMessage needs this feature, you don't change any existing code; you simply extend ChatMessage by adding use Likeable;. The models are open for extension but closed for modification.
L: Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types without altering the correctness of the program.
This is a stricter, more formal version of OCP. It states that if you have a class Child that inherits from Parent, you should be able to use an object of Child anywhere you expect an object of Parent without anything breaking.
This means the child class must honor the contract of the parent class. It must not:
- Require stricter input parameters in its methods than the parent.
- Return a different type of value from its methods.
- Throw new types of exceptions that the parent doesn't.
The following article provides a great real-world example of the subtleties involved in adhering to LSP.
SOLID Principles with Laravel - Martin Joo
This section discusses LSP in the context of different EmailProvider implementations. It highlights practical issues that can cause violations, even when method signatures seem to match.
Please read the section on the Liskov Substitution Principle. The key insight here is how two subclasses can both return an array, yet violate LSP because the structure of those arrays is different, potentially breaking the client code. This is why using Data Transfer Objects (DTOs) is often recommended.
In PHP, strict adherence to type-hinting for method arguments and return types is the first and most important step towards satisfying LSP. Your experience with modern PHP (7.4+) and its type system is crucial here. LSP ensures that polymorphism works reliably. When your controller type-hints an interface, it can trust that any concrete implementation passed by Laravel's service container will behave as expected according to that interface's contract.
I: Interface Segregation Principle (ISP)
Clients should not be forced to depend on interfaces they don't use.
This principle tackles the problem of "fat" interfaces. If you have an interface with many methods, any class that implements it is forced to provide an implementation for all of those methods, even if it only needs one or two. This leads to cluttered code and often results in methods that just throw a NotImplementedException.
The solution is to break down large interfaces into smaller, more specific ones. A class can then implement only the interfaces for the functionality it actually provides.
SOLID Design Principles Explained: Building Better ...
This reading clearly illustrates the problem with a single, large interface and shows how to segregate it into smaller, more focused ones.
Please read the section on the Interface Segregation Principle. The example of splitting a ShapeInterface with area() and volume() into two separate interfaces is a perfect illustration of this principle.
You can see ISP in practice throughout the Laravel framework itself. For example, instead of one large "Convertible" interface, Laravel provides many small, single-method interfaces like:
Illuminate\Contracts\Support\Jsonable(with atoJson()method)Illuminate\Contracts\Support\Arrayable(with atoArray()method)Illuminate\Contracts\Support\Htmlable(with atoHtml()method)
A class can implement just the conversions it supports, without being forced to deal with the others.
D: Dependency Inversion Principle (DIP)
High-level modules should not depend on low-level modules; both should depend on abstractions.
This principle is at the heart of building decoupled, flexible, and testable applications. It states that a class should not depend on a concrete implementation of its dependency, but rather on an abstraction (typically an interface).
This "inverts" the traditional flow of dependency. Instead of HighLevelClass -> LowLevelClass, the relationship becomes HighLevelClass -> Interface <- LowLevelClass.
The PasswordReminder example in the DigitalOcean article is the canonical illustration of this.
SOLID Design Principles Explained: Building Better ...
This section explains Dependency Inversion using a clear example of a PasswordReminder class that needs a database connection. It shows how depending on an interface instead of a concrete class allows for easy swapping of implementations.
Please read the section on the Dependency Inversion Principle. This is the fundamental concept behind dependency injection and service containers.
This entire principle is the "why" behind Laravel's Service Container. When you type-hint an interface in a controller's constructor or method, you are applying DIP. You are telling Laravel, "I don't care how this functionality is implemented (MySQL, PostgreSQL, API); just give me something that satisfies this contract."
The binding of the interface to a concrete class happens in a Service Provider. This decouples your high-level business logic (the controller) from the low-level implementation details (the database driver). The following video shows this exact Laravel workflow.
SOLID Principles in Laravel: 5 Examples (+ New Course!)
This video segment provides a practical demonstration of Dependency Inversion in Laravel, showing how to bind an interface to a concrete class in a Service Provider.
Watch the segment on the Dependency Inversion Principle. This connects the theory of DIP directly to the AppServiceProvider and the app()->bind() method, which you've likely used many times.
Conclusion
In this lesson, we've formalized the five SOLID principles that guide robust object-oriented design. While they may seem theoretical, they have very practical implications for writing code that is clean, maintainable, and testable.
Here are the key takeaways:
- Single Responsibility Principle (SRP): Each class should have one job. Think of refactoring bloated controllers into services and form requests.
- Open/Closed Principle (OCP): Extend functionality by adding new code, not changing old code. Traits and polymorphism are key tools here.
- Liskov Substitution Principle (LSP): Subclasses must be perfect substitutes for their parent classes, honoring their contracts. Strict type-hinting is your best friend.
- Interface Segregation Principle (ISP): Prefer many small, specific interfaces over a few large, general-purpose ones.
- Dependency Inversion Principle (DIP): Depend on abstractions (interfaces), not on concrete implementations. This is the core idea behind Laravel's Service Container.
It's important to view these as guidelines, not unbreakable laws. As the Laravel Daily video mentioned, SRP and LSP (via type-hinting) are principles you'll apply almost constantly. The others, especially those heavy on interfaces, become increasingly critical as system complexity grows, particularly when building reusable packages or services—a domain you are quite familiar with.
In our next lesson, we will put this knowledge to the test. You will be given PHP code samples containing various design flaws, and your task will be to identify which SOLID principles are being violated.