A co se ani v dokumentaci nedočtete, včetně záplaty na bezpečnostní díru a rady, jak zrychlit odezvu serveru a naopak ji nezbrzdit.
Output buffering umožňuje, aby výstup PHP skriptu (především funkcí
echo
) nebyl okamžitě odeslán do prohlížeče nebo terminálu,
ale byl uchováván v paměti (tj. bufferu). Což se hodí k celé
řadě věcí.
Zabránění vypisování na výstup:
ob_start(); // zapne output buffering
$foo->bar(); // veškerý výstup jde pouze do bufferu
ob_end_clean(); // buffer smaže a ukončí buffering
Zachytávání výstupu do proměnné:
ob_start(); // zapne output buffering
$foo->render(); // výstup jde pouze do bufferu
$output = ob_get_contents(); // obsah bufferu uloží do proměnné
ob_end_clean(); // buffer smaže a ukončí buffering
Dvojici ob_get_contents()
a ob_end_clean()
lze nahradit jedinou funkcí ob_get_clean()
,
z jejíhož názvu se sice vytratilo end
, ale skutečně output
buffering i vypíná:
$output = ob_get_clean(); // obsah bufferu uloží do proměnné a vypne buffering
V uvedených příkladech se obsah bufferu na výstup vůbec nedostal. Pokud
jej naopak na výstup poslat chci, namísto ob_end_clean()
jej
ukončím funkcí ob_end_flush()
. Pro současné získání obsahu bufferu, odeslání na výstup a ukončení
bufferování existuje opět zkratka (i včetně chybějícího
end
v názvu): ob_get_flush()
.
Buffer lze kdykoliv vyprázdnit i bez nutnosti jej ukončit, a to pomocí ob_clean()
(smaže jej) a nebo ob_flush()
(pošle jej na výstup):
ob_start(); // zapne output buffering
$foo->bar(); // veškerý výstup jde pouze do bufferu
ob_clean(); // smažu obsah bufferu, ale buffering zůstává aktivní
$foo->render(); // výstup jde stále do bufferu
ob_flush(); // buffer posílám na výstup
$none = ob_get_contents(); // obsah bufferu je nyní prázdný řetězec
ob_end_clean(); // vypne output buffering
Do bufferu se posílá i výstup zapisovaný na php://output
,
naopak buffery lze obejít zápisem na php://stdout
(nebo do
STDOUT
), což je k dispozici pouze pod CLI, tedy při spouštění
skriptů z příkazové řádky.
Zanoření
Buffery je možné zanořovat, takže zatímco je jeden buffer aktivní,
dalším voláním ob_start()
se aktivuje buffer nový. Tedy ob_end_flush()
a
ob_flush()
neposílají obsah bufferu na výstup, ale do
nadřazeného bufferu. A teprve když žádný nadřazený není, posílá se
obsah na skutečný výstup, tj. do prohlížeče nebo terminálu.
Proto je důležité buffering ukončit, a to i v případě, že v průběhu nastane výjimka:
ob_start();
try {
$foo->render();
} finally { // finally existuje od PHP 5.5
ob_end_clean(); // nebo ob_end_flush()
}
Velikost bufferu
Buffer může také zrychlit
generování stránky tím, že se do prohlížeče nebude odesílat
každé jednotlivé echo
, ale až větší objem dat (například
4kB). Stačí na začátku skriptu zavolat:
ob_start(null, 4096);
Jakmile velikost bufferu překročí 4096 bajtů (tzv.
chunk size
), automaticky se provede flush
, tj. buffer
se vyprázdní a odešle ven. Téhož se dá dosáhnout i nastavením direktivy
output_buffering
.
V CLI režimu se ignoruje.
Ale pozor, spuštění bufferingu bez uvedení velikosti, tedy
prostým ob_start()
, způsobí, že se stránka nebude neodesílat
průběžně, ale až se vykreslí celá, takže server bude naopak působit
velmi líně!
HTTP hlavičky
Output buffering nemá žádný vliv na odesílání HTTP hlaviček, ty se zpracovávají jinou cestou. Nicméně díky bufferingu je možné odeslat hlavičky i poté, co se vypsal nějaký výstup, jelikož se stále drží v bufferu. Ovšem jde o vedlejší efekt, na který neradno spoléhat, protože není jistota, kdy výstup překročí velikost bufferu a odešle se.
Bezpečnostní díra
Při ukončení skriptu se všechny neukončené buffery vypíší na výstup. Což lze považovat za nepříjemnou bezpečnostní díru, pokud si například v bufferu připravujete citlivá data, která nejsou určená pro výstup a dojde přitom k chybě. Řešením je použít vlastní handler:
ob_start(function () { return ''; });
Handlery
Na output buffering lze navázat vlastní handler, tj. funkci, která obsah paměti zpracuje před odesláním ven:
ob_start(
function ($buffer, $phase) { return strtoupper($buffer); }
);
echo 'Ahoj';
ob_end_flush(); // na výstup se dostane AHOJ
I funkce ob_clean()
nebo ob_end_clean()
vyvolají
handler, ale výstup zahodí a ven neposílají. Přičemž handler může
zjistit, která funkce je volána a reagovat na to. Používá se k tomu druhý
parametr $phase
, což je bitová maska (od PHP 5.4):
PHP_OUTPUT_HANDLER_START
při otevření bufferuPHP_OUTPUT_HANDLER_FINAL
při ukončení bufferuPHP_OUTPUT_HANDLER_FLUSH
při voláníob_flush()
(ale nikolivob_end_flush()
neboob_get_flush()
)PHP_OUTPUT_HANDLER_CLEAN
při voláníob_clean()
,ob_end_clean()
aob_get_clean()
PHP_OUTPUT_HANDLER_WRITE
při automatickémflush
Fáze start, final a flush (resp. clean) mohou klidně nastat současně,
rozliší se pomocí binárního operátoru &
:
if ($phase & PHP_OUTPUT_HANDLER_START) { ... }
if ($phase & PHP_OUTPUT_HANDLER_FLUSH) { ... }
elseif ($phase & PHP_OUTPUT_HANDLER_CLEAN) { ... }
if ($phase & PHP_OUTPUT_HANDLER_FINAL) { ... }
Fáze PHP_OUTPUT_HANDLER_WRITE
nastává jen tehdy, pokud má
buffer velikost (chunk size
) a ta byla překročena. Jedná se tedy
o zmíněný automatický flush. Jen pozor, konstanta
PHP_OUTPUT_HANDLER_WRITE
má hodnotu 0, proto nelze použít
bitový test, ale:
if ($phase === PHP_OUTPUT_HANDLER_WRITE) { .... }
Handler nemusí podporovat všechny operace. Při aktivaci funkcí
ob_start()
lze jako třetí parametr uvést bitovou masku
podporovaných operací:
PHP_OUTPUT_HANDLER_CLEANABLE
– lze volat funkceob_clean()
a souvisejícíPHP_OUTPUT_HANDLER_FLUSHABLE
– lze volat funkciob_flush()
PHP_OUTPUT_HANDLER_REMOVABLE
– buffer lze ukončitPHP_OUTPUT_HANDLER_STDFLAGS
– je kombinací všech tří flagů, výchozí chování
Tohle se týká i bufferingu bez vlastního handleru. Například pokud chci
zachytávat výstupu do proměnné, nenastavím flag
PHP_OUTPUT_HANDLER_FLUSHABLE
a buffer tak nebude možné (třeba
omylem) poslat na výstup funkcí ob_flush()
. Nicméně lze tak
učinit pomocí ob_end_flush()
nebo ob_get_flush()
,
takže to poněkud ztrácí smysl.
Obdobně by měla absence flagu PHP_OUTPUT_HANDLER_CLEANABLE
zamezit mazání bufferu, ale opět to nefunguje.
A nakonec absence PHP_OUTPUT_HANDLER_REMOVABLE
činní buffer
uživatelsky neodstranitelný, vypne se až při ukončení skriptu. Příkladem
handleru, který je vhodné takto nastavit, je ob_gzhandler
,
který komprimuje výstup a tedy snižuje objem a zvyšuje rychlost datového
přenosu. Jakmile se tento buffer otevře, odešle HTTP hlavičku
Content-Encoding: gzip
a veškerý další výstup musí být
komprimovaný. Odstranění bufferu by rozbilo stránku.
Správné použití je tedy:
ob_start(
'ob_gzhandler',
16000, // bez chunk size by server data neodesílal průběžně
PHP_OUTPUT_HANDLER_FLUSHABLE // ale ne removable nebo cleanable
);
Komprimaci výstupu můžete aktivovat také direktivou zlib.output_compression
,
která zapne buffering s jiným handlerem (netuším, v čem konkrétně se
liší), bohužel chybí příznak, že má být neodstranitelný. Protože je
vhodné komprimovat přenos všech textových souborů, nejen v PHP
generovaných stánek, je lepší kompresi aktivovat přímo na straně HTTP
serveru.
Komentáře
Petr Soukup #1
Guláš v tom dělá ještě nginx, který má zase svůj buffering. Při testování v prohlížeči to tak nemusí vůbec reagovat na flush, protože to bufferuje webserver.
Martin Mach #2
„Buffer může také zrychlit generování stránky (nemám to podložené měřením, ale zní to logicky) tím, že se do prohlížeče nebude odesílat každé jednotlivé echo“ – V tomhle případě ještě bude hrát roli systémový buffer, tj. i bez output bufferingu nedojde k tomu, že by každé echo vyslalo data skutečně „do prohlížeče“. Pokud ho k tomu PHP nějakým způsobem nenutí (což by bylo dost neefektivní), k jeho flushnutí dojde taky stejně až po dosažení určité velikosti. IMHO tam nějaké výkonnostní zlepšení bude, ale spíše z nějakého jiného důvodu.
Vojtech Kurka #3
Je dobré nad tím přemýšlet v kontextu HTTP serveru. Nejlepší varianta je vygenerovat response rychle v jednom bufferu (buď PHP buffer, nebo buffer na straně HTTP serveru) a odeslat do prohlížeče najednou. Pokud se totiž podaří celou gzipovanou HTTP odpověď nacpat do jednoho IP packetu, neexistuje nic lepšího.
Pokud má server problém vygenerovat celou odpověď rychle, nebo je stránka extrémně dlouhý seznam, pak může postupné odesílání pomoct. Pro 99% webových aplikací to ale není dobrá cesta.
v6ak #4
Při kompresi pozor na BREACH. Ja to řeším typicky tak, že komprimuju jen statické věci, jako třeba CSS a JS. (Ty mimochodem mohou být předkomprimované.) Případně by někdy šlo komprimovat data nepřihlášenému uživateli, ale i ten může mít v session nějaké tajnosti promítané do stránky, např. CSRF token.
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.