Practical Symfony #26: PHPMentorsPageflowerBundleを使ったページフロー定義と対話の管理
Symfony Advent Calendar 2014 (Qiita) 10日目
PHPMentorsPageflowerBundleは筆者が開発したSymfonyアプリケーション向けのページフローエンジンです。特徴としては、以下のものが挙げられます。
- アノテーションによるページフロー定義
- 対話の管理
- アクセス制御されたアクション
- 対話スコープのプロパティ
- 対話開始直後に実行されるユーザー定義メソッド
- 複数のブラウザーウィンドウまたはタブのサポート
PHPMentorsPageflowerBundle
を使うと、コントローラーに断片的に埋め込まれたページフローに関するコードを明示的な定義で置き換えることができます。また、対話と対話スコープのプロパティの導入によってコントローラーの状態管理コードを大幅を削減することができます。
では、早速コードを見てみましょう。以下はSymfony2ベースのユーザー登録サンプルのコードです。
Example\UserRegistrationBundle\Controller\UserRegistrationController:
<?php /* * Copyright (c) 2012-2014 KUBO Atsuhiro <[email protected]>, * 2014 YAMANE Nana <[email protected]>, * All rights reserved. * * This file is part of PHPMentors_Training_Example_Symfony. * * This program and the accompanying materials are made available under * the terms of the BSD 2-Clause License which accompanies this * distribution, and is available at http://opensource.org/licenses/BSD-2-Clause */ namespace Example\UserRegistrationBundle\Controller; use PHPMentors\DomainKata\Usecase\UsecaseInterface; use PHPMentors\PageflowerBundle\Annotation\Accept; use PHPMentors\PageflowerBundle\Annotation\EndPage; use PHPMentors\PageflowerBundle\Annotation\Init; use PHPMentors\PageflowerBundle\Annotation\Page; use PHPMentors\PageflowerBundle\Annotation\Pageflow; use PHPMentors\PageflowerBundle\Annotation\StartPage; use PHPMentors\PageflowerBundle\Annotation\Stateful; use PHPMentors\PageflowerBundle\Annotation\Transition; use PHPMentors\PageflowerBundle\Controller\ConversationalControllerInterface; use PHPMentors\PageflowerBundle\Conversation\ConversationContext; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Example\UserRegistrationBundle\Entity\User; use Example\UserRegistrationBundle\Form\Type\UserRegistrationType; /** * @Route("/users/registration", service="example_user_registration.user_registration_controller") * @Pageflow({ * @StartPage({"input", * @Transition("confirmation"), * }), * @Page({"confirmation", * @Transition("success"), * @Transition("input") * }), * @EndPage("success") * }) */ class UserRegistrationController extends Controller implements ConversationalControllerInterface { const VIEW_INPUT = 'ExampleUserRegistrationBundle:UserRegistration:input.html.twig'; const VIEW_CONFIRMATION = 'ExampleUserRegistrationBundle:UserRegistration:confirmation.html.twig'; const VIEW_SUCCESS = 'ExampleUserRegistrationBundle:UserRegistration:success.html.twig'; /** * {@inheritDoc} */ private $conversationContext; /** * @var User * * @Stateful */ private $user; /** * {@inheritDoc} */ public function setConversationContext(ConversationContext $conversationContext) { $this->conversationContext = $conversationContext; } /** * @Init */ public function initialize() { $this->user = new User(); } /** * @return Response * * @Route("/") * @Method("GET") * @Accept("input") * @Accept("confirmation") */ public function inputGetAction() { if ($this->conversationContext->getConversation()->getCurrentPage()->getPageId() == 'confirmation') { $this->conversationContext->getConversation()->transition('input'); } $form = $this->createForm(new UserRegistrationType(), $this->user, array('action' => $this->generateUrl('example_userregistration_userregistration_inputpost'), 'method' => 'POST')); return $this->render(self::VIEW_INPUT, array( 'form' => $form->createView(), )); } /** * @param Request $request * @return Response * * @Route("/") * @Method("POST") * @Accept("input") * @Accept("confirmation") */ public function inputPostAction(Request $request) { if ($this->conversationContext->getConversation()->getCurrentPage()->getPageId() == 'confirmation') { $this->conversationContext->getConversation()->transition('input'); } $form = $this->createForm(new UserRegistrationType(), $this->user, array('action' => $this->generateUrl('example_userregistration_userregistration_inputpost'), 'method' => 'POST')); $form->handleRequest($request); if ($form->isValid()) { $this->conversationContext->getConversation()->transition('confirmation'); return $this->redirect($this->conversationContext->generateUrl('example_userregistration_userregistration_confirmationget')); } else { return $this->render(self::VIEW_INPUT, array( 'form' => $form->createView(), )); } } /** * @return Response * * @Route("/confirmation") * @Method("GET") * @Accept("confirmation") */ public function confirmationGetAction() { $form = $this->createFormBuilder(null, array('action' => $this->generateUrl('example_userregistration_userregistration_confirmationpost'), 'method' => 'POST')) ->add('prev', 'submit', array('label' => '修正する')) ->add('next', 'submit', array('label' => '登録する')) ->getForm(); return $this->render(self::VIEW_CONFIRMATION, array( 'form' => $form->createView(), 'user' => $this->user, )); } /** * @param Request $request * @return Response * * @Route("/confirmation") * @Method("POST") * @Accept("confirmation") */ public function confirmationPostAction(Request $request) { $form = $this->createFormBuilder(null, array('action' => $this->generateUrl('example_userregistration_userregistration_confirmationpost'), 'method' => 'POST')) ->add('prev', 'submit', array('label' => '修正する')) ->add('next', 'submit', array('label' => '登録する')) ->getForm(); $form->handleRequest($request); if ($form->isValid()) { if ($form->get('prev')->isClicked()) { return $this->redirect($this->conversationContext->generateUrl('example_userregistration_userregistration_inputget')); } if ($form->get('next')->isClicked()) { $this->createUserRegistrationUsecase()->run($this->user); $this->conversationContext->getConversation()->transition('success'); return $this->render(self::VIEW_SUCCESS); } } $this->conversationContext->getConversation()->transition('input'); return $this->render(self::VIEW_CONFIRMATION, array( 'form' => $form->createView(), )); } /** * @return UsecaseInterface */ private function createUserRegistrationUsecase() { return $this->get('example_user_registration.user_registration_usecase'); } }
アノテーションによるページフロー定義
... /** * ... * @Pageflow({ * @StartPage({"input", * @Transition("confirmation"), * }), * @Page({"confirmation", * @Transition("success"), * @Transition("input") * }), * @EndPage("success") * }) */ ...
@Pageflow
アノテーションによるページフロー定義では、ページとページ間の関係を記述します。ページは1
つの@StartPage
、0
以上の@Page
、1
つの@EndPage
で構成されます。@StartPage
は対話
が開始された後に遷移するページ(開始ページ)、@EndPage
はそこに遷移すると対話
が終了するページ(終了ページ)を示します。ページ間の関係は@Transition
によって記述します。
対話の管理
対話はリクエストされたURLがページフローのコントローラーである場合に自動的に開始される、ページフローのインスタンスです。1つのページフローに対して複数の対話を実行することができます。
対話は固有のID(対話ID)によって識別されます。リダイレクトURLの生成等で対話IDが埋め込まれたURLが必要な場合はController::generateUrl()
の代わりにConversationContext::generateUrl()
を使うことができます。また、フォームではCONVERSATION_ID
フィールドが自動的に提供されます。
開発者はコントローラーの中でConversation::transition()
を呼び出すことにより、カレントページを変更します。Conversation::transition()
によって終了ページに遷移すると、対話は自動的に破棄されます。
アクセス制御されたアクション
... /** * ... * @Accept("confirmation") */ public function confirmationPostAction(Request $request) { ...
@Accept
アノテーションによって、アクションを実行可能なページをホワイトリスト形式で記述します。カレントページがリストに存在しない場合、HTTPステータスコード403
が返されます。
対話スコープのプロパティ
... /** * @var User * * @Stateful */ private $user; ...
@Stateful
アノテーションによって、プロパティを対話に紐付けることができます。プロパティは対話に限定されたセッション変数のように振る舞います。
対話開始直後に実行されるユーザー定義メソッド
... /** * @Init */ public function initialize() { $this->user = new User(); } ...
@Init
アノテーションが付与されたメソッドは、対話の開始直後に自動的に実行されます。これらのメソッドは主に@Stateful
が付与されたプロパティの初期化のために使われます。
複数のブラウザーウィンドウまたはタブのサポート
ブラウザーウィンドウまたはタブでそれぞれ別々の対話を実行することができます。
PHPMentorsPageflowerBundleを使いはじめるには?
PHPMentorsPageflowerBundle
を使いはじめるに際には、ページフローの登録を含めた具体的なコード[Controller] ユーザー登録のページフローを実装した。 · 270b1c3 · phpmentors-jp/phpmentors-training-example-symfonyが参考になるでしょう。
参考