【Laravel12入門】自作モデルを定義してみる〜ログイン認証モデルの準備〜

## はじめに
本記事は、下記記事にて構築した環境で開発しております。

https://qiita.com/kinoha/items/4cd216d69877846345ef

また、本記事は**Laravel公式ドキュメントを参考**にしています。ぜひそちらをご覧になりながら、開発を進めていただければと思います。

https://laravel.com/docs/12.x

## 本記事のゴール
**Laravel12でモデルクラスを作成すること**
とてもざっくり言うと、**新たなテーブルを作成すること**が本記事のゴールとなります。

ちなみに、ここで作成するモデルを用いてログイン認証を行うことを見据えています。
今回はその準備にもなっておりますので、最低限よりは少し過剰な実装となります。

## デフォルトのログイン認証
Laravelは、普通にプロジェクトを作成すると`User`モデルが作成されます。**デフォルトでは、このモデルを使ってログイン認証が行われます**。

しかし、開発する規模が大きくなっていけば、以下のような壁が出てきます。
- **自分で定義したモデルでもログイン認証をしたい**
- **何なら自作モデルをデフォルトの認証モデルにしたい**

本記事ではこれらを行うことを見据えて実装していきます。

## モデルの作成・マイグレーション
なにはともあれ、モデルを作成します。
### モデルの設計
今回は以下のような属性を持つモデルを考えます。
| カラム名 | 型 | 備考|
|:------:|:--:|:---:|
| id | BigInteger | 主キー |
| user_code | string | ログインIDとして利用，自動生成，ユニーク制約有り |
| user_name | string | 表示ユーザ名，ユーザが自由に設定可 |
| password | string | パスワード，ハッシュ化して保持 |
| email | string | メールアドレス |
| email_verified_at | timestamp | メール認証日時の保持 |

テーブル名（モデル名）は`Member`とでもしましょう。
これに則ってモデルを作ります。

### モデルの作成
今回は、以下のコマンドを実行します。
```php:bash
php artisan make:model Member -mscrR
```
コマンド実行後、以下のファイルが作成されていることを確認してください。

- `app/Http/Controllers/MemberController.php`
- `app/Http/Requests/StoreMemberRequest.php`
- `app/Http/Requests/UpdateMemberRequest.php`
- `app/Models/Member.php`
- `database/migrations/yyyy_mm_dd_xxxxxx_create_members_table`

また、コントローラーの中には、以下の７つのメソッドが定義されていると思います。
- index
- create
- store
- show
- edit
- update
- destroy

リソースコントローラに対するお馴染み７つのメソッドですね。

---

テーブル構造が決まったところで、モデルとなるテーブルをDBに作成したり、モデルの設定を行ったりしていきます。マイグレーションファイルを作成してテーブルを定義したり、モデルファイルを作成してモデルを定義したり…という工程です。
1. マイグレーションファイルの作成／マイグレーション実行
`php artisan make:migration create_hoge_table`
1. モデルファイルの作成／モデルファイルの編集
`php artisan make:model Hoge`

このように、それぞれのファイルを都度作成して記述していくことが可能です。今回のように新たなモデルを作るときに利用する他、マイグレーションファイルに関しては**既存のテーブルのカラム追加・削除・変更を行うような場合**にも利用します。結局のところ、テーブル定義に関する操作はマイグレーションを通して行うのです。

しかし今回は、**モデルもテーブルも、どちらも定義しなければいけません**。だったら、マイグレーションファイルとモデルファイルは一緒に作れたほうが、直感的な気がします。
さらに、先を見据えると、この`Member`モデルは**CRUD（作成／閲覧／更新／削除）機能が実装される**とも考えられます。こういった基本機能に関しては、設計段階で必要かどうか分かることでしょう。こういった機能を作るとき、Ruby on Railsだとイメージしやすいのですが、**リソース（resources）を利用する**のがとても効率的だと思います。

Laravelでは、モデルファイルを作成するときに、これらを一括して作成・定義することができるのです。詳細は公式ドキュメントを御覧ください。

https://laravel.com/docs/12.x/eloquent

これを行っているのが、今回実行するコマンドです。モデルファイル作成時にオプションをつけることで、他の関連ファイル作成や最低限の記述をしてくれます。
```php:bash
// -m: マイグレーションファイルの作成， -s: シーディングファイルの作成
// -c: コントローラの作成， -r: resourcesクラスの作成， -R: リクエストファイルの作成
php artisan make:model Member -mscrR
```
今回はマイグレーションファイルに加えて、シーディングファイルとコントローラ・リクエストファイルも同時に作成しています。ニュアンスとしては以下のとおりです。

- シーディングファイル：テーブルに最初から登録しておくデータを作成する
- コントローラ：あるURLにアクセスされたときの処理をする
- リクエスト：フォームから飛んできたデータに処理を加える

今は無視しちゃってよいです。とりあえず。
これらがあって、いよいよフレームワークっぽい挙動が出来上がってきます。

### モデルの設定
作成したモデルに対して、**入力やキャスティングに関する規約を設定**していきます。
```php:app/Models/Member.php
class Member extends Model
{
    protected $fillable = [
        'user_name',
        'password',
        'email'
    ];

    protected $hidden = [
        'password'
    ];

    public function casts(): array
    {
        return [
            'password' => 'hashed',
            'email_verified_at' => 'datetime'
        ];
    }
}
```
これらにより、以下の規約がモデルに追加できます。
- 一括挿入できるカラム
- シリアライズ時に隠すカラム
- キャスティングするカラムとキャスト先の型

---

#### `$fillable`
**レコード作成時に一括挿入できるカラムを制限する**ための配列です。
モデルは、テーブル作成時にデータを挿入するだけでなく、フォームによる新規作成や更新がされることもあります。このとき、やろうと思えば「フォームに入力欄はないけど、データ送ってやろ」ということだってできます。
そういった**予期しないカラムへのデータ登録**を防ぐための配列となります。

ちなみに**レコード作成時に一括挿入を禁止するカラムの設定**は`$guarded`となります。
公式では「全てのカラムを一括挿入OKにするときは、これを空の配列として設定してね」と言っています。基本的には`$fillable`で設定した方が直感的ですし、推奨っぽいですね。

では、「一括挿入しちゃダメなカラムにはどうするのか？」という話ですが、デフォルト値設定 or カラムへの直接代入の２つの方法が考えられます。
- デフォルト値設定
後述のマイグレーションファイル設定時にデフォルト値設定をする。
- カラムへの直接代入
`$model_object->column = 'value'`のようにモデルオブジェクトに対して直接代入

**データをプログラム側で自動生成するような場合は、後者の対応が必要**になります。今回の`Member`モデルでは、`user_code`をその対象としています。

#### `$hidden`
**シリアライズ文字列に入れないカラムを設定する**ための配列です。
モデルオブジェクトをサーバから送信するようなとき、JSONや文字列として送ることになります。その中にパスワード等の「見えちゃマズいデータ」が入っていたら洒落になりません。これを防ぐための配列となります。

#### `function casts()`
**モデルの属性⇔実際にDBに格納される値の変換を行うカラムと変換方法を設定する**ためのメソッドです。正直、私はあまり良い説明が思いつかないです。すみません。

パスワードを入力するとき、私達は単なる文字列として入力し送信します。これをそのままDBへ登録するのは危ない、というのは現代における常識です。そのため、モデル属性として送られてきたパスワードをハッシュ化してDBへ登録するのが定石となります。このハッシュ化を**DB登録時に勝手に行ってくれるようにするため**`'password' => 'hashed'`という記述が役立つのです。
日時に関してのフォームを送信すると、内部的には文字列として扱われることもあります。これをDBに特に苦がなく登録できるようにするため、また、DBから取り出した日時データを適切にdatetime型として取り扱うため`'email_verified_at' => 'datetime'`という記述が役立つのです。

実際にやってくれる変換についても公式が挙げてくれているので、実際に作るときはこれを見ながら、試しながらやってみると良いと思います。

https://laravel.com/docs/12.x/eloquent-mutators#attribute-casting

### マイグレーション
DBにモデルの実体を作ります。作成されたマイグレーションファイルの`upメソッド`の記述を追加します。
```php:create_people_table.php
public function up(): void
{
    Schema::create('members', function (Blueprint $table) {
        $table->id();
        $table->string('user_code')->unique()->nullable(false);
        $table->string('user_name')->nullable(false);
        $table->string('password')->nullable(false);
        $table->string('email')->nullable(false);
        $table->timestamp('email_verified_at')->nullable(true);
        $table->timestamps();
    });
}
```
マイグレーションファイルの記述ができたら、マイグレーションを行います。テーブルの作成です。
```:bash
php artisan migrate
```

---
マイグレーションファイルでは、**カラムの追加だけでなく、各カラムに対して型や制約を設定できます**。

#### カラムの型設定（ごく一部）
- `string(column_name)`：文字列型のカラム`column_name`を作成
- `timestamp(column_name)`：操作が行われた日時を保持するカラム`column_name`を作成
- `integer(column_name)`：整数型のカラム`column_name`を作成

https://laravel.com/docs/12.x/migrations#available-column-types

#### カラムの制約設定（ごく一部）
- `unique()`：カラムにユニーク制約を追加
- `nullable(bool)`：カラムに非null制約を付けるかどうか設定（false→null NG）
- `default(value)`：カラムのデフォルト値を設定

https://laravel.com/docs/12.x/migrations#column-modifiers

### シードデータの作成
テーブルにデフォルトで置くレコードを作成します。シーディングファイルに追記していきます。
```php:database/seeders/MemberSeeder.php
/* add writing */
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;

class MemberSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        /* add writing */
        DB::table('members')->insert([
            'user_code' => Str::random(3) . random_int(100000, 999999),
            'user_name' => 'Taro',
            'password' => Hash::make('password'),
            'email' => 'test@example.com'
        ]);
        /* add writing end */
    }
}
```
また、同ディレクトリにある`DatabaseSeeder.php`の`runメソッド`の内容はごっそり削除してしまって構いません。
```php:database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            MemberSeeder::class,
        ]);
    }
}
```
そしてシーディングを行います。
```:bash
php artisan db:seed
```
これで`members`テーブルにシードデータが投入されました。

---

#### シードデータ
「モデル作ったしテストしたいけど、そもそもデータがないのでテストができない」を回避することができます。

#### デフォルトで作られている`DatabaseSeeder.php`
**`php artisan db:seed`を実行すると、内部的にはコレが動きます**。そのため、これ以外のシーディングファイル（シードクラス）を呼び出すために`callメソッド`が必要だったのです。
```php:DatabaseSeeder.php
$this->call([SeedClass::class, ...]);
```
**メソッドの引数となる配列内に呼び出したいシードクラスを連ねる**ことで、そのクラスのシーディングが実行されます。

#### モデルごとのシーディング記述
`insertメソッド`を用いて、テーブルに直接レコードを挿入します。
以下２点に注意してください。
- **DBファザードのインポートが必要**
- **テーブル名の選択が必要**

```php:MemberSeeder.php
use Illuminate\Support\Facades\DB;
...
...
    public function run(): void
    {
        DB::table('table_name')->insert([...]);
    }
```
「なぜデフォルトで書いてくれてないんだ！」という声が聞こえてきそうですが、それは恐らく、後述の方法との選択肢を残すためだと思われます。DBにレコードを直接挿入する場合は、忘れず記述しましょう。

これ以外のやり方だと、モデルクラスを利用してモデル作成を行う方法があります。
```php:database/seeders/MemberSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\DB;
use App\Models\Member; // add writing
use Illuminate\Database\Seeder;

class MemberSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Member::create([
            'user_code' => Str::random(3) . random_int(100000, 999999),
            'user_name' => 'Jiro',
            'password' => 'password',
            'email' => 'test@example.com'
        ]);
    }
}
```
この場合、Appディレクトリから**Modelクラスのインポートが必要**になります。

ちなみに、シーディング実行中のみ全てのカラムが「一括挿入可能」となります（公式ドキュメント参照）。そのため、`createメソッド`に対して、一括挿入を許可した記憶のないカラム`user_code`を含めた全カラムにデータを挿入した状態で実行することが可能です。

https://laravel.com/docs/12.x/seeding

#### Factoryについて
ここまでのシーディング記述では、どうしても「１件ずつレコードを登録していく」という方式のため、大量のシードデータ投入には向いていません。
これを効率的に行うための機能・クラスが`Factory`になります。これにより、大量のシードデータを生成することが可能になります。

なお、本記事では１人さえデータが入っていればログイン認証やメール認証のテストもできるため、割愛します。ご了承ください。気になる方・必要な方は、まず公式ドキュメントを読んでみてください。

https://laravel.com/docs/12.x/eloquent-factories

### テーブルの完全作り直し
```:bash
php artisan migrate:fresh
```
このコマンドにより、全テーブルの削除→全テーブルの作成が行われます。必要に応じて、`--seed`オプションをつけてください。これを追加すると、シーディングまで実行してくれます。

---

場合によっては、「今までのテーブルを全部まっさらにしたい」と考えるときがあるかもしれません。そんなとき、ただマイグレーションするだけでは`Nothing to migrate`や、場合によってはエラーが出力されることもあります。
細かく言えば、ロールバックするといった安全なやり方があります。そのため、「本当に全テーブルをまっさらにしても良い」という場合に限って、覚悟を決めて実行する価値はあると思います。

こちらも公式ドキュメントがオプションを説明してくれているので、ご一読ください。

https://laravel.com/docs/12.x/migrations

## 最後に
本記事では、**自作モデルの作成**を行いました。このモデルのCRUD実装やログイン認証も見据えているため、少し過剰な実装となりました。必要なときに、必要な部分をピックアップしながら読み返していただければと思います。

また。今回は各セクションで公式ドキュメントへのリンクを示しております。本記事のような浅い説明だけでなく、ぜひそちらも読みながら、深い理解と共に実装していただければと思います。