BEAR.Sundayでコンタクトフォームを作ってみる④

BEAR.Sundayでコンタクトフォームを作ってみる③の続きです。

CSRF対策を入れたいところですが、その前にBEAR.Sunday(正確にはBEAR.Package)に含まれているAura Formを使うようにフォームをリファクタリングします。フォームの処理にAuraのライブラリ(Aura.Inputなど)を使うということです。

Aura FormにはCSRF対策も含まれているため、同時に目的も達成できます。

インターセプターの作成

まず、インターセプターを作成し、そこでフォームとそのバリデーションを定義します。

src/Interceptor/Contact/Form.php

<?php
/**
 * Kenjis.Contact
 *
 * @author     Kenji Suzuki <https://github.com/kenjis>
 * @license    MIT License
 * @copyright  2014 Kenji Suzuki
 * @link       https://github.com/kenjis/Kenjis.Contact
 */

namespace Kenjis\Contact\Interceptor\Contact;

use Ray\Aop\MethodInterceptor;
use Aura\Input\FilterInterface;
use BEAR\Package\Module\Form\AuraForm\AuraFormTrait;

/**
 * Aura.Input form
 *
 * @see https://github.com/auraphp/Aura.Input
 */
class Form implements MethodInterceptor
{
    use AuraFormTrait;

    /**
     * Set form
     *
     * @param FilterInterface $filter
     */
    private function setForm(FilterInterface &$filter)
    {
        $this->form
            ->setField('name')
            ->setAttribs(
                [
                    'class' => 'form-control',
                    'id' => 'name',
                    'name' => 'name',
                    'size' => 20,
                    'maxlength' => 50
                ]
            );
        $filter->setRule(
            'name',
            'Enter your name (max 50 letters).',
            function ($value) {
                if (mb_strlen($value) == 0) return false;
                if (mb_strlen($value) > 50) return false;
                return true;
            }
        );

        $this->form
            ->setField('email')
            ->setAttribs(
                [
                    'class' => 'form-control',
                    'id' => 'email',
                    'name' => 'email',
                    'size' => 20,
                    'maxlength' => 100,
                ]
            );
        $filter->setRule(
            'email',
            'Enter your email adrress (max 100 letters).',
            function ($value) {
                if (mb_strlen($value) > 100) return false;
                return filter_var($value, FILTER_VALIDATE_EMAIL);
            }
        );

        $this->form
            ->setField('comment', 'textarea')
            ->setAttribs(
                [
                    'class' => 'form-control',
                    'id' => 'comment',
                    'name' => 'comment',
                    'cols' => 40,
                    'rows' => 5,
                ]
            );
        $filter->setRule(
            'comment',
            'Enter comment (max 400 letters).',
            function ($value) {
                if (mb_strlen($value) == 0) return false;
                if (mb_strlen($value) > 400) return false;
                return true;
            }
        );
    }
}

テンプレートの変更

フォームのテンプレートをform()関数を使うように変更します。

form()関数はAuraのヘルパーをTwigの関数化したものです。引数にフォーム生成のための配列を受け取り、inputタグなどを生成します。

--- a/src/Resource/Page/Contact.twig
+++ b/src/Resource/Page/Contact.twig
@@ -19,21 +19,23 @@
     <p>Thank you!</p>
 {% else %}
     <form role="form" action="" method="post" enctype="multipart/form-data">
+        {{ form(form['__csrf_token']['hint']) }}
+
         <div class="form-group{%if form['name']['error'] %} has-error{% endif %}">
             <label class="control-label" for="name">Name</label>
-            <input id="name" type="text" name="name" class="form-control" size="20" maxlength="50" value="{{ form['name']['value'] }}" />
+            {{ form(form['name']['hint']) }}
             <label class="control-label" for="name">{{ form['name']['error'] }}</label>
         </div>

         <div class="form-group{%if form['email']['error'] %} has-error{% endif %}">
             <label class="control-label" for="email">Email</label>
-            <input id="email" type="text" name="email" class="form-control" size="20" maxlength="100" value="{{ form['email']['value'] }}" />
+            {{ form(form['email']['hint']) }}
             <label class="control-label" for="email">{{ form['email']['error'] }}</label>
         </div>

         <div class="form-group{%if form['comment']['error'] %} has-error{% endif %}">
             <label class="control-label" for="comment">Comment</label>
-            <textarea id="comment" name="comment" class="form-control" cols="40" rows="5">{{ form['comment']['value'] }}</textarea>
+            {{ form(form['comment']['hint']) }}

             <label class="control-label" for="comment">{{ form['comment']['error'] }}</label>
         </div>

最初の{{ form(form['__csrf_token']['hint']) }}はCSRF対策のためのトークンです。

ページコントローラの変更

ページコントローラContactを変更し、@BEAR\Sunday\Annotation\Formアノテーションを追加します。

このアノテーションがあるメソッドを先ほどのContact\Formインターセプターがインターセプトして処理するようにするということです。

それから、バリデーションのロジックはインターセプターに移りますので削除します。

--- a/src/Resource/Page/Contact.php
+++ b/src/Resource/Page/Contact.php
@@ -25,17 +25,19 @@ class Contact extends ResourceObject
         $this->mailer = $mailer;
     }

+    /**
+     * @BEAR\Sunday\Annotation\Form
+     */
     public function onGet()
     {
         return $this;
     }

+    /**
+     * @BEAR\Sunday\Annotation\Form
+     */
     public function onPost($name, $email, $comment)
     {
-        if (! $this->validation($name, $email, $comment)) {
-            return $this;
-        }
-
         $this->sendmail($name, $email, $comment);

         $this['code'] = $this->code = Code::CREATED;
@@ -46,34 +48,6 @@ class Contact extends ResourceObject
         return $this;
     }

-    private function validation($name, $email, $comment)
-    {
-        $pass = true;
-
-        if ((mb_strlen($name) == 0) || (mb_strlen($name) > 50)) {
-            $this->body['form']['name']['error'] = 'Enter your name (max 50 letters).';
-            $pass = false;
-        }
-
-        if ((mb_strlen($email) > 100) || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
-            $this->body['form']['email']['error'] = 'Enter your email adrress (max 100 letters).';
-            $pass = false;
-        }
-
-        if ((mb_strlen($comment) == 0) || (mb_strlen($comment) > 400)) {
-            $this->body['form']['comment']['error'] = 'Enter comment (max 400 letters).';
-            $pass = false;
-        }
-
-        if (! $pass) {
-            $this->body['form']['name']['value']    = $name;
-            $this->body['form']['email']['value']   = $email;
-            $this->body['form']['comment']['value'] = $comment;
-        }
-
-        return $pass;
-    }
-
     private function sendmail($name, $email, $comment)
     {
         $data = [

インターセプターの設定

AppModule.phpで、Aura Formに必要なAura.Sessionモジュールのインストール、および、インターセプターのバインドを定義します。

--- a/src/Module/AppModule.php
+++ b/src/Module/AppModule.php
@@ -6,6 +6,7 @@ use BEAR\Package\Module\Package\StandardPackageModule;
 use Ray\Di\AbstractModule;
 use Ray\Di\Di\Inject;
 use Ray\Di\Di\Named;
+use BEAR\Package\Module\Session\AuraSession\SessionModule;

 class AppModule extends AbstractModule
 {
@@ -35,6 +36,16 @@ class AppModule extends AbstractModule

         // override twig module
         $this->install(new Provider\TwigModule($this));
+
+        // install aura.session
+        $this->install(new SessionModule());
+
+        // aspect @Form annotaion
+        $this->bindInterceptor(
+            $this->matcher->subclassesOf('Kenjis\Contact\Resource\Page\Contact'),
+            $this->matcher->annotatedWith('BEAR\Sunday\Annotation\Form'),
+            [$this->requestInjection('Kenjis\Contact\Interceptor\Contact\Form')]
+        );

         // override module
         // $this->install(new SmartyModule($this));

これで、Resource\Page\Contactクラス(とそのサブクラス)のメソッドに@BEAR\Sunday\Annotation\Formというアノテーションがあると、Contact\Formインターセプターが処理をインターセプトするようになります。

これは、メソッドインターセプターによるアスペクト指向プログラミング(AOP)ということになります。メソッドインターセプターとは、メソッドの実行を横取り(intercept)して代理実行するものです。

メソッドインターセプターによる処理の解説

Resource\Page\ContactクラスのonGet()およびonPost()メソッドに@BEAR\Sunday\Annotation\Formアノテーションがありますので、これらのメソッドの実行はインターセプトされ、Interceptor\Contact\Formインターセプターで処理されます。

ただし、このFormインターセプターにはフォームの定義とバリデーションしかありません。実際の処理は、BEAR\Package\Module\Form\AuraForm\AuraFormTraitにまとめられています。

実際には、このAuraFormTraitのinvoke()メソッドが実行されます。

vendor/bear/package/src/Module/Form/AuraForm/AuraFormTrait.php

    public function invoke(MethodInvocation $invocation)
    {
        list($args, $hasSubmit) = $this->getSubmit();
        $page = $invocation->getThis();

        $this->setForm($this->filter);
        $hasSubmit && $this->form->fill($args);
        if ($this->form->filter()) {
            // action
            return $invocation->proceed();
        }

        // set hint and error message
        foreach ($this->form->getIterator() as $name => $value) {
            $errors = $this->form->getMessages($name);
            $error = ($hasSubmit && $errors) ? $this->getErrorMessage($this->form->getMessages($name)) : '';
            $page->body['form'][$name]['error'] = $error;
            $page->body['form'][$name]['hint'] = $this->form->get($name);
        }

        return $page->onGet();
    }

フォームをセット($this->setForm($this->filter))して、バリデーションを実行($this->form->filter())し、検証をパスしたら元のメソッドを実行($invocation->proceed())しています。

検証をパスしなかった場合は、エラーメッセージをセットしてページリソースのonGet()メソッドを実行して返しています。つまり、フォームが表示されるというわけです。

これで、CSRF対策もできました。今日はここまでにします。

BEAR.Sundayでコンタクトフォームを作ってみる⑤へ続く。

過去記事

関連

Date: 2014/08/14

Tags: bear, aop, aura, validation