Table of Contents
Repository vs Service: What Goes Where in Laravel
When refactoring Laravel applications, one common anti-pattern I see is services that directly perform database queries. This violates separation of concerns and makes code harder to test and maintain.
The Problem: Bloated Service Classes
Consider a ReportService that handles business logic for generating reports. Over time, it accumulates methods like:
class ReportService
{
public function generateReport($projectId, $startDate, $endDate)
{
// Business logic here
$data = $this->fetchReportData($projectId, $startDate, $endDate);
// More business logic
}
private function fetchReportData($projectId, $startDate, $endDate)
{
// Direct database query
return DB::table('reports')
->where('project_id', $projectId)
->whereBetween('created_at', [$startDate, $endDate])
->get();
}
private function getProjectSettings($projectId)
{
// Another direct database query
return DB::table('project_settings')
->where('project_id', $projectId)
->first();
}
}
This service is doing two jobs: orchestrating business logic AND querying the database.
The Solution: Move Database Queries to Repositories
Repositories should handle all database access. Services should orchestrate business logic by calling repositories.
Create a Repository:
class ReportRepository
{
public function findReportData($projectId, $startDate, $endDate)
{
return DB::table('reports')
->where('project_id', $projectId)
->whereBetween('created_at', [$startDate, $endDate])
->get();
}
public function getProjectSettings($projectId)
{
return DB::table('project_settings')
->where('project_id', $projectId)
->first();
}
}
Clean up the Service:
class ReportService
{
public function __construct(
private ReportRepository $reportRepo
) {}
public function generateReport($projectId, $startDate, $endDate)
{
// Pure business logic - no database queries
$data = $this->reportRepo->findReportData($projectId, $startDate, $endDate);
$settings = $this->reportRepo->getProjectSettings($projectId);
// Apply business rules, transformations, etc.
return $this->processReportData($data, $settings);
}
}
The Rule
Services orchestrate. Repositories query.
If your service has DB::table() or Eloquent queries, move them to a repository. Your service should read like a business workflow, not a database script.
Bonus: Testing Becomes Easier
With this separation, you can mock the repository in tests:
public function test_generates_report()
{
$mockRepo = Mockery::mock(ReportRepository::class);
$mockRepo->shouldReceive('findReportData')->once()->andReturn(collect([...]));
$service = new ReportService($mockRepo);
$result = $service->generateReport(1, '2024-01-01', '2024-12-31');
$this->assertInstanceOf(Report::class, $result);
}
Clean separation = easier testing + clearer code architecture.
Leave a Reply