Pokud píšete vlastní error handler pro PHP, je
bezpodmínečně nutné dodržet několik pravidel. Jinak může nabourat
chování dalších knihoven a aplikací, které nečekají v error
handleru zradu.
Parametry
Signatura handleru vypadá takto:
function errorHandler(
int $severity,
string $message,
string $file,
int $line,
array $context = null // pouze v PHP < 8
): ?bool {
...
}
Parametr $severity obsahuje úroveň chyby
(E_NOTICE, E_WARNING, …). Pomocí handleru nelze
zachytávat fatální chyby, jako třeba E_ERROR, takže těchto
hodnot nikdy nebude parametr nabývat. Naštěstí fatální chyby v podstatě
z PHP zmizely a byly nahrazeny za výjimky.
Parametr $message je chybová hláška. Pokud je zapnutá
direktiva html_errors,
jsou speciální znaky jako < apod. zapsány jako HTML entity,
takže do podoby plain textu je musíte dekódovat.
Ovšem pozor, některé znaky jako entity zapsány nejsou, což je bug.
Samotné zobrazování chyb v čistém PHP je tak náchylné na XSS.
Parametry $file a $line představují název
souboru a řádek, kde k chybě došlo. Pokud chyba nastala uvnitř
eval(), bude $filedoplněný o tuto informaci.
A nakonec parametr $context obsahuje pole lokálních
proměnných, což představuje pro debugování užitečnou informaci, ale od
PHP 8 je zrušený. Pokud má handler fungovat v PHP 8, parametr vynechte nebo
mu dejte výchozí hodnotu.
Návratová hodnota
Návratová hodnota handleru může být null nebo
false. Pokud handler vrátí null, nestane se nic.
Pokud vrátí false, zavolá se ještě standardní PHP handler.
Ten podle konfigurace PHP může chybu vypsat, zalogovat atd. Co je důležité,
tak že také naplní interní informaci o poslední chybě, kterou
zpřístupňuje funkce error_get_last().
Potlačené chyby
V PHP lze potlačit zobrazování chyb buď pomocí shut-up operátoru
@ nebo pomocí error_reporting():
// potlač chyby úrovně E_USER_DEPRECATED
error_reporting(~E_USER_DEPRECATED);
// potlač všechny chyby při volání fopen()
$file = @fopen($name, 'r');
I při potlačení chyb dojde k volání handleru. Proto je nejprve
nutné ověřit, zda chyba je potlačená, a pokud ano, tak musíme
vlastní handler ukončit:
if (!($severity & error_reporting())) {
return false;
}
Ale pozor, musíme je v tomto případě ukončit pomocí
return false, aby se spustil ještě standardní error handler. Ten
nic nevypíše ani nezaloguje (protože chyba je potlačená), ale zajistí, že
chybu půjde zjistit pomocí error_get_last().
Ostatní chyby
Pokud náš handler chybu zpracuje (například vypíše vlastní hlášku
atd.), už není potřeba volat standardní handler. Sice pak nebude možné
chybu zjistit pomocí error_get_last(), ale to v praxi nevadí,
protože tato funkce se používá především v kombinaci s shut-up
operátorem.
Pokud handler naopak chybu z jakéhokoliv důvodu nezpracuje, měl by
vrátit false, aby ji nezatajil.
Ukázkový příklad
Takto by vypadal kód vlastního error handleru, který transformuje chyby na
výjimky ErrorException:
set_error_handler(function (int $severity, string $message, string $file, int $line) {
if (!(error_reporting() & $severity)) {
return false;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
Hurá, Nette už má první záznam v CVE! To znamená, že
v něm byla objevena první vážná zranitelnost. Co se vlastně stalo?
Na konci prázdnin mi napsal vývojář Cyku
Hong z malebného Taiwanu, že našel v Nette zranitelnost a
v následujícím e-mailu vysvětlil princip možného zneužití. Ověřil
jsem, že jde o uskutečnitelný útok. Dovoluje útočníkovi za určitých
okolností na některých webech pomocí speciálně sestaveného URL spustit
kód, tedy jde o zranitelnost Remote code execution (RCE). Cyku, díky!
Musím říct, že to bylo v 13leté historii frameworku Nette vlastně
poprvé, co někdo našel takto závažnou zranitelnost. Dříve byly
několikrát reportovány drobné záležitosti, např. letos v březnu Jan
Gocník odhalil možnou zranitelnost v případě, že by programátor
deserializoval a vypsal query proměnnou
echo unserialize($_GET['a']), což je samo o sobě principiálně
velmi nebezpečné, nicméně jeho nález jsem samozřejmě opravil. Také jsem
dostal řadu hlášení, které nebyly opodstatněné, například že
uploadovaný obrázek vyhovující testu isImage() může v sobě
obsahovat PHP kód. Což samozřejmě může, například v metadatech, ale
není to bezpečnostní problém Nette.
Ale zpět k chybě, o které je tento článek. Bezprostředně po
nahlášení jsem ji opravil a vydal nové verze balíčků
nette/application a nette/nette.
Nejstarší zasaženou verzí bylo Nette 2.0, které už sice není 6 let
udržované, ale protože Nette má bezpečnost jako jednu z priorit, vydal
jsem nové verze také u všech nepodporovaných verzí. Což je ve světě
opensource frameworků ojedinělý krok. Díky tomu mohou uživatelé snadno
a bez prodlení aktualizovat nejen projekty udržované a běžící na
současných verzích, ale i projekty s technologickým dluhem. Vlastně
se teď dá říci, že každá řada Nette je nejen Long-Term Support
Release (tedy podporovaná alespoň dva roky, viz tabulka), ale z pohledu
bezpečnostních fixů i Forever-Term Supported 🙂
Druhým krokem bylo o chybě informovat. Samotné zveřejnění chyby na
blogu by i bez podrobného popisu zneužití představovalo vodítko pro
darebáky, kteří by se o chybě dozvěděli a mohli se pokusit ji zneužít.
Proto mi připadalo fér nejprve informovat všechny podporovatele Nette, poté i další
uživatele na které mám kontakt a teprve s určitým časovým odstupem
publikovat oznámení veřejně na blogu,
GitHubu
a katalogu
CVE. Prostě dát partnerům určitý čas zaktualizovat všechny weby
dříve, než by se objevil první útočník. Původně jsem zamýšlel dát
odstup týden, ale pak jsem na základě diskusí pochopil, že to je doslova
šibeniční termín a vhodnější je dát alespoň 2–4 týdny.
Jak už jsem zmiňoval, šlo o mou první zkušenost s takovou situací,
ale chtěl jsem ji zvládnout příkladně. Abych se nedopustil žádného
přešlapu, napsat jsem Michalu
Špačkovi, kterého považuji za nejlepšího odborníka v této oblasti,
a všechno s ním konzultoval. Michal mi schválil postup, dal řadu
užitečných rad, připomínkoval emaily atd. Michale, moc děkuji!
Ačkoliv žádný z mnou provozovaných webů nebyl tímto způsobem
zranitelný, prohledal jsem jejich access logy za posledních 8 let (co díra
existuje) a zjistil, že tento typ útoku na ně historicky nikdo nezkusil.
Soudím, že na zranitelnost nikdo dříve nepřišel. Útočníci totiž
obvykle zkouší testovat také přímo web nette.org.
Nechci zveřejňovat přesný postup zneužití chyby a doufám, že to ani
nikdo jiný neudělá. Alespoň ne v dohledné době, protože by tím
způsobil ostatním nepříjemnosti a zpronevěřil se duchu open source.
Aktualizujte prosím co nejdříve na nejnovější
setinkové verze:
nette/application 3.0.6 (případně 3.0.2.1, 3.1.0-RC2 nebo dev)
nette/application 2.4.16
nette/application 2.3.14
nette/application 2.2.10
nette/nette 2.1.13
nette/nette 2.0.19
Nejrychlejší oprava
Michal připravil Linuxový
skript a já obdobu
v PHP, který automaticky aplikuje patch přímo do zdrojových kódů
Nette na disku. Hodí se v případě, že udržujete velké množství
projektů, které nemáte čas korektně aktualizovat pomocí Composeru.
Informaci o chybě rozeslal emailem svým klientům VSHosting, WEDOS nebo
HostingBB, zároveň některé hostingy přímo blogují problematickou URL,
případně rovnou aplikovaly výše uvedený fix. Díky!!!
SameSite cookies poskytují mechanismus, jak rozpoznat, co
vedlo k načtení stránky. Jestli to bylo prokliknutí odkazu na jiném webu,
odeslání formuláře, načtení uvnitř iframe, pomocí JavaScriptu atd.
Rozlišit, jak byla stránka načtena, je totiž naprosto zásadní kvůli
bezpečnosti. Závažná zranitelnost Cross-Site
Request Forgery (CSRF) je tu s námi už dlouhých dvacet let a teprve
SameSite cookie nabízí systémovou cestu, jak ji řešit.
Útok CSRF spočívá v tom, že útočník naláká oběť na stránku,
která nenápadně v prohlížeči oběti vykoná požadavek na webovou
aplikaci, na které je oběť přihlášena, a ta se domnívá, že požadavek
vykonala oběť o své vůli. A tak pod identitou oběti provede nějaký
úkon, aniž by ta o tom věděla. Může jít o změnu nebo smazání dat,
odeslání zprávy atd. Aby aplikace útoku zabránila, musí rozlišit, jestli
požadavek vznikl povolenou cestou, např. odesláním formuláře v ní
samotné, nebo nějak jinak. SameSite cookie tohle umí.
Jak to funguje? Řekněme, že mám web běžící na nějaké doméně a
vytvořím na něm tři různé cookies s atributy SameSite=Lax,
SameSite=Strict a SameSite=None. Název ani hodnota
nehrají roli. Prohlížeč si je uloží.
Když libovolnou URL na mém webu otevřu přímým zadáním do adresního
řádku nebo kliknutím na záložku, prohlížeč všechny tři cookie
odešle.
Když se na libovolnou URL na mém webu dostanu jakkoliv ze stránky
z téhož webu, prohlížeč všechny tři cookie odešle.
Když se na libovolnou URL na mém webu dostanu ze stránky z jiného
webu, prohlížeč pošle jen cookie s atributem None a
v určitých případech i Lax, viz tabulka:
Kód na jiném webu
Odeslané cookie
Link
<a href="…">
None + Lax
Form GET
<form method="GET" action="…">
None + Lax
Form POST
<form method="POST" action="…">
None
iframe
<iframe src="…">
None
AJAX
$.get('…'), fetch('…')
None
Image
<img src="…">
None
Prefetch
<link rel="prefetch" href="…">
None
…
None
SameSite cookies dokáží rozlišit jen několik málo případů, ale jde
právě o ty podstatné pro ochranu před CSRF.
Pokud mám třeba na webu v administraci formulář nebo nějaký odkaz pro
smazání položky a ten byl odeslán/odkliknut, tak nepřítomnost cookie
vytvořené s atributem Strict znamená, že se tak nestalo na
mém webu, ale že požadavek přišel odjinud, tedy že jde o CSRF útok.
Cookie pro odhalení CSRF útoku vytvářejte jako tzv. session cookie bez
atributu Expires, platnost je pak v podstatě nekonečná.
Doména vs site
„Na mém webu“ není to stejné jako „na mé doméně“, nejde
o doménu, ale o web site (proto i název SameSite). Site sice často
odpovídá doméně, ale třeba u služby github.io odpovídá
subdoméně. Požadavek z doc.nette.org na
files.nette.org je same-site, zatímco požadavek z
nette.github.io na tracy.github.io je už cross-site.
Tady je to hezky
vysvětlené.
<iframe>
Z předchozích řádků již vyplynulo, že pokud je stránka z mého webu
načtená uvnitř <iframe> na jiném webu, nepošle jí
prohlížeč Strict ani Lax cookies. Je tu ale ještě
jedna důležitá věc: pokud takto načtená stránka vytvoří
Strict nebo Lax cookie, prohlížeč je
ignoruje.
Tím vzniká možnost se bránit proti podvodnému získávání cookie
neboli Cookie
Stuffing, kde dosud systémová obrana taky chyběla. Trik spočívá
v tom, že podvodník inkasuje provizi za affiliate marketing, ačkoliv
uživatele na web obchodníka nepřivedl. Místo odkazu s affiliate ID, na
který by musel uživatel kliknout, vloží do stránky neviditelný
<iframe> se stejným odkazem a značkuje tak všechny
návštěvníky.
Cookie bez atributu SameSite
Sušenky bez atributu SameSite se vždy posílaly při jakémkoliv same-site
i cross-site požadavku. Stejně jako SameSite=None. Jenže
v blízké budoucnosti začnou prohlížeče považovat příznak
SameSite=Lax za výchozí, takže sušenky bez atributu budou
považovány za Lax. Což je docela nebývale velký BC break
v chování prohlížečů. Pokud chcete, aby se cookie i nadále chovala
stejně a přenášela se při jakémkoliv cross-site požadavku, je potřeba
jí nastavit SameSite=None. (Pokud nevyvíjíte embedované widgety
apod., moc často to nechcete.) Bohužel pro loňské prohlížeče je hodnota
None nečekaná. Safari 12 ji chápe jako Strict,
takže na starších iOS a macOS vzniká ošemetný problém.
A ještě pozor: None funguje jen když je nastaven
s atributem Secure.
Co udělat při útoku?
Utéct! Základní pravidlo sebeobrany, jak v reálném životě, tak na
webu. Obrovskou chybou spousty frameworků je, že při detekci CSRF útoku
zobrazí formulář znovu a napíší něco jako „Token CSRF je neplatný.
Zkuste prosím formulář znovu odeslat“. Tím, že jej uživatel odešle
znovu, je útok dokonán. Taková ochrana postrádá smysl, když vlastně
uživatele vyzvete, aby ji obešel.
Ještě nedávno dělal Chrome v případě cross-site požadavku to, že po
refreshi stránku zobrazil znovu, ale tentokrát cookie s atributem
Strict poslal. Takže refresh vyřadil ochranu před CSRF
založenou na SameSite cookie. Dnes už to naštěstí nedělá, ale je možné,
že to dělají jiné nebo starší prohlížeče. Uživatel také může
stránku „refreshnout“ kliknutím na adresní řádek + enter, což se bere
jako přímé zadání URL (bod 1) a všechny cookie se odešlou.
Takže při detekci CSRF je nejlepší přesměrovat s HTTP kódem
302 jinam, třeba na homepage. Zbavíte se tak nebezpečných POST dat a ani
problematická URL se neuloží do historie.
Nekompatibility
SameSite dlouho nefungovalo ani zdaleka tak, jak by mělo. Především
kvůli chybám v prohlížečích a nedostatkům ve specifikaci, která třeba
vůbec neřešila přesměrování nebo refresh. Samesite cookie se
nepřenášely třeba při uložení nebo tisku stránky, naopak se přenášely
po refreshi, když zrovna neměly atd. Naštěstí dnes už je situace lepší.
Mám za to, že z vážných nedostatků přetrvává v aktuálních verzích
prohlížečů jen ten výše zmíněný u Safari.
Doplnění: kromě SameSite lze velmi čerstvě rozlišit původ
požadavku i hlavičkou Origin,
což je nástupce hlavičky Referer více respektující soukromí uživatelů a
pravopis.
Abych si přiblížil vývoj na lokálním počítači tomu
ostrému, zprovoznil jsem si lokální https. Jak na to?
Nejsnazší cesta
V komentářích padla zmínka o aplikaci mkcert, která následující
postup udělá za vás. Ale pokud máte rádi ruční práci, čtěte
dál :)
Vygenerujeme SSL certifikát
K tomu použijeme program openssl, který určitě na
počítači najdete, třeba jako součást Gitu na
C:\Program Files\Git\usr\bin\openssl.exe nebo jinde. Následující
příkaz vygeneruje klíč do souboru rootCA.key. Bude to po vás
chtít vymyslet nějaké heslo, které si uložte.
openssl genrsa -des3 -out rootCA.key 2048
Další příkaz z klíče vygeneruje kořenový SSL certifikát a uloží
ho do souboru rootCA.pem. Jeho platnost bude 2000 dní (tedy pět
let), ale číslo klidně změňte:
Teď oznámíme operačnímu systému, aby certifikát považoval za
důvěryhodný. Postup pro Mac jsem našel třeba
tady, pro Windows je pěkně obrázek za obrázkem sepsaný
zde a pro české Windows jsem vám jej přeložil:
spustit mmc
v menu Soubor > Přidat nebo odebrat moduly snap-in
doubleclick na Certifikáty v levém okně
zvolte Účet počítače a Další
ponechte Místní počítač a Dokončit
zavřete okno tlačítkem OK
v levém okně zvolte Certifikáty, v pravém Důvěryhodné kořenové
certifikační autority
pravé tlačítko a v kontextovém menu Všechny úkoly >
Importovat
objeví se Průvodce importem certifikátu, dejte Další
vyberte soubor rootCA.pem a odklikejte průvodce na konec
Ufff, jsme v půlce. Pokračujeme v Konzoli:
opět v menu Soubor > Přidat nebo odebrat moduly snap-in
doubleclick na Editor objektů zásad skupiny
stiskněte Dokončit a zavřete okno tlačítkem OK
v levém okně otevřete Místní počítač – zásady > Konfigurace
počítače > Nastavení systému Windows > Nastavení zabezpečení >
Zásady veřejných klíčů
doubleclick na Nastavení ověření cesty certifikátů
zatrhněte Definovat tato nastavení, Povolit ověřování certifikátů
… a Povolit uživatelům důvěřovat certifikátum …
potvrďte tlačítkem OK
Hotovo, zavřete Konzoli.
Všichni už certifikátu věří, akorát Firefox chce ještě
popostrčit:
Klikačku máme za sebou, teď vyrobíme certifikáty pro server. Vytvoříme
soubor server.csr.cnf s tímto obsahem:
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
[dn]
C = CZ
ST = Random
L = Random
O = Random
OU = Random
emailAddress = example@example.com
CN = localhost
A dále soubor v3.ext, kde bude uveden seznam všech domén,
které na localhostu provozujete:
Jak vidíte, lokální verze webů provozuji na doménách, které končí na
.l, u vás to třeba bude jinak. Ty jednotlivé klíče
DNS.1 apod. je potřeba fakt jako idiot postupně očíslovat.
Následujícím krokem vygenerujeme soubory server.key
aserver.csr:
Všechny dosud vytvořené soubory včetně hesla si někam uložte. Ať se
totiž změní domény, které provozujete lokálně, jen upravíte
v3.ext a posledním příkazem znovu vygenerujete certifikát pro
server.
Konfigurace webového serveru
Zbývá povolit https na serveru. Tj. nakonfigurovat server tak, aby
naslouchal na portu 443, na kterém běží https, a používal vygenerovaný
certifikát.
Používám Apache, do jehož konfiguračního soubor httpd.conf
jsem přidal následující řádky s cestou k souborům
server.key aserver.crt:
Stránky Nette existují ve dvou jazykových verzích: české
a anglické. Udržovat tak obsáhlý projekt ve dvou mutacích vyžaduje dost
práce, ale vidím v tom smysl.
Plnohodnotná česká dokumentace a fórum je totiž výhodou pro zdejší
začínající programátory, protože žádný jiný framework ničím takovým
nedisponuje. Samozřejmě ideální by bylo, kdyby každý Čech už od
základní školy uměl plynně anglicky, ušetřilo by to hodně práce, ale
není úkolem frameworku tohle suplovat. Nicméně angličtina není
v žádném případě druhotným jazykem. Naopak. Posuďte sami:
Dokumentace. Její současná velikost
čítá 3 MB textů (pro srovnání celé
dílo Shakespeare má 5 MB). Anglická a česká jsou přitom zcela identické.
Nejsem si vědom, že by jeden jediný odstavec existoval třeba v české
verzi a anglické chyběl. Případně by se v jedné verzi objevil mnohem
dříve než v druhé. Obě verze jsou neustále synchronizované. Pokud byste
na nesoulad narazili, vytvořte prosím issue na GitHubu.
Anglické forum bylo spuštěno v roce
2008. Na českém až do minulého týdne svítila zpráva: „Chcete se
zdokonalovat v angličtině? Zkuste psát do anglického fóra. I když by to
bylo s chybami – těmi se člověk učí.“, takže i mnoho Čechů
používá anglické fórum.
Nette Blog v tuto chvíli čítá
34 článků v angličtině a 19 v češtině. Rozhodně doporučuji blog
sledovat, třeba přes RSS,
stává se z něj hlavní zdroj informací o novinkách.
Diskuse na GitHubu jsou výhradně v angličtině. Tuším od roku 2012 je
to oficiální podmínka. Jedinou výjimkou jsou diskuse o české dokumentaci,
což je snad pochopitelné.
V celém kódu se nikdy neobjevilo jediné české slovo. Kód je od
prapočátků pouze v angličtině, včetně všech chybových hlášek,
komentářů atd. Tuším jen jednou se v něm vyskytlo slovo fuck 🙂
Dále je tu oficiální Twitter
kanál, který je od počátku kompletně anglicky.
Co naopak anglicky není? Tak především jsou to videa z Posobot a
facebooková stránka Nette Framework
CZ/SK, která slouží zejména k informování o lokálních akcích,
jako je právě Posobota nebo NettePivo. A taky kanál na Slacku
Péhápkaři, což tedy není oficiální součást Nette.
Ještě pár technických záležitostí: pokud přijdete na úvodní
stránku webu, fóra či blogu, nejspíš vás to přesměruje na českou verzi.
Je to pouze proto, že máte v prohlížeči nastavený jako preferovaný jazyk
češtinu nebo jste se už někdy v minulosti do české mutace přepnuli.
Jinak samozřejmě přesměrovává na anglickou verzi.
Taktéž je určitý rozdíl mezi výpisem článků či příspěvků na
českém a anglickém fóru. Web zkrátka předpokládá, že český uživatel
anglicky číst umí, takže českým uživatelům se například u nejnovějších příspěvků
vypisují i ty anglické nebo na českém blogu i anglické články,
u který neexistuje českých překlad. Zahraniční uživatel tento mix
nevidí.
Česká verze je zkrátka přidaným komfortem. Ale rozhodně preferujeme
tvořit obsah v angličtině.
Content Security Policy (CSP) je dodatečný bezpečnostní prvek, který
prohlížeči říká, jaké další zdroje může stránka načítat a jak
může být zobrazena. Chrání tak před vkládáním škodlivého kódu a
útokům jako je XSS. Odesílá se v podobě hlavičky sestavené z řady direktiv.
Jeho nasazení ale není vůbec triviální.
Obvykle chceme používat JavaScriptové knihovny umístěné na různých
místech mimo náš server, například měřící kód Google Analytics,
reklamní systémy, captchy atd. A tady bohužel první verze CSP selhává.
Vyžaduje přesnou analýzu načítaného obsahu a nastavení správných
pravidel. Tedy vytvořit whitelist, výčet všech domén, což není snadné,
jelikož některé skripty dynamicky dotahují další skripty z jiných
domén, nebo jsou na jiné domény přesměrované atd. A i když si dáte
práci a seznam vytvoříte ručně, nikdy nevíte, co se může v budoucnu
změnit, takže musíte neustále sledovat, jestli je seznam stále aktuální a
opravovat ho. Analýza Google ukázala, že i to pečlivé ladění ve finále
vede k tomu, že povolíte tak široký přístup, že celý smysl CSP padá,
jen posíláte s každým požadavek mnohem větší hlavičky.
CSP level 2 už k problému přistupuje jinak, pomocí nonce, nicméně
teprve třetí verze řešení dotáhla do konce. Bohužel zatím (rok 2019)
nemá dostatečnou podporu u prohlížečů.
O tom, jak sestavit direktivy script-src a
style-src, aby správně fungovaly i ve starších
prohlížečích a přitom s tím bylo co nejméně práce, jsem sepsal podrobný
článek v partnerské sekci Nette. V zásadě výsledná podoba může
vypadat nějak takto:
Než nastavíte nová pravidla pro CSP, vyzkoušejte si je nejprve nanečisto
pomocí hlavičky Content-Security-Policy-Report-Only. Ta funguje
ve všech prohlížečích podporujících CSP. Při porušení pravidel
prohlížeč nezablokuje skript, ale jen pošle notifikaci na URL uvedené
v direktivě report-uri. K příjmu notifikací a jejich analýze
můžete použít třeba službu Report
URI.
Můžete zároveň používat obě hlavičky a v
Content-Security-Policy mít ověřené a aktivní pravidla a
zároveň v Content-Security-Policy-Report-Only si testovat jejich
úpravu. Samozřejmě i selhání ostrých pravidlech si můžete nechat
monitorovat.
Po dvou verzích PHP, které nepřinesly nic moc zajímavého, se blíží
verze, pro kterou bude mít opět smysl aktualizovat knihovny. Jde o PHP 7.4 a
hlavním tahákem jsou typed properties, které
uzavírají mnohaletý posun ke striktně typovanému jazyku, což PHP
zvýhodňuje oproti jiným webovým jazykům.
Ve zkratce, tahle novinka vám umožní deklarovat typy přímo
u proměnných třídy:
class Config
{
public string $dsn;
public ?string $user;
public ?string $password;
public bool $debugger = true;
}
Je potřeba říct, že pokud jste si navykli používat privátní
proměnné a přistupovat k nim přes typehintované metody (což je
správně), tak vlastně o žádnou killer feature nejde. Druhotná kontrola
typů je zbytečná a vlastně jen zpomaluje kód. Diametrálně jiná situace
se týká public/protected proměnných, kde dosud neexistoval žádný způsob,
jak mít jejich hodnotu (a dokonce existenci) pod kontrolou. Až dosud.
Což nevyhnutelně povede k otázce:
Je nutné dál psát settery a
gettery?
Sice veškerý boilerplate kód nám dnes na kliknutí generují editory, ale
určitě vypadá hezky, když tohle:
class Circle
{
private $radius;
function setRadius(float $val)
{
$this->radius = $val;
}
function getRadius(): float
{
return $this->radius;
}
}
nahradíte za:
class Circle
{
public float $radius;
}
Nehledě na to, že i užití objektu je stručnější
$circle->radius = $x vs
$circle->setRadius($x).
Problém ale je, že velkou spoustu setterů a getterů nelze jednoduše
nahradit. Třeba zrovna v uvedeném příkladu by se hodilo ještě ověřit,
že poloměr není záporné číslo:
function setRadius(float $val)
{
if ($val < 0) {
throw new InvalidArgumentException;
}
$this->radius = $val;
}
A v ten moment už nelze kód zredukovat do veřejné proměnné.
Jindy zase chceme, aby jednou nastavená hodnota byla neměnná, což nelze
u public proměnné zajistit.
Nebo vůbec nechceme dávat k dispozici getter, protože nepatří do
veřejného API třídy.
Anebo chceme mít setter či getter součástí rozhraní.
Atd, atd.
Zkrátka někdy bude možné použít typované veřejné proměnné místo
metod, jindy ne, rozdíl bude dost často otázkou vnitřní implementace
třídy a pro uživatele neprůhledný. Což je cesta v nekonzistentnímu API,
kdy jednou se používá proměnná, jindy metoda a uživatel v tom nevidí
logiku. (Podobně jako třeba metoda PDOStatement::errorInfo() vs.
proměnná PDOException::$errorInfo).
Prohlubování nekonzistence ale nechceš. Raději konzistentní setrvání
u metod, privátních proměnných a všeho toho boilerplate kódu. A pro
privátní proměnné, jak jsem zmiňoval v úvodu, je přínos typehintů
sporný. Nebo ne?
V čem je tedy výhoda?
Vlastně je výhod dost, i když v jiných oblastech. Typované proměnné
budou užitečné pro kompilátor kvůli optimalizacím, pro práci s reflexí
nebo nástroje analyzující kód. Důležité budou v šedé zóně protected
proměnných. Umožňují zapsat prostředky jazyka to, co se dosud obcházelo
komentářem. A navíc přinášejí do jazyka nový příznak neinicializovaného
stavu, jakousi obdobu undefined z JavaScriptu.