Laravel 開発におけるModel、Trait、Service、Repository、Resourceの役割と使い方

こんにちは！Webエンジニアとして働き始めてそろそろ１年目になります。
日々の業務の中で学んだことを備忘録的に残していければと思います。

最近Laravelについて学習をする中で各クラスやディレクトリの役割について混乱することがあったので、学習もかねてまとめてみたいと思います。
同じようにLaravelについて学習をしている方の助けになれば幸いです。

### Model (モデル)
#### 役割:
データベースのテーブルとのやりとりを担当。通常、1つのモデルクラスが1つのデータベーステーブルに対応している。(Eloquent ORM)。
データの取得、挿入、更新、削除 (CRUD) の基本的な操作をカプセル化します。
テーブル間のリレーションシップ (例: hasMany, belongsTo) を定義します。
データのバリデーションルール、アクセサ (取得時のデータ加工)、ミューテータ (保存時のデータ加工) を定義することもあります。
#### 使い方:
主にRepository層から、あるいは小規模なアプリケーションや単純なCRUD操作ではControllerやService層から直接利用されます。
``User::find(1), Post::create([...]), $user->posts()->create([...])`` のように、Eloquentのメソッドを通じてデータベースと対話します。
#### 特徴:
Laravelの根幹をなすデータアクセス機能を提供します。
ビジネスロジックではなく、データとその永続化に焦点を当てます。
```
PHP

// app/Models/User.php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; // Traitの例

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable; // Traitを利用

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    // Postモデルとのリレーション
    public function posts()
    {
        return $this->hasMany(Post::class);
    }

    // アクセサの例 (名前を大文字で取得)
    public function getNameAttribute($value)
    {
        return strtoupper($value);
    }
}
```
### Trait (トレイト)
#### 役割:
クラスに特定の機能や振る舞いを「混ぜ込む (mix-in)」ための仕組み。継承とは異なり、複数のTraitを1つのクラスで使用できる。
コードの再利用性を高めることが主な目的。複数のクラスで共通して必要となるメソッド群をTraitとして定義し、必要なクラスで use キーワードを使ってインポートすることができる。
#### 使い方:
モデル、コントローラー、サービスなど、様々なクラスで利用できます。
例えば、複数のモデルで共通の「公開/非公開」機能を持たせたい場合に Publishable Traitを作成したり、APIで使うソフトデリート (SoftDeletes) や認証トークン (HasApiTokens) など、Laravel自体も多くのTraitを提供している。
#### 特徴:
クラスの継承階層を複雑にすることなく、機能を追加できる。
特定のドメインロジックというよりは、横断的な関心事 (cross-cutting concerns) や共通の振る舞いを実装するのに適している。
```
PHP

// app/Traits/ApiResponseHelper.php
namespace App\Traits;

use Illuminate\Http\JsonResponse;

trait ApiResponseHelper
{
    protected function successResponse($data, $message = 'Success', $statusCode = 200): JsonResponse
    {
        return response()->json([
            'success' => true,
            'message' => $message,
            'data' => $data,
        ], $statusCode);
    }

    protected function errorResponse($message = 'Error', $statusCode = 400, $errors = []): JsonResponse
    {
        $response = [
            'success' => false,
            'message' => $message,
        ];
        if (!empty($errors)) {
            $response['errors'] = $errors;
        }
        return response()->json($response, $statusCode);
    }
}

// app/Http/Controllers/Api/UserController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use App\Traits\ApiResponseHelper; // Traitを利用

class UserController extends Controller
{
    use ApiResponseHelper; // Traitを取り込む

    public function show(User $user)
    {
        // Traitで定義したメソッドを利用
        // return $this->successResponse($user); // Resourceを使う前の例
        // Resourceを使う場合、Traitは主にエラーレスポンス等で役立つ
        return new \App\Http\Resources\UserResource($user);
    }
}
```
### Repository (リポジトリ / リポジトリパターン)
#### 役割:
データアクセスロジックを抽象化し、カプセル化する層。
ビジネスロジック (Service層) とデータ永続化 (Model/Eloquent) の間の仲介役となります。
「どのようにデータを取得・保存するか」という詳細を隠蔽し、Service層からは「何を取得・保存したいか」というインターフェースだけが見えるようにする。
#### 使い方:
通常、インターフェース (UserRepositoryInterface) を定義し、具体的な実装クラス (EloquentUserRepository) を作成します。
Service層はインターフェースに依存し、Laravelのサービスコンテナ (DI) を使って具体的な実装クラスを注入します。
``$userRepository->find(1), $userRepository->create([...]) ``のように、定義されたメソッドを通じてデータ操作を行います。
#### 特徴:
データソースからの独立性: 理論上は、Eloquentから別のORMやデータソース (例: 外部API) に切り替える際に、Repositoryの実装クラスを変更するだけで済み、Service層への影響を最小限に抑えられます (ただし、Eloquentの機能に強く依存している場合はその限りではない)。
テスト容易性の向上: Service層のテストを行う際に、Repositoryインターフェースのモック (ダミーデータ) を注入することで、実際にデータベースにアクセスすることなくテストを実行できます。
データアクセスに関する複雑なクエリやロジック (特定の条件での検索、集計など) を集約する場所として適している。
```
PHP

// app/Repositories/Interfaces/UserRepositoryInterface.php
namespace App\Repositories\Interfaces;

use App\Models\User;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; // ページネーション対応
use Illuminate\Support\Collection;

interface UserRepositoryInterface
{
    public function find(int $id): ?User;
    public function findWithRelations(int $id, array $relations): ?User; // リレーション付き取得の例
    public function getAll(): Collection;
    public function getPaginated(int $perPage = 15): LengthAwarePaginator; // ページネーション取得
    public function create(array $data): User;
    public function update(int $id, array $data): bool;
    public function delete(int $id): bool;
}

// app/Repositories/Eloquent/EloquentUserRepository.php
namespace App\Repositories\Eloquent;

use App\Models\User;
use App\Repositories\Interfaces\UserRepositoryInterface;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;

class EloquentUserRepository implements UserRepositoryInterface
{
    protected $model;

    public function __construct(User $user)
    {
        $this->model = $user;
    }

    public function find(int $id): ?User
    {
        return $this->model->find($id);
    }

    public function findWithRelations(int $id, array $relations): ?User
    {
        return $this->model->with($relations)->find($id);
    }

    public function getAll(): Collection
    {
        return $this->model->all();
    }

     public function getPaginated(int $perPage = 15): LengthAwarePaginator
    {
        return $this->model->paginate($perPage);
    }

    public function create(array $data): User
    {
        return $this->model->create($data);
    }

    public function update(int $id, array $data): bool
    {
        $user = $this->find($id);
        if ($user) {
            return $user->update($data);
        }
        return false;
    }

    public function delete(int $id): bool
    {
        $user = $this->find($id);
        if ($user) {
            return $user->delete();
        }
        return false;
    }
}

// App\Providers\RepositoryServiceProvider.php でインターフェースと実装を紐付ける
use App\Repositories\Interfaces\UserRepositoryInterface;
use App\Repositories\Eloquent\EloquentUserRepository;

public function register()
{
    $this->app->bind(UserRepositoryInterface::class, EloquentUserRepository::class);
}
```
### Service (サービス / サービスレイヤー)
#### 役割:
アプリケーション固有のビジネスロジックやユースケースを担当します。
複数のモデルやリポジトリにまたがる操作、外部APIとの連携、複雑な計算や処理の調整など、ドメインに関わる具体的な処理を実装する。
Controllerを「薄く (Thin)」保つ役割も担います。ControllerはHTTPリクエストの処理、入力のバリデーション、Serviceの呼び出し、レスポンスの整形（Resourceへの委譲）に専念し、複雑なロジックはService層に委譲するのが望ましい。
#### 使い方:
Controllerから呼び出されます。
必要なRepositoryや他のServiceをコンストラクタインジェクション (DI) で受け取ります。
特定のビジネスプロセス (例: ユーザー登録, 注文処理, レポート生成) をメソッドとして実装します。
#### 特徴:
ビジネスロジックの集約: アプリケーションの核となるロジックがこの層に集まるため、コードの見通しが良くなり、再利用性が高まります (API Controller、Web Controller、Artisan Commandなどから同じServiceを呼び出せる)。
テスト容易性の向上: Repositoryと同様に、依存するコンポーネント (Repositoryなど) をモック化することで、ビジネスロジック単体のユニットテストが容易になる。
```
PHP

// app/Services/UserService.php
namespace App\Services;

use App\Repositories\Interfaces\UserRepositoryInterface;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Exception;
use App\Models\User; // Type Hintingのため

class UserService
{
    protected $userRepository;

    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * 新規ユーザーを登録する
     */
    public function registerUser(array $data): ?User
    {
        try {
            $data['password'] = Hash::make($data['password']);
            $user = $this->userRepository->create($data);
            Log::info("User registered successfully: ID {$user->id}");
            // ここでイベントを発行したり、メールを送信したりすることも可能
            return $user;
        } catch (Exception $e) {
            Log::error("User registration failed: " . $e->getMessage());
            return null;
        }
    }

    /**
     * ユーザー一覧をページネーションで取得する
     */
    public function getUsersWithPagination(int $perPage = 15): LengthAwarePaginator
    {
        return $this->userRepository->getPaginated($perPage);
    }

    /**
     * 特定のユーザーを関連情報付きで取得する
     */
     public function findUserWithRelations(int $id, array $relations = []): ?User
     {
         // Repositoryのメソッドを呼び出す
         return $this->userRepository->findWithRelations($id, $relations);
     }

    // 他のビジネスロジックメソッド (例: プロフィール更新、退会処理など)
}
```
### API Resource / Collection (APIリソース / コレクション) - レスポンス整形層
#### 役割:
Eloquentモデルやコレクションを、APIのクライアントに返すJSONレスポンス形式に変換（整形）するための専用レイヤーです。
データベースの構造（Modelの属性）とAPIの公開インターフェース（JSONのキーや構造）を分離する。これにより、Modelの内部構造を変更してもAPIレスポンスへの影響を最小限に抑えたり、APIレスポンスの形式を自由に変更することができる。
レスポンスに含める属性を明示的に選択し、不要な情報（例: password フィールド）を公開しないように制御できます。
属性名の変更（例: created_at を creation_date に）、関連リソースの情報の追加、メタデータの付与など、レスポンスの構造を柔軟にカスタマイズできる。
API全体のレスポンス形式を統一し、一貫性を保つのに役立つ。
#### 使い方:
Resource (単数): 1つのモデルインスタンスを変換するために使用します。``php artisan make:resource UserResource`` コマンドで作成します。toArray メソッド内に、レスポンスに含めたい属性とその形式を定義します。
Resource Collection: モデルのコレクション（配列や paginate() の結果など）を変換するために使用します。単数のResourceクラスの静的メソッド collection() を使うのが最も一般的です。``php artisan make:resource UserCollection`` で専用のコレクションリソースを作ることもできますが、多くの場合 ``UserResource::collection($users)`` で十分。
Controller内で、ServiceやRepositoryから取得したモデルやコレクションをResourceに渡してレスポンスとして返します。LaravelはResourceインスタンスが返された場合、自動的に適切なJSONレスポンスに変換してくれます。
#### 特徴:
レスポンス構造の定義: APIの出力形式を明確に定義する場所を提供します。
#### データ変換: 
データのフォーマット変更（日付形式など）、属性名の変更、条件付きでの属性の追加（whenLoaded など）が容易に行える。
#### 標準化:
API全体で統一されたレスポンス構造（例: data キーでのラップ、メタデータの追加）を実現しやすくなる。ページネーションの結果も自動的に適切なメタデータ（links, meta）付きで整形される。
#### Controllerの簡潔化: 
レスポンスの整形ロジックをResourceクラスに分離することで、Controllerはビジネスロジックの呼び出しとレスポンスの返却に責務を分けることができる。
```
PHP

// app/Http/Resources/UserResource.php
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class UserResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
            'email' => $this->email,
            'registered_at' => $this->created_at->toDateTimeString(),
            // postsリレーションがloadされている場合のみ、PostResourceのコレクションとして含める
            'posts' => PostResource::collection($this->whenLoaded('posts')),
            // 必要に応じてメタデータを追加することも可能
            // 'meta' => [...]
        ];
    }
}

// app/Http/Resources/PostResource.php (UserResourceから参照される例)
namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray($request)
    {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'content_preview' => \Illuminate\Support\Str::limit($this->content, 100), // 例: プレビュー
            'posted_at' => $this->created_at,
            // Userリソースをネストすると循環参照や過剰なデータになる可能性があるので注意
            // 必要最低限の情報（IDや名前など）に留めるか、IDのみにするのが一般的
            // 'author_id' => $this->user_id,
            // 'author_name' => $this->whenLoaded('user', fn() => $this->user->name),
        ];
    }
}
```
### 関係性のまとめとワークフロー例 (Resourceを含む)
一般的なAPIリクエスト (例: 特定ユーザーの詳細情報取得) の流れは以下のようになります。

1. リクエスト: クライアントがAPIエンドポイント (/api/users/{id}) にGETリクエストを送信
2. ルーティング: Laravelのルーターがリクエストを適切なControllerのアクション(UserController@show) に振り分ける
3. Controller (UserController):
ルートパラメータ ({id}) を受け取る。
4. UserService の findUserWithRelations メソッドを呼び出し、ユーザーデータ（関連する投稿データも含む）を取得する。
5. UserService から返された User オブジェクト（または null）を受け取る。
ユーザーが見つかった場合、UserResource を使って User オブジェクトを整形する (new UserResource($user))。
UserResource インスタンスをレスポンスとして返す（Laravelが自動でJSONに変換）。
6. ユーザーが見つからなかった場合、ApiResponseHelper Trait などを使って404エラーのJSONレスポンスを返す。
6. Service (UserService):
Controllerから受け取った id と、必要ならリレーション情報 (['posts'] など) を ``UserRepositoryInterface`` の ``findWithRelations`` メソッドに渡す。
Repositoryから返された結果 (User オブジェクトまたは null) をControllerにそのまま返す。
8.  Repository (EloquentUserRepository):
Serviceから渡された id とリレーション情報を使って、User Model の Eloquent メソッド ``(User::with('posts')->find($id))`` を呼び出し、データベースからデータを取得する。
取得した User Model インスタンス（リレーション含む）または null をServiceに返す。
9. Modl (User, Post):
Eloquentの機能により、データベースとやりとりし、データを取得する。リレーション (posts()) の定義もここで利用される。
10. Resource (UserResource, PostResource):
Controllerから渡された User オブジェクトを受け取る。
toArray メソッドの定義に従って、公開する属性を選択・整形し、ネストされた PostResource も適用して最終的なJSON構造を組み立てる。
### まとめ
Model: データとその永続化、DBレベルのリレーション定義。
Trait: クラス間で共通の振る舞いや機能を提供 (コード再利用)。
Repository: データアクセスロジックを抽象化・カプセル化 (データ層とビジネス層の分離、テスト容易性向上)。
Service: アプリケーション固有のビジネスロジック・ユースケースを実装 (ビジネスロジックの集約、テスト容易性向上)。
Resource/Collection: APIレスポンスのJSON形式を定義・整形 (プレゼンテーション層、データ構造とAPI形式の分離)。