PHP triky

Weblog o elegantním programování v PHP pro mírně pokročilé

Co se mi nelíbí na Go

Go je relativně nový programovací jazyk navržený v Google. Setkal jsem se s názorem, že do pěti let půjde o nejpoužívanější programovací jazyk, tak jsem se přihlásil na školení základů tohoto jazyka. Jinou zkušenost s Go nemám, takže moje postřehy jsou povrchní a nepodložené praxí (kromě příkladů, které jsme na školení dělali). Některé body se v praxi jako tak problematické projevit nemusí, některé další naopak můžou vyplout na povrch.

Go považuji za celkem zdařilý programovací jazyk, ovlivněný z mého pohledu především JavaScriptem, Pythonem a C. Místy dosahuje expresivity Perlu, ale přitom zůstává čitelný. Co se mi nelíbí?

  1. Ukazatele. V C a C++ jsem ukazatele vždycky nesnášel. V Go jsou naštěstí mnohem umírněnější, např. nejde ukazatel přetypovat, stejně by se bez nich ale podle mě vysokoúrovňový programovací jazyk měl obejít. PHP má místo ukazatelů reference, ty bych klidně taky zrušil (např. Recki-CT je nepodporuje).
  2. Magie. Dereferenci pointeru někde udělá kompilátor za vás, někde ji musíte udělat sami. Nezapamatoval jsem si, kde přesně, a nevidím důvod, proč by to nemohlo jít skoro všude. Typy na některých místech uvádět musíte, jinde ne. To je ale naštěstí celkem logické. V definici konstant se za určitých okolností nemusí uvádět hodnota a odvodí se z předchozí konstanty.
  3. Ošetření chyb. Podle mě jde o nejzásadnější problém jazyka. Neexistují výjimky, místo toho funkce obvykle vrací dvojici návratových hodnot (výsledek, chyba). Volající musí chybovou část návratové hodnoty zkontrolovat. Pokud to neudělá, tak se nic nestane. Pokud k chybě dojde, funkce stejně musí něco předat i ve výsledku, např. (0, chyba), což jen podporuje volajícího v tom, aby chybu nekontroloval. Pokud chcete zjistit, jestli v mapě existuje nějaký prvek, tak se to dělá také druhou návratovou hodnotou, tentokrát ale v přesně opačném významu než u funkcí (v, ok := a[1] oproti v, err = f()).
  4. Nejednotnost. Funkce append změněný slice vrátí, funkce delete mapu změní na místě.
  5. Zkratky. Na většině míst to ničemu nevadí a řádky jsou aspoň kratší. Ale aby se např. zřídka používaná funkce pro zjištění kapacity musela jmenovat cap, to si nemyslím.
  6. Slices. Slice je jen pohled na pole. Když máte dva různé pohledy na to stejné pole a změníte hodnotu v jednom z nich, tak se změní i ten druhý. Klidně můžete přistupovat i k prvkům za délkou slice, pokud to kapacita jeho pole umožňuje. Nevím, jestli tyto vlastnosti vedou k reálným chybám, ale přijdou mi velmi nebezpečné.

Go má i řadu příjemných vlastností, ale tyto problémy jsou pro mě natolik zásadní, že se Go nechystám začít používat.

Jakub Vrána, Seznámení s oblastí, 22.9.2014, diskuse: 17 (nové: 17)

V čem je PHP navrženo lépe než Java

Článek vyšel na serveru Zdroják.

Existuje spousta článků, které kritizují návrh PHP, nejznámější je asi PHP: a fractal of bad design. Na ten jsem napsal jen poměrně krátkou reakci, protože se zbytkem článku v zásadě souhlasím. PHP je skutečně v mnoha ohledech špatně navržený jazyk. V čem je ale navržený lépe než Java?

Anonymní funkce

Java dlouhou dobu neměla anonymní funkce a tzv. lambda expressions zavedla až letos. Do té doby se používaly jen krkolomné anonymní třídy. V PHP jsou naproti tomu anonymní funkce už pět let (2 roky s podporou $this) a do značné míry změnily, jakým způsobem se v PHP programuje.

Nutnost používání $this

V PHP se musí při přístupu k vlastnostem a metodám objektů vždy uvádět $this. V Javě to není potřeba, tedy pokud název vlastnosti nekoliduje s názvem lokální proměnné. Zní to jako výhoda Javy, ale mně dalo dost práce, než jsem si na to zvykl. Při pohledu do kódu Javy často přemýšlím – je tohle lokální proměnná, vlastnost objektu nebo je snad deklarovaná v rodiči? Připomíná mi to doby register_globals, kde jsem také často dumal, kde se nějaká proměnná vlastně vzala.

Hodnota null

V Javě je hodnota null implicitně povolená u všech neprimitivních datových typů. Ošetření této hodnoty prolézá kódem jako rakovina. Existuje pokus o řešení v podobě anotace NotNull, ale její použití a ostatně i definice jsou nejednotné. Jiný pokus o řešení téhle hrůzy je nově uvedená třída Optional, místo které ale taky můžete dostat null… V PHP naproti tomu jde do parametru s type hintem poslat null, jen pokud je volitelný. Je velká úleva nemuset v těle funkce přemýšlet o tom, jestli mi náhodou někde nemůže přijít null.

Volitelné parametry

Java nezná volitelné parametry. Místo toho si můžete nadefinovat více metod stejného jména, které přijímají různé parametry. V některých situacích se dají použít i varargs, které mimochodem do jazyka byly dohackované neuvěřitelným způsobem (lze je předat jako seznam parametrů nebo jako pole, takže pokud chcete předat jediný parametr obsahující pole, tak se pěkně zapotíte). Přetěžování metod obecně nepovažuji za dobrý nápad, často v kódu koukám na volání a lámu si hlavu tím, co se vlastně zavolá, a odpověď často poskytne až IDE. Přístup PHP, kdy jeden název může mít jen jedna metoda, mi vyhovuje mnohem líp. Jen je škoda, že pokud parametr může být různých typů bez společného předka, tak musíme oželet type hint. V tomhle se mi líbí Closure, kde se sjednocení různých typů používá zcela běžně.

API pro regulární výrazy

Java má hezký koncept explicitní kompilace regulárních výrazů. V PHP se zkompilované regulární výrazy automaticky kešují a programátor nad tím nemá kontrolu. To nevadí u krátkých skriptů, ale u dlouhoběžících aplikací (např. démonů) to může být problém, obzvlášť pokud někdo vytváří regulární výrazy dynamicky. Potíž s Javou je v tom, že funkce pro pohodlnou práci s regulárními výrazy String.matches a String.replaceAll přijímají regulární výraz ve tvaru řetězce a nikoliv jeho zkompilovanou podobu. To by mimochodem bylo jedno z mála smysluplných použití přetěžování. V Javě si tedy můžete vybrat, jestli budete používat pohodlné API s anti-patternem (za který se opakované používání nezkompilovaných regulárních výrazů považuje) nebo API nepohodlné. Mimochodem rozlišit metody přijímající řetězec nebo regulární výraz pomocí názvů replace a replaceAll nepostrádá notnou dávku zbabranosti.

Inicializace polí a map

Java dovoluje inicializovat pole konstrukcí {}, bohužel to jde ale jenom u deklarace. Předávání pohotově vytvořeného pole by se hodilo třeba v testech. Pole si musíte přiřadit do proměnné a tu teprve poslat funkci. Že byste proměnnou přepsali nějakým jiným pohotově vytvořeným polem, na to rovnou zapomeňte a na inicializaci map raději ani nemyslete. Když chcete vytvořit třeba konstantu s mapou, musíte ji inicializovat ve statickém konstruktoru. Guava nabízí alespoň jednoduchou možnost vytvoření ImmutableMap, na vytvoření Map ale nic standardního není. V PHP jde pole inicializovat triviálně, včetně přiřazení klíčů.

Pole a seznamy

Java vznikla s podporou polí (array), později doplnila také seznamy (List). Obojí se používá v zásadě pro to stejné, některá API používají pole, jiné seznamy. Trochu mi to připomíná nejednotnost PHP v chování k nativním polím a ArrayObject, zmatek v Javě je ale přeci jen o poznání větší.

Iterace

Java 1.5 přinesla jednoduchou možnost iterace pomocí for each. Pokud ale chcete procházet klíče i hodnoty mapy, tak se i s touto konstrukcí pěkně zapotíte. Asi nejčistší je použití metody entrySet, která vrací Map.Entry, na které můžete zavolat getKey a getValue. To by se místo:

for (Map.Entry<String, String> entry : map.entrySet()) {
    String key = entry.getKey();
    String value = entry.getValue();
}

… nemohlo psát třeba for (String key, String value : map) {}? V PHP je iterace klíčů i hodnot polí maximálně pohodlná.

Zbytečné přetypování

V Javě je potřeba na mnoha místech zbytečně přetypovávat. Např. v tomto kódu:

if (x instanceof String) {
    f((String) x);
}

To by si kompilátor nemohl domyslet, že x nemůže být jiného typu než String? Closure Compiler to zvládne. V PHP žádné přetypování na třídy neexistuje.

Kompilace

PHP skript spustíte a okamžitě běží. Java se nejprve musí zkompilovat, což je např. ve srovnání s Go neuvěřitelně zdlouhavý proces. Kompilace má svou výhodu v tom, že se při ní zaručeně dozvím o chybách, na které by se při běhu ani nemuselo narazit. V PHP tuto roli suplují lintery. Kompilaci a spuštění bych si představoval oddělené. Nerad bych se vzdával všech chyb, o kterých se díky kompilaci dozvím, zároveň bych ale chtěl mít možnost program rychle spustit. Java stejně zkompilovaný bajtkód optimalizuje při běhu, to by to nemohla dělat rovnou ze zdrojového kódu?

Porovnávání řetězců

Řetězce jsou v Javě objekty, takže se nedají porovnávat operátorem ==. Místo toho musíte použít metodu equals, takže místo $a != "a" se obvykle píše podle mě zcela nepřehledné !"a".equals(a). Nejhorší ze všeho je, že Java používá tzv. string interning, takže program porovnávající řetězce pomocí == bude normálně fungovat až do té doby, kdy z ničeho nic fungovat přestane.

import *

Když PHP zavedlo jmenné prostory, tu a tam někdo naříkal, že nepodporuje import všech objektů z daného jmenného prostoru. Java tuto možnost má a v jednorázovém skriptu se docela hodí napsat si import java.util.* nebo něco podobného. V seriózních projektech jde ale o anti-pattern, protože co se asi stane, když nějaký jmenný prostor přidá třídu, jejíž název už používáte z jiného jmenného prostoru (ideálně také přes hvězdičku)?

Vlákna a synchronizace

Může být užitečné vytvořit si více vláken, ale způsob, jakým se s nimi potom v Javě pracuje, je tak nízkoúrovňový, že bych se bez toho snad radši obešel. Mluvím především o potřebě synchronizace. Přístup PHP, kde všechno běží v jednom vlákně nebo procesu a o spuštění více věcí najednou se stará webový server, mi vyhovuje mnohem víc. Když už je někdy potřeba dělat víc věcí najednou v rámci jednoho skriptu, tak je to obvykle čekání na výsledek, a to jde v PHP poměrně snadno pomocí futures. Ostatně i v JavaScriptu plném callbacků najednou běží vždy jen jeden kód.

Závěr

Článek se jednostranně zaměřuje na nevýhody Javy ve srovnání s PHP z mého pohledu. Rozhodně nejde o nezávislé porovnání obou jazyků, ale spíše o ukázku toho, kde měli tvůrci PHP šťastnější ruku. Napadají vás další oblasti, kde je Java špatně navržená? Podělte se o ně v diskuzi. Naopak se prosím zdržte kritiky PHP, ať se zaujatý pohled článku zbytečně nerozmělní.

Viz též Effective Java.

Jakub Vrána, Seznámení s oblastí, 4.7.2014, diskuse: 3 (nové: 3)

Rekurze regulárních výrazů

Když někdo říká, že umí dobře regulární výrazy, tak mu obvykle položím tuto otázku: „Jaký je rozdíl mezi once-only subpattern a possessive quantifier a kdy je použiješ?“ Schválně si na tuhle otázku teď zkuste odpovědět.

Once-only subpattern (?>) se vyznačuje tím, že zkonzumuje, co se dá, a nikdy se nevrací. Pokud tedy za zkonzumovanou částí nenajde text odpovídající zbytku regulárního výrazu, tak skončí chybou. Jindy se v takových situacích pokusí regulární stroj vrátit a hledat shodu dřív.

Possessive quantifier se zapisuje jako plus za kvantifikátorem, např. *+. Tento výraz je velmi podobný – sežere, co jde, a nikdy se nevrací. Jediný rozdíl je tedy v tom, že possessive quantifier je trochu stručnější, pokud chceme vyjádřit opakování. a*+ se dá zapsat jako (?>a*). Naopak to jde taky, i když trochu krkolomněji – (?>ab) se dá zapsat jako (?:ab){1}+. Odpověď na první část otázky tedy je, že rozdíl mezi těmito výrazy není žádný a používají se ve stejných situacích.

Používají se tehdy, když potřebujeme zabránit rekurzi, kterou regulární stroj používá při hledání shody. Na její limit (pcre.recursion_limit) můžeme narazit především při použití opakování uvnitř opakování, např. (\d+,)*.

Alternativou k regulárním strojům používajícím rekurzi nabízí knihovna RE2, která zaručuje běh v lineárním čase. Je trochu omezenější než Perlové regulární výrazy, např. nepodporuje lookahead aserce. Existuje i verze pro PHP, nicméně kvůli její nedostatečné rozšířenosti ji nepoužívám.

Jakub Vrána, Seznámení s oblastí, 25.6.2014, diskuse: 1 (nové: 1)

Adminer 4.1.0

Asi největší novinkou právě vydané verze Admineru je ochrana proti pokusu o získání hesla k databázi hrubou silou. Adminer nově dovolí jen 30 neplatných pokusů o přihlášení za 30 minut (počítáno od prvního neplatného pokusu). Další pokusy (ať už se správným nebo špatným heslem) se ignorují, dokud nevyprší časový limit. Limit se vztahuje na IP adresu, nikoliv na uživatelské jméno. To je ze dvou důvodů:

  1. Útočník může zkoušet různá uživatelská jména, nemusí být zaměřen jen na jedno.
  2. Při vazbě na uživatelské jméno by útočník mohl oprávněnému uživateli zabránit v přihlášení úmyslným zadáváním špatných hesel (DoS).

Limit je poměrně velkorysý, protože mě rozčiluje, když mi některé služby třeba po třech neplatných pokusech o přihlášení ukážou dlouhý nos. Také jsem zohlednil, že stejnou IP adresu může používat více uživatelů, např. ze stejné firmy s proxy serverem. Pokud máte webový server umístěný za reverzní proxy a všechny požadavky chodí ze stejné IP adresy, tak je vhodné implementovat metodu Adminer::bruteForceKey() tak, aby vracela poslední část hlavičky X-Forwarded-For. Jinak vám hrozí DoS. Hlavička X-Forwarded-For se bohužel nemůže kontrolovat automaticky, protože jde triviálně podstrčit.

Přemýšlel jsem i o přidání CAPTCHA, což by zamezilo riziku DoS. reCAPTCHA ale používá privátní klíč, který by musel být součástí Admineru, a ostatní řešení jsou obvykle celkem snadno prolomitelná.

Data se ukládají do upload_tmp_dir nebo pokud není nastaven, tak do sys_get_temp_dir(). Tyto adresáře někdo pravidelně promazává, nicméně vzhledem k poměrně krátké platnosti dat by to nemělo moc vadit.

I přes tuto ochranu je vhodné přístup k Admineru omezit, např. v Apache pomocí Access Control. Případně pokud Adminer použijete někde jen jednorázově, tak ho zase po sobě smažte. Odkaz na Adminer také nikam zbytečně nedávajte a neumisťujte ho na snadno uhodnutelnou adresu. Pokud Adminer používají zákazníci vašeho hostingu, tak jim k němu umožněte přístup jen pokud jsou přihlášeni do administrace hostingu.

Další novinky

Jakub Vrána, Adminer, 18.4.2014, diskuse: 36 (nové: 36)

Přechod na SafeHtml

Ve většině moderních aplikací vzniká HTML kód převážně v šablonách. To má nespornou výhodu v tom, že ošetřování dat stačí vyřešit na jednom místě. Můžeme použít automatické escapování, kontextově citlivé escapování, můžeme úplně zakázat vypsat text bez jeho ošetření. I v těchto aplikacích může být ale šikovné si tu a tam vygenerovat HTML kód, který do šablony pošleme. Můžeme totiž využít plnou sílu programovacího jazyka – metoda může vracet HTML kód a potomek ji může přetížit a vrátit něco jiného. Samozřejmě si tyto metody vevnitř můžou také zavolat šablonu, ale jednak někdy šablonovací systém nemají po ruce a jednak to je trochu jako kanón na vrabce, když chtějí vrátit třeba jenom <em>text</em>.

Facebook

Facebook žádné šablony nepoužívá a když nějaká část programu chce vytvořit HTML, tak to udělá pomocí XHP. Vede to k trochu odlišnému způsobu programování – místo velké aplikace, která generuje datové struktury, a velkých šablon, které tyto struktury zpracovávají a které jsou od hlavního kódu obvykle poměrně daleko, je všechno pěkně pohromadě. XHP je obvykle malé a jednoduché, protože vytváří jen malou část výsledku s využitím dalších komponent, které taky vytváří XHP. Všechny výhody šablon týkající se ošetřování dat jsou přitom díky XHP zachovány.

Phabricator

Phabricator vznikl ve Facebooku a je napsán do značné míry podobně. Nepoužívá žádné šablony, HTML kód se vytváří lokálně s využitím síly programovacího jazyka. Problém je, že nemá k dispozici XHP, takže HTML kód se vracel jako ručně ošetřované řetězce. Vzhledem k tomu, že kód vytváří velmi zkušení programátoři a všechny změny prochází code review, tak v historii nedošlo k mnoha případům nedostatečného nebo nadbytečného ošetřování. S kódem se ale pracovalo poměrně nepohodlně především proto, že nebylo jasné, co vrací nebo přijímá správně ošetřený HTML kód a co vrací nebo přijímá čistý text, který je nutno ošetřit.

Navrhl a realizoval jsem proto změnu, po které se všechny řetězce považují za čistý text a pokud něco chce vytvořit ošetřený HTML kód, tak musí vrátit PhutilSafeHtml. Vtip je v tom, že tento objekt si nemůže vytvořit jen tak někdo, ale musí k tomu použít jednu ze dvou funkcí. Buď phutil_tag, která přijímá název značky, pole atributů a obsah, nebo hsprintf, která přijímá formátovací řetězec a libovolný počet hodnot, které se před dosazením ošetří. Obsah může být buď řetězec, který se automaticky ošetří, nebo PhutilSafeHtml, kterému se věří. hsprintf v prvním parametru přijímá jen konstantní řetězec, za což ručí lint rule.

Při pohledu zpět se funkce hsprintf ukázala jako zbytečná – někdy je sice jednodušší ji použít, ale zbytečně zmírňuje silné záruky, které dává funkce phutil_tag. Ta dovoluje vytvořit jen správně spárované HTML značky, navíc může kontrolovat např. i hodnotu atributu <a href="">, aby nezačínala třeba javascript:. hsprintf nic z toho dost dobře dělat nemůže. Úplně by stačila funkce vytvářející značku a funkce spojující kombinaci neošetřených řetězců a PhutilSafeHtml do jednoho PhutilSafeHtml. A možná ani ta ne, protože funkce jako obsah přijímá i pole hodnot, které zřetězí.

Když odhlédnu od množství nudné práce, kdy bylo potřeba všechny řetězce vytvářející HTML kód převést na jednu z těchto dvou funkcí, tak byla změna vlastně docela jednoduchá. Pokud někdo konzumoval čistý text a dostal PhutilSafeHtml, tak se zavolala jeho metoda __toString. Maximálně hrozilo, že pokud tento text poslal dál, tak se při příštím použití znovu ošetřil. Tyto případy se nám podařilo celkem rychle vychytat. Pokud někdo začal konzumovat PhutilSafeHtml a dostal řetězec, tak ho prostě poslal dál – všechny funkce považují řetězec za platný vstup, který se na PhutilSafeHtml ve finále převede jeho ošetřením. Díky tomu není potřeba nikde v kódu ručně ošetřovat jakákoliv data.

Proč mi změna přišla vlastně docela jednoduchá, i když se týkala desítek tisíc řádek kódu? Hlavní důvod je ten, že celý kód máme pod kontrolou – pokud se rozhodneme změnit API nějaké metody, tak můžeme změnit i všechna její volání.

Closure Library

Obdobnou změnu nyní pomáhám realizovat i pro Closure Library. Closure Tools sice obsahuje vynikající šablony Closure Templates (alias Soy), knihovna na nich ale nezávisí, takže je nemůže používat.

Snažíme se odstranit všechna místa, která přiřazují řetězec do innerHTML, a nahradit je voláním funkce, která přijímá SafeHtml. Pokud budete chtít SafeHtml vytvořit z uživatelského vstupu, tak k tomu můžete použít funkci SafeHtml.from, která pro vás data ošetří. Pro vytvoření něčeho složitějšího jsem původně navrhl takovéto API:

var html = new SafeHtmlBuilder(TagName.A)
	.setAttribute('href', href)
	.addContent(text)
	.addContent(new SafeHtmlBuilder(TagName.BR).build())
	.build();

Byl to moc velký Javismus, takže jsme nakonec skončili u něčeho podobného jako ve Phabricatoru: SafeHtml.create('a', {'href': href}, [text, SafeHtml.create('br')]). Kromě toho vznikla ještě funkce SafeHtml.concat umožňující spojit více SafeHtml do jednoho.

Převod stávajícího API tak jednoduchý jako ve Phabricatoru bohužel nebude. Vezměme si třeba metodu goog.ui.tree.BaseNode#getHtml. Ta může být jak přetížená, tak ji může kdokoliv volat. Nemůžeme ji tedy změnit tak, aby prostě začala vracet SafeHtml, protože bychom všechny rozbili a navíc by naši potomci SafeHtml stejně nevraceli. Zvolili jsme tedy takovéto, poněkud krkolomné řešení:

Závěr

Zápis HTML jako řetězce sice napomohl jeho masivnímu rozšíření, ale ještě dnes přináší problémy i ve velkých společnostech. Pokud někde ve svém kódu uvidíte něco jako return "<br/>", tak je nejspíš vaše infrastruktura špatně a vyplatí se investovat energii do toho ji co nejdříve změnit. Čím později to uděláte, tím to bude bolestivější.

Jakub Vrána, Řešení problému, 12.3.2014, diskuse: 5 (nové: 5)

Starší články naleznete v archivu.

avatar © 2005-2014 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.