Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

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

Komentáře

  1. HosipLan http://www.kdyby.org #1

    avatar

    jak se ty properties hezky hodí že? O:-)

    před 6 lety
  2. Honza Marek #2

    inu, dobrá práce

    před 6 lety
  3. johno http://johno.jsmf.net #3

    avatar

    To {sep} je fajn finta, ale nepaci sa mi, ze ciarka je sucastou odkazu. Nejaka finta aj na to?

    před 6 lety | reagoval [7] David Grudl
  4. despiq #4

    používat facebook a mít tu tlačítko I like tak na něj snad kliknu

    před 6 lety
  5. v6ak http://v6ak.profitux.cz/ #5

    Nepochopil jsem část „Když operuješ sám sebe“. Přece se jen prohlížíš nějakou sondou a do sebe nezasahuješ, ne? Čemu to vadí?

    před 6 lety | reagoval [6] Jan Janoušek
  6. Jan Janoušek http://janjanousek.cz #6

    avatar

    #5 v6aku, Řekl bych, že tomu, když použiješ RobotLoader tak nemůžeš mít 2 třídy co se jmenují stejně, protože by při vytváření instance nebylo jak poznat, instance jaké třídy, že se to má vytvořit. Dvě třídy jsou tam proto, že Apigen sám běží na Nette, takže má naindexované vlastní třídy + musí naindexovat třídy, ke kterým bude dělat dokumentaci a pokud dělá dokumentaci k Nette, tak by to byly tytéž (takže 2×).

    před 6 lety | reagoval [10] v6ak
  7. David Grudl http://davidgrudl.com #7

    avatar

    #3 johno, jéj, to je samozřejmě chyba, opraveno

    před 6 lety
  8. Michal Till http://www.nodejs.cz #8

    Sencha nedávno udělala facelift Javadoc, možná by z toho šlo něco použít: http://www.sencha.com/…-a-facelift/

    před 6 lety
  9. PHX #9

    avatar

    Nemá to nějaký log s výpisem, že zda a zde chybí ta či ona dokumentace? (parametry, návratové hodnoty, …)

    před 6 lety
  10. v6ak http://v6ak.profitux.cz/ #10

    #6 Jane Janoušku, No jo. Ale stejně, pokud by jel Apigen na dokumentované verzi Nette, tak by to šlo bez problému.

    Mimochodem, přidat možnost načítání z externích tříd, které už v Nette asi je kvůli anotacím a nějakému příliš aktivnímu optimalizátoru, by byla asi čistější varianta.

    před 6 lety | reagoval [11] Jan Janoušek
  11. Jan Janoušek http://janjanousek.cz #11

    avatar

    #10 v6aku, Tím se, ale omezíš jen na dokumentaci k Nette. Co když budeš chtít dokumentaci k něčemu jinému? Pokud by to bralo to na čem to samo jede, tak by se ti tam vždycky přigenerovala dokumentace k Nette (leda by se to dalo nějak vypnout, ale to se mi zdá zbytečně komplikované + i když to asi není moc pravděpodobné, tak by se mohlo stát, že při změně v Nette přestane fungovat Apigen, protože nebude kompatibilní s tou verzí a musel by se „zbytečně“ přepisovat). Sice je to vyvíjeno pro dokumentaci k Nette, ale proč se omezit na jeden projekt, když to může jít na jakýkoliv?

    před 6 lety
  12. Edke #12

    avatar

    A máš to ešte cez hooks zavesené na git a commit ?

    před 6 lety
  13. koubel http://mirin.cz #13

    avatar

    pěkné, osobně bych to finalizoval do podoby pharu, a zdrojáky měl klidně nekomprimované, minimálně v repository. NetteX bych očistil na nutné minimum, ale to jsou drobnosti. Jinak psychologicky v pozadí trochu tuším tohle jako pádným argument proti těm, kterým se nelíbí snahy Nette vylepšovat PHP (mě), tato vylepšení by z toho NetteX asi jako jediná zůstala. Každopádně na první pohled pěkná a užitečná věcička a výborné PR pro Nette.

    před 6 lety
  14. Jirka #14

    „jednou tolik“ = „totéž“. Nečekal bych, že zrovna ty tohle nebudeš vědět ;-)

    před 6 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.