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

Symfony Best Practice 訳してみた - Chapter7 -

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

前回の記事

hanahirodev.hatenablog.com

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

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

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

3行まとめ

  • フォームと表示の関心を分離しよう。

  • フォーム内でコンテナに格納された情報を使うときは、フォームもサービスと同じように登録しよう。

  • 表示とsubmitを受け取るアクションは同じにしよう。


第7章 Forms

フォームはスコープが広大な上に、膨大な*1機能を有しているため、もっとも誤用されることが多いSymfonyコンポーネントの一つです。 本章で紹介するベストプラクティスをもとに、フォームを活用して、素早く開発できるようになりましょう。

フォームの組み立て
  • PHPのクラスとしてフォームを定義しましょう。

フォームコンポーネントはコントローラーの中に記述することができます。もしそのフォームを他の場所で再利用しないのであれば、全く問題ありません。 しかし、コードの体系化と再利用のために個別のPHPのクラスとして定義することをお勧めします。

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('summary', TextareaType::class)
            ->add('content', TextareaType::class)
            ->add('authorEmail', EmailType::class)
            ->add('publishedAt', DateTimeType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Post'
        ));
    }
}
  • data transformerなどカスタムフォームクラスを使っていないのであれば、AppBundle\Formネームスペースにフォームタイプクラスを配置しましょう。

Formクラスを使うには、createForm()に完全修飾クラス名(Fully Qualified Class Name:FQCN)を引数として渡します。

// ...
use AppBundle\Form\PostType;

// ...
public function newAction(Request $request)
{
    $post = new Post();
    $form = $this->createForm(PostType::class, $post);

    // ...
}
Formをサービスとして登録する

フォームタイプをサービスとして登録することもできます。ただし、フォームタイプがコンテナからなんらかの依存性を注入する必要が有るときのみ必要な操作で、そうでないときはオーバーヘッドの増加にしかならないのでお勧めしません。

フォームボタンの設定

フォームクラス自体は、自分自身がどこで使われるかを意識してはいけません。こうしておくことで、後々再利用しやすくなります。

  • フォームクラスや、コントローラーではなくテンプレートにボタンを追加しましょう。

Symfonyのフォームコンポーネントには、ボタンをフィールドとして追加することができます。フォーム表示の単純化という意味では良い方法です。しかし、直接フォームクラスにボタンを追加することは、そのフォームのスコープを制限することになります。

class PostType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            // ...
            ->add('save', SubmitType::class, array('label' => 'Create Post'))
        ;
    }

    // ...
}

上記のフォームは、記事を投稿*2するために作られたようですが、編集画面として再利用したくなったときに、ボタン名が適切ではありません。開発者によっては、コントローラーにボタンの設定を書く方法をとります。

namespace AppBundle\Controller\Admin;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use AppBundle\Entity\Post;
use AppBundle\Form\PostType;

class PostController extends Controller
{
    // ...

    public function newAction(Request $request)
    {
        $post = new Post();
        $form = $this->createForm(PostType::class, $post);
        $form->add('submit', SubmitType::class, array(
            'label' => 'Create',
            'attr'  => array('class' => 'btn btn-default pull-right')
        ));

        // ...
    }
}

上記の例も重大な誤りです。画面表示のマークアップ(label,Cssなど)とPHPのコードを混合してしまっています。関心の分離*3には常に従うべきです。viewの関心はviewレイヤーにおきましょう。

{{ form_start(form) }}
    {{ form_widget(form) }}

    <input type="submit" value="Create"
           class="btn btn-default pull-right" />
{{ form_end(form) }}
フォームの表示

フォームの表示方法はいろいろあります。1行ですべての要素を表示する方法から、個別の要素を一つ一つ表示する方法まで様々です。最善策はフォームに必要なカスタマイズの量によります。 最も簡単な方法は(開発中は特に便利です)formタグとform_widget()関数により、すべてのフィールドを表示する方法です。

{{ form_start(form, {'attr': {'class': 'my-form-class'} }) }}
    {{ form_widget(form) }}
{{ form_end(form) }}

どのように表示されるかを制御したいのであれば、form_widget(form)の代わりに、一つ一つフィールドを指定して表示させてください。詳細はフォームの表示をカスタマイズするをみてください。フィールドの個別表示、フォームテーマを使ってアプリケーション全体でフォームの表示方法を制御する方法がわかります。

フォームのsubmitを扱う

フォームのsubmitデータの扱い方は、テンプレートの扱い方とよく似ています。

public function newAction(Request $request)
{
    // build the form ...

    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {
        $em = $this->getDoctrine()->getManager();
        $em->persist($post);
        $em->flush();

        return $this->redirect($this->generateUrl(
            'admin_post_show',
            array('id' => $post->getId())
        ));
    }

    // render the template
}

注意すべきことは2つだけです。1つめはフォーム表示とsubmitデータを扱うのは、同じアクションで行うこと。たとえば、フォーム表示だけを行うnewAction()とsubmitデータを扱うだけのcreateAction()があったとしましょう。これら二つのアクションはほとんど一緒になるので、newAction()ですべてを処理した方がシンプルです。 2つめは、$form->isSubmitted()を明示的にif文で判定することです。isValid()の中で、最初にisSubmitted()を呼び出すので、技術的には不要です。しかし、こうしないと、フォーム送信のが常に(GETリクエストであっても)処理されているように見えてしまい、処理フローが読みづらくなってしまいます。


感想

フォームの生成と受け取りのアクションを同じにしていて、Controllerが肥大化しているのは、受け取った情報の処理をコントローラー内でやってしまっているから。 そもそも受け取ったデータを加工してDBに格納するのがよくないのかも。 DBには「事実」のみを保存するという観点で考えると、フォームの内容をそのまま突っ込むテーブルと、付随情報のテーブルは分けるべきなのかも。

勉強会まであと二日。明日明後日は2章分ずつ投稿します。

(2016/11/5 更新)

next

hanahirodev.hatenablog.com

*1:訳注:"endles"と書かれていることから、無限ともいえるほど多くの意と思われる。

*2:訳注:Post

*3:訳注:関心の分離