Edited at

【Laravel サービスコンテナ 中級編】メソッドインジェクションがしたい!


TL;DR



  • app()->call() のざっくりとした使い方解説です。

  • メソッドインジェクション!って言いたいだけ。なんか必殺技みたいでテンション上がるんだ。


何がしたい?

以前に「サービスコンテナは新しいnewである」という記事を書きました。

$sampleobject = new App\SampleClass;

↑ これと

$sampleobject = app()->make('App\SampleClass');

↑ これは、同じことだと。


コンストラクタインジェクション

class MyService {

}

class MyController {

// コンストラクタ
public function __construct( MyService $service )
{
$service; // 使える!
}

}

$controller = app()->make('MyController');

この解釈は僕がサービスコンテナの大事なところを理解するのにとても役に立ち、長いこと謎だった「コンストラクタインジェクション」を楽に扱えるようにしてくれました。 app()->make() は new と同じ。おまけとして、コンストラクタに書いたクラスのインスタンスを自動で作ってくれるんだって。話がややこしかったのは、そのおまけが強力すぎた、というだけの話で......。


メソッドインジェクション

class MyService {

}

class MyController {

// クラスメソッド
public function show( MyService $service )
{
$service; // 使える!
}

}

しかし、Laravelにはもう1つ、「メソッドインジェクション」 という機能があります。

あなたも使ってますよね?

たとえば以下の例。

コントローラのメソッドでRequestを受け取る、と。

use Illuminate\Http\Request;

class MyController {

public function show( Request $request )
{
$request->all(); // 使える!
}

}

他にも...

    public function store( Request $request, $id ) ...

public function store( StoreRequest $request ) ...

public function create( FormBuilder $formBuilder ) ...

どんな初心者向けのLaravel入門書にも何の説明もなく載っているアレからはじまり、「フォームビルダーを使ってみる」 にも書いたように何の説明もなく「こう書け」と書いてある、アレです。

最初はなんでこの $request が使えるのかわかりませんでした。

メソッドだから誰かが呼び出しているんだけど、だれがそのメソッドを呼び出しているのかと。いやもっと謎なのが、どうして引数を適当な順序で書いているのに、正しい順序で、正しく Request インスタンスを入れてくれる んだろう、と。

Laravelと付き合っているうちに、それはサービスコンテナさんが自動的にやってくれているということを知りました。

メソッドに書かれた引数の種類を調べ、その種類に応じたクラスのインスタンスを作って、順番を整えて、メソッドを呼び出してくれていたのです。ちょうどコンストラクタインジェクションでやっているように。

ありがとうサービスコンテナ。

君がやってくれたなんて知らなかったよ。

わかったよ! もう大丈夫!! 今度は僕がやってみるからね。

(僕が、コントローラのインスタンスを作って、僕がメソッドを呼んでみるよ!)

use Illuminate\Http\Request;

class MyController {

public function show( Request $request )
{
$request->all();
}

}

$controller = new MyController; // こうかな?
$controller->show( ); // Too few arguments to function MyController::show(), 0 passed

あぁ、ですよね。

use Illuminate\Http\Request;

class MyController {

public function show( Request $request )
{
$request->all(); // null
}

}

$controller = new MyController;
$request = new Request; // あぁ こうでしょ?!
$controller->show( $request ); // ......

おお?!

あたりまえだけど、メソッドに書かれた引数は、何かを入れないと動かないし、何を入れたら良いのかや、入れるものをどうやって初期化するかを全部自分でやらないときちんと動きません。

サービスコンテナさん!たすけて!

あなたは、どーやって、メソッドに、依存オブジェクトを入れてるんですか?

メソッドインジェクトしてもらいたいんじゃない。

メソッドインジェクト し・た・い

app()->make() のような秘密兵器があるんじゃないのか?

その武器、僕に貸してください。

そんな案件です。


結論

こうなります。

use Illuminate\Http\Request;

class MyController {

public function show( Request $request )
{
$request->all(); // 使える!
}

}

$controller = new MyController;
app()->call( [$controller,'show'] ); // メソッドインジェクション!!

クラスのインスタンスとメソッド名を、配列でペアにして app()->call() するだけです。

これこれ。この、クラスインスタンスをサービスコンテナに作ってもらうときの app()->make と同じような書き方でできるじゃないかなぁと思っていた、まさにそれです。


色々な使い方


クラスとメソッドを文字列で

インスタンス化していなくても呼び出せます。まるでstaticメソッド。

app()->call( 'MyController@show' ); 

おっと。ルート定義で Route::get(....,'MyController@show') とかしているのに似てますねー。


クロージャーでもいいよ

クラスメソッドじゃなくてもいいんですか?!

$func = function( Request $request ){

$request->all();
};
app()->call( $func );

これはなんだ!?「クロージャーインジェクション」か!! 強そうだ!


引数付

クラス型指定のない引数は、サービスコンテナは自動注入してくれません。

そこで、その場で変数を渡して、インジェクトしてもらうことができます。

まるで名前付き引数!

app()->call( 'MyController@show', ['id'=> $id] ); 

どういうケースかというと、例えば下記のようなメソッドが定義されていた場合(CRUDのRoute定義でよく見かけるリソースID付きのメソッドです)。


MyController.php

public function show( $id, Request $request )


app()->call( 'MyController@show' ); 

// Type error: Too few arguments
app()->call( 'MyController@show', ['id'=> $id] );
// OK!

引き渡す変数配列のキーが、インジェクトされるメソッドの引数名で、一致するところに入れてくれます。ちなみに、このキーを省略しても案外柔軟に入れてくれたりしますが、意図しない動作になることがあるのでキーはきちんと入れましょう(Laravel自体がこういった使い方を推奨していない、というウワサもあります)。

最初これが、コントローラのルート定義で '/user/{id}' とするときにやっていることだ!と思いましたが、ルート定義の場合は変数名が一致していなくてもかなり正確に注入してくれます。実はやっていることがちょっと違うんですが……ここでは割愛させていただきます。


使いどころ Use Case

公式ドキュメントには一切触れられていない機能なので、それだけ使いたいケースもなかなか無いわけですが (また「誰得記事」な予感) 、たとえば以下のようなケースで使ったりできます。


コントローラで別のメソッドを再利用したい

store (新規登録)はモデルを new した update (更新)のことだ!

と以下のようにする場合。

use Illuminate\Http\Request;

class MyController {

public function store( Request $request )
{
// $this->update( null, $request ); // これでも動くけど
// あえてこうする
app()->call( [$this,'update'], ['id'=>null ] );
}

public function update( $id, Request $request )
{
$model = $id ? Model::find($id) ? new Model();
//...
}
}

このケースだと、通常のupdateしたとき、引数は自動注入されるので、あとから数や順番が変わるかもしれないんですよね。同じクラスだったらほぼ確実に問題になりませんが。updateはメソッドインジェクトしながら使いたいというハードコアな要求に応えるコードです。


コントローラメソッドをテストしたい

たとえば先程の MyController@update をテストしたい場合。

      $controller = new MyController;

$request = new Request;
$request->merge( [
'date' => '2017-01-01',
'price' => 35000
] );

$result = $controller ->update(null, $request);

こうしないでください。Requestくらいならまだいいんですが、サービスクラス他、インジェクトするクラスが増えてくると大変です。ここでメソッドインジェクションを利用すると…


MyControllerTest.php

      $controller = new MyController;

$request = new Request;
$request->merge( [
'date' => '2017-01-01',
'price' => 35000
] );

app()->instance( Request::class, $request ); // バインドし直してこのインスタンスをDIしてもらう
$result = app()->call([$controller ,'update']);


ただ......そもそもコントローラをテストするな!という話かもしれません。

レイヤーを1段上げて、brower kit testing を使った「URLを叩くテスト」をするか、1段下げて、コントローラが使う機能をテストしたほうが良いかもです......。


幻の Route::controller 的なルーティングをしちゃう

Laravle5.3で削除された Route::controller が便利だったので復活していただきましょう。

削除されたんだからそれなりの理由があるんですが、ごめん。無視。


routes/api.php

Route::get('{type}', 'MyController@get');


class MyController {

public function get($type)
{
if (method_exists($this, $type)) {
return app()->call( [$this,$type] ); // メソッドインジェクション
}
return 'end point not found';
}

public function dosomething( MyService $service )
{
$service; // 使える!
}
}

こうすると、URLで、/dosomething と打ち込むと、MyController@dosomething が呼び出されます。RouteもControllerもスッキリしますね★(本番運用では使わないでくださいね)


派生クラスでオーバーライドするメソッドでインジェクトしてもらう

上手い説明が見つからずなんのこっちゃですが、要するにメソッドインジェクションの王道です。例えば モデルでバリデーションするトレイト を書いたときに rules() というメソッドでバリデーションルールを書けるようにしました。

trait ValidateOnSave

{
protected function rules(){ // このメソッド
return [
];
}

public function save(array $options = [])
{
$rules = $this->rules();
if( !empty($rules) ){
\Validator::validate( $this->attributes, $rules );
}

parent::save( $options );
}
}

このメソッドは、LaravelのFormRequestに倣ったものですが、本家のrulesはメソッドインジェクションが使えます。

class MyFormRequest extends FormRequest 

{
public function rules( MyService $service ) // メソッドインジェクション!
{
return [
// インジェクトされたサービスからルールを動的に生成!とか
'id' => 'in:' . implode( ',', $service->getList() )
];
}

これをするために、呼び出し元であるさきほどのトレイトをこのように直します。

trait ValidateOnSave

{
// 引数の数が違う!って警告が出るので元クラスでは定義しない
// protected function rules(){
// return [
// ];
// }

public function save(array $options = [])
{
$rules = app()->call( [$this, 'rules'] ); // メソッドインジェクション!
// ...

これがメソッドインジェクションしたい!の本当にしたかったこと、なのかもしれません。

よく考えたらコントローラのメソッドも、まさにこれですよ。

ちなみにこのような実装をするときの注意点は2点。


  1. rules などの呼び出すメソッドは定義しない。定義しておくと引数の数が違うと怒られる。(定義しなくても動くようにするには事前に method_exists などする)

  2. メソッドは必ず public 。一見同一クラス内から呼ばれているように見えますが、実際にメソッドを呼ぶのはサービスコンテナなので、外からアクセスできないとダメ。

です。

ぜひご活用くださいませ。


感想

「もう怖くない!サービスコンテナの使い方」という記事に書いたように、コンストラクタインジェクションを使いこなすのってそれほど難しくないんですよね。情報も盛りだくさんですし。でも、メソッドインジェクションを理解しようと思ったら、とたんに難しく、情報も少なくなってきます。というより「メソッドインジェクション」で調べると、メソッドインジェクションをしてもらう方法しか出てこないんですよね。しかも「メソッドインジェクションしたい」なんていう欲求はふつう出てこない。1回この壁にぶつかってみて、あれ?どうやるんだ!?とならないと。なったとして、調べ方も全然わからない。ググれない。

あれ?

この記事にもだれもたどり着けない?

また誰得記事ですか?

ホントは「サービスコンテナ初心者講座 第3回 メソッドインジェクションを使ってみる」的な記事を書こうと思っていたのですが、先に難しげな内容の記事を書いてしまいました。次回サービスコンテナ講座はこんな役立たず領域じゃなくて、「結合」あたりを攻める予定です。ご期待下さい!