PHPStan: hledání chyb v kódu bez psaní testů

Na Medium.com jsem se rozepsal o tom, na čem pracuji již několik let, z toho poslední rok intenzivně – na nástroji pro statickou analýzu PHP kódu. PHPStan hledá chyby, aniž by bylo třeba daný kód spouštět, čímž se blíží kompilátorům staticky typovaných jazyků. Využívá informací, které jsou k dispozici při parsování PHP kódu - typehinty, dokumentační komentáře a reflexi.

Různé vývojové verze PHPStanu nám už přes rok hlídají produkční kód ve Slevomatu a už mnohokrát včas na CI serveru odhalil chybu, která by se jinak dostala do produkce. Pochvalují si ho také vývojáři v Hele.cz, DámeJídlo.cz a Bonami.

Oproti podobným nástrojům nabízí především rychlost, rozšiřitelnost1 a různé nastavení striktnosti, aby vás napoprvé nezahltil nadbytečným množstvím chyb, ale pouze těmi nejzásadnějšími, a mohli jste ho tak rovnou začít používat.

Delší a podrobnější povídání o něm najdete na najdete na Medium.com nebo se rovnou podívejte na GitHub, jak ho zprovoznit na svém projektu. V případě, že byste s integrací a nastavením chtěli pomoct, určitě se ozvěte.

  1. Pro popis magického chování tříd, které si dynamicky definují properties a metody pomocí implementace magických metod __get, __set a __call.

Zkracování feedback loop

V odvětví softwarového vývoje existuje spousta best practices, které vedou ke zrychlení a zjednodušení práce programátora. O čistém a čitelném kódu, SOLID principech a testování bylo již napsáno mnoho a myslím si, že kdo chce, už to všechno dlouho dělá. Poslední dobou se ale zabývám něčím, co je pro fungování týmu vývojářů a jejich projektů neméně důležité a zásadní, ale v praxi to ještě příliš často nepotkávám, zato vidím důsledky absence tohoto postupu.

Řada článků a přednášek odrazuje od kompletního přepisu funkčního software a mají k tomu pádné důvody. Nutnost dohánět roky vývoje současné verze, nulová hodnota pro uživatele a nejistota, že to všechno vynaložené úsilí dopadne lépe. Kompletní přepis nikdy nedává smysl. V kontextu tohoto článku ho ale považuji pouze za jeden extrém na dlouhé škále různých řešení a chci ukázat, že lekce, které z něj plynou, lze aplikovat při každodenní práci, i když zrovna něco nepřepisujete od nuly a zdánlivě děláte vše dobře.

Jaký důvod mají týmy ke kompletnímu přepisu? Potřebují něco udělat jinak, než to v aktuálním projektu je, často si to chtějí jen vyzkoušet. Nikdo, ani sebevětší expert, často neví, jestli směr, kterým se vývoj ubírá, je správný. Proto bychom se to měli snažit zjistit co nejdříve, co nejrychleji získat zpětnou vazbu. Kámen úrazu tkví především v tom, že přepis trvá příliš dlouho a celé měsíce či roky týmy budují něco, u čeho nikdo netuší, jestli je to správně a jestli se to vyplatí. Tento problém se však netýká jen kompletních přepisů, ale i běžného iterativního vývoje.

Feedback loop, v češtině „kolečko zpětné vazby“, je základním kamenem jakékoli tvůrčí práce. Vždy byste se měli soustředit na to, abyste co nejdříve dostali zpětnou vazbu k vašemu výtvoru. Od kolegů, od hardwaru, od uživatelů i zákazníků. Jen tak si zajistíte, že stavíte něco, co má cenu.

Že se to někomu nedaří se dá navenek velmi snadno poznat. Z nedávné doby mě napadá např. ČEZ a jeho nový zákaznický systém:

Energetická společnost ČEZ přechází na nový zákaznický systém. Kvůli změně však není možné až do poloviny října provést jakoukoliv změnu v zákaznických smlouvách. Část zákazníků dostane se zpožděním roční vyúčtování.

Měsíc (!) nemůže jedna z největších českých společností hýbat s daty zákazníků. A to kvůli tomu, že nesbírali feedback k novému produktu průběžně, ale nasadili ho celý najednou a přišli příliš pozdě na to, že nefunguje.

Rychlejšího sběru zpětné vazby se dá dosáhnout častějšími releasy. To ale není jediná metrika, o kterou by se tým měl snažit. Zásadní je držet množství nového kódu, který není v produkci, na naprostém minimu. Kód, který není nasazený, představuje riziko.

Čím častěji budete nasazovat, tím menší změny v jedné dávce dostanete do produkce. S malými změnami se pojí řada výhod. Snadněji provedete poctivé a detailní code review. Máte větší šanci, že v kódu odhalíte problémy, pokud reviewujete desítky a nikoli tisíce řádků.

10 lines of code = 10 issues.
500 lines of code = „looks fine.“
twitter.com/iamdevloper

Čím menší změna, tím menší riziko, že se něco pokazí. Menší změny se snadno revertují. Krátké větve v Gitu se také daleko snadněji spravují, rebasují a mergují.

Koncentrace na rychlé získávání feedbacku vyžaduje změnu mindsetu. U každé změny, kterou provádíte, musíte uvažovat, jak ji provést tak, aby se dala ihned bez problému nasadit.

Při vývoji nové funkcionality vás typicky čeká spousta „přípravných“ prací, na kterých pak novou funkcionalitu stavíte. Takové refaktoringy by neměly nic rozbít a měly by jít rovnou nasadit. Tím si ověříte, že fungují a opravíte případné chyby, kterých určitě nebude tolik, jako kdybyste je nasazovali až ve velkém finálním balíku změn s hotovou celou funkcionalitou. Určitě je výhodnější opravovat 15× dvě chyby v průběhu několika týdnů, než třicet chyb najednou.

Kromě toho, že sbíráte feedback na své změny v produkci, vám poděkují i kolegové, se kterými sdílíte váš čerstvý kód a mohou z něj rovnou těžit ve svém úkolu, nebo vám s tím vaším snadno pomoci.

Tento přístup je přes své výhody ale i v lecčems náročnější. Zadání úkolů je třeba rozfázovat na malé části. Tyto části vám mohou zpočátku připadat až směšně malé, to ale znamená, že na to jdete dobře. Stejně jako objektový model aplikace by se měl skládat z mnoha jednoduchých malých objektů, tak i vývoj by se měl skládat z mnoha malých změn, kde každá dává izolovaně i společně smysl.

Rozfázování úkolu znamená, že je potřeba promyslet i to, jak aplikace vypadá v mezistavech. Získáte cit pro to, jaké požadavky spolu souvisí, které části úkolu jsou zásadní a které podružné. Pokud např. redesignujete část aplikace a zároveň do ní implementujete nové funkce, využijte příležitosti rozdělit tento velký projekt na dva menší – nejdřív naimplementujte a nasaďte redesign se současnými funkcemi a až poté do něj nové funkce dodělejte. Tento přístup k práci vyžaduje určitou režii navíc, ale zato si tím snížíte rizika spojená s nasazováním nových verzí aplikace na minimum. A je navenek vidět progres. Malé změny se nebojíte nasadit ani v pátek odpoledne. A pokud se to naučíte, váš vývoj bude sestávat pouze z těchto malých bezpečných změn.

The more s/w projects I see, the more I am getting convinced that most code quality guidelines have zero impact compared to feedback loops
twitter.com/pembleton

Pokud pracujete na funkcionalitě, která ještě nemá být vidět, převedete větve ve verzovacím systému na větve v kódu, tzv. feature toggles. Rozpracovanou ji tedy nasadíte do produkce, ale zpřístupníte pouze určité uživatelské roli či pod speciální URL. Stejným způsobem můžete provádět i A/B testy či postupný roll out. Počet feature toggles byste měli také ale držet na nutném minimu a jakmile je nová funkce přístupná všem, měli byste kód aplikace zase zjednodušit a feature toggle odstranit.

Pokud pracujete na něčem úplně novém, měli byste si co nejdříve ověřit užitečnost produktu. Je tedy nesmysl začínat vývoj od registračního a přihlašovacího formuláře, který je vždy stejný, ale vždy začněte od toho, co je jádrem daného produktu, co představuje jeho přínos. Pokud vám první verze bude přinášet užitek, můžete ji začít vylepšovat.

Těší mě, když vidím tyto principy aplikované i mimo softwarový vývoj. Píšete článek a nemá to konce? Rozdělte ho na více částí a ty vydávejte postupně. Píšete knihu? Vydávejte ji čtenářům po kapitolách. Zní to šíleně? Pro někoho už běžná praxe.

Další čtení: Why Continuous Deployment?, Code spiral. Doporučuju také followovat Michiela Rooka, vývojáře Phingu, který o tématu často píše a odkazuje.

Kečup nebo tatarku?

Představte si, že k vám na pohovor přijde absolvent filosofie, který se živí jako údržbář v Písku (skutečnost byla opravdu jen trošku jiná). Řekne vám, že volba filosofie byla životní omyl, ale že už půl roku se učí programovat. Nic moc, ale docela mu to myslí, tak mu nabídnete dvojnásobek jeho stávajícího platu… a za týden odepíše, že dostal lepší nabídku v jiné IT firmě. Zhruba tak dnes vypadá trh práce.

O blokování reklam

Čas od času se o něm strhne debata. Jedni reklamu obhajují jako jedinou možnou obživu pro provozovatele webů, druzí poukazují na zahlcení reklamou, její nevkusnost, vlezlost a celkově nižší pohodlí jako důvody pro její blokování. Proč na pohledu druhého tábora nevidím nic špatného (a řadím se do něj) vysvětlím v následujících odstavcích.

Začnu lehkým úvodem do toho, jak funguje současný web. Servery (něčí počítače) vystavují volně dostupný obsah na nějakém portu a nějaké adrese. Kdokoli si o ten obsah může říct HTTP požadavkem. Ve většině případů se stáhne text ve formátu HTML, a je na klientovi, jak s tímto textem naloží. Může ho zobrazit v čisté podobě, vykreslit v ASCII artu v textové konzoli, nechat ho přečíst čtečkou pro nevidomé, možností je nepočítaně. Jedna z nich je předat HTML kód vykreslovacímu jádru moderního webového prohlížeče. I v něm mám řadu možností, jak stažený kód interpretovat - mohu zakázat či povolit vykonávání JavaScriptu, nestahovat obrázky, nespustit Flash, namluvit serveru, že jsem na pomalém připojení nebo že mám slabou baterii.

Dnes se hodně mluví o strojovém učení. Představte si prohlížeč, který by se na základě pohybu očí uživatele učil a při opakovaných návštěvách webu by uživateli prezentoval pouze ty části stránky, kterým v minulosti věnoval pozornost. Aby ho irelevantní bloky nadpisů, textů a grafiky nerušily od toho, co ho ve skutečnosti zajímá. Lákavá myšlenka, o kterou by v případě povedeného provedení mohla mít zájem spousta uživatelů.

Až na to, že něco takového tu dnes už máme, a budí to obrovský rozruch. Blokátory reklamy si udržují seznamy prvků na stránkách, které většinu uživatelů obtěžují, a při vykreslování je automaticky odstraňují. Proč to provozovatelům webů vadí? Vždyť mi poskytli ke stažení obsah, který lze interpretovat různými způsoby, a já si jeden z nich vybral.

Provozovatelé se totiž ve svém obchodním modelu spoléhají na to, že ten kód, který si od nich stáhnu, si můj prohlížeč vyloží tím způsobem, který jim vydělá peníze. Jenže to je zastaralé uvažování, které bude fungovat čím dál tím hůř. Jak z toho ven?

Existují snahy, jak blokování reklam zamezit, ale pokud jste pořádně přečetli předchozí odstavce, tak už víte, že to principiálně není možné. Ano, obě strany mohou reagovat na kroky toho druhého a přizpůsobovat se jim, ale vždy to bude hon kočky s myší a blokující strana bude mít zpravidla vždy navrch, protože principy webových technologií jsou ji nakloněny. Stejně jako při sledování televize mám možnost přepnout program a při čtení novin přeskočit stránku s placenou inzercí, tak i na webu budu mít vždy možnost se reklamám vyhnout. Nad blokováním pop-upů vestavěným přímo v prohlížeči se dnes již také nikdo nepozastavuje.

Tudy tedy cesta dlouhodobě nevede. Provozovatelé webů si musí uvědomit, proč uživatelé mají potřebu reklamy blokovat – protože je vlezlá, agresivní, nevkusná, žere daleko více dat, procesorového času a baterie1. Dokud musím při vstupu na zpravodajský portál zavírat křížkem animovanou a ozvučenou reklamu na mobilního operátora, která mi užírá cenné megabajty z FUP, tak ji prostě blokovat budu. Inzerenti, reklamní systémy a provozovatelé by tedy v první řadě měli reklamám napravit reputaci - servírovat je v takovém množství a kvalitě, aby od nich lidé neutíkali a reklamu neignorovali.

I když se nedívám a neklikám na reklamy na obsahových webech, nepřipadám si jako příživník. Věřím, že jsem provozovateli webu i tak užitečný. Věnuji jim svůj čas, čtu jejich články, přemýšlím nad danými tématy a pokud mě zaujmou, tak je sdílím dál.

Z předchozích odstavců vám může připadat, že jsem pro zrušení oboru marketingu, tak to ale není. Marketing je kreativní obor a na kreativcích je, aby ke mně v pravou chvíli dostali obsah, který mi bude užitečný a bude mě zajímat. Jsem zastávcem native advertisingu. Pokud se forma reklamy skloubí se správným zacílením, nic proti ní nemám. Takže pokud se v mém oblíbeném technologickém podcastu objeví zmínka o kvalitním registrátoru domén nebo o VPS hostingu, je to pro mě trefa do černého.2

Další čtení: The ethics of modern web ad-blocking

  1. Svižnosti webů se zapnutým blokátorem, obzvlášť na mobilních zařízeních, se nic nevyrovná. iOS 9 přišel na 64bitových zařízeních s podporou tzv. content blockers, které přináší do uzavřeného ekosystému Applu nečekanou podporu pro blokování reklam. Každému doporučuji si nějaký nainstalovat.

  2. Na základě reklam v podcastech jsem si koupil už minimálně čtyři produkty. Kdo tohle může říct o bannerech na českých webech?

Dokonalé scrollování přesně tam, kam uživatel potřebuje

Při vývoji nového košíku Slevomatu jsme čelili řadě výzev. Jedna z nich souvisela s tím, že celý košík byl prezentován na jedné stránce a nové kroky prodlužovaly stránku směrem dolů. Pro uživatele to má ten přínos, že na pozdějších krocích nemusí dohledávat, co v košíku vlastně má, ale stačí se podívat na vrchol stránky.

Aktuální košík

Celý košík je koncipovaný jako SPA1. I když jsme měli wireframy s návrhem podoby jednotlivých kroků košíku, statický návrh, který sám o sobě vypadá dobře, neposkytuje kompletní obraz o fungování aplikace. Musel jsem přijít na to, jak udělat přechod mezi jednotlivými kroky košíku tak, aby se uživatel hned zorientoval a měl představu, kde se nachází.

V momentě, kdy uživatel vyjádří svůj úmysl (pokračuje v košíku do dalšího kroku, nebo se vrací zpět) by mu aplikace neměla klást žádné překážky a další „úkoly“ před tím, než bude moct pokračovat v tom, co chce udělat. V případě našeho košíku se tedy nabízí automatické scrollování, aby uživatel poté, co klikne na „Pokračovat“, mohl rovnou vybírat platbu a nemusel už sahat na kolečko myši.

První naivní implementace posouvala stránku vždy tak, že horní okraj aktuálního kroku košíku byl srovnaný s horním okrajem viewportu:

$('html, body').animate({
    scrollTop: $target.offset().top
}, 500);

Košík -- prvek je zarovnaný s horním okrajem

Stránka se hýbala pokaždé a někdy urazila i zbytečně dlouhou vzdálenost, což bylo nepříjemné pro oči. Řekl jsem si, že cílem vlastně není, aby se aktuální krok objevil vždy na vrcholu viewportu, ale pouze aby byl celý viditelný a to kdekoli na obrazovce.

První optimalizace spočívala v tom, že pokud už je aktuální krok celý ve viewportu, nemusím scrollovat vůbec. K tomu potřebuji výšku viewportu a relativní pozici prvku vůči němu. Žádné scrollování je nejlepší scrollování!

var viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
var isWholeElementVisible = $element[0].getBoundingClientRect().top >= 0 &&
    $element[0].getBoundingClientRect().bottom <= viewportHeight;
if (isWholeElementVisible) {
    return;
}

Pokud tato podmínka neprošla, přistoupil jsem opět k naivnímu scrollu k hornímu okraji. To ale vedlo ke stejně zbytečným a příliš velkým pohybům jako před touto optimalizací. Napadlo mě, že bych mohl nějak pracovat s dolním okrajem prvku a zarovnávat ho ke spodnímu okraji viewportu:

Košík -- prvek je zarovnaný s dolním okrajem

Ale v jakém případě? Nemůžu tak scrollovat pokaždé, protože by to since jakž-takž fungovalo pro přechod do následujících kroků, ale při návratu do předchozích by opět docházelo k většímu pohybu stránky, než je ve skutečnosti potřeba. A to je ten pravý klíč - porovnám vzdálenost, jakou musí stránka cestovat pro zarovnání horního okraje prvku s horním okrajem viewportu a dolního okraje prvku s dolním okrajem viewportu a zascrolluju tam, kam je to blíž!

Ze scrollování na dolní okraje musím ještě vyřadit prvky, které jsou vyšší než celý viewport, protože uživatel by neměl přijít o začátek kroku, kde může být jeho název, vysvětlivky nebo třeba i část formuláře, který musí vyplnit.

var currentScrollPosition = window.scrollY;
var scrollingOffsetTop = $element.offset().top;
var offsetToScrollTo = scrollingOffsetTop;
var fitsInViewPort = $element.height() < viewportHeight;
if (fitsInViewPort) {
    var scrollingDistanceToTop = Math.abs(currentScrollPosition - scrollingOffsetTop);
    var scrollingOffsetBottom = scrollingOffsetTop + $element.outerHeight() - viewportHeight;
    var scrollingDistanceToBottom = Math.abs(currentScrollPosition - scrollingOffsetBottom);
    if (scrollingDistanceToBottom < scrollingDistanceToTop) {
        offsetToScrollTo = scrollingOffsetBottom;
    }
}

$('html, body').animate({
    scrollTop: offsetToScrollTo
}, 500);

Toto řešení se mi už celkem líbilo. Poslední detail, který jsem upravil, byla délka trvání animace. Pro větší vzdálenosti proběhl scroll příliš rychle a výsledný efekt opět nebyl příjemný. Nastavil jsem animaci tedy tak, že pokud během scrollu urazí stránka více jak 60 % viewportu, prodloužím výchozí délku o 30 %:

var duration = 500;
var scrollingDistance = Math.abs(currentScrollPosition - offsetToScrollTo);
if (scrollingDistance / viewportHeight > 0.6) {
    duration *= 1.3;
}
$('html, body').animate({
    scrollTop: offsetToScrollTo
}, duration);

Mám za to, že nad pozicí při scrollování je potřeba uvažovat vždy a nezůstat u naivního řešení v první ukázce. Věřím, že popsaný postup najde uplatnění nejen u jednostránkových košíků.

Osobně se jako doma cítím spíše při programování backendu, ale baví mě občas skočit i na frontend a vyřešit nějaký zajímavý problém - počítání se souřadnicemi na displeji je úplně jiná liga, než psaní SQL dotazů a plnění šablon2. Minulý týden jsem např. strávil nějakou dobu3 laděním pohybu vinylové desky, aby se plynule zastavovala a rozjížděla v závislosti na kurzoru uživatele.

  1. Single-page application – po prvotním načtení už veškerá uživatelova interakce a komunikace se serverem probíhá pomocí AJAXu a nedochází ke znovunačítání stránky, což vede k rychlejší práci s aplikací a zvýšenému uživatelskému komfortu.

  2. Samozřejmě, že práce na backendu není jen o tomhle, ale s klientskými aplikacemi si připadám tak nějak blíž lidem 😉

  3. Víc, než jsem ochotný přiznat.

Zbavte se větví v kódu pomocí promises

Nemám rád rozvětvený kód. Více větví (if/elseif/else) představuje více kombinací k testování, komplikovanější a křehčí kód. Řady ifů se dá zbavit pomocí polymorfismu, to je známá a věřím, že dostatečně zakořeněná technika.

Dnes chci ale představit způsob, jak se můžete zbavit ifů, pokud splňujete tyto dva předpoklady:

  1. Váš kód je asynchronní.
  2. Na něco čekáte.

S asynchronním kódem se můžete běžně setkat ve světě JavaScriptu – v prohlížeči i na serveru v node.js. PHP může být asynchronní, pokud využijete ReactPHP. Všechny tyto implementace mají společné to, že běh aplikace je řízený pomocí event loop. Blokující operace, jako provádění HTTP requestů, dotazování do databáze, čtení z disku apod., se v asynchronním prostředí stávají neblokujícími. To znamená, že během čekání na jejich výsledek (odpověď od vzdáleného serveru, výsledek dotazu, obsah souboru) můžete spouštět jiný kód, proces aplikace se tak nikdy nenudí a využíváte efektivněji systémové prostředky.

Druhou podmínku splňujete, pokud váš kód obsahuje podmínky ve stylu:

  • „Už doběhl ten dotaz?“
  • „Podařilo se navázat spojení, nebo stále ještě čekáme?“
  • „Uplynulo X sekund?“

Cílem je upravit kód tak, aby fungoval, pokud už daná záležitost byla, nebo ještě nebyla splněna, a nemusel kód větvit před/po splnění.

Vezměte si následující příklad: Proces má vykonat nějakou práci a zároveň zůstat alespoň 5 sekund naživu, aby Supervisor jeho spuštění považoval za úspěšné. S běžným sychronním kódem bychom postupovali takto:

$startTime = microtime(true);

// vykonávám práci...

$stayAliveSeconds = 5;
$uptime = microtime(true) - $startTime;
if ($uptime < $stayAliveSeconds) {
    usleep(round(($stayAliveSeconds - $uptime) * 1000000));
}

exit(0);

Ošklivý if, který nás donutí při testování danou metodu spustit alespoň dvakrát.

Díky tomu, že naši consumeři front z RabbitMQ běží pod knihovnou Bunny a tedy v rámci event loop z ReactPHP, můžeme namísto řešení výše použít promises.

Promise je objekt, který se může nacházet ve tří stavech. Pending, resolved a rejected. Při pending čeká na výsledek, při resolved ho už získal a rejected představuje selhání. Na promise se pomocí metody then() navěšují zájemci o výsledek. Vedle promise žije ještě objekt deferred, který slouží jako ovladač ke své promise. Určuje, kdy bude jeho promise splněna. Pro splnění zapouzdření byste ven měli dát k dispozici pouze objekt promise, nikoli deferred.

To, co vám pomůže zbavit se ifů v kódu, je právě chování metody then(). Když ji voláte, tak nezáleží na tom, v jakém stavu se právě promise nachází. Pokud ještě není splněná, tak bude předaný callback zavolaný později, jinak okamžitě.

Příklad s usleep() se dá přepsat takto:

$stayAliveDeferred = new \React\Promise\Deferred();
$this->loop->addTimer(5, function () use ($stayAliveDeferred) {
    $stayAliveDeferred->resolve();
});
$stayAlivePromise = $stayAliveDeferred->promise();

// vykonávám práci...

$stayAlivePromise->then(function () {
    exit(0);
});

Pokud první část zrefaktorujete do zvláštní třídy PromiseTimer, protože se tato logika v kódu často opakuje, tak se kód zredukuje na příjemnější:

$stayAlivePromise = (new \PromiseTimer($this->loop))->wait(5);

// vykonávám práci...

$stayAlivePromise->then(function () {
    exit(0);
});

Zbavil jsem se nejen jakýchkoli ifů, ale i všech počtů s milisekundami.

Stejný trik lze použít na frontendu při získávání dat AJAXem. Pokud má o ta samá data zájem více komponent zároveň, ale chcete je od serveru žádat až pokud si o ně jedna z nich řekne, najednou ve vašem kódu bojujete s těmito stavy: Nikdo si o ta data ještě neřekl, data se právě stahují ze serveru, data už máme. Abyste se vyhli duplicitním požadavkům na server a dalším chybám, které se mohou projevit třeba v momentě, kdy se sejde rychle klikající uživatel na pomalém připojení, můžete napsat sadu nepřehledných a těžko testovatelných ifů, nebo využít promises. Veškerý kód, který má o data zájem, si o ně řekne pomocí then() – nebude tedy předpokládat, že už jsou stažena, ale pokud už jsou, zavolá se předaný callback okamžitě:

getProducts().then(function (products) {
    // ...
});

Why Is Everyone Outraged?

Phil Sturgeon naprosto racionálně, objektivně a se zdravým odstupem o nedávných kauzách ohledně Code of Conduct v PHP, politické korektnosti a rovnosti v IT:

So, instead of freaking out about problems you don’t understand, assuming everyone is just being overly sensitive, being absolutely awful to under-represented groups when they point out reasons they feel uncomfortable in the tech community, then having the gall to suggest there are far fewer of these people in the tech community because they aren’t as interested in tech… maybe take a few steps back and think about that whole situation.

Osobně jsem nikdy nepocítil potřebu po Code of Conduct v komunitách, ve kterých se pohybuji, ale také by mě v žádném případě nenapadlo zesměšňovat projevy těch, kteří po CoC volají. Nejsem v jejich kůži a respektuji, že tu potřebu mají. A nenávistné a trollící projevy těch, kteří proti CoC brojí, asi opravdu dokazují, že ho potřebujeme.

Slevomat Coding Standard

English version of the article is available on Medium.

Každý vývojářský tým by měl mít k dispozici kvalitní podpůrnou infrastrukturu, která pomáhá zajišťovat a vynucovat konzistentní výstupy všech jeho členů. Coding standard je jednou z mnoha věcí, které drží projekt pohromadě.

Ze zdrojového kódu by nemělo být poznat, kdo ho psal. Celý tým by měl mít jednotné zvyklosti. Některé (způsob formátování) jdou kontrolovat automaticky, jiné (např. dodržování navržené architektury) je potřeba řešit při code review.

Ve Slevomatu máme již rok a půl velmi striktní standard, který nám v každém pull requestu za pomoci PHP_CodeSnifferu kontroluje Jenkins. Před šesti měsíci jsme k základu v podobě Consistence Coding Standardu přidali řadu pokročilých sniffů, které jsme měli provizorně commitnuté v našem privátním repozitáři. Některé podporují i automatické opravy, což ulehčuje jejich integraci do projektu.

Na dnešek jsem čekal hodně dlouho. Tyto sniffy konečně vydáváme jako open-source pro veřejné použití. Vypíchnu zde ty, které považuji za nejzajímavější a nejužitečnější:

Nepoužité privátní properties a metody

SlevomatCodingStandard.Classes.UnusedPrivateElements

PHP_CodeSniffer je sice nevhodný pro statickou analýzu zdrojových kódů, protože umí analyzovat najednou pouze jeden soubor a nemá přístup k reflexi, ale některé specifické kontroly pomocí něj provádět jdou. Tento sniff detekuje nepoužité a write-only privátní properties a nepoužité metody, které lze bez obav smazat. Díky tomuto sniffu se pravidelně zbavujeme mrtvého kódu při refaktoringu a zbytečně injektovaných nepoužívaných závislostí v konstruktoru.

Čárka za posledním prvkem pole

SlevomatCodingStandard.Arrays.TrailingArrayComma

Čárka za posledním prvkem ve víceřádkovém poli zjednodušuje přidávání dalších prvků a zpřehledňuje verzovací diffy.

Zákaz Yoda podmínek

SlevomatCodingStandard.ControlStructures.YodaComparison

Pokud máte radši klasické pořadí při zápisu podmínek namísto Yoda stylu, tento sniff dokáže najít a dokonce i automaticky opravit prohřešky.

Abecedně seřazené uses

SlevomatCodingStandard.Namespaces.AlphabeticallySortedUses

Dobrý coding standard by měl vynucovat sjednocení podoby veškerého kódu, co jde objektivně sjednotit. Tento sniff odbourává hádky o tom, jak řadit importy z jiných jmenných prostorů na začátku každého souboru.

Nepoužité uses

SlevomatCodingStandard.Namespaces.UnusedUses

Další detekce mrtvého kódu. Proč trpět nadbytečné uses, když je tento sniff umí najít a dokonce i smazat? Poradí si i s Doctrine anotacemi.

Začněte je používat ještě dnes!

Zmíněné sniffy nejsou jediné, které jsme dnes vydali. Jejich kompletní výčet a vyčerpávající návod k použití celého standardu, ale i třeba jen pár jednotlivých sniffů, společně se zdrojovými kódy naleznete na GitHubu. Kód považujeme za tak prozkoušený a stabilní, že jsme neváhali vydat verzi 1.0.0.

Detekce neuzavřených transakcí

Vezměte si následující kód:

public function processRow(Row $row)
{
    $this->databaseConnection->begin();
    try {
        // nějaké počáteční kontroly
        // $isProcessed = ...

        if ($isProcessed) {
            return;
        }

        // spousta práce se zpracováním row

        $this->databaseConnection->commit();
    } catch (\Exception $e) {
        $this->databaseConnection->rollback();
        throw $e;
    }
}

Spatřili jste tu chybu? V případě, že $isProcessed === true, dojde k opuštění metody bez commitu či rollbacku transakce, čímž zůstane v databázi otevřená až do uzavření spojení. A může zbytečně držet příliš dlouho zámky, o které mohou mít zájem ostatní vlákna aplikace.

Pokud by vám nevadily zamčené řádky, tak se vám stejně nebude líbit další důsledek tohoto bugu:

$this->databaseConnection->begin();
try {
    // volaná metoda vyhodnotí předaný řádek jako $isProcessed
    $this->processRow($fooRow);
    $this->databaseConnection->commit();
} catch (\Exception $e) {
    $this->databaseConnection->rollback();
    throw $e;
}

V tomto případě totiž commitujete či rollbackujete jinou transakci, než byste čekali!

S lepším transakčním API se nastalá situace dá detekovat:

$databaseTransaction = $this->databaseConnection->begin();
try {
    // volaná metoda vyhodnotí předaný řádek jako $isProcessed
    $this->processRow($fooRow);
    $databaseTransaction->commit();
} catch (\Exception $e) {
    $databaseTransaction->rollback();
    throw $e;
}

V tomto případě je databázová transakce reprezentovaná odděleným objektem, takže je vždy jednoznačně určeno, s jakou konkrétní transakcí pracujeme.

Jak ale detekovat, že nám v aplikaci zůstává viset nevyřešená transakce? Pomůžeme si destruktorem:

class DatabaseTransaction
{

    //...

    public function __destruct()
    {
        if (!$this->resolved) {
            // destruktor nemůže vyhazovat výjimky
            trigger_error('Unresolved transaction!', E_USER_NOTICE);
        }
    }

    //...

}

Kdy ale PHP destruktor objektu zavolá?

The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence.

V závislosti na podobě implementace DatabaseConnection se destruktor zavolá jakmile PHP opustí metodu, ve které proběhlo přiřazení $databaseTransaction, a nebo taky ne, třeba v případě, že si všechny transakce ukládáme do interního pole pro pozdější odkazování.

Pokud se tedy destruktor transakce zavolá ihned po opuštění metody, tak v zalogované chybě dostaneme stack trace, ve které snadno dohledáme to místo, kde je špatně ošetřená transakce, a budeme ho moci opravit.

Pokud se destruktor zavolá až při shutdown sekvenci, tak už nám stack trace chyby o místu, kde vzniká neošetřená transakce, nic neřekne. V případě, že máte velkou mnohovrstevnatou aplikaci s mnoha transakcemi, se takové místo hledá velmi těžko.

Přišel jsem ale s funkčním řešením, které v této situaci debugging transakce usnadní. V PHP lze instanciovat výjimku, aniž bychom jí vyhazovali, a takto vytvořená výjimka si v sobě nese stack trace z místa svého vzniku!

class DatabaseTransaction
{

    /** @var UnresolvedTrasactionException */
    private $originException;

    public function __construct(DatabaseConnection $databaseConnection/*, ...*/)
    {
        // ...
        $this->originException = new UnresolvedTrasactionException();
    }

    public function __destruct()
    {
        if (!$this->resolved) {
            \Tracy\Debugger::log($this->originException);

            // destruktor nemůže vyhazovat výjimky
            trigger_error('Unresolved transaction!', E_USER_NOTICE);
        }
    }

    //...

}

Pro každou započatou transakci tedy vytvořím výjimku, kterou v případě nevyřešené transakce zaloguji a získám tak místo, kde chybná transakce vznikla. V destruktoru stále vyvolávám notice, aby si programátor této chyby všiml i během vývoje, kdy běžně složku s logy nesleduje.

Z hlediska čistoty a architektury kódu jde samozřejmě o dost neobvyklé až šílené řešení, které bych nikdy při code review nevpustil do běžné business logiky aplikace, kde si vývojář musí vystačit s běžnými vyjadřovacími prostředky a návrhovými vzory z OOP, ale v tomto případě jde o infrastrukturní pomocnou záležitost, která se v celé aplikaci vyskytuje právě jednou a nijak neovlivňuje architekturu zbytku aplikace. Pro usnadnění debuggingu a tedy ušetření času vývojáře jsem ochotný překročit hranici, ke které bych se běžně vůbec nepřiblížil.

What is Code?

Naprosto epický článek extrémní délky (38 tisíc slov) se spoustou animovaných ilustrací, který popisuje vše okolo vývoje software, co potřebujete vědět. Od pochopitelného popisu, jak funguje procesor a co všechno se musí stát, aby se znak stisknutý na klávesnici zobrazil na obrazovce, přes programovací jazyky, knihovny, algoritmy, debugging, datové struktury, databáze a verzování, až po vývojářské konference, flamewary a management. Pokud zvažujete nebo již máte kariéru v softwarovém vývoji, tohle je pro vás skutečně must-read.