前置き
本記事は, Laravel のグローバルヘルパー関数は 使い所を考えよう という趣旨の記事です.
リモートワークの朝
ワイ「ファ〜〜〜」
ワイ「今日も12時間寝てもうたわ」
ワイ「さて,業務開始や」
ワイ「今日も Laravel 書いていくで〜」
use Illuminate\Support\Collection;
// とりあえず心配なので作っとく
$collection = new Collection();
2歳息子「(PC の画面を覗き込む)」
ワイ「どうかしたん?」
息子「・・・」
リモートワークの昼
ワイ「よーし,昼休憩3時間取ったし,そろそろ業務再開や」
ワイ「さっきの続きやけど,まずは現在時刻を取得せなあかんな」
use Carbon\Carbon;
// 多分現在時刻が必要になる予感がする
$now = Carbon::now();
息子「(また PC の画面を覗き込む)」
ワイ「な,なんやねんさっきから」
ワイ「ワイの画面にゴミでも付いとるか?」
息子「うーむ」
息子くんは何か気になることがあるようです
息子「ねぇパパ」
息子「なんで グローバルヘルパー を使わないの?」
ワイ「へっ?」
ワイ「なんや,グローバルヘルパーって」
ワイ「グローバルなヘルパーさん」
ワイ「つまり, 英語ペラペラな介護員さん のことかいな?」
ワイ「まだ介護が必要なほど衰えてないわ!」
ワイ「最近肩も首もバキバキやけど!」
ワイ「あと,ワイは英語分からへんし...」
息子「何を言ってるの?」
息子「Laravel で使える グローバルヘルパー関数 の話なんだけど...」
ワイ「あぁ,そっちかいな」
ワイ「グローバルヘルパーな」
ワイ「知ってるで」
ワイ「collect()
とか config()
とか」
ワイ「Laravel フレームワークが予め作ってくれてはる便利な関数 やろ?」
息子「そうそう」
息子「さっきパパ,」
息子「new Collection()
とか Carbon::now()
とか」
息子「グローバルヘルパーで置き換えられそうなコード書いてたから」
息子「もしかしてパパ,グローバルヘルパーを知らないのかなと思って」
ワイ「たしかに」
ワイ「new Collection()
は collect()
に」
ワイ「Carbon::now()
は now()
に置き換えられて」
ワイ「コードがスッキリしそうやな」
ワイ「でもな息子くん」
ワイ「グローバルヘルパーは 使いすぎも禁物 なんやで」
ワイ「(って 後輩くんがこの前 Twitter 言ってた のをチラ見したんやけど)」
息子「そうなの?」
息子「なんで?」
ワイ「そ...それはやな...」
ワイ「し,知らんけどなんとなくや!」
ワイ「グローバルって言葉がなんとなくスケールデカそうで怖いからや!」
息子「えぇ〜」
息子「それじゃぁ納得いかないよ!」
ワイ「そ...そうよな...」
ワイ「ちょっと一緒に考えてみよか...」
親子で考えてみる
ワイ「じゃぁ,まずはグローバルヘルパーの良いところを出してみよか」
ワイ「息子くん,なんかある?」
息子「そうだね〜」
息子「すぐ思いつくのは, コードの記述量が少なくて済む ってことかな」
息子「さっきのパパのやつがいい例だね」
息子「Carbon::now()
って書くより now()
って書いたほうが短くて済むよね」
ワイ「たしかになぁ」
ワイ「ヘルパーって何回も使われそうなフレーズの ラッパー 的なポジションかもしれへんな」
息子「そうだね,まさに now()
とか collection()
なんかはラッパーになってて」
息子「実装はこんな感じになってるの」
function now($tz = null)
{
return Date::now($tz);
}
function collect($value = null)
{
return new Collection($value);
}
ワイ「ほえ〜」
ワイ「ホンマにラッパーやないか」
息子「そうだね」
息子「でも,ラッパーっぽくないやつもあるよ」
息子「例えば...」
function abort($code, $message = '', array $headers = [])
{
if ($code instanceof Response) {
throw new HttpResponseException($code);
} elseif ($code instanceof Responsable) {
throw new HttpResponseException($code->toResponse(request()));
}
app()->abort($code, $message, $headers);
}
息子「この, abort()
ってやつは」
息子「ステータスコードとメッセージを渡してやると」
息子「簡単に HttpException
を投げてくれるやつで」
息子「Contoller で使うと, Laravel の ExceptionHandler
がこの例外を捕まえてくれるから」
息子「簡単にエラーレスポンスを投げられるって感じのヘルパーだね」
ワイ「なるほどな」
ワイ「ヘルパーの中には,薄いラッパーばかりじゃなくて」
ワイ「こういう ちょっとしたロジックを持った ヘルパーもあるんやな」
息子「そうそう」
息子「色んなラッパーがあるけど」
息子「どのヘルパー関数も どこからでも簡単に呼べる し」
息子「クラスのメソッドではないから インスタンスを作る必要もない し」
息子「めちゃくちゃ使いやすいんだよ」
ワイ「確かに言われてみれば, new
しなくていいし,use
を書かんでも使えるもんな」
ワイ,謎の関数 app()
を発見する
ワイ「なぁ,息子くん」
ワイ「グローバルヘルパーの実装を眺めてたんやけどさ」
ワイ「この response()
ってやつ」
function response($content = '', $status = 200, array $headers = [])
{
$factory = app(ResponseFactory::class);
if (func_num_args() === 0) {
return $factory;
}
return $factory->make($content, $status, $headers);
}
息子「あぁ,これね」
息子「Contoller で,わざわざ return new Response()
って書かなくても」
息子「いい感じにレスポンスを返してくれるやつね」
息子「これがどうかしたの?」
ワイ「この, app()
ってのはなにをしてるん?」
息子「お,パパ,良い所に目をつけたね」
息子「まぁこれも実はグローバルヘルパーなんだけど」
息子「実態はこんな感じ」
function app($abstract = null, array $parameters = [])
{
if (is_null($abstract)) {
return Container::getInstance();
}
return Container::getInstance()->make($abstract, $parameters);
}
息子「Laravel の サービスコンテナ っていう目玉機能を使って」
息子「インスタンスを取得しているんだよ」
ワイ「インスタンスを取得...」
ワイ「new
するんじゃあかんの?」
息子「最終的にはやってることは new
と同じなんだけど」
息子「サービスコンテナを使う最大のメリットは, 依存性の注入を自動でやってくれること かな」
ワイ「出た...依存性の注入...」
ワイ「言葉が難しくてイマイチ分からんやつ...!」
息子「言ってしまえば, new
するときコンストラクタに渡す必要があるインスタンスを」
息子「再帰的に自動で作って代入までしてくれる って感じかな」
息子「もっと砕いて説明すると」
class A
{
public function __construct(
private B $b,
private C $c,
) {
}
}
class B
{
public function __construct(
private D $d
) {
}
}
class C
{
}
class D
{
}
息子「こんな感じで, new
するのに B のインスタンスと C のインスタンスが必要な A があったとして」
息子「B は new
するのに D のインスタンスが必要だとすると」
息子「A のインスタンスを作るときはどうしたら良いと思う?」
ワイ「えっと...」
ワイ「A を作るには B と C が必要で, B を作るのに D が必要だから...」
$d = new D();
$b = new B($d);
$c = new C();
$a = new A($b, $c);
ワイ「こうや!」
息子「正解!」
息子「でも,これって結構大変じゃない?」
ワイ「そうやな...」
ワイ「こんな簡単な例でも, A を作るのに 3 つもインスタンス作らなあかんしな」
ワイ「実際はもっと複雑なものも多そうやし...」
息子「そこでサービスコンテナの出番!」
息子「サービスコンテナを使うと,こんなふうに書けるの」
// 直接サービスコンテナを使うパターン
$a = Container::getInstance()->make(A::class);
// app() を使うパターン
$a = app(A::class);
ワイ「ファッ!?」
ワイ「これだけでいいん!?」
息子「そうなの」
息子「サービスコンテナは,作りたいインスタンスが依存しているもの...」
息子「つまり, new
するときに必要なインスタンスを自動的に作って入れてくれる の」
ワイ「はぇ〜」
ワイ「サービスコンテナはん,最強やないか!」
話は戻って
ワイ「それで,なんの話してたんやっけ」
息子「グローバルヘルパーの良い所と悪い所だね」
ワイ「そうやったな」
ワイ「思ったんやけど」
ワイ「グローバルヘルパーを使ってるクラスって」
ワイ「依存関係分かりづらくならん?」
息子「どういうこと?」
ワイ「だって,使う側のクラスでは, new
したり,コンストラクタから依存注入したりせずに」
ワイ「代わりにヘルパーの中でインスタンスを作るわけやろ?」
ワイ「パット見,何のクラスに依存したクラスなのか分かりづらくならへん?」
ワイ「コンストラクタからの依存注入やったら」
ワイ「そのクラスが何のクラスに依存しているかは」
ワイ「コンストラクタを見れば一発で分かるやん?」
息子「たしかにね」
息子「Laravel の Facade
にも同じことが言えるけど」
息子「確かに,サービスコンテナを使って依存解決してるパターンだと」
息子「依存関係は追いづらくなるね」
息子「依存関係が追いづらいと」
息子「コードを修正しようとしたときに,」
息子「見通しが悪くてどこのコードがどこに影響を与えているのか分からない」
息子「みたいなことは起こるかもね!」
ワイ「せやなぁ」
ワイ「つまり, 変更容易性 が失われてしまう可能性があるんやな」
息子「うんうん」
息子「あと,サービスコンテナで思ったんだけど」
息子「サービスサービスコンテナを使っているヘルパー」
息子「つまり,内部で app()
を呼び出してるヘルパーを使ったメソッドは」
息子「ユニットテストできない かも」
ワイ「へ,そうなん?」
息子「サービスサービスコンテナってね」
息子「Laravel の機能なんだけど」
息子「アプリケーションの立ち上げが必要 なんだよね」
息子「Laravel アプリケーション自体も1つのインスタンスになっていて」
息子「こいつを作らないことには, Laravel の便利な機能は使えないの.」
息子「それでね」
息子「ユニットテストは,Laravel アプリケーションを立ち上げることはせず」
息子「純粋に ,テスト対象のメソッドをただの PHP のスクリプトとして実行する から」
息子「サービスコンテナに依存したコードは動かないの」
ワイ「なるほどな」
ワイ「ユニットテストは Laravel フレームワークに依存しないテスト ってことか」
ワイ「その代わり, Laravel に依存したり,データベースを使ったりするテストは」
ワイ「機能テスト(Feature テスト) として書いてやればええんやな」
息子「そのとおりだね!」
息子「Feature テストは アプリケーションの立ち上げを最初にやってから実行するテスト だから」
息子「こっちはもちろんサービスコンテナやデータベースを使ったテストができるよ!」
ワイ「いちいちアプリケーションの立ち上げが必要な Feature テストは実行速度も遅いから」
ワイ「小さなロジックのテストは動作が軽いユニットテストでやりたいところやけど」
ワイ「それができなくなるんはちょっと辛いな」
息子「テストで思い出したんだけど」
息子「グローバルヘルパーはモックできないね」
ワイ「モック...?ってなんやっけ」
ワイ「聞いたことはあるけど」
息子「簡単に言うと,外部に依存している処理をダミーのものに差し替えることだね」
息子「ユニットテストなんかはさ,」
息子「テスト対象のメソッドに書かれた純粋なロジックの挙動のみを確かめたい から」
息子「それ以外の,外部のメソッドやクラスに依存する部分はモックに差し替えてあげて」
息子「テスト側からコントロールするのが望ましいんだよね」
息子「できるだけ副作用を起こしたくないし」
息子「逆に副作用を抱えたままだと」
息子「純粋なロジックのテストとは言えないからね」
ワイ「へ〜」
ワイ「ライブラリとか別のクラスに作ったメソッドを呼んでいる部分は」
ワイ「”こういう値を返します” と仮定した上でテストを実行するから」
ワイ「仮にテストが落ちたとき」
ワイ「テスト対象のメソッドの外に原因があるかもしれないという可能性を取っばらえるわけやな!」
ワイ「それで,そのモックは,どうやって差し替えるん?」
息子「基本的には,注入しているインスタンスを本物じゃなくてモックに差し替えるよ」
息子「例えばこんな風に」
// モックを使わない場合
$a = new A();
$b = new B($a);
// A をモックに差し替える場合
$mock = Mockery::mock(A::class);
$b = new B($mock);
息子「本来コンストラクタに渡すべきインスタンスを,モックインスタンスに差し替えるだけ」
ワイ「そうか, 依存注入してないヘルパー関数 は」
ワイ「こんな風に 差し替えが効かない から」
ワイ「モックできないんやな!」
息子「そのとおり!」
息子「今日のパパ,冴えてるね」
ワイ「へへへ」
これまでの議論をまとめると
ワイ「色々グローバルヘルパーの良い点悪い点出てきたけど」
ワイ「まとめてみると」
グローバルヘルパーの良い所
- コードの記述量が少なくて済む
- インスタンスを作ること無くどこからでも簡単に呼ぶことができる
グローバルヘルパーのイマイチな所
- 依存注入しないため,依存関係の見通しが悪くなる
- そのせいで,変更容易性が失われる可能性がある
- ユニットテストができない(ことがある)
- サービスコンテナを使うタイプのヘルパーは,アプリケーションの立ち上げが必要なため,ユニットテストができない
- モックができない
- 外から注入しているわけではないので,差し替えが効かない
ワイ「こんなところかいな」
息子「そうだね,いい感じだね!」
息子「こうしてみると,意外とイマイチな点が多いんだね」
ワイ「そうやなぁ」
ワイ「もちろん便利ではあるんやけど」
ワイ「プロダクトが大きくなってきたり」
ワイ「保守性のことをしっかり考えるようなフェーズになったら」
ワイ「ヘルパーは 技術的な負債になりうる かも知らんなぁ」
息子「そうだね,使い方はしっかり考えたほうがいいのかも.」
その日の夜
ワイ「それにしても」
ワイ「息子くん,Laravel のこと詳しすぎひんか?」
ワイ「フレームワークの内部の方までめちゃくちゃ知ってるやんけ」
ワイ「どうしたら 2 歳でそんな詳しくなるん?」
息子「んー」
息子「ねんねするときに,いつも絵本を読んでもらうじゃん?」
息子「パパと一緒にねんねするときは普通の絵本だけど」
息子「ママとねんねするときは」
息子「GitHub で Laravel の実装のソースコードリーディングしてるんだよね」
ワイ「ファーーーーーーーーーーー(失神)」