最近更新: 2012-07-30

PHP 透過 HTTP POST 方法上傳資料與檔案給 RESTful 服務

本文說明三種 PHP 透過 HTTP POST 方法上傳資料與檔案給 RESTful 服務的方法。 包括上傳 JSON 文件給 RESTful 服務的情形。

HTTP Wrapper

PHP5 內建一種它稱為 Wrapper 的機制,讓使用者可以透過一致的檔案串流函數存取不同協定的資源。 這個機制,也可以讓使用者透過 HTTP POST 方法調用 RESTful 服務,只是不好用。

使用範例如下:

#!/usr/bin/phpunit
<?php
require_once 'PHPUnit/Framework.php';

function do_stream_post_request($url, $postdata = false, $files = false) {
    $boundary = md5(time());

    $data = "--${boundary}\r\n";
    if ($postdata) {
        foreach($postdata as $k => $v) {
            $data .= "--${boundary}\r\n";
            $data .= "Content-Disposition: form-data; name="${k}"\n\n${v}\n";
        }
    }
    $data .= "--${boundary}\n";

    if ($files) {
        foreach ($files as $k => $content) {
            $data .= "Content-Disposition: form-data; name="${k}"; filename="${k}"\r\n";
            $data .= "Content-Type: application/octet-stream\r\n";
            $data .= "Content-Transfer-Encoding: binary\r\n\r\n";
            $data .= $content . "\r\n";
        }
        $data .= "--${boundary}--\n\n";
    }

    $params = array(
    'http' => array(
        'method' => 'POST',
        'header' =>
            "Accept: */*\r\n" .
            "Content-Type: multipart/form-data; boundary=${boundary}\r\n",
        'content' => $data
    ));

    $ctx = stream_context_create($params);
    $response = @file_get_contents($url, FILE_TEXT, $ctx);
    // if status is not 200, here will throw an exception.
    return $response;
}

class StreamPostTest extends PHPUnit_Framework_TestCase {
    var $uri = 'http://localhost/rock/post/post-dump.php';

    public function test_post_by_stream()
    {
        $content = do_stream_post_request($this->uri,
            array(
            'field1' => 'hello world.',
            'field2' => 'post form by stream.'
            ),
            array(
            '1.png' => file_get_contents('1.png')
            )
        );

        echo $content;
        $this->assertContains('post form by stream.', $content);
        $this->assertContains('hello world.', $content);
        $this->assertContains('[name] => 1.png', $content);
        $this->assertContains('[size] => 33108', $content);
    }
}
?>

這個方法使用時的準備工作太麻煩了。 再者,它也不能得知服務端傳回的 HTTP 狀態碼與文件型態。 當目標的 RESTful 服務所傳回的狀態碼與文件型態具有特定用途時,這個方法派不上用場。

如果你只是要透過 HTTP GET 方法調用 RESTful 服務,那麼 HTTP Wrapper 比較簡便。 但若要透過 HTTP POST 方法調用的話,大多數人還是會選擇使用 curl 。

curl - POST

PHP 最常用的資料上傳方法。curl 內部會處理很多瑣事,使用者只需要填對資料欄位。

使用範例如下:

#!/usr/bin/phpunit
<?php
require_once 'PHPUnit/Framework.php';

class CurlPostFormTest extends PHPUnit_Framework_TestCase {
    var $uri = 'http://localhost/rock/post/post-dump.php';

    protected function setUp() {
        $this->hr = curl_init(); // HttpRequest
        curl_setopt($this->hr, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->hr, CURLOPT_URL, $this->uri);
    }

    public function test_get() {
        $opts = array(
            CURLOPT_HTTPGET => true
        );

        curl_setopt_array($this->hr, $opts);
        $content = curl_exec($this->hr);
        $status = curl_getinfo($this->hr, CURLINFO_HTTP_CODE);
        $content_type = curl_getinfo($this->hr, CURLINFO_CONTENT_TYPE);

        $this->assertEquals($status, 200);
    }

    public function test_post_by_curl() {
        $data =array(
            'field1' => 'hello world.',
            'field2' => 'post form by curl.',
            // 'pic.png' => '@1.png'  # 說明2. <= PHP 5.4.
            'pic.png' => curl_file_create('1.png')  #說明3. >= PHP 5.5.
        );
        $opts = array(
            CURLOPT_POST    => true,  #說明1
            CURLOPT_POSTFIELDS => $data
        );

        curl_setopt_array($this->hr, $opts);
        $content = curl_exec($this->hr);
        $status = curl_getinfo($this->hr, CURLINFO_HTTP_CODE);
        $content_type = curl_getinfo($this->hr, CURLINFO_CONTENT_TYPE);

        echo $content;
        $this->assertEquals($status, 200);
        $this->assertEquals($content_type, 'text/html');
        $this->assertContains('post form by curl.', $content);
        $this->assertContains('hello world.', $content);
        $this->assertContains('[name] => 1.png', $content);
        $this->assertContains('[size] => 33108', $content);
    }
}
?>

使用說明:

  1. 啟用 CURLOPT_POST 。curl 會根據這個設定,在內部自動啟用必要的處理程序。
  2. CURLOPT_POSTFIELDS 陣列的欄位值,如果第一個字元是 @ 的話,就代表這是要上傳的檔案內容。 @ 後面的內容是檔案路徑,實務上應該使用絕對路徑以保安全。
  3. 由於檔案存取的安全性問題, PHP 5.5 之後的版本關閉了以 @ 標示上傳檔案的途徑。要改用函數 curl_file_create() 。參考 PHP curl post file

curl - CUSTOMREQUEST

有些 RESTful 服務只接受 JSON 型態的文件。 雖然同樣是透過 HTTP POST 上傳,但是此時 curl 的使用選項不能用 CURLOPT_POST ,而必須改用 CURLOPT_CUSTOMREQUEST 。 curl 若啟用了 CURLOPT_POST 選項,它就一定會把上傳的文件型態設為 multipart/form-data , 並將 CURLOPT_POSTFIELDS 欄位的內容按 multipart/form-data 的形式編碼。 對於只接受 JSON 文件的 RESTful 服務來說,這就出局了。

但若 curl 使用選項 CURLOPT_CUSTOMREQUEST 並指派其值為 POST 的話, 那麼 curl 就不會在內部自動處理文件編碼與型態,而是完全按照使用者指定的內容上傳。 故可用來上傳 JSON 文件給 RESTful 服務。

使用範例如下:

#!/usr/bin/phpunit
<?php
require_once 'PHPUnit/Framework.php';

class CurlPostJsonTest extends PHPUnit_Framework_TestCase {
    var $uri = 'http://localhost/rock/post/post-dump.php';

    protected function setUp() {
        $this->hr = curl_init(); // HttpRequest
        curl_setopt($this->hr, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($this->hr, CURLOPT_URL, $this->uri);
    }

    public function test_post_json_by_curl() {
        $data = array(
            'field1' => 'hello world.',
            'field2' => 'post form by curl customrequest.',
            '1.png'  => base64_encode(file_get_contents('1.png')) #說明4
        );
        $opts = array(
            CURLOPT_CUSTOMREQUEST => 'POST', # 說明1
            CURLOPT_HTTPHEADER => array(
                'Content-Type: application/json' #說明2
            ),
            CURLOPT_POSTFIELDS => json_encode($data) #說明3
        );

        curl_setopt_array($this->hr, $opts);
        $content = curl_exec($this->hr);
        $status = curl_getinfo($this->hr, CURLINFO_HTTP_CODE);
        $content_type = curl_getinfo($this->hr, CURLINFO_CONTENT_TYPE);

        echo $content;
        $this->assertEquals($status, 200);
        $this->assertContains('post form by curl customrequest.', $content);
        $this->assertEquals($content_type, 'text/html');
        $this->assertContains('hello world.', $content);
    }
}
?>

使用說明:

  1. CURLOPT_CUSTOMREQUEST => 'POST'CURLOPT_POST => true 兩者的差別在於 CURLOPT_CUSTOMREQUEST 不會在內部進行文件內容編碼, 而 CURLOPT_POST 內部則一律按 multipart/form-data 的形式編碼。
  2. CURLOPT_CUSTOMREQUEST 不會預設上傳文件的型態,故使用者必須明確告知你將上傳什麼文件型態。 在此案例中,要上傳 JSON 文件(application/json)。
  3. 啟用 CURLOPT_CUSTOMREQUEST 後,CURLOPT_POSTFIELDS 的內容要自己按照文件形態要求的形式格式化。 在此案例要上傳 JSON 文件,故調用 json_encode() 處理資料內容。
  4. 注意,JSON 文件不接受非 UTF-8 字元的內容。 若要夾帶檔案的話,必須選擇一個編碼結果全為可讀字元的編碼法處理檔案內容。 base64_encode() 是不錯的選擇。
相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/20068918.html