Laravel 6.x でモデルを分割して肥大化を防ぐ方法

はじめに

Laravel のモデルは1ファイルで構成され、またリレーションを関数で実装することもあって、カラム数が多いテーブルでは行数が多くなりがちです。

そこで今日は Laravel 6.x で、なるべく Laravel の使用感を保ちつつ、モデルの一部を別ファイルに分割するアイディアをご紹介します。

確認に使用したのは Laravel 6.20.7 です。

目次
  1. 下準備
  2. モデルを分割して実装
  3. 動作確認
  4. おわりに

1. 下準備

動作確認用のデータベースとして articles と users を用意しました。

マイグレーションとシードは以下の通りです。

/database/migrations/2020_12_27_000000_create_tables.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateTables extends Migration
{
    public function up()
    {
        Schema::create('articles', function (Blueprint $table) {
            $table->increments('id');
            $table->unsignedInteger('user_id');
            $table->text('body');
        });
        Schema::create('users', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
        });
    }

    public function down()
    {
        Schema::dropIfExists('articles');
        Schema::dropIfExists('users');
    }
}
/database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        DB::table('articles')->insert([
            ['user_id' => 1, 'body' => 'King の本文'],
            ['user_id' => 2, 'body' => 'Queen の本文']
        ]);
        DB::table('users')->insert([
            ['id' => 1, 'name' => 'King'],
            ['id' => 2, 'name' => 'Queen']
        ]);
    }
}

両ファイルとも動作確認用なのでまとめて書いていますが、実際の開発作業では1テーブルごとに分割するのがいいと考えています。

下記コマンドなどでデータベースを更新してください。
(fresh を使うと既存テーブルが削除されます。)

$ cd /path/to/my-project
$ php artisan migrate:fresh --seed

2. モデルを分割して実装

モデル分割には PHP の trait を使いました。

ファイル構成と内容は以下の通りです。

/path/to/my-project/
  ├ app/
  │  ├ Database/
  │  │  ├ Models/
  │  │  │  ├ Article.php
  │  │  │  └ User.php
  │  │  │
  │  │  ├ Relations/
  │  │  │  └ ArticleRelations.php
  │  │  │
  │  │  ├ Scopes/
  │  │  │  └ ArticleScopes.php
  ~~~~~
/app/Database/Relations/ArticleRelations.php
<?php

namespace App\Database\Relations;

trait ArticleRelations
{
    public function user()
    {
        return $this->belongsTo('App\Database\Models\User', 'user_id');
    }
}
/app/Database/Scopes/ArticleScopes.php
<?php

namespace App\Database\Scopes;

trait ArticleScopes
{
    public function scopeOwnedBy($query, $user_id)
    {
        return $query->where('user_id', $user_id);
    }
}
/app/Database/Models/Article.php
<?php

namespace App\Database\Models;

use App\Database\Relations\ArticleRelations;
use App\Database\Scopes\ArticleScopes;
use Illuminate\Database\Eloquent\Model;

class Article extends Model
{
    use ArticleRelations;
    use ArticleScopes;
}

そしてリレーション用に User モデルを用意します。

/app/Database/Models/User.php
<?php

namespace App\Database\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
}

これで完了です。

3. 動作確認

動作確認に使用したコントローラを置いておきます。

適宜変更してご利用ください。

/app/Http/Controllers/ArticleController.php
<?php

namespace App\Http\Controllers;

use App\Database\Models\Article;
use Illuminate\Http\Request;

class ArticleController extends Controller
{
    public function index()
    {
        $article = Article::ownedBy(2)->first();
        dd($article->user);
    }
}
/routes/web.php
// 追加
Route::get('/article', 'ArticleController@index');

4. おわりに

Laravel は自由度が高いフレームワークなのでカスタムしやすいのですが、自由ゆえに実装方法を悩むことも多いです。

今回は特にフォルダ名(ファイル構成)で悩みました。Laravel 本体の構成と同じように「Eloquent」フォルダに入れることも考えたのですが、今回は階層が深くなるのを避けるため Database の中に入れています。

ところで、この記事ではリレーションとスコープの分割方法をご紹介しましたが、アクセサなども同様に別ファイル化が可能です。

またスコープにつきましては、もしグローバルスコープを使用する場合には、同じフォルダに混在させず、違いを分かりやすくするための工夫が必要だと考えています。