Сервисы
Сервис — это слой бизнес-логики приложения. Контроллер принимает запрос и
делегирует работу сервису; сервис выполняет операцию, обращаясь к репозиториям и
другим сервисам. В Winter это стереотип Service — маркер архитектурного слоя,
который создаёт и внедряет контейнер.
Что такое сервис и зачем
Сервис (service) — класс, где живёт бизнес-логика: регистрация пользователя, расчёт заказа, проведение платежа.
Проблема. Если писать логику прямо в контроллере, он «толстеет» и намертво срастается с HTTP: ту же операцию не переиспользовать из консольной команды или фоновой джобы, а тестировать приходится через весь HTTP-цикл. Логика, размазанная по обработчикам, дублируется и расходится.
Решение. Вынесите бизнес-логику в отдельный слой — сервис. Контроллер становится тонким (принял → делегировал → вернул), а логику можно вызвать откуда угодно: из контроллера, команды, джобы, другого сервиса. Об этом и раздел.
Место в архитектуре
Winter поощряет три слоя с чёткими ролями:
Controller — вход HTTP: разбор запроса, вызов сервиса, ответ
│
▼
Service — бизнес-логика: правила, оркестрация, транзакции
│
▼
Repository — доступ к данным: запросы и запись в БДКаждый слой зависит только от следующего и не знает о предыдущем: сервис ничего не знает про HTTP, репозиторий — про бизнес-правила. Это делает слои тестируемыми и переиспользуемыми по отдельности.
Создание
Сгенерируйте сервис командой make (суффикс Service добавится сам):
php call make -s .User # → main/UserService.phpСтереотип Service минимален — это чистый маркер слоя:
namespace Flytachi\Winter\K2\Stereotype;
abstract class Service {}Всё наполнение — ваша логика и внедрённые зависимости.
Пример
Сервис получает репозитории и другие зависимости через #[Autowired] и
реализует операцию целиком:
<?php
namespace Main;
use Flytachi\Winter\DI\Attribute\Autowired;
use Flytachi\Winter\K2\Exception\ClientError;
use Flytachi\Winter\K2\Stereotype\Service;
use Psr\Log\LoggerInterface;
class UserService extends Service
{
#[Autowired] private UserRepository $repo;
#[Autowired] private LoggerInterface $logger;
public function register(CreateUserDto $dto): User
{
if ($this->repo->existsByEmail($dto->email)) {
throw new ClientError('Email already taken'); // → 409
}
$user = User::fromDto($dto);
$user->id = $this->repo->insert($user);
$this->logger->info('user registered', ['id' => $user->id]);
return $user;
}
}Контроллер при этом остаётся тонким:
#[Autowired] private UserService $service;
#[PostMapping]
public function create(#[RequestJson, Valid] CreateUserDto $dto): ResponseEntity
{
return ResponseEntity::created($this->service->register($dto));
}Переиспользование
Раз сервис не привязан к HTTP, одну и ту же логику вызывают из разных точек входа:
// из контроллера
$this->service->register($dto);
// из консольной команды (Cmd)
$this->service->register($dto);
// из фоновой джобы (Job)
$this->service->register($dto);Сервисы также свободно зависят друг от друга — OrderService может внедрить
PaymentService и NotificationService через #[Autowired].
Жизненный цикл
По умолчанию сервисы ведут себя как singleton (один экземпляр на процесс) — они
без состояния запроса. Если сервису нужно состояние текущего запроса, вынесите его
в отдельный #[Request]-класс (см. Внедрение зависимостей).
Дальше
- Контроллеры — кто вызывает сервис
- Внедрение зависимостей — как сервис получает зависимости
- База данных (PPA) — репозитории, к которым обращается сервис