枠が空いてたので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 クラスを継承しなくて良くなったことを歓迎する声をよく聞きます。

私も、コントローラー内にコンテナを持たず、依存先サービスを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);
    }

    // ...
}

https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php

この 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で注入するためには、デフォルトではインストールされないバンドル・コンポーネント( SecurityBundleSerializer )を入れる必要があったり、デフォルトではOFFになっている機能(セッションやCSRFチェック)を有効にする必要があります。

これでは Symfony Flex のいいところ「最低限のコンポーネントでスタートして必要なものだけ入れる」が生きないということで、コンテナ利用版に戻されたのではないかと思います。(autowiringでの注入は対象サービスが存在しないとエラーになってしまいますが、コンテナからの取得なら、事前に $container->has($serviceId) でチェックして、有効になっていないサービスは使わせないということができるからです。

ControllerTraitの便利メソッド群も、よく見るとジャンルが別のもの(HTTPエラー関係、セキュリティ関係、URL関係、DB、フォーム)が一つのtrait内に同居している状態です。 個人的には、今後、この便利メソッド群はジャンル別のtraitに分けられて、開発者は必要な時に必要なtraitを使う形に進化していくのではないかと期待しています。

まとめ

長々と書いてきましたが、何が言いたいかというと「Autowiring楽しいよ!」という一言です :smile: symfony1時代やSymfony2時代に「Symfonyは設定ファイルが多すぎて…」と敬遠してしまった開発者にも、ぜひ一度試してみてほしいです。

明日は smd8122 さんです!