PHP Mentors — Practical Symfony #26:...

1.5M ratings
277k ratings

See, that’s what the app is perfect for.

Sounds perfect Wahhhh, I don’t wanna

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つの@StartPage0以上の@Page1つの@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が参考になるでしょう。

参考

symfony practical.symfony dsl pageflow generative.programming

See more posts like this on Tumblr

#symfony #practical.symfony #dsl #pageflow #generative.programming