シムノート

PHPフレームワークSymfonyの学習帳

ユーザ用ツール

サイト用ツール


サイドバー

メニュー



このエントリーをはてなブックマークに追加

blog:2015-12-15:超入門_symfony3_form_後編

超入門 Symfony3 : (10) Form【後編】

Formクラスの作成

前回、コントローラの中で直接Formを作成しました。今回はフォームの再利用性を高めるためにFormのクラスを作成して、そのクラスからFormを作成するように修正します。

以下のコマンドでUnseiエンティティ用のFormクラスを生成します。

$ php bin/console doctrine:generate:form AppBundle:Unsei

UnseiType.phpが生成されました。コマンドに引数で渡しているAppBundle:Unseisrc/AppBundle/Entity/Unsei.phpのショートカット表記になります。

UnseiType.phpの中を見てみましょう。

<?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class UnseiType extends AbstractType
{
    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name') // ①
        ;
    }
    
    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Unsei'
        ));
    }
}

① フォームにInput Text入力フィールドを追加しています。

<input type="text" name="name" ...>

本来、add()メソッドの第2引数には入力フィールドの型を指定するのですが、省略した場合、Formが型を推測してHTMLタグを作成します。タグが間違って作成されるような時は、add()メソッドの第2引数を指定するようにしてください。

use Symfony\Component\Form\Extension\Core\Type\TextType;
...
        $builder
            ->add('name', TextType::class)
        ;

入力フィールドのラベルはadd()の第1引数に渡した文字列が使われます(つまり英語)。ラベルを変更したい場合は、add()の第3引数にオプションを渡して指定します。

        $builder
            ->add('name', TextType::class, ['label' => '名前'])
        ;

newActionの修正

先ほど生成したUnseiTypeを使用するようコントローラを修正します。

...
use AppBundle\Form\UnseiType; (a)
...
class UnseiController extends Controller
{
    ...
    
    /**
     * 運勢の新規作成
     * 
     * @Route("/new", name="unsei_new")
     * @Method({"GET", "POST"})    // ②
     * 
     * @param Request $request
     * @return Response
     */
    public function newAction(Request $request)
    {
        $unsei = new Unsei();
        
//      $form = $this->createFormBuilder($unsei)
//          ->add('name', TextType::class)
//          ->getForm();

        $form = $this->createForm(UnseiType::class, $unsei); // ①
        ...        
    }
    ...
}

① 前回はcreateFormBuilder()を使いましたが、createForm()を使うよう変更します。(a)でインポートしたUnseiTypeのクラス名を引数に渡します。

http://localhost:8000/unsei/new にアクセスして動作確認を行います。前回と同様にフォームから新しい運勢が追加できるはずです。

editActionの実装

運勢の編集を実装します。

...
class UnseiController extends Controller
{
    ...
    
    /**
     * 運勢の編集
     * 
     * @Route("/{id}/edit", name="unsei_edit")
     * @Method({"GET", "PUT"})  // (a)
     * 
     * @param Request $request
     * @return Response
     */
    public function editAction(Request $request, $id)
    {
        $repository = $this->getDoctrine()->getRepository(Unsei::class);
        $unsei = $repository->find($id);

        if (!$unsei) {
            throw $this->createNotFoundException('No unsei found for id '.$id);
        }
        
        $form = $this->createForm(UnseiType::class, $unsei, [ // ①
            'method' => 'PUT',  // ②
        ]);
        
        $form->handleRequest($request); // ③
        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->flush(); // ④
            
            return $this->redirectToRoute('unsei_index');
        }

        return $this->render('unsei/edit.html.twig', [
            'form' => $form->createView(),
        ]);
    }

    ...
}

① Formを作成します。ルートパラメータで渡されたidをキーに取得したUnseiエンティティをFormにセットしています。

② FormにHTTPメソッドのPUTを指定しています。(a)のルーティングで指定しているHTTPメソッドと合わせて下さい。HTTPメソッドの指定を省略した場合はデフォルト値のPOSTになります。

③ FormにRequestを処理するように渡しています。Fromは送信データの有無をチェックして、送信データ(ここでは'name')が有れば、それを関連づいているエンティティ(ここではUnsei)にセットします。その時、バリデーションチェックも行います。

④ DBに更新されたUnseiエンティティを保存します。$em->persist($unsei)を行っていないことに注目してください。リポジトリから取得したエンティティは既にエンティティマネージャの管理下にある為、persist()を行う必要はありません。

次にunsei/edit.html.twigを追加します。

{% extends 'base.html.twig' %}

{% block title %}運勢編集{% endblock %}

{% block body %}
    <h1>運勢編集</h1>

    {{ form_start(form) }}
        {{ form_widget(form) }}
        <input type="submit" value="更新" />
    {{ form_end(form) }}

    <hr>
    <a href="{{ path('unsei_index') }}">運勢一覧へ戻る</a>
{% endblock %}

edit.html.twigがレンダリングされるとformタグは以下のように表示されます。

...
<form name="unsei" method="post">
  <input type="hidden" name="_method" value="PUT" />
  <div id="unsei">
    <div>
      <label for="unsei_name" class="required">Name</label>
      <input type="text" id="unsei_name" name="unsei[name]" required="required" value="大吉" />
    </div>
    <input type="hidden" id="unsei__token" name="unsei[_token]" value="KKqUhaQ0YIUlDdCCY02MQdDk8hYlYcgjmVAIkgrUN4k" />
  </div>
  <input type="submit" value="更新" />
</form>
...

formタグのmethodがpostになっていることと、hiddenのinputタグで_methodキーにPUTが設定されていることに注目してください。ウェブブラウザはHTTPのメソッドにGETPOSTしか使えません。その為、Symfonyは_methodを使って擬似的にPUTDELETEを実現しています。

http://localhost:8000/unsei にアクセスして動作確認を行います。[編集]リンクをクリックして、運勢の名前を変更してみてください。

deleteActionの実装

運勢の削除を実装します。削除処理では入力項目の表示やバリデーションチェックの必要がないのでFromクラスを使わずに実装することにします。

...
class UnseiController extends Controller
{
    ...

    /**
     * 運勢の削除
     * 
     * @Route("/{id}", name="unsei_delete")
     * @Method("DELETE") // ①
     * 
     * @param Request $request
     * @return Response
     */
    public function deleteAction(Request $request, $id)
    {
        $repository = $this->getDoctrine()->getRepository(Unsei::class);
        $unsei = $repository->find($id);

        if (!$unsei) {
            throw $this->createNotFoundException('No unsei found for id '.$id);
        }
        
        if ($this->isCsrfTokenValid('unsei', $request->get('_token'))) { // ②
            $em = $this->getDoctrine()->getManager();
            $em->remove($unsei); // ③
            $em->flush();
        }

        return $this->redirectToRoute('unsei_index');
    }
}

① ルーティングにHTTPのDELETEメソッドを定義しています。

CSRF対策の為にトークンをチェックしています。 isCsrfTokenValid()の第1引数にはトークンIDを渡します。トークンIDはトークン生成時に指定した物に合わせます。第2引数にはRequestで渡されてきたトークンを渡します。

remove()でエンティティマネージャに削除対象のエンティティを伝えています。flush()でDBからエンティティが削除されます。

次にunsei/index.html.twigを修正します。削除は運勢一覧画面から行えるようにします。

{% extends 'base.html.twig' %}
 
{% block title %}運勢一覧{% endblock %}
 
{% block body %}
<table>
  <tr>
    <th>運勢</th>
    <th>操作</th>
  </tr>
  {% for unsei in unseis %}
  <tr>
    <td>{{ unsei.name }}</td>
    <td>
      <a href="{{ path('unsei_edit', {'id': unsei.id}) }}">編集</a>
       
      {# ① 削除フォーム #}
      <form action="{{ path('unsei_delete', {'id': unsei.id}) }}" method="post" style="display:inline-block">
        <input type="hidden" name="_method" value="DELETE" />
        <input type="hidden" name="_token" value="{{ csrf_token('unsei') }}" />
        <input type="submit" value="削除" />
      </form>
    </td>
  </tr>
  {% endfor %}
</table>

<hr>

<a href="{{ path('unsei_new') }}">新規追加</a>
{% endblock %}

① 削除フォームを手書きで作成しています。
path('unsei_delete', {'id': unsei.id})でformタグのaction属性を指定します。
_methodでHTTPのDELETEメソッドを指定します。
_tokenでCSRFトークンを指定します。
csrf_toke('トークンID)でCSRFトークンを生成します。ここではトークンIDを'unsei'としました。これは上記のdeleteAction()アクションメソッド内でCSRFトークンのチェック時に使用します。

http://localhost:8000/unsei にアクセスして動作確認を行います。[削除]ボタンをクリックして、運勢を削除してみてください。


コラム:SensioFrameworkExtraBundle

今まで当たり前のように使ってきた@Routeや@Methodアノテーションですが、実はSensioFrameworkExtraBundleによって、素のSymfonyフレームワークに組み込まれた機能です。symfony new xxxxで作成されたプロジェクトは全てSymfony Standard Editionと言われ、SensioFrameworkExtraBundleを含んだ物となっています。

SensioFrameworkExtraBundleはコントローラーを簡素にする為に、アノテーションと規約をによる機能拡張をしてくれるバンドルです。このバンドルが提供するアノテーションの中から@ParamConverterアノテーションを紹介します。@ParamConverterはルートパラメータからオブジェクトを検索し、コントローラの引数で渡してくれる機能です。

@ParamConverterの使用例

    // Controller

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

    //...
    
    /**
     * @Route("/{id}/edit", name="post_edit")
     * @Method({"GET", "PUT"})
     */
    public function editAction(Request $request, $id) // ルートパラメータのidを受け取る
    {
        // ① $idでPostを検索します。
        $repository = $this->getDoctrine()->getRepository(Post::class);
        $post = $repository->find($id);

        // ② Postが存在しない時、Exceptionを発生させます。
        if (!$post) {
            throw $this->createNotFoundException('No post found for id '.$id);
        }

        // Do Something
        
        return $this->render('post/edit.html.twig', [
            'post' => $post,
        ]);        
    }

    // Controller

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

    //...
    
    /**
     * @Route("/{id}/edit", name="post_edit")
     * @Method({"GET", "PUT"})
     * @ParamConverter("post", class="AppBundle:Post") // $post引数がPostオブジェクトであることを指定
     */
    public function editAction(Request $request, $post) // $post引数を受け取る
    {
        // Do something
        
        return $this->render('post/edit.html.twig', [
            'post' => $post,
        ]);        
    }

@ParamConverterを使うことで、{id}Postの検索を行い、$post引数に渡してくれます。
その際に、Postが見つからなければ、NotFoundExceptionの発行も自動で行ってくれます。
使用前の例の①②の処理が不要になり、コントローラーがシンプルになりました。

また、以下のように引数にタイプヒントを明記すれば、@ParamConverterを省略することも可能です。

    // Controller

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

    //...
    
    /**
     * @Route("/{id}/edit", name="post_edit")
     * @Method({"GET", "PUT"})
     */
    public function editAction(Request $request, Post $post) // タイプヒントで"Post"を明記
    {
        // Do something
        
        return $this->render('post/edit.html.twig', [
            'post' => $post,
        ]);        
    }

とてもクールです!

Symfonyは規約が少ない分、ソースの記述や設定ファイルが多く面倒くさいと思っていたのですが、徐々に進化し、アノテーション等も取り入れながら洗練されて来た印象があります。


Comments



29 +13 = ?
blog/2015-12-15/超入門_symfony3_form_後編.txt · 最終更新: 2016/01/07 18:24 by tsubo