Creating Your Own Services
Creating Your Own Services
~15 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Time to extract our FIRST piece of custom business logic into a dedicated service, instead of leaving it in the controller – the statistics computation from chapter 24.
Why business logic belongs outside the controller
Controllers SHOULD stay thin: receive the request, delegate to the right logic, return the response. More complex computations that combine SEVERAL repositories or should be reusable (e.g. also from a console command in block 7) belong in a DEDICATED service.
Generating a service
php bin/console make:service ProjectStatsService<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Project;
use App\Repository\TaskRepository;
class ProjectStatsService
{
public function __construct(
private readonly TaskRepository $taskRepository,
) {
}
/**
* @return array{open: int, in_progress: int, done: int, progress: float}
*/
public function computeStats(Project $project): array
{
$rawStats = $this->taskRepository->countTasksByStatus($project);
$countByStatus = ['open' => 0, 'in_progress' => 0, 'done' => 0];
foreach ($rawStats as $row) {
$countByStatus[$row['status']] = $row['count'];
}
$total = array_sum($countByStatus);
$progress = $total > 0
? round(($countByStatus['done'] / $total) * 100, 1)
: 0.0;
return [
...$countByStatus,
'progress' => $progress,
];
}
}NO attributes, NO special base class needed – ANY ordinary PHP class with typed constructor parameters is AUTOMATICALLY usable as a service via autowiring (Symfony's "services are autowired and autoconfigured by default" convention from config/services.yaml, more on that in chapter 34).
Using the service in the controller
use App\Service\ProjectStatsService;
#[Route('/projects/{id}', name: 'project_show', requirements: ['id' => '\d+'])]
public function show(
int $id,
ProjectRepository $projectRepository,
ProjectStatsService $statsService,
): Response {
$project = $projectRepository->find($id);
if ($project === null) {
throw $this->createNotFoundException();
}
$this->denyAccessUnlessGranted(ProjectVoter::VIEW, $project);
return $this->render('project/show.html.twig', [
'project' => $project,
'stats' => $statsService->computeStats($project),
]);
}EXACTLY the same autowiring principle as with LoggerInterface or ProjectRepository – the controller doesn't know HOW ProjectStatsService works internally, only THAT it can call computeStats().
Services can use other services
Autowiring works IDENTICALLY between services – a service can request any number of OTHER services in its own constructor, EXACTLY like a controller. ProjectStatsService itself could, for instance, later get a CacheInterface service injected (chapter 45), WITHOUT the calling controller code needing to change.
Rule of thumb: when does a custom service pay off?
| Situation | Recommendation |
|---|---|
| Simple, ONE-OFF logic in ONE controller method | Leave it directly in the controller – a custom service would be unnecessary indirection. |
| Logic needed from MULTIPLE places (controller AND console command AND event listener) | Extract into a service – ONE source of truth, instead of code duplication. |
| Complex computation/orchestration of MULTIPLE repositories | Extract into a service – keeps the controller readable and the logic testable in isolation (block 7). |
Tipp: A good naming convention tip: service class names often end in Service, Manager, or directly describe their task (e.g. ProjectStatsCalculator) – MORE IMPORTANT than the exact naming style is CONSISTENCY within a project.