Help us understand the problem. What is going on with this article?

Laravelで大規模プロジェクトと戦うにはRepository層を導入するといいかも

Laravelをなんとなく書いていると起こる問題点

  • コントローラ膨らみすぎ問題
  • Model膨らみすぎ問題
  • MdoelにORMマッパーにベタベタに依存したビジネスロジックを書いてしまってテスト書くのがしんどくなる問題

設計方針

  1. 重要度が高いビジネスロジックはDBやLaravelの機能に依存させない
    LaravelのModelにビジネスロジックを書いてしまうとEloquent依存のコードを簡単にかけてしまいます。
    とりあえず動かすにはこれが一番早いので選択肢としてはありかと思いますが、後に機能が複雑になってくるとテスト結果がDBの状態になったりテストがしにくくなるのでやめましょう
    とはいえEloquentは便利なのでRepository内で使いましょう

  2. できるところはLaravelに依存する
    ログイン、ページネーション、バリデーションなどはLaravelに依存しましょう。

  3. できる限りテストを簡単に
    テストを簡単にかけるような設計でないとテスト書くのが苦痛になり、テストが雑になりがちです。

解決策

DDDや、クリーンアーキテクチャはそのまま導入しようとしましたが、現状に対してあまりに壮大過ぎる&チームでやるには学習コストが高すぎるので以下を導入することにしました。

Modelからビジネスロジックを分離させる

Entitiesディレクトリを新規に作成し、ここに重要度が高いビジネスロジックをModelに依存させない形で作成します。
LaravelのFacadeも使ってはいけません。CakePHPやWordPressにそのままコピペできるようにしましょう。

<?php


namespace App\Entities;


class Video
{
    protected int $id;
    protected string $title;
    protected string $description;
    protected string $publish_at;
    protected User $author;

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getTitle(): string
    {
        return $this->title;
    }

    /**
     * @return string
     */
    public function getDescription(): string
    {
        return $this->description;
    }

    /**
     * @return User
     */
    public function getAuthor(): User
    {
        return $this->author;
    }

    /**
     * @return User|string
     */
    public function getPublishAt()
    {
        return $this->publish_at;
    }

    /**
     * Video constructor.
     * @param int $id
     * @param string $title
     * @param string $description
     * @param string $publish_at
     * @param User $author
     */
    public function __construct(int $id, string $title, string $description, string $publish_at, User $author)
    {
        $this->id = $id;
        $this->title = $title;
        $this->description = $description;
        $this->publish_at = $publish_at;
        $this->author = $author;
    }

    public function check_publish(){
        // いろいろなロジックを書いていく
    }
}

Repository層の導入

Repositoriesディレクトリを作成し、ここでEloquentを使いながら上のEntityを作成します。
まずはapp/Repositoriesにインターフェイスを作成します。インターフェイスを作成することでデータベースへの依存を防げます。

<?php


namespace App\Repositories;


use App\Entities\Video;

interface VideoRepository
{
    public function getById(int $id): ?Video;
}

app/Repositories/implに実際にEloquentを使った組み込みを書いていきます。

<?php


namespace App\Repositories\impl;


use App\Entities\Video;
use App\Repositories\VideoRepository;

class VideoDBRepository implements VideoRepository
{
    function getById(int $id): ?Video
    {
        $video = \App\Model\Video::find($id);
        if(empty($video)){
            return null;
        }
        return new Video($video->id, $video->title, $video->discription, $video->author}
}

サービスプロバイダーを追加し、VideoRepositoryとVideoDBRepositoryをバインドします。
これでVideoRepository参照するとVideoDBRepositoryが読み込まれます。
VideoRepositoryに依存することにより入れ替えで簡単にテストができます。

<?php

namespace App\Providers;

use App\Repositories\impl\VideoDBRepository;
use App\Repositories\VideoRepository;
use Illuminate\Support\ServiceProvider;

class RepositoryServiceProvider extends ServiceProvider
{
    /**
     * Register services.
     */
    public function register(): void
    {
    }

    /**
     * Bootstrap services.
     */
    public function boot(): void
    {
        $this->app->bind(VideoRepository::class, VideoDBRepository::class);
    }
}

実際にリポジトリを使用する時はコンストラクタインジェクションを使います。
サービス・プロバイダーでbindしているのでInterfaceからアクセス可能です。

<?php

namespace App\Http\Controllers;

use App\Entities\Video;
use App\Repositories\VideoRepository;


class VideoController extends Controller
{
    protected VideoRepository $videoRepository;

    /**
     * VideoController constructor.
     * @param VideoRepository $videoRepository
     */
    public function __construct(VideoRepository $videoRepository)
    {
        $this->videoRepository = $videoRepository;
    }

    public function video(VideoRequest $request){
        /** @var Video $video */
        $video = $this->videoRepository->getById($request->get('id'));
        return view('video', [
            'video' => $video
        ]);
    }
}

まとめ

ControllerはRepositoryのIntarfaceに依存しているので、ControllerはDBを意識しないでよくなりました。
テスト用のMockRepositoryでも、DBではなくAPIで永続化をするApiRepositoryでもimpl組み込みを追加しbindし直すだけで対応が可能になりました。
そしてEntityではRepositoryもControllerも意識する必要がありません。ロジックのテストにDBの依存が入らないのでテストがやりやすくなります。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした