Zend_Form + TwitterBootstrap + Smarty のフォームを作ってみる

Smarty

最近、すっかり更新頻度が下がっているので、
たまには気合入れてみます。

Zend_FormのviewヘルパーとTwitterBootstrapのフォームのマークアップがけっこう違うので、
取り込もうとするとあれこれする必要があります。
過去作ったやつのソースなので、そのままでは動かないと思いますが、自分用も含めお役に立てれば幸いです。

ちなみにZendFrameworkは1系です。
今更がっつり書く内容でもないのかもしれませんが・・まだ現役なので(汗)

今回はこのTwitterBootstrapのマークアップの出力になるようにZend_Form周りを作ります。
http://twitter.github.io/bootstrap/base-css.html#forms

簡単なトピックス投稿フォームです。

ページャー付きの一覧リストから、新規投稿、編集、削除があって
トピックスはタイトルと本文、送信ボタン
そして確認画面と保存。

こんな流れを想定します。

完成イメージ
完成イメージ

フォームのグループ化を使い、フォームタイトルを出しています。
グループタイトルなので見出しを分けたフォームも可能な設計をします。

Zendコントローラーはだいたいこんな感じ。
継承先のCommon_Controller_Actionは、アプリのいろいろ定義してあるファイルと想定します。

_dbInfo = new DbTable_info();
        parent::_init();
    }


    public function preDispatch()
    {
        parent::_preDispatch();
    }


    public function postDispatch()
    {
        parent::_postDispatch();
    }


    /**
     * 登録済みリスト
     */
    public function indexAction()
    {
        $select = $this->_dbInfo->select()
            ->from($this->_dbInfo, array(
                'info_id', 'info_title', 'info_comment', 'info_udate'
            ))
            ->order('info_udate DESC')
        ;

        $adapter = new Zend_Paginator_Adapter_DbTableSelect($select);
        $paginator = new Zend_Paginator($adapter);
        $paginator->setCurrentPageNumber($this->_getParam('p', 1))
            ->setItemCountPerPage(10)
            ->setPageRange(7);
        $this->view->paginator = $paginator;

    }


    /**
     * 新規登録
     */
    public function addAction()
    {
        $row = $this->_dbInfo->createRow();
        $this->_editform($row);

    }


    /**
     * 編集
     */
    public function editAction()
    {
        $select = $this->_dbInfo->select()
            ->from($this->_dbInfo, array(
                'info_id', 'info_title', 'info_comment', 'info_udate'
            ))
            ->where('info_id=?', $this->_getParam('id'))
        ;
        $row = $this->_dbInfo->fetchRow($select);
        if (!$row) {
            return $this->_forward('notfound', 'error');
        }
        $this->_editform($row);

    }


    /**
     * 編集フォーム
     *
     * @param type $row
     */
    private function _editform($row)
    {

        $form = new Default_Form_Info($row);
        $this->_helper->viewRenderer->setNoController();


        //確認画面
        if ($this->getRequest()->getPost('btnConfirm') && $form->isValid($this->getRequest()->getPost())) {

            $this->view->form = $form->confirmDecorator();

            //確認画面
            $this->_helper->viewRenderer
                ->setScriptAction('scripts/confirm');
        } elseif ($this->getRequest()->getPost('save')) {

            $input = $form->getSession();

            $row->setFromArray($input);
            $row->info_udate = new Zend_Db_Expr('NOW()');
            $row->save();


            //セッションの削除
            $form->delSession();

            $this->_flm->addMessage('保存しました');


            $this->_redirector->gotoRoute(array(
                'action' => 'index',
                'id' => null,
            ));

        } else {

            if ($this->getRequest()->getPost('back')) {
                $form->setDefaults($form->getSession());
            } elseif ($form->isErrors()) {
                $form->setDefaults($form->getValues());
            } else {
                $default = $row->toArray();
                $form->setDefaults($default);
            }

            $this->_helper->viewRenderer->setScriptAction('scripts/form');
        }

        $this->view->form = $form;

    }


    /**
     * 記事の削除
     */
    public function delAction()
    {
        $select = $this->_dbInfo->select()
            ->from($this->_dbInfo, array('info_id'))
            ->where('info_id=?', $this->_getParam('id'))
        ;
        $row = $this->_dbInfo->fetchRow($select);


        if (!$row) {
            throw new Exception('パラメータが違います。削除できませんでした。');
        }

        //商品データを削除
        $row->delete();



        $this->_flm->addMessage('削除しました。');

        return $this->_redirector->gotoRoute(array(
                'action' => 'index',
                'id' => null,
        ));

    }


}

public function indexAction()

ここは記事の一覧を出力。
ページャーのアダプターにZend_Paginator_Adapter_DbTableSelectを使います。
このアダプタの種類はArrayDbSelectDbTableSelectIteratorNullと複数あるので間違えないように。
特にDbSelectDbTableSelectはうっかり間違えやすい。

Manual - Documentation - Zend Framework
Zend Framework project official website
public function addAction()
public function editAction()

僕の場合は、新規も編集も同じfunctionへ飛ばします。
引数にZend_Db_Rowを持っているかどうかだけの判別です。

public function delAction()

削除部分に関しては手抜きですが、
$this->_dbInfo->select()を使わずともfind()でも充分です。
元ソースは関連データなどの削除もしたかったので、select()を使いました

続いてフォーム部分。
こちらも継承先のCommon_Form_Registration
フォーム全体のもろもろを設定してある想定です。

_row = $row;
        parent::__construct($options);

    }


    public function init()
    {
        $this->addElement('text', 'info_title', array(
            'label' => 'タイトル',
            'required' => true,
            'filters' => array('StringTrim'),
            'Decorators' => $this->ElementDecorators()
        ));

        $this->addElement('textarea', 'info_comment', array(
            'label' => 'コメント',
            'rows' => 6,
            'cols' => 80,
            'required' => true,
            'filters' => array('StringTrim'),
            'Decorators' => $this->ElementDecorators()
        ));


        foreach ($this->getElements() as $elem) {
            $names[] = $elem->getName();
        }
        $this->addDisplayGroup($names, 'main', array(
            'Legend'=>'お知らせ投稿','Decorators' => $this->GroupDecorators()
            ));

    }


    /**
     * 入力チェック
     * ViewHelperの変わりに値を出力
     *
     * @return Common_Form_Registration
     */
    public function confirmDecorator()
    {
        return parent::_confirmDefaultDecorator();
    }
}

グループ化を手抜きでループしちゃっていますが、
見出しを分けたいときは、別々にしましょう。

foreach ($this->getElements() as $elem) {
   $names[] = $elem->getName();
}
$this->addDisplayGroup($names, 'main', array(
   'Legend'=>'お知らせ投稿','Decorators' => $this->GroupDecorators()
));

こっからが今回のキモです。
さきほどのもろもろを入れた部分。

フォームはだいたい複数設置するパターンが多いのでコアを1つにまとめています。
確認画面用のデコレーターの設定等

入力内容はセッション保存してます。

_ctrl = Zend_Controller_Front::getInstance();
        $this->_session = new Zend_Session_Namespace(
            $this->_ctrl->getRequest()->getControllerName()
        );


        $this->setMethod('post')
            ->setDescription(
                ' 必須 '
                . 'の項目は必ず入力してください。'
        );

        parent::__construct($options);

    }


    public function loadDefaultDecorators()
    {
        $this->setDecorators(array(
            'FormElements',
            array('decorator' => 'Description', 'options' => array('tag' => 'p', 'class' => 'description', 'escape' => false)),
            array('decorator' => 'HtmlTag', 'tag' => 'div', 'options' => array('class' => 'form-horizontal')),
            'Form',
        ));
    }


    public function GroupDecorators()
    {
        return array(
            'FormElements',
            'Fieldset',
            array('decorator' => 'Description', 'options' =>
                array('tag' => 'div', 'class' => 'help-block', 'escape' => false)
            )
        );
    }


    public function ElementDecorators()
    {
        return array(
            'ViewHelper',
            array('Errors', 'options' => array('class' => 'alert alert-error')),
            array('Description', array('tag' => 'span', 'class' => 'help-block', 'escape' => false)),
            array('HtmlTag', array('tag' => 'div', 'class' => 'controls')),
            array('Label', 'options' => array('class' => 'control-label', 'requiredSuffix' => ' 必須', 'escape' => false)),
            array('decorator' => array('OuterHtmlTag' => 'HtmlTag'),
                'options' => array('tag' => 'div', 'class' => 'control-group'))
        );

    }

    public function isValid($data)
    {
        $res = parent::isValid($data);

        foreach (parent::getValues() as $key => $value) {
            $this->_session->$key = $value;
        }
        return $res;

    }



    /**
     * submit ボタンを各サブフォームに追加する
     *
     * @param  Zend_Form $form
     */
    public function inputForm()
    {
        $this->setAction(Zend_View_Helper_Url::url());

        $this->addElement('submit', 'btnConfirm', array(
            'label' => ' 確認 ',
            'type' => 'submit',
            'class' => 'btn btn-primary',
            'Decorators' => array('viewHelper', 'Errors')
        ));

        $this->addElement('button', 'btnReset', array(
            'ignore' => true,
            'label' => 'リセット',
            'type' => 'reset',
            'class' => 'btn',
            'Decorators' => array('viewHelper', 'Errors')
        ));
        $this->addDisplayGroup(array(
            'btnConfirm',
            'btnReset'
            ), 'subform');

        $this->subform->clearDecorators()
            ->addDecorator('FormElements')
            ->addDecorator('HtmlTag', array('tag' => 'div', 'class' => 'form-actions'));


        $this->setElementFilters(array('StringTrim'));
        return $this;

    }


    /**
     *
     * @param  $element
     * @return 
     */
    private function _checkElement($element)
    {

        //選択なら項目を表示
        switch ($element->getType()) {

            //ファイル
            case 'Zend_Form_Element_File':
                if ($this->getValue($elem_id)) {
                    $value = basename($element->getFileName());
                }
                break;


            //選択項目系
            case 'Zend_Form_Element_Select':
                if ($element->getValue() != "") {
                    $select = $element->getMultiOptions();
                    if (!$value = $select[$element->getValue()]) {
                        foreach ($select as $k => $v) {
                            if (is_array($v)) {
                                if ($value = $v[$element->getValue()]) {
                                    break;
                                }
                            }
                        }
                    }
                }
                break;


            case 'Zend_Form_Element_Radio':
                $select = $element->getMultiOptions();
                $value = $select[$element->getValue()];
                break;

            //複数選択項目系
            case 'Zend_Form_Element_MultiCheckbox':
            case 'Zend_Form_Element_MultiSelect':
                $select = $element->getMultiOptions();

                if (is_array($element->getValue())) {
                    foreach ($element->getValue() as $k => $v) {
                        $tmp[] = $select[$v];
                    }
                    $value = implode('
', $tmp); } else { $value = '-'; } break; default: $value = $element->getValue(); } return $value; } /** * 入力チェック * ViewHelperの変わりに値を出力 * * @return Common_Form_Registration */ protected function _confirmDefaultDecorator() { foreach ($this->getDisplayGroup('main') as $element) { $decorators = $element->getDecorators(); foreach ($decorators as $key => $deco) { if ($key == 'Zend_Form_Decorator_ViewHelper') { $decorators[$key] = 'ConfirmDefault'; } elseif ($key == 'Zend_Form_Decorator_File') { $decorators[$key] = 'ConfirmDefaultFile'; } elseif ($key == 'Default_Form_Decorator_Upload') { $decorators[$key] = array('ConfirmUpload', $deco->getOptions()); } } $element->setDecorators($decorators); } return $this; } }
protected function _confirmDefaultDecorator()

グループをmainと指定しちゃっていますが、
複数グループを作ったときは、それぞれ取得できるように二重ループにしましょう。

エラー用デコレーター

2013-09-25追記
入力エラーが起きたときに要素を赤くします。

getElement();
        $view = $element->getView();
        if (null === $view) {
            return $content;
        }

        $errors = $element->getMessages();
        if (empty($errors)) {
            return $content;
        }

        $decorators = $element->getDecorators();
        foreach ($decorators as $key => $deco) {
            if ($key == 'OuterHtmlTag') {
                //var_dump($decorators[$key]);
                $decorators[$key]->setOption('class', 'control-group error');
            }
        }


        $separator = $this->getSeparator();
        $placement = $this->getPlacement();
        $errors = $view->formErrors($errors, $this->getOptions());

        switch ($placement) {
            case self::APPEND:
                return $content . $separator . $errors;
            case self::PREPEND:
                return $errors . $separator . $content;
        }

    }
}
?>

一覧リスト

index.tpl





{include 'scripts/pagination.tpl' pager=$paginator->getPages() assign="pagination"}


{$pagination}
    {foreach $paginator->getCurrentItems() as $row}
  • {$row->info_title}

    {$row->info_comment|bbcode2html}
    編集 削除
  • {/foreach}
{$pagination}

info_confirm
削除ボタンとかはTwitterBootstrapのconfirmモーダルウィンドウを使って確認アラートを表示させています。

ページャー

スタイルシートのvisible-phoneを使って、PC用と前後ページ送りしかないスマホ用の2パターン作っています。

scripts/pagination.tpl

{strip}
    


    

{/strip}

scripts/form.tpl


{if $form->isErrors()}
    
入力エラー
{/if} {$form->inputForm()->render()}

zend_form_input_error
入力エラーサンプル
TwitterBootstrapのエラー

scripts/confirm.tpl

以下の内容でよろしいですか?
{$form->render()}
{$this->formButton('save',$this->translate("Save"),['type'=>'submit',class=>"btn btn-primary"])} {$this->formButton('back',$this->translate("Back"),['type'=>'submit',class=>"btn"])}

zend_form_confirm

えーっと・・これで
一通りの説明ができたのかな。。

話が長すぎて、1回の記事では無理があったかも(汗)

でも頑張った俺。
質問などあれば追記していきまふ。

コメント

タイトルとURLをコピーしました