Laravel SOLID princliples project example

ThePatrykOOO
4 min readFeb 23, 2023

Today, I wrote my first technical blog post about a Laravel project that I recently completed. In the post, I focused on the implementation of SOLID principles, clean code, and design patterns.

> Link to my Github repository <

I didn’t focus on theory in this article because there are already a lot of materials available online that cover SOLID principles. Instead, I wanted to provide a practical example of a project that demonstrates the principles in action. I can recommend you check this materials:

Some code snippets with description

app/Http/Controllers/DepartmentController.php

This is a CRUD controller. I’ve separated the responsibilities for some features such as FormRequest, Responses, and Repository. As a result, the code is clear and the methods in the class are small and easy to maintain.

<?php

namespace App\Http\Controllers;

use App\Http\Requests\DepartmentRequest;
use App\Http\Resources\DepartmentResource;
use App\Repositories\DepartmentRepository;
use App\Services\Departments\DepartmentReportService;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

class DepartmentController extends Controller
{
private DepartmentRepository $departmentRepository;

public function __construct(DepartmentRepository $departmentRepository)
{
$this->departmentRepository = $departmentRepository;
}

/**
* Display a listing of the resource.
*/
public function index(): AnonymousResourceCollection
{
$listOfDepartments = $this->departmentRepository->findAll();
return DepartmentResource::collection($listOfDepartments);
}

/**
* Store a newly created resource in storage.
*/
public function store(DepartmentRequest $request): DepartmentResource
{
$department = $this->departmentRepository->store($request->validated());
return new DepartmentResource($department);
}

/**
* Display the specified resource.
*/
public function show(int $departmentId): DepartmentResource
{
$department = $this->departmentRepository->findOrFail($departmentId);
return new DepartmentResource($department);
}

/**
* Update the specified resource in storage.
*/
public function update(DepartmentRequest $request, int $departmentId): JsonResponse
{
$this->departmentRepository->update($request->validated(), $departmentId);
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
}

/**
* Remove the specified resource from storage.
*/
public function destroy(int $departmentId): JsonResponse
{
$this->departmentRepository->delete($departmentId);
return new JsonResponse(null, Response::HTTP_NO_CONTENT);
}

public function generateReportListOfEmployees(
int $departmentId,
string $reportType,
DepartmentReportService $departmentReportService
): JsonResponse {
$reportData = $departmentReportService->generateReport($departmentId, $reportType);
return new JsonResponse($reportData, Response::HTTP_CREATED);
}
}

app/Repositories/DepartmentRepository.php

I used the repository pattern to communicate with the database and persist data. The DepartmentRepository implements two interfaces: CrudRepositoryInterface and DepartmentReportRepositoryInterface. This is a great opportunity to apply the Interface Segregation Principle. The CrudRepositoryInterface is also used by the EmployeeRepository.

DepartmentRepository class is an example of code that allows for extension, but is difficult to modify.

<?php

namespace App\Repositories;

use App\Interfaces\CrudRepositoryInterface;
use App\Interfaces\DepartmentReportRepositoryInterface;
use App\Models\Department;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

class DepartmentRepository implements CrudRepositoryInterface, DepartmentReportRepositoryInterface
{
public function findAll(): Collection
{
return Department::all();
}

public function findOrFail(int $id): Model
{
return Department::query()
->findOrFail($id);
}

public function store(array $data): Model
{
return Department::query()
->create($data);
}

public function update(array $data, int $id): void
{
Department::query()
->findOrFail($id)
->update($data);
}

public function delete(int $id): void
{
$this->findOrFail($id)->delete();
}

public function getListEmployees(Department $department): Collection
{
return $department
->employees()
->select('id', 'first_name', 'last_name')
->get();
}

public function getSalaryEmployees(Department $department): Collection
{
return $department
->employees()
->select('id', 'first_name', 'last_name', 'usd_salary')
->get();
}

public function getRoleEmployees(Department $department): Collection
{
return $department
->employees()
->select('id', 'first_name', 'last_name', 'role')
->get();
}
}
<?php

namespace App\Interfaces;

use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;

interface CrudRepositoryInterface
{
public function findAll(): Collection;

public function findOrFail(int $id): Model;

public function store(array $data): Model;

public function update(array $data, int $id): void;

public function delete(int $id): void;
}
<?php

namespace App\Interfaces;

use App\Models\Department;
use Illuminate\Database\Eloquent\Collection;

interface DepartmentReportRepositoryInterface
{
public function getListEmployees(Department $department): Collection;

public function getSalaryEmployees(Department $department): Collection;

public function getRoleEmployees(Department $department): Collection;
}

Generate report by department

This is a nice opportunity to implement the Builder and Template Method design patterns.

Let’s create a service that is responsible for generating reports.

<?php

namespace App\Services\Departments;

use App\Models\Department;
use App\Reports\Department\DepartmentReport;
use App\Reports\Department\ListOfEmployeesReport;
use App\Reports\Department\RoleEmployeesReport;
use App\Reports\Department\SalaryEmployeesReport;
use App\Repositories\DepartmentRepository;

class DepartmentReportService
{

private DepartmentRepository $departmentRepository;

public function __construct(DepartmentRepository $departmentRepository)
{
$this->departmentRepository = $departmentRepository;
}

public function generateReport(int $departmentId, string $reportType): array
{
$department = $this->departmentRepository->findOrFail($departmentId);

$departmentReport = $this->initialize($department, $reportType);

return $departmentReport->generate();
}

private function initialize(Department $department, string $reportType): DepartmentReport
{
return match ($reportType) {
'list' => new ListOfEmployeesReport($department, $this->departmentRepository),
'salary' => new SalaryEmployeesReport($department, $this->departmentRepository),
'role' => new RoleEmployeesReport($department, $this->departmentRepository),
default => throw new \Exception("Report type not found"),
};
}
}

We can select the report type and generate the relevant data. For instance, we can generate a salary report for a selected department by using the below code:

<?php

namespace App\Reports\Department;

class SalaryEmployeesReport extends DepartmentReport
{

public function fetchData(): void
{
$this->data = $this->departmentRepository->getSalaryEmployees($this->department);
}

public function prepareSummarize(): void
{
$this->summary = [
'count_rows' => $this->data->count(),
'sum_all_of_salaries' => $this->data->sum('usd_salary'),
'avg_salary' => $this->data->avg('usd_salary'),
];
}
}

The class contain 2 methods. Once responsible for fetch data from database from repository and second prepare summarize. Summarize it’s depend on type of report. Every department report is extend by DepartmentReport class

<?php

namespace App\Reports\Department;

use App\Models\Department;
use App\Repositories\DepartmentRepository;
use Illuminate\Database\Eloquent\Collection;

abstract class DepartmentReport
{
protected DepartmentRepository $departmentRepository;
protected Department $department;

protected Collection $data;
protected array $summary;

public function __construct(Department $department, DepartmentRepository $departmentRepository)
{
$this->departmentRepository = $departmentRepository;
$this->department = $department;
}

final public function generate(): array
{
$this->fetchData();
$this->prepareSummarize();

return [
'department' => [
'id' => $this->department->id,
'name' => $this->department->name,
],
'rows' => $this->data->toArray(),
'summary' => $this->summary
];
}

abstract public function fetchData(): void;

abstract public function prepareSummarize(): void;


}

The generate() method is crucial in this class, as it returns an array of data. The data included in the report depends on the type of report being generated.

Summary

I’ve prepared a sample SOLID project. Maybe I will develop this code and add additional features in the future. I think that a department can be deleted when there are no employees assigned to it.

Have a nice day :)

— — — — — —

Health For IT — Newsletter for programmers

Linktree Profile

--

--

ThePatrykOOO

Freelance Full-Stack Developer - Laravel, Vue.js, Nest.js