Laravelは良くも悪くも便利機能が備わっています。
ここではModelに搭載されている$fillable
や$guarded
のような便利機能を整理しました。
Modelクラスを確認しよう
ドキュメントで確認することはもちろん可能なのですが、やはりModelのことは直接Modelクラスを参照することとします。
Laravelのバージョンによって中身が異なります
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
の設定と違う場合に使用)
サンプル
class User extends Model
{
protected $connection = 'mariadb2';
}
$table
用途
- モデル名と違うテーブル名を指定したい場合
サンプル
class User extends Model
{
protected $table = 'staffs';
}
$primaryKey
用途
-
id
以外のカラムに主キーを設定したい場合
サンプル
class Article extends Model
{
protected $primaryKey = 'article_id';
}
$keyType
用途
- 主キーにint系意外の型を利用したい場合
サンプル
class User extends Model
{
protected $keyType = 'string';
}
$incrementing
用途
- 主キーのauto increment(増分整数値)を阻止したい場合
サンプル
class User extends Model
{
public $incrementing = false;
}
$timestamps
用途
-
created_at
.updated_at
をINSERT, UPDATEに伴い自動更新させたくない場合
サンプル
class User extends Model
{
public $timestamps = false;
}
$dateFormat
用途
-
created_at
やupdated_at
のようなタイムスタンプのフォーマットをカスタムしたい場合
サンプル
class User extends Model
{
protected $dateFormat = 'Y-m-d H:i:s';
}
CREATED_AT / UPDATED_AT
用途
- タイムスタンプのカラム名を
created_at
,updated_at
から変更したい場合
サンプル
class User extends Model
{
const CREATED_AT = 'creat_at';
const UPDATED_AT = 'updat_at';
}
$attributes
用途
- モデルのデフォルト値の定義をしたい場合
サンプル
class User extends Model
{
protected $attributes = [
'icon_url' => 'https://storage.com/xxxx/xxx.png', // デフォルト画像
'is_admin' => false,
];
}
$with
用途
- 常にロードしたいリレーションがある場合
サンプル
class Book extends Model
{
/**
* 常にロードする必要があるリレーション
*
* @var array
*/
protected $with = ['author'];
/**
* この本を書いた著者を取得
*/
public function author()
{
return $this->belongsTo(Author::class);
}
}
$withCount
用途
- $withの件数だけ取得したい場合
サンプル
class Book extends Model
{
/**
* 常にロードする必要があるリレーション
*
* @var array
*/
protected $withCount = ['author'];
/**
* この本を書いた著者を取得
*/
public function author()
{
return $this->belongsTo(Author::class);
}
}
$preventsLazyLoading
用途
- 遅延ローディングの有効無効設定
- N + 1問題の対策としても有効
サンプル
以下のような場合発生する遅延ロードが発生した際にエラーで教えてくれるようです。
$posts = Post::all(); // すべての投稿を取得
foreach ($posts as $post) {
echo $post->comments; // 各投稿に対してコメントをロード(1つの投稿ごとに1回クエリ)
}
$posts = Post::with('comments')->get(); // 投稿とコメントを一括でロード
foreach ($posts as $post) {
echo $post->comments; // 追加クエリなしでコメントにアクセス
}
以下のように利用します
public function boot()
{
Model::preventLazyLoading(true);
}
$perPage
用途
-
->paginate
メソッドでページネーションを実装した際のページ単位の件数を変更したい場合
モデルクラスにはデフォルトで15が設定されています
protected $perPage = 15;
サンプル
class User extends Model
{
protected $perPage = 30;
}
$exists
用途
- DB上に存在するかどうかを指定したい場合
サンプル
$user = User::first();
Log::info($user->exists);
$wasRecentlyCreated
用途
- 現在のリクエスト中に作成されたか確認する
-
set
による更新時はfalseを返す
サンプル
$user = new User();
$user->fill($data)->save();
Log::info($user->wasRecentlyCreated);
$escapeWhenCastingToString
用途
- オブジェクトの文字列表現が__toStringメソッドで呼び出された際に、エスケープされるべきであることを示す
サンプル
class User extends Model
{
$escapeWhenCastingToString = false;
}
// name: <script>alert('XSS');</script>
// name: <script>alert('XSS');</script>
$resolver
用途
- 通常の設定以外の特殊なDB接続をしたい場合
サンプル
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などでイベントが発火してほしくない時もここで管理する
サンプル
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()
はモデル呼び出し時に自動でコールされるメソッド
サンプル
class User extends Model
{
$booted = ['books', 'doctors'];
}
$traitInitializers
用途
- モデルクラスがuseしているトレイトを走査する
- そのトレイトにinitialize{TraitName}というメソッド(例えばinitializeSoftDeletes)があるかをチェックする
- 見つかった場合、そのメソッドを呼び出すためのクロージャを$traitInitializersに格納する。
サンプル
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()で以下のように実装します
サンプル
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
されてしまうのでそれを阻止したい場合
サンプル
class Post extends Model
{
protected static $ignoreOnTouch = [User::class];
}
$modelsShouldPreventLazyLoading
用途
- モデルがまだロードしていないリレーションへアクセスした際に行われる「レイジーローディング」を全モデルで制御するための静的プロパティ
- trueに切り替えると、アプリケーション内の全てのEloquentモデルがまだロードされていないリレーションへアクセスすると例外がスローされます
サンプル
以下のように設定することでアプリ全体に適応することができます。
ただし、例外を投げてしまうので十分なテストをするか本番環境ではオフとしておきましょう。
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を返すのでカスタム例外を実装したい場合
サンプル
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に指定のないパラメーターを指定した場合例外をスローする
サンプル
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の時に発生する例外のカスタム
サンプル
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
用途
- モデルに存在しないパラメーターへアクセスしようとした場合に例外を発生させる
サンプル
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の際にスローされる例外のカスタム
サンプル
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など)をブロードキャストする機能を管理する
サンプル
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 となっていればイベントはブロードキャストされず、
// 通常のイベントディスパッチ(アプリ内ロジックのみ)に留まります。
以上です。
マイナーなものも当然ありますが、これらを用いることでファットモデルを解消する手助けになったり、品質向上につながったりすると思います。気になるものがあれば活用してみてください。