30分で理解するEloquentの巨大な全貌

HAYASHI Masayuki

Eloquentしっかり理解して使えてますか?

気軽に使えるけど、本当に理解して使うのは難しい

$fillableは具体的にどこで有効か

<?php

namespace App\Models;

// これはEloquent\Modelを継承したクラス
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    // ...
}

createの場合は、setterでの代入の場合は

<?php

// createの場合は?
User::create([
    'name'              => 'tarou',
    'email'             => 'tarou@example.com',
    'password'          => bcrypt('password'),
    'email_verified_at' => now()
]);

// setterの場合は?
$user = new User();
$user->name              = 'tarou';
$user->email             = 'tarou@example.com';
$user->password          = bcrypt('password');
$user->email_verified_at = now();
$user->save();

firstOrCreateの場合は

<?php

$user = User::firstOrCreate([
    'email'             => 'tarou@example.com',
], [
    'name'              => 'tarou',
    'password'          => bcrypt('password'),
    'email_verified_at' => now(),
]);

このリレーションクエリはなにがおかしい

<?php

$user->posts()->updateOrCreate(['user_id' => $user->id], $attributes);

insertでcreated_at, updated_atが入らないのはなぜか

<?php

use App\Models\User;

User::insert([
  ['name' => 'tarou', 'email' => 'tarou@example.com', 'password' => bcrypt('password')],
  ['name' => 'jirou', 'email' => 'jirou@example.com', 'password' => bcrypt('password')],
]);
>>> User::all()
=> Illuminate\Database\Eloquent\Collection {#3880
     all: [
       App\Models\User {#4502
         id: 1,
         name: "tarou",
         email: "tarou@example.com",
         email_verified_at: null,
         #password: "$2y$10$cwEulK.T4gTcHuycWBIpXOATx7gQFZqzpxEXqhc2wUb7MqSp51jpW",
         #remember_token: null,
         created_at: null,
         updated_at: null,
       },
       App\Models\User {#4240
         id: 2,
         name: "jirou",
         email: "jirou@example.com",
         email_verified_at: null,
         #password: "$2y$10$AlitastY/2.g.sTzw8EnGuBfrCxoq21Zvc7exttPmn8dE6VEKhRQm",
         #remember_token: null,
         created_at: null,
         updated_at: null,
       },
     ],
   }

Eloquentの全貌を理解していれば、
問題解決もスムーズかも

どうやって理解するか

  • ソースコードぜんぶ読む
    • 無謀
  • どういう機能があり、どう配置されているか、の把握に絞る
    • くらいならできそう

Eloquentの「範囲」

  • Illuminate\Database\Eloquent\Modelを継承したクラス→Eloquentのモデル

「範囲外」

  • Illuminate\Database以下でもEloquent以外は「範囲外」
  • make:modelコマンド
  • ファクトリ

名前について

  • Illuminate\Database\Eloquent\Model -> Eloquent\Model
  • Illuminate\Database\Query\Builder -> Query\Builder

そもそもEloquentって本当に「巨大」なの?

  • コードの規模の計測は難しい
  • よく使われるのは行数だが、行数だと規模感掴むのが難しそう
    • メソッド数なら?

数えてみよう

count((new ReflectionClass(new class extends Illuminate\Database\Eloquent\Model {}))->getMethods())

これをtinkerで実行してみると、

350

__call __callStatic __construct __get __isset __set __sleep __toString __unset __wakeup addCastAttributesToArray addDateAttributesToArray addGlobalScope addMutatedAttributesToArray addObservableEvents all append asDate asDateTime asDecimal asJson asTimestamp attributesToArray belongsTo belongsToMany boot bootIfNotBooted bootTraits booted booting broadcastChannel broadcastChannelRoute cacheMutatedAttributes callNamedScope castAttribute castAttributeAsEncryptedString castAttributeAsJson clearBootedModels created creating decrement decrementQuietly delete deleteOrFail deleteQuietly deleted deleting destroy deviateClassCastableAttribute encryptUsing escapeWhenCastingToString fill fillJsonAttribute fillable fillableFromArray filterModelEventResults finishSave fireCustomModelEvent fireModelEvent flushEventListeners forceDelete forceFill forwardCallTo forwardDecoratedCallTo fresh freshTimestamp

freshTimestampString fromDateTime fromEncryptedString fromFloat fromJson getActualClassNameForMorph getArrayAttributeByKey getArrayAttributeWithValue getArrayableAppends getArrayableAttributes getArrayableItems getArrayableRelations getAttribute getAttributeFromArray getAttributeMarkedMutatorMethods getAttributeValue getAttributes getAttributesForInsert getCastType getCasts getChanges getClassCastableAttributeValue getConnection getConnectionName getConnectionResolver getCreatedAtColumn getDateFormat getDates getDirty getEnumCastableAttributeValue getEventDispatcher getFillable getForeignKey getGlobalScope getGlobalScopes getGuarded getHidden getIncrementing getKey getKeyForSaveQuery getKeyForSelectQuery getKeyName getKeyType getMorphClass getMorphs getMutatedAttributes getMutatorMethods getObservableEvents getOriginal getOriginalWithoutRewindingModel getPerPage getQualifiedCreatedAtColumn getQualifiedKeyName

getQualifiedUpdatedAtColumn getQueueableConnection getQueueableId getQueueableRelations getRawOriginal getRelation getRelationValue getRelations getRelationshipFromMethod getRouteKey getRouteKeyName getTable getTouchedRelations getUpdatedAtColumn getVisible guard guessBelongsToManyRelation guessBelongsToRelation handleLazyLoadingViolation handleLazyLoadingViolationUsing hasAppended hasAttributeGetMutator hasAttributeMutator hasAttributeSetMutator hasCast hasChanges hasGetMutator hasGlobalScope hasMany hasManyThrough hasNamedScope hasOne hasOneThrough hasSetMutator increment incrementOrDecrement incrementQuietly initializeTraits insertAndSetId is isClassCastable isClassDeviable isClassSerializable isClean isCustomDateTimeCast isDateAttribute isDateCastable isDateCastableWithCustomFormat isDecimalCast isDirty isEncryptedCastable isEnumCastable isFillable isGuardableColumn isGuarded isIgnoringTouch isImmutableCustomDateTimeCast

isJsonCastable isNot isRelation isStandardDateFormat isUnguarded joiningTable joiningTableSegment jsonSerialize load loadAggregate loadAvg loadCount loadExists loadMax loadMin loadMissing loadMorph loadMorphAggregate loadMorphAvg loadMorphCount loadMorphMax loadMorphMin loadMorphSum loadSum makeHidden makeHiddenIf makeVisible makeVisibleIf mergeAttributesFromAttributeCasts mergeAttributesFromCachedCasts mergeAttributesFromClassCasts mergeCasts mergeFillable mergeGuarded morphEagerTo morphInstanceTo morphMany morphOne morphTo morphToMany morphedByMany mutateAttribute mutateAttributeForArray mutateAttributeMarkedAttribute newBaseQueryBuilder newBelongsTo newBelongsToMany newCollection newEloquentBuilder newFromBuilder newHasMany newHasManyThrough newHasOne newHasOneThrough newInstance newModelQuery newMorphMany newMorphOne newMorphTo newMorphToMany newPivot newQuery newQueryForRestoration

newQueryWithoutRelationships newQueryWithoutScope newQueryWithoutScopes newRelatedInstance newRelatedThroughInstance normalizeCastClassResponse observe offsetExists offsetGet offsetSet offsetUnset on onWriteConnection only originalIsEquivalent parseCasterClass performDeleteOnModel performInsert performUpdate preventLazyLoading preventsLazyLoading push qualifyColumn qualifyColumns query refresh registerGlobalScopes registerModelEvent registerObserver reguard relationLoaded relationsToArray removeObservableEvents replicate replicateQuietly replicating resolveCasterClass resolveChildRouteBinding resolveChildRouteBindingQuery resolveConnection resolveRelationUsing resolveRouteBinding resolveRouteBindingQuery resolveSoftDeletableChildRouteBinding resolveSoftDeletableRouteBinding retrieved save saveOrFail saveQuietly saved saving serializeClassCastableAttribute serializeDate setAppends setAttribute setAttributeMarkedMutatedAttributeValue

setClassCastableAttribute setConnection setConnectionResolver setCreatedAt setDateFormat setEnumCastableAttribute setEventDispatcher setHidden setIncrementing setKeyName setKeyType setKeysForSaveQuery setKeysForSelectQuery setMutatedAttributeValue setObservableEvents setPerPage setRawAttributes setRelation setRelations setTable setTouchedRelations setUpdatedAt setVisible syncChanges syncOriginal syncOriginalAttribute syncOriginalAttributes throwBadMethodCallException toArray toJson totallyGuarded touch touchOwners touches transformModelValue unguard unguarded unsetConnectionResolver unsetEventDispatcher unsetRelation unsetRelations update updateOrFail updateQuietly updateTimestamps updated updating usesTimestamps wasChanged with withoutBroadcasting withoutEvents withoutRelations withoutTouching withoutTouchingOn

Eloquentはどのように使うか

<?php

// 行を取得する
$users = User::where('email', 'like', '%@example.com')->get();
$user  = User::where('id', 1)->first();

// 取得した行を使う
echo $user->name;
$user->update(['password' => bcrypt('password')]);

Eloquent\Modelにはwhere, get, firstはない

whereはEloquent\Builderに実装されているものを委譲で呼び出している

User::where
→ User::__callStatic
→ User::__call
→ Eloquent\Builder::where

__call __callStatic __clone __construct __get addHasWhere addNestedWiths addNewWheresWithinGroup addTimestampsToUpsertValues addUpdatedAtColumn addUpdatedAtToUpsertColumns addWhereCountQuery applyScopes baseSole callNamedScope callScope canUseExistsForExistenceCheck chunk chunkById chunkMap clone combineConstraints create createNestedWhere createSelectWithConstraint cursor cursorPaginate cursorPaginator decrement defaultKeyName delete doesntHave doesntHaveMorph each eachById eagerLoadRelation eagerLoadRelations enforceOrderBy ensureOrderForCursorPagination find findMany findOr findOrFail findOrNew first firstOr firstOrCreate firstOrFail firstOrNew firstWhere forceCreate forceDelete forwardCallTo forwardDecoratedCallTo fromQuery get getBelongsToRelation getEagerLoads getGlobalMacro getMacro getModel getModels getOriginalColumnNameForCursorPagination getQuery getRelation getRelationWithoutConstraints groupWhereSliceForScope has hasGlobalMacro hasMacro hasMorph hasNamedScope hasNested hydrate increment isNestedUnder latest lazy lazyById lazyByIdDesc make mergeConstraintsFrom

newModelInstance oldest onDelete orDoesntHave orDoesntHaveMorph orHas orHasMorph orWhere orWhereBelongsTo orWhereDoesntHave orWhereDoesntHaveMorph orWhereHas orWhereHasMorph orWhereMorphRelation orWhereMorphedTo orWhereNot orWhereNotMorphedTo orWhereRelation orderedLazyById paginate paginateUsingCursor paginator parseNameAndAttributeSelectionConstraint parseWithRelations pluck prepareNestedWithRelationships qualifyColumn qualifyColumns registerMixin relationsNestedUnder removedScopes requalifyWhereTables scopes setEagerLoads setModel setQuery simplePaginate simplePaginator sole soleValue tap throwBadMethodCallException toBase unless update updateOrCreate upsert value valueOrFail when where whereBelongsTo whereDoesntHave whereDoesntHaveMorph whereHas whereHasMorph whereKey whereKeyNot whereMorphRelation whereMorphedTo whereNot whereNotMorphedTo whereRelation with withAggregate withAvg withCasts withCount withExists withGlobalScope withMax withMin withOnly withSum withWhereHas without withoutEagerLoad withoutEagerLoads withoutGlobalScope withoutGlobalScopes

Eloquent\ModelにもEloquent\Builderにもないメソッド

  • select
  • join
  • groupBy
  • orderBy
  • etc...

Eloquent\BuilderにもないメソッドはQuery\Builderのものを呼び出している

Eloquent\Builder::select
→ Eloquent\Builder::__call
→ Illuminate\Database\Query\Builder::select

User::whereが返すのはEloquent\Builder

<?php

echo get_class(User::where('id', 1));
// → 'Illuminate\Database\Eloquent\Builder'

User::where('id', 1) // whereはEloquent\Builderのメソッド
    ->select('id')   // idはQuery\Builderのメソッド
    ->get()          // getはまた、Eloquent\Builderのメソッド

Query\Builderとは

  • 200を超えるメソッドの半数以上が、自身を返すメソッド
    • 大半の機能がメソッドチェーンでSQLを組み立てるもの
    • ちなみにEloquent\Builderと合わせると、メソッド数は340ほどに
  • 残りの多くは、取得した行、そのコレクションを返す
  • Query\Builderは、メソッドチェーンでSQLクエリを表現し、それを実行するもの

Query\BuilderはEloquentの一部……ではない

  • 単純に、名前空間が違う
    • Eloquentは、Illuminate\Database\Eloquent以下
    • Query\Builderは、Illuminate\Database\Query\Builder

なぜ直接Query\Builderを使わないのか?

  • Eloquent\Builderがなにを実装しているかを見ればよさそう
    • Query\Builderのメソッドをオーバーライドしているもの
    • それ以外=Eloquent\Builder独自のもの

Eloquent\Builderのメソッド

  • Query\Builderをオーバーライドしているもの
    • where
    • find get
    • latest oldest
    • update delete
    • increment decrement
    • etc...

Eloquent\Builderのメソッド

  • Query\Builderをオーバーライドしているもの
    • where
    • find get
    • latest oldest
    • update delete
    • increment decrement
    • etc...

→グローバルスコープをQuery\Builderに適用したい

Eloquent\Builderのメソッド

  • Query\Builderをオーバーライドしているもの
    • where
    • find get
    • latest oldest
    • update delete
    • increment decrement
    • etc...

→戻り値をモデルのインスタンスやそのEloquentコレクションに変換したい

Eloquent\Builderのメソッド

  • Query\Builderをオーバーライドしているもの
    • where
    • find get
    • latest oldest
    • update delete
    • increment decrement
    • etc...

→updated_atの処理をしたい

Eloquent\Builderの独自のメソッド

  • Eloquent\Builderだけにあるもの
    • 名前にkeyとかupdated_atを含むもの
      • whereKey whereKeyNot addUpdatedAtColumn
    • 名前にscopeを含むもの
      • withGlobalScope withoutGlobalScope callNamedScope
    • 名前にeagerLoad relation withを含むもの
      • eagerLoadRelations getRelation with without
    • etc...

なぜ直接Query\Builderを使わないのか

  • Eloquent特有の機能のため
    • リレーション
    • グローバルスコープ
    • テーブルの情報
    • Eloquent\BuilderはQuery\Builderにこれらの機能を含めたサブクラス的な存在なのかも

ここまでにわかったこと

  • Eloquentの機能のうち、少なくない部分がEloquent\Builder, Query\Builderにある
    • Eloquentの外部のものであるQuery\Builderに、Eloquent特有の機能のための対応を加えた形のEloquent\Builder
  • Builder 2つを合わせたメソッド数は、Eloquent\Modelのメソッド数に匹敵
    • Eloquentの半分は、クエリビルダでできている?

ここまでのお話

  • Eloquent全体
    • SQLクエリを組み立てて実行する部分
    • SQLクエリを実行して取得した結果を使う部分?

取得したインスタンスの型

<?php

echo is_subclass_of(User::find(1), Illuminate\Database\Eloquent\Model::class);
// →true

データベースのカラム=Eloquentのアトリビュート

<?php

$user = User::where('name', 'tarou')->first();
echo $user->name; // 'tarou'
$user->name = 'jirou';
echo $user->name; // 'jirou'

Eloquent\Model::__get, ::__setの中身

<?php

namespace Illuminate\Database\Eloquent;

abstract class Model
{
    use Concerns\HasAttributes;

    public function __get($key)
    {
        return $this->getAttribute($key);
    }

    public function __set($key, $value)
    {
        $this->setAttribute($key, $value);
    }
}

Eloquent\Modelは多数のトレイトを使用している

<?php

namespace Illuminate\Database\Eloquent;

abstract class Model
{
    use Concerns\HasAttributes,    // 102 (メソッド数)
        Concerns\HasEvents,        //  26
        Concerns\HasGlobalScopes,  //   4
        Concerns\HasRelationships, //  45
        Concerns\HasTimestamps,    //  11
        Concerns\HidesAttributes,  //   8
        Concerns\GuardsAttributes, //  15
        ForwardsCalls;             // これはユーティリティなのでノーカン

    // ...
}

HasAttributesの調査に苦労した話

  • 苦労した理由
    • 使い方ベースではなく、コードリーディングで調べようとした

HasAttributesの調査に苦労した話

  • Eloquent\Model
    • __callStatic → __call
      • Eloquent\Builder
    • __get, __set
      • HasAttributes::getAttribute
      • HasAttributes::setAttribute

getAttributeの奥の方で行われていること

<?php

namespace Illuminate\Database\Eloquent\Concerns;

trait HasAttributes
{
    protected function transformModelValue($key, $value)
    {
        // アクセサ(ミューテタ)があるなら、それを
        if ($this->hasGetMutator($key)) {
            return $this->mutateAttribute($key, $value);
        } elseif ($this->hasAttributeGetMutator($key)) {
            return $this->mutateAttributeMarkedAttribute($key, $value);
        }

        // キャストがあるなら、それを
        if ($this->hasCast($key)) {
            return $this->castAttribute($key, $value);
        }

        // ...

        // なければ、素のアトリビュートの値を返す
        return $value;
    }
}

アトリビュートとは

  • アトリビュートは、データベースの行の各カラムの値と対応
  • アトリビュートは、取得・設定される際に、アクセサ・ミューテタ、キャストによって変更

アトリビュート、アクセサ・ミューテタ、キャスト関係の
メソッドを除いたもの

append encryptUsing getArrayableAppends getArrayableItems getArrayableRelations getChanges getDateFormat getDates getDirty getOriginal getOriginalWithoutRewindingModel getRawOriginal getRelationValue getRelationshipFromMethod handleLazyLoadingViolation hasAppended hasChanges isClassDeviable isClassSerializable isClean isDirty isRelation isStandardDateFormat only originalIsEquivalent relationsToArray serializeDate setAppends setDateFormat syncChanges syncOriginal transformModelValue wasChanged

アトリビュート、アクセサ・ミューテタ、キャスト関係の
メソッドを除いたもの

append encryptUsing getArrayableAppends getArrayableItems getArrayableRelations getChanges getDateFormat getDates getDirty getOriginal getOriginalWithoutRewindingModel getRawOriginal getRelationValue getRelationshipFromMethod handleLazyLoadingViolation hasAppended hasChanges isClassDeviable isClassSerializable isClean isDirty isRelation isStandardDateFormat only originalIsEquivalent relationsToArray serializeDate setAppends setDateFormat syncChanges syncOriginal transformModelValue wasChanged

Eloquentのアトリビュートのライフサイクル

  • 構成要素
    • 3つのプロパティ
      • $attributes
      • $original
        • 原本となるデータ
      • $changes
        • 前回変更した内容
    • いくつかのメソッド
      • syncOriginal
      • syncChanges

Eloquentのアトリビュートのライフサイクル

$attributes $original $changes
1. 未保存の行 あり なし なし
2. 未保存から保存した行 あり あり なし
3. 取得した行 あり あり なし
4. 2, 3の状態から保存した行 あり あり あり

HasAttributesまとめ

  • アトリビュートが中心
  • アトリビュートを取り囲むようにアクセサ・ミューテタ、キャスト
  • アトリビュートのライフサイクルとして、$original $changes

HasRelationships

  • Eloquent\Modelから使われているトレイト
  • HasAttributesに次ぐ規模
  • とはいえメソッド数は50以下
  • リレーション関係の機能を持つ

リレーションの基本的な使い方

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

hasOne, hasManyのようなメソッドがしていること

<?php

namespace Illuminate\Database\Eloquent\Concerns;

// Illuminate\Database\Eloquent\Relations\Relationを継承しているクラス
use Illuminate\Database\Eloquent\Relations\HasOne;

trait HasRelationships
{
    public function hasOne($related, $foreignKey = null, $localKey = null)
    {
        // ...

        return $this->newHasOne($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
    }

    protected function newHasOne(Builder $query, Model $parent, $foreignKey, $localKey)
    {
        return new HasOne($query, $parent, $foreignKey, $localKey);
    }
}

リレーションに関係するクラス

  • すべてEloquent\Relations\Relationを直接・間接に継承している
    • BelongsTo
    • BelongsToMany
    • HasMany
    • HasManyThrough
    • HasOne
    • HasOneThrough
    • MorphMany
    • MorphOne
    • MorphTo
    • MorphToMany

そもそもリレーションとは

そもそもリレーションとは: パターン1

親テーブルのキーの値を、関連するテーブルが持つ

  • users
    • id
    • ...
  • posts
    • id
    • user_id
    • ...

→hasOne, hasMany

そもそもリレーションとは: パターン2

親テーブルが、関連するテーブルのキーの値を持つ

  • posts
    • id
    • user_id
    • ...
  • users
    • id
    • ...

→belongsTo

そもそもリレーションとは: パターン3

中間テーブルが、親と関連するテーブルの両方のキーを持つ

  • users
    • id
    • ...
  • role_user
    • role_id
    • user_id
  • roles
    • id
    • ...

→belongsToMany

リレーションのパターンと対応するクラス

  • BelongsTo
  • BelongsToMany
  • HasMany
  • HasManyThrough
  • HasOne
  • HasOneThrough
  • MorphMany
  • MorphOne
  • MorphTo
  • MorphToMany

それ以外のクラス

  • BelongsTo
  • BelongsToMany
  • HasMany
  • HasManyThrough
  • HasOne
  • HasOneThrough
  • MorphMany
  • MorphOne
  • MorphTo
  • MorphToMany

HasOneThrough, HasManyThrough

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    public function postComments()
    {
        return $this->hasManyThrough(Comment::class, Post::class);
    }
}

Eloquent\Relations\Relationからの継承のツリー

  • Eloquent\Relations\Relation *
    • HasOneOrMany *
      • HasOne
      • HasMany
      • MorphOneOrMany *
        • MorphOne
        • MorphMany
    • BelongsTo
      • MorphTo
    • BelongsToMany
      • MorphToMany
    • HasManyThrough
      • HasOneThrough

HasOne, HasManyは大差ない

  • Eloquent\Relations\Relation *
    • HasOneOrMany *
      • HasOne
      • HasMany
      • MorphOneOrMany *
        • MorphOne
        • MorphMany
    • BelongsTo
      • MorphTo
    • BelongsToMany
      • MorphToMany
    • HasManyThrough
      • HasOneThrough

ポリモーフィック関連のクラスは、元となるクラスを少し拡張しただけ

  • Eloquent\Relations\Relation *
    • HasOneOrMany *
      • HasOne
      • HasMany
      • MorphOneOrMany *
        • MorphOne
        • MorphMany
    • BelongsTo
      • MorphTo
    • BelongsToMany
      • MorphToMany
    • HasManyThrough
      • HasOneThrough

HasOneOrManyが個別に存在するのに、HasOneOrManyThrough相当はない

  • Eloquent\Relations\Relation *
    • HasOneOrMany *
      • HasOne
      • HasMany
      • MorphOneOrMany *
        • MorphOne
        • MorphMany
    • BelongsTo
      • MorphTo
    • BelongsToMany
      • MorphToMany
    • HasManyThrough
      • HasOneThrough

Eloquent\Relations\RelationはEloquent\Builderと同等

  • Eloquent\Relations\Relation *
    • HasOneOrMany *
      • HasOne
      • HasMany
      • MorphOneOrMany *
        • MorphOne
        • MorphMany
    • BelongsTo
      • MorphTo
    • BelongsToMany
      • MorphToMany
    • HasManyThrough
      • HasOneThrough

Eloquentのリレーション

  • Eloquentの中でも、大きな範囲を占める
  • SQL/RDBMSのリレーションとは思想が違う(と思う)

Eloquent\Modelが使用するその他のトレイト

<?php

namespace Illuminate\Database\Eloquent;

abstract class Model
{
    use Concerns\HasAttributes,
        Concerns\HasEvents,
        Concerns\HasGlobalScopes,
        Concerns\HasRelationships,
        Concerns\HasTimestamps,
        Concerns\HidesAttributes,
        Concerns\GuardsAttributes,
        ForwardsCalls;

    // ...
}

HasEvents

<?php

namespace App\Models;

use App\Events\UserCreated;
use App\Events\UserRetrieved;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected $dispatchesEvents = [
        'created' => UserCreated::class,
        'retrieved' => UserRetrieved::class,
    ];
}

HasGlobalScopes

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected static function booted()
    {
        static::addGlobalScope('available', function (Builder $builder) {
            $builder->where('is_available', 1);
        });
    }
}

HasTimestamps

>>> $user = User::create([ 'name' => 'tarou', 'email' => 'tarou@example.com', 'password' => bcrypt('password')]);
=> App\Models\User {#4530
     name: "tarou",
     email: "tarou@example.com",
     #password: "$2y$10$dP4QYy/dpXW.iG4ZhuCuMeCm987b9xtXhMFpHceV2eBf8t/yQcTtu",
     updated_at: "2022-08-18 08:49:29",
     created_at: "2022-08-18 08:49:29",
     id: 1,
   }

HidesAttributes

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected $hidden = [
        'password',
        'remember_token',
    ];
}

GuardsAttributes

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    // ...
}

Eloquent\Model本体に実装されている機能

  • クラスを使うときに常に一度だけ動く、boot的な処理
  • 自身を含む関連オブジェクトを生成するファクトリ的なメソッド
  • データベース接続関係
  • テーブル・カラム情報関係
  • リレーション関係
  • シリアライズ関係
  • グローバルスコープ・ローカルスコープ関係
  • ページネーション関係
  • オブジェクトとしての有用性のための機能
  • インターフェイスを実装するために必要なメソッド

fill, save, update, delete

<?php

// 以下2行はほぼ同等
$user->fill(['name' => 'saburou'])->save();
$user->update(['name' => 'saburou']);

$user->delete();

createはどこ?

  • update, deleteはEloquent\Modelにある
  • createは、Eloquent\Builderにある

create, update, deleteの使い方

<?php

$user = User::create(...);
$user->update(...);
$user->delete();

モデルをクラスの場合とインスタンスの場合とで、
表現しているものが違う?

モデルクラスは、クラスとして使用するときテーブルを表現している

<?php

// usersテーブルから、id = 1の行を取得
User::find(1);

// usersテーブルから、email like '%@example.com'の行をすべて取得
User::where('email', 'like', '%@example.com')->get();

// another_db上のusersテーブルの行をすべて取得
User::on('another_db')->all();

モデルクラスは、クラスとして使用するときテーブルを表現している

<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    // usersテーブルのemailカラムにデフォルト値を設定
    protected $attributes = ['email' => 'dummy@example.com'];

    // シリアライズするときに表示しないカラムを指定
    protected $hidden = ['password', 'remember_token'];

    // 作成日時のタイムスタンプとして扱うカラムを指定
    const CREATED_AT = 'create_datetime';

    // postsテーブルへのリレーションを設定
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

インスタンスとして使用するときは、行を表現している

<?php

$user = User::find(1);
echo $user->name;

foreach ($user->posts as $post) {
    // ...
}

$user->update([...]);

モデルインスタンスからwhereが生えているコード

<?php

$user = User::find(1);

// User::where('id', 1)と同じように動く
$user->where('id', 1);

Eloquentの抽象構造

  • Eloquent\Modelを継承したクラス→モデル
    • データベースのテーブル、行を表現
    • クラスとしてはテーブルを、インスタンスとしては行を
  • データベースとオブジェクトのギャップの対処
  • データベースの機能を超えた部分
  • 外部連携
  • ユーティリティ的な機能

Eloquentの抽象構造

  • Eloquent\Modelを継承したクラス→モデル
  • データベースとオブジェクトのギャップの対処
    • カラムの型のマッピング。アクセサ・ミューテタ、キャスト
    • カラムの値のライフサイクルがデータベースとEloquentで違うことへの対処
  • データベースの機能を超えた部分
  • 外部連携
  • ユーティリティ的な機能

Eloquentの抽象構造

  • Eloquent\Modelを継承したクラス→モデル
  • データベースとオブジェクトのギャップの対処
  • データベースの機能を超えた部分
    • リレーションはオブジェクトのリンクの形に
    • グローバルスコープという、全体への制約
    • ローカルスコープは実装としては小さい機能だけど……
    • 行の直接操作。行を直接update, delete
  • 外部連携
  • ユーティリティ的な機能

Eloquentの抽象構造

  • Eloquent\Modelを継承したクラス→モデル
  • データベースとオブジェクトのギャップの対処
  • データベースの機能を超えた部分
  • 外部連携
    • シリアライズ
    • 入出力セキュリティ
    • ブロードキャスト
    • ページネーション
  • ユーティリティ的な機能

Eloquentの抽象構造

  • Eloquent\Modelを継承したクラス→モデル
  • データベースとオブジェクトのギャップの対処
  • データベースの機能を超えた部分
  • 外部連携
  • ユーティリティ的な機能
    • ブート処理
    • is isNotのようなオブジェクトの比較
    • replicateのようなオブジェクトの複製
    • 関連オブジェクトを生成するファクトリメソッド
    • インターフェイスを実装するためのメソッド

Eloquentの物理構造

  • Eloquent\Model
    • Eloquent\Builder
      • Query\Builder
    • HasAttributes
    • HasRelationships
      • Eloquent\Relations\Relation
    • その他トレイト

私が理解したEloquentの全貌

  • SQL/RDBMSの、テーブルと行をクラスとインスタンスで表現しつつ、強力なリレーション機能と、関連する細々とした機能を持つモジュール

自己紹介

  • HAYASHI Masayuki (Twitter: @hayashi_msyk)
  • 神戸在住
  • フリーランスのサーバサイドプログラマ
    • ♥PHP/Laravel
    • だったんだけど、最近はなぜかRubyの強い会社でフロントエンド(Vue/TypeScript)書いてます
    • 週24時間労働実践中
  • カンファレンス初登壇!

ご清聴ありがとうございました