鍍金池/ 教程/ PHP/ 應(yīng)用 Form 和 Fieldset
了解 Router
回顧博客應(yīng)用程序
應(yīng)用 Form 和 Fieldset
編輯數(shù)據(jù)和刪除數(shù)據(jù)
介紹我們第一個“博客” Module
介紹 Zend\Db\Sql 和 Zend\Stdlib\Hydrator
介紹 Service 和 ServiceManager
為不同的數(shù)據(jù)庫后臺做準(zhǔn)備

應(yīng)用 Form 和 Fieldset

到目前我們已經(jīng)實(shí)現(xiàn)了從數(shù)據(jù)庫讀取數(shù)據(jù)。在現(xiàn)實(shí)生活中的應(yīng)用程序這并不是十分實(shí)用,畢竟多數(shù)情況下我們至少需要實(shí)現(xiàn)完整的增刪改查功能。最普遍的添加數(shù)據(jù)到數(shù)據(jù)庫的方法是讓用戶將數(shù)據(jù)添加到 Web <form> 表單標(biāo)簽內(nèi)提交,然后我們的應(yīng)用程序?qū)⒂脩糨斎氡4娴胶笈_。

核心組件

我們想要能夠準(zhǔn)確的實(shí)現(xiàn)目標(biāo),而 Zend Framework 提供了所有完成目標(biāo)所需要的工具。在我們直接開始編碼之前,我們有必要了解一下這個工作的兩個核心組件。所以我們來看看這些組件是什么,并了解它們是怎么工作的。

Zend\Form\Fieldset (字段集)

首先你需要知道的組件就是 Zend\Form\Fieldset。一個 Fieldset 就是一個包含可重用的元素集合。你將會使用 Fieldset 來存儲前臺輸入以便讓后臺模型處理。每個 Model 都準(zhǔn)備一個 Fieldset 通常被認(rèn)為是良好實(shí)踐。

Fieldset 組件,然而,并不是 Form 表單本身,意味著你不可以只使用 Fieldset 卻不將其附著在 Form 組件上。這樣子做的優(yōu)勢是你擁有一個可以重用的元素集合,可以用于任意多個 Form 組件上,而不需要為了 Model 重新聲明所有輸入,因?yàn)?Model 已經(jīng)被 Fieldset 代表。

Zend\Form\Form (表單)

這是你所需要的主要部件,而且你很可能已經(jīng)聽說過了。Form 組件是所有的 Web <form> 元素的主要容器。你可以向其添加單個元素,也可以通過 Fieldset 的形式添加多個元素。

創(chuàng)建你的第一個 Fieldset

要解釋 Zend\Form 組件是如何工作的,不如通過你自己實(shí)戰(zhàn)編碼體會來的深刻。所以我們直接切入正題,創(chuàng)建我們的 Blog 模組所需的所有表單。我們首先創(chuàng)建包含所有需要處理的輸入元素的 Fieldset 用來處理我們的 Blog 數(shù)據(jù)。

  • 你需要為 id 屬性準(zhǔn)備一個隱藏的輸入域,僅僅在編輯和刪除數(shù)據(jù)時有用。
  • 你需要一個文本輸入框來存放 text 屬性。
  • 你需要另一個文本輸入框來存放 title 屬性。

創(chuàng)建文件 /module/Blog/src/Blog/Form/PostFieldset.php 并且添加下述代碼:

<?php
// 文件名: /module/Blog/src/Blog/Form/PostFieldset.php
namespace Blog\Form;

use Zend\Form\Fieldset;

class PostFieldset extends Fieldset
{
   public function __construct()
   {
      $this->add(array(
         'type' => 'hidden',
         'name' => 'id'
      ));

      $this->add(array(
         'type' => 'text',
         'name' => 'text',
         'options' => array(
           'label' => 'The Text'
         )
      ));

      $this->add(array(
         'type' => 'text',
         'name' => 'title',
         'options' => array(
            'label' => 'Blog Title'
         )
      ));
   }
}

如您所見這個類是十分有用的。我們做的事情是讓我們的類 extends Zend\Form\Fieldset,然后我們編寫一個 __construct() 函數(shù)并且添加所有我們需要的元素到字段集。這個 fieldset 現(xiàn)在就能隨我們意愿用于任意多個表單中了。所以接下來讓我們創(chuàng)建第一個 Form 表單吧。

創(chuàng)建 PostForm

現(xiàn)在我們已經(jīng)準(zhǔn)備好了我們的 PostFieldset,還需要在 Form 內(nèi)使用它。我們接下來需要添加一個表單的提交按鈕,這樣用戶就能夠提交數(shù)據(jù)了。所以在同一個路徑 /module/Blog/src/Blog/Form/PostForm 下創(chuàng)建 PostForm,并且將 PostFieldset 添加進(jìn)去:

 <?php
 // 文件名: /module/Blog/src/Blog/Form/PostForm.php
 namespace Blog\Form;

 use Zend\Form\Form;

 class PostForm extends Form
 {
     public function __construct()
     {
         $this->add(array(
             'name' => 'post-fieldset',
             'type' => 'Blog\Form\PostFieldset'
         ));

         $this->add(array(
             'type' => 'submit',
             'name' => 'submit',
             'attributes' => array(
                 'value' => 'Insert new Post'
             )
         ));
     }
 }

這就是我們的表單了。并沒有什么特別的,我們添加了 PostFieldset 到表單里,還添加了一個提交按鈕,然后沒別的了?,F(xiàn)在我們來讓這個表單發(fā)揮作用。

添加一個新 Post(帖子)

現(xiàn)在我們已經(jīng)寫好了 PostForm。現(xiàn)在想要使用它,還有幾個任務(wù)需要完成。目前你要直接面對的任務(wù)是:

  • 創(chuàng)建一個新的控制器 WriteController
  • 添加一個 PostService 服務(wù),將其設(shè)定為 WriteController 的依賴對象
  • 添加一個 PostForm 表單,將其設(shè)定為 WriteController 的依賴對象
  • 創(chuàng)建一個新的路徑 blog/add,并讓其轉(zhuǎn)發(fā)到 WriteController 和它附屬的 addAction()
  • 創(chuàng)建一個新的視圖用于顯示表單

創(chuàng)建 WriteController(寫控制器)

如您在任務(wù)清單上所見,我們需要一個新的控制器,而且這個控制器應(yīng)該擁有兩個依賴對象。一個依賴對象時 PostService,它也在 ListController 中被使用,而另一個依賴對象 PostForm 是全新的。由于在顯示博客數(shù)據(jù)的時候,PostFrom 是一個 ListController 不需要的依賴對象,所以我們會創(chuàng)建一個新的控制器來讓讀和寫兩邊的事務(wù)分離。首先,在配置文件中注冊一個控制器工廠(controller-factory):

 <?php
 // 文件名: /module/Blog/config/module.config.php
 return array(
     'db'              => array( /** DB Config */ ),
     'service_manager' => array( /** ServiceManager Config */),
     'view_manager'    => array( /** ViewManager Config */ ),
     'controllers'     => array(
         'factories' => array(
             'Blog\Controller\List'  => 'Blog\Factory\ListControllerFactory',
             'Blog\Controller\Write' => 'Blog\Factory\WriteControllerFactory'
         )
     ),
     'router'          => array( /** Router Config */ )
 );

下一步就是編寫 WriteControllerFactory。讓 factory 返回 WriteController 并且在構(gòu)造器中添加所需的依賴對象:

 <?php
 // 文件名: /module/Blog/src/Blog/Factory/WriteControllerFactory.php
 namespace Blog\Factory;

 use Blog\Controller\WriteController;
 use Zend\ServiceManager\FactoryInterface;
 use Zend\ServiceManager\ServiceLocatorInterface;

 class WriteControllerFactory implements FactoryInterface
 {
     public function createService(ServiceLocatorInterface $serviceLocator)
     {
         $realServiceLocator = $serviceLocator->getServiceLocator();
         $postService        = $realServiceLocator->get('Blog\Service\PostServiceInterface');
         $postInsertForm     = $realServiceLocator->get('FormElementManager')->get('Blog\Form\PostForm');

         return new WriteController(
             $postService,
             $postInsertForm
         );
     }
 }

在這個代碼示例中,這里有幾樣事情需要注意。第一件,WriteController 暫時還不存在,不過我們會在下一步創(chuàng)建這個控制器所以我們先假設(shè)其稍后會存在。第二件事情,我們通過訪問 FormElementManager 來獲得對 PostForm 的讀寫。所有表單都應(yīng)該通過 FormElementManager 來訪問。即使我們還沒有在配置文件中注冊 PostForm,FormElementManager 也會自動認(rèn)出表單并且將其當(dāng)做 invokables 對象,只要你的對象沒有依賴對象,你便不需要顯式地注冊它。

下一步,創(chuàng)建我們的控制器。請確保通過輸入依賴對象的接口并且添加 addAction() 函數(shù)來提示依賴對象。

 <?php
 // 文件名: /module/Blog/src/Blog/Controller/WriteController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Form\FormInterface;
 use Zend\Mvc\Controller\AbstractActionController;

 class WriteController extends AbstractActionController
 {
     protected $postService;

     protected $postForm;

     public function __construct(
         PostServiceInterface $postService,
         FormInterface $postForm
     ) {
         $this->postService = $postService;
         $this->postForm    = $postForm;
     }

     public function addAction()
     {
     }
 }

接下來創(chuàng)建新路徑:

 <?php
 // 文件名: /module/Blog/config/module.config.php
 return array(
     'db'              => array( /** Db Config */ ),
     'service_manager' => array( /** ServiceManager Config */ ),
     'view_manager'    => array( /** ViewManager Config */ ),
     'controllers'     => array( /** Controller Config */ ),
     'router'          => array(
         'routes' => array(
             'blog' => array(
                 'type' => 'literal',
                 'options' => array(
                     'route'    => '/blog',
                     'defaults' => array(
                         'controller' => 'Blog\Controller\List',
                         'action'     => 'index',
                     )
                 ),
                 'may_terminate' => true,
                 'child_routes'  => array(
                     'detail' => array(
                         'type' => 'segment',
                         'options' => array(
                             'route'    => '/:id',
                             'defaults' => array(
                                 'action' => 'detail'
                             ),
                             'constraints' => array(
                                 'id' => '\d+'
                             )
                         )
                     ),
                     'add' => array(
                         'type' => 'literal',
                         'options' => array(
                             'route'    => '/add',
                             'defaults' => array(
                                 'controller' => 'Blog\Controller\Write',
                                 'action'     => 'add'
                             )
                         )
                     )
                 )
             )
         )
     )
 );

最后我們創(chuàng)建一個沒實(shí)際作用的模板文件:

 <!-- Filename: /module/Blog/view/blog/write/add.phtml -->
 <h1>WriteController::addAction()</h1>

檢查當(dāng)前狀態(tài)

如果你視圖訪問新的路徑 localhost:8080/blog/add,那么你應(yīng)該會看見下面這樣的錯誤信息:

 Fatal error: Call to a member function insert() on a non-object in
 {libraryPath}/Zend/Form/Fieldset.php on line {lineNumber}

如果你看見的和上面寫出來的不一樣,那么請回過頭認(rèn)真檢查一下先前的步驟是否準(zhǔn)確地跟隨著教程,并且檢查你所有的文件。接下來,假設(shè)你已經(jīng)看見了這個錯誤信息,讓我們來尋找原因并且解決它!

上述錯誤是非常常見的,但是它的解決方法卻沒有那么直觀。它看上去像是 Zend/Form/Fieldset.php 中出現(xiàn)了一個錯誤,但實(shí)際上卻不是這個情況。這個錯誤信息讓你知道了你在創(chuàng)建你的表單的時候有一些事情出現(xiàn)了差錯。事實(shí)上,當(dāng)同時創(chuàng)建 PostForm 表單和 PostFieldset 的時候,我們忘記了一些非常,非常重要的事情。

注意:當(dāng)重寫 Zend\Form 組件的 __construct() 函數(shù)的時候,請永遠(yuǎn)不要忘記調(diào)用 parent::__construct()!

由于缺少了 parent::__construct() 調(diào)用,表單和字段集都不能正確的初始化。讓我們通過在表單和字段級中調(diào)用父級構(gòu)造器來修正這個問題。為了能擁有更好的可伸縮性我們也會包含能夠接收多個參數(shù)的 __construct() 函數(shù)的簽名。

 <?php
 // 文件名: /module/Blog/src/Blog/Form/PostForm.php
 namespace Blog\Form;

 use Zend\Form\Form;

 class PostForm extends Form
 {
     public function __construct($name = null, $options = array())
     {
         parent::__construct($name, $options);

         $this->add(array(
             'name' => 'post-fieldset',
             'type' => 'Blog\Form\PostFieldset'
         ));

         $this->add(array(
             'type' => 'submit',
             'name' => 'submit',
             'attributes' => array(
                 'value' => 'Insert new Post'
             )
         ));
     }
 }

如您所見我們的 PostForm 現(xiàn)在接受兩個參數(shù)分別定義我們的表單的名字和一些列的設(shè)置。兩個參數(shù)都會被傳給父對象。如果你仔細(xì)觀察我們是如何添加 PostFieldset 的,便會發(fā)現(xiàn)我們?yōu)樽侄渭x予了一個名字。這些選項(xiàng)都會在 PostFieldset 創(chuàng)建時通過 FormElementManager 傳出。不過要讓這些正常工作,我們需要在字段集里面做同樣的工作:

<?php
// 文件名: /module/Blog/src/Blog/Form/PostFieldset.php
namespace Blog\Form;

use Zend\Form\Fieldset;

class PostFieldset extends Fieldset
{
public function __construct($name = null, $options = array())
{
    parent::__construct($name, $options);
    $this->add(array(
        'type' => 'hidden',
        'name' => 'id'
    ));
    $this->add(array(
        'type' => 'text',
        'name' => 'text',
        'options' => array(
            'label' => 'The Text'
        )
    ));
    $this->add(array(
        'type' => 'text',
        'name' => 'title',
        'options' => array(
            'label' => 'Blog Title'
        )
    ));
}
}

重新載入你的應(yīng)用程序,你便可以看見你想要的結(jié)果了。

顯示表單

現(xiàn)在我們在 WriteController 里有了我們的 PostForm,是時候?qū)⑦@個表單傳遞給視圖,并讓其通過指定的來自 Zend\Form 組件的 ViewHelpers 來進(jìn)行渲染。首先修改你的控制器,讓表單被傳遞到視圖。

  <?php
 // 文件名: /module/Blog/src/Blog/Controller/WriteController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Form\FormInterface;
 use Zend\Mvc\Controller\AbstractActionController;
 use Zend\View\Model\ViewModel;

 class WriteController extends AbstractActionController
 {
     protected $postService;

     protected $postForm;

     public function __construct(
         PostServiceInterface $postService,
         FormInterface $postForm
     ) {
         $this->postService = $postService;
         $this->postForm    = $postForm;
     }

     public function addAction()
     {
         return new ViewModel(array(
             'form' => $this->postForm
         ));
     }
 }

然后我們需要修改視圖來讓表單得以正確渲染:

 <!-- Filename: /module/Blog/view/blog/write/add.phtml -->
 <h1>WriteController::addAction()</h1>
 <?php
 $form = $this->form;
 $form->setAttribute('action', $this->url());
 $form->prepare();

 echo $this->form()->openTag($form);

 echo $this->formCollection($form);

 echo $this->form()->closeTag();

首先,我們告訴了表單它應(yīng)該將它的數(shù)據(jù)發(fā)送給目前的 URL 然后我們告訴表單讓其 prepare()(準(zhǔn)備)自己(這個函數(shù)會觸發(fā)一些內(nèi)部操作)。

注意: HTML 表單可以通過 POST 或者 GET 方式來進(jìn)行傳輸。 ZF2 默認(rèn)是使用 POST,所以你不需要對此進(jìn)行顯式的設(shè)定。但是如果你希望使用 GET 方式,只需要在調(diào)用 prepare() 之前設(shè)置好這個特定的屬性: $form->setAttribute('method', 'GET');

接下來我們會使用幾個 ViewHelpers 來負(fù)責(zé)幫我們渲染表單。使用 Zend Framework 渲染表單的方法有很多種,不過使用 formCollection() 可能是最快的方法。

刷新您的瀏覽器,現(xiàn)在就能看見你的表單被正確顯示出來了。然而,現(xiàn)在提交表單的話,我們只能看見先前提交的表單原封不動的回顯出來。很簡單,這是因?yàn)槲覀冞€沒有為控制器添加任何邏輯。

注意:請記住這個教程僅僅聚焦于面向?qū)ο缶幊桃暯恰O襁@樣子渲染表單,不應(yīng)用任何樣式表是無法反映出絕大多數(shù)設(shè)計(jì)師關(guān)于一個美麗的表單的想法的。您將會在 Zend\Form\View\Helper 章節(jié)中學(xué)習(xí)到更多關(guān)于表單渲染的內(nèi)容。

幾乎適用于所有類型表單的控制器邏輯

編寫一個控制器來處理表單工作流是非常簡單的,而且基本上對于應(yīng)用程序中每一種表單的手段都是一樣的。

  1. 你會想先檢查目前的請求是否一個 POST 請求,這意味著確認(rèn)表單是否被發(fā)出。
  2. 如果表單已經(jīng)被發(fā)出,你會想要:
    • 將表單的 POST 數(shù)據(jù)存放起來
    • 檢查這個表單數(shù)據(jù)是否合法
  3. 如果表單通過了檢測,你會想要:
    • 將表單數(shù)據(jù)傳給你的服務(wù)進(jìn)行處理
    • 將用戶重定向到剛才輸入的數(shù)據(jù)的詳情頁面或者一些概覽頁面
  4. 針對所有其他情形,你會希望表單繼續(xù)被顯示,有時會伴隨一些錯誤信息

然而要實(shí)現(xiàn)上述所有功能并不需要你想象中那么多的代碼。首先,按照下例修改你的 WriteController 代碼:

 <?php
 // 文件名: /module/Blog/src/Blog/Controller/WriteController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Form\FormInterface;
 use Zend\Mvc\Controller\AbstractActionController;
 use Zend\View\Model\ViewModel;

 class WriteController extends AbstractActionController
 {
     protected $postService;

     protected $postForm;

     public function __construct(
         PostServiceInterface $postService,
         FormInterface $postForm
     ) {
         $this->postService = $postService;
         $this->postForm    = $postForm;
     }

     public function addAction()
     {
         $request = $this->getRequest();

         if ($request->isPost()) {
             $this->postForm->setData($request->getPost());

             if ($this->postForm->isValid()) {
                 try {
                     $this->postService->savePost($this->postForm->getData());

                     return $this->redirect()->toRoute('blog');
                 } catch (\Exception $e) {
                     // 某些數(shù)據(jù)庫錯誤發(fā)生了,記錄并且讓用戶知道
                 }
             }
         }

         return new ViewModel(array(
             'form' => $this->postForm
         ));
     }
 }

這個示例代碼應(yīng)該十分簡單明了。首先我們將目前的請求保存到一個本地變量中。然后我們檢查目前的請求是不是一個 POST 請求,如果是,將請求的 POST 數(shù)據(jù)存儲到表單中。如果表單經(jīng)過檢查之后確認(rèn)是有效的,我們就試圖將表單數(shù)據(jù)通過我們的服務(wù)進(jìn)行保存,然后將用戶重定向到路徑 blog。如果在這途中任何時候出現(xiàn)了任何錯誤,我們就再次將表單顯示出來。

現(xiàn)在提交表單的話,我們會遇到下述錯誤:

 Fatal error: Call to undefined method Blog\Service\PostService::savePost() in
 /module/Blog/src/Blog/Controller/WriteController.php on line 33

我們通過拓展 PostService 來修復(fù)這個錯誤,同時不要忘記更改 PostServiceInterface 的簽名!

 <?php
 // 文件名: /module/Blog/src/Blog/Service/PostServiceInterface.php
 namespace Blog\Service;

 use Blog\Model\PostInterface;

 interface PostServiceInterface
 {
     /**
      * Should return a set of all blog posts that we can iterate over. Single entries of the array are supposed to be
      * implementing \Blog\Model\PostInterface
      *
      * @return array|PostInterface[]
      */
     public function findAllPosts();

     /**
      * Should return a single blog post
      *
      * @param  int $id Identifier of the Post that should be returned
      * @return PostInterface
      */
     public function findPost($id);

     /**
      * Should save a given implementation of the PostInterface and return it. If it is an existing Post the Post
      * should be updated, if it's a new Post it should be created.
      *
      * @param  PostInterface $blog
      * @return PostInterface
      */
     public function savePost(PostInterface $blog);
 }

如您所見 savePost() 函數(shù)已經(jīng)被添加,并且需要在 PostService 里被實(shí)現(xiàn)。

 <?php
 // 文件名: /module/Blog/src/Blog/Service/PostService.php
 namespace Blog\Service;

 use Blog\Mapper\PostMapperInterface;

 class PostService implements PostServiceInterface
 {
     /**
      * @var \Blog\Mapper\PostMapperInterface
      */
     protected $postMapper;

     /**
      * @param PostMapperInterface $postMapper
      */
     public function __construct(PostMapperInterface $postMapper)
     {
         $this->postMapper = $postMapper;
     }

     /**
      * {@inheritDoc}
      */
     public function findAllPosts()
     {
         return $this->postMapper->findAll();
     }

     /**
      * {@inheritDoc}
      */
     public function findPost($id)
     {
         return $this->postMapper->find($id);
     }

     /**
      * {@inheritDoc}
      */
     public function savePost(PostInterface $post)
     {
         return $this->postMapper->save($post);
     }
 }

現(xiàn)在我們對 postMapper 做出了假設(shè),所以需要擴(kuò)展我們的 PostMapperInterface 接口以及其實(shí)現(xiàn)。首先我們拓展接口:

 <?php
 // 文件名: /module/Blog/src/Blog/Mapper/PostMapperInterface.php
 namespace Blog\Mapper;

 use Blog\Model\PostInterface;

 interface PostMapperInterface
 {
     /**
      * @param int|string $id
      * @return PostInterface
      * @throws \InvalidArgumentException
      */
     public function find($id);

     /**
      * @return array|PostInterface[]
      */
     public function findAll();

     /**
      * @param PostInterface $postObject
      *
      * @param PostInterface $postObject
      * @return PostInterface
      * @throws \Exception
      */
     public function save(PostInterface $postObject);
 }

然后我們實(shí)現(xiàn) save 函數(shù):

<?php
// 文件名: /module/Blog/src/Blog/Mapper/ZendDbSqlMapper.php
namespace Blog\Mapper;

use Blog\Model\PostInterface;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Adapter\Driver\ResultInterface;
use Zend\Db\ResultSet\HydratingResultSet;
use Zend\Db\Sql\Insert;
use Zend\Db\Sql\Sql;
use Zend\Db\Sql\Update;
use Zend\Stdlib\Hydrator\HydratorInterface;

class ZendDbSqlMapper implements PostMapperInterface
{
   /**
    * @var \Zend\Db\Adapter\AdapterInterface
    */
   protected $dbAdapter;

   /**
    * @var \Zend\Stdlib\Hydrator\HydratorInterface
    */
   protected $hydrator;

   /**
    * @var \Blog\Model\PostInterface
    */
   protected $postPrototype;

   /**
    * @param AdapterInterface  $dbAdapter
    * @param HydratorInterface $hydrator
    * @param PostInterface    $postPrototype
    */
   public function __construct(
      AdapterInterface $dbAdapter,
      HydratorInterface $hydrator,
      PostInterface $postPrototype
   ) {
      $this->dbAdapter      = $dbAdapter;
      $this->hydrator       = $hydrator;
      $this->postPrototype  = $postPrototype;
   }

   /**
    * @param int|string $id
    *
    * @return PostInterface
    * @throws \InvalidArgumentException
    */
   public function find($id)
   {
      $sql    = new Sql($this->dbAdapter);
      $select = $sql->select('posts');
      $select->where(array('id = ?' => $id));

      $stmt   = $sql->prepareStatementForSqlObject($select);
      $result = $stmt->execute();

      if ($result instanceof ResultInterface && $result->isQueryResult() && $result->getAffectedRows()) {
         return $this->hydrator->hydrate($result->current(), $this->postPrototype);
      }

      throw new \InvalidArgumentException("Blog with given ID:{$id} not found.");
   }

   /**
    * @return array|PostInterface[]
    */
   public function findAll()
   {
      $sql    = new Sql($this->dbAdapter);
      $select = $sql->select('posts');

      $stmt   = $sql->prepareStatementForSqlObject($select);
      $result = $stmt->execute();

      if ($result instanceof ResultInterface && $result->isQueryResult()) {
         $resultSet = new HydratingResultSet($this->hydrator, $this->postPrototype);

         return $resultSet->initialize($result);
      }

      return array();
   }

   /**
    * @param PostInterface $postObject
    *
    * @return PostInterface
    * @throws \Exception
    */
   public function save(PostInterface $postObject)
   {
      $postData = $this->hydrator->extract($postObject);
      unset($postData['id']); // Insert 和 Update 都不需要數(shù)組中存在 ID

      if ($postObject->getId()) {
         // ID 存在,是一個 Update
         $action = new Update('posts');
         $action->set($postData);
         $action->where(array('id = ?' => $postObject->getId()));
      } else {
         // ID 不存在,是一個Insert
         $action = new Insert('posts');
         $action->values($postData);
      }

      $sql    = new Sql($this->dbAdapter);
      $stmt   = $sql->prepareStatementForSqlObject($action);
      $result = $stmt->execute();

      if ($result instanceof ResultInterface) {
         if ($newId = $result->getGeneratedValue()) {
            // 每當(dāng)一個值被生成時,將其賦給對象
            $postObject->setId($newId);
         }

         return $postObject;
      }

      throw new \Exception("Database error");
   }
}

在這里 save() 函數(shù)處理了兩種情況:insertupdate 流程。首先我們提取 Post 對象,因?yàn)槲覀冃枰獢?shù)組數(shù)據(jù)來實(shí)現(xiàn) InsertUpdate。然后,我們從數(shù)組中刪除了 id ,因?yàn)閷σ粋€元組進(jìn)行更新的時候,我們不需要更新 id 屬性;同時,我們插入一個新元組的時候也不需要 id 字段,所以兩種情況均不需要 id 這個字段,將其簡單去除即可。

在我們?nèi)コ?id 字段之后,檢查那些動作需要被調(diào)用。如果 Post 對象擁有一個 id 集,我們便創(chuàng)建一個新的 Update 對象,否則我們創(chuàng)建一個 Insert 對象。我們將數(shù)據(jù)傳給合適的 action 然后數(shù)據(jù)會被傳給 Sql 對象,最終進(jìn)行真正的數(shù)據(jù)庫操作。

最后,我們檢查我們是否接收到一個有效的結(jié)果,檢查一下有沒有新產(chǎn)生的 id。如果是的話,我們調(diào)用我們博客的 setId() 函數(shù)并且將對象返回。

讓我們再次提將我們的表單,看看這次會得到什么。

 Catchable fatal error: Argument 1 passed to Blog\Service\PostService::savePost()
 must implement interface Blog\Model\PostInterface, array given,
 called in /module/Blog/src/Blog/Controller/InsertController.php on line 33
 and defined in /module/Blog/src/Blog/Service/PostService.php on line 49

表單,默認(rèn)的時候,會將數(shù)據(jù)以數(shù)組形式傳給你。不過我們的 PostService 卻期待接收到的對象是 PostInterface 的一個實(shí)現(xiàn)。這意味著我們需要找到一個方法來將這個數(shù)組數(shù)據(jù)轉(zhuǎn)換成對象數(shù)據(jù)。如果你還記得上一章節(jié),就會知道要通過 hydrators 實(shí)現(xiàn)。

注意:在更新查詢中,你會注意到我們添加了一個條件語句讓其只更新與給出的 id 匹配的元組: $action->where(array('id = ?' => $postObject->getId())); 你會看見條件是:id equals ?。這個問號代表著 POST 對象的 id。同樣的,你可以添加一個條件語句來更新(或者選擇)所有大于給定 id 的元組: $action->where(array('id > ?' => $postObject->getId())); 這些操作符可以用于所有類型的條件語句:=、>、<>=<=。

讓 Zend\Form 和 Zend\Stdlib\Hydrator 協(xié)調(diào)工作

在我們繼續(xù)前進(jìn)并且將充水器放進(jìn)表單之前,先讓我們對表單的數(shù)據(jù)做一個 dump。這樣做可以讓我們很方便的注意到充水器做的所有變更。根據(jù)下例修改你的 WriteController

 <?php
 // 文件名: /module/Blog/src/Blog/Controller/WriteController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Form\FormInterface;
 use Zend\Mvc\Controller\AbstractActionController;
 use Zend\View\Model\ViewModel;

 class WriteController extends AbstractActionController
 {
     protected $postService;

     protected $postForm;

     public function __construct(
         PostServiceInterface $postService,
         FormInterface $postForm
     ) {
         $this->postService = $postService;
         $this->postForm    = $postForm;
     }

     public function addAction()
     {
         $request = $this->getRequest();

         if ($request->isPost()) {
             $this->postForm->setData($request->getPost());

             if ($this->postForm->isValid()) {
                 try {
                     \Zend\Debug\Debug::dump($this->postForm->getData());die();
                     $this->postService->savePost($this->postForm->getData());

                     return $this->redirect()->toRoute('blog');
                 } catch (\Exception $e) {
                     // 某些數(shù)據(jù)庫錯誤發(fā)生了,記錄并且讓用戶知道
                 }
             }
         }

         return new ViewModel(array(
             'form' => $this->postForm
         ));
     }
 }

做好之后請?jiān)俅翁峤槐韱巍D悻F(xiàn)在應(yīng)該能看到數(shù)據(jù) dump,和下例差不多的情形:

 array(2) {
   ["submit"] => string(16) "Insert new Post"
   ["post-fieldset"] => array(3) {
     ["id"] => string(0) ""
     ["text"] => string(3) "foo"
     ["title"] => string(3) "bar"
   }
 }

現(xiàn)在讓你的字段集將數(shù)據(jù)注水成 Post 對象是非常簡單的。你需要做的事情僅僅是指定注水器和對象原型,如下例所示:

 <?php
 // 文件名: /module/Blog/src/Blog/Form/PostFieldset.php
 namespace Blog\Form;

 use Blog\Model\Post;
 use Zend\Form\Fieldset;
 use Zend\Stdlib\Hydrator\ClassMethods;

 class PostFieldset extends Fieldset
 {
     public function __construct($name = null, $options = array())
     {
         parent::__construct($name, $options);
         $this->setHydrator(new ClassMethods(false));
         $this->setObject(new Post());
         $this->add(array(
             'type' => 'hidden',
             'name' => 'id'
         ));
         $this->add(array(
             'type' => 'text',
             'name' => 'text',
             'options' => array(
                 'label' => 'The Text'
             )
         ));
         $this->add(array(
             'type' => 'text',
             'name' => 'title',
             'options' => array(
                 'label' => 'Blog Title'
             )
         ));
     }
 }

如您所見我們做了兩件事情,我們告知了字段集使用 ClassMethods hydrator,然后我們還告知了它應(yīng)該返回給我們 Blog 模型、不過,當(dāng)你再次提交表單的時候你會注意到什么都沒有改變。我們?nèi)匀恢坏玫綌?shù)組數(shù)據(jù),而不是對象。

這是因?yàn)槭聦?shí)上表單本身不知道自己需要返回一個對象。當(dāng)表單不知道自己要返回什么的時候就會默認(rèn)遞歸使用 ArraySeriazable hydrator。要改變這點(diǎn),我們需要讓我們的 PostFieldset 變成所謂的 base_fieldset

base_fieldset 基本上就告訴了表單“這個表單是關(guān)于我的,請不要操心其他數(shù)據(jù),只操心我就好”。而且當(dāng)表單意識到這個字段集是來真的,它就會乖乖使用字段集提供的 hydrator,并且將我們想要的對象返回出來。修改你的 PostForm 并且將 PostFieldset 設(shè)置成 base_fieldset

 <?php
 // 文件名: /module/Blog/src/Blog/Form/PostForm.php
 namespace Blog\Form;

 use Zend\Form\Form;

 class PostForm extends Form
 {
     public function __construct($name = null, $options = array())
     {
         parent::__construct($name, $options);

         $this->add(array(
             'name' => 'post-fieldset',
             'type' => 'Blog\Form\PostFieldset',
             'options' => array(
                 'use_as_base_fieldset' => true
             )
         ));

         $this->add(array(
             'type' => 'submit',
             'name' => 'submit',
             'attributes' => array(
                 'value' => 'Insert new Post'
             )
         ));
     }
 }

現(xiàn)在再次提交你的表單。你應(yīng)該能看見如下輸出:

 object(Blog\Model\Post)#294 (3) {
   ["id":protected] => string(0) ""
   ["title":protected] => string(3) "foo"
   ["text":protected] => string(3) "bar"
 }

現(xiàn)在你可以回滾你的 WriteController 到它的先前狀態(tài)了,來讓表單數(shù)據(jù)傳給 PostService。

 <?php
 // 文件名: /module/Blog/src/Blog/Controller/WriteController.php
 namespace Blog\Controller;

 use Blog\Service\PostServiceInterface;
 use Zend\Form\FormInterface;
 use Zend\Mvc\Controller\AbstractActionController;
 use Zend\View\Model\ViewModel;

 class WriteController extends AbstractActionController
 {
     protected $postService;

     protected $postForm;

     public function __construct(
         PostServiceInterface $postService,
         FormInterface $postForm
     ) {
         $this->postService = $postService;
         $this->postForm    = $postForm;
     }

     public function addAction()
     {
         $request = $this->getRequest();

         if ($request->isPost()) {
             $this->postForm->setData($request->getPost());

             if ($this->postForm->isValid()) {
                 try {
                     $this->postService->savePost($this->postForm->getData());

                     return $this->redirect()->toRoute('blog');
                 } catch (\Exception $e) {
                     // 某些數(shù)據(jù)庫錯誤發(fā)生了,記錄并且讓用戶知道
                 }
             }
         }

         return new ViewModel(array(
             'form' => $this->postForm
         ));
     }
 }

你現(xiàn)在只需要再次發(fā)送表單就能夠隨心所欲任意添加博客帖子了,不錯!

總結(jié)

在這個章節(jié)中,你學(xué)習(xí)了許多關(guān)于 Zend\Form 組件的知識。你也知道了 Zend\Stdlib\HydratorZend\Form 組件扮演多么重要的角色,并且通過使用這兩個組件你已經(jīng)能為博客模組創(chuàng)建插入表單。

在下一個章節(jié)中,我們會創(chuàng)建更新和刪除程序,從而完成博客模組的所有增刪改查功能。