「ググって解決しづらかったこと」というアドベントカレンダーの記事になります。
はじめに
ググって解決しづらいこと、僕がよくぶつかるのは「局所解のようなもの」でした。
この「局所解のようなもの」は、多くの場合で「うちの会社/プロジェクトだけで必要な機能」と言い換えることができると思います。
つまり、ググってもそもそもやってる人が居ないという種の「ググって解決しづらいこと」です。
この場合、いくらググるスキルを高めても、無いものは見つけられません。
どんな凄腕の漁師でも砂漠じゃ魚は取れませんよね。
この記事でその「局所解」を解説したいわけではなく、「局所解が必要な場面への立ち向かい方」を紹介できればと思いました。
とはいえ、実例があったほうが説明しやすいので、実例をもとに説明します。
自分の場合の「局所解」と、立ち向かい方
自分の場合は 「Laravel-admin の管理画面上でのみ、モデルのグローバルスコープを外したい」という目的でした。
LaravelやLaravel-admin に馴染みがない方に説明しておくと、
-
グローバルスコープ:Laravelのモデルの機能。モデルに、「毎回勝手に実行してくれるクエリ」を設定することができる。
- 使い方としては、「駅のDBがあるが、ユーザー向けには常に閉鎖済みの駅を隠したい」となったときに活躍。
駅モデルに対してこのグローバルスコープを設定することで毎回閉鎖済みの駅を除外するクエリを書かなくて済みます。
- 使い方としては、「駅のDBがあるが、ユーザー向けには常に閉鎖済みの駅を隠したい」となったときに活躍。
-
Laravel-admin:管理画面をコードだけで作成できるパッケージ
この課題におけるググりづらさというのは「特定のパッケージに依存している」という点ですね。
「Laravel-admin グローバルスコープ」とか、「Laravel-admin withoutGlobalScope」とかググっても僕は見つかりませんでした。
なにせ中国の方が作成しているパッケージで、ドキュメントも中国語と英語のみ。
「管理画面」という要件において他の選択肢が多くあるため、ユーザーはLaravel本体から比べると大きく減少します。
こんなときは、ソースコードを当たるのが有効な場面があります。
STEP1:アタリをつける
とはいえ、いきなりソースコードに聞いてみてもよくわかりません。
まずは公式ドキュメントを探して、アタリをつけましょう。
今回の場合は、2つのドキュメントを読んでみます。
もちろん、Laravel-adminのドキュメントにはグローバルスコープを外す方法は書いてありません。
ただ、わかることもありました。わかったことは
- グローバルスコープはモデルクラスにある
withoutGlobalScope
というメソッドで一時無効化できる。
特定のクエリのグローバルスコープを削除する場合は、withoutGlobalScopeメソッドを使用できます。
- Laravel-admin は、モデルのインスタンスを生成して使用している。(下記コード)
use App\Models\Movie;
use Encore\Admin\Grid;
use Encore\Admin\Facades\Admin;
$grid = new Grid(new Movie); // ←Movieモデルのインスタンスを生成して使用している!
という2点です。※Eloquentモデルとかそういった厳密性はこの記事では無視します。
ここで、**「モデルを生成するときにグローバルスコープを無効化する処理入れれば何となくイケそう」**というアタリをつけることができました!
次は実際にコードに変更を加えてみます。
STEP2:やってみる
今度は手を動かしてみます。さっきのソースコードをもとに、変更を加えてみましょう。
use App\Models\Movie;
use Encore\Admin\Grid;
use Encore\Admin\Facades\Admin;
- $grid = new Grid(new Movie);
+ $grid = new Grid((new Movie)->withoutGlobalScope("visible"));
さて、これで実行してみると…?
エラーが出ます
STEP3:現状を理解する
そうはいっても諦めません。エラーメッセージを読んで、何が起きているかを理解していきましょう。
Argument 1 passed to Encore\Admin\Grid::__construct() must be an instance of Illuminate\Database\Eloquent\Model, instance of Illuminate\Database\Eloquent\Builder given, called in /var/www/html/src/app/Admin/Controllers/ClinicController.php on line 48 {"userId":1,"exception":"[object] (TypeError(code: 0): Argument 1 passed to Encore\\Admin\\Grid::__construct() must be an instance of Illuminate\\Database\\Eloquent\\Model, instance of Illuminate\\Database\\Eloquent\\Builder given, called in /var/www/html/src/app/Admin/Controllers/ClinicController.php on line 48 at /var/www/html/src/vendor/encore/laravel-admin/src/Grid.php:175)
ちなみにこういったケースでは大体エラーメッセージをググっても出てこないことが多いので、ググって出てこなくても泣かないようにしましょう。
このエラーメッセージもほとんど情報出てきません!
書いてあることをざっくり和訳するとこんな感じです。
Gridを初期化するときにはModelのインスタンスが必要やのにBuilderのインスタンスが渡されとるで!
どうやら、メソッドから思っているものが返っていないようです。
仕方がないので手元でテストしてみましょう。
Laravelには、コンソールで手軽に動作確認ができる tinker というツールがあります。
今回はこれを使いましょう。(Rails では rails c
が近いコマンドらしいです)
$ php artisan tinker
>>> new Movie;
=> App\Models\Movie # MovieはModelクラスを継承してるので、Movieインスタンス は Modelインスタンスとしても振る舞える
>>> (new Movie)->withoutGlobalScope("visible");
=> Illuminate\Database\Eloquent\Builder # Builderクラスが返ってきている!
実際にBuilderクラスに変わってしまっているようです。
現状を理解したら、いよいよソースコードを探っていきます
STEP4:ソースコードを探る
まず、先ほどのエラーで書いてあることはこんな感じでした
Gridを初期化するときにはModelのインスタンスが必要やのにBuilderのインスタンスが渡されとるで!
逆に言えば、BuilderではなくModelを渡せば動くということです。
ただ、元のインスタンスはBuilderクラスではなくModelクラスだったはずです。
つまり、Builderクラスから元のModelクラスを取り出すことができれば、上手くいきそうです!
ここまできたら、ソースコードでBuilderクラスを読んでみましょう。
先ほどのBuilderクラスは Illuminate\Database\Eloquent\Builder
クラスをGihubで読みにいきます。
framework/Builder.php at 6.x · laravel/framework
まず変数を見ていきましょう。変数は基本的にメソッドよりも先に宣言され、処理も挟まらないので見通しが良いです。
めぼしいものがあるとアタリです。
そしてなんと、 $model
変数を見つけました!
/**
* The model being queried.
*
* @var \Illuminate\Database\Eloquent\Model
*/
protected $model;
中身は \Illuminate\Database\Eloquent\Model
と書いてあります。こいつを取得できれば完璧ですね!
とはいえ、protected
なので直接取ってくることはできません(万が一出来たとしてもカプセル化を破壊するのでやめましょう)
なので、この $model
変数を返してくれるメソッドがないか探すと…
ありました!
/**
* Get the model instance being queried.
*
* @return \Illuminate\Database\Eloquent\Model|static
*/
public function getModel()
{
return $this->model;
}
@return \Illuminate\Database\Eloquent\Model
の記述を見るとおり、Modelクラスの変数を返してくれることで間違いなさそうです。
これを使えば、Builderクラスのインスタンスから元々のModelクラスのインスタンスを取り出せそうです!
STEP5:再挑戦
そして改めて挑戦!
getModel() を付けて、型を揃えにいきます。
use App\Models\Movie;
use Encore\Admin\Grid;
use Encore\Admin\Facades\Admin;
- $grid = new Grid((new Movie)->withoutGlobalScope("visible"));
+ $grid = new Grid((new Movie)->withoutGlobalScope("visible")->getModel());
そして今度は、エラーもなく期待通りの挙動が実現できました!
解決法の1つとして「ソースコード」を
実例を踏まえて色々書きましたが、実際のところ「ググって分かりづらいこと」に対する、全部の場面に使える万能薬や銀の弾丸というのは無い印象です。
ただ、「ググって出てくる」ということは、きっと誰かが上記のような分析をやっているはずだと私は思っています。
(もちろん公式ドキュメントに書いてあることはそんなことないかもしれませんが)
「英語でググる」「英語の質問と回答を読む」なども非常に効果的ではありますが、ソースコードに聞くことで、「ピンポイントの解決策を自力で導くことができる」というケースがあることを今回はお伝えできればと思い記事を書きました。
何より、フレームワークやパッケージのソースコードを読む中で自分の力が伸びていく実感があるのも楽しいです。
なので、改めて解決の流れとして、
- アタリをつける
- やってみる
- ダメでも諦めずに原因を確認する
- ソースコードを読みにいく
- 1~4をうまくいくまで繰り返す
これを試してもらえればなと思います。
実際にソースコードを読むような経験があまりない方は、この記事を参考に「前できなかった実装」「納得いかなかった実装」について振り返ってドキュメントを読んでみてはいかがでしょうか。
そして、今後壁にぶつかったときも、選択肢の1つとして「ソースコードを読む」を覚えておいていただければと思います。