Jak se píše generátor API dokumentace?
Dlouhá léta mi ze zdrojáků generoval API dokumentace phpDocumentor. Vývoj tohoto nástroje ustrnul a PHP 5.3 je mu cizí, zejména chybí podpora jmenných prostorů. Začal jsem hledat alternativu. Bohužel žádná sláva.
On ani samotný phpDocumentor nebyl žádný zázrak. Dodáván se sadou šablon, jedna hnusnější než druhá, vedle toho i taková Lupa.cz vypadá jako fešanda. Chápu, „programátoři sobě“, oželel bych nějaké eye-candy, ale šablony jsou tak zoufale nepřehledné a odpudivé, že si neumím představit takovou dokumentaci používat dobrovolně. Užitná hodnota nulová. A nabídka alternativních šablon není.
Vyzkoušel jsem phpDoctor, doxygen a PHP_UML, u kterého jsem
nakonec zůstal. Jeho standardně vygenerovaná dokumentace asi nejvíc
odpovídala tomu, co jsem hledal (ukázka). Jenže
aplikace je taková PEARovská, hned v úvodu člověka přivítá
Notice: Undefined variable: errorLevel, musel jsem se hrabat ve
zdrojácích a opravit několik bugů, strávil moře času úpravou XSL
šablon. Ještě štěstí, že PHP_UML generuje dokumentaci neskutečně
rychleji než phpDocumentor, takže jsem viděl výsledek každého zásahu
takřka ihned a ne až po dlouhých minutách. Stále tomu ale chyběly dost
podstatné věci, a tehdy mě napadlo…
…že si prostě napíšu vlastní generátor. Za čas, co jsem investoval do úprav, jsem ho mohl mít hotový několikrát. Ach ta narušená schopnost používat cizí knihovny.
Pokusím se vysvětlit postup, jak jsem Apigen psal. Dopředu prozradím, že se vešel do pouhých 150 řádků (!) kódu + šablony.
Jak na to
Nejprve: zdrojový kód lze analyzovat buď vlastním parserem, nebo využít
reflection. Jedním
z hlavních požadavků bylo, že generátor bude umět plnohodnotně pracovat
s interními třídami PHP (třeba takto). Což mi jinde
citelně chybělo. Jelikož interní třídy žádný zdrojový kód nemají,
zvolil jsem reflexi. To znamená, že všechny třídy, pro které chci
generovat dokumentaci, musím načíst. Projít adresářovou strukturu třeba
Finderem a postupně volat
require pro každý soubor nejde – mezi třídami jsou
závislosti a je nutné je načítat v pořadí: rozhraní, rodiče, potomci.
Nelehký úkol, ale vyřešil jej RobotLoader:
$robot = new Nette\Loaders\RobotLoader;
$robot->addDirectory($dir); // adresář, který chceme dokumentovat
$robot->register(); // zapne autoloading
// getIndexedClasses vrací seznam nalezených tříd a souborů
foreach ($robot->getIndexedClasses() as $file) {
require_once $file;
}
$robot->unregister();
Když nyní načtu třídu, která má vazbu na dosud nenačtenou třídu nebo rozhraní, přijde ke slovu RobotLoader a situaci vyřeší. Takže to bychom měli. Paráda.
Když operuješ sám sebe
Avšak objevil se tu oříšek. Jak generovat API dokumentaci pro knihovnu, kterou pro generování API dokumentace používám? Slepice a vejce hadr. Řešení vidím dvě: buď použít jako vstup přímo tu kopii Nette, kterou používá generátor, nebo vytvořit alternativní vesmír, kde se Nette nejmenuje Nette. Druhý způsob se mi zdá rozumnější, nakonec ho používám pro generování distribučních balíčků Nette. Jde o to, že generátor používá knihovnu NetteX, která se liší jen v tom, že sídlí v „oiksovaném“ jmenném prostoru.
Ano, anotace
Aby byl model plnohodnotný, potřeboval jsem přihlížet k anotacím.
Například vynechat všechny elementy s anotací @internal,
určovat vrácené hodnoty metod dle anotace @return atd. Tady jsem
opět využil skutečnosti, že Nette Framework podporuje anotace a pro
reflektování použil Nette\Reflection.
$class = new Nette\Reflection\ClassReflection($name);
if ($class->hasAnnotation('internal')) {
...
}
Třídy z Nette\Reflection podporují tzv. properties,
tudíž se příjemněji používají v šablonách, kde místo
$tmp = $method->getAnnotations();
foreach ($tmp['params'] as $value) ...
mohu psát stručnější
foreach ($method->annotations['params'] as $value) ...
Nakonec jsem pro reflexi tříd použil vlastního potomka
Nette\Reflection\ClassReflection s přidanou funkcionalitou.
Přičemž Nette automaticky zajišťuje, aby metody jako
getDeclaredClass() opět vracely instanci mé třídy.
Svěží ![]()
Ša-la-la-blo-ny
Při generování HTML souborů jsem naplno využil sílu šablonovacího jazyka Latte. Ten lze používat nejen ve spojitosti s MVC aplikacemi, ale naprosto kdekoliv. Využil jsem oddělený layout, dědičnost bloků i tzv. n:attributy. Výsledkem jsou skutečně dobře čitelné šablony. Příklad:
{* $implementers je pole objektů ClassReflection *}
<div n:if="$implementers">
<h4>Direct Known Implementers</h4>
{foreach $implementers as $item}
<a href="{$item|classLink}">{$item->name}</a>{sep}, {/sep}
{/foreach}
</div>
Uvedený <div> se vykreslí pouze v případě, že pole
$implementers je neprázdné. Jednotlivé položky vykreslí jako
odkazy oddělené čárkou. A makro {sep}...{/sep} zajistí, aby
se čárka (tj. separátor) neobjevila za poslední položkou. Živé to
můžete vidět třeba tady.
Jiným příkladem je šablona, která vygeneruje v JavaScriptu seznam všech tříd pro potřeby našeptávače (výsledek):
// $classes je opět pole objektů ClassReflection
{contentType javascript}
var classes = {$classes|values|map:'return $value->name;'};
Pro generování obarvených zdrojových kódů jsem použil skvělou knihovnu FSHL, kde stačilo doplnit seznam klíčových slov PHP 5.3. Obsah doc-bloků jsem zkusil formátovat pomocí Texy ve spojení s FSHL a výsledek se mi zdá dostačující.
Světlo světla spatřil Apigen
Naprogramovat celý generátor trvalo pár hodin, což je řádově méně, než bych strávil úpravou existujících knihoven. Výsledkem totiž bylo, jak jsem zmínil, pouhých 150 řádků kódu. Ač jsem ho nijak rychlostně neoptimalizoval, dokumentaci k Nette vygeneruje za cca 11s, zatímco phpDocumentor se s tím trápí přes dvě minuty. Ani jsem nečekal, že se Nette Framework tak výborně hodí na tak netypický úkol.
Mnohem víc času jsem pak strávil vylaďováním šablon, připojil jsem
jQuery a hrál si s tříděním metod a dalšíma opičkama. Generátor jsem
opatřil rozhraním pro ovládání z příkazové řádky, přidal další
fíčůrky a dopsal komentáře, takže v tuto chvíli je těch řádků
určitě alespoň jednou tolik ![]()
Jestli chcete, tak si Apigen můžete stáhnout
Programátoři chyby neignorují
Tedy alespoň by neměli. PHP je jazyk s poměrně laxním přístupem k chybám a tudíž vyžaduje od programátora vyvinout větší úsilí při jejich ošetřování. Nenechte si namluvit opak. Článek je reakcí na dobře míněnou radu Jakuba Vrány.
Existují dva tradiční způsoby, jak chyby oznamovat:
- vyhozením výjimky
- návratovou hodnotou
Takřka všechny knihovny dodávané s PHP používají druhý způsob, protože výjimky byly do jazyka zavedeny až v páté verzi. Což je v mnoha případech na škodu. Programátor totiž musí neustále otrocky kontrolovat návratové hodnoty a ošetřovat chybové stavy. A zároveň je tu riziko, že na to zapomene. Přesněji řečeno, ani ne tak „zapomene“, jako spíš se na to „vy…kašle“, protože to ve světě PHP platí za normu. Nakonec, podívejte se třeba do dokumentace fread a spočítejte, kolikrát v příkladech ošetřili návratovou hodnotu jakékoliv funkce.
Při takovém stylu práce pak není divu, že programátoři považují výjimky za něco otravného, co se (cituju Jakuba) „nedá ignorovat“, zatímco „chyby indikované návratovou hodnotou ignorovat lze a program nejspíš nějak pracovat bude.“ Notykrávo.
Co přesně znamená ono nějak pracovat? Třeba:
// přesouváme z disku na disk
copy('c:/oldfile', 'd:/newfile');
unlink('c:/oldfile');
// pokud první operace selže, soubor se nenávratně smaže
$link = new mysqli('localhost', $user, $pass, 'eshop');
...
$link->query('USE testdata'); // přepneme se z ostré do testovací databáze
$link->query('DELETE FROM orders');
// pokud předchozí operace selže, vymažou se objednávky z ostré databáze
// BTW věřili byste, že výchozí nastavení query() při chybě nevyhodí ani noticku?
// smažeme soubor z adresáře 'test'
ftp_chdir($connection, 'test');
ftp_delete($connection, 'database.sdb');
// pokud předchozí operace selže, vymaže se kupříkladu ostrá databáze
A pak jsou případy, kdy nějak pracovat znamená zařvat s neošetřitelnou fatální chybou:
// načtení konfigurace
$defaults = array('host' => 'localhost', 'charset' => 'utf8');
$config = parse_ini_file('config.ini');
$config = $config + $defaults;
// pokud předchozí operace selže a vrátí FALSE, skončí sčítání fatální chybou
function saveConfig(array $config) { ... }
$config = parse_ini_file('config.ini');
saveConfig($config);
// pokud předchozí operace selže a vrátí FALSE, skončí opět fatální chybou
mysqli_connect('localhost', $user, $pass)
->query("SET NAMES 'utf8'");
// pokud první operace selže, skončí opět fatální chybou
Bastlení zdar!
Víte, komu ublížil mod_rewrite?
Schválně, který software má v dokumentaci uvedeno, že se jedná o voodoo? No jistě, jde o mod_rewrite. Ze zkušenosti mohu říci, že programátoři se dělí do dvou skupin:
- ti, kteří mod_rewrite nerozumí
- ti, kteří si myslí, že mod_rewrite rozumí, avšak mýlí se
Do které skupiny patříte vy? Zkuste nahlédnout do svých souborů
.htaccess a podívejte se, zda vám u pravidel pro
přesměrování (příznak
R) nechybí také příznak NE (noescape)?
Vysvětlím na příkladu: do kořenového adresáře webu
www.example.cz vložím soubor .htaccess s pravidlem
pro přesměrování:
RewriteEngine On RewriteRule .* http://www.example.com/$0 [R=301] #tohle je spatne!
Server pak přesměruje
- z
http://www.example.cz/index.php?title=d%C3%ADvka(parametrtitleobsahuje slovodívka) - na
http://www.example.com/index.php?title=d%25C3%25ADvka(parametrtitleobsahuje řetězecd%C3%ADvka)
Jak vidíte, mod_rewrite ublížil dívce! Je to jeho přirozené chování,
aby to nedělal, musíte mu říct NE:
RewriteEngine On RewriteRule .* http://www.example.com/$0 [R=301,NE] #tohle uz je spravne
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
novější články
