LaravelのModelで使える$xxx

Laravelは良くも悪くも便利機能が備わっています。
ここではModelに搭載されている`$fillable`や`$guarded`のような便利機能を整理しました。

## Modelクラスを確認しよう

ドキュメントで確認することはもちろん可能なのですが、やはりModelのことは直接Modelクラスを参照することとします。

:::note warn
Laravelのバージョンによって中身が異なります
:::

```php
abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToString, HasBroadcastChannel, Jsonable, JsonSerializable, QueueableEntity, UrlRoutable
{
    use Concerns\HasAttributes,
        Concerns\HasEvents,
        Concerns\HasGlobalScopes,
        Concerns\HasRelationships,
        Concerns\HasTimestamps,
        Concerns\HidesAttributes,
        Concerns\GuardsAttributes,
        ForwardsCalls;

    /**
     * The connection name for the model.
     *
     * @var string|null
     */
    protected $connection;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table;

    /**
     * The primary key for the model.
     *
     * @var string
     */
    protected $primaryKey = 'id';

    /**
     * The "type" of the primary key ID.
     *
     * @var string
     */
    protected $keyType = 'int';

    /**
     * Indicates if the IDs are auto-incrementing.
     *
     * @var bool
     */
    public $incrementing = true;

    /**
     * The relations to eager load on every query.
     *
     * @var array
     */
    protected $with = [];

    /**
     * The relationship counts that should be eager loaded on every query.
     *
     * @var array
     */
    protected $withCount = [];

    /**
     * Indicates whether lazy loading will be prevented on this model.
     *
     * @var bool
     */
    public $preventsLazyLoading = false;

    /**
     * The number of models to return for pagination.
     *
     * @var int
     */
    protected $perPage = 15;

    /**
     * Indicates if the model exists.
     *
     * @var bool
     */
    public $exists = false;

    /**
     * Indicates if the model was inserted during the current request lifecycle.
     *
     * @var bool
     */
    public $wasRecentlyCreated = false;

    /**
     * Indicates that the object's string representation should be escaped when __toString is invoked.
     *
     * @var bool
     */
    protected $escapeWhenCastingToString = false;

    /**
     * The connection resolver instance.
     *
     * @var \Illuminate\Database\ConnectionResolverInterface
     */
    protected static $resolver;

    /**
     * The event dispatcher instance.
     *
     * @var \Illuminate\Contracts\Events\Dispatcher
     */
    protected static $dispatcher;

    /**
     * The array of booted models.
     *
     * @var array
     */
    protected static $booted = [];

    /**
     * The array of trait initializers that will be called on each new instance.
     *
     * @var array
     */
    protected static $traitInitializers = [];

    /**
     * The array of global scopes on the model.
     *
     * @var array
     */
    protected static $globalScopes = [];

    /**
     * The list of models classes that should not be affected with touch.
     *
     * @var array
     */
    protected static $ignoreOnTouch = [];

    /**
     * Indicates whether lazy loading should be restricted on all models.
     *
     * @var bool
     */
    protected static $modelsShouldPreventLazyLoading = false;

    /**
     * The callback that is responsible for handling lazy loading violations.
     *
     * @var callable|null
     */
    protected static $lazyLoadingViolationCallback;

    /**
     * Indicates if an exception should be thrown instead of silently discarding non-fillable attributes.
     *
     * @var bool
     */
    protected static $modelsShouldPreventSilentlyDiscardingAttributes = false;

    /**
     * The callback that is responsible for handling discarded attribute violations.
     *
     * @var callable|null
     */
    protected static $discardedAttributeViolationCallback;

    /**
     * Indicates if an exception should be thrown when trying to access a missing attribute on a retrieved model.
     *
     * @var bool
     */
    protected static $modelsShouldPreventAccessingMissingAttributes = false;

    /**
     * The callback that is responsible for handling missing attribute violations.
     *
     * @var callable|null
     */
    protected static $missingAttributeViolationCallback;

    /**
     * Indicates if broadcasting is currently enabled.
     *
     * @var bool
     */
    protected static $isBroadcasting = true;

    /** .... */
```

上から確認していきましょう

## $connection

#### 用途

- 別のDBに接続させたい場合
（`config/database.php`の設定と違う場合に使用）

#### サンプル
```php
class User extends Model
{
    protected $connection = 'mariadb2';
}
```

## $table

#### 用途

- モデル名と違うテーブル名を指定したい場合

#### サンプル
```php
class User extends Model
{
    protected $table = 'staffs';
}
```


## $primaryKey

#### 用途

- `id`以外のカラムに主キーを設定したい場合

#### サンプル
```php
class Article extends Model
{
    protected $primaryKey = 'article_id';
}
```

## $keyType

#### 用途

- 主キーにint系意外の型を利用したい場合

#### サンプル
```php
class User extends Model
{
    protected $keyType = 'string';
}
```


## $incrementing

#### 用途

- 主キーのauto increment（増分整数値）を阻止したい場合

#### サンプル
```php
class User extends Model
{
    public $incrementing = false;
}
```

## $timestamps

#### 用途

- `created_at`. `updated_at`をINSERT, UPDATEに伴い自動更新させたくない場合

#### サンプル
```php
class User extends Model
{
    public $timestamps = false;
}
```

## $dateFormat

#### 用途

- `created_at`や`updated_at`のようなタイムスタンプのフォーマットをカスタムしたい場合

### サンプル

```php
class User extends Model
{
    protected $dateFormat = 'Y-m-d H:i:s';
}
```

## CREATED_AT / UPDATED_AT

#### 用途

- タイムスタンプのカラム名を`created_at`, `updated_at`から変更したい場合

#### サンプル
```php
class User extends Model
{
    const CREATED_AT = 'creat_at';
    const UPDATED_AT = 'updat_at';
}
```

## $attributes

#### 用途

- モデルのデフォルト値の定義をしたい場合

#### サンプル
```php
class User extends Model
{
    protected $attributes = [
        'icon_url' => 'https://storage.com/xxxx/xxx.png', // デフォルト画像
        'is_admin' => false,
    ];
}
```

## $with

#### 用途

- 常にロードしたいリレーションがある場合

#### サンプル
```php
class Book extends Model
{
    /**
     * 常にロードする必要があるリレーション
     *
     * @var array
     */
    protected $with = ['author'];

    /**
     * この本を書いた著者を取得
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}
```

## $withCount

#### 用途

- $withの件数だけ取得したい場合

#### サンプル
```php
class Book extends Model
{
    /**
     * 常にロードする必要があるリレーション
     *
     * @var array
     */
    protected $withCount = ['author'];

    /**
     * この本を書いた著者を取得
     */
    public function author()
    {
        return $this->belongsTo(Author::class);
    }
}
```


## $preventsLazyLoading

#### 用途

- 遅延ローディングの有効無効設定
- N + 1問題の対策としても有効

#### サンプル

以下のような場合発生する遅延ロードが発生した際にエラーで教えてくれるようです。

```php
$posts = Post::all();  // すべての投稿を取得
foreach ($posts as $post) {
    echo $post->comments;  // 各投稿に対してコメントをロード（1つの投稿ごとに1回クエリ）
}
```

```php
$posts = Post::with('comments')->get();  // 投稿とコメントを一括でロード
foreach ($posts as $post) {
    echo $post->comments;  // 追加クエリなしでコメントにアクセス
}
```

以下のように利用します

```php
public function boot()
{
    Model::preventLazyLoading(true);
}
```

## $perPage

#### 用途

- `->paginate`メソッドでページネーションを実装した際のページ単位の件数を変更したい場合

モデルクラスにはデフォルトで15が設定されています
```php
protected $perPage = 15;
```

#### サンプル
```php
class User extends Model
{
    protected $perPage = 30;
}
```

## $exists

#### 用途

- DB上に存在するかどうかを指定したい場合

#### サンプル
```php
$user = User::first();
Log::info($user->exists);
```

## $wasRecentlyCreated

#### 用途

- 現在のリクエスト中に作成されたか確認する
- `set`による更新時はfalseを返す

#### サンプル
```php
$user = new User();
$user->fill($data)->save();
Log::info($user->wasRecentlyCreated);
```

## $escapeWhenCastingToString

#### 用途

- オブジェクトの文字列表現が__toStringメソッドで呼び出された際に、エスケープされるべきであることを示す

#### サンプル
```php
class User extends Model
{
    $escapeWhenCastingToString = false;
}

// name: <script>alert('XSS');</script>
// name: &lt;script&gt;alert(&#039;XSS&#039;);&lt;/script&gt;
```

## $resolver
#### 用途

- 通常の設定以外の特殊なDB接続をしたい場合

#### サンプル
```php
use Illuminate\Database\ConnectionResolverInterface;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Connection;

// 独自のConnectionResolver実装例
class CustomConnectionResolver implements ConnectionResolverInterface
{
    protected $connections = [];
    protected $default = 'default';

    public function __construct()
    {
        // ここで必要な接続インスタンスを生成または注入
        $this->connections['default'] = app('db')->connection(); // デフォルト接続
        $this->connections['test'] = app('db')->connection('sqlite'); // テスト用接続 など
    }

    public function connection($name = null): Connection
    {
        $name = $name ?: $this->getDefaultConnection();
        return $this->connections[$name];
    }

    public function getDefaultConnection(): string
    {
        return $this->default;
    }

    public function setDefaultConnection($name)
    {
        $this->default = $name;
    }
}

// カスタムリゾルバのセット
$customResolver = new CustomConnectionResolver();
Model::setConnectionResolver($customResolver);

// 以降、このアプリケーション内の全てのEloquentモデルは
// `$customResolver`を通してDB接続が解決される
$user = User::find(1); // カスタムリゾルバ経由で接続取得
```


## $dispatcher
#### 用途

- LaravelのEloquentモデル内部のイベント（creating, created, updating, updated, saving, saved, deleting, deleted）を管理
- SeederやFactory, Testなどでイベントが発火してほしくない時もここで管理する

#### サンプル

```php
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Model;
use Mockery as m;

class User extends Model {
    // 通常のEloquentモデル
}

// テストなどで、カスタムディスパッチャをセット
$mockDispatcher = m::mock(Dispatcher::class);

// 例えば、"creating"イベントが発火されたことを確認したいとき
$mockDispatcher->shouldReceive('dispatch')->once()->with(m::on(function($event) {
    return $event->model instanceof User && $event->name === 'eloquent.creating: ' . User::class;
}));

User::setEventDispatcher($mockDispatcher);

// ここでUserを保存すると、"creating"イベントがディスパッチされ、モックで受け取れる
$user = new User();
$user->name = 'John Doe';
$user->save();
```

## $booted

#### 用途

- 対象モデル以外の`boot()`を呼び出したいモデルを羅列
- `boot()`はモデル呼び出し時に自動でコールされるメソッド

#### サンプル
```php
class User extends Model
{
    $booted = ['books', 'doctors'];
}
```

## $traitInitializers

#### 用途

- モデルクラスがuseしているトレイトを走査する
- そのトレイトにinitialize{TraitName}というメソッド（例えばinitializeSoftDeletes）があるかをチェックする
- 見つかった場合、そのメソッドを呼び出すためのクロージャを$traitInitializersに格納する。


#### サンプル
```php
trait HasSluggable {
    // このメソッドは「イニシャライザーメソッド」として認識される
    protected function initializeHasSluggable()
    {
        // このトレイトに必要な初期化処理
        $this->slug = Str::slug($this->title);
    }
}

class Post extends Model {
    use HasSluggable;
}

// 上記のPostモデルがロードされた際、LaravelはHasSluggableトレイト内に
// `initializeHasSluggable`メソッドがあることを検出し、
// `$traitInitializers[] = function($model) { $model->initializeHasSluggable(); };`
// のようなクロージャを内部的に登録する。

$post = new Post(['title' => 'Hello World']);
// インスタンス生成時、$traitInitializers内のクロージャが呼ばれ、
// initializeHasSluggable()が実行され、$post->slug に "hello-world" が設定される。
```

## $globalScopes

#### 用途

- グローバルスコープを管理します
- $globalScopesへの追加は`Modelクラス::addGlobalScope`で行われます
- モデルのbooted()で以下のように実装します

#### サンプル
```php
class AncientScope implements Scope
{
    /**
     * Apply the scope to a given Eloquent query builder.
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('created_at', '<', now()->subYears(2000));
    }
}

class User extends Model
{
    /**
     * The "booted" method of the model.
     */
    protected static function booted(): void
    {
        static::addGlobalScope(new AncientScope);
    }
}
```

## $ignoreOnTouch

#### 用途

- 通常モデルが更新された時には`touch`メソッドにより`updated_at`も更新されるがそれを阻止したい場合
- 通常、`belongTo`のようにリレーションがあるモデルも`touch`されてしまうのでそれを阻止したい場合
 
#### サンプル
```php
class Post extends Model
{
    protected static $ignoreOnTouch = [User::class];
}
```


## $modelsShouldPreventLazyLoading

#### 用途

- モデルがまだロードしていないリレーションへアクセスした際に行われる「レイジーローディング」を全モデルで制御するための静的プロパティ
- trueに切り替えると、アプリケーション内の全てのEloquentモデルがまだロードされていないリレーションへアクセスすると例外がスローされます

#### サンプル

以下のように設定することでアプリ全体に適応することができます。
ただし、例外を投げてしまうので十分なテストをするか本番環境ではオフとしておきましょう。

```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // 本番以外の環境ではレイジーローディングを防ぐ
        // 本番環境であればパフォーマンス問題にはならないように、この制御はオフにするなど環境によって切り替えるとよい
        Model::preventLazyLoading(! $this->app->isProduction());
    }
}
```


## $lazyLoadingViolationCallback

#### 用途

- 上記の`$modelsShouldPreventLazyLoading`がtrueの場合有効
- 通常[LazyLoadingViolationException](https://laravel.com/api/10.x/Illuminate/Database/LazyLoadingViolationException.html)を返すのでカスタム例外を実装したい場合

#### サンプル
```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\LazyLoadingViolationException;

// 独自の違反処理ロジックを定義
Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
    // 例えばログを出した後、例外をスローするといったカスタム処理
    \Log::warning("Lazy loading violation on model " . get_class($model) . " for relation {$relation}.");
    
    // デフォルト動作（例外スロー）を保ちたい場合
    throw new LazyLoadingViolationException($model, $relation);
});
```

## $modelsShouldPreventSilentlyDiscardingAttributes

#### 用途

- $fillableに指定のないパラメーターを指定した場合例外をスローする

#### サンプル
```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\MassAssignmentException;

class AppServiceProvider extends ServiceProvider
{
    public function boot()
    {
        // ローカル環境では非フィルアブル属性を指定した時に例外を起こすようにする
        Model::preventSilentlyDiscardingAttributes(! $this->app->isProduction());
    }
}

class User extends Model
{
    protected $fillable = ['name', 'email'];
}

$user = new User();

// 'password'はfillableではない属性として仮定
try {
    $user->fill(['name' => 'John Doe', 'password' => 'secret']);
} catch (MassAssignmentException $e) {
    // ここに来ることで、"password"が非許可属性だったことをすぐに把握できる
}
```

## $discardedAttributeViolationCallback

#### 用途

- `$modelsShouldPreventSilentlyDiscardingAttributes`がtrueの時に発生する例外のカスタム

#### サンプル
```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\MassAssignmentException;

// たとえば、無視しようとしている属性が見つかったら例外をスローするコールバックを登録
Model::handleDiscardedAttributesUsing(function ($model, $attributes) {
    // ここで任意の処理を行う。例えばログを残したり、例外を投げたりできる
    \Log::warning('Attempted to mass assign non-fillable attributes', [
        'model' => get_class($model),
        'attributes' => $attributes,
    ]);
    
    throw new MassAssignmentException(sprintf(
        'Attempted to set non-fillable attributes on %s: %s',
        get_class($model),
        implode(', ', $attributes)
    ));
});

// `$modelsShouldPreventSilentlyDiscardingAttributes`をtrueにして
// 実際にdiscardedAttributeViolationCallbackを有効化する
Model::preventSilentlyDiscardingAttributes(true);
```

## $modelsShouldPreventAccessingMissingAttributes

#### 用途

- モデルに存在しないパラメーターへアクセスしようとした場合に例外を発生させる

#### サンプル
```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\MissingAttributeException;

// アプリケーション起動時、たとえばAppServiceProviderで設定
Model::preventAccessingMissingAttributes(true);

$user = User::find(1);

// Userモデルには存在しない属性 'nickname' にアクセスしてみる
try {
    $nickname = $user->nickname; // 存在しない属性
} catch (MissingAttributeException $e) {
    // ここで例外がスローされる
    dd($e->getMessage());
}
```

## $missingAttributeViolationCallback

#### 用途

- `$modelsShouldPreventAccessingMissingAttributes`がtrueの際にスローされる例外のカスタム

#### サンプル
```php
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\MissingAttributeException;

// 1. 開発環境などで、存在しない属性アクセスを例外として扱う設定を有効にする
Model::preventAccessingMissingAttributes(true);

// 2. 存在しない属性にアクセスした際に呼ばれるカスタムコールバックを登録
Model::handleMissingAttributeViolationUsing(function ($model, $key) {
    // ここでログを残したり、独自処理を加えたりできる
    \Log::error("Attempted to access missing attribute [{$key}] on model [".get_class($model)."]");

    // デフォルトでは MissingAttributeException をスローするが、
    // カスタム例外を投げてもいいし、単にnull返すなども可能
    throw new MissingAttributeException($model, $key);
});

// テスト
$user = User::find(1);
$nickname = $user->nickname; 
// `nickname`が存在しなければ、上記で登録したコールバックが呼び出され、ログ記録＋例外スローとなる。
```

## $isBroadcasting

#### 用途

- モデルイベント（created, updated, deletedなど）をブロードキャストする機能を管理する

#### サンプル
```php
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    /**
     * Indicates if broadcasting is currently enabled.
     *
     * @var bool
     */
    protected static $isBroadcasting = true;

    /**
     * モデルイベントと紐づけるイベントクラス
     * ここでは created イベント時に UserCreated イベントをディスパッチすることを想定
     */
    protected $dispatchesEvents = [
        'created' => \App\Events\UserCreated::class,
    ];

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

// ---------------------------------------
// コントローラやtinkerなどでの挙動テスト例
// ---------------------------------------

// Userモデルを作成
$user = User::create([
    'name' => 'John Doe',
    'email' => 'john@example.com'
]);

// 上記では、$isBroadcasting が true の場合、UserCreatedイベントがブロードキャストチャネルを通して
// リアルタイムにフロントエンドへ通知されます。

// もしブロードキャストを一時的に止めたい場合は、以下のようにすることで停止可能
User::preventBroadcasting(true);
// または、モデルクラス上で $isBroadcasting = false; に設定し直す

$user = User::create([
    'name' => 'Jane Doe',
    'email' => 'jane@example.com'
]);
// ここでは、$isBroadcasting = false となっていればイベントはブロードキャストされず、
// 通常のイベントディスパッチ（アプリ内ロジックのみ）に留まります。

```


以上です。
マイナーなものも当然ありますが、これらを用いることでファットモデルを解消する手助けになったり、品質向上につながったりすると思います。気になるものがあれば活用してみてください。