Boosting Laravel Quality with SOLID Principles: Best Practices and Examples

Niranjan Shrestha
6 min readMar 12, 2023

--

SOLID Principles

When we talk about take a next step as a developer, we have to understand that the code quality is always the main focus on this road.

There is a lot of ideas of how to improve your code, like KISS (Keep it Simple), DRY (Don’t Repeat Yourself) and the one which will be approached here: SOLID.

There are many programming guidelines to keep our code reliable, understandable, and maintainable. The SOLID principle is one of them.

SOLID is an acronym that stands for:

  • Single responsibility principle
  • Open/closed principle
  • Liskov substitution principle
  • Interface segregation principle
  • Dependency inversion principle

SOLID is an acronym to rules that makes easier the life of who is READING the code for a next maintenance, since they are like “laws” that you have to follow on Object Oriented Programming for a better code legibility and manageability.

These principles were first introduced by the Robert C. Martin, aka Uncle Bob in his 2000 paper “Design principles and Design Patterns”, later these principles was rearranged and the SOLID acronyms was introduced by Micheal Feathers.

SOLID principles are not related to PHP only, they apply to various OOPS languages. It is more about software design principles. It is not a feature it is a way of thinking about how you structure your code, so that code is more maintainable, understandable, flexible, and easy to extend.

Why SOLID Principles?

When working on a legacy code, you need to re-read the code many times to get to the part where you need the modification. It becomes really hard to understand what a method does and it takes too much time to read the code than writing.

Purpose of SOLID Principles

  • To make code easier to read and understand
  • To make it easier to quickly extend with new functionality without breaking the existing one
  • To make the code more extendable and testable.

S — Single Responsibility Principle(SRP)

The Single Responsibility Principle states that “a class should do one thing and therefore it should have only a single reason to change”.

Example 1

So, let’s say we have a class SaleReports.php class inside a App\Solid.

<?php

namespace App\Solid;

use DB;

class SaleReports {
public function export() {
$sales = DB::table('sales')
->latest()
->get();

return 'CSV format';
}
}

In this export method, it is doing two jobs, fetching the data from database and returning a respective data format. So, this method has two reasons to change.

In software development business requirement always change. For example, let’s say in future we have to add PDF format for sales reports then above class will look like

<?php

namespace App\Solid;

use DB;

class SaleReports {
public function export($format) {
$sales = DB::table('sales')
->latest()
->get();

if($format === 'pdf'){
return 'PDF format'
}

return 'CSV format';
}
}

Here, we are violating SRP because in above class lots of things are going on a single class. Now lets implement SRP on above class.

<?php

namespace App\Solid;

class PdfExport
{
public function export($data) {
return 'PDF format';
}
}
<?php

namespace App\Solid;

class CsvExport
{
public function export($data) {
return 'CSV format';
}
}
<?php

namespace App\Solid;

use DB;

class SaleReports {
public function export($format) {
$sales = DB::table('sales')
->latest()
->get();
}
}

Now, above classes are doing exactly what they should do. PdfExport class will only export PDF related data, similarly CsvExport class will export CSV.

While generating PDF SaleReports we can use them as follows:

...
$salesReports = new SaleReports();
$pdfExport = new PdfExport();

return $pdfExport->export($salesReports);

and if we want to generate CSV SaleReports and so on.

...
$salesReports = new SaleReports();
$csvExport = new CsvExport();

return $csvExport->export($salesReports);

Example 2

Lets imagine a scenario where we have a random chat and there we have an user that sends and receives messages. On this scenario, we’re going to save all messages from our user on database after the validation.

We’re going to use the Laravel ecosystem, where the request entrance will be the Controller. The Controller has as responsibility:

  • Receive a Request;
  • Process a Request;
  • Return a Response for the Request.

Here is the snippet:

namespace App\Http\Controllers;

use DB;
use Illuminate\Foundation\Http\Request;
use App\Events\ChatMessage;

class MessagesController extends Controller {

public function postMessage(Request $request) {
$this->validate($request, [
'user_id' => 'required|exists:users,id',
'message' => 'required'
]);

if ($this->getUserSpecificMessagesCount($data['message']) >= 5) {
Log::alert('[User Alert] Flooding', $data)
}

$model = Message::create($request->all());
broadcast(new ChatMessage($model));

return response()->json(['message' => 'message created'], 201);
}

public function getUserSpecificMessagesCount(int $userId, string $message) {
return DB::table('user_messages')->where([
['user_id', '=', $userId],
['message', '=', $message],
])->count();
}

}

Let’s list what the can see on the MessagesController snippet

  • Receive the request;
  • Validate the data;
  • Check a possibility of flooding;
  • Create a new register of the message on Database;
  • Broadcast the message to some channel;
  • Return a message to the client.

Now thinking in the responsibility that controller should have, we can see that was a little bit far from that and it was not expected.

Let’s start to refactoring from top to bottom, starting by the validation. On the Laravel ecosystem, there’s a way to validate requests where you isolate the responsibility in a FormRequest Class.

Using the command php artisan make:request CreateMessageRequest you will generate a FormRequest class that will appear on the folder/namespace App\Http\Requests that will have the UNIQUE responsibility of validate your request and nothing else:

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateMessageRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'user_id' => 'required|exists:users,id',
'message' => 'required'
];
}
}

Now we’re going to implement the solution on our snippet above and it should look like this:

namespace App\Http\Controllers;

use DB;
use App\Http\Requests\CreateMessageRequest;
use App\Events\ChatMessage;

class MessagesController extends Controller {

public function postMessage(CreateMessageRequest $request) {
$data = $request->validated();

if ($this->getUserSpecificMessagesCount($data['message'])) {
Log::alert('[User Alert] Flooding', $data)
}

$model = Message::create($data);
broadcast(new ChatMessage($model));

return response()->json(['message' => 'message created'], 201);
}

public function getUserSpecificMessagesCount(int $userId, string $message) {
return DB::table('user_messages')->where([
['user_id', '=', $userId],
['message', '=', $message],
])->count();
}

}

Alright, we separated the validation of our main function. Now we have to extract the business rule to a new abstraction layer, that is known as Repository Pattern. The idea of Repository Pattern is you have a place to work with methods/classes that communicates with database, mailing and whatever else you need it. It’s literally where you put all the business logic (if you prefer).

The Repository Pattern is not the last abstraction layer, in the reality you can abstract how many layers you want for your code being more readable as possible.

namespace App\Repositories;

use App\Models\Message;
use App\Events\ChatMessage;

class MessageRepository {

private $model;

public function __construct()
{
$this->model = new Message();
}

public function create(array $payload): bool
{
if ($this->checkFloodPossibility($data['message'])) {
Log::alert('[User Alert] Flooding', $data)
}

$model = Message::create($payload);
broadcast(new ChatMessage($model));

return true;
}

public function getUserSpecificMessagesCount(int $userId, string $message) {
return DB::table('user_messages')->where([
['user_id', '=', $userId],
['message', '=', $message],
])->count();
}
}

After we create our repository and throw all the due responsibility there, we’ll have three ways to invoke it on our controller:

  1. Instantiate directly inside the function
$repository = new MessageRepository();

2. Injecting the dependency on the class constructor

class MessagesController {
public $repository;

public function __construct(MessageRepository $repository)
{
$this->repository = $repository;
}
}

3. Using Laravel Containers

$repository = app(MessageRepository::class)->create();

In our code, we’re going to use Dependency Injection so we can have a better view of the code. Particularly is the one who makes more sense to me, have in sight that we have to maintaining the the code cleaner as possible.

namespace App\Http\Controllers;

use App\Http\Requests\CreateMessageRequest;
use App\Repositories\MessageRepository;

class MessagesController extends Controller {
private $repository;

public function __construct(MessageRepository $repository)
{
$this->repository = $repository;
}

public function postMessage(CreateMessageRequest $request)
{
$data = $request->validated();
$this->repository->create($data);

return response()->json(['message' => 'message created'], 201);
}
}

With that, our code become a lot more organized and cleaner to read. Each one of responsibilities was distributed and the Controller responsibilities was followed like we told on the beginning of this article: receive, process and response.

Next >> O — Open-Closed Principle (OCP)

--

--