0Pricing
PHP Academy · Урок

Варианты использования и прикладные службы

Представляйте бизнес-действия в виде вариантов использования, не зависящих от фреймворка

«Варианты использования и прикладные службы» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. От многоуровневой к чистой архитектуре
  2. Порты и адаптеры
  3. Варианты использования и прикладные службы
  4. Инверсия зависимостей на практике
← Назад к PHP Academy