鍍金池/ 教程/ PHP/ 表單和動作
開始構建一個框架應用程序
模塊
路由和控制器
表單和動作
結尾
開始使用 Zend Framework 2
樣式和翻譯
數(shù)據(jù)和模型

表單和動作

添加新的專輯

我們現(xiàn)在可以添加新專輯代碼的功能。這分為兩個部分:

  • 展示表單給用戶用來提供細節(jié)

  • 處理表單提交并存儲到數(shù)據(jù)庫

我們用 Zend\Form 來處理這些。Zend\Form 控件管理表單和處理表單驗證,添加一個 Zend\InputFilterAlbum 實體。開始寫我們的新類 Album\Form\AlbumForm,這個類繼承自 Zend\Form\Form。在 module/Album/src/Album/Form 目錄下新建一個 AlbumForm.php 文件,內(nèi)容如下:

namespace Album\Form;

use Zend\Form\Form;

class AlbumForm extends Form
{
    public function __construct($name = null)
    {
        // we want to ignore the name passed
        parent::__construct('album');

        $this->add(array(
           'name' => 'id',
            'type' => 'Hidden',
        ));
        $this->add(array(
            'name' => 'title',
            'type' => 'Text',
            'options' => array(
                'label' => 'Title',
            ),
        ));
        $this->add(array(
            'name' => 'artist',
            'type' => 'Text',
            'options' => array(
                'label' => 'Artist',
            ),
        ));
        $this->add(array(
            'name' => 'submit',
            'type' => 'Submit',
            'attributes' => array(
                'value' => 'Go',
                'id' => 'submitbutton',
            ),
        ));
    }
}

AlbumForm 的構造函數(shù)中,我們需要做一些事情。首先我們要設置表單的名字,調(diào)用父類構造函數(shù)。接著我們創(chuàng)建四個表單元素:id,title,artist,以及提交按鈕。對每一項,我們都要設置各種各樣的屬性和設置,包括要顯示的標簽。

注意

HTML-Forms 可以使用 POSTGET 來發(fā)送。ZF2s 默認使用 POST,因此你不必顯式的設置這個選項。如果你想改成 GET,你所做的就是需要在構造函數(shù)中指定。

$this->setAttribute('method', 'GET');

我們需要為表單設置驗證。在 Zend Framework 2,驗證通過使用輸入過濾器處理,這個過濾器可以是獨立的或者可以在類中定義。它繼承自 InputFilterAwareInterface 接口類,就像一個模型實體。在本例中,將輸入過濾器添加到 Album 類,module/Album/src/Album/Model 路徑下的 Album.php 文件修改如下:

namespace Album\Model;

// Add these import statements
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;

class Album implements InputFilterAwareInterface
{
    public $id;
    public $artist;
    public $title;
    protected $inputFilter;                       // <-- Add this variable

    public function exchangeArray($data)
    {
        $this->id     = (isset($data['id']))     ? $data['id']     : null;
        $this->artist = (isset($data['artist'])) ? $data['artist'] : null;
        $this->title  = (isset($data['title']))  ? $data['title']  : null;
    }

    // Add content to these methods:
    public function setInputFilter(InputFilterInterface $inputFilter)
    {
        throw new \Exception("Not used");
    }

    public function getInputFilter()
    {
        if (!$this->inputFilter) {
            $inputFilter = new InputFilter();

            $inputFilter->add(array(
                'name'     => 'id',
                'required' => true,
                'filters'  => array(
                    array('name' => 'Int'),
                ),
            ));

            $inputFilter->add(array(
                'name'     => 'artist',
                'required' => true,
                'filters'  => array(
                    array('name' => 'StripTags'),
                    array('name' => 'StringTrim'),
                ),
                'validators' => array(
                    array(
                        'name'    => 'StringLength',
                        'options' => array(
                            'encoding' => 'UTF-8',
                            'min'      => 1,
                            'max'      => 100,
                        ),
                    ),
                ),
            ));

            $inputFilter->add(array(
                'name'     => 'title',
                'required' => true,
                'filters'  => array(
                    array('name' => 'StripTags'),
                    array('name' => 'StringTrim'),
                ),
                'validators' => array(
                    array(
                        'name'    => 'StringLength',
                        'options' => array(
                            'encoding' => 'UTF-8',
                            'min'      => 1,
                            'max'      => 100,
                        ),
                    ),
                ),
            ));

            $this->inputFilter = $inputFilter;
        }

        return $this->inputFilter;
    }
}

InputFilterAwareInterface 定義了兩方法:setInputFilter()getInputFilter()。我們需要實現(xiàn) getInputFilter() 方法,而 setInputFilter() 只要簡單的拋一個異常就行了。

getInputFilter() 中,實例化一個 InputFilter,然后添加我們想要的輸入框。 為每個屬性對應添加過濾和驗證。例如為 id 字段添加整型過濾器,為文本元素添加兩個過濾器,StripTagsStringTrim,用來移除不想要的 HTML 和不必要的空白字符。還要為這些屬性添加 StringLength,確保不會輸入太多的字符,以便存入數(shù)據(jù)庫。

現(xiàn)在需要獲取表單進行顯示,然后在提交時進行處理。在 AlbumControlleraddAction()

// module/Album/src/Album/Controller/AlbumController.php:

//...
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Album\Model\Album;          // <-- Add this import
use Album\Form\AlbumForm;       // <-- Add this import
//...

    // Add content to this method:
    public function addAction()
    {
        $form = new AlbumForm();
        $form->get('submit')->setValue('Add');

        $request = $this->getRequest();
        if ($request->isPost()) {
            $album = new Album();
            $form->setInputFilter($album->getInputFilter());
            $form->setData($request->getPost());

            if ($form->isValid()) {
                $album->exchangeArray($form->getData());
                $this->getAlbumTable()->saveAlbum($album);

                // Redirect to list of albums
                return $this->redirect()->toRoute('album');
            }
        }
        return array('form' => $form);
    }
//...

添加 AlbumForm 到使用列表之后,我們實現(xiàn) addAction()。看一看 addAction() 的內(nèi)部細節(jié)吧:

 $form = new AlbumForm();
 $form->get('submit')->setValue('Add');

實例化 AlbumForm 然后設置提交按鈕的標簽為 Add。在編輯專輯會使用到不同的標簽,就可以復用代碼。

 $request = $this->getRequest();
 if ($request->isPost()) {
     $album = new Album();
     $form->setInputFilter($album->getInputFilter());
     $form->setData($request->getPost());
     if ($form->isValid()) {

如果 Request 對象的 isPost() 方法返回 true,表明表單已經(jīng)被提交了。從專輯實例設置表單的輸入過濾器,然后我們將報文數(shù)據(jù)設置到表單中,使用表單對象的 isValid() 成員函數(shù)來檢查數(shù)據(jù)是否有效。

 $album->exchangeArray($form->getData());
 $this->getAlbumTable()->saveAlbum($album);

如果表單是有效的,就從表單中獲取數(shù)據(jù),使用 saveAlbum() 存儲到模型中。

// Redirect to list of albums
 return $this->redirect()->toRoute('album');

在保存新記錄之后,使用重定向控制器插件重定向到專輯的列表。

 return array('form' => $form);

最終,返回我們想指定給視圖的變量。在本例中,僅僅是表單對象。注意 Zend Framework 2 也運行返回變量的數(shù)組,然后指定給視圖,這將會在場景后邊創(chuàng)建一個 ViewModel 。可以少輸入點字。

現(xiàn)在我們需要在 add.phtml 視圖腳本中渲染表單。

<?php
// module/Album/view/album/album/add.phtml:

$title = 'Add new album';
$this->headTitle($title);
?>
 <h1><?php echo $this->escapeHtml($title); ?></h1>
<?php
$form->setAttribute('action', $this->url('album', array('action' => 'add')));
$form->prepare();

echo $this->form()->openTag($form);
echo $this->formHidden($form->get('id'));
echo $this->formRow($form->get('title'));
echo $this->formRow($form->get('artist'));
echo $this->formSubmit($form->get('submit'));
echo $this->form()->closeTag();

我們先展示一個標題,然后再渲染表單。Zend 框架提供一些視圖輔助函數(shù),可以十分簡單地完成上訴要求。form() 輔助函數(shù)有一個 openTag()closeTag() 方法,用來控制表單的打開和關閉。對每一個元素的標簽,可以使用 formRow(),但是兩個元素太單一了,還要使用 formHidden()formSubmit()。

http://wiki.jikexueyuan.com/project/zend2-user-guide/images/stylingandtranslations3.png" alt="image" />

或者,渲染表單的過程可以綁定到視圖輔助方法 formCollection 上。例如,在上面的視圖腳本替代所有的表單渲染的輸出語句是:

 echo $this->formCollection($form);

注意:你仍然需要使用 openTagcloseTag 方法來控制表單。上面的代碼,你可以替代其他輸入語句,調(diào)用 formCollection

這將會對表單結構進行遍歷,對每個元素調(diào)用合適的標簽,元素和視圖輔助的錯誤提示,你通過打開和關閉表單標簽包裝 formCollection($form)。

現(xiàn)有應該使用程序主頁上的 Add new album 鏈接來增加一條新的 album 記錄。

編輯專輯

編輯專輯和添加一個專輯的代碼幾乎是相同,所以代碼都很簡單。這次在 AlbumController 中使用 editAction()

// module/Album/src/Album/Controller/AlbumController.php:
//...

    // Add content to this method:
    public function editAction()
    {
        $id = (int) $this->params()->fromRoute('id', 0);
        if (!$id) {
            return $this->redirect()->toRoute('album', array(
                'action' => 'add'
            ));
        }

        // Get the Album with the specified id.  An exception is thrown
        // if it cannot be found, in which case go to the index page.
        try {
            $album = $this->getAlbumTable()->getAlbum($id);
        }
        catch (\Exception $ex) {
            return $this->redirect()->toRoute('album', array(
                'action' => 'index'
            ));
        }

        $form  = new AlbumForm();
        $form->bind($album);
        $form->get('submit')->setAttribute('value', 'Edit');

        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setInputFilter($album->getInputFilter());
            $form->setData($request->getPost());

            if ($form->isValid()) {
                $this->getAlbumTable()->saveAlbum($album);

                // Redirect to list of albums
                return $this->redirect()->toRoute('album');
            }
        }

        return array(
            'id' => $id,
            'form' => $form,
        );
    }
//...

代碼看地來很簡單。讓我們看看與添加 album 之間的不同。首先查找配置 route 中 id,然后加載對應的專輯,代碼如下:

$id = (int) $this->params()->fromRoute('id', 0);
if (!$id) {
    return $this->redirect()->toRoute('album', array(
        'action' => 'add'
    ));
}

// Get the album with the specified id.  An exception is thrown
// if it cannot be found, in which case go to the index page.
try {
    $album = $this->getAlbumTable()->getAlbum($id);
}
catch (\Exception $ex) {
    return $this->redirect()->toRoute('album', array(
        'action' => 'index'
    ));
}

params 是一個控制器插件,提供一個簡便的方式來檢索匹配的路由。在 module.config.php,我們創(chuàng)建在模塊中的 route,使用它來進行檢索 id。如果 id 是零,就會重定向到添加動作,否則,我們繼續(xù)從數(shù)據(jù)庫中獲取專輯實體。

必須檢查,確保指定 id 的專輯可以被找到。如果不行,數(shù)據(jù)訪問方法將會拋出異常。捕獲該異常并重新輸入用戶索引頁面。

 $form = new AlbumForm();
 $form->bind($album);
 $form->get('submit')->setAttribute('value', 'Edit');

表單的 bind() 方法附著于模型。有如下兩個方式:

  • 當顯示表單時,每個元素的初始值都從模型中提取。

  • isValid() 成功驗證后,表單中的數(shù)據(jù)推送回模型中。

這些操作通過復合對象完成的。有許多的復合對象,但是只會使用 Zend\Stdlib\Hydrator\ArraySerializable 作為默認復合對象,這個復合對象在模型指定了兩個方法:getArrayCopy()exchangeArray()。我們早已在 Album 實體中寫好了 exchangeArray(),所以只要寫好 getArrayCopy()

// module/Album/src/Album/Model/Album.php:
// ...
    public function exchangeArray($data)
    {
        $this->id     = (isset($data['id']))     ? $data['id']     : null;
        $this->artist = (isset($data['artist'])) ? $data['artist'] : null;
        $this->title  = (isset($data['title']))  ? $data['title']  : null;
    }

    // Add the following method:
    public function getArrayCopy()
    {
        return get_object_vars($this);
    }
// ...

復合對象使用 bind() 的結果是,我們不用往 $album 填充表單的數(shù)據(jù),因為已經(jīng)自動填充好了,只要調(diào)用 mappers 的 saveAlbum() 來保存修改到數(shù)據(jù)庫。

視圖模板,edit.phtml,添加一個專輯的如下所示:

<?php
 // module/Album/view/album/album/edit.phtml:

 $title = 'Edit album';
 $this->headTitle($title);
?>
 <h1><?php echo $this->escapeHtml($title); ?></h1>

<?php
$form = $this->form;
$form->setAttribute('action', $this->url(
    'album',
    array(
        'action' => 'edit',
        'id'     => $this->id,
    )
));
$form->prepare();

echo $this->form()->openTag($form);
echo $this->formHidden($form->get('id'));
echo $this->formRow($form->get('title'));
echo $this->formRow($form->get('artist'));
echo $this->formSubmit($form->get('submit'));
echo $this->form()->closeTag();

唯一的變化是使用 Edit Album 的標題和設置表單的動作到 edit 的動作。

現(xiàn)在可以編輯專輯了。

刪除專輯

為完善我們的程序,我們需要添加刪除操作。列表中每一個專輯都有一個刪除鏈接,使用最原始點擊方式來對應刪除記錄。這或許很糟糕,記住使用 HTTP 的規(guī)范,執(zhí)行一個不可撤銷的動作,應該使用 POST 而不是使用 GET。

在用戶點擊刪除時,我們要顯示一個確認窗口,在用戶點擊 yes 后,就會進行刪除。如果表單并不重要,就將代碼直接寫入視圖腳本中(畢竟,Zend\Form 是可選!)。

AlbumController::deleteAction() 寫下如下代碼:

// module/Album/src/Album/Controller/AlbumController.php:
//...
    // Add content to the following method:
    public function deleteAction()
    {
        $id = (int) $this->params()->fromRoute('id', 0);
        if (!$id) {
            return $this->redirect()->toRoute('album');
        }

        $request = $this->getRequest();
        if ($request->isPost()) {
            $del = $request->getPost('del', 'No');

            if ($del == 'Yes') {
                $id = (int) $request->getPost('id');
                $this->getAlbumTable()->deleteAlbum($id);
            }

            // Redirect to list of albums
            return $this->redirect()->toRoute('album');
        }

        return array(
            'id'    => $id,
            'album' => $this->getAlbumTable()->getAlbum($id)
        );
    }
//...

在獲取匹配專輯的表單 id,使用請求對象的 isPost() 來決定顯示確認頁面或者直接刪除專輯。使用表對象的 deleteAlbum() 方法刪除記錄,然后重定向回到專輯列表。如果不是 POST 請求,我們就會取回正確的數(shù)據(jù)庫記錄,然后連同 id 返回給視圖。

視圖腳本的簡單表單:

<?
// module/Album/view/album/album/delete.phtml:

$title = 'Delete album';
$this->headTitle($title);
?>
 <h1><?php echo $this->escapeHtml($title); ?></h1>

 <p>Are you sure that you want to delete
    '<?php echo $this->escapeHtml($album->title); ?>' by
    '<?php echo $this->escapeHtml($album->artist); ?>'?
 </p>
<?php
$url = $this->url('album', array(
    'action' => 'delete',
    'id'     => $this->id,
));
?>
 <form action="<?php echo $url; ?>" method="post">
  <div>
    <input type="hidden" name="id" value="<?php echo (int) $album->id; ?>" />
    <input type="submit" name="del" value="Yes" />
    <input type="submit" name="del" value="No" />
  </div>
 </form>

在這個腳本中,我們展示一個帶有 YesNo 按鈕的確認信息。如果用戶點擊 Yes 我們就會執(zhí)行刪除操作。

確保主頁顯示專輯列表

最后一點。此刻,主頁 http://zf2-tutorial.localhost/ 并沒有顯示專輯列表。

這是由于在 Application 模塊中的 module.config.php route 的設置。為了改變設置,打開 module/Application/config/module.config.php 找到主頁的 route。

 'home' => array(
     'type' => 'Zend\Mvc\Router\Http\Literal',
     'options' => array(
         'route'    => '/',
         'defaults' => array(
             'controller' => 'Application\Controller\Index',
             'action'     => 'index',
         ),
     ),
 ),

將控制器由 Application\Controller\Index 改為 Album\Controller\Album

'home' => array(
     'type' => 'Zend\Mvc\Router\Http\Literal',
     'options' => array(
         'route'    => '/',
         'defaults' => array(
             'controller' => 'Album\Controller\Album', // <-- change here
             'action'     => 'index',
         ),
     ),
 ),

就這些了,現(xiàn)在你有一個可以運行的程序了。