Největší programátorský evergreen jsou zmatky a nejasnosti
kolem escapování. Neznalost způsobuje, že nejtriviálnější metody
narušení webových stránek, jako třeba Cross Site Scripting (XSS) nebo SQL
injection, patří bohužel mezi nejrozšířenější.
Escapování je náhrada znaků majících v daném kontextu speciální
význam za jiné odpovídající sekvence.
Příklad: do řetězce ohraničeného uvozovkami chceme zapsat uvozovky.
Jelikož uvozovky mají v kontextu řetězce speciální význam a jejich
prosté zapsání by bylo chápáno jako ukončení řetězce, je potřeba je
zapsat jinou odpovídající sekvencí. Jakou přesně určují pravidla
kontextu.
Předpoklady
Každá escapovací funkce předpokládá, že vstupem je vždy surový řetězec v určitém
kódování (znakové sadě).
Ukládat třeba do databáze řetězce již předem escapované pro HTML
výstup a podobně je zcela kontraproduktivní.
S jakými kontexty se
setkáváme?
Jak bylo řečeno, escapování převádí znaky mající v určitém
kontextu speciální význam. Pro každý kontext se používají jiné
escapovací funkce. Tato tabulka je pouze orientační, je nutné si
přečíst poznámky níže.
Vysvětlení k následujícím poznámkám:
- řada kontextů má své podkontexty a v nich se escapování liší.
Nebude-li řečeno jinak, je uvedená escapovací funkce použitelná plošně
bez dalšího rozlišování podkontextů.
- pod pojmem obvyklá znaková sada se rozumí znaková sada s 1bajtovým nebo UTF-8 kódováním
HTML
V HTML kontextech mají souhrnně speciální význam znaky
< & " '
a odpovídající sekvence jsou
< & " '
. Výjimkou je ovšem HTML
komentář, kde má speciální význam jen dvojice --
.
K escapování se používá:
$s = htmlspecialchars($s, ENT_QUOTES);
Funguje s libovolnou obvyklou znakovou sadou. Ale nezohledňuje podkontext
HTML komentářů (tj. neumí nahradit dvojici --
za něco
jiného).
Reverzní funkce:
$s = html_entity_decode($s, ENT_QUOTES, 'UTF-8');
XML / XHTML
XML 1.0 se od HTML liší v tom, že zakazuje použití kontrolních znaků
C0 (a to včetně zápisu v podobě entity) s výjimkou tabulátoru,
odřádkování a mezery. XML 1.1 tyto zakázané znaky s výjimkou
NUL
v podobě entit naopak povoluje a dále přikazuje kontrolní
znaky C1 s výjimkou NEL
taktéž zapisovat jako entity. Dále
v XML má speciální význam sekvence ]]>
, proto je třeba
jeden z těchto znaků také escapovat.
Pro XML 1.0 a libovolnou obvyklou znakovou sadu tak lze použít:
$s = preg_replace('#[\x00-\x08\x0B\x0C\x0E-\x1F]+#', '', $s);
$s = htmlspecialchars($s, ENT_QUOTES);
Regulární výraz
V Perlových regulárních
výrazech mají souhrnně speciální význam znaky
. \ + * ? [ ^ ] $ ( ) { } = ! < > | : -
a tzv. delimiter,
což je znak ohraničující regulární výraz (např. pro výraz
'#[a-z]+#i'
je to #
). Escapuje se znakem
\
.
$s = preg_quote($s, $delimiter);
V řetězci, kterým se hledaný výraz nahrazuje (tedy například
2. parametr funkce preg_replace
), má speciální význam zpětné
lomítko a dolar:
$s = addcslashes($replacement, '$\\');
Kódování musí být buď 1bajtové nebo UTF-8, podle modifikátoru
v regulárním výrazu. Viz také Escapování
v regulárních výrazech.
PHP řetězce
PHP rozlišuje tyto typy řetězců:
- v jednoduchých uvozovkách, kde speciální význam mohou mít
znaky
\ '
- ve dvojitých uvozovkách, kde speciální význam mohou mít
znaky
\ " $
- NOWDOC, kde speciální význam nemá žádný znak
- HEREDOC, kde speciální význam mohou mít znaky
\ $
Escapuje se znakem \
. To obvykle provádí programátor při
psaní kódu, pro generátory PHP kódu lze využít funkci var_export.
Poznámka: protože zmíněné regulární výrazy se obvykle zapisují
uvnitř PHP řetězce, je potřeba zkombinovat obě escapování. Např. znak
\
se pro regulární výraz zapíše jako \\
a
v řetězci s uvozovkami je třeba psát \\\\
.
SQL a databáze
Každá databáze má svou vlastní escapovací funkci, viz tabulka výše.
Téměř vždy je ale dostupná jen funkce pro escapování řetězců a tu
nelze použít k ničemu jinému, zejména chybí funkce escapující
zástupné znaky používané v konstrukcích LIKE
(v MySQL jde
o znaky % _
) nebo identifikátory, jako jsou názvy tabulek či
sloupců. Databáze nevyžadují odstraňování escapování na
výstupu! (S výjimkou např. typu bytea.)
Znakové sady s neobvyklým vícebajtovým kódováním je nutné v MySQL
nastavit funkcí mysql_set_charset resp. mysqli_set_charset.
Doporučuji používat databázový layer (např. dibi, Nette Database, PDO) nebo
parametrické dotazy, které escapování obstarají
za vás.
JavaScript, JSON
Jakožto programovací jazyk má řadu velmi odlišných podkontextů.
K escapování řetězců lze využít vedlejší
efekt funkce
$s = json_encode((string) $s);
která navíc obalí řetězec do uvozovek. Striktně vyžaduje UTF-8.
JavaScript zapsaný uvnitř HTML atributů (např. onclick
) je
nutné ještě escapovat podle HTML pravidel, neplatí
to však pro JavaScript uvnitř značek <script>
, kde musí
být ošetřen pouze případný výskyt koncové značky
</script>
uvnitř řetězce. To ovšem funkce json_encode
zajistí, jelikož JSON escapuje lomítko /
. Neošetří však
konec HTML komentáře -->
(což v HTML nevadí) nebo XML bloku
CDATA ]]>
, do kterého se skript obaluje. Pro XML/XHTML je
řešením
$s = json_encode((string) $s);
$s = str_replace(']]>', ']]\x3E', $s);
Jelikož JSON využívá podmnožinu syntaxe JavaScriptu, je reverzní funkce
json_decode plně použitelná jen pro
JSON, pro JavaScript omezeně.
CSS
V CSS kontextech je rozsah platných znaků přesně
vymezen, pro escapování identifikátorů lze použít například tuto
funkci:
$s = addcslashes($s, "\x00..\x2C./:;<=>?@[\\]^`{|}~");
Pro CSS uvnitř HTML kódu platí totéž, co bylo řečeno o JavaScriptu a
jeho escapování uvnitř HTML atributů a značek (zde se jedná o atributy
style
a značky <style>
).
URL
V kontextu URL se escapuje vše kromě písmen anglické abecedy, číslic a
znaků - _ .
nahrazením za %
+ hexadecimálně
vyjádřený bajt.
$s = rawurlencode($s);
Podle RFC 2718 (z roku 1999) nebo RFC 3986 (z roku 2005) je preferován
zápis znaků v kódování UTF-8.
Reverzní funkcí je v tomto případě urldecode, která rozeznává i znak
+
s významem mezery.
Pokud se vám zdá celá problematika příliš složitá, nezoufejte. Brzy
přijdete na to, že jde vlastně o jednoduché tranformace a celý trik
spočívá v uvědomění, v jakém kontextu se nacházím a jakou musím pro
něj zvolit funkci. Nebo ještě lépe, zkuste použít inteligentní
šablonovací systém, který dokáže kontexty rozeznat sám a použít správné escapování: Latte
…ale nebylo kde se zeptat.
Nejsem příliš nakloněn tomu, aby se v diskusních fórech Nette nebo
Dibi řešily obecné otázky programování, PHP a vůbec. Diskutéry jsem
vždy odkázal do patřičných míst. Například na Builder, diskusi.JakPsátWeb,
WebTrh či Interfórum.
Jednoho dne jsem se podíval, kam mé anchory vlastně míří, a pochopil jsem,
že žádné z fór není hodno být PHP fórem, že nereflektuje to, čím PHP
bezesporu je. Tedy populárním a nepřeberným semeništěm drobných,
zásadních, zpětně kompatibilních a zcela nových chyb. No a také mi vadila
absence obarvování PHP kódu.
Vzhledem k tomu, že disponuji jak softwarovým vybavením, tak volnou
doménou, rozhodl jsem se oželet jeden díl Simpsonových a věnovat 21 minut
zprovoznění nového českého PHP fóra
na adrese
http://forum.php7.org
Fórum je rozděleno na sekce:
- Programování věnované obecným problémům s jazykem a
interpretem PHP, jakož i spřízněným oblastem jako je mod_rewrite,
.htaccess, JavaScript nebo AJAX.
- Design, frameworky, knihovny pro pokročilé a zkušené matadory,
kteří i přesto setrvávají u PHP.
- SQL, databáze
- Různé, kde je malá burza práce nebo informace, jak správně
zapisovat kód na fóru.
Třeba se fórum ujme a bude někomu užitečné. Uvidíme. Tož endžoj.
Je tomu právě tři a půl roku, kdy jsem distribuci Texy doplnil o tzv. kompaktní verzi.
Zjednodušeně se dá říci, že jde o skript vzniklý spojením všech
souborů tvořících knihovnu do jednoho jediného a vynecháním komentářů
a nadbytečných mezer. Výsledkem je funkčně identický kód, jen s krapet
hustší konzistencí. (Prostě je hustéééj. Jak Salko!)
Záměrem bylo zjednodušit nahrávání knihovny na server – uploadovat
desítku malých souborů přes FTP bývá značně pomalejší, než nahrát
soubor jeden. Jako vedlejší efekt se ukázalo, že jednosouborová knihovna
znatelně zvyšuje výkon aplikace – klidně o 30 %. Používání
kompaktních verzí se ujalo a od té doby většina bugreportů hlásila chybu
na řádce 1 🙂
S příchodem jmenných prostoru v PHP 5.3 se generování kompaktních
verzí poněkud zkomplikovalo – zatím totiž platí, že jeden soubor
může obsahovat jen jeden jmenný prostor už to neplatí. To znamená,
že „jednosouborová“ verze bude mít přinejmenším tolik souborů, kolik
je v knihovně jmenných prostorů, plus případné další soubory řešící
problémy se vzájemnou závislostí mezi prostory.
Zároveň však PHP 5.3 přichází s novinkou, která si klade za cíl
vnést do kompaktních verzí nový svěží vítr: PHAR – PHP archive.
Ostatně myslím, že promluvím za nás všechny, když řeknu: nemusí pršet,
hlavně když kape!
Jak takový PHAR balíček vytvořit? Nejprve je nutné v PHP.INI zapnout
extension=php_phar.dll
a nastavit direktivu
phar.readonly = Off
. Následující kód zkonvertuje celý Nette Framework do
souboru nette.phar
$phar = new Phar('nette.phar');
$phar->buildFromDirectory('libs/Nette'); // enter here path to Nette
$phar->setStub("<?php
Phar::mapPhar('nette.phar');
require 'phar://nette.phar/loader.php';
__HALT_COMPILER();");
$phar->compressFiles(Phar::GZ); // or Phar::BZ2
Nejprve se vytvoří nekomprimovaný archiv ze všech souborů v určeném
adresáři a jeho podadresářích – jde o obdobu TAR archivu. Následně se
definuje tzv. stub neboli zaváděcí program. Jeho úkolem bude načíst soubor
loader.php
, který je součástí Nette Frameworku a nalézá se
vlastně uvnitř PHAR archivu. Stub musí být ukončen zvoláním
__HALT_COMPILER()
. Poté je možno archiv zkomprimovat.
Příklad použití:
require 'nette.phar';
echo Html::el('strong')->setText('It works!');
Vypadá to báječně, že? Ovšem podívejme se na věc podrobněji. PHP
skripty nejsou v archivu nijak minimalizované, nejsou z nich odstraněny
zbytečné komentáře ani mezery. To je potřeba udělat ručně. Takže
zatímco komprimovaný nette.phar
má nějakých 180 kB, stejný
archiv vygenerovaný z kompaktní verze Nette (tj. místo adresáře
libs/Nette
použijeme libs/Nette.compact
) váží
pouhých 50 kB. Nicméně pokud si uvědomíme, že
- minimalizace kódu vede k rychlejšímu parsování (závislost bude dost
možná lineární)
- dekomprimace naopak nutně zpomaluje
dojdeme k závěru, že ideální bude generovat PHAR rovnou z kompaktní
verze a ukládat jej nekomprimovaný. Nojo – ale proč teda rovnou
nezůstat u kompaktní verze? Bez PHAR režie navíc?
Důvod existuje. Kompaktní verze se vždy načte celá, zatímco PHAR lze
parsovat a kompilovat po částech. Takže Zend Framework bude vhodnější
zabalit do PHARu, u dibi nebo Texy si můžeme dovolit plný výkon
kompaktní verze.
Slíbil jsem přijít se silným lidským příběhem.
Příběhem programátorky, která stojí před těžkou životní volbou. Její
jméno je Sofie.
Jak už jste pochopili z druhé věty, Sofie nebyla právě pohledná
žena. Zpoza prořídlých neupravených kadeří na vás přes velké
umaštěné brýle hleděly dioptriemi zvětšená drobná očka, kostěné
obroučky marně skrývaly uhří farmu táhnoucí se podél líček až ke
knírku. V redakci, kde pracovala, platila za velmi zdatnou programátorku. Na
svůj redakční systém byla náležitě pyšná, snad ještě víc, než na
titul Miss Second Life 2007. Pýcha byla oprávněná, kód systému byl tak
křišťálově čistý, že ani křišťál křišťálově čistější být
nemůže.
Ačkoliv měla Sofie z návrhových vzorů velmi ráda právě singleton,
jednalo se spíš o spřízněnost duchovní – v kódu jej používala
umírněně, jen tam, kde se skutečně hodil. Například pro zapouzdření
HTTP požadavku:
/**
* @copyright Sofie S., 2006
*/
class HttpRequest
{
/********************* singleton *******************/
private static $instance;
private function __construct()
{}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self;
}
return self::$instance;
}
private function __clone()
{}
private function __wakeup()
{}
/********************* HTTP request encapsulation *******************/
public function getRemoteAddress()
{
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null;
}
...
}
// užití
if (HttpRequest::getInstance()->isPost()) {
echo 'Hey mister POSTman!';
}
To pro představu snad stačí. Víc kódu ani ukázat nemůžu, protože
nemám Sofiin souhlas a mazat kopyrajty si mi nechce. Ale určitě se shodneme
na tom, že zapouzdření HTTP požadavku do formy singletonu je
perfektní volba:
- program v PHP vždy obsluhuje jen jeden HTTP požadavek (a
právě jeden)
- je praktická jeho globální dostupnost
- objekt se hezky líně inicializuje až při prvním použití
Klíčový je samozřejmě první bod. Mohlo by se stát, že by v budoucnu
bylo potřeba operovat nad dvěma HTTP požadavky? To bezpochyby ne,
odpověděla si Sofie už v době návrhu systému a volba singletonu byla
jasná věc.
Nemá smysl zastírat, že se Sofii líbil jeden redaktor z vedlejší
kanceláře. Věděli to všichni. Ona z něj byla doslova hotová! Stačilo,
aby prošel kolem jejího počítače a rvala si rodidla. Proto když jednoho
pondělního rána našla na stole vzkaz, že s ní chce o něčem mluvit,
málem štěstím omdlela.
Řeč se týkala redakčního systému. Eman nešetřil chválou a dokonce
prohlásil, že Sofie je jejich poklad a co by si bez ní počal. Pak se jí,
snad omylem, letmo dotkl ramene. Sofie pocítila v podbřišku slastné
zachvění. „Je tu něco, co jsem ti chtěl už dávno říct,“ pohlédl jí
Eman do očí, „když mám dlouho otevřený článek a pak kliknu na
uložit, tak se mi objeví: byl jste automaticky odhlášen, bla bla bla,
a celý článek je v řiti. Můžeš s tím něco udělat?“
Nebylo na světě nic, co by Sofie pro Emana neudělala. Ihned se pustila
do práce.
V podstatě šlo o banální úkol. Dokonce tuhle slyšela, že stejný
problém dokázali po dvou letech vyřešit borci na jednom blogovém serveru,
to by bylo, aby si s tím ona během půl hodinky neporadila! Princip je
jednoduchý: v okamžiku, kdy se detekuje vypršení časového limitu a
uživatel se automaticky odhlásí, tak se objekt s aktuálním HTTP
požadavkem, který představuje třeba právě odeslání formuláře, uloží
do session. A pak stačí jen upravit přihlašovací rutinu, aby po
úspěšném přihlášení, je-li v session uložený tento objekt, tak se
nastavil jako aktuální a vykonal. Je to otázka pár řádek kódu a za dvacet
minut už bude sedět u Emana a předvádět mu, jak to báječně funguje.
Dají si společně šálek kávy, plánovala si. A možná … možná ji bude
chtít pozvat na skleničku, ačkoliv ona sklo nerada. Ale Emanovi neumí
říci ne.
Na kávu ten den nedošlo. Zrada přišla z míst, odkud je Sofie nečekala.
Zradil ji singleton. Sofie musela povolit serializaci objektu HttpRequest,
musela vyřešit prohození těchto objektů v metodě getInstance, ale
především se musela smířit s faktem, že na ten krátký okamžik, kdy se
uložený požadavek obnovil ze session, tak na tu chvíli existovaly
požadavky dva. I singleton tvořil pár. Jen ona zůstala sama.
Singleton je jedním z nejpopulárnějších návrhových vzorů. Jeho
úkolem je zajistit existenci pouze jediné instance určité třídy a
zároveň poskytnout globální přístup k ní. Pro úplnost malý
příklad:
class Database
{
private static $instance;
private function __construct()
{}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self;
}
return self::$instance;
}
...
}
// singleton je globálně dostupný
$result = Database::getInstance()->query('...');
Typickými rysy jsou:
- privátní konstruktor, znemožňující vytvoření instance
mimo třídu
- statická vlastnost $instance, kde je unikátní instance uložená
- statická metoda getInstance(), která zpřístupní instanci a při prvním
volání ji vytvoří (lazy loading)
Jednoduchý a snadno pochopitelný kód, který řeší dva problémy
objektového programování. Přesto v dibi
nebo Nette Framework žádné singletony
nenajdete. Proč?
Unikátnost jen zdánlivá
Podívejme se na kód pozorněji – skutečně ručí za existenci pouze
jedné instance? Obávám se, že nikoliv:
$dolly = clone Database::getInstance();
// nebo
$dolly = unserialize(serialize(Database::getInstance()));
// nebo
class Dolly extends Database {}
$dolly = Dolly::getInstance();
Proti tomu existuje obrana:
final public static function getInstance()
{
// finální getInstance
}
final public function __clone()
{
throw new Exception('Clone is not allowed');
}
final public function __wakeup()
{
throw new Exception('Unserialization is not allowed');
}
Jednoduchost implementace singletonu je ta tam. Ba co hůř – s každým
dalším singletonem se nám bude opakovat kusanec stejného kódu. Také
třída najednou plní dva zcela odlišné úkoly: kromě svého původního
úkolu se stará o to být dost single. Obojí je varovný signál, že něco
není v pořádku a kód by si zasloužil refaktoring. Vydržte, za chvíli se
k tomu vrátím.
Globální = ošklivé?
Singletony poskytují globální přístupový bod k objektům. Není nutné
si odkaz na ně neustále předávat. Zlí jazykové však tvrdí, že taková
technika se nijak neliší od používání globálních proměnných a ty jsou
přece čiré zlo.
(Pokud metoda pracuje s objektem, který jí byl nějak explicitně
předán, parametrem nebo jde o proměnnou objektu, říkám tomu „drátové
spojení“. Pokud pracuje s objektem, který si získá přes globální bod
(např. skrz singleton), říkám tomu „bezdrátové spojení“. Docela fajn
analogie, ne?)
Zlí jazykové se v jedné věci mýlí – na „globálním“ není a
priori nic špatného. Stačí si uvědomit, že název každé třídy a metody
není nic jiného, než globální identifikátor. Mezi bezproblémovou
konstrukcí $obj = new MyClass
a kritizovanou
$obj = MyClass::getInstance()
není ve skutečnosti zásadní
rozdíl. Tím méně u dynamických jazyků jako je PHP, kde lze psát $obj = $class::getInstance()
.
Co ale může způsobit bolení hlavy, to jsou
- skryté závislosti na globálních proměnných
- nečekané používání „bezdrátových spojení“, které z API tříd
nevyčtete (viz Singletons are
Pathological Liars)
První bod se dá eliminovat, pokud se nebudou singletony chovat jako
globální proměnné, ale spíš jako globální funkce či služby. Jak tomu
rozumět? Vezměte si třeba google.com – hezký příklad singletonu
jakožto globální služby. Existuje jedna instance (fyzická serverová farma
kdesi v USA) globálně dostupná přes identifikátor
www.google.com
. (Dokonce ani clone www.google.com
nefunguje, jak zjistil Microsoft, maj to holt kluci vychytaný.) Důležité je,
že tato služba nemá skryté závislosti typické pro globální
proměnné – vrací odpovědi bez neočekávatelné souvislosti s tím, co
před chvíli hledal někdo úplně jiný. Naopak nenápadná funkce strtok trpí vážnou závislostí na
globální proměnné a její používání může vést k velmi těžko
odhalitelným chybám. Jinými slovy – problém není v „globálnosti“,
ale v návrhu.
Také druhý bod je čistě záležitostí návrhu kódu. Není chybou
použít „bezdrátové spojení“ a přistoupit ke globální službě,
chybou je to dělat proti očekávání čili skrytě. Programátor by měl
přesně vědět, jaký objekt využívá které třídy. Poměrně čistým
řešením je mít v objektu proměnnou odkazující na objekt služby, která
se inicializuje na globální službu, pokud programátor nerozhodne jinak
(technika convention over configuration).
Unikátnost může být na škodu
K singletonům se váže problém, na který narazíme nejpozději při
testování kódu. A tím je potřeba podstrčit jiný, testovací objekt.
Vraťme se ke Google jakožto ukázkovému singletonu. Chceme otestovat
aplikaci, která jej využívá, jenže po pár stech testech začne Google
protestovat We're
sorry… a jsme kde? A jsme někde. Řešením je pod identifikátor
www.google.com
podstrčit fiktivní (mock) službu. Potřebujeme
upravit soubor hosts
– jenže (zpět z analogie do světa OOP)
jak toho dosáhnout u singletonů?
Jednou z možností je implementovat statickou metodu
setInstance($mockObj)
. Ale ouha! Copak asi chcete slečně metodě
předat, když žádná jiná instance, než ona jedna jediná,
neexistuje?
Jakýkoliv pokus odpovědět na tuto otázku povede nevyhnutelně k rozpadu
všeho, co singleton dělá singletonem.
Pokud odstraníme restrikce na existenci jen jedné instance, přestane být
singleton single a my řešíme pouze potřebu globálního úložiště. Pak je
nabíledni otázka, proč v kódu stále opakovat stejnou metodu
getInstance()
a nepřesunout ji do extra třídy, do nějakého
globálního registru?
Anebo ponecháme restrikce, pouze identifikátor v podobě třídy
nahradíme za interface (Database
→ IDatabase
),
čímž se vynoří problém nemožnosti implementovat
IDatabase::getInstance()
a řešením je opět globální
registr.
O pár odstavců výše jsem sliboval vrátit se k otázce opakujícího se
kódu ve všech singletonech a možnému refaktoringu. Jak vidíte, problém se
mezitím vyřešil sám. Singleton zemřel.
Dobře utajenou novinkou PHP 5.3 je řetězení výjimek (exception
chaining). Tuto vlastnost můžete znát třeba z Javy (caused exception) nebo
ASP.NET (inner exception). O co jde? Každá vyhozená výjimka se může
odkazovat na jinou výjimku, která její vznik zapříčinila. Jinými slovy,
je možné zachycenou výjimku zabalit do obecnější výjimky.
function loadFile($file)
{
if (!is_file($file)) {
throw new FileNotFoundException("File '$file' not found.");
}
...
}
function getConfig()
{
try {
$s = loadFile('config.ini');
...
} catch (FileNotFoundException $e) {
// obalíme výjimku
throw new ConfigException("Missing configuration.", 0, $e);
}
}
try {
$config = getConfig();
} catch (ConfigException $e) {
echo $e;
}
Jak vidíte, konstruktor třídy Exception se dočkal rozšíření
o třetí nepovinný parametr, kterým lze zřetězenou výjimku nastavit.
Získat ji můžeme metodou getPrevious(). Zřetězená výjimka se zahrne i do
textového výpisu:
exception 'FileNotFoundException' with message 'File 'config.ini' not found.' in demo.php:8
Stack trace:
#0 demo.php(17): loadFile('config.ini')
#1 demo.php(28): getConfig()
#2 {main}
Next exception 'ConfigException' with message 'Missing configuration.' in demo.php:21
Stack trace:
#0 demo.php(28): getConfig()
#1 {main}
Všimněte si, že výjimky se poněkud nešťastně vypisují v pořadí,
v jakém vznikly. Očekával bych pořadí opačné.
Zdá se, že PHP 5.3 bude ještě větší bomba, než jsme si mysleli. Do
dlouho očekávané a neustále oddalované verze se na poslední chvíli
dostaly lambda funkce a tzv. closures. Český ekvivalent termínu neznám, ale
dalo by se použít anonymní nebo lambda
funkce. Pokud máte zkušenost s JavaScriptem nebo jazykem Ruby, budou
vám připadat velmi povědomé:
$lambda = function () {
echo 'Hello World!';
};
$lambda(); // vypíše Hello World!
Lambda funkce najdou uplatnění především tam, kde potřebujeme vytvořit
callback na jedno použití. Příkladem jsou třeba volání preg_replace_callback nebo array_map, které bylo doposud spojeno
s nutností deklarovat funkci, která však pro zbytek programu byla zcela
zbytečná. Od PHP 5.3 ji bude možné nahradit lambda funkcí:
// convert to camelCaps
$s = 'hello-world';
$s = preg_replace_callback(
'#-([a-z])#',
function ($matches) { return strtoupper($matches[1]); },
$s
);
echo $s; // vypíše helloWorld
Closures rozšiřují schopnosti lambda funkcí tak, že jim zpřístupní
(vybrané) proměnné z kontextu, ve kterém jsou deklarované:
function getMultiplier($product)
{
return function ($value) use ($product) {
return $value * $product;
};
}
// $multi23 je "násobička 23"
$multi23 = getMultiplier(23);
echo $multi23(3); // vypíše 69
Jiným příkladem je přesměrování výstupu do souboru:
function redirect($file)
{
$handle = fopen($file, 'w');
ob_start(function($buffer) use ($handle) {
fwrite($handle, $buffer);
});
}
redirect('output.html');
echo 'Hello World!'; // místo na obrazovku se vypíše do souboru
Jak to funguje interně? Lambda funkce a closures jsou representovány
objektem finální třídy Closure
s jedinou magickou funkcí
__invoke()
. Ta sama o sobě zajišťuje, že s objekty
spolupracují interní funkce a lze je použít všude tam, kde se očekává
pseudotyp callback. Také is_callable($lambda)
vrací true.
Zapouzdření callbacku tak dostává nový rozměr:
class Salute
{
public function __invoke($name)
{
echo "Hello $name!";
}
}
$salute = new Salute;
$salute('World'); // vypíše Hello World!
Jsou lambda funkce v PHP skutečně tak převratnou novinkou? V PHP je
totiž lze do jisté míry nahradit skrze eval nebo create_function. Jenže za jakou
cenu – tělo funkce je nutné zapsat jako řetězec, což je nepřehledné a
nefunguje zvýrazňování kódu v IDE, takové funkce nelze debugovat, je
znemožněno použití opcode cache atd. Dá se říci, že lambda funkce
legalizují osm let starou funkci create_function pro použití ve
zdravém kódu.
Podrobný popis najdete v propozicích a můžete si je
rovnou i vyzkoušet – stačí stáhnout vývojovou verzi PHP 5.3.
Timy přišel s geniálním
nápadem, jak psát na Twitter přímo z adresního řádku
prohlížeče.
Tedy podobně, jako když googlíte pomocí „g keyword“, můžete štěbetat způsobem „tw Ach jo, dostala
jsem zase krámy.“ Kromě toho, že je to pohodlné, to navíc řeší
problém – pokud se zprávu nepodaří odeslat, máte šanci to zkusit znovu.
V případě ajaxového odesílání přímo ze stránek Twitteru se totiž
nezřídkakdy stane, že zpráva odejde do věčných lovišť. Celý svět pak
zůstane ochuzen o váš 140 znakový elaborát.
Jak na to? Jednak můžete využít přímo Timyho formulář, ale protože
mi nevyhoval, a také protože nechci prozrazovat své přihlašovací údaje,
udělal jsem si vlastní. Stáhněte si knihovničku Twitter for PHP a vytvořte skript
twitter-send.php
:
<?php
header('Content-type: text/html; charset=utf-8');
if (isset($_POST['message'])) {
require_once 'twitter.class.php';
// SEM VLOŽTE SVÉ PŘIHLAŠOVACÍ ÚDAJE
$twitter = new Twitter('DavidGrudl', '******');
$status = $twitter->send($_POST['message']);
if ($status) {
header('Location: http://twitter.com/home');
exit;
} else {
echo '<body style="background:red"><h1>Chyba</h1>';
}
}
?>
<h1>Send message to Twitter</h1>
<form action="#" method="post">
<textarea name="message" cols="100"><?php
echo @htmlSpecialChars($_POST['message']) ?></textarea>
<br><input type="submit">
</form>
Oba soubory nahrajte na svůj server (klidně i lokální) a otevřete
twitter-send.php
v prohlížeči. Nyní s ním asociujte
vyhledávací klíč (tedy jako pro Google je „g“, tak pro tento skript bude
např. „tw“). Popis definice asociace ve Firefoxu nebo IE nechám na
komentátorech, protože to sám neumím. V Opeře je to třeskutě
jednoduché: klikněte pravým tlačítkem myši na textové pole, zvolte
Vytvořit vyhledávač… a do okénka Klíč zadejte „tw“ a
potvrďte.
To je vše! Příjemné štěbetání.
Programátoři, kteří se k PHP dostanou od jiných jazyků, se snaží
předchozí zkušenosti adaptovat na PHP a tak někdy vznikají zlozvyky.
Typickým příkladem je chápání referencí jakožto ukazatelů. Programátor
se snaží optimalizovat výkon aplikace tím, že proměnné předává
funkcím přes reference. Jenže, PHP nemá
ukazatele a ve skutečnosti tím může aplikaci naopak zpomalit. Tyhle chyby jsem
samozřejmě dělal také. Zkušenosti z jiných jazyků se mohou v novém
prostředí vymstít.
Další věc, na kterou se hledí s nedůvěrou, je vytváření
„jepičích objektů“, tedy objektů, které jen splní drobnou funkci nebo
předají data a hned zase zaniknou. Programátoři uvažují, zda-li není
možné objekt nahradit polem. Mají totiž zažitou představu, že vytvoření
objektu je zatíženou velkou režií.
Ve skriptovacích jazycích tomu tak nebývá. Kdyby už PHP 4 bylo
koncipované jako objektové, výkonnostní rozdíl mezi objektovým a
neobjektovým přístupem by neexistoval. Leč z historických důvodů tu
rozdíl je, objektovost je o něco pomalejší. S každou další verzí mám
pocit, že je zanedbatelnější. Za zanedbatelný se dá považovat i rozdíl
mezi vytvořením objektu a pole:
$obj = new Object(10);
$obj->val = 10;
// versus ekvivalent
$arr = array('__class' => 'Object');
__construct($arr, 10);
$arr['val'] = 10;
Zkusil jsem změřit obě operace a výsledek je 2.0E-6 sec : 1.8E-6 sec.
Nemá pak smysl pozdvihnout obočí nad tím, že nějaká operace
„zbytečně“ vytvoří objekt, jen aby předala data. Je to totiž totéž,
jako když se vytváří jepičí pole:
$s = preg_replace_callback($pattern, array($obj, 'method'), $s);
Můžete namítnout, že výše uvedené konstrukce nejsou pro pole vůbec
typické. Jistě, použil jsem je jen proto, abychom srovnávali funkčně
podobný kód. V praxi je na rozhodnutí programátora zvolit, kdy použít
pole a kdy objekt. Já se jen snažím ukázat, že do volby by neměly
zasahovat mylné představy o (ne)výkonnosti.
Na stránce www.phpbench.com, která je
bohužel plná zavádějících měření, jsem narazil na zajímavou
konstrukci:
foreach ($arr as $key[] => $val[]);
Funguje to od PHP 4 a jde vlastně o obdobu
$key = array_keys($arr);
$val = array_values($arr);
Vůbec jsem netušil, že za as
může následovat podobný
výraz. Podíval jsem se do souboru zend_language_parser.output
(najdete ve zdrojácích PHP) a ono je možné dokonce vytvářet konstrukce
jako foreach ($arr as $key[fce($obj->x)] => $val[$y])
.
Praktické využití mě nenapadá, dívky tím neohromíte, ale blognout jsem
to musel 🙂
Ať je alespoň něčím tento spot užitečný: víte, že funkce array_keys má tři parametry?
Ještě ke stránce The PHP Benchmark: snaha autora i vizuální provedení
je rozhodně chválihodné, bohužel některé výsledky bez patřičného
komentáře vyvolávají dojem, že ta či ona konstrukce je zbytečně pomalá.
Pro pochopení je ale potřeba důkladná znalost vnitřností PHP (viz Půvab optimalizace rychlosti,
Černá magie optimalizace,
Derick Rethans: Understanding
the PHP Engine).