Laravel は PHP のフレームワークでその機能の中には RDB(Relational Database) を操作しやすくする機能もあります。とはいえ MySQL をはじめとした多数の RDB に対応しているためか、どうにも細かい部分で対応するメソッドのないコマンドもあります。生 SQL を書くのも手ですが、あまりしたくありません。そういった足りないメソッドを補完できると便利なのでその方法を紹介します。
この記事で登場する各ファイルのディレクトリ構造は次です。
app ├── Database │ ├── AppMySqlConnection.php │ └── Schema │ ├── AppBlueprint.php │ ├── AppMySqlBuilder.php │ └── Grammars │ └── AppMySqlGrammar.php └── Providers ├── AppServiceProvider.php └── DatabaseServiceProvider.php config └── app.php
Laravel のデータベース機能を拡張するにはデータベースの接続部から改造する必要があります。これは次の様に Provider の内部で行うのが良いです。
データベース接続クラス変更部のソースコード
/** app/Providers/DatabaseServiceProvider.php **/ <?php namespace App\Providers; use App\Database\AppMySqlConnection; use DateTime; use DB; use Illuminate\Database\Connection; class DatabaseServiceProvider extends AppServiceProvider { public function boot(): void { // .env 中の DB_CONNECTION が mysql の時のコネクションの解決を行うときのクロージャを登録 Connection::resolverFor('mysql', function (...$parameters) { // Laravel のMySQL接続クラスを継承した AppMySqlConnection クラスを使うと定義 return new AppMySqlConnection(...$parameters); }); } } /** config/app.php **/ 'providers' => [ /** * 省略 * Laravel 組み込みプロバイダ色々 * 省略 */ // ↓から自前のプロバイダ色々 App\Providers\DatabaseServiceProvider::class, // Laravel は初めてのデータベース接続時にデータベース接続クラスをシングルトンとして確定させます // このためデータベース接続定義のプロバイダクラスは他のプロバイダクラスより優先して登録すると事故が起きにくいです App\Providers\AppServiceProvider::class, /** * 省略 * 自作プロバイダ色々 * 省略 */ ],
データベース接続クラスではこの接続時に使う様々な機能をどのクラスで実現にするかも定義しています。ここでは外部から呼び出されるメソッド群を定義した SchemaBuider と文法を定義した Grammer を拡張し、それを使うと定義します。
データベース接続クラスのソースコード
<?php namespace App\Database; use App\Database\Schema\Grammars\AppMySqlGrammar; use App\Database\Schema\AppMySqlBuilder; use Illuminate\Database\MySqlConnection as BaseMySqlConnection; class AppMySqlConnection extends BaseMySqlConnection { /** * デフォルトのスキーマ文法インスタンスを取得します。 * * @return \Illuminate\Database\Grammar */ protected function getDefaultSchemaGrammar() { return $this->withTablePrefix(new AppMySqlGrammar()); } /** * 接続用のスキーマビルダーのインスタンスを取得します。 * * @return \Illuminate\Database\Schema\MySqlBuilder */ public function getSchemaBuilder() { if (is_null($this->schemaGrammar)) { $this->useDefaultSchemaGrammar(); } return new AppMySqlBuilder($this); } }
デフォルトの Laravel においてマイグレーション中にテーブルを扱うクラスは \Illuminate\Database\Schema\Blueprint です。これの拡張を拡張し、スキーマビルダークラスの中でこれが呼び出されるようにします。
スキーマビルダーのソースコード
<?php namespace App\Database\Schema; use Closure; use Illuminate\Database\Schema\MySqlBuilder as BaseMySqlBuilder; class AppMySqlBuilder extends BaseMySqlBuilder { // Blueprint を継承した拡張クラスである AppBlueprint を Blueprint の代わりに用意させます /** * クロージャーで新しいコマンドセットを作成します。 * * @param string $table * @param \Closure|null $callback * @return \Illuminate\Database\Schema\Blueprint */ protected function createBlueprint($table, Closure $callback = null) { $prefix = $this->connection->getConfig('prefix_indexes') ? $this->connection->getConfig('prefix') : ''; if (isset($this->resolver)) { return call_user_func($this->resolver, $table, $callback, $prefix); } return new AppBlueprint($table, $callback, $prefix); } /** * Blueprint インスタンスを生成するためのクロージャを定義します。 * * @param \Closure $resolver * @return void */ public function blueprintResolver(Closure $resolver) { $this->resolver = static function ($table, $callback) { return new AppBlueprint($table, $callback); }; } }
ここまでで色々拡張する準備が整いました。例として MySQL の全文検索用インデックスの一つである N-gram インデックスをマイグレーション中に記述できる様に拡張します。
MySQL :: MySQL 8.0 Reference Manual :: 12.10.8 ngram Full-Text Parser
便利さが目的なのでまずどんな呼び出しができると便利なのか考えて Blueprint から拡張します。index の生成メソッドは基本的にエイリアスで元クラスの indexCommand メソッドに繋がっています。また、削除メソッドも同様に dropIndexCommand メソッドに集約されます。特別オレオレにする理由もないため元々のコードの流れに乗っています。
拡張された Blueprint のソースコード
<?php namespace App\Database\Schema; use Illuminate\Database\Schema\Blueprint as BaseBluePrint; use Illuminate\Support\Fluent; class AppBlueprint extends BaseBluePrint { /** * fulltext index 生成の指定 * * @param string|array $columns * @param string|null $name * @param string|null $algorithm * @return Fluent */ public function fulltextIndex($columns, ?string $name = null, ?string $algorithm = null): Fluent { // $this-indexCommand メソッドに値を渡すと // Grammar クラス中の compile"${第一引数に渡した文字列のアッパーキャメルケース}"メソッドが呼ばれます // Grammar クラス中では compile の他にも type. modify といった接頭辞による振る舞い分けがされています。 return $this->indexCommand('fulltextIndex', $columns, $name, $algorithm); } /** * fulltext index 削除の指定 * * @param string|array $index * @return Fluent */ public function dropFulltextIndex($index): Fluent { return $this->dropIndexCommand('dropFulltextIndex', 'fulltextIndex', $index); } /** * fulltext index with parser ngram 生成の指定 * * @param string|array $columns * @param string|null $name * @return Fluent */ public function ngramIndex($columns, ?string $name = null): Fluent { return $this->indexCommand('fulltextIndex', $columns, $name, 'ngram'); } /** * fulltext index with parser ngram 削除の指定 * * @param string|array $index * @return Fluent */ public function dropNgramIndex($index): Fluent { return $this->dropIndexCommand('dropFulltextIndex', 'fulltextIndex', $index); } }
上記までのコードが用意できれば、次の様にマイグレーションファイル中で追加したメソッドを呼べます。
マイグレーションファイル例
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateMembersIndex extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('members', function (Blueprint $table) { // ['first_name', 'last_name', 'full_name'] の 3カラムに渡る ngram インデックスが作られることを期待する記述 $table->ngramIndex(['first_name', 'last_name', 'full_name'], 'name_index'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('members', function (Blueprint $table) { $table->dropNgramIndex('name_index'); }); } }
基本的な文法のエイリアスなどはここまでで十分できます。MySQL 独自機能の様なニッチな部分は時折、文法から欠けており文法を表現するクラス(生 SQL 構築クラスともいえます)である Grammar クラスの拡張も必要になります。
Grammer クラスを拡張する前に、まずどんな生 SQL が必要になるか確認します。ここでは次です。
ALTER TABLE tbl_name ADD FULLTEXT INDEX index_name (index_col_name,...) WITH PARSER ngram; # 例 ALTER TABLE articles ADD FULLTEXT INDEX ft_index (title,body) WITH PARSER ngram;
Laravel デフォルトの文法クラスの拡張である AppMySqlGrammar クラスでこれを生成できるようにします。AppMySqlGrammar クラスは先述したデータベース接続クラスの中で呼び出し方を定義されています。AppMySqlGrammar のコードは次です。
AppMySqlGrammar のソースコード
<?php namespace App\Database\Schema\Grammars; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Grammars\MySqlGrammar as BaseMySqlGrammar; use Illuminate\Support\Fluent; class AppMySqlGrammar extends BaseMySqlGrammar { // Blueprint::indexCommand メソッドに値を渡すと // Grammar クラス中の compile"${第一引数に渡した文字列のアッパーキャメルケース}"メソッドが呼ばれます // this->indexCommand('fulltextIndex', $columns, $name, 'ngram'); // としたならば compileFulltextIndex メソッドが呼ばれます /** * FULLTEXT インデックス作成コマンドの生成 * * @param Blueprint $blueprint * @param Fluent $command * @return string|null */ public function compileFulltextIndex(Blueprint $blueprint, Fluent $command): ?string { // Blueprint::indexCommand としたならば $command の中の index, columns, algorithm プロパティに // マイグレーションファイル中で渡した値が格納されています return sprintf( 'alter table %s add %s %s(%s)%s', $this->wrapTable($blueprint), 'fulltext index', $this->wrap($command->index), $this->columnize($command->columns), $command->algorithm ? ' with parser '.$command->algorithm : '', ); } /** * FULLTEXT インデックス削除コマンドの生成 * * @param Blueprint $blueprint * @param Fluent $command * @return string */ public function compileDropFulltextIndex(Blueprint $blueprint, Fluent $command): string { return $this->compileDropIndex($blueprint, $command); } }
かなり大がかりですが、これでマイグレーション時のメソッドを始めとして Laravel の中のデータベースの根元を拡張できます。ここでは拡張の仕方を紹介しましたがあまりにも拡張の必要がある時は無理に Laravel の機能を使わない方が楽です。拡張が多くなるにつれて Laravel のソースコードに知悉する必要もでてきてコードを読むのが大変になります。そうなってきた時は Laravel にこだわらず Doctrine を使う、いっそ生 SQL を書く(生 SQL が有効なのは外部からのアクセスを考慮しないでよい処理に限ります)とした方が適していそうです。