枠が空いてたので2日連続で書いちゃいます。
Symfony Advent Calendar 2017 7日目の記事です。
Symfony3.3
以降のSymfonyプロジェクトでは、コントローラクラスを書く時にFrameworkBundleの Controller
を継承する必要がなくなりました。
<?php
namespace App\Controller;
use App\Entity\Task;
use Doctrine\ORM\EntityManagerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;
class TaskController
{
// ...
}
https://github.com/77web/sf3.4-functional-test-sample/blob/master/src/Controller/TaskController.php
Controller
クラスを継承しなくて良くなったことを歓迎する声をよく聞きます。
bin/console make:controllerってやったらcontrollerクラス継承されてて辛い・・・ #symfony_ja
— polidog(@polidog) 2017年12月5日
私も、コントローラー内にコンテナを持たず、依存先サービスをinterface名やクラス名ではっきり指定することで、コントローラクラスの見通しが良くなったと思います。まだどこかのコントローラで使っているサービスをうっかり削除という事故も減りますね。
しかし!Controllerを継承しないということは、以前のコントローラに標準装備されていた便利メソッドが使えなくなってしまいます。
generateUrl()
とか redirectToRoute()
とか createNotFoundException()
がないとかなり辛いです…。
何か解決策は無いかとsymfony/symfonyのソースコードをさまよった挙句、昔のControllerにあった便利メソッド群は Symfony3.3
以降では ControllerTrait
というクラスにまとめられていることを発見しました。
<?php
namespace Symfony\Bundle\FrameworkBundle\Controller;
// ...
trait ControllerTrait
{
// ...
/**
* Generates a URL from the given parameters.
*
* @see UrlGeneratorInterface
*
* @final since version 3.4
*/
protected function generateUrl(string $route, array $parameters = array(), int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): string
{
return $this->container->get('router')->generate($route, $parameters, $referenceType);
}
// ...
}
この ControllerTrait
を使えば、Controllerクラスを継承しなくても便利メソッドが使える!
…というのはぬか喜びでした。なんと、ControllerTraitのメソッドは $this->container
の存在が前提になっているのです。
かと言って、ControllerTraitを使うために$containerを注入してしまうと、せっかくFrameworkBundleのControllerを継承しないメリットがほとんどなくなってしまいます。
そこで!
私が さいきょうのControllerTrait を考えたので発表したいと思います!
Controllerを継承しないコントローラでも、このtraitを使えば、従来の便利メソッドを利用し続けることができます。 どうです?最強だと思いませんか?
どうやって実現したのか
Symfony3.3から Autowiring
という仕組みが取り入れられ、デフォルトでコントローラもautowiringの恩恵を受けるサービスになっています。(もちろん、不都合な場合は除外することもできます)
https://symfony.com/doc/current/service_container/3.3-di-changes.html
コントローラに対するAutowiringの発動条件は
- コンストラクタインジェクションが定義されている
- setterインジェクションが定義されていて、かつ
@required
アノテーションがついている
のいずれかを満たす場合です。
さきほどの さいきょうのControllerTrait をよく見ていただくと、 @required
つきsetterインジェクションをつけてあるのが見つかると思います。
Autowiringで狙ったオブジェクトを差し込むコツはいくつかあるので、 ドキュメント をよく読んで使ってみてください。
ControllerTraitについて補足
一度はFrameworkBundleにもコンテナに依存しない版ControllerTraitが入ったのですが、現在はコンテナを注入したものに再度変更されています。
最強のControllerTraitを実際に使ってみるとわかる のですが、すべての便利メソッドを使うための必要サービスをautowiringで注入するためには、デフォルトではインストールされないバンドル・コンポーネント( SecurityBundle
や Serializer
)を入れる必要があったり、デフォルトではOFFになっている機能(セッションやCSRFチェック)を有効にする必要があります。
これでは Symfony Flex
のいいところ「最低限のコンポーネントでスタートして必要なものだけ入れる」が生きないということで、コンテナ利用版に戻されたのではないかと思います。(autowiringでの注入は対象サービスが存在しないとエラーになってしまいますが、コンテナからの取得なら、事前に $container->has($serviceId)
でチェックして、有効になっていないサービスは使わせないということができるからです。
ControllerTraitの便利メソッド群も、よく見るとジャンルが別のもの(HTTPエラー関係、セキュリティ関係、URL関係、DB、フォーム)が一つのtrait内に同居している状態です。 個人的には、今後、この便利メソッド群はジャンル別のtraitに分けられて、開発者は必要な時に必要なtraitを使う形に進化していくのではないかと期待しています。
まとめ
長々と書いてきましたが、何が言いたいかというと「Autowiring楽しいよ!」という一言です symfony1時代やSymfony2時代に「Symfonyは設定ファイルが多すぎて…」と敬遠してしまった開発者にも、ぜひ一度試してみてほしいです。
明日は smd8122 さんです!