„Kdy se sejdeme?“ – „Zítra ve tři.“ „Kdy je ta
schůzka?“ – „Příští měsíc.“ Pro běžný život jsou takové
údaje o čase zcela postačující. Jenže zkuste totéž v programování a
rychle zjistíte, že jste vstoupili do bludiště plného nástrah a
neočekávaných překvapení.
Čas v programování je jako šelma, která vypadá krotce, dokud na ni
nešlápnete. A jednou z nejmocnějších lstí této šelmy je letní čas a
jeho zákeřné přechody. Systém, který měl údajně ušetřit svíčky,
dnes způsobuje programátorům bezesné noci (pravděpodobně kolem 2:30 ráno,
kdy najednou zjistí, že jejich servery dělají podivné věci).
Vydejme se na průzkum temných zákoutí přechodů na letní čas a zpět,
jak je PHP (ne)zvládá a jak jsem se pokusil napravit toto šílenství
v Nette Utils. Připravte se na momenty, kdy 1 + 1 ≠ 2 a kdy přidání
delšího času vám paradoxně vrátí dřívější hodinu. Tohle by
nevymyslel ani Einstein.
Nejprve si prosvištíme
některá slovíčka
Než se ponoříme do problematiky, vysvětleme si několik
klíčových pojmů:
UTC (Coordinated Universal Time) – koordinovaný světový čas,
základní časový standard, od kterého se odvozují všechny ostatní
časové zóny. Je to v podstatě „nulový bod“ pro měření času na
celém světě.
Časový posun (offset) – kolik hodin je potřeba přičíst nebo
odečíst od UTC, abychom dostali místní čas. Označuje se jako UTC+X
nebo UTC-X.
CET (Central European Time) – středoevropský čas, který
používáme v zimě. Má posun UTC+1, což znamená, že když je v UTC
poledne, u nás je 13:00.
CEST (Central European Summer Time) – středoevropský letní
čas, který používáme v létě. Má posun UTC+2, takže když je v UTC
poledne, u nás je 14:00.
ČEST – komunistický pozdrav, něco, co patří doufám už pouze
do starý časů
Letní čas – systém, kdy v určité části roku (obvykle
v létě) posuneme hodiny o hodinu dopředu, abychom lépe využili denní
světlo.
Ten okamžik trval celý
světelný rok
Pojďme si sekundu po sekundě rozebrat, jak probíhá přechod na letní
čas a zpátky. Jako příklad si vezměme nedávnou změnu času v České
republice v neděli 30. března 2025:
V 01:59:58 středoevropského času (CET) je vše normální.
V 01:59:59 CET je stále vše normální.
V další sekundě NEnastane 02:00:00 CET. Místo toho se hodiny
magickým skokem posunou vpřed.
Následuje 03:00:00 středoevropského letního času (CEST).
Celá hodina mezi 02:00:00 a 02:59:59 v tento den lokálně
„neexistuje“. Pokud jste měli mít ve 2:30 ráno důležitý telefonát,
máte smůlu.
Podobně, při přechodu zpět na standardní čas (někdy označovaný jako
„zimní“) na podzim (např. 26. října 2025), nastane opačná
situace:
V 02:59:59 letního času (CEST) je vše normální.
V další sekundě nenastane 03:00:00 CEST. Hodiny se
vrátí zpět.
Následuje 02:00:00 standardního středoevropského
času (CET).
V tomto případě hodina mezi 02:00:00 a 02:59:59 nastane dvakrát.
Poprvé v letním čase (CEST) a podruhé ve standardním čase (CET). Jak
rozlišíme, kterou 2:30 myslíme? Právě pomocí označení času (CET/CEST),
posunu od UTC (+01:00 / +02:00) nebo prostě slovem „letního“ /
„zimního“ času.
Časové zóny: Co
vlastně označuje Europe/Prague?
Když v PHP (nebo jinde) použijeme identifikátor časové zóny jako
Europe/Prague, není to jen informace o aktuálním posunu od UTC.
Je to odkaz na záznam v IANA Time Zone Database, která obsahuje
komplexní historii a budoucí pravidla pro danou geografickou oblast:
Standardní posun od UTC (kolik hodin se přičítá nebo odečítá
od UTC).
Pravidla pro letní čas (kdy začíná, kdy končí).
Historické změny v posunech nebo pravidlech letního času (ty se mohou
měnit rozhodnutím vlád).
Existují stovky takových zón (America/New_York,
Asia/Tokyo, Australia/Sydney). Některé oblasti
letní čas vůbec nepoužívají (např. většina Afriky a Asie, nebo oblasti
kolem rovníku) a mají po celý rok stejný posun od UTC (např.
Etc/UTC nebo Africa/Nairobi).
Absolutní čas: UTC a Timestamp
Abychom se vyhnuli zmatkům s lokálními časy a letním časem, existují
absolutní časové reference:
UTC: Jak jsme si již řekli, je to základní časový standard,
který nemá letní čas. Všechny lokální časy jsou definovány jako posun
od UTC. UTC je v podstatě „čistý“ čas, ke kterému si pak každá
časová zóna přidá svůj posun.
Unix Timestamp: Počet sekund, které uplynuly od začátku Unixové
éry (1. ledna 1970, 00:00:00 UTC), nepočítaje přestupné sekundy. Timestamp
je také absolutní a nezávislý na časové zóně nebo letním čase.
Právě převod mezi absolutním časem (UTC/timestamp) a lokálním časem
v konkrétní zóně je místo, kde vstupují do hry pravidla
letního času.
PHP DateTime: Když se hodiny
přetočí
Když v PHP pracujete s objektem DateTime nebo
DateTimeImmutable, vždy má přiřazenou časovou zónu. Pokud ji
explicitně neuvedete, použije se výchozí zóna nastavená v PHP
(konfigurací nebo pomocí date_default_timezone_set()).
Co se stane, když se pokusíte vytvořit čas, který kvůli letnímu času
neexistuje, nebo čas, který existuje dvakrát?
Neexistující čas (jarní skok):
// Pokus vytvořit čas v "díře" 30. března 2025
$dt = new DateTime('2025-03-30 02:30:00', new DateTimeZone('Europe/Prague'));
echo $dt->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-03-30 03:30:00 CEST (+02:00)
PHP typicky „normalizuje“ tento neplatný čas tím, že ho posune
vpřed o hodinu na první platný čas po skoku. Takže 02:30 se
stane 03:30.
Nejednoznačný čas (podzimní návrat):
// Pokus vytvořit čas v "překryvu" 26. října 2025
$dt = new DateTime('2025-10-26 02:30:00', new DateTimeZone('Europe/Prague'));
echo $dt->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-10-26 02:30:00 CET (+01:00)
PHP zde standardně zvolí druhý výskyt toho času. Proč druhý a
ne první? Protože PHP považuje standardní čas (CET) za výchozí,
základní stav a letní čas (CEST) pouze za dočasnou úpravu, a proto při
nejednoznačnosti dává přednost standardnímu času.
Relativní časové
výrazy a jejich záludnosti
Teď se dostáváme k opravdu záludné části. PHP umožňuje pracovat
s relativními časovými výrazy – tedy řetězci jako
+30 minutes, -1 hour nebo 1 day 2 hours.
Tyto výrazy můžeme použít dvěma způsoby:
Přímo v konstruktoru new DateTime('+50 minutes')
V metodě $date->modify('+50 minutes')
Mimochodem, Nette tyto relativní časové výrazy odjakživa „tlačí“,
protože jsou srozumitelné a přehledné. Určitě je znáte například
z konfigurace „expiration“ u session nebo v dalších částech
frameworku.
Intuitivně bychom čekali, že když k času přičteme delší dobu,
výsledný čas bude pozdější. S relativními časovými výrazy to ale
během jarního přechodu nemusí platit! A tento problém se projevuje jak
při použití v konstruktoru DateTime, tak v metodě
modify().
Představte si, že je právě půl druhé ráno, těsně před jarním
skokem. V tu chvíli se ve vaší aplikací může odehrávat něco velmi
bizarního, čeho si většina z nás ovšem nevšimne, protože buď
spokojeně chrupeme v posteli, nebo ještě spokojeněji vykládáme moudra
v hospodě. Jenže v serverovnách po celém světě kód tiše
běží dál…
// pro všechny další příklady nastavíme výchozí časovou zónu
date_default_timezone_set('Europe/Prague');
// Je právě 2025-03-30 01:30:00 a vytvoříme DateTime s relativním časem +50 minut
$dt50 = new DateTime('+50 minutes');
echo $dt50->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-03-30 03:20:00 CEST (+02:00)
„No počkat, 1:30 plus 50 minut je přece 2:20. Proč to ukazuje 3:20?“
Jak už jsme si říkali, hodina mezi 2:00 a 3:00 neexistuje. Takže 2:20 je
neplatný čas, který PHP opraví tak, že ho posune o hodinu dál. Tedy na
3:20 v letním čase.
A co když k tomu stejnému výchozímu času přičteme delší
interval – řekněme 100 minut?
$dt100 = new DateTime('+100 minutes');
echo $dt100->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-03-30 03:10:00 CEST (+02:00)
Vidíte to! Ano, čtete správně. Po přidání 100 minut jsme dostali čas
(03:10), který je dřívější než čas po přidání
50 minut (03:20).
+50 minutes: 01:30 + 50 min = 02:20. Tento
čas neexistuje, protože je v té „ztracené hodině“. PHP ho normalizuje
posunem o hodinu vpřed na 03:20 CEST.
+100 minutes:
01:30 + 100 min (1h 40m) = 03:10. Tento čas už existuje. PHP ho
tedy použije tak, jak je.
Metoda modify() a konstruktor s relativními řetězci v PHP
mají tendenci provádět aritmetiku nejprve na úrovni „hodinkového času“
a až potom řešit neplatné časy vzniklé skokem na letní čas.
Výsledkem je naprosto neintuitivní chování, které většina knihoven pro
práci s časem v jiných jazycích nedělá. Ty typicky interpretují
+X minut jako přidání přesné doby trvání (X *
60 sekund) k absolutnímu časovému okamžiku.
DateInterval: Další vrstva
komplikací
Příběh se ještě komplikuje třídou DateInterval. Ta byla
vytvořena speciálně pro práci s časovými intervaly a mohla by nabízet
řešení našeho problému. Jenže ouha…
K vytvoření instance DateInterval musíte použít formát
podle normy ISO 8601. Upřímně, rozuměli byste na první pohled, co znamená
PT100M? Ne? Já taky ne. Je to „Period of Time, 100 Minutes“
(doba trvání 100 minut). Standardizované, ale rozhodně ne na první
pohled jasné.
Přesto, pokud tento podivný zápis překousneme, funguje najednou všechno
správně!
$dt = new DateTime('2025-03-30 01:30:00');
$dt->add(new DateInterval('PT100M')); // 100 Minutes - ten báječný ISO 8601 formát
echo $dt->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-03-30 04:10:00 CEST (+02:00) - hurá, funguje správně!
Skvělé! Tady se to opravdu chová tak, jak bychom čekali – přidá
přesně 100 minut k absolutnímu času. To by mohlo být naše řešení…
ale co ten podivný formát?
PHP vývojáři si byli vědomi, že PT100M není zrovna
uživatelsky přívětivé, a tak přidali metodu
DateInterval::createFromDateString(), která rozumí těm
příjemným textovým výrazům jako 100 minutes:
$dt = new DateTime('2025-03-30 01:30:00');
$dt->add(DateInterval::createFromDateString('100 minutes'));
echo $dt->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-03-30 03:10:00 CEST (+02:00) - au, zase špatně!
A jsme zase tam, kde jsme byli! Stejný problém jako s
modify(). Co se to děje?
Ve skutečnosti máme co do činění s jakousi „dvojí tváří“
třídy DateInterval. Záleží na tom, jakým způsobem ji
vytvoříme:
Když použijeme konstruktor s ISO 8601 formátem
new DateInterval('PT100M'), vytvoří se skutečná doba
trvání, která se přičítá k absolutnímu času.
Když použijeme createFromDateString('100 minutes'), vytvoří
se spíše jakýsi kalendářní interval, který se chová podobně
jako modify() – nejprve provede „hodinkovou“ aritmetiku a
pak až řeší problémy s neplatnými časy.
Takže není DateInterval jako DateInterval. Je to
úplně jiná tvář stejně pojmenovaného objektu podle toho, jak ho
vytvoříme.
Jedna možnost řešení: Útěk
do UTC
Jedním ze způsobů, jak se těmto problémům vyhnout, je provádět
veškerou časovou aritmetiku v UTC, kde žádný letní čas neexistuje, a až
finální výsledek převést do požadované lokální zóny:
$dt = new DateTime('2025-03-30 01:30:00');
$dt->setTimezone(new DateTimeZone('UTC')); // Převeď do UTC
$dt->modify('+100 minutes'); // Proveď operaci v UTC
$dt->setTimezone(new DateTimeZone('Europe/Prague')); // Převeď zpět
echo $dt->format('Y-m-d H:i:s T (P)');
// Správný výstup: 2025-03-30 04:10:00 CEST (+02:00)
Hurá! Nebo ne? Tento trik může být naopak kontraproduktivní, když
přičítáme celé dny nebo jiné kalendářní jednotky. Nejprve si
ověříme, že když přičteme 1 den k času před jarním skokem, dostaneme
očekávaný výsledek:
$dt = new DateTime('2025-03-30 01:30:00'); // Před skokem (CET +01:00)
$dt->modify('+1 day');
echo $dt->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-03-31 01:30:00 CEST (+02:00)
Vidíme, že při přičtení jednoho dne zůstává stejná „hodinková“
hodnota (01:30), ale mění se časová zóna z CET na CEST.
Ale co se stane, když použijeme náš UTC trik?
$dt = new DateTime('2025-03-30 01:30:00'); // Před skokem (CET +01:00)
$dt->setTimezone(new DateTimeZone('UTC')); // Převede na UTC
$dt->modify('+1 day');
$dt->setTimezone(new DateTimeZone('Europe/Prague')); // Převede zpět do lokální zóny
echo $dt->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-03-31 02:30:00 CEST (+02:00) - o hodinu více!
Ups! Hodina se nám posunula z 1:30 na 2:30. Proč?
Původní čas (01:30 CET) jsme převedli do UTC (00:30 UTC)
Přičetli jsme den v UTC (00:30 UTC následující den)
Ale následující den už platí v Praze letní čas (CEST), který má
posun +2 hodiny od UTC
Takže když převedeme zpět 00:30 UTC, dostaneme 02:30 CEST
Tento „útěk do UTC“ tedy může způsobit, že kalendářní operace se
nebudou chovat intuitivně z pohledu lokálního času. Co je tedy vlastně
správné chování? To záleží na vašich potřebách – někdy chcete
zachovat absolutní časový interval (jako 24 hodin), jindy chcete zachovat
kalendářní význam (jako „stejný čas následující den“).
Řešení v Nette Utils
Protože práce s časem, časovými zónami a letním časem je notoricky
složitá, rozhodl jsem se do Nette Utils přidat opravu problematického
chování PHP. Konkrétně do třídy Nette\Utils\DateTime, a to
opravu jak konstruktoru, tak metody modify(). Jen váhám, zda
nejde o BC break – k tomu se vrátím v závěru článku.
$dt = new Nette\Utils\DateTime('2025-03-30 01:30:00');
$dt->modify('+100 minutes');
echo $dt->format('Y-m-d H:i:s T (P)');
// Výstup: 2025-03-30 04:10:00 CEST (+02:00) - SPRÁVNĚ!
S Nette\Utils\DateTime je výsledek pro
+100 minutes vždy pozdější než pro +50 minutes,
i když je půl druhé ráno!
Kdy je 1 + 1 ≠ 2? Když
pracujeme s časem!
Implementace v Nette Utils řeší i složitější případy, kdy
kombinujeme přičítání dnů a hodin. Tady se dostáváme k opravdu
zajímavému problému: existují totiž dva možné výklady relativního
výrazu jako „+1 day +1 hour“. A tyto dvě interpretace dávají při
přechodu na letní čas různé výsledky! Pojďme si to ukázat na
příkladu:
První interpretace:
$dt = new \DateTime('2025-03-30 01:30:00'); // CET
$dt1 = clone $dt;
$dt1->modify('+1 day'); // Nejprve přičtu den: 2025-03-31 01:30:00 CEST
$dt1->modify('+1 hour'); // Pak přičtu hodinu: 2025-03-31 02:30:00 CEST
Druhá interpretace:
$dt2 = clone $dt;
$dt2->modify('+1 hour'); // Nejprve hodinu: 2025-03-30 03:30:00 CEST
$dt2->modify('+1 day'); // Pak den: 2025-03-31 03:30:00 CEST
Rozdíl je celá hodina! Jak vidíte, pořadí operací zde hraje
zásadní roli.
V Nette\Utils\DateTime jsem zvolil první interpretaci jako
výchozí chování, protože je intuitivnější. Chceme-li přičíst „1 den
a 1 hodinu“, obvykle tím myslíme „stejný čas následující den plus
hodina“. A co je nejlepší? Je jedno, v jakém pořadí jednotky
zapíšete. Ať už použijete +1 day +1 hour nebo
+1 hour +1 day, výsledek bude vždy stejný.
Tato konzistence dělá práci s časovými výrazy mnohem
předvídatelnější a bezpečnější.
Čas je těžký, netrapte se
sami
Práce s časem v PHP může být zrádná, zvláště kolem přechodů na
letní čas. Relativní časové výrazy a dokonce i některé způsoby
použití DateInterval mohou vést k neintuitivním
výsledkům.
Pokud potřebujete spolehlivou manipulaci s časem:
Používejte Nette\Utils\DateTime, který opravuje
problematické chování.
Nebo provádějte časovou aritmetiku v UTC zóně a až pak převádějte
zpět do lokální zóny.
Vždy testujte chování vašeho kódu během přechodů na
letní čas.
Teď jen váhám, jestli oprava chování DateTime v Nette Utils nebude BC
break. Upřímně si nemyslím, že by kdokoliv vědomě spoléhal na zrádné
současné chování při přechodech na letní čas. Tak bych to asi zařadil
do Nette Utils 4.1.
Čas je těžké téma ve všech programovacích jazycích, ne jen v PHP.
Kam se na to hrabe invalidace keše.
Představte si, že by vaše PHP objekty mohly být čistší,
přehlednější a lépe použitelné. Dobrá zpráva – už nemusíte snít!
PHP 8.4 přichází s revoluční novinkou v podobě property hooks a
asymetrické viditelnosti, které kompletně mění pravidla hry
v objektově orientovaném programování. Zapomeňte na neohrabané gettery a
settery – konečně máme k dispozici moderní a intuitivní způsob, jak
kontrolovat přístup k datům objektů. Pojďme se podívat na to, jak tyto
novinky mohou změnit váš kód k nepoznání.
Property hooks představují promyšlený způsob, jak definovat chování
při čtení a zápisu vlastností objektu – a to mnohem čistěji a
výkonněji než dosavadní magické metody __get/__set. Je to jako
byste dostali k dispozici sílu magických metod, ale bez jejich typických
nevýhod.
Podívejme se na jednoduchý příklad z praxe, který vám ukáže, proč
jsou property hooks tak užitečné. Představme si běžnou třídu
Person s veřejnou property age:
class Person
{
public int $age = 0;
}
$person = new Person;
$person->age = 25; // OK
$person->age = -5; // OK, ale to je přece nesmysl!
PHP sice díky typu int zajistí, že věk bude celé číslo
(to lze od PHP 7.4), ale co s tím záporným věkem? Dříve bychom museli
sáhnout po getterech a setterech, property by musela být private, museli
bychom doplnit spoustu kódu… S hooks to vyřešíme elegantně:
class Person
{
public int $age = 0 {
set => $value >= 0 ? $value : throw new InvalidArgumentException;
}
}
$person->age = -5; // Ups! InvalidArgumentException nás upozorní na nesmysl
Krása tohoto řešení spočívá v jeho jednoduchosti – navenek se
property chová úplně stejně jako dřív, můžeme číst i zapisovat
přímo přes $person->age. Ale máme plnou kontrolu nad tím,
co se při zápisu děje. A to je teprve začátek!
Můžeme jít ještě dál a vytvořit třeba hook pro čtení. Hookům lze
přidat atributy. A samozřejmě mohou obsahovat složitější logiku než
jednoduchý výraz. Podívejte se na tento příklad práce se jménem:
class Person
{
public string $first;
public string $last;
public string $fullName {
get {
return "$this->first $this->last";
}
set(string $value) {
[$this->first, $this->last] = explode(' ', $value, 2);
}
}
}
$person = new Person;
$person->fullName = 'James Bond';
echo $person->first; // vypíše 'James'
echo $person->last; // vypíše 'Bond'
A něco důležitého: kdykoliv se přistupuje k proměnné (i uvnitř
samotné třídy Person), vždy se využijí hooks. Jediná výjimka je přímý
přístup k reálné proměnné uvnitř kódu samotného hooku.
Ohlédnutí do
minulosti: Co nás naučil SmartObject?
Pro uživatele Nette může být zajímavé ohlédnout se do minulosti.
Framework totiž podobnou funkcionalitu nabízel už před 17 lety ve
formě SmartObject,
který výrazně vylepšoval práci s objekty v době, kdy PHP v této
oblasti značně zaostávalo.
Pamatuju si, že tehdy přišla vlna bezbřehého nadšení, kdy se
properties používaly prakticky všude. Tu pak vystřídala vlna opačná –
nepoužívat je nikde. Důvod? Chybělo jasné vodítko, kdy je lepší použít
metody a kdy property. Ale dnešní nativní řešení je kvalitativně úplně
jinde.Property hooks a asymetrická viditelnost jsou plnohodnotné nástroje,
které nám dávají stejnou úroveň kontroly jako máme u metod. Proto dnes
můžeme mnohem lépe rozlišit, kdy je property skutečně tím správným
řešením.
Představte si, že byste mohli svým datům dát pevnou půdu pod
nohama – jednou je nastavíte a pak si můžete být jistí, že je nikdo
nezmění. Přesně to přineslo PHP 8.1 s readonly vlastnostmi. Je to jako
dát vašim objektům neprůstřelnou vestu – chrání jejich data před
nechtěnými změnami. Pojďme se podívat, jak vám tento mocný nástroj
může usnadnit život a na co si při jeho používání dát pozor.
Začněme jednoduchým příkladem:
class User
{
public readonly string $name;
public function setName(string $name): void
{
$this->name = $name; // První nastavení - vše OK
}
}
$user = new User;
$user->setName('John'); // Paráda, máme jméno
echo $user->name; // "John"
$user->setName('Jane'); // BOOM! Výjimka: Cannot modify readonly property
Jakmile jednou jméno nastavíte, je to jako vytesané do kamene. Žádné
náhodné přepsání, žádné nechtěné změny.
Kdy je uninitialized
opravdu uninitialized?
Často se setkávám s mýtem, že readonly vlastnosti musí být nastaveny
v konstruktoru. Ve skutečnosti je PHP mnohem flexibilnější – můžete je
inicializovat kdykoliv během života objektu, ale pouze jednou! Před prvním
přiřazením jsou ve speciálním stavu ‚uninitialized‘, což je takový
limbo stav mezi nebytím a bytím.
A tady přichází zajímavý detail – readonly vlastnosti nemohou mít
výchozí hodnotu. A proč? Kdyby měly výchozí hodnotu, staly by se de facto
konstantami – hodnota by byla nastavena při vytvoření objektu a už by
nešla změnit.
Vyžadují se typy
Readonly proměnné vyžadují explicitní definici datového typu. Je to
proto, že stav ‚uninitialized‘, který využívají, existuje pouze
u typovaných proměnných. Bez uvedení typu tedy readonly proměnnou nelze
definovat. Pokud si nejste jistí typem, můžete použít
mixed.
Vývoj softwaru často přináší dilema. Například jak
řešit situace, kdy getter nemá co vrátit. V tomto článku prozkoumáme
tři strategie pro implementaci getterů v PHP, které ovlivňují strukturu a
čitelnost kódu, a každá má své specifické výhody i nevýhody. Pojďme
se na ně podrobněji podívat.
Univerzální getter
s parametrem
Prvním a v Nette používaným řešením je vytvoření jediné getter
metody, která, pokud hodnota není dostupná, může dle potřeby vrátit buď
null nebo vyhodit výjimku. O chování rozhoduje volitelný
parametr. Zde je příklad, jak by mohla metoda vypadat:
public function getFoo(bool $need = true): ?Foo
{
if (!$this->foo && $need) {
throw new Exception("Foo not available");
}
return $this->foo;
}
Hlavní výhodou tohoto přístupu je, že eliminuje potřebu mít několik
verzí getteru pro různé scénáře použití. Někdejší nevýhodou byla
horší srozumitelnost uživatelského kódu používajícího booleovské
parametry, ale ta padla s příchodem pojmenovaných parametrů, kdy lze psát
getFoo(need: false).
Dále tento přístup může způsobit komplikace v oblasti statické
analýzy, jelikož dle signatury se zdá, že getFoo() může
vrátit null v každé situaci. Nicméně nástroje jako PHPStan
umožňují explicitní dokumentaci chování metody pomocí speciálních
anotací, které zlepšují porozumění kódu a jeho správnou analýzu:
/** @return ($need is true ? Foo : ?Foo) */
public function getFoo(bool $need = true): ?Foo
{
}
Tato anotace jasně určuje, jaké návratové typy může metoda
getFoo() generovat v závislosti na hodnotě parametru
$need. Ale například PhpStorm jí nerozumí.
Dvojice metod: hasFoo()
a getFoo()
Další možností je rozdělit zodpovědnost na dvě metody:
hasFoo() pro ověření existence hodnoty a getFoo()
pro její získání. Tento přístup zvyšuje přehlednost kódu a je
intuitivně srozumitelný.
public function hasFoo(): bool
{
return (bool) $this->foo;
}
public function getFoo(): Foo
{
return $this->foo ?? throw new Exception("Foo not available");
Hlavním problémem je redundance, zvláště v případech, kdy je kontrola
dostupnosti hodnoty sama o sobě náročným procesem. Pokud
hasFoo() provádí složité operace k ověření, zda je hodnota
dostupná, a tato hodnota je poté opět získávána pomocí
getFoo(), dojde k jejich opětovnému provedení. Hypoteticky
může být stav objektu nebo dat změněn mezi voláním hasFoo()
a getFoo(), což může vést k nesrovnalostem. Z uživatelského
pohledu může být tento přístup méně pohodlný, protože nás nutí volat
dvojici metod s opakujícím se parametrem. A nemůžeme využít například
null-coalescing operátor.
Výhodou je, že některé nástroje pro statickou analýzu umožňují
definovat pravidlo, že po úspěšném volání hasFoo() nedojde v
getFoo() k vyhození výjimky.
Metody getFoo() a
getFooOrNull()
Třetí strategií pro je rozdělení funkcionality na dvě metody:
getFoo() pro vyhození výjimky, pokud hodnota neexistuje, a
getFooOrNull() pro vrácení null. Tento přístup
minimalizuje redundanci a zjednodušuje logiku.
public function getFoo(): Foo
{
return $this->getFooOrNull() ?? throw new Exception("Foo not available");
}
public function getFooOrNull(): ?Foo
{
return $this->foo;
}
Alternativou je dvojice getFoo() a
getFooIfExists(), ale v tomto případě nemusí být zcela
intuitivní pochopit, která metoda vyhazuje výjimku a která vrací
null. O trošku výstižnější by byla dvojice
getFooOrThrow() a getFoo(). Další možností je
getFoo() a tryGetFoo().
Každý z představených přístupů k implementaci getterů v PHP má
své místo v závislosti na specifických potřebách projektu a preferencích
vývojářského týmu. Při výběru vhodné strategie je důležité zvážit,
jaký dopad bude mít na čitelnost, údržbu a výkon aplikace. Volba by
odrážet snahu o co nejsrozumitelnější a nejefektivnější kód.
Chcete se ponořit do světa objektově orientovaného
programování v PHP, ale nevíte, kde začít? Mám pro vás nového
stručného průvodce OOP, který vás seznámí se všemi těmi pojmy, jako
class, extends, private atd.
Průvodce si neklade za cíl udělat z vás mistry v psaní čistého kódu
nebo podat zcela vyčerpávající informace. Jeho cílem je vás rychle
seznámit se základními koncepty OOP v současném PHP a dát vám fakticky
správné informace. Tedy poskytnout pevný základ, na kterém můžete dále
stavět. Třeba aplikace v Nette.
Jako navazující čtení doporučuji podrobný průvodce
světem správného návrhu kódu. Ten je přínosný i pro všechny, co
PHP a objektově orientované programování ovládají.
Programování v jazyce PHP byla vždycky trošku výzva, ale naštěstí
prošlo mnohými změnami k lepšímu. Pamatujete na časy před verzí PHP 7,
kdy skoro každá chyba znamenala fatal error, což aplikaci okamžitě
ukončilo? V praxi to znamenalo, že jakákoli chyba mohla aplikaci zcela
zastavit, aniž by programátor měl možnost ji zachytit a náležitě na ni
reagovat. Nástroje jako Tracy využívaly magických triků, aby dokázaly
takové chyby vizualizovat a logovat. Naštěstí s příchodem PHP 7 se tohle
změnilo. Chyby nyní vyvolávají výjimky, jako jsou Error, TypeError a
ParseError, které lze snadno zachytávat a ošetřit.
Avšak i v moderním PHP existuje slabé místo, kdy se chová stejně jako
ve své páté verzi. Mluvím o chybách během kompilace. Ty nelze zachytit a
okamžitě vedou k ukončení aplikace. Jedná se o chyby úrovně
E_COMPILE_ERROR. PHP jich generuje na dvě stovky. Vzniká paradoxní situace,
že když v PHP načteme soubor se syntaktickou chybou, což může být třeba
chybějící středník, vyhodí zachytitelnou výjimku ParseError. Ovšem
v případě, že kód je sice syntakticky v pořádku, leč obsahuje chybu
odhalitelnou až při kompilaci (například dvě metody se stejným názvem),
vyústí to ve fatální chybu, kterou zachytit nelze.
Bohužel, kompilační chyby v PHP nemůžeme ověřit interně. Existovala
funkce php_check_syntax(), která navzdory názvu odhalovala
i kompilační chyby. Byla zavedena v PHP 5.0.0, ale záhy odstraněna ve
verzi 5.0.4 a od té doby nikdy nebyla nahrazena. Pro ověření správnosti
kódu se musíme spolehnout na linter z příkazové řádky:
php -l soubor.php
Z prostředí PHP lze ověřit kód uložený v proměnné
$code třeba takto:
Nicméně režie spouštění externího PHP procesu kvůli ověření
jednoho souboru je docela velká. Ale dobrá zpráva přichází s verzí PHP
8.3, která přinese možnost ověřovat více souborů najednou:
Jenže udělal to v době, kdy potřeba používat isset()
značně klesla. Dnes častěji počítáme s tím, že data, ke kterým
přistupujeme, existují. A pokud neexistují, tak se o tom sakra chceme
dozvědět.
Operátor ?? má ale vedlejší efekt a to schopnost detekovat
null. Což je taky nejčastější důvod k jeho užití:
$len = $this->length ?? 'default value'
Bohužel zároveň zatajuje chyby. Zatajuje překlepy:
Bylo by úžasné, kdyby PHP 9.0 mělo odvahu chování operátoru
?? upravit k trošku větší striktnosti. Udělat z „isset
operátoru“ opravdu „null coalesce operator“, jak se mimochodem oficiálně jmenuje.
Před mnoha lety jsem si uvědomil, že když v PHP ve funkci používám
proměnnou obsahující předdefinovanou tabulku dat, tak při každém volání
funkce musí být pole znovu „vytvořené“, což je překvapivě dost
pomalé. Příklad:
Zrychlení, pokud pole bylo trošku větší, se pohybovalo v několika
řádech (jako třeba klidně 500×).
Takže od té doby jsem u konstantních polí vždy používal
static. Je možné, že tento zvyk někdo následoval, a třeba ani
netušil, jaký má skutečný důvod. Ale to nevím.
Před pár týdny jsem psal třídu, která nesla v několika properties
velké tabulky předdefinovaných dat. Uvědomil jsem si, že to bude zpomalovat
vytváření instancí, tedy že operátor new bude pokaždé
„vytvářet“ pole, což jak víme je pomalé. Tudíž musím properties
změnit na statické, nebo možná ještě lépe použít konstanty.
A tehdy jsem si položil otázku: Hele a nejsi jen ve vleku cargo kultu? Opravdu pořád
platí, že bez static je to pomalé?
Těžko říct, PHP prošlo revolučním vývojem a staré pravdy nemusí
být platné. Připravil jsem proto testovací vzorek a udělal pár měření.
Samozřejmě jsem si potvrdil, že v PHP 5 použití static uvnitř funkce
nebo u properties přineslo zrychlení o několik řádů. Ale pozor, v PHP
7.0 už šlo jen o jeden řád. Výborně, projev optimalizací v novém
jádře, ale stále je rozdíl podstatný. Nicméně u dalších verzí PHP
rozdíl dál klesal a až postupně téměř vymizel.
Dokonce jsem zjistil, že použití static uvnitř funkce v PHP 7.1 a
7.2 běh zpomalovalo. Zhruba 1,5–2×, tedy z pohledu řádů, o kterých se
tu celou dobu bavíme, zcela zanedbatelně, ale byl to zajímavý paradox. Od
PHP 7.3 rozdíl zmizel zcela.
Zvyklosti jsou dobrá věc, ale je nutné jejich smysl stále validovat.
Zbytečný static v těle funkcí už používat nebudu. Nicméně u oné
třídy, která držela velké tabulky předdefinovaných dat v properties,
jsem si řekl, že je programátorsky správné konstanty použít. Za chvíli
jsem měl refaktoring hotový, ale už jak vznikal jsem naříkal nad tím, jak
se kód stává ošklivým. Místo $this->ruleToNonTerminal nebo
$this->actionLength se v kódu objevovalo řvoucí
$this::RULE_TO_NON_TERMINAL a $this::ACTION_LENGTH a
vypadalo to fakt hnusně. Zatuchlý závan ze sedmdesátých let.
Až jsem zaváhal, jestli vůbec chci koukat na tak hnusný kód, a jestli
raději nezůstanu u proměnných, případně statických proměnných.
A tehdy mi to došlo: Hele nejsi jen ve vleku cargo kultu?
No jasně že jsem. Proč by měla konstanta řvát? Proč by měla na sebe
upozorňovat v kódu, být vyčnívajícím elementem v toku programu? Fakt,
že struktura slouží jen ke čtení, není důvod PRO ZASEKNUTÝ CAPSLOCK,
AGRESIVNÍ TÓN A HORŠÍ ČITELNOST.
TRADICE VELKÝCH PÍSMEN POCHÁZÍ Z JAZYKA C, KDE SE TAKTO OZNAČOVALY
MAKROKONSTANTY PREPROCESORU. BYLO UŽITEČNÉ NEPŘEHLÉDNUTELNĚ ODLIŠIT KÓD
PRO PARSER OD KÓDU PRO PREPROCESOR. V PHP SE ŽÁDNÉ PREPROCESORY NIKDY
NEPOUŽÍVALY, TAKŽE NENÍ ANI DŮVOD psát konstanty velkými písmeny.
Ještě ten večer jsem je všude zrušil. A stále nemohl pochopil, proč
mě to nenapadlo už před dvaceti lety. Čím větší blbost, tím tužší
má kořínek.
Vždycky mi vadila jakákoliv nadbytečnost nebo duplicita v kódu. Už jsem
o tom psal před mnoha lety. Při
pohledu na tento kód prostě trpím:
interface ContainerAwareInterface
{
/**
* Sets the container.
*/
public function setContainer(ContainerInterface $container = null);
}
Obsahovou zbytečnost komentáře u metody ponechme stranou.
A protentokrát i projev nepochopení dependency injection, pokud knihovna
potřebuje disponovat takovým rozhraním. O tom, že použití slova
Interface v názvu rozhraní je pro změnu projevem nepochopení
objektového programování, chystám samostatný článek. Koneckonců jsem si
tím sám prošel.
Ale proč proboha uvádět viditelnost public? Vždyť je to pleonasmus. Kdyby to nebylo
public, tak to pak není rozhraní, ne? No a ještě někoho napadlo z toho
udělat „standard“ ?♂️
Uff, omlouvám se za dlouhý úvod, to, kam celou dobu směřuju, je zda
psát volitelné nullable typy s otazníkem nebo bez. Tj:
// bez
function setContainer(ContainerInterface $container = null);
// s
function setContainer(?ContainerInterface $container = null);
Osobně jsem se vždycky klonil k první možnosti, protože informace daná
otazníkem je redundantní (ano, oba zápisy znamenají z pohledu jazyka
totéž). Zároveň se tak zapisoval veškerý kód do příchodu PHP 7.1, tedy
verze, která otazník přidala, a musel by být dobrý důvod jej
najednou měnit.
S příchodem PHP 8.0 jsem názor změnil a vysvětlím proč. Otazník
totiž není volitelný v případě properties. Na tomhle PHP zařve:
class Foo
{
private Bar $foo = null;
}
// Fatal error: Default value for property of type Bar may not be null.
// Use the nullable type ?Bar to allow null default value
A dále od PHP 8.0 lze používat promoted
properties, což umožňuje psát takovýto kód:
class Foo
{
public function __construct(
private ?Bar $foo = null,
string $name = null,
) {
// ...
}
}
Zde je vidět nekonzistence. Pokud je v kódu použito ?Bar
(což je nutnost), mělo by o řádek níže následovat ?string.
A pokud v některých případech budu psát otazník, měl bych ho psát
ve všech.
Zůstává otázka, zda není lepší používat místo otazníku přímo
union typ string|null. Pokud bych třeba chtěl zapsat
Stringable|string|null, verze s otazníkem možná
vůbec není.
Aktualizace: vypadá to, že PHP 8.4 bude zápis s otazníkem přímo
vyžadovat.
Ukončení požadavku v PHP se skládá z těchto kroků prováděných
v uvedeném pořadí:
Volání všech funkcí registrovaných pomocí
register_shutdown_function()
Volání všech metod __destruct()
Vyprázdnění všech output bufferů
Ukončení všech rozšíření PHP (např. sessions)
Vypnutí výstupní vrstvy (odeslání HTTP hlaviček, vyčištění output
handlerů atd.)
Zaměříme se podrobněji na krok č. 2, tedy volání destruktorů.
Samozřejmě už v prvním kroku, tedy při volání registrovaných shutdown
funkcí, může dojít k destrukci objektů, např. pokud některá z funkcí
držela poslední referenci na nějaký objekt nebo pokud byla samotná shutdown
funkce objektem.
Volání destruktorů probíhá takto:
PHP se nejprve pokusí zrušit objekty v globální tabulce symbolů.
Poté volá destruktory všech zbývajících objektů.
Pokud je provádění zastaveno např. kvůli exit(),
zbývající destruktory se nevolají.
ad 1) PHP projde globální tabulku symbolů pozpátku, tj. začne od
proměnné, která byla vytvořena jako poslední, a postupuje k proměnné,
která byla vytvořena jako první. Při procházení zruší všechny objekty
s refcount=1. Tato iterace se provádí, dokud takové objekty existují.
V podstatě se tedy dělá to, že a) odstraní všechny nepoužívané
objekty v globální tabulce symbolů b) pokud se objeví nové nepoužívané
objekty, odstraní je také c) a tak dále. Tento způsob destrukce se
používá proto, aby objekty mohly být závislé na jiných objektech
v destruktoru. Obvykle to funguje dobře, pokud objekty v globálním oboru
nemají komplikované (např. kruhové) vzájemné vazby.
Destrukce globální tabulky symbolů se výrazně liší od destrukce
ostatních tabulek symbolů, viz dále. Pro globální tabulku symbolů tedy PHP
používá chytřejší algoritmus, který se snaží respektovat závislosti
objektů.
ad 2) Ostatní objekty se prochází v pořadí podle jejich vytvoření a
zavolá se jejich destruktor. Ano, PHP pouze zavolá __destruct,
ale ve skutečnosti objekt nezruší (a dokonce ani nezmění jeho refcount).
Pokud se tedy na objekt budou odkazovat jiné objekty, bude stále k dispozici
(i když destruktor již byl zavolán). V jistém smyslu budou používat
jakýsi „napůl zničený“ objekt.
ad 3) V případě, že je provádění zastaveno během volání
destruktorů např. kvůli exit(), zbývající destruktory se
nevolají. Místo toho PHP označí objekty za již destruované. Důležitý
důsledek je, že volání destruktorů není jisté. Případy, kdy se tak
stane, jsou spíše vzácné, ale stát se to může.