0x00 前言
在平時使用框架的時候我發現我們可以隨意的設定 HTTP 頭,而不用擔心之前的程式是否輸出過內容。但在 PHP 官網手冊中設定 HTTP 頭函式 header
和設定 Cookie 函式 setcookie
卻有著如下警告:
請注意 header() 必須在任何實際輸出之前呼叫,不管是普通的 HTML 標籤,還是檔案或 PHP 輸出的空行,空格。
setcookie() 定義了 Cookie,會和剩下的 HTTP 頭一起傳送給客戶端。 和其他 HTTP 頭一樣,必須在指令碼產生任意輸出之前傳送 Cookie(由於協議的限制)。 請在產生任何輸出之前(包括 和 或者空格)呼叫本函式。
0x01 為什麼輸出內容後就不可以修改 HTTP 頭?
原因其實很簡單,因為 HTTP 響應報文結構如下:
我們可以看見響應頭是在響應體前面的,而 PHP 的任何輸出都將屬於響應體。也就是說一旦 PHP 輸出內容,HTTP 響應頭便已經傳送給客戶端了。此時木已成舟就算再用 header
函式設定頭已經沒有用了,因為 HTTP 頭都發給客戶端了/(ㄒoㄒ)/~~。
0x02 框架是怎麼解決這個問題的?
很多用上框架的同學就幾乎不會遇到這種情況,想設定 HTTP 頭的時候就直接呼叫內建函式或框架的方法設定就 OK 了,從來都不用管之前有沒有輸出過內容。我們能夠這樣做依靠的就是:輸出緩衝區。輸出緩衝區的作用是將 PHP 的輸出內容快取在一塊記憶體中,當緩衝區的記憶體滿了或者程式執行完畢後便會將緩衝區的內容傳送給客戶端。這樣做的主要原因是在 Web 場景裡通過 Socket 一個個位元組的傳送資料比一塊塊的傳送資料效率要低,所以採用輸出緩衝區,便能將資料一塊塊的傳送提高效能。當然這不是解決這個問題最關鍵的部分,最關鍵部分的是 PHP 提供了一系列的輸出控制函式讓我們能夠控制輸出快取區!點此檢視<輸出控制函式>官網手冊
解決此問題的重要函式 ob_start
,作用如下:
此函式將開啟輸出緩衝。當輸出緩衝啟用後,指令碼將不會輸出內容(http 頭除外),需要輸出的內容被儲存在內部緩衝區中。
注意: ob_start
的 chunk_size
引數,預設為 0,即緩衝區不限制大小,便不會因為輸出內容長度過長而刷送
我們使用的框架一般都會在使用者程式碼執行前就呼叫了這個函式,因此之後的任何的輸出語句都不會真正輸出給客戶端而是保留在輸出快取區中,所以我們便可以隨意的設定 HTTP 頭,而不用擔心之前是否已經有內容輸出。
0x03 輸出緩衝區的更多應用
調整內容的輸出順序
try {
ob_start();
$response = $this->process($this->container->get('request'), $response);
} catch (InvalidMethodException $e) {
$response = $this->processInvalidMethod($e->getRequest(), $response);
} finally {
$output = ob_get_clean();
}
if (!empty($output) && $response->getBody()->isWritable()) {
$outputBuffering = $this->container->get('settings')['outputBuffering'];
if ($outputBuffering === 'prepend') {
// prepend output buffer content
$body = new Http\Body(fopen('php://temp', 'r+'));
$body->write($output . $response->getBody());
$response = $response->withBody($body);
} elseif ($outputBuffering === 'append') {
// append output buffer content
$response->getBody()->write($output);
}
}
複製程式碼
這段程式碼是在 Slim 框架中複製來的,在 Slim 中有個配置項叫 outputBuffering
,能夠控制非 response
的輸出內容在 response
之前、之後或不顯示。實現這個功能主要利用的函式就是 ob_start
和 ob_get_clean
,下面有個簡易的示例:
<?php
ob_start();
echo 'world';
$str = ob_get_clean();
echo 'hello ' . $str;
// 輸出 hello world
複製程式碼
忽略 include 的輸出
有時候我們想通過 include
包含並執行一個指令碼但卻不希望輸出指令碼的內容,我們可以這樣處理:
ob_start();
include 'file.php';
ob_end_clean();
複製程式碼
0x04 總結
嗯哼,在享受框架的方便的同時最好還是想多一步思考背後的原因,能夠帶來更多的收穫!另外文章中若出現錯誤,希望大家能夠指出,若有疑問可以互相討論:-D。