Zrádné regulární výrazy v PHP
V PHP jsou k dispozici tři knihovny pro regulární výrazy: PCRE, Oniguruma a POSIX Regex. Druhá jmenovaná nemusí být vždy k dispozici a třetí je zavržená, proto byste měli používat výhradně šikovnější a rychlejší knihovnu PCRE. Bohužel implementace trpí docela nepříjemnými nedostatky, a to ve všech verzích PHP.
Činnost jednotlivých funkcí preg_* lze rozdělit do
dvou kroků:
- kompilace regulárního výrazu
- exekuce (hledání, záměna, filtrování, …)
Sympatické je, že PHP zkompilovanou podobu regulárních výrazů udržuje v cache a tudíž se kompilují vždy jen jednou. Proto je vhodné používat statické regulární výrazy, tj. negenerovat je parametricky.
Teď k těm nepříjemným záležitostem. Pokud se během kompilace odhalí
chyba, PHP na ni upozorní chybou úrovně E_WARNING, avšak
návratová hodnota funkce je nejednotná:
preg_filter,preg_replace_callback,preg_replacevracíNULLpreg_grep,preg_match_all,preg_match,preg_splitvracíFALSE
Dobré je vědět, že funkce vracející skrze referenci pole
$matches (tj. preg_match_all a
preg_match) při kompilační chybě argument nevynulují, tudíž
testovat návratovou hodnotu má opodstatnění.
PHP od verze 5.2.0 disponuje funkcí preg_last_error vracející kód
poslední chyby. Avšak pozor, týká se to pouze chyb vzniklých během
exekuce! Pokud dojde k chybě během kompilace, hodnota
preg_last_error se nevynuluje a vrací předchozí hodnotu. Pokud
tedy návratová hodnota preg_* funkce není NULL
resp. FALSE (viz výše), rozhodně nepřihlížejte
k tomu, co preg_last_error vrací.
K jakým chybám může dojít během exekuce? Nejčastějším případem
je překročení pcre.backtrack_limit nebo nevalidní UTF-8 vstup
při použití modifikátoru u. (Poznámka: neplatné
UTF-8 v samotném regulárním výrazu se odhalí již při kompilaci.)
Nicméně způsob, jak PHP s takovou chybou naloží, je naprosto
neadekvátní:
- nevygeneruje žádnou zprávu (silent error)
- návratová hodnota funkce může naznačovat, že je vše v pořádku
- chybu lze zjistit až následným zavoláním
preg_last_error
Zastavím se u té návratové hodnoty, což je asi největší zrada.
Proces se totiž vykonává do chvíle, než se chyba objeví a poté se vrátí
částečně zpracovaný výsledek. A to v naprosté tichosti. Jenže ani
tohle neplatí vždy, třeba trojice funkcí preg_filter,
preg_replace_callback, preg_replace umí i při
exekutivních chybách vracet NULL.
Zda došlo během exekuce k chybě lze zjistit jedině voláním
preg_last_error. Ale jak už víte, tato funkce vrací nesmyslný
výsledek v případě, že došlo naopak k chybě kompilace, musíme tedy
obě situace rozlišit přihlédnutím k návratové hodnotě funkce, zda-li je
NULL resp. FALSE. A jelikož funkce vracející
NULL při chybě kompilace umí vracet NULL i při
chybě exekuce, lze konstatovat asi jen tolik, že PHP je nadevší pochybnost
zkurvený jazyk.
Jak by vypadalo bezpečné použití PCRE funkcí? Například takto:
function safeReplaceCallback($pattern, $callback, $subject)
{
// callback musíme ověřit sami
if (!is_callable($callback)) {
throw new Exception('Neplatny callback.');
}
// testujeme výraz nad prázdným řetězcem
if (preg_match($pattern, '') === FALSE) { // chyba kompilace?
$error = error_get_last();
throw new Exception($error['message']);
}
// zavoláme PCRE
$result = preg_replace_callback($pattern, $callback, $subject);
// chyba exekuce? Ale nepoznáme, pokud k ní došlo uvnitř callbacku :-/
if (preg_last_error()) {
throw new Exception('Chyba zpracovani regularniho vyrazu.', preg_last_error());
}
return $result;
}
Uvedený kód transformuje chyby do výjimek, nesnaží se však potlačit výpis varování. Ovšem ani v tomto případě nemáme vyhráno – pokud dojde ke vzniku chyby uvnitř callbacku, bude mylně považována za chybu funkce preg_replace_callback.
Bezpečné zpracování regulárních výrazů je implementováno ve třídě Nette\Utils\Strings.
Heuréka: example.l na localhost
Mám ve zvyku vyvíjet a spouštět webové aplikace na doménách
s příponou .l, takže třeba vývojová verze
http://nette.org mi běží na http://nette.l.
Což znamená přidat do souboru hosts řádek pro každou
subdoménu, např.:
nette.l 127.0.0.1 www.nette.l 127.0.0.1 forum.nette.l 127.0.0.1
To je přinejmenším otravné. Kéž by hosts podporoval zápis
pomocí wildcards, stačilo by napsat
*.l 127.0.0.1
a měl bych vystaráno. Jenže tohle nefunguje. Hledal jsem proto jiné řešení. K velkému překvapení, internet se návody nejen že nehemží, nenašel jsem vůbec nic.
Bylo zřejmé, že budu potřebovat najít lokální DNS server, který toto
umožní. Narazil jsem na Simple DNS
Plus. Po instalaci je potřeba jej manuálně aktivovat, tj. říci
síťovému připojení, že má používat DNS server na adrese
127.0.0.1. Poté přímo v aplikaci v Tools / Options / Plug-Ins vytvořit
instanci pluginu Regular Expressions a určit, že maska \.l$ se
bude mapovat na adresu 127.0.0.1.
Funguje to výborně, jen cena $79 mi nepřipadá odpovídající, využívám-li okrajové vlastnosti jinak našlapaného programu.
Další možností je instalace multiplatformního opensource DNS serveru BIND. Stáhl jsem si distribuci pro
Windows a nainstaloval do výchozího adresáře. Ovšem zapomeňte na nějaké
klikací prostředí, BIND zná jen příkazovou řádku a textové
konfigurační soubory. Pro mě španělská vesnice. Naštěstí mě pohled do
dokumentace neodradil rovnou a
podařilo se mi vytvořit konfigurační soubory. Ty se nacházejí
v podadresáři etc, v mém případě je to
c:\Windows\System32\dns\etc\. Na vašem počítači může být
cesta odlišná.
Soubor etc\named.conf
options {
directory "C:\Windows\System32\dns\etc"; // změňte pokud používáte jinou cestu.
};
zone "localhost" {
type master;
file "localhost";
};
zone "l" {
type master;
file "localhost";
};
zone "0.0.127.in-addr.arpa" {
type master;
file "localhost.rev";
};
Sourbor etc\localhost (využívající wildcard DNS
record)
$TTL 86400
@ IN SOA @ root (
2005022501 ; serial
3H ; refresh
15M ; retry
1W ; expiry
1D ) ; minimum
IN NS @
IN A 127.0.0.1
*.l. IN A 127.0.0.1
a nakonec soubor etc\localhost.rev
$TTL 86400
@ IN SOA localhost. root.localhost. (
2005022501 ; serial
3H ; refresh
15M ; retry
1W ; expiry
1D ) ; minimum
IN NS localhost.
1 IN PTR localhost.
Poté stačí na síťovém připojení aktivovat DNS server na adrese 127.0.0.1 (viz postup výše) a spustit službu ISC BIND. Světe div se, ono to funguje!
C:\>ping nette.l Příkaz PING na nette.l [127.0.0.1] - 32 bajtů dat: Odpověď od 127.0.0.1: bajty=32 čas < 1ms TTL=128
Apel na hostéry: chceme PHP 5.3!

Drazí provozovatelé hostingů, už je to tady. Před bezmála půl rokem vyšlo PHP 5.3.0. Přirozený nástupce řady 5.2 bez zpětně nekompatibilních změn. Víme, že jste s nasazením čekali, než se objeví následující setinková verze. Dnes se tak stalo, PHP 5.3.1 je venku.
Trojková řada přináší velké množství vylepšení, na které my, krásní a bohatí programátoři, toužebně čekáme. Neotálejte proto více a novou verzi PHP nám dopřejte. Když budete chtít, stihnete ji nasadit ještě dnes v noci! Stejně v televizi nedávají nic zajímavého. Děkujeme!
(tipy na hostingy, které už novou verzi mají, můžete posílat do komentářů)
Čistý Programátorský Experiment
Dovolte mi malý experiment. Týká se všech programátorů, které baví návrh aplikací a OOP. Zadám vám velmi jednoduchý úkol, který má mnoho možných řešení. A spíš než konkrétní kód mě zajímá způsob uvažování. Budu rád, když se zapojí programátoři používající různé jazyky. Proto také zadání zapíši v pseudokódu.
Mějme třídu WebPage, které zadáme URL a ona načte stránku
a vrátí jeji obsah, hlavičky a dokonce i náhled v podobě obrázku.
Příklad použití:
page = new WebPage
page.url = 'http://phpfashion.com'
echo page.url
echo page.body
echo page.headers
echo page.thumbnail
Chápejme url, body, headers a
thumbnail jako vlastnosti (properties, accessors) třídy
WebPage. Je asi zřejmé z logiky věci a principu zapouzdření,
že zatímco url umožňuje zápis i čtení, ostatní vlastnosti
lze pouze číst.
Protože funkčnost třídy WebPage je výkonnostně i časově
náročná, je vhodné jednou získaná data ukládat do databáze. Jak to ale
implementovat? Sice nejsnadnější by bylo rovnou upravit kód třídy
WebPage, jenže takový postup je špinavý. Proč by třída s jednou
srozumitelnou funkcionalitou měla navíc ještě komunikovat s databází?
Pověříme tím tedy třídu WebPageStorage. Ta nám bude, mimo
jiné, schopna dodat objekt WebPage rovnou z databáze:
page = WebPageStorage.load('http://phpfashion.com')
echo page.url
echo page.body
echo page.headers
echo page.thumbnail
Otázka zní: jak to naprogramovat?
Zopakuji, že se pídíme po nejčistším řešení. Jak docílit toho, aby
metoda load mohla vytvořit objekt a nastavit mu read-only
vlastnosti body, headers a thumbnail na
hodnoty načtené z databáze? Vytvořením setterů nebo zpřístupněním
vnitřních proměnných by se porušil princip zapouzdření. Navrhli byste
redesign třídy WebPage? Jaký? Navrhli byste redesign
WebPageStorage? A co když úkol zkomplikujeme tím, že
thumbnail se z databáze načte až při vyžádání?
Věřím tomu, že pro řadu čtenářů je zadání naprosto triviální. Přesto se zkuste zamyslet nad nejčistším řešením a vysvětlete jej v komentářích. Klidně obšírně. Díky!
Jak zazálohovat všechny své twíty
Pokud máte dojem, že ty 140 znakové kravinky, co píšete na Twitter, je nutné zálohovat pro příští generace, ať už z důvodu, že Twitter má občas výpadky doprovázené ztrátou dat, nebo vám někdo může účet ukrást a smazat, nebo se blížíte k limitu 3200 štěbetnutí, po kterém se (prý) nejstarší kusy odmazávají, nebo prostě chcete mít vše na disku kvůli lepšímu vyhledávání, je tento článek pro vás.
Protože jsme na blogu o PHP, nebudu zde popisovat online služby určené k zálohování, ale rovnou vypustím z klávesnice kus kvalitního objektového kódu ;)
Nejprve si stáhněte knihovničku Twitter for
PHP (verzi 2.0) od stejnojmenného autora s autorem blogu. A pak si
vytvořte zálohovač twitter-backup.php:
<?php
set_time_limit(0);
require 'twitter.class.php';
// zde dejte své přihlašovací údaje
$twitter = new Twitter($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret);
// naráz lze načíst maximálně 200 twittů, tož budeme stránkovat
$page = 1;
$retry = 0;
do {
try {
$channel = $twitter->load(Twitter::ME, 200, $page);
if (empty($channel->status)) { // prázdný výstup? narazili jsme na konec
break;
}
file_put_contents("twitter-backup.$page.xml", $channel->asXml());
echo "Ulozena stranka c. $page\n";
$page++;
$retry = 0;
sleep(1);
} catch (TwitterException $e) {
echo "Error: {$e->getMessage()}\n";
if ($retry > 3) break; // chyby s občas stávají, dáme 3 pokusy
$retry++;
}
} while (TRUE);
Po spuštění se vytvoří soubory twitter-backup.1.xml,
twitter-backup.2.xml atd., podle toho, jak jste aktivní
štěbetal. XML obsahuje skutečně vše, včetně informací, na koho zpráva
reaguje, z jakého zařízení byla poslána nebo jaké máte barvičky
v profilu.
novější články
