Geekflare is supported by our audience. We may earn affiliate commissions from buying links on this site.
Share on:

Writing Maintainable Code: SOLID Principles Explained in PHP (Laravel)

Invicti Web Application Security Scanner – the only solution that delivers automatic verification of vulnerabilities with Proof-Based Scanning™.

Writing computer programs is great fun. Unless you have to work with other people’s code.

If you’ve worked as a professional developer for more than three days, you know our jobs are anything but creative and exciting. One part of the reason is the company leadership (read: folks who never get it), while the other part is the complexity of the code we have to work with. Now, while we can do absolutely nothing about the former, we can do a lot about the latter.

So, why are the codebases so complex that we feel like gutting ourselves? Simply because the folks that wrote the first version were in a hurry, and those who came later just kept adding to the mess. The end result: a soupy mess that very few want to touch and nobody understands.

Welcome to the first day of the job!

“Nobody said it’d be so hard . . .”

But it doesn’t have to be this way.

Writing good code, code that is modular and easy to maintain, isn’t that hard. Just five simple principles — long-established and well known — if followed with discipline, will make sure your code is readable, to others and to you when you look at it six months later. 😂

These guiding principles are represented by the acronym SOLID. Perhaps you’ve heard of the term “SOLID principles” before, perhaps not. In case you have but have been putting this learning off to “someday”, well, let’s make sure today is that day!

So, without further ado, let’s look at what this SOLID stuff is all about and how it can help us write a really neat tight code.

“S” is for Single Responsibility

If you look at different sources describing the Single Responsibility Principle, you’ll get some variation in its definition. However, in simpler terms, it boils down to this: Each class in your codebase should have a very specific role; that is, it should be responsible for nothing more than a single goal. And whenever a change a needed in that class, it follows that we will need to change it only because that one specific responsibility has changed.

When I came across this the first time, the definition I was presented with was, “There should be one, and only one reason for a class to change”. I was like, “What??! Change? What change? Why change?”, which is why I said earlier that if you read about this in different places, you’ll get related but somewhat different and potentially confusing definitions.

Anyway, enough of it. It’s time for something serious: if you’re like me, you’re probably wondering, “Okay, all good. But why the heck should I care? I’m not going to start writing code in a totally different style from tomorrow just because some madman who once wrote a book (and is now dead) says so.”


And that’s the spirit we need to maintain if we want to truly learn things. So, why does all this song and dance about “Single Responsibility” matter at all? Different people explain this differently, but for me, this principle is all about bringing discipline and focus into your code.

Focus, my lad. Focus!

Let’s see an example before I explain my interpretation. Unlike other resources found on the web that provide examples that you do understand but then leave you wondering how they’d help you in real-world cases, let’s dive into something specific, a coding style we see over and over, and perhaps even write in our Laravel applications.

When a Laravel application receives a web request, the URL is matched against the routes you’ve defined in web.php and api.php, and if there’s a match, the request data reaches the controller. Here’s what a typical controller method looks like in actual, production-level applications:

class UserController extends Controller {
    public function store(Request $request)
        $validator = Validator::make($request->all(), [
           'first_name' => 'required',
           'last_name' => 'required',
           'email' => 'required|email|unique:users',
           'phone' => 'nullable'
       if ($validator->fails()) {
            Session::flash('error', $validator->messages()->first());
            return redirect()->back()->withInput();
       // create new user
       $user = User::create([
           'first_name' => $request->first_name,
           'last_name' => $request->last_name,
           'email' => $request->email,
           'phone' => $request->phone,
       return redirect()->route('login');

We’ve all written code like this. And it’s easy to see what it does: register new users. It looks fine and works fine, but there’s a problem — it’s not future-proof. And by future-proof, I mean that it’s not ready to handle change without creating a mess.

Why so?

You can tell that the function is meant for routes defined in the web.php file; that is, traditional, server-rendered pages. A few days go by, and now your client/employer is getting a mobile app developed, which means this route will be of no use for users registering from mobile devices. What do you do? Create a similar route in the api.php file and write a JSON-driven controller function for it? Fine, and then what? Copy all the code from this function, make a few changes, and call it a day? This is indeed what many developers are doing, but they’re setting themselves up for failure.

The problem is that HTML and JSON are not the only API formats in the world (let’s just consider HTML pages to be an API for the sake of argument). What about a client who has a legacy system running on the XML format? And then there’s another one for SOAP. And gRPC. And God knows what else will come the next day.

You might still consider creating a separate file for each of these API types and copy the existing code, modifying it slightly. Sure there are ten files, you’d argue, but it’s all working fine so, why complain? But then comes the gut-punch, the nemesis of software development — change. Suppose now, that your client/employer’s needs have changed. They now want that, at the time of user registration, we log the IP address as well as add an option for a field that indicates they’ve read and understood the terms and conditions.

Oh, oh! We no have ten files to edit, and we must make sure that the logic is handled exactly the same in all of them. Even a single error can cause major business loss. And now imagine the horror in large-scale SaaS apps, as the complexity of the code is already quite high.

Damn . . .

How did we reach this hell?

The answer is that the controller method that looks so harmless is actually doing a number of different things: it’s validating the incoming request, it’s handling redirects, and it’s creating new users.

It’s doing too many things! And yes, as you might have noticed, knowing how to create new users in the system shouldn’t actually be a controller methods job. If we were to take this logic out of the function and put it in a separate class, we’d now have two classes, each with a single responsibility to handle. While these classes can take help from each other by calling their methods, they are not allowed to know what’s going on inside the other one.

class UserController extends Controller {
    public function store(Request $request)
        $validator = Validator::make($request->all(), [
           'first_name' => 'required',
           'last_name' => 'required',
           'email' => 'required|email|unique:users',
           'phone' => 'nullable'
       if ($validator->fails()) {
            Session::flash('error', $validator->messages()->first());
            return redirect()->back()->withInput();
       return redirect()->route('login');

Look at the code now: much more compact, easy to understand . . . and most importantly, adaptive to change. Continuing our earlier discussion where we had ten different types of API, each of those now calls a single function UserService::createNewUser($request->all()); and be done with it. If changes are needed in the user registration logic, the UserService class will see to it while the controller methods don’t need to change at all. If SMS confirmation needs to be set after user registration, the UserService will take care of it (by calling some other class that knows how to send SMS), and again the controllers are left untouched.

This is what I meant by focus and discipline: focus in code (one thing doing one thing only) and discipline by the developer (not falling for short-term solutions).

Well, that was quite a tour! And we’ve covered just one of the five principles. Let’s move on!

“O” is for Open-Closed

I must say that whoever came up with the definitions of these principles certainly wasn’t thinking of less experienced developers. The same is the case with the Open-Closed Principle, and the ones to come are a step ahead in weirdness. 😂😂

Regardless, let’s look at the definition found everyone for this principle: Classes should be open for extension but closed for modification. Eh?? Yes, I too wasn’t amused when I first came across it, but over time I’ve come to understand — and admire — what this rule is trying to say: code once is written shouldn’t need to be changed.

In a philosophical sense, this rule is great — if code doesn’t change, it will remain predictable and new bugs will not be introduced. But how is it even possible to dream of code that doesn’t change when all we’re doing as developers is chasing the coattails of change all the time?

Well, first off, the principle doesn’t mean that not even one line of existing code is allowed to change; that would be straight out of a fairyland. The world changes, business changes, and thus, code changes — no getting around that. But what this principle does mean is that we restrict the possibility of changing existing code as much as possible. And it also kinda tells you how to do that: classes should be open for extension and closed for modification.

“Extension” here means reuse, whether the reuse comes in the form of child classes inheriting functionality from a parent class, or other classes store instances of a class and call its methods.

So, back to the million-dollar question: how do you write code that survives change? And here, I’m afraid, nobody has a clear answer. In Object-Oriented Programming, several techniques have been discovered and refined to achieve this goal, right from these SOLID principles we are studying to common design patterns, enterprise patterns, architectural patterns, and whatnot. There’s no perfect answer, and so a developer must keep going higher and higher, gathering as many tools as he can and try to do his best.

With that in mind, let’s look at one such technique. Suppose we need to add the functionality to convert a given HTML content (maybe an invoice?) into a PDF file and also force an immediate download in the browser. Let’s also suppose we have the paid subscription of a hypothetical service called MilkyWay, which will do the actual PDF generation. We might end up writing a controller method like this:

class InvoiceController extends Controller {
    public function generatePDFDownload(Request $request) {
        $pdfGenerator = new MilkyWay();
        $pdfGenerator->apiKey = env('MILKY_WAY_API_KEY');
        $pdfGenerator->setContent($request->content); // HTML format
        $pdfFile = $pdfGenerator->generateFile('invoice.pdf');

        return response()->download($pdfFile, [
            'Content-Type' => 'application/pdf',

I left out request validation, etc., in order to focus on the core issue. You’ll notice that this method does a good job of following the Single Responsibility Principle: it doesn’t attempt to walk through the HTML content passed to it and create a PDF (in fact, it doesn’t even know it’s been given HTML); instead, it passes on that responsibility to the specialized MilkyWay class, and presents whatever it gets, as a download.

But there’s a slight problem.

Our controller method is too dependent on the MilkyWay class. If the next version of the MilkyWay API changes the interface, our method will stop working. And if we wish to use some other service someday, we’ll have to literally do a global search in our code editor and change all the code snippets that mention MilkyWay. And why is that bad? Because it greatly increases the chance of making a mistake and is a burden on the business (developer time spent on sorting out the mess).

All this waste because we created a method that was not closed to change.

Can we do better?

Yes, we can!

In this case, we can take advantage of a practice that goes something like this — program to interfaces, not implementations.

Yeah, I know, it’s another of those OOPSisms that make no sense the first time. But what it’s saying is that our code should depend on types of things, and not particular things themselves. In our case, we need to free ourselves from having to depend on the MilkyWay class, and instead depend on a generic, a type of PDF class (it will all become clear in a second).

Now, what tools do we have in PHP for creating new types? Broadly speaking, we have Inheritance and Interfaces. In our case creating a base class for all PDF classes will not be a great idea because it’s hard to imagine different types of PDF engines/services sharing the same behavior. Maybe they can share the setContent() method, but even there, the process of acquiring content could be different for each PDF service class, so typing everything up in an Inheritance hierarchy will make things worse.

With that understood, let’s create an interface that specifies which methods we want all of our PDF engine classes to contain:

interface IPDFGenerator {
    public function setup(); // API keys, etc.
    public function setContent($content);
    public function generatePDF($fileName = null);

So, what do we have here?

Through this interface, we’re saying that we expect all our PDF classes to have at least those three methods. Now, if the service we want to use (MilkyWay, in our case) doesn’t follow this interface, it’s our job to write a class that does that. A rough sketch of how we might write a wrapper class for our MilkyWay service is as follows:

class MilkyWayPDFGenerator implements IPDFGenerator {
    public function __construct() {

    public function setup() {
        $this->generator = new MilkyWay();
        $this->generator->api_key = env('MILKY_WAY_API_KEY');

    public function setContent($content) {

    public function generatePDF($fileName) {
        return $this->generator->generateFile($fileName);

And just like this, whenever we have a new PDF service, we will write a wrapper class for it. As a result, all those classes will be considered to be of type IPDFGenerator.

So, how is all this connected to the Open-Closed Principle and Laravel?

To reach that point we must know two more key concepts: Laravel’s container bindings, and a very common technique called dependency injection.  Again, big words, but dependency injection simply means that instead of creating objects of classes yourself, you mention them in function arguments and something will create them for you automatically. This frees you from having to write code such as $account = new Account(); all the time and makes the code more testable (a topic for another day). This “something” that I mentioned takes the form of the Service Container in the Laravel world.

For now, just think of it as something that can create new class instances for us. Let’s see how this helps.

In the Service Container in our example, we can write something like this:

$this->app->bind('App\Interfaces\IPDFGenerator', 'App\Services\PDF\MilkyWayPDFGenerator');

This is basically saying, anytime someone asks for an IPDFGenerator, hand them the MilkyWayPDFGenerator class. And after all that song and dance, ladies and gentlemen, we come to the point where it all falls into place and the Open-Closed Principle is revealed at work!

Armed with all this knowledge, we can rewrite our PDF download controller method like this:

class InvoiceController extends Controller {
    public function generatePDFDownload(Request $request, IPDFGenerator $generator) {
        $pdfFile = $generator->generatePDF('invoice.pdf');

        return response()->download($pdfFile, [
            'Content-Type' => 'application/pdf',

Notice the difference?

First off, we are receiving our PDF generator class instance in the function argument. This is being created and passed to us by the Service Container, as e discussed earlier. The code is also cleaner and there’s no mention of API keys, etc. But, most importantly, there’s no trace of the MilkyWay class. This also has an added side-benefit of making the code easier to read (someone reading it for the first time won’t go, “Whoa! WTF is this MilkyWay??” and constantly worry about it at the back of their head).

But the biggest benefit of all?

This method is now closed for modification and change-resistant. Allow me to explain. Suppose tomorrow we feel that the MilkyWay service is too expensive (or, as it often happens, their customer support has become shitty); as a result, we tried out another service called SilkyWay and want to move to it. All we now need to do is write a new IPDFGenerator wrapper class for SilkyWay and change the binding in our Service Container code:

$this->app->bind('App\Interfaces\IPDFGenerator', 'App\Services\PDF\SilkyWayPDFGenerator');

That’s all!!

Nothing else needs to change, because our application is written according to an interface (the IPDFGenerator interface) instead of a concrete class. The business requirement changed, some new code was added (a wrapper class) and only one line of code was changed — everything else remains untouched and the entire team can go home with confidence and sleep peacefully.

Want to sleep peacefully? Follow the Open-Closed Principle! 🤭😆

“L” is for Liskov Substitution


This thing sounds like something straight out of an Organic Chemistry textbook. It might even make you regret picking up software development as a career because you thought it was all practical and no theory.

“Quick, kid! What’s the largest prime number that can be expressed as a sum of two prime numbers?”

But hold your horses for a second! Trust me, this principle is as easy to understand as it is intimidating in its name. In fact, it might just be the easiest principle of the five to understand (well, err . . . if not the easiest, then at least it will have the shortest, most straightforward explanation).

This rule simply says that code that works with parent classes (or interfaces) should not break when those classes are replaced with child classes (or interface-implementing classes). The example we finished just before this section is a great illustration: if I replace the generic IPDFGenerator type in the method argument with the specific MilkyWayPDFGenerator instance, would you expect the code to break or keep on working?

Keep on working, of course! That’s because the difference is only in names, and both the interface as well as the class have the same methods working in the same way, so our code will work as before.

So, what’s the big deal about this principle? Well, in simpler terms, that’s all this principle is saying: make sure your subclasses implement all the methods exactly as required, with the same number and type of arguments, and the same return type. If even one parameter were to differ, we would unknowingly keep on building more code on top of it, and one day we will have the kind of stinking mess whose only solution would be to take it down.

There. That wasn’t so bad now, was it? 😇

A lot more can be said about Liskov Substitution (look up its theory and read up on Covariant Types if you’re really feeling brave), but in my opinion, this much is enough for the average developer coming across these mysterious lands of patterns and principles for the first time.

“I” is for Interface Segregation

Interface Segregation . . . hmm, that doesn’t sound so bad, does it? Sounds like it’s something to do with segregating . . . umm, separating . . . interfaces. I just wonder where and how.

If you were thinking along these lines, trust me, you’re almost done understanding and using this principle. If all the five SOLID principles were investment vehicles, this one would deliver the most long-term value in learning to code well (okay, I realize I say that about every principle, but you know, you get the idea).

Stripped of highfalutin jargon and distilled down to its most basic form, the principle of Interface Segregation has this to say: The more numerous, more specialized interfaces there are in your application, the more modular and less weird your code will be.

Let’s look at a very common and practical example. Every Laravel developer comes across the so-called Repository Pattern in their career, after which they spend the next few weeks going through a cyclical phase of high and lows, and eventually discard the pattern. Why? In all tutorials covering the Repository Pattern, you’re advised to create a common interface (called a Repository) which will define the methods needed to access or manipulate data. This base interface might look something like this:

interface IRepository {
    public function getOne($id);
    public function getAll();
    public function create(array $data);
    public function update(array $data, $id);
    public function delete($id);

And now, for your User model, you’re supposed to create a UserRepository that implements this interface; then, for your Customer model, you’re supposed to create a CustomerRepository that implements this interface; you get the idea.

Now, it so happened in one of my projects that some of the models were not supposed to be writable by anyone but the system. Before you start rolling your eyes, consider that logging or maintaining an audit trail is a good, real-world example of such “read-only” models. The problem I faced was that since I was supposed to create repositories that all implemented the IRepository interface, say the LoggingRepository, at least two of the methods in the interface, update() and delete() were of no use to me.

Yes, a quick fix would be to implement these anyway and either leave them blank or raise an exception, but if relying on such duct-tape solutions were okay for me, I wouldn’t be following the Repository Pattern in the first place!

Heeeeelp! I’m stuck. :'(

Does that mean it’s all the fault of the Repository pattern?

No, not at all!

In fact, a Repository is a well known and accepted pattern that brings consistency, flexibility, and abstraction to your data access patterns. The problem is that the interface we created — or should I say the interface that popularized in practically every tutorial — is too broad.

Sometimes this idea is expressed by saying that the interface is “fat”, but it means the same thing — the interface makes too many assumptions, and thus adds methods that are useless to some classes but still those classes are forced to implement them, resulting in brittle, confusing code. Our example might have been a bit simpler, but imagine what mess can be created when several classes have implemented methods they didn’t want, or those they wanted but were missing in the interface.

The solution is simple, and is also the name of the principle we’re discussing: Interface Segregation.

The point is, we shouldn’t create our interfaces blindly. And we also shouldn’t make assumptions, no matter how experienced or smart we think we are. Instead, we should create several smaller, specialized interfaces, letting classes implement those that are needed, and leaving out those that are not.

In the example we discussed, I might have created two interfaces instead of one:  IReadOnlyRespository (containing the functions getOne() and getAll()), and IWriteModifyRepository (containing the rest of the functions). For regular repositories, I’d then say class UserRepository implements IReadOnlyRepository, IWriteModifyRepository { . .. }. (Side note: Special cases might still arise, and that is fine because no design is perfect. You might even want to create a separate interface for every method, and that will be fine too, assuming your project’s needs are that granular.)

Yes, there are more interfaces now, and some might say there’s too much to remember or that the class declaration now is too long (or looks ugly), etc., but look at what we have gained: specialized, small-sized, self-contained interfaces that can be combined as needed and won’t get in each other’s way. As long as you write software for a living, remember that it’s the ideal everyone is striving for.

“D” is for Dependency Inversion

If you’ve read the earlier parts of this article, you might possibly feel that you understand what this principle is trying to say. And you’d be right, in the sense that this principle is more or less a repetition of what we’ve discussed till now. Its formal definition isn’t too scary, so let’s look at it: High-level modules shouldn’t depend on low-level modules; both should depend upon abstractions.

Yes, makes sense. If I have a high-level class (high-level in the sense that it uses other smaller, more specialized classes to perform something and then make some decisions), we shouldn’t have that high-level class depending on a particular low-level class for some type of work. Rather, they should be coded to rely on abstractions (such as base classes, interfaces, etc.).


We already saw a great example of it in the earlier part of this article. If you used a PDF-generation service and your code was littered with new ABCService() class, the day the business decided to use some other service would be the day remembered forever — for all the wrong reasons! Rather, we should use a general form of this dependency (create an interface for PDF services, that is), and let something else handle its instantiation and pass it to us (in Laravel, we saw how the Service Container helped us do that).

All in all, our high-level class, which was earlier in control of creating lower-level class instances, now has to look to something else. The tables have turned, and that’s why we call this an inversion of dependencies.

If you’re looking for a practical example, go back to the part in this article where we discuss how to rescue our code from having to depend exclusively on the MilkyWay PDF class.

. . .

Guess what, that’s it! I know, I know, it was quite a long and hard read, and I want to apologize for that. But my heart goes out to the average developer who’s been doing things intuitively (or the way they were taught to him) and can make neither head nor tail of the SOLID principles. And I’ve done my best to keep the examples as close to a Laravel developer’s workday as possible; after all, what use to us are examples that contain Vehicle and Car classes — or even advanced generics, reflection, etc. — when none of us is going to be authoring libraries.

If you found this article useful, please leave a comment. This will confirm my notion that developers are really struggling to make sense of these “advanced” concepts, and I’ll be motivated to write on other such topics. Until later! 🙂

Thanks to our Sponsors
More great readings on Development
Power Your Business
Some of the tools and services to help your business grow.
  • Invicti uses the Proof-Based Scanning™ to automatically verify the identified vulnerabilities and generate actionable results within just hours.
    Try Invicti
  • Web scraping, residential proxy, proxy manager, web unlocker, search engine crawler, and all you need to collect web data.
    Try Brightdata
  • Semrush is an all-in-one digital marketing solution with more than 50 tools in SEO, social media, and content marketing.
    Try Semrush
  • Intruder is an online vulnerability scanner that finds cyber security weaknesses in your infrastructure, to avoid costly data breaches.
    Try Intruder