Основы веб-разработки

Сервисы

Сервис — это слой бизнес-логики приложения. Контроллер принимает запрос и делегирует работу сервису; сервис выполняет операцию, обращаясь к репозиториям и другим сервисам. В Winter это стереотип Service — маркер архитектурного слоя, который создаёт и внедряет контейнер.

База Stereotype\ServiceСлой бизнес-логикаСоздаётся контейнером (DI)

Что такое сервис и зачем

Сервис (service) — класс, где живёт бизнес-логика: регистрация пользователя, расчёт заказа, проведение платежа.

Проблема. Если писать логику прямо в контроллере, он «толстеет» и намертво срастается с HTTP: ту же операцию не переиспользовать из консольной команды или фоновой джобы, а тестировать приходится через весь HTTP-цикл. Логика, размазанная по обработчикам, дублируется и расходится.

Решение. Вынесите бизнес-логику в отдельный слой — сервис. Контроллер становится тонким (принял → делегировал → вернул), а логику можно вызвать откуда угодно: из контроллера, команды, джобы, другого сервиса. Об этом и раздел.

Место в архитектуре

Winter поощряет три слоя с чёткими ролями:

text
Controller   — вход HTTP: разбор запроса, вызов сервиса, ответ


Service      — бизнес-логика: правила, оркестрация, транзакции


Repository   — доступ к данным: запросы и запись в БД

Каждый слой зависит только от следующего и не знает о предыдущем: сервис ничего не знает про HTTP, репозиторий — про бизнес-правила. Это делает слои тестируемыми и переиспользуемыми по отдельности.

Создание

Сгенерируйте сервис командой make (суффикс Service добавится сам):

bash
php call make -s .User   # → main/UserService.php

Стереотип Service минимален — это чистый маркер слоя:

php
namespace Flytachi\Winter\K2\Stereotype;

abstract class Service {}

Всё наполнение — ваша логика и внедрённые зависимости.

Пример

Сервис получает репозитории и другие зависимости через #[Autowired] и реализует операцию целиком:

main/UserService.php
<?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;
  }
}

Контроллер при этом остаётся тонким:

php
#[Autowired] private UserService $service;

#[PostMapping]
public function create(#[RequestJson, Valid] CreateUserDto $dto): ResponseEntity
{
  return ResponseEntity::created($this->service->register($dto));
}

Переиспользование

Раз сервис не привязан к HTTP, одну и ту же логику вызывают из разных точек входа:

php
// из контроллера
$this->service->register($dto);

// из консольной команды (Cmd)
$this->service->register($dto);

// из фоновой джобы (Job)
$this->service->register($dto);

Сервисы также свободно зависят друг от друга — OrderService может внедрить PaymentService и NotificationService через #[Autowired].

Жизненный цикл

По умолчанию сервисы ведут себя как singleton (один экземпляр на процесс) — они без состояния запроса. Если сервису нужно состояние текущего запроса, вынесите его в отдельный #[Request]-класс (см. Внедрение зависимостей).

Дальше