Варианты использования и прикладные службы
Представляйте бизнес-действия в виде вариантов использования, не зависящих от фреймворка
«Варианты использования и прикладные службы» — бесплатный урок PHP Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения PHP Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс PHP Academy содержит 4 уроков всего.
Что на самом деле представляет собой сценарий использования
Сценарий использования (также сервис приложения или интерактор) описывает ровно одну специфичную для приложения операцию: Регистрация пользователя, Размещение заказа, Отмена подписки. Он координирует сущности и порты для выполнения одного намерения. Что особенно важно, он не зависит от фреймворка: никаких Request, Response или глобальных вспомогательных функций — только обычный PHP, который можно вызвать откуда угодно.
Объекты передачи команды и результата
Сценарий использования принимает неизменяемый объект передачи данных команды и возвращает объект передачи данных результата. Такие объекты служат простыми переносчиками данных — в них нет поведения и логики проверки, кроме проверки структуры. Свойства, доступные только для чтения (PHP 8.1+), делают их защищёнными от изменений.
<?php
final class RegisterUserCommand
{
public function __construct(
public readonly string $email,
public readonly string $plainPassword,
) {}
}
final class RegisterUserResult
{
public function __construct(public readonly string $userId) {}
}Тело сервиса приложения
Сервис преобразует команду в операции предметной области. Он выполняет оркестрацию на уровне приложения — проверку уникальности, сохранение данных и возврат идентификаторов, — делегируя правила сущностям.
<?php
final class RegisterUser
{
public function __construct(
private Users $users,
private PasswordHasher $hasher,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
if ($this->users->existsByEmail($c->email)) {
throw new EmailAlreadyRegistered($c->email);
}
$user = User::register(
UserId::generate(),
new Email($c->email),
$this->hasher->hash($c->plainPassword),
);
$this->users->add($user);
return new RegisterUserResult((string) $user->id());
}
}Храните логику в сущностях
Остерегайтесь анемичной модели предметной области: сущностей, сведённых к методам получения и установки значений, тогда как вся логика находится в сервисах. Инварианты должны принадлежать сущности. Сценарий использования должен читаться как короткий сценарий намерений, а не как длинный перечень бизнес-правил.
<?php
final class User
{
private function __construct(
private UserId $id,
private Email $email,
private string $passwordHash,
private bool $active = false,
) {}
public static function register(UserId $id, Email $e, string $hash): self {
return new self($id, $e, $hash); // invariants enforced here
}
public function activate(): void {
if ($this->active) throw new AlreadyActive();
$this->active = true;
}
public function id(): UserId { return $this->id; }
}Границы транзакций
Сценарий использования — естественная граница транзакции: один сценарий использования = одна согласованная рабочая единица. Вместо того чтобы рассыпать по сервисам вызовы beginTransaction(), оберните их транзакционным декоратором, чтобы ядро не зависело от способа сохранения данных.
<?php
interface TransactionManager {
public function transactional(callable $work): mixed;
}
final class TransactionalRegisterUser
{
public function __construct(
private RegisterUser $inner,
private TransactionManager $tx,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
return $this->tx->transactional(fn() => ($this->inner)($c));
}
}Где должна находиться проверка
Разделите проверку на два уровня:
- Проверка входных данных (формат, обязательные поля) выполняется во входном адаптере или отдельном проверяющем компоненте до запуска сценария использования.
- Проверка предметной области (инварианты, бизнес-правила) находится в объектах-значениях и сущностях, которые выбрасывают исключения предметной области.
Сценарий использования предполагает, что входные данные имеют правильную форму, и проверяет их смысл.
<?php
final class Email
{
public function __construct(public readonly string $value) {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email: $value");
}
}
}
try { new Email('nope'); } catch (Throwable $e) { echo $e->getMessage(), PHP_EOL; }
echo (new Email('a@b.com'))->value, PHP_EOL;Возврат результата без HTTP
Есть два способа возвращать данные, сохраняя независимость от фреймворка:
- Вернуть объект передачи данных результата (просто, синхронно).
- Выходной порт / представление — сценарий использования передаёт результат во внедрённую выходную границу, позволяя адаптеру выбрать формат (JSON, HTML, CLI). Благодаря этому даже форма ответа остаётся за пределами ядра.
<?php
interface RegisterUserOutput {
public function present(RegisterUserResult $r): void;
}
final class RegisterUserWithPresenter {
public function __construct(private Users $users, private PasswordHasher $h) {}
public function __invoke(RegisterUserCommand $c, RegisterUserOutput $out): void {
$user = User::register(UserId::generate(), new Email($c->email), $this->h->hash($c->plainPassword));
$this->users->add($user);
$out->present(new RegisterUserResult((string) $user->id()));
}
}События предметной области из сценариев использования
Сценарии использования часто записывают события предметной области, создаваемые сущностями, а затем отправляют их после фиксации транзакции. Это отделяет побочные эффекты (отправку приветственного письма, обновление модели чтения) от основного рабочего процесса.
<?php
trait RecordsEvents {
private array $events = [];
protected function record(object $e): void { $this->events[] = $e; }
public function releaseEvents(): array {
$e = $this->events; $this->events = []; return $e;
}
}
final class UserRegistered {
public function __construct(public readonly string $userId) {}
}
// Use case calls $user->releaseEvents() and hands them to a dispatcher
echo 'event recorded pattern', PHP_EOL;Один класс на каждый сценарий использования
Предпочитайте класс с одним действием (одним открытым методом, часто __invoke) перегруженному сервису с десятью методами. Преимущества:
- Чёткая единственная ответственность и понятное именование (
CancelSubscription, а неSubscriptionService::cancel). - Конструктор получает только то, что нужно этой операции.
- Класс легко оборачивать декораторами (транзакция, ведение журнала, авторизация).
Связывание в корне композиции
Сценарий использования никогда не создаёт свои зависимости самостоятельно — это делает корень композиции. Ниже приведён пример ручного связывания, который можно разместить в определении DI-контейнера.
<?php
$pdo = new PDO('sqlite::memory:');
$users = new PdoUsers($pdo);
$hasher = new BcryptHasher();
$register = new RegisterUser($users, $hasher);
// Decorate with a transaction boundary
$register = new TransactionalRegisterUser($register, new PdoTransactionManager($pdo));
// Driving adapter calls it
$result = $register(new RegisterUserCommand('dev@coddykit.com', 's3cret!'));
echo $result->userId, PHP_EOL;Сквозные задачи с помощью декораторов
Ведение журнала, сбор метрик и авторизация — это сквозные задачи; не помещайте их в тело сценария использования. Оборачивайте сервис декораторами, реализующими тот же интерфейс: тогда ядро сосредоточено на рабочем процессе, а задачи инфраструктуры компонуются вокруг него.
<?php
interface RegisterUserHandler {
public function __invoke(RegisterUserCommand $c): RegisterUserResult;
}
final class LoggingRegisterUser implements RegisterUserHandler {
public function __construct(
private RegisterUserHandler $inner,
private LoggerInterface $log,
) {}
public function __invoke(RegisterUserCommand $c): RegisterUserResult {
$this->log->info('register.start', ['email' => $c->email]);
$r = ($this->inner)($c);
$this->log->info('register.ok', ['id' => $r->userId]);
return $r;
}
}Быстрая проверка
Где должно находиться правило «адрес электронной почты должен быть уникальным и иметь правильный формат»?
Итоги
Сценарии использования, независимые от фреймворка, создают чистый слой приложения:
- Для каждой операции используется один класс с одним действием, принимающий объект передачи данных команды и возвращающий объект передачи данных результата (или передающий его в выходной порт).
- Сущности и объекты-значения владеют инвариантами; сервис только координирует работу — избегайте анемичных моделей.
- Сценарии использования являются границами транзакций и оборачиваются декораторами, а не содержат вызов
beginTransactionвнутри себя. - Разделяйте проверку входных данных (адаптер/объект-значение) и проверку предметной области (сущности).
- События предметной области отделяют побочные эффекты; зависимости связываются в корне композиции.
Часто задаваемые вопросы
Урок «Варианты использования и прикладные службы» бесплатный?
Да — полный текст урока «Варианты использования и прикладные службы» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс PHP Academy, подпишись на CoddyKit PRO. Курс PHP Academy содержит 4 уроков всего.
Чему я научусь в уроке «Варианты использования и прикладные службы»?
Представляйте бизнес-действия в виде вариантов использования, не зависящих от фреймворка Ты практикуешь PHP Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать PHP Academy?
Предыдущий опыт не требуется. PHP Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Варианты использования и прикладные службы»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке PHP Academy?
Да. Каждый урок PHP Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- От многоуровневой к чистой архитектуре
- Порты и адаптеры
- Варианты использования и прикладные службы
- Инверсия зависимостей на практике