読者です 読者をやめる 読者になる 読者になる

Symfony Best Practice 訳してみた - Chapter5 -

Symfony Best Practiceの翻訳をしてます。 五日目です。

前回の記事

hanahirodev.hatenablog.com

※多分に意訳が入っておりますので、誤訳がある場合はご指摘ください。

※読み進めていくSymfony Best Practiceは 2016/10/23時点の情報です。

※写経をする場合は、事前にPHPの実行ができる環境をご用意ください。

3行まとめ

  • "thin controller and fat models"・5-10-20ルール

  • 単純なEntityへの問い合わせはParamConverterを使うべし。

  • EventDispatcherによりコントローラー実行前後に処理を差し込める。


第5章 Controllers

Syfonyは"thin controller and fat models"(コントローラーは小さく、Modelは大きく)という哲学に依拠しています。 これは、コントローラーは、アプリケーションの異なる部分同士を結びつけるのに必要な糊コードだけを持つ小さなレイヤーであるべきということ意味します。 経験則からいって、5-10-20の法則に従うべきです。5-10-20の法則とは、コントローラーに定義する変数は5個以内に、アクションは10個以内、各アクションは20行以内におさめるというものです。科学的根拠はありませんが、コントローラーからサービスへ処理を移すといったリファクタリングをするべきタイミングに気づく指標にはなります。

  • コントローラーはフレームワークバンドルを拡張して作りましょう。可能な限り、ルーティング、キャッシュ、セキュリティの設定はアノテーションで記述しましょう。

コントローラーを基底のフレームワークに対応させることで、フレームワークを最大限活用でき、開発の効率も上がります。 さらに、コントローラーを小さく、糊コードだけが記述されているように保たれているので、何時間もかけてそれらを切り離す作業は、長い目で見ても無駄です。 加えて、ルーティング、キャッシュ、セキュリティの設定にアノテーションを使うことで、設定を単純化できます。異なるフォーマット(YAML,XML,PHPなど)の何十ものファイルを見る必要はありません。すべての設定を1つのフォーマットで管理できます。 開発者はビジネスロジックフレームワークを分離しつつ、コントローラーとルーティングの対応に集中でき、フレームワークを最大限活用できるのです。

ルーティングの設定

アノテーションによって定義されたルーティングを読み込むために、以下の設定が必要です。

# app/config/routing.yml
app:
    resource: '@AppBundle/Controller/'
    type:     annotation

上記の設定によって、src/AppBundle/Controller/配下とそのサブディレクトリにあるコントローラーのアノテーションを読み込むことができます。なので、もし多くのコントローラーを定義しているのであれば、サブディレクトリ内で整理することができます。

<your-project>/
├─ ...
└─ src/
   └─ AppBundle/
      ├─ ...
      └─ Controller/
         ├─ DefaultController.php
         ├─ ...
         ├─ Api/
         │  ├─ ...
         │  └─ ...
         └─ Backend/
            ├─ ...
            └─ ...
テンプレートの設定
  • コントローラーが使うテンプレートを指定するのに@Templateアノテーションを使ってはいけません。

@Templateアノテーションは便利ですが、トリッキーな側面もありますので、どちらかといえばあまりお勧めできるものではありません。 ほとんどの場合、@Templateアノテーションはパラメーターの指定なく使われます。このため、どのテンプレートが表示されるのかわかりにくくなってしまっています。また、初心者にとっては、コントローラーが(Viewレイヤーを挟んでいるとしても)常にレスポンスオブジェクトを返しているということがわかりにくくなってしまいます。

コントローラーの概観

以上を踏まえて、ホームページ表示のためのコントローラーがどのようになるか、みてみましょう。

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction()
    {
        $posts = $this->getDoctrine()
            ->getRepository('AppBundle:Post')
            ->findLatest();

        return $this->render('default/index.html.twig', array(
            'posts' => $posts
        ));
    }
}
ParamConverterを使う

Doctrineを利用しているのであれば、エンティティーへの問い合わせと、コントローラーへの引き渡しを自動的に行ってくれる、ParamConverterを使うことができます。

  • 単純かつ有効な場合は、Doctrineエンティティに対する問い合わせを自動化するParamConverterを使いましょう。

例:

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 * @Route("/{id}", name="admin_post_show")
 */
public function showAction(Post $post)
{
    $deleteForm = $this->createDeleteForm($post);

    return $this->render('admin/post/show.html.twig', array(
        'post'        => $post,
        'delete_form' => $deleteForm->createView(),
    ));
}

普通はshowAction()の引数に$idを渡すと思います。ParamConverterを使うことにより、 DoctrineエンティティのPostクラスでタイプヒンティングした($post)を引数に渡すことで、$id{id}の値に一致するオブジェクトを自動的に判別して返してくれます。Postが見つからない場合は、404ページを返します。

さらに高度な使いかた

上記の例では、ワイルドカード{id}がエンティティオブジェクトのプロパティにマッチしたので、特別な設定をすることなく設定しなくても動作しました。 ワールドカード名とマッチしない、あるいは複雑なロジックになってくると、エンティティの問い合わせには手動で行うのが最も簡単な方法になります。 Blogアプリの例ではCommentControllerが該当します。

/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 */
public function newAction(Request $request, $postSlug)
{
    $post = $this->getDoctrine()
        ->getRepository('AppBundle:Post')
        ->findOneBy(array('slug' => $postSlug));

    if (!$post) {
        throw $this->createNotFoundException();
    }

    // ...
}

@ParamConverterアノテーションによる柔軟な設定も利用できます。

use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;

/**
 * @Route("/comment/{postSlug}/new", name = "comment_new")
 * @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
 */
public function newAction(Request $request, Post $post)
{
    // ...
}

ParamConverterは単純な場合にその真価を発揮するということが重要です。そうはいっても、エンティティに直接問い合わせを行うことも簡単であることを忘れないでください。

事前・事後フック

コントローラーの実行前/ 実行後に実行したい処理があるなら、EventDispatcherコンポーネントのフィルターにより設定できます。


感想

"thin controller and fat models"は聞いたことあったけど「5-10-20ルール」は初耳。普段書いているFatControllerをリファクタリングする基準を得ることができて嬉しい。 ParamConverter使ったことないので、使ってみよう。

(2016/11/2 更新)

next

hanahirodev.hatenablog.com