1. Home
  2. PHP
  3. Laravel
  4. LaravelのEloquentORMの基本から具体的な使い方(モデルを使ったDB操作)

LaravelのEloquentORMの基本から具体的な使い方(モデルを使ったDB操作)

  • 公開日
  • 更新日
  • カテゴリ:Laravel
  • タグ:PHP,Laravel,EloquentORM,QueryBuilder
LaravelのEloquentORMの基本から具体的な使い方(モデルを使ったDB操作)

Laravel でデータベースと連携しデータを取得したり登録・更新・削除など一連の操作を行うにはどうしたらよいでしょうか?

Laravel には Eloquent ORM という、データ操作を簡単に行う為の機能が提供されています。

今回は EloquentORM の基本から具体的な使い方までを見ていきます。

Contents

  1. 開発環境
  2. Eloquent ORM
  3. モデル
  4. モデル・マイグレーション・シーダー生成
  5. テーブルと初期データ作成
  6. モデル実装
    1. テーブルの指定
    2. 主キーの指定
    3. タイムスタンプの指定
    4. データベースの指定
    5. デフォルト値の指定
  7. Eloquent でのデータ取得
    1. 全てのデータを取得する
    2. 条件を指定する
  8. レコード挿入
    1. save()
    2. create()
  9. データ更新
  10. OR 生成メソッド
    1. firstOrCreate
    2. firstOrNew
    3. updateOrCreate
  11. レコードの削除
    1. delete()
    2. destroy()
  12. クエリスコープ
    1. グローバルスコープ
    2. ローカルスコープ
  13. イベント
    1. オブザーバクラス

開発環境

今回の開発環境は以下の通りです。

  • MySQL 8.0
  • PHP 7.3
  • Laravel 5.8

Model クラスの設置個所はデフォルトでは laravel/app 配下ですが、本記事では laravel/app/Models 配下に設置しています。

Laravel のルートディレクトリを「laravel/」としています。

Eloquent ORM

Eloquent ORM(Eloquent Object Relational Mapping) とは、Laravel で提供されているデータ操作の為の機能です。データベースとモデルを関連付け、柔軟なデータ操作を行う為の Laravel 独自の機能になります。

とはいえ ORM(Object Relational Mapping)という言葉は昔から存在していて、オブジェクト指向のプログラムとデータベースを繋ぐ「オブジェクト関係マッピング」を意味します。つまり、Eloquent ORM とは、Laravel 独自の O/R マッパーという事になります。

モデル

Eloquent では、モデルクラスを定義した上で処理を実装していきます。ここで言うモデルとは、MVC の M 「Model 」に当たります。 モデルクラスに挙動や制約を定義し、データベース操作を行う際にそのモデルを使っていく、という流れになります。

ちなみに「モデル」の取りうる範囲はこれに限定されません。一般的な Model とはもっと広義の意味で定義されています。 Laravel で Eloquent を使用する場合に、MVC というデザインパターンの、モデルという範囲の一つにこのクラスがあると認識しておいてください。

モデル・マイグレーション・シーダー生成

モデルを生成します。ついでにマイグレーションとシーダも作成し、データも用意していきます。

Laravel ルートディレクトリへ移動し、以下の artisan コマンドを叩きます。

# Laravel ルートディレクトリへ移動
cd /path/to/laravel

# モデルとマイグレーションファイルを生成する
php artisan make:model Models/Framework -m

# 実行結果
[demo@localhost laravel]$ php artisan make:model Models/Framework -m
Model created successfully.
Created Migration: xxxx_create_frameworks_table

laravel/app/Models 配下にモデルファイル Framework.php が生成され、laravel/database/migrations 配下にマイグレーションファイル xxxx_create_frameworks_table.php が生成されます。

laravel
├─ app
│   ├─ Models
│   │    ├─ Framework.php

laravel
├─ database
│   ├─ migrations
│   │    ├─ xxxx_create_frameworks_table.php

次に初期データ投入用のシーダです。 Laravel ルートディレクトリへ移動し、以下の artisan コマンドを叩きます。

# Laravel ルートディレクトリへ移動
cd /path/to/laravel

# シーダファイルを生成する
php artisan make:seeder FrameworksTableSeeder

# 実行結果
[demo@localhost laravel]$ php artisan make:seeder FrameworksTableSeeder
Seeder created successfully.

database/seeds 配下に FrameworksTableSeeder.php が生成されます。

laravel
├─ database
│   ├─ seeds
│   │    ├─ FrameworksTableSeeder.php

テーブルと初期データ作成

モデルを実装する前にデータベースのテーブルとデータを作成しておきます。

マイグレーションを以下のように定義します。

laravel/database/migrations/xxxx_create_frameworks_table.php
<?php

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

class CreateFrameworksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('frameworks', function (Blueprint $table) {
            $table->increments('id');
            $table->string('category', 20);
            $table->string('name', 255);
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('frameworks');
    }
}

シーダーを以下のように定義します。

laravel/database/seeds/FrameworksTableSeeder.php
<?php

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;

class FrameworksTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        DB::table('frameworks')->insert([
            [ 'category' => 'PHP', 'name' => 'Laravel' ],
            [ 'category' => 'PHP', 'name' => 'Symfony' ],
            [ 'category' => 'PHP', 'name' => 'CakePHP' ],
            [ 'category' => 'PHP', 'name' => 'zend framework' ],
            [ 'category' => 'PHP', 'name' => 'codeigniter' ],
            [ 'category' => 'PHP', 'name' => 'Phalcon' ],
            [ 'category' => 'PHP', 'name' => 'FuelPHP' ],
            [ 'category' => 'PHP', 'name' => 'Slim' ],
            [ 'category' => 'PHP', 'name' => 'Yii' ],
            [ 'category' => 'PHP', 'name' => 'Silex' ],
            [ 'category' => 'PHP', 'name' => 'Flight' ],
            [ 'category' => 'PHP', 'name' => 'BEAR.Sunday' ],
            [ 'category' => 'PHP', 'name' => 'Ethna' ],
            [ 'category' => 'Javascript', 'name' => 'React' ],
            [ 'category' => 'Javascript', 'name' => 'AngularJS' ],
            [ 'category' => 'Javascript', 'name' => 'Vue.js' ],
            [ 'category' => 'Javascript', 'name' => 'Backbone.js' ],
            [ 'category' => 'Javascript', 'name' => 'jQuery' ],
            [ 'category' => 'Javascript', 'name' => 'Knockout.js' ],
            [ 'category' => 'Python', 'name' => 'Django' ],
            [ 'category' => 'Python', 'name' => 'Bottle' ],
            [ 'category' => 'Python', 'name' => 'Flask' ],
            [ 'category' => 'Python', 'name' => 'Tornado' ]
        ]);
    }
}

シーディング対象を追加します。

laravel/database/seeds/DatabaseSeeder.php
<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     *
     * @return void
     */
    public function run()
    {
        $this->call(FrameworksTableSeeder::class);
    }
}

マイグレーションとシーディングを実行し、データベースを構築します。

# Laravel ルートディレクトリへ移動
cd /path/to/laravel

# マイグレーション実行
php artisan migrate

# 実行結果
[demo@localhost laravel]$ php artisan migrate
Migrating: xxxx_create_frameworks_table
Migrated:  xxxx_create_frameworks_table

# シーディング実行
php artisan db:seed

# 実行結果
[demo@localhost laravel]$ php artisan db:seed
Seeding: FrameworksTableSeeder

尚、マイグレーションとシーディングは一度のコマンドで実行する事も可能です。その場合は、マイグレーションにオプションでシーディングをつけるようにします。

# マイグレーション&シーディング実行
php artisan migrate --seed

ちなみにこの時点で上手くシーディングが実行されない場合は、オートローダを再生成してから実行してみてください。

# autoloader 再生成
composer dump-autoload

これでデータべースと初期データの準備は完了です。

mysql> select * from frameworks;
+----+------------+----------------+
| id | category   | name  
+----+------------+----------------+
|  1 | PHP        | Laravel
|  2 | PHP        | Symfony
|  3 | PHP        | CakePHP
|  4 | PHP        | zend framework
|  5 | PHP        | codeigniter
|  6 | PHP        | Phalcon
|  7 | PHP        | FuelPHP
|  8 | PHP        | Slim
|  9 | PHP        | Yii
| 10 | PHP        | Silex
| 11 | PHP        | Flight
| 12 | PHP        | BEAR.Sunday
| 13 | PHP        | Ethna
| 14 | Javascript | React
| 15 | Javascript | AngularJS
| 16 | Javascript | Vue.js
| 17 | Javascript | Backbone.js
| 18 | Javascript | jQuery
| 19 | Javascript | Knockout.js
| 20 | Python     | Django
| 21 | Python     | Bottle
| 22 | Python     | Flask
| 23 | Python     | Tornado
+----+------------+----------------+

モデル実装

準備が整ったので、モデルの実装を行っていきます。まずは初期のソースを確認します。

laravel/app/Models/Framework.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    //
}

クラス宣言のみのミニマムの状態です。ここから Eloquent ORM を使う為のモデルを定義していきます。

テーブルの指定

このモデルに紐づけるテーブル名を設定します。

基本的には、Laravel がクラス名に紐づくテーブル名を自動的に引いてきてくれるので、クラス名とテーブル名が同じ名前の場合は指定しなくても OK です。例えば、今回使用している Framework クラスに対して、紐づけるテーブルは frameworks テーブルなので、この場合は設定なくても動作します。

ちなみに、テーブル名がスネークケースの場合は、パスカルケースでのクラス名で一致します。

  • テーブル名「php_frameworks」=クラス名「PhpFramework」

モデルクラス名が単数形なのに対して、テーブル名は複数形になる関係性です。この関係性が成立する場合に、Laravel ではモデルから自動でテーブルを解決してくれます。

もし、上記ルールに沿わない場合や、作成したテーブルとクラスの名前が全く違う場合は、以下のようにしてメンバ変数 $table にテーブル名をセットします。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    // テーブル名の指定
    protected $table = 'web_frameworks';
}

主キーの指定

Eloquent でのモデル実装時は、暗黙で「主キー=カラム名 id 」として処理を行います。

もし主キーのカラム名が「id 」以外の場合は、以下のようにメンバ変数 $primaryKey に主キーのカラム名をセットします。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    // 主キーの指定
    protected $primaryKey = 'framework_id';
}

ちなみに Eloquent では、暗黙で主キー=自動増分(AUTO_INCREMENT)であるとして処理されます。併せて、デフォルトでは主キーは数値型であるという想定で処理が行われます。

上記ルールに沿わない場合は、以下で設定を変更できます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    // 自動増分ではない場合
    public $incrementing = false;

    // 主キーが数値型ではない場合
    protected $keyType = 'string';
}

タイムスタンプの指定

Eloquent では、暗黙で created_at(作成日時) カラムと updated_at(更新日時) カラムがある前提でそれらを自動的に更新しようとします。

自動更新を OFF にしたい場合は、メンバ変数 $timestamps に false を指定します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    // created_atとupdated_atの自動更新をOFFにする
    public $timestamps = false;
}

タイムスタンプはデフォルトで timestamp 型として日時を挿入しますが、フォーマットを変更する事も可能です。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    // 日付カラムのフォーマットを指定する
    protected $dateFormat = 'U';
}

上記の場合は、UNIX 時間で挿入されます。

ちなみに、そもそも作成日時と更新日時のカラムが created_at と updated_at ではない場合もあります。そんな場合は以下のように定数を設定する事でカラムを指定できます。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    // 作成日時カラムの指定
    const CREATED_AT = 'create_dt';

    // 更新日時カラムの指定
    const UPDATED_AT = 'update_dt';
}

データベースの指定

Eloquent はデフォルトでデータベースの設定ファイルから、default プロパティの値を見てそれを用いて接続を行いに行きます。

laravel/config/database.php
'default' => env('DB_CONNECTION', 'mysql'),
.env
DB_CONNECTION=mysql

接続先を変更したい場合は、メンバ変数 $connection にコネクション名を設定します。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    // データベースコネクション先の指定
    protected $connection = 'redis';
}

デフォルト値の指定

インサート時のデフォルト値を指定したい場合は $attributes を設定します。

laravel/app/Models/Framework.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    protected $attributes = [
        'category' => 'PHP',
    ];
}

上記の場合は、インサート時に category フィールドへの値がなければ「PHP 」という値が入ります。

Eloquent でのデータ取得

モデルの設定が完了したら、実際にデータを取得してみます。

全てのデータを取得する

まずは、全てのデータを取得します。条件を一切つけずに全てのデータを取得するには、all() メソッドを使います。

// 全てのデータを取得する
$data = Framework::all();

結果データはコレクションオブジェクトで返却され、ループで取り出す事が出来ます。

$data = Framework::all();

$item = [];
foreach ($data as $d) {
    $item = [
        'id' => $d->id,
        'name' => $d->name,
        'category' => $d->category,
    ];

    // print_r($item);
    // => Array
    // (
    //     [id] => 1
    //     [name] => Laravel
    //     [category] => PHP
    // )
}

これは条件を指定した場合も同様です。

条件を指定する

条件を指定する場合は、クエリビルダと同様の記述で指定が可能です。

例えば、PHP のフレームワークのみを取得する場合は、以下のように記述します。

$data = Framework::where('category', 'PHP')->get();

第一引数には条件付けを行うフィールド、第二引数には条件となる値を渡します。この場合は、暗黙的に=「イコール」として処理されます。

条件がイコールではない場合は、第二引数に演算子を渡し、第三引数に条件となる値を渡します。

// id が 10 未満のデータを取得する
$data = Framework::where('id', '<',  10)->get();

ちなみに分割処理や集計に関してもクエリビルダと同様に実装できます。 (条件のバリエーションについては、Laravel のクエリビルダ記法まとめ にまとめてあります。)

また、Eloquent 独自のメソッドとして、主キーを指定してデータを取得する場合は、find() メソッドが使えます。

// id = 1 のレコードを取得する
$data = Framework::find(1);

レコード挿入

データを新規に登録する、新しいレコードを挿入する場合は、2つの方法があります。

save()

Eloquent でテーブルに新しいレコードを挿入する場合は、save() メソッドが使えます。

public function index()
{
    // 1 件のレコードを挿入する
    $fw = new Framework();
    $fw->name = "D3";
    $fw->category = "Javascript";
    $fw->save();
}

実行後、MySQL を確認するとデータが挿入されている事が確認できます。

mysql> SELECT * FROM `frameworks` ORDER BY id DESC LIMIT 1;
+----+------------+------+---------------------+---------------------+
| id | category   | name | created_at          | updated_at          |
+----+------------+------+---------------------+---------------------+
| 24 | Javascript | D3   | 2019-08-11 02:35:31 | 2019-08-11 02:35:31 |
+----+------------+------+---------------------+---------------------+
1 row in set (0.00 sec)

作成日時と更新日時も自動で更新されています。

create()

登録できるカラムに制約を設けた形でレコード挿入(1件のみ)も行えます。その場合には create() メソッドを使います。

クエリを書く前に、モデルへレコード挿入を許可するカラムを指定します。

laravel/app/Models/Framework.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    // 登録を許可するフィールド
    protected $fillable = ['name', 'category'];
}

ここで許可しないカラムへはデータを挿入できません。

逆に、許可しない設定もできます。

// 登録を許可しないカラム
protected $guarded = ['admin'];

ここで禁止しないカラムはデータ挿入を許可する事になります。

全てを許可する場合は、以下のようにも記述できます。

protected $guarded = [];

許可する guarded は、どちらか一方のみを設定できます。両方は設定できません。

これらの設定を行ったら、create() メソッドでレコード挿入を行えます。

Framework::create([
    'name' => 'Ice Framework',
    'category' => 'PHP'
]);

データ更新

データを更新する場合ですが、これも Eloquent なら同じく save() メソッドが使えます。

// id=24 のレコードを取得
$target = Framework::find(24);
// name カラムを変更
$target->name = "D3.js";
// 更新
$target->save();

MySQL を確認します。

mysql> SELECT * FROM `frameworks` WHERE `id` = 24;
+----+------------+-------+---------------------+---------------------+
| id | category   | name  | created_at          | updated_at          |
+----+------------+-------+---------------------+---------------------+
| 24 | Javascript | D3.js | 2019-08-11 06:31:07 | 2019-08-11 06:32:08 |
+----+------------+-------+---------------------+---------------------+
1 row in set (0.00 sec)

データが更新されました。

また、複数のレコードを同時に更新する事も出来ます。例えば category カラム値が「Javascript 」のレコードを一括で「JS 」へ変更します。

// category が 'Javascript' のレコードを 'JS' へ更新する
Framework::where('category', 'Javascript')
         ->update(['category' => 'JS']);

実行後データを確認すると、更新が成功している事が確認できます。

mysql> SELECT * FROM `frameworks` WHERE `category` = 'JS';
+----+----------+-------------+---------------------+---------------------+
| id | category | name        | created_at          | updated_at          |
+----+----------+-------------+---------------------+---------------------+
| 14 | JS       | React       | NULL                | 2019-08-11 06:42:02 |
| 15 | JS       | AngularJS   | NULL                | 2019-08-11 06:42:02 |
| 16 | JS       | Vue.js      | NULL                | 2019-08-11 06:42:02 |
| 17 | JS       | Backbone.js | NULL                | 2019-08-11 06:42:02 |
| 18 | JS       | jQuery      | NULL                | 2019-08-11 06:42:02 |
| 19 | JS       | Knockout.js | NULL                | 2019-08-11 06:42:02 |
| 24 | JS       | D3.js       | 2019-08-11 06:31:07 | 2019-08-11 06:42:02 |
+----+----------+-------------+---------------------+---------------------+
7 rows in set (0.00 sec)

OR 生成メソッド

Eloquent ではただの登録や更新だけではなく、対象の値を持ってデータベースを見に行き、その時の状態によって登録や更新、インスタンスを行う事が出来ます。

firstOrCreate

firstOrCreate() メソッドは、送信したカラム/値を持つレコードが存在するかをまず探します。 そして該当するレコードが無かった場合は、レコード1件が挿入されます。 逆に、該当するレコードが存在する場合はインサートは行われず、そのレコードが返されます。

$data = Framework::firstOrCreate(
            ['name' => 'Laravel'],
            ['category' => 'PHP']
        );

このメソッドの注意点として、上記のように複数のカラム/値ペアをセットする場合ですが、レコードに値が存在するかどうかを評価するのは先頭のカラム/値ペアのみになります。 従って、先頭のカラム/値ペアがレコードに存在していなければ、他のカラム/値ペアを合わせてインサートが行われます。

もう少し噛み砕くと、上記の場合は以下の条件で動作する事になります。

  • 「name 」に「Laravel 」という値を持つレコードが無ければ、その時点でインサートが走ります。
  • 「name 」に「Laravel 」という値を持つレコードが存在する場合は、「category 」に未知の値がある場合でもインサートは行われず、「name=Laravel 」に該当するレコード(の1件目)が返却されます。

firstOrNew

firstOrNew() メソッドは、送信したカラム/値を持つレコードが存在するかをまず探し、存在すればそのモデルインスタンスを返しますが、存在しなかった場合は、送信した値で新しいモデルインスタンスが生成され、返却されます。つまり、レコードは挿入されません。

$data = Framework::firstOrNew(
            ['name' => 'Symfony'],
            ['category' => 'PHP']
        );

戻り値として受け取った新しいモデルインスタンスを保存する場合は、そのモデルインスタンスに対して save() メソッドを発行します。

$data = Framework::firstOrNew(
            ['name' => 'Kohana'],
            ['category' => 'PHP']
        );

// 保存
$data->save();

ちなみにこの時、レコードが無かった場合は Insert となり、レコードが存在していた場合は Update となります。

updateOrCreate

updateOrCreate() メソッドは、渡したカラム/値ペアに合致するレコードがあるかを探し、あれば更新を行い、なければ新しいレコードが挿入されます。

Framework::updateOrCreate(
    ['name' => 'React', 'category' => 'JS'],
    ['category' => 'Javascript']
);

このメソッドのポイントは、第一引数に渡した配列内のカラム/値ペアでレコードの存在を確認し、存在していれば第二引数の配列内のカラム/値ペアで更新を行います。

レコードが存在していない場合は、これらの値を以てレコード挿入が行われます。

レコードの削除

レコード(モデル)を削除する場合は、いくつかの方法が存在します。

delete()

通常のモデル削除であれば、delete() メソッドがあります。

// id=29 のレコードを取得
$model = Framework::find(29);

// 削除
$model->delete();

Eloquent ではモデルベースで操作が行われるので、delete() メソッドを用いる場合は、削除するレコードを指定し削除を行うのではなく、削除するレコードのモデルインスタンスを一旦取得した後に delete() メソッドを叩くという流れになっている点がポイントです。

と言いつつも、メソッドチェーンで一撃削除も出来ます。

// 条件式で複数取得+ delete メソッドをチェインして削除
Framework::where('category', 'JS')->delete();

destroy()

主キーが判明している場合は、destroy() メソッドでダイレクトにレコードを削除する事が出来ます。

// 1件の削除
Framework::destroy(27);

// 複数件まとめて削除 - 1(配列で渡す)
Framework::destroy([23, 25, 26]);

// 複数件まとめて削除 - 2(1つずつ渡す)
Framework::destroy(20, 21, 22);

上記例では3パターンで記述していますが、一番上は一件のみの削除、あとの2つは複数件のレコードをまとめて削除するパターンの書式です。

あくまでも、主キーを渡す事で削除が行えるというのがこのメソッドのポイントです。

クエリスコープ

Eloquent ではクエリスコープという観念があり、全てのモデルに対して、予め何らかの条件を付与しておく事が出来ます。

例えば Laravel で論理削除(ソフトデリート) の仕組みをテーブルに組みこんだ場合、自動的に論理削除されたレコードは select されなくなりますが、これと同じ事です。

例えばあるコミュニティサイトがあったとして、友達を検索する際に年齢を選べたとして、クエリスコープを仕込んでおくと、例えば 20 才未満は自動的に検索結果に表示されない(DB への問い合わせから除外される)などといった仕組みを構築できます。

尚、クエリスコープには「グローバルスコープ」と「ローカルスコープ」があります。

グローバルスコープ

グローバルスコープは、前述した通り全体にがっつりクエリスコープを仕込みます。ではスコープを作成します。

laravel/app 配下に Scopes ディレクトリを作成し、CategoryScope.php を作成します。

<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class CategoryScope
{
    /**
     * @param  \Illuminate\Database\Eloquent\Builder  $builder
     * @param  \Illuminate\Database\Eloquent\Model  $model
     * @return void
     */
    public function apply(Builder $builder, Model $model)
    {
        // category は PHP のみ
        $builder->where('category', 'PHP');
    }
}

グローバルスコープを定義します。

laravel/app/Scopes/CategoryScope.php
<?php

namespace App\Scopes;

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class CategoryScope implements Scope
{
    /**
     * @param Builder $builder
     * @param Model $model
     */
    public function apply(Builder $builder, Model $model)
    {
        // category は PHP のみ
        $builder->where('category', 'PHP');
    }
}

category カラムが PHP であるもののみを取得するように定義しました。

次に、対象のモデルクラスへスコープを登録します。

laravel/app/Models/Framework.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Scopes\CategoryScope; // 追加

class Framework extends Model
{
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new CategoryScope);
    }
}

これで設定は完了しました。コントローラから取得してみると、条件指定を行わなくても、category が PHP のものしか取得されなくなります。

$data = Framework::all();
// =>  SELECT * FROM `frameworks` WHERE `category` = 'PHP';

一時的にグローバルスコープを解除する

ある状況で一時的にあるグローバルスコープを外したい場合があるかもしれません。その場合は withoutGlobalScope() メソッドを使います。

// グローバルスコープをスキップ
$data = Framework::withoutGlobalScope(\App\Scopes\CategoryScope::class)->get();

複数、もしくは全てのグルーバルスコープを解除したい場合は以下のように定義します。

// 複数のグローバルスコープをスキップ
$data = Framework::withoutGlobalScopes([
    \App\Scopes\OneScope::class, \App\Scopes\TwoScope::class
])->get();

// 全てのグローバルスコープをスキップ
$data = Framework::withoutGlobalScopes()->get();

ローカルスコープ

ローカルスコープは、予め定義しておいたある条件を任意のタイミングでスポット的に使用できるものです。つまり、前もって条件を定義しておき、必要な時にそれらを当てて適用する事ができます。

ローカルスコープも、モデルに定義します。

laravel/app/Models/Framework.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Framework extends Model
{
    /**
     * categoryが「PHP」を含むものとするクエリスコープ
     * @param $query
     * @return mixed
     */
    public function scopeCategoryInPhp($query)
    {
        return $query->orWhereIn('category', ['PHP']);
    }

    /**
     * categoryが「Python」を含むものとするクエリスコープ
     * @param $query
     * @return mixed
     */
    public function scopeCategoryInPython($query)
    {
        return $query->orWhereIn('category', ['Python']);
    }
}

カテゴリが PHP のものとするスコープと、カテゴリが Python のものとするスコープを記述しました。

これで定義は完了です。使用してみます。

// CategoryInPhp スコープを適用する
$data = Framework::CategoryInPhp()->get();

// CategoryInPhp スコープと CategoryInPython スコープの両方を適用する
$data = Framework::CategoryInPhp()->CategoryInPython()->get();

これでスコープに定義した条件が適用されます。ちなみにスコープをチェインして複数適用させる事もできます。

スコープに引数を渡したい場合は、以下のように定義します。

laravel/app/Models/Framework.php
/**
 * 任意のカテゴリを含むものとするクエリスコープ
 * @param $query
 * @param array $types
 * @return mixed
 */
public function scopeCategoryInSomething($query, array $types)
{
    return $query->orWhereIn('category', $types);
}

あとはスコープメソッドに引数を渡せば OK です。

// 取得したいカテゴリを配列で渡す
$data = Framework::CategoryInSomething(['PHP', 'Javascript'])->get();

イベント

Eloquent では、モデルから行った操作について、細かくイベントが発行できるようになっています。

  • creating
    • 新しいレコードをインサートした時
  • created
    • 新しいレコードのインサートが完了した時
  • updating
    • 既存のレコードを更新した時
  • updated
    • 既存のレコード更新が完了した時
  • saving
    • 登録・更新を行った時
  • saved
    • 登録・更新が完了した時
  • deleting
    • レコードを削除した時
  • deleted
    • レコード削除が完了した時
  • restoring
    • 論理削除(ソフトデリート)を行ったレコードを復帰させる時
  • restored
    • 論理削除からの復帰が完了した時
  • retrieved
    • データを取得した時

saving/saved に関しては、creating/created と updating/updated の共通版です。

イベントを設定するにはイベント&リスナーの仕組みが必要ですが、以下を参考にしてください。
Laravel イベント&リスナ

ではイベントを設定していきます。まずはモデルにイベントをマップします。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Events\AccessDetection; // イベントクラスをuse

class Framework extends Model
{
    protected $dispatchesEvents = [
        // データ取得時に発火
        'retrieved' => AccessDetection::class,
        //'saved' => Slack::class,
    ];
}

データを参照した時に発行される retrieved イベント に対して設定しました。また、saved イベントもコメントアウトされていますが、複数設定する場合の例として記述しています。

設定はこれだけです。必要な挙動はもちろんイベントリスナーで実装されていますから、あとはモデルからデータが参照された時点でイベントが発行され処理が行われます。

オブザーバクラス

イベントを登録する時に、例えばデータを保存した時の saved イベントには Slack通知・メール配信・ログファイル書き込み・別のテーブルへのログ書き込み…など、登録するイベントがたくさんある場合があります。しかもそれが別のイベントの時も…となると結構な数です。

そんな場合は、オブザーバクラスを作成する事で、イベントの登録をまとめる事が出来ます。

laravel/app 配下に Observers ディレクトリを作成し、そこへ FrameworksObserver.php を作成します。

laravel
├─ app
│   ├─ Observers
│   │   └─ FrameworkObserver.php

オブザーバクラスを定義します。

laravel/app/Observers/FrameworksObserver.php
<?php

namespace App\Observers;

use App\Models\Framework;
use Illuminate\Support\Facades\Log;
use App\Events\AccessDetection;

class FrameworkObserver
{
    /**
     * モデル取得イベントのリッスン
     *
     * @param  \App\Models\Framework  $Framework
     * @return void
     */
    public function retrieved(Framework $Framework)
    {
        // ログの書き込み
        Log::debug($Framework->name);
        // イベント発火
        event(new AccessDetection(str_random(100)));
    }

    /**
     * 作成イベントのリッスン
     *
     * @param  \App\Models\Framework  $Framework
     * @return void
     */
    public function created(Framework $Framework)
    {
        //
    }

    /**
     * 作成イベントのリッスン
     *
     * @param  \App\Models\Framework  $Framework
     * @return void
     */
    public function updated(Framework $Framework)
    {
        //
    }

    /**
     * 削除イベントのリッスン
     *
     * @param  \App\Models\Framework  $Framework
     * @return void
     */
    public function deleting(Framework $Framework)
    {
        //
    }
}

上記のようにして、各イベントメソッドに対して処理を定義していく事でそのイベントに対する処理を行う事が出来ます。ログの書き込みなど、必ずイベント&リスナでなくてはいけないという事はなく、自由に実装できます。

まとめ

Eloquent ORM はモデルという観念を用いた有用なデータ操作方法の1つですが、Laravel ではデータ操作の方法はこれだけではありません。アプリケーションの要件や自身の使いやすさなどから最適な方法を使っていくと良いと思います。そういった意味でも、Laravel ではデフォルトで Model ディレクトリというのを用意していません。

Eloquent ORM は、Laravel で簡単にデータ操作を行うには便利なので是非試してみてください。

サンプルコード

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級