公開日: 2020-05-30
更新日: 2020-05-30

Laravelのグローバルスコープを使って、MySQLのテーブルに保存されたデータのフォーマットを変換する方法

Laravel グローバルスコープの使い方

Laravelのマイグレーションを使ってテーブルを作成、データを登録、モデルを作成する。

Laravelのマイグレーションを使って、以下のテーブルを作成します。

<?php

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

class CreateBusinessHoursTable extends Migration
{
  /**
    * Run the migrations.
    *
    * @return  void
    */
  public function up()
  {
    Schema::create('business_hours', function (Blueprint $table) {
      $table->smallIncrements('id');
      $table->morphs('parent_model');
      $table->unsignedTinyInteger('day_of_week');
      $table->time('open');
      $table->time('lastcall')
            ->nullable();
      $table->time('close');
      $table->timestamps();
    });
  }

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

SQLを使って、作成したテーブルに以下のデータを登録します。

INSERT INTO `business_hours` VALUES (1,'App\\User',1,0,'10:30:00','18:00:00','18:30:00','2020-05-24 14:35:55','2020-05-24 14:35:55'),
(2,'App\\User',1,1,'10:30:00','18:00:00','18:30:00','2020-05-24 14:35:55','2020-05-24 14:35:55'),
(3,'App\\User',1,2,'10:30:00','18:00:00','18:30:00','2020-05-24 14:35:55','2020-05-24 14:35:55'),
(4,'App\\User',1,3,'10:30:00','18:00:00','18:30:00','2020-05-24 14:35:55','2020-05-24 14:35:55'),
(5,'App\\User',1,4,'10:30:00','18:00:00','18:30:00','2020-05-24 14:35:55','2020-05-24 14:35:55'),
(6,'App\\User',1,5,'10:30:00','18:00:00','18:30:00','2020-05-24 14:35:55','2020-05-24 14:35:55'),
(7,'App\\User',1,6,'10:30:00','18:00:00','18:30:00','2020-05-24 14:35:55','2020-05-24 14:35:55');
            

そして、このテーブルを操作するBusinessHourモデルを作成します。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;

class BusinessHour extends Model
{
  const TABLE            = 'business_hours';
  const ID               = 'id';
  const PARENT_MODEL     = 'parent_model';
  const DAY_OF_WEEK      = 'day_of_week';
  const OPEN             = 'open';
  const LAST_ORDER       = 'last_order';
  const CLOSE            = 'close';

  const KEY              = self::ID;
  protected $primaryKey  = self::KEY;
  const DELETED_AT       = 'deleted_at';

  protected $table       = self::TABLE;
  protected $dates       = [self::DELETED_AT];
  protected $perPage     = 100;
  protected $fillable    = [
    BusinessHour::PARENT_MODEL,
    BusinessHour::DAY_OF_WEEK,
    BusinessHour::OPEN,
    BusinessHour::LAST_ORDER,
    BusinessHour::CLOSE,
  ];
}
            

このモデルは、飲食店やサービス業の営業時間を保存するためのテーブルです。
マイグレーションで使用した$table->morphs('parent_model');は、ポリモーフィックリレーションを保存するためのカラムの宣言です、
ポリモーフィックリレーションとは、他のテーブルのカラムを親として持つことができるカラムです。
例えば、レストランの情報を保存するrestaurantsテーブルやスーパーマーケットの情報を保存するsupermarketsテーブルのカラムを親に持つことができます。
この宣言を使うと、parent_modelとparent_model_idというカラムを自動的に作成してくれます。
今回の例では、parent_modelにApp\Userを、parent_model_idに1を指定しているので、usersテーブルの1カラム目が親になっています。

それでは、マイグレーションで作成したテーブルにデータが登録されていることを確認しましょう。
データの存在を確認するには、tinkerを使うのがラクです。

 php artisan tinker
 Psy Shell v0.10.4 (PHP 7.2.29 — cli) by Justin Hileman
 >>> App\BusinessHour::get();
 => Illuminate\Database\Eloquent\Collection {#4258
      all: [
        App\BusinessHour {#4238
          id: 1,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 0,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4241
          id: 2,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 1,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4224
          id: 3,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 2,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4297
          id: 4,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 3,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4298
          id: 5,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 4,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4299
          id: 6,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 5,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4300
          id: 7,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 6,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
      ],
    }
            

グローバルスコープを追加して、tinkerで確認する。

MySQLのtime型は時間・分・秒を保存する宣言ですが、実際のサービスの表示では、秒までを使うことはあまりないでしょう。 レストランの開店時間に秒を表示するのはやりすぎです。
常に指定したフォーマットでデータを取得したい場合、Laravelでは、グローバルスコープを使うのが便利です。 それでは、BusinessHourモデルに以下のコードを追加してみましょう。


/**
  * モデルの「初期起動」メソッド
  *
  * @return  void
  */
protected static function booted()
{
  static::addGlobalScope('time_format', function (Builder $builder) {

    $builder->addSelect(
      '*',
      DB::raw('TIME_FORMAT('.BusinessHour::OPEN.      ', "%H:%i")'.' as '.BusinessHour::OPEN),
      DB::raw('TIME_FORMAT('.BusinessHour::LAST_ORDER.', "%H:%i")'.' as '.BusinessHour::LAST_ORDER),
      DB::raw('TIME_FORMAT('.BusinessHour::CLOSE.     ', "%H:%i")'.' as '.BusinessHour::CLOSE),
    );
  
  });
}
            

この例では、グローバルスコープをtime_formatという名前で登録しました。
Builderのメソッドにselect()ではなく、addSelect()を使いました。
select()は1つのクエリで一回だけしか呼び出せません。複数回、呼び出すと、一番最後の呼び出しのみが有効となります。
なので、グローバルスコープでselect()を呼び出すと、他の箇所では必ずaddSelect()で呼び出す必要があります。
そのように設計していれば問題ないのですが、ここでaddSelect()を使えば、あとでselect()が呼び出されても問題ありません。もちろんaddSeelct()が呼び出されても問題ありません。
また、addSelect()の引数に'*'を指定して、ポリモーフィックリレーションが壊れないようにしています。
そして、DB::raw()を呼び出して、MySQLのメソッドを指定して、時間のフォーマットを書き換えます。
'as'を使って、元のカラムと同じ名前を指定することで、既存のカラムを上書きしていますが、複数のフォーマットを使いたいのであれば、そのフォーマットに適した名前をつけるのがいいと思います。

それでは、再びtinkerを使って、グローバルスコープが適用されていることを確認しましょう。

 php artisan tinker
 Psy Shell v0.10.4 (PHP 7.2.29 — cli) by Justin Hileman
 >>> BusinessHour::get();
 => Illuminate\Database\Eloquent\Collection {#4279
      all: [
        App\BusinessHour {#4280
          id: 1,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 0,
          open: "10:30",
          last_order: "18:00",
          close: "18:30",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4281
          id: 2,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 1,
          open: "10:30",
          last_order: "18:00",
          close: "18:30",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4282
          id: 3,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 2,
          open: "10:30",
          last_order: "18:00",
          close: "18:30",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4283
          id: 4,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 3,
          open: "10:30",
          last_order: "18:00",
          close: "18:30",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4284
          id: 5,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 4,
          open: "10:30",
          last_order: "18:00",
          close: "18:30",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4285
          id: 6,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 5,
          open: "10:30",
          last_order: "18:00",
          close: "18:30",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4286
          id: 7,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 6,
          open: "10:30",
          last_order: "18:00",
          close: "18:30",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
      ],
    }
            

グローバルスコープをキャンセルする方法をtinkerで確認する。

時間のフォーマットから秒が表示されなくなっていますね。
グローバルスコープの適用をキャンセルしてデータを取得したい場合は、グローバルスコープの名前を引数にwithoutGlobalScope()メソッドをチェーンメソッドでつなげて呼び出します。

 php artisan tinker
 Psy Shell v0.10.4 (PHP 7.2.29 — cli) by Justin Hileman
 >>> App\BusinessHour::withoutGlobalScope('time_format')->get();
 => Illuminate\Database\Eloquent\Collection {#4258
      all: [
        App\BusinessHour {#4238
          id: 1,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 0,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4241
          id: 2,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 1,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4224
          id: 3,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 2,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4297
          id: 4,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 3,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4298
          id: 5,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 4,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4299
          id: 6,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 5,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
        App\BusinessHour {#4300
          id: 7,
          parent_model_type: "App\User",
          parent_model_id: 1,
          day_of_week: 6,
          open: "10:30:00",
          last_order: "18:00:00",
          close: "18:30:00",
          created_at: "2020-05-24 22:35:55",
          updated_at: "2020-05-24 22:35:55",
        },
      ],
    }
            

また秒が表示されていますね。
グローバルスコープは応用範囲が広く、さまざまな使い方ができます。例えば、生年月日から年齢を計算したり、貨幣の区切りを変更したり、数値で持っている性別を文字列に変更したり、本当にさまざまな使いみちがあります。
次の例は、時間のフォーマット変更に加えて、date_formatという名前で日付のフォーマットを変更するグローバルスコープを追加しています。

/**
  * モデルの「初期起動」メソッド
  *
  * @return  void
  */
protected static function booted()
{
  static::addGlobalScope('time_format', function (Builder $builder) {
    $builder->addSelect(
      '*',
      DB::raw('TIME_FORMAT('.BusinessHour::OPEN.      ', "%H:%i")'.' as '.BusinessHour::OPEN),
      DB::raw('TIME_FORMAT('.BusinessHour::LAST_ORDER.', "%H:%i")'.' as '.BusinessHour::LAST_ORDER),
      DB::raw('TIME_FORMAT('.BusinessHour::CLOSE.     ', "%H:%i")'.' as '.BusinessHour::CLOSE),
    );
  });

  static::addGlobalScope('date_format', function (Builder $builder) {
    $builder->addSelect(
      DB::raw('DATE_FORMAT('.BusinessHour::CREATED_AT.', "%Y/%m/%d")'.' as '.BusinessHour::CREATED_AT),
      DB::raw('DATE_FORMAT('.BusinessHour::UPDATED_AT.', "%Y/%m/%d")'.' as '.BusinessHour::UPDATED_AT)
    );
  });
}
            

アプリケーションサーバに比べて、データベースサーバは多重化しにくいので、このようなフォーマット変換をデータベース側でやらせることには異論があるかもしれません。
そこまでの負荷ではない場合、私はできるだけの処理をデータベースサーバ側でやらせるようにしています。

今回は、Laravelでグローバルスコープを使って、フォーマット変換を行う方法を説明しました。
合わせて、tinkerとポリモーフィックリレーションにも少しだけ触れました。
Laravelって知れば知るほど、かゆいところに手が届くすばらしいフレームワークですね。