phpFashion

Rubrika PHP

100 minut je méně než 50? Paradoxy PHP při změně času

„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:

  1. Přímo v konstruktoru new DateTime('+50 minutes')
  2. 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:

  1. 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.
  2. 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č?

  1. Původní čas (01:30 CET) jsme převedli do UTC (00:30 UTC)
  2. Přičetli jsme den v UTC (00:30 UTC následující den)
  3. Ale následující den už platí v Praze letní čas (CEST), který má posun +2 hodiny od UTC
  4. 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:

  1. Používejte Nette\Utils\DateTime, který opravuje problematické chování.
  2. Nebo provádějte časovou aritmetiku v UTC zóně a až pak převádějte zpět do lokální zóny.
  3. 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.


Property Hooks v PHP 8.4: Revoluce nebo Past?

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.

…pokračování


Readonly vlastnosti v PHP a jejich skrytá úskalí

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.

…pokračování


Jak zvládnout gettery, když nemají co vrátit?

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.


První kroky v OOP v PHP: Základy, které musíte znát

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.

V průvodci se dozvíte, co je to:

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í.


Kompilační chyby v PHP: proč jsou stále problémem?

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.

try {
	require 'cesta_k_souboru.php';
} catch (ParseError $e) {
	echo "Syntaktická chyba v PHP souboru";
}

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:

$code = '... PHP kód pro ověření ...';
$process = proc_open(
	PHP_BINARY . ' -l',
	[['pipe', 'r'], ['pipe', 'w'], ['pipe', 'w']],
	$pipes,
	null,
	null,
	['bypass_shell' => true],
);
fwrite($pipes[0], $code);
fclose($pipes[0]);
$error = stream_get_contents($pipes[1]);
if (proc_close($process) !== 0) {
	echo 'Chyba v PHP souboru: ' . $error;
}

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:

php -l soubor1.php soubor2.php soubor3.php

Proč je operátor ?? holé neštěstí

Na operátor ?? se v PHP čekalo neskutečně dlouho, snad deset let. Dnes je mi ale líto, že se nečekalo déle.

  • Počkej, cože? Deset let? Tak to přeháníš, ne?
  • Opravdu. Začal se řešit v roce 2004, pod názvem „ifsetor“. A dostal se do PHP až v prosinci 2015 ve verzi 7.0. Takže téměř 12 let.
  • Aha! Notyvole.

Škoda, že se nečekalo déle. Do současného PHP totiž nezapadá.

PHP počínaje verzí 7.0 udělalo neuvěřitelný posun ke striktnosti. Klíčové okamžiky:

Operátor ?? zjednodušil otravné:

isset($necoCo[$musimNapsatDvakrat]) ? $necoCo[$musimNapsatDvakrat] : 'default value'

na pouhé:

$pisu[$jednou] ?? 'default value'

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:

// vždy vrátí 'default value', víte proč?
$len = $this->lenght ?? 'default value'

Zkrátka ?? jsme dostali přesně ve chvíli, kdy bychom naopak nejvíc potřeboval zkrátit tohle:

`php
$necoCo[$musimNapsatDvakrat] === null
? ‚default value‘
: $necoCo[$musimNapsatDvakrat]
`

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.

S detekcí překlepů zamlčených operátorem ?? vám pomůže PHPStan s nastavením checkDynamicProperties: true.


Nejsi ve vleku cargo kultů?

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:

function isSpecialName(string $name): bool
{
	$specialNames = ['foo' => 1, 'bar' => 1, 'baz' => 1, ...];
	return isset($specialNames[$name]);
}

A přišel jsem na jednoduchý trik, který znovuvytváření zabránil. Stačilo proměnnou definovat jako statickou:

function isSpecialName(string $name): bool
{
	static $specialNames = ['foo' => 1, 'bar' => 1, 'baz' => 1, ...];
	return isset($specialNames[$name]);
}

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.


Zapisovat nullable types s otazníkem nebo bez?

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.


Jak probíhá shutdown v PHP a volání destruktorů?

Ukončení požadavku v PHP se skládá z těchto kroků prováděných v uvedeném pořadí:

  1. Volání všech funkcí registrovaných pomocí register_shutdown_function()
  2. Volání všech metod __destruct()
  3. Vyprázdnění všech output bufferů
  4. Ukončení všech rozšíření PHP (např. sessions)
  5. 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:

  1. PHP se nejprve pokusí zrušit objekty v globální tabulce symbolů.
  2. Poté volá destruktory všech zbývajících objektů.
  3. 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.

Zdroj https://stackoverflow.com/…ucted-in-php


phpFashion © 2004, 2025 David Grudl | o blogu

Ukázky zdrojových kódů smíte používat s uvedením autora a URL tohoto webu bez dalších omezení.