0Pricing
PHP Academy · レッスン

ユースケースとアプリケーションサービス

フレームワークに依存しないユースケースとしてビジネスアクションを表現します。

「ユースケースとアプリケーションサービス」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。

ユースケースの本当の意味

ユースケース(別名:アプリケーションサービス、インタラクター)は、アプリケーション固有の1つの操作だけを正確に表します。たとえば、ユーザー登録、注文の作成、サブスクリプションのキャンセルなどです。エンティティとポートを調整し、1つの意図を実現します。重要なのは、フレームワークから独立していることです。RequestもResponseもグローバルヘルパーもなく、どこからでも呼び出せる通常のPHPだけで構成します。

コマンドDTOと結果DTO

ユースケースは不変のコマンドDTOを入力として受け取り、結果DTOを返します。DTOは単純なデータ運搬役であり、振る舞いも、データの形状以外のバリデーションロジックも持ちません。PHP 8.1以降のreadonlyプロパティを使えば、値が変更されないことを保証できます。

<?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; }
}

トランザクション境界

ユースケースは自然なトランザクション境界です。1つのユースケースを、整合性のある1つの作業単位として扱います。サービスのあちこちに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));
    }
}

バリデーションを置く場所

バリデーションは次の2段階に分けます。

  • 入力バリデーション(形式、必須フィールド)は、ユースケースの実行前に駆動アダプターまたは専用のバリデーターで行います。
  • ドメインバリデーション(不変条件、ビジネスルール)は値オブジェクトとエンティティに置き、ドメイン例外をスローします。

ユースケースは、形式が正しい入力を前提とし、その意味を保証します。

<?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なしで出力を返す

フレームワークから独立したままデータを返す方法は2つあります。

  • 結果DTOを返す(単純で同期的)。
  • 出力ポート/プレゼンター — ユースケースが注入された出力境界に結果を渡し、形式(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;

ユースケースごとに1クラス

10個のメソッドを持つ肥大化したサービスよりも、単一アクションのクラス(パブリックメソッドを1つだけ持ち、多くの場合は__invoke)を優先してください。利点は次のとおりです。

  • 単一責任と命名が明確になります(SubscriptionService::cancelではなくCancelSubscription)。
  • コンストラクターには、この操作に必要なものだけを注入します。
  • デコレーター(トランザクション、ロギング、認可)で簡単にラップできます。

コンポジションルートで接続する

ユースケース自身が依存関係をnewして生成することはありません。生成するのはコンポジションルートです。これは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;
    }
}

簡単な確認

「メールアドレスは一意で、形式も正しくなければならない」というルールはどこに置くべきですか?

まとめ

フレームワークから独立したユースケースによって、クリーンなアプリケーション層を構築できます。

  • 操作ごとに単一アクションのクラスを1つ用意し、コマンドDTOを受け取り、結果DTOを返します(または出力ポートに渡します)。
  • エンティティと値オブジェクトが不変条件を所有し、サービスは調整だけを行います。貧血モデルは避けてください。
  • ユースケースはトランザクション境界とし、beginTransactionを内部に直接書くのではなく、デコレーターでラップします。
  • 入力バリデーション(アダプター/値オブジェクト)とドメインバリデーション(エンティティ)を分けます。
  • ドメインイベントで副作用を切り離し、コンポジションルートで依存関係を接続します。

よくある質問

「ユースケースとアプリケーションサービス」レッスンは無料ですか?

はい。「ユースケースとアプリケーションサービス」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。

「ユースケースとアプリケーションサービス」で何を学びますか?

フレームワークに依存しないユースケースとしてビジネスアクションを表現します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

PHP Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「ユースケースとアプリケーションサービス」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPHP Academyレッスンでコードを書いて実行できますか?

はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. レイヤードアーキテクチャからクリーンアーキテクチャへ
  2. ポートとアダプターの概要
  3. ユースケースとアプリケーションサービス
  4. 依存性逆転の実践
← PHP Academyに戻る