最近更新: 2013-02-07

CommonGateway 初步第二篇 - JSON 的處理與資料上傳

接續「CommonGateway 初步」的內容,說明 CommonGateway 如何處理 JSON 文件與資料上傳。

HTTP協定關於要求與提交文件型態的規範

HTTP協定定義了兩項關於文件型態的標頭(header),一為 Accept,二為 Content-Type

在傳統的 REST-like 應用中,這兩個標頭的原始定義並沒有被廣泛採用。大多數程式人員都是透過自定的額外參數來決定服務端該回應什麼文件型態給客戶端。 但在 RESTful 服務的設計場合中,文件型態的交換方式則回歸到這兩個標頭的原始定義了。 所以程式人員必須先分清楚這兩個標頭的使用場合。

Accept

Accept 用於宣告客戶端要求服務端回應的文件型態。

The Accept request-header field can be used to specify certain media types which are acceptable for the response.
HTTP/1.1: 14.1 Accept

Accept 可以一次宣告多種文件型態,服務端會按照本身可回應的文件型態與客戶端宣告的期望值決定該回應什麼文件給客戶。例如瀏覽器通常會宣告 Accept: text/html;q=0.9,application/json;q=0.8;表示瀏覽器希望 Web 伺服器回應 HTML 或 JSON 文件給它,其中 HTML 型態優先於 JSON 型態 (比分 0.9 > 0.8)。如果 Web 伺服器的應用項目支援回應這兩種文件型態,則應該優先回應 HTML 文件給客戶。如果服務端只實作了 JSON 文件型態的回應內容,則可以忽略 HTML 的請求,徑自回應 JSON 文件給瀏覽器。

不過在大多數的 RESTful 應用中,客戶端的 Accept 通常只會宣告一種文件型態。

Content-Type

Content-Type 用於宣告遞送給對方的文件型態。此標頭用途較廣,客戶端用於宣告提交給服務端的文件型態,服務端用於宣告回覆客戶端的文件型態。

The Content-Type entity-header field indicates the media type of the entity-body sent to the recipient
HTTP/1.1: 14.17 Content-Type

舉例來說,瀏覽器上的表單(FORM)要提交表單內容給服務端程式,預設的文件型態會是 multipart/form-data 。服務端的 PHP 程式若要動態地產生一份 JPEG 圖檔內容回應給瀏覽器,則必須加上一行 header('Content-Type: image/jpeg');

在傳統的 Web 應用中,Content-Type 的使用頻率已然不低。在 RESTful 架構中,它的使用頻率就更高了。許多 RESTful 服務供應者直接限定它只接受 JSON 或 XML 型態的提交文件。若用傳統的表單遞交方法,根本無法提交內容給這類 RESTful 服務。

在 CommonGateway (以下簡稱 CG) 中,會儘量幫程式人員預先處理好這些事,讓 PHP 程式人員可以延續傳統的 Web 應用寫作經驗。

RESTful 客戶端

為了簡化本文內容,本文不會告訴你如何設計一個 RESTful 客戶端。 請你自己找一個來用。 Firefox 的用戶可以考慮安裝擴充套件「RESTClient」。RESTClient 的使用畫面如下圖所示:

RESTClient 使用畫面

本文後續內容將使用這些 RESTful 客戶端操作。

如果你想知道如何以 PHP 上傳資料給 RESTful 服務,可參考「PHP 透過 HTTP POST 方法上傳資料與檔案給 RESTful 服務」。

回應 JSON 文件給客戶端

CG 遵循 HTTP 協定內容,要求容戶端以標頭 Accept 描述它希望取得的文件型態。這也是大部份 RESTful 服務供應者所要求的使用方式。

本文接續上一篇的進度。首先,宣告 Accept: application/json 標頭,請服務端回應 JSON 文件。

先不修改任何程式碼,用 RESTClient 送出請求,看看會回應什麼。

  • Headers: Accept: application/json
  • Method: GET
  • URL: index.php/book
    (此處省略了前面的位址)

Response Body (Raw):

Template is missing. Missing views/book/index.pjs.

雖然第一篇中我已經實作了 index.phtml 而且也確實會回應內容給客戶端。但這次我加上了 Accept: application/json ,所以 CG 這次載入視圖時不是找 index.phtml 而是找 index.pjs 。

在第一篇中,我已經知道在 index 視圖中,會有兩個資料,分別是 $books$time。我決定 JSON 文件中只需要用 $books 的內容就好,所以我簡單地調用 json_encode() 產生 $books 的 JSON 內容,回應給客戶端。 index.pjs 實作內容如下。

<?php
echo json_encode($books);
?>

CG 會依 Accept 載入對應的視圖,也會自動添加正確的 Content-Type 文件型態宣告。所以我不用寫出 header('Content-Type: application/json');

RESTClient 再送出一次請求。現在我就會看到預期的 $books 內容了,而且是 JSON 文件。

[{"isbn":"123","title":"book 123"},{"isbn":"456","title":"Book XYZ"}]

依樣畫葫蘆,接著實作 get.pjs 。

<?php
echo json_encode($book);
?>

RESTClient 送出請求內容。

  • Headers: Accept: application/json
  • Method: GET
  • URL: index.php/book/123

得到下列 JSON 內容。

{"isbn":"123","title":"book 123"}

只要你實作了 *.pxml 的內容,使用者就可索取 XML 文件內容;實作了 *.ppdf ,使用者就可取回 PDF 文件內容,以下類推。只要你實作了與 Accept 相對應的文件型態視圖(View),CG 就會自動載入它,並將控制項(Controller)回傳的資料(Model)放入其中,最後將視圖所呈現的內容回應給客戶。

既然這項工作決定的是最終呈現給客戶的內容,那麼 CG 就讓程式人員把焦點放在視圖的設計工作,而不用更動控制項或資料模型。

提交文件給服務端

RESTful 提交文件給服務端的方法有兩種。一為 POST ,對應資料建立行為。二為 PUT ,對應資料更新行為。

簡單的 Book 表單

我先很快速地實作一個 Book 表單,以便我先用傳統的表單新增書籍資料。

<?php
class BookForm {
    function edit() {
        return;
    }
}
?>
<html>
<form action=<?=$_SERVER['SCRIPT_NAME'].'/book'?> method="POST">
    <label for="isbn">ISBN: </label>
    <input type="text" id="isbn" name="isbn" />
    <br/>
    <label for="title">Title: </label>
    <input type="text" id="title" name="title" />
    <br/>
    <button type="submit">Submit</button>
</form>
</html>

表單重點在於以 POST 方法提交表單內容給 index.php/book 。

在某些 RESTful 服務中,其實不提供這類傳統表單。它們僅提供 API ,而操作介面與表單等 UI 內容一律讓程式人員自己設計。

POST 實作

我想沒有必要再一步步引導了。我直接在 controllers/book.php 中添加 post() 內容。

class Book {
    // ... 省略其他部份
    function get($isbn) {
        /*
        $book = $query->from('book')->
                    ->where(array('isbn' => $isbn))
                    ->select();
        */
        $data_filepath = '/tmp/' . $isbn;
        if (!file_exists($data_filepath)) {
            HttpResponse::exception(HttpResponse::NOT_FOUND);
        }

        $this->book = unserialize(file_get_contents($data_filepath));
        return;
    }

    function post() {
        //var_dump($_POST);
        //return false;
        $book = new BookModel($_POST['isbn'], $_POST['title']);
        file_put_contents('/tmp/' . $book->isbn, serialize($book));
        return $book;
    }
}

$_POST 中取得表單內容這件事,對 PHP 程式人員是常識。 在此還不打算用資料庫,所以我只是很簡單地把書籍資料儲存在個別的檔案中,以 ISBN 為檔名。

我再實作一個 views/book/post.phtml ,以便回應新增成功的訊息。 不過在 RESTful 架構中,一般只需要回應狀態碼 200 (Ok) 或 201 (Created) 表示成功即可。錯誤通常回應狀態碼 400 (Bad request)、406 (Not acceptable) 或 409 (Conflict) 。

<html>
<p>Create new book:
<a href="<?=$_SERVER['SCRIPT_NAME'].'/book/'.$book->isbn?>">
<?=$book->title?></a>.
</p>
</html>

這個回應的網頁內容提供一個連結指向新增的書籍資料。

以傳統表單新增書籍資料

用瀏覽器開啟 index.php/bookForm/edit ,出現書籍資料表單。 填入 ISBN 與 Title ,然後送出。例如填寫 ISBN: 123456,Title: BookXyz 。 回應:

Create new book: BookXyz.

PHP 程式人員都很熟悉這個操作了,不作說明。

以 JSON 文件新增書籍資料

接著,我要如何像一般 RESTful 服務讓使用者用 JSON 文件遞交將要新增的書籍資料呢?

答案是,什麼都不必寫。

以 RESTClient 提交內容給 index.php/book :

  • Headers: Content-Type: application/json
  • Method: POST
  • URL: index.php/book
  • Body: {"isbn": 789, "title": "NO 789"}

<html>
<p>Create new book:
<a href="/rock/cg/index.php/book/789">
NO 789</a>.
</p>
</html>

因為這次我用 POST 方法提交的文件型態是 JSON ,所以我要用 Content-Type 告訴服務端上傳的文件是 JSON 。當然文件內容(Body)也必須符合 JSON 格式。

CG 在收到資料後,會根據 Content-Type 的宣告,從 php://input 讀取客戶端上傳的 JSON 文件內容,解碼它(CG目前僅支援傳統表單和JSON文件上傳)。最後把解碼結果保存在 $_POST$_REQUEST 變數中。

雖然這次的資料提交方式完全是 RESTful 的操作方式。但 CG 在不改變 PHP 語義的前提下,把客戶端提交的內容不著痕跡地轉變為 PHP 傳統的處理內容。所以 book.php 的內容不需要修改任何地方,就能按過去的表單處理方式處理掉 RESTful 的請求。

PUT 實作

對 CG 而言,PUT 和 POST 的處理策略相同。一樣地根據 Content-Type 將上傳資料保存在 $_POST$_REQUEST 變數 (需要更新 CG 到 r60 以上版本才會這樣做)。

在 controllers/book.php 中實作 put() 內容如下:

class Book {
    /**
    @resource request
     */
    var $book;

    // ... 其他內容省略

    function put($isbn = false) {
        if (empty($isbn))
            HttpResponse::exception(HttpResponse::BAD_REQUEST);
        //global $HTTP_RAW_POST_DATA; var_dump($HTTP_RAW_POST_DATA);
        //var_dump($GLOBALS['HTTP_RAW_POST_DATA']);
        //var_dump($this->book); // CG inject.
        //var_dump($_POST);
        //var_dump($_REQUEST);
        //return false;
        $data_filepath = '/tmp/' . $isbn;
        if (!file_exists($data_filepath)) {
            HttpResponse::exception(HttpResponse::NOT_FOUND);
        }
        $book = unserialize(file_get_contents($data_filepath));
        $book->title = $_REQUEST['title'];
        file_put_contents($data_filepath, serialize($book));
        return $book;
    }
}

PUT 用於更新現有的書籍資料,所以需要一個參數 $isbn 告知它要更新的目標資料。 通常這也是一般 RESTful 服務中, POST 和 PUT 方法的差異:POST 不用 ID,而 PUT 要。

當客戶端沒有指定 ISBN 時,就按 HTTP 的語義,回應狀態碼 400 (Bad request) 。此外回應 404 (Not found) 也符合 HTTP 語義。這由設計者自己決定。

其次,此處有一個特殊的修改。我在 Book 類的屬性成員 $book 前加上了一個文件區塊,並在裡面寫了一個 @resource request 注記(annotation)。這個注記讓 CG 知道它可以將客戶端上傳的資料,注入這個屬性成員中。在 CG r60 版本之前,PUT 方法只能透過這個途徑取得客戶端上傳資料。

Book 中不必實作任何關於 $this->book 的內容,而是由 CG 透過屬性注入的途徑賦予 $this->book 實際內容。所以在 put() 就可以透過 $this->book 取得使用者上傳資料。

這個程式技巧被稱為「屬性注入」,是 IoC 模式的一種技巧。可參考我寫的「PHP 自訂註記與屬性注入功能」。想進一步了解 IoC 實作的人,參考「PHP 實作 IoC/DI 設計模式」。

最後,加入 PUT 的視圖 views/book/put.phtml 。

<html>
<p>Update book.
Back to <a href="<?=$_SERVER['SCRIPT_NAME'].'/book/'.$book->isbn?>">
<?=$book->title?></a>.
</p>
</html>

在這個視圖中,有一個與 CG r59 補強有關的用法。先前,當控制項回傳資料的型態是 object 時, CG 會賦予其變數名稱為 $model。在 r59 的補強中, CG 還會賦予控制項同名名稱。

在此例中,控制項的 put() 方法回傳的 $book 是一個 object ,又控制項名稱為 book 。故 CG 會賦予這個資料兩個變數名稱,一為 $model ,另一為 $book

最後,以 RESTClient 提交內容給 index.php/book/789 :

  • Headers: Content-Type: application/json
  • Method: PUT
  • URL: index.php/book/789
  • Body: {"title": "NO 789 2nd edition"}

Response Body (Raw):


<html>
<p>Update book.
Back to <a href="/rock/cg/index.php/book/789">
NO 789 2nd edition</a>.
</p>
</html>

結語

POST 是傳統的 Web 應用上傳方法中,最常被應用的途徑。甚至發展出了許多技巧。但在 RESTful 應用場合,它的作用很單純。傳統的 Web 程式人員通常要花一些時間改變過去的設計習慣。

CG 是針對 PHP 熟練者的 RESTful MVC 容器。它的目標是一方面消除HTML表單與 RESTful 的資料來源差異性,另一方面保留原本的程式寫法與工具,而不讓程式人員牽就 framework 所提供的方法。所以它會儘可能地在符合 PHP 語義的前提下,將資料轉變為 PHP 程式人員熟悉的傳統用法;例如用 $_POST 取得使用者上傳的文件內容。

CG 所要展現的 RESTful 設計概念,在這兩篇文章中已經說明的差不多了。接下來會是一些比較零碎的主題。例如 Config 、更多的資源注入內容、基於 XForm 設計概念的 UI 設計方式以及搭配資料庫函數庫。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/21334380.html