phpFashion

Jak si opravou pro PHPStan zadělat na bug 21.2.2026 15:29

PHPStan hlásí chybu, vy ji opravíte. Jenže tou opravou jste kód paradoxně zhoršili. Jak je to možné?

Pamatuju si, jak jsem jednou projížděl výstup PHPStanu a systematicky opravoval hlášku za hláškou. Cítil jsem se produktivně. Kód je čistší, typy sedí, zelená všude. O měsíc později jsem si lámal hlavu, proč mi funkce vrací prázdný string, když by neměla. Detektivka na hodinu, přitom viník byl jasný: já a moje „oprava“.

Nejdřív důležitá věc: PHPStan dělá přesně to, co má. Upozorní vás, že funkce může vrátit null nebo false, a donutí vás se nad tím zamyslet. To je skvělé. Problém je až vaše reakce.

Nevinný příklad

Mějme funkci, která z textu odstraní nadbytečné mezery:

function normalizeSpaces(string $s): string
{
	return preg_replace('#\s+#', ' ', $s);
}

PHPStan zahlásí: Function preg_replace returns string|null but function should return string. No jasně, preg_replace může vrátit null, pokud dojde k chybě v regexu. Tak to opravíme, ne?

function normalizeSpaces(string $s): string
{
	return (string) preg_replace('#\s+#', ' ', $s);
}

PHPStan je spokojený. Commit, push, hotovo.

Jenže.

Co jste vlastně udělali

Ten původní kód byl ve skutečnosti lepší. Pokud by preg_replace někdy vrátil null, třeba kvůli … (ne, nenapadá mě proč), PHP by vyhodilo TypeError. Fatální chyba. Tracy by se rozsvítila, v logu by se to objevilo, prostě byste se o tom dozvěděli. (Jo aha! Kvůli tomuto může vrátit null!)

Po vaší „opravě“ se null tiše přetypuje na prázdný string. Funkce vrátí "", aplikace jede dál a vy nemáte tušení, že se něco pokazilo. Data se poškodí bez jakéhokoli varování.

Gratuluju, právě jste kód zhoršili 🙂

A preg_replace není ojedinělý případ. Spousta PHP funkcí vrací false nebo null pro situace, které při normálním použití prakticky nenastanou: json_encode, ob_get_contents, getcwd, gzcompress, celá řada Intl funkcí. Pokaždé, když sáhnete po přetypování, zastavte se a položte si otázku: nezahazujete tím informaci o (nepravděpodobné) chybě?

Správné řešení

Pokud chcete uspokojit PHPStan a zároveň zachovat původní chování, použijte throw expression:

function normalizeSpaces(string $s): string
{
	return preg_replace('#\s+#', ' ', $s)
		?? throw new \LogicException('preg_replace failed');
}

Tím říkáte: „Vím, že teoreticky může nastat chyba. Pokud nastane, chci o tom vědět.“ V podstatě jste explicitně zapsali to, co tam bylo implicitně předtím, fatálku při selhání. PHPStan spokojený, kód nezhoršený.

Ale moment. Ono to jde i jednodušeji. Tyhle bagatelní chyby můžete v PHPStanu prostě ignorovat, přidat je do ignoreErrors v phpstan.neon nebo anotací @phpstan-ignore. A je to naprosto legitimní. Vždyť to původní chování, kdy PHP vyhodí TypeError, je v podstatě to, co chcete. Proč byste kvůli tomu měnili kód?

Ještě lepší je problém vůbec nemít. Proto vznikají wrappery, kupříkladu Nette\Utils\Strings::replace() obaluje preg_replace a při chybě vyhodí výjimku. Podobně Nette\Utils\Json::encode() místo json_encode. Použijete jednu funkci a problém zmizí, žádný null, žádný false, nic k řešení.

Další možnost je vyřešit to na úrovni PHPStanu rozšířením, které u vybraných funkcí odstraní false nebo null z návratového typu. Například nette/phpstan-rules tohle dělá pro desítky PHP funkcí. U regexových funkcí navíc kontroluje, zda je pattern konstantní řetězec, a pokud ano, null z typu odstraní, protože chyba v regexu nenastane.

Je to samozřejmě opinionated přístup. A to mi na PHPStanu přesně vyhovuje: je přísný, a můžu si ho přizpůsobit rozšířením podle svého.


Tichá chyba je horší než hlasitá. A (string) je ten nejtišší způsob, jak ji vyrobit.

Velký přehled porovnávání v PHP je tu! 26.12.2025 20:37

Už žádné psaní testovacích skriptů, když si nejste stoprocentně jistí. Už žádné zdlouhavé listování v dokumentaci. Konečně je tu tabulka pravdy PHP. Připravil jsem pro vás definitivní PHP Comparison Cheat Sheet. Je to mapa pro území, kde neplatí ===.

Protože PHP 8 v tomto ohledu přepsalo pravidla, tabulky jsou dvě:

👉 Tabulka pro PHP 8.x (Současnost, kterou musíte znát)
👉 Tabulka pro PHP 7.x (Pro legacy warriors a archeology)

Všichni jsme se naučili používat ===, abychom měli klidné spaní. Je to naše jistota. Jenže co ve chvíli, kdy nepotřebujete vědět, jestli jsou hodnoty totožné, ale která je větší nebo menší? Tady veškerá jistota končí. Pro operátory <, >, <= a >= totiž žádná „strict“ verze neexistuje. PHP v tu chvíli přebírá otěže a spouští type juggling. Víte s jistotou, jak se zachová porovnání čísla a řetězce? Nebo null a false?

Stačí se podívat do tabulky a okamžitě vidíte, jak se k sobě typy chovají, když je PHP nutí do interakce. Začíná kompletním přehledem všech operátorů včetně spaceship (<=>).

Odhalíte tiché chyby dřív, než nastanou

Uveďme si dva příklady, které vás mohou stát hodiny ladění. Různé PHP funkce používají různé strategie porovnávání. Třeba funkce sort() má jako výchozí nastavení SORT_REGULAR.

Jak se zachová u řetězců, které vypadají jako čísla, například "042" a " 42"? Jak je seřadí?

A co array_unique()? „Nezkanibalizuje“ mi potichu data, když se v poli potká "042" a " 42"? Nemusíte nic zkoušet.

Díky tabulce nemusíte hádat. Okamžitě vidíte, kdy musíte přepnout flag, aby aplikace dělala přesně to, co chcete.

(A ano, žádný flag pro striktní porovnávání bez type juggling v PHP neexistuje 😤)

Fajnšmekroviny: DateTime a Closures

Aneb co nejspíš nevíte o porovnávání objektů v PHP.

Vezměte si takový DateTime. Mnoho vývojářů má zafixováno, že objekty se porovnávat nedají, a tak data zoufale převádí na timestampy nebo formátované stringy typu 'Y-m-d H:i:s', jen aby zjistili, co nastalo dřív. Zbytečně! Třídy DateTime a DateTimeImmutable mají implementovanou logiku pro běžné porovnávací operátory. Můžete se ptát na větší/menší stejně přirozeně jako u čísel. Žádné helpery, žádné formátování, čistá syntaxe. Proto si to zasloužilo vlastní sekci DateTime v tabulce.

Ještě větší zábava začíná u rovnosti. Zatímco === je u objektů nekompromisní a zajímá ho, jestli držíte v ruce identickou instanci, operátor == je u data mnohem pragmatičtější a porovnává časovou hodnotu. Díky tomu můžete porovnat dva různé objekty, a pokud ukazují stejný čas, PHP řekne „ano, to se rovná“. A co víc – funguje to i křížem mezi DateTime a DateTimeImmutable!

A třešnička na dortu? Closures. I anonymní funkce jsou objekty. Kdy jsou dvě closures rovny? Podívejte se do tabulky!

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

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

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:

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

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:

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

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.

Var, Let, Const: Přestaňte si komplikovat život v JavaScriptu 6.2.2025 23:54

JavaScript nabízí tři způsoby, jak deklarovat proměnné: var, let a const. Pro mnoho programátorů není úplně jasné, kdy kterou z nich použít, většina tutoriálů a linterů vás nutí používat je špatně. Pojďme si ukázat, jak psát čistší a srozumitelnější kód bez zbytečných pravidel, která nám ve skutečnosti nepomáhají.

Začněme tím nejnebezpečnějším

JavaScript má jednu zákeřnou vlastnost: pouhým opomenutím deklarace proměnné můžete nevědomky používat globální proměnnou. Stačí zapomenout na var, let nebo const:

function calculatePrice(amount) {
	price = amount * 100;    // Opomenutí! Chybí 'let'
	return price;            // Používáme globální proměnnou 'price'
}

function processOrder() {
	price = 0;               // Používáme tu samou globální proměnnou!
	// ... nějaký kód volající calculatePrice()
	return price;            // Vracíme úplně jinou hodnotu, než čekáme
}

Tohle je noční můra každého vývojáře – kód funguje zdánlivě správně, dokud nezačne někde jinde v aplikaci něco záhadně selhávat. Debugování takových chyb může zabrat hodiny, protože globální proměnná může být přepsána kdekoliv v aplikaci.

Proto je naprosto zásadní vždy deklarovat proměnné pomocí let nebo const.

Zapomeňte na var

Klíčové slovo var je v JavaScriptu od jeho počátku v roce 1995 a nese s sebou pár problematických vlastností, které byly v době vzniku jazyka považovány za features, ale časem se ukázaly jako zdroj mnoha chyb. Po dvaceti letech vývoje jazyka se autoři JavaScriptu rozhodli tyto problémy řešit – ne opravou var (kvůli zachování zpětné kompatibility), ale představením nového klíčového slova let v ES2015.

Na internetu najdete spoustu článků rozebírajících problémy var do nejmenších detailů. Ale víte co? Není potřeba se v tom babrat. Berme var prostě jako překonaný archaismus a pojďme se soustředit na moderní JavaScript.

Kdy použít let

let je moderní způsob deklarace proměnných v JavaScriptu.

Příjemné je, že proměnná existuje vždy pouze uvnitř bloku kódu (tedy mezi složenými závorkami), kde byla definována. To dělá kód předvídatelnější a bezpečnější.

if (someCondition) {
	let temp = calculateSomething();
	// temp je dostupná jen zde
}
// temp už zde neexistuje

V případě cyklů je deklarace přísně vzato umístěna před složenými závorkami, ale nenechte si tím zmást, proměnná existuje jen v cyklu:

for (let counter = 0; counter < 10; counter++) {
	// Proměnná counter existuje jen v cyklu
}
// counter už zde nejsou dostupné

Kdy použít const

const slouží k deklarování konstant. Typicky jde o důležité hodnoty na úrovni modulu nebo aplikace, které se nikdy nemají měnit:

const PI = 3.14159;
const API_URL = 'https://api.example.com';
const MAX_RETRY_ATTEMPTS = 3;

Je ale důležité pochopit jeden klíčový detail: const pouze zabraňuje přiřazení nové hodnoty do proměnné – neřeší, co se děje s hodnotou samotnou. Tento rozdíl se projevuje zejména u objektů a polí (pole je ostatně také objekt) – const z nich nedělá immutable objekty, tj. nezabraňuje změnám uvnitř objektu:

const CONFIG = {
	url: 'https://api.example.com',
	timeout: 5000
};

CONFIG.url = 'https://api2.example.com';  // Toto funguje!
CONFIG = { url: 'https://api2.example.com' };  // Toto vyhodí TypeError!

Pokud potřebujete skutečně neměnný objekt, musíte jej nejprve zmrazit.

Dilema let vs const

Nyní se dostáváme k zajímavější otázce. Zatímco u var vs let je situace jasná, použití const je předmětem mnoha diskuzí v komunitě. Většina tutoriálů, style-guides a linterů prosazuje pravidlo „používej const všude, kde můžeš“. Takže použití const vídáme zcela běžně v tělech funkcí nebo metod.

Pojďme si vysvětlit, proč je tato populární „best practice“ ve skutečnosti anti-pattern, který dělá kód méně čitelný a zbytečně svazující.

Přístup „pokud se proměnná v kódu nepřepisuje, měla by být deklarována jako const“ se na první pohled jeví logický. Proč by jinak bůh stvořil const? Čím víc „konstant“, tím bezpečnější a předvídatelnější kód, že? A navíc rychlejší, protože ho kompilátor může lépe optimalizovat.

Jenže celý tento přístup je ve skutečnosti nepochopení toho, k čemu konstanty slouží. Jde především o komunikaci záměru – opravdu chceme sdělit ostatním vývojářům, že do této proměnné se už nesmí nic přiřadit, nebo do ní jen náhodou v současné implementaci nic nepřiřazujeme?

// Skutečné konstanty - hodnoty, které jsou konstantní ze své podstaty
const PI = 3.14159;
const DAYS_IN_WEEK = 7;
const API_ENDPOINT = 'https://api.example.com';

// vs.

function processOrder(items) {
	// Toto NEJSOU konstanty, jen náhodou je nepřepisujeme
	const total = items.reduce((sum, item) => sum + item.price, 0);
	const tax = total * 0.21;
	const shipping = calculateShipping(total);
	return { total, tax, shipping };
}

V prvním případě máme hodnoty, které jsou konstantami ze své podstaty – vyjadřují neměnné vlastnosti našeho systému nebo důležitá konfigurační data. Když někde v kódu vidíme PI nebo API_ENDPOINT, okamžitě chápeme, proč jsou tyto hodnoty konstanty.

V druhém případě používáme const jen proto, že zrovna teď náhodou hodnoty nepřepisujeme. Ale není to jejich podstatná vlastnost – jsou to běžné proměnné, které bychom v příští verzi funkce klidně mohli chtít změnit. A když to budeme chtít udělat, const nám v tom bude zbytečně bránit.

V dobách, kdy byl JavaScript jeden velký globální kód, mělo smysl snažit se zabezpečit proměnné proti přepsání. Ale dnes píšeme kód v modulech a třídách. Dnes je běžné a správné, že scope je malá funkce a v jejím rámci vůbec nemá smysl rozdíl mezi let a const řešit.

Protože to vytváří naprosto zbytečnou kognitivní zátěž:

  1. Programátor musí při psaní přemýšlet: „Budu tuhle hodnotu měnit? Ne? Tak musím dát const…“
  2. Čtenáře to ruší! Vidí v kódu const a ptá se: „Proč je tohle konstanta? Je to nějaká důležitá hodnota? Má to nějaký význam?“
  3. Za měsíc potřebujeme hodnotu změnit a musíme řešit: „Můžu změnit const na let? Nespoléhá na to někdo?“

Používejte jednoduše let a tyto otázky nemusíte vůbec neřešit.

Ještě horší je, když toto rozhodnutí dělá automaticky linter. Tedy když linter „opraví“ proměnné na const, protože vidí jen jedno přiřazení. Čtenář kódu pak zbytečně přemýšlí: „Proč tady musí být tyto proměnné konstanty? Je to nějak důležité?“ A přitom to není důležité – je to jen shoda okolností. Nepoužívejte v ESLint pravidlo prefer-const!

Mimochodem, argument o optimalizaci je mýtus. Moderní JavaScript engine (jako V8) dokáže snadno detekovat, zda je proměnná přepisována nebo ne, bez ohledu na to, jestli byla deklarována pomocí let nebo const. Takže používání const nepřináší žádný výkonnostní benefit.

Implicitní konstanty

V JavaScriptu existuje několik konstrukcí, které implicitně vytvářejí konstanty, aniž bychom museli použít klíčové slovo const:

// importované moduly
import { React } from 'react';
React = something; // TypeError: Assignment to constant variable

// funkce
function add(a, b) { return a + b; }
add = something; // TypeError: Assignment to constant variable

// třídy
class User {}
User = something; // TypeError: Assignment to constant variable

Je to logické – tyto konstrukce definují základní stavební bloky našeho kódu a jejich přepsání by mohlo způsobit chaos v aplikaci. Proto je JavaScript automaticky chrání proti přepsání, stejně jako kdyby byly deklarovány pomocí const.

Konstanty ve třídách

Třídy byly do JavaScriptu přidány relativně nedávno (v ES2015) a jejich funkcionalita teprve postupně dospívá. Například privátní členy označené pomocí # přišly až v roce 2022. Na podporu konstant ve třídách JavaScript stále čeká. Prozatím můžete používat static, který ale není zdaleka to samé – označuje hodnotu sdílenou mezi všemi instancemi třídy, nikoliv však neměnnou.

Závěr

  1. var nepoužívejte – je to přežitek
  2. const používejte pro skutečné konstanty na úrovni modulu
  3. Ve funkcích a metodách používejte let – je to čitelnější a jasnější
  4. Nenechte linter automaticky měnit let na const – není to o počtu přiřazení, ale o záměru

Jak vyřešit chaos s prázdnými řetězci a NULL hodnotami v MySQL? 6.2.2025 14:10

Znáte to – vytvoříte dotaz WHERE street = '', ale systém nevrátí všechny záznamy, které byste čekali. Nebo vám nefunguje LEFT JOIN tak, jak má. Důvodem je častý problém v databázích: nekonzistentní používání prázdných řetězců a NULL hodnot. Pojďme si ukázat, jak tento chaos vyřešit jednou provždy.

Kdy použít NULL a kdy prázdný řetězec?

Teoreticky je rozdíl jasný: NULL znamená „hodnota není zadaná“, zatímco prázdný řetězec znamená „hodnota je zadaná a je prázdná“. Podívejme se na reálný příklad z e-shopu, kde máme tabulku objednávek. Každá objednávka má povinnou dodací adresu a volitelnou fakturační adresu pro případ, že zákazník chce fakturovat na jiné místo (typické zatržítko „Fakturovat na jinou adresu“):

CREATE TABLE orders (
    id INT PRIMARY KEY,
    delivery_street VARCHAR(255) NOT NULL,
    delivery_city VARCHAR(255) NOT NULL,
    billing_street VARCHAR(255) NULL,
    billing_city VARCHAR(255) NULL
);

Pole billing_city a billing_street jsou nullable, protože fakturační adresa nemusí být vyplněná. Ale je mezi nimi rozdíl. Zatímco ulice může být legitimně prázdná (obce bez ulic), nebo nezadaná (použije se dodací adresa), město musí být vždy vyplněné, pokud je fakturační adresa použita. Buď tedy billing_city obsahuje název města, nebo je NULL – v tomto případě se použije dodací adresa.

Realita velkých databází

V praxi ale často dochází k tomu, že se v databázi začnou míchat oba přístupy. Příčin může být několik:

Tohle vede k situacím, kdy máme v databázi mix hodnot a musíme psát složité podmínky:

SELECT * FROM tbl
WHERE foo = '' OR foo IS NULL;

Daleko horší je, že NULL se chová neintuitivně při porovnání:

SELECT * FROM tbl WHERE foo = ''; -- nezahrne NULL
SELECT * FROM tbl WHERE foo <> ''; -- taky nezahrne NULL

-- musíme použít
SELECT * FROM tbl WHERE foo IS NULL;
SELECT * FROM tbl WHERE foo <=> NULL;

Tato nekonzistence v chování porovnávacích operátorů je další důvod, proč je výhodnější používat v databázi jen jeden způsob reprezentace prázdné hodnoty.

Proč se vyhnout dvojímu přístupu

Podobná situace jako v MySQL existuje i v JavaScriptu, kde máme null a undefined. Po letech zkušeností mnoho JavaScript vývojářů dospělo k závěru, že rozlišování mezi těmito dvěma stavy přináší víc problémů než užitku a raději se rozhodli používat pouze systémově nativní undefined.

V databázovém světě je situace podobná. Místo toho, abychom stále řešili, jestli něco je prázdný řetězec nebo NULL, je často jednodušší zvolit jeden přístup a toho se držet. Například databáze Oracle prázdné řetězce a NULL hodnoty v podstatě ztotožňuje, čímž tento problém elegantně obchází. Je to jedno z míst, kde se Oracle odchyluje od SQL standardu, ale zároveň tím zjednodušuje práci s prázdnými/NULL hodnotami.

Jak něčeho podobného dosáhnout v MySQL?

Co vlastně chceme vynutit?

  1. U povinných polí (NOT NULL) chceme vynutit, aby vždy obsahovala smysluplnou hodnotu. Tedy zabránit vložení prázdného řetězce (nebo řetězce obsahujícího pouze mezery)
  2. U volitelných polí (NULL) chceme zabránit ukládání prázdných řetězců. Když je pole volitelné, měl by být NULL jedinou reprezentací „nevyplněné hodnoty“. Míchání obou přístupů v jednom sloupci vede k problémům s dotazováním a JOIN operacemi, které jsme si ukázali výše.

Řešení v MySQL

V MySQL dávalo historicky smysl naopak používat výhradně prázdné řetězce ('') místo NULL hodnot. Byl to totiž jediný přístup, který šlo vynutit pomocí NOT NULL constraintu. Pokud jsme chtěli automaticky konzistentní databázi, byla to jediná cesta.

Existuje ale jeden důležitý případ, kdy tento přístup selže – když potřebujeme nad sloupcem unikátní index. MySQL totiž považuje více prázdných řetězců za stejné hodnoty, zatímco více NULL hodnot za různé:

Nicméně od MySQL verze 8.0.16 můžeme použít CHECK constraint a mít tak větší kontrolu nad tím, jaké hodnoty povolíme. Můžeme například vynutit, že sloupec bude buď NULL, nebo bude obsahovat neprázdný řetězec:

CREATE TABLE users (
    id INT PRIMARY KEY,

    -- Povinné pole - musí obsahovat nějaký neprázdný text
    email VARCHAR(255) NOT NULL UNIQUE
        CONSTRAINT email_not_empty      -- název pravidla
        CHECK (email != ''),

    -- Nepovinné pole - buď NULL nebo neprázdný text
    nickname VARCHAR(255)
        CONSTRAINT nickname_not_empty
        CHECK (nickname IS NULL OR nickname != '')
);

Při vytváření CHECK constraintu je důležité dát mu smysluplný název pomocí klíčového slova CONSTRAINT. Díky tomu dostaneme v případě porušení pravidla srozumitelnou chybovou hlášku Check constraint ‚nickname_not_empty‘ is violated místo obecného oznámení o porušení constraintu. To výrazně usnadňuje debugging a údržbu aplikace.

Problém jsou nejen prázdné řetězce, ale i řetězce obsahující pouze mezery. Řešení pomocí CHECK constraintu můžeme vylepšit použitím funkce TRIM:

CREATE TABLE users (
    id INT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE
        CONSTRAINT email_not_empty
        CHECK (TRIM(email) != ''),
   ...
);

Nyní neprojdou ani tyto pokusy o obejití validace:

INSERT INTO users (email) VALUES ('   ');  -- samé mezery

Praktické řešení v Nette Framework

Konzistentní přístup k prázdným hodnotám je potřeba řešit i na úrovni aplikace. Pokud používáte Nette Framework, můžete využít elegantní řešení pomocí metody setNullable():

$form = new Form;
$form->addText('billing_street')
    ->setNullable(); // prázdný input se transformuje na NULL

Doporučení pro praxi

  1. Na začátku projektu se rozhodněte pro jeden přístup:
    • Buď používejte pouze NULL pro chybějící hodnoty
    • Nebo pouze prázdné řetězce pro prázdné/chybějící hodnoty
  2. Toto rozhodnutí zdokumentujte v dokumentaci projektu
  3. Používejte CHECK constrainty pro vynucení konzistence
  4. U existujících projektů:
    • Proveďte audit současného stavu
    • Připravte migrační skript pro sjednocení přístupu
    • Nezapomeňte upravit aplikační logiku

Tímto přístupem se vyhnete mnoha problémům s porovnáváním, indexováním a JOIN operacemi, které vznikají při míchání NULL a prázdných řetězců. Vaše databáze bude konzistentnější a dotazy jednodušší.

Přejmenování hodnot v ENUM bez ztráty dat: bezpečný návod 27.1.2025 02:16

Přejmenování hodnot v MySQL ENUMu je operace, která může být zrádná. Mnoho vývojářů se pokouší o přímou změnu, což často vede ke ztrátě dat nebo chybám. Ukážeme si, jak na to správně a bezpečně.

Představme si typický scénář: Máte v databázi tabulku objednávek (orders) se sloupcem status, který je typu ENUM. Obsahuje hodnoty waiting_payment, processing, shipped a cancelled. Požadavek je přejmenovat waiting_payment na unpaid a shipped na completed. Jak to udělat bez rizika?

Co nefunguje

Nejprve se podívejme na to, co nefunguje. Mnoho vývojářů zkusí tento přímočarý přístup:

-- TOHLE NEFUNGUJE!
ALTER TABLE orders
MODIFY COLUMN status ENUM(
    'unpaid',      -- původně 'waiting_payment'
    'processing',  -- beze změny
    'completed',   -- původně 'shipped'
    'cancelled'    -- beze změny
);

Takový přístup je receptem na katastrofu. MySQL se v takovém případě pokusí mapovat existující hodnoty na nový ENUM, a protože původní hodnoty už v definici nejsou, nahradí je prázdným řetězcem nebo vrátí chybu Data truncated for column 'status' at row X. V produkční databázi by to znamenalo ztrátu důležitých dat.

Nejprve zálohujte!

Před jakoukoli změnou struktury databáze je naprosto klíčové vytvořit zálohu dat. Použijte MySQL-dump nebo jiný nástroj, kterému důvěřujete.

Správný postup

Správný postup se skládá ze tří kroků:

  1. Nejprve rozšíříme ENUM o nové hodnoty
  2. aktualizujeme data
  3. nakonec odstraníme staré hodnoty.

Pojďme si to ukázat:

1. Prvním krokem je přidání nových hodnot do ENUMu, zatímco ponecháme ty původní:

ALTER TABLE orders
MODIFY COLUMN status ENUM(
    'waiting_payment',  -- původní hodnota
    'processing',       -- zůstává stejná
    'shipped',         -- původní hodnota
    'cancelled',       -- zůstává stejná
    'unpaid',          -- nová hodnota (nahradí waiting_payment)
    'completed'        -- nová hodnota (nahradí shipped)
);

2. Nyní můžeme bezpečně aktualizovat existující data:

UPDATE orders SET status = 'unpaid' WHERE status = 'waiting_payment';
UPDATE orders SET status = 'completed' WHERE status = 'shipped';

3. A konečně, když jsou všechna data převedena na nové hodnoty, můžeme odstranit ty staré:

ALTER TABLE orders
MODIFY COLUMN status ENUM(
    'unpaid',
    'processing',
    'completed',
    'cancelled'
);

Proč tento postup funguje?

Je to díky tomu, jak MySQL pracuje s ENUM hodnotami. Když provádíme ALTER TABLE s modifikací ENUMu, MySQL se snaží mapovat existující hodnoty podle jejich textové podoby. Pokud původní hodnota v novém ENUMu neexistuje, dojde v závislosti na nastavení sql_mode buď k chybě (při zapnutém STRICT_ALL_TABLES) nebo k náhradě prázdným řetězcem. Proto je klíčové mít v ENUMu vždy současně jak staré, tak nové hodnoty.

V našem případě to znamená, že během přechodné fáze, kdy máme v ENUMu hodnoty jako 'waiting_payment' i 'unpaid', každý záznam v databázi najde svůj přesný textový protějšek. Teprve po UPDATE dotazech, kdy už víme, že všechna data používají nové hodnoty, můžeme bezpečně odstranit ty staré.

Property Hooks v PHP 8.4: Revoluce nebo Past? 25.11.2024 03:23

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.

Backed nebo Virtual? Dobrá otázka!

Podívejte se na tento kód a zkuste si rychle odpovědět – je to vlastně jednoduchý kvíz:

class Person
{
	public int $age = 0 {
		set => $value >= 0 ? $value : throw new InvalidArgumentException;
	}

	public bool $adult {
		get => $this->age >= 18;
	}
}

Samozřejmě $age je, jak jsme si řekli už dříve, property pro čtení i pro zápis. Ale $adult je jen pro čtení!

A tady narážíme na první zapeklitost v designu property hooks. Ze signatury property vůbec nepoznáme, jestli do ní můžeme zapisovat nebo ji číst!

Odpověď se totiž skrývá v kódu, v implementaci hooků. Property totiž mohou být dvojího druhu: backed (se skutečným úložištěm v paměti) a virtuální (které pouze simulují existenci property). To, zda je property backed nebo virtuální, rozhoduje, zda se v kódu hooku na ni odkazujeme.

Property je backed (má vlastní úložiště), když:

V našem příkladu tedy:

Je to sice mazané řešení, ale ne zrovna šťastné. Tak zásadní informaci, jako zda lze property číst nebo do ní zapisovat, má prozradit API a signatura na první pohled, ne až studium implementace.

Když reference, tak bezpečně!

Reference existují v PHP od jeho počátků. Pomocí znaku & můžete propojit dvě proměnné tak, aby ukazovaly na stejné místo v paměti. Je to jako mít dva dálkové ovladače k jedné televizi – ať zmáčknete kterýkoliv, ovládáte tu samou obrazovku.

Ale co kdyby někdo mohl získat referenci na property s set hookem? Mohl by její hodnotu měnit přímo a kompletně tak obejít veškerou validaci. Podívejte se na tento příklad:

class Person
{
	public int $age = 0 {
		set => $value >= 0 ? $value : throw new InvalidArgumentException;
	}
}

$person = new Person;
$ref = &$person->age;    // Fatal error: Tohle neprojde!
$ref = -5;               // Kdyby to prošlo, validace by byla k ničemu

PHP na to myslelo a elegantně to vyřešilo (tedy pardon, mysleli na to Ilija Tovilo a Larry Garfield, autoři hooků). Získat referenci na takovou property prostě není možné (myšleno na backed proměnnou se set hookem). Je to správné řešení – property hook má zajistit, že se do property dostane jen platná hodnota, a reference by tuto kontrolu obcházely.

Když pole potká property hooks – zajímavá výzva!

Práce s poli v PHP je obvykle příjemně přímočará. Do pole v property můžeme přidávat prvky různými způsoby:

class Person
{
	public array $phones = [];
}

$person = new Person;
$person->phones[] = '777 123 456';          // přidá číslo na konec pole
$person->phones['bob'] = '777 123 456';     // přidá číslo s konkrétním klíčem

A právě tady narážíme na zajímavý problém s property hooks. Představme si, že chceme vytvořit třídu Person, která bude obsahovat seznam telefonních čísel, a chceme, aby se u nich automaticky ořezávaly mezery na začátku a konci:

class Person
{
	public array $phones = [] {
		set => array_map('trim', $value);
	}
}

$person = new Person;
$person->phones[] = '777 123 456';  // Překvapení! Error: Indirect modification of Person::$phones is not allowed

Proč to nefunguje? Operace $person->phones[] totiž v PHP funguje ve dvou krocích:

  1. Nejdřív získá referenci na pole pomocí get
  2. Pak do získaného pole přidá novou hodnotu

Tedy vůbec se nevolá set hook. Ba co víc, jak už víme z předchozí kapitoly, nelze získat referenci na backed proměnnou se set hookem (tedy udělat první krok). Proto ta chybová hláška.

Ani metoda addPhone(), která by volala $this->phones[] = $phone, nám nepomůže – všechny přístupy k property (i uvnitř třídy) totiž procházejí přes hooky.

Tak jak z toho ven? Pojďme si projít možná řešení. První, které vás možná napadne:

$phones = $person->phones;    // načteme pole
$phones[] = ' 777 123 456 ';  // přidáme číslo
$person->phones = $phones;    // uložíme zpět

Funguje to, ale… představte si pole s tisíci čísly. Náš set hook by musel provést trim() na všech číslech znovu, i když se přidalo jediné. To není zrovna ukázka efektivity.

Existuje lepší cesta – uvědomit si, že pokud má pole nějak specificky pracovat se svými prvky (třeba ořezávat mezery), mělo by to být jeho zodpovědností, ne úkolem třídy, která ho jen drží. Jasně, pole samo o sobě nenaučíme novým trikům, ale můžeme ho „zabalit“ do objektu s rozhraním ArrayAccess:

class Phones implements ArrayAccess
{
	private array $data = [];

	public function __construct(array $data = [])
	{
		$this->data = array_map('trim', $data);
	}

	public function offsetSet(mixed $offset, mixed $value): void
	{
		$value = trim($value);
		if ($offset === null) {
			$this->data[] = $value;
		} else {
			$this->data[$offset] = $value;
		}
	}

	// implementace dalších metod pro ArrayAccess...
}

class Person
{
	function __construct(
		public Phones $phones = new Phones,
	) {}
}

$person = new Person;
$person->phones[] = ' 777 123 456 ';  // Hurá! Číslo se uloží pěkně ořezané

A teď třešnička na dortu – můžeme využít hook k tomu, aby do $person->phones šlo zapsat i obyčejné pole:

class Person
{
	function __construct(
		public Phones $phones = new Phones {
			set(array|Phones $value) => is_array($value) ? new Phones($value) : $value;
		},
	) {}
}

$person = new Person;
$person->phones = ['  888 999 000  ', '777 888 999'];  // Magicky se převede na Phones a ořeže řetězce

Jak vidíte, hooks mohou obsahovat i promoted properties.

Ještě se podívejme na alternativní řešení. Vzpomeňte si, že kromě backed property máme ještě virtuální property – ty, které nepoužívají $this->propertyName v těle hooku. A tady se skýtá druhé řešení:

class Person
{
	private array $_phones = []; // skutečné úložiště čísel

	public array $phones {  // virtuální property pro veřejný přístup
		get => $this->_phones;
		set {
			$this->_phones = array_map('trim', $value);
		}
	}

	public function addPhone(string $phone): void
	{
		$this->_phones[] = trim($phone);
	}
}

$person = new Person;
$person->addPhone(' 777 123 456 ');  // Přidá ořezané číslo
echo $person->phones[0];             // Vypíše "777 123 456"
$person->phones = ['  888 999 000  ']; // Nastaví nové pole s ořezanými čísly

Tady jsme zůstali u klasického pole, ale schovali jsme ho za privátní proměnnou. Na venek nabízíme virtuální property pro čtení celého pole a jeho kompletní přepsání, plus specializovanou metodu pro přidávání jednotlivých čísel.

Hooks a dědičnost: Když potomci přebírají žezlo

Potomci mohou nejen přidávat hooks k vlastnostem, které je dosud neměly, ale také předefinovat ty existující. Podívejme se na příklad:

class Person
{
	public string $email;

	public int $age {
		set => $value >= 0
			? $value
			: throw new InvalidArgumentException('Věk nemůže být záporný');
	}
}

class Employee extends Person
{
	// Přidá hook k vlastnosti, která žádný neměla
	public string $email {
		set => strtolower($value);  // Emaily vždy převedeme na malá písmena
	}

	// Rozšíří existující validaci věku
	public int $age {
		set {
			if ($value <= 130) {  // First check the original condition
				throw new InvalidArgumentException('130 years? Not buying it!');
			}
			parent::$age::set($value);
		}
	}
}

Všimněte si té zajímavé syntaxe parent::$age::set($value). Na první pohled možná vypadá zvláštně, ale dává perfektní smysl – nejdřív se odkážeme na vlastnost v rodiči a pak na její hook. Je to jako bychom řekli „hej, zavolej set hook na age vlastnosti mého rodiče“.

A co víc – můžeme hooks označit jako final, pokud chceme zabránit jejich přepsání v potomcích. Dokonce můžeme jako final označit celou property – pak ji potomci nemohou změnit žádným způsobem (ani přidat hooks, ani rozšířit její viditelnost).

class Person
{
	// Tenhle hook už nikdo nepřepíše
	public int $age {
		final set => $value >= 0 ? $value : throw new InvalidArgumentException;
	}

	// A tuhle property už vůbec nikdo nezmění
	final public string $id;
}

Property v rozhraních

Překvapivou novinkou je podpora property v rozhraních a abstraktních třídách. Představte si, že vytváříte rozhraní pro entity, které obsahují řetězec se jménem. Doteď jsme museli psát něco takového:

interface Named
{
	public function getName(): string;
	public function setName(string $name): void;
}

Nuda, že? S property hooks můžeme být mnohem elegantnější! V rozhraní teď můžeme deklarovat přímo property, a to dokonce asymetricky – můžeme říct zvlášť, co má být čitelné a co zapisovatelné:

interface Named
{
	// Říkáme: "Implementující třída musí mít veřejně čitelnou property name"
	public string $name { get; }
}

A teď to zajímavé – jak můžeme takové rozhraní implementovat? Máme hned několik možností:

class Person implements Named
{
	public string $name;     // Nejjednodušší řešení - obyčejná property
}

class Employee implements Named
{
	public string $name {    // Pokročilejší - složené jméno
		get => $this->firstName . ' ' . $this->lastName;
	}
	private string $firstName;
	private string $lastName;
}

Všimněte si zajímavého detailu – rozhraní Named požaduje property pouze pro čtení, ale třída Person nabízí property čitelnou i zapisovatelnou. A to je naprosto v pořádku – rozhraní totiž definuje jen minimální požadavky. Je to jako když řeknete „potřebuju auto, co jede dopředu“ a dostanete auto, co umí i couvat – splňuje to vaše minimální požadavky a přidává něco navíc.

Pro puntičkáře: V rozhraní musíme u property použít klíčové slovo public, i když je to vlastně nadbytečné – vše v rozhraní je ze své podstaty veřejné. U metod uvádět public je hloupost, ale u property je to vyžadováno kvůli konzistenci syntaxe.

A ještě jedna věc stojí za zmínku – všimli jste si té zvláštní syntaxe { get; set; }? Zatímco ve třídě můžeme napsat jednoduše public string $name, v rozhraní musíme explicitně říct, jaké operace property podporuje. Je to sice trochu pracnější, ale dává to smysl – u rozhraní chceme být maximálně explicitní v tom, co požadujeme.

Property v abstraktních třídách: To nejlepší z obou světů

Abstraktní třídy si vezmou to nejlepší z rozhraní a přidají vlastní šťávu. Mohou nejen deklarovat property, ale také nabídnout výchozí implementaci některých hooků:

abstract class Person
{
	// Čistě abstraktní property - implementaci dodá potomek
	abstract public string $name { get; }

	// Protected property s oběma operacemi
	abstract protected int $age { get; set; }

	// Tady už nabízíme hotovou validaci emailu
	abstract public string $email {
		get; // tento hook je abstraktní a potomek ho musí implementovat
		set => Nette\Utils\Validators::isEmail($value)
			? $value
			: throw new InvalidArgumentException('Tohle nevypadá jako email...');
	}
}

A teď něco opravdu zajímavého – kovarianci a kontravarianci!

Zní to jako zaklínadlo, ale je to vlastně jednoduchá věc. Podívejte se:

class Animal {}
class Dog extends Animal {}

interface PetShop
{
	// Property jen pro čtení může vracet specifičtější typ
	public Animal $pet { get; }
}

class DogShop implements PetShop
{
	// Vrací psa místo zvířete - to je v pohodě!
	public Dog $pet { get; }
}

Když má property pouze hook get, může v potomkovi vracet specifičtější typ (tomu se říká kovariance). Představte si to jako: „Slíbil jsem ti zvíře, a pes je přece taky zvíře, ne?“

Naopak property pouze s hookem set může v potomkovi přijímat obecnější typ (kontravariance). Je to logické – když umím pracovat s konkrétním typem, zvládnu i jeho předka.

Jakmile má property oba hooky get i set, musí typ zůstat stejný. Proč? Protože by to mohlo vést k nekonzistencím – nemůžeme slíbit, že vrátíme psa, když nám někdo může přes setter podstrčit kočku!

Asymetrická viditelnost: Každému, co jeho jest

Představte si, že vytváříte třídu Person a chcete, aby datum narození mohl číst kdokoliv, ale měnit ho mohla jen samotná třída. Dřív byste museli sáhnout po getterech a setterech, ale teď? Teď máme elegantní řešení:

class Person
{
	public private(set) DateTimeImmutable $dateOfBirth;
}

Tenhle zápis říká: „Číst může každý, zapisovat jen třída sama.“ První modifikátor public určuje viditelnost pro čtení, druhý private(set) pro zápis. A protože veřejné čtení je default, můžeme ho vynechat a psát prostě:

class Person
{
	private(set) DateTimeImmutable $dateOfBirth;
}

Samozřejmě platí logické pravidlo – viditelnost pro zápis nemůže být širší než pro čtení. Nemůžeme použít třeba protected public(set) – to by bylo jako říct „číst můžou jen potomci, ale zapisovat může každý“. Trochu podivné, ne?

A co dědičnost? V PHP platí, že potomek může viditelnost buď zachovat, nebo rozšířit z protected na public. To samé platí i pro asymetrickou viditelnost:

class Person
{
	public protected(set) string $name;  // Číst může každý, zapisovat jen potomci
}

class Employee extends Person
{
	public public(set) string $name;     // Potomek může rozšířit práva zápisu
}

Zajímavý je případ private(set). Taková property je automaticky final – když řekneme, že zapisovat může jen třída sama, logicky to znamená, že ani potomci nemají právo to měnit.

A nejlepší na tom je, že asymetrickou viditelnost můžeme kombinovat s hooks:

class Person
{
	private(set) DateTimeImmutable $birthDate {
		set => $value > new DateTimeImmutable
			? throw new InvalidArgumentException('Narození v budoucnosti? Sci-fi!')
			: $value;
	}
}

Tahle property má všechno: je veřejně čitelná, zapisovat do ní může jen třída sama, a ještě kontroluje, jestli datum není v budoucnosti. Hooks řeší „co se má stát“, asymetrická viditelnost „kdo to může udělat“. Perfektní tým!

Asymetrická viditelnost a pole: Elegantní řešení starého problému

Pamatujete na naše trápení s telefonními čísly? Asymetrická viditelnost nám nabízí ještě jedno řešení:

class Person
{
	private(set) array $phones = [];

	public function addPhone(string $phone): void
	{
		$this->phones[] = trim($phone);
	}
}

$person = new Person;
var_dump($person->phones);     // OK: můžeme číst
$person->addPhone('...');      // OK: můžeme přidat číslo
$person->phones = [];          // CHYBA: nemůžeme přepsat celé pole

Pole je veřejně čitelné, ale nikdo zvenčí ho nemůže přepsat. Pro přidání nového čísla máme specializovanou metodu. Žádné složité objekty simulující pole, žádné virtual properties – jen čistá, jasná kontrola přístupu.

Pro úplnost dodejme, že u vlastnosti s omezeným zápisem nemůžete získat referenci zvenku:

$ref = &$person->phones;    // Fatal error: Takhle ne!

Reference jsou povolené jen ze scope, ze kterého je property zapisovatelná. Je to logické – reference by mohla obejít naše omezení pro zápis.

Když to shrneme, pro práci s polem v property máme teď několik možností:

  1. Chytrý objekt simulující pole (přináší víc možností, ale taky víc kódu)
  2. Backed property s hookem (znemožňuje přímou modifikaci pole)
  3. Virtual property s privátním úložištěm (vyžaduje metody pro úpravy)
  4. Asymetrická viditelnost (přesouvá logiku do metod)

Který přístup vybrat? Jak už to bývá – záleží na konkrétním případu. Prostě si zkusit, které API nejlépe sedne do ruky.

Readonly a asymetrická viditelnost: Konečně svoboda volby!

Modifikátor readonly jsou ve skutečnosti dva modifikátory v jednom: zakazuje vícenásobný zápis a zároveň omezuje zápis na private. Vlastně to není žádný readonly, je to spíš writeonce zkřížený s private(set).

To druhé mi přišlo vždy zbytečně přísné a nepraktické. Proč by readonly vlastnost nemohla být zapisovatelná třeba v potomcích?

PHP 8.4 to konečně změnilo. Teď readonly dělá property defaultně protected(set), tedy zapisovatelnou i v potomcích. A když potřebujeme jinou viditelnost? Jednoduše si ji nastavíme:

class Person
{
	// Readonly přístupná jen uvnitř třídy (staré chování)
	public private(set) readonly string $name;

	// Readonly přístupná i v potomcích (nové výchozí chování)
	public readonly string $dateOfBirth;

	// Readonly zapisovatelná kdekoliv (ale jen jednou!)
	public public(set) readonly string $id;

	public function rename(string $newName): void
	{
		$this->name = $newName;    // Uvnitř třídy můžeme měnit
	}
}

class Employee extends Person
{
	public function setBirthDate(DateTimeImmutable $date): void
	{
		$this->dateOfBirth = $date;  // V potomkovi můžeme měnit
	}
}

$person = new Person;
$person->id = 'abc123';     // Tohle projde
$person->id = 'xyz789';     // Ale tohle už ne - readonly!

Což nám dává přesně tu flexibilitu, kterou potřebujeme.

Když si terminologie protiřečí…

Pojďme se podívat na malý terminologický zmatek v PHP:

Bylo by logičtější používat termíny read a write, ne?

U hooks bych ještě pochopil použití get/set – jde o akce a navíc to navazuje na magické metody __get/__set. Ale u asymetrické viditelnosti? To je přece koncepčně odlišná věc – neřeší „co se má stát“ jako hooks, ale „kdo to může udělat“. Proto by dávalo mnohem větší smysl použít termín write, tedy například private(write):

class Person
{
	private(set) string $name;     // takhle to je
	private(write) string $name;   // takhle by to dávalo větší smysl
}

Druhá varianta by byla mnohem intuitivnější. Navíc by lépe ladila s existujícím modifikátorem readonly.

Vypadá to, že PHP ve snaze o syntaktickou konzistenci mezi hooks a asymetrickou viditelností obětovalo sémantickou konzistenci s již existujícími koncepty v jazyce.

Nová éra v PHP: Revoluce v objektovém návrhu

V PHP světě jsme byli dlouho odkázáni na jediný správný způsob objektově orientovaného návrhu: všechny vlastnosti private a přístup k nim výhradně přes gettery a settery. Nebyla to rozmazlenost vývojářů – public property prostě byly problematické:

Kdo chtěl programovat správně, používat rozhraní a dependency injection, musel sáhnout po getterech a setterech. Byla to jediná cesta, jak mít plnou kontrolu nad tím, co se v objektech děje.

Ale s PHP 8.4 přichází nová doba! Property hooks a asymetrická viditelnost nám konečně dávají nad vlastnostmi stejnou kontrolu jako nad metodami. Property se stávají plnohodnotnou součástí veřejného API, protože:

V podstatě můžete property hooks brát jako elegantní náhradu getterů a setterů bez zbytečného boilerplate kódu. Nebo naopak – gettery a settery byly jen takové provizorní řešení, než PHP dospělo k něčemu lepšímu.

Ze své vlastní zkušenosti s Nette můžu mluvit velmi konkrétně – jak jsem říkal, framework podobnou funkcionalitu nabízel už před 17 lety. To znamená, že jsem měl možnost s property přístupem pracovat dlouho. A musím říct, že to bylo nesmírně návykové. Porovnejte:

// Starý svět
$this->getUser()->getIdentity()->getName()

// Nový svět
$this->user->identity->name

Druhý zápis není jen kratší a čitelnější – je taky přirozenější. Je to jako rozdíl mezi „Prosím, mohli byste mi laskavě podat informaci o vašem jméně?“ a normálním „Jak se jmenuješ?“.

Jasně, možná namítnete, že přímý přístup k datům může svádět k porušování principů objektově orientovaného návrhu. Že místo ptaní se objektu na data bychom ho měli požádat o akci (Tell-Don't-Ask). To je pravda – ale hlavně pro objekty s bohatým chováním, které implementují business logiku. Pro datové transfer objekty, value objects nebo konfigurační třídy je přímý přístup k datům naprosto přirozený.

Zároveň nám tu vzniká pořádné dilema. Co s existujícími projekty? Pokud máte knihovnu nebo framework, který důsledně používá gettery a settery, bylo by možná kontraproduktivní do něj najednou zavádět property. Rozbili byste tím konzistenci API – uživatel by musel hádat, kde použít metodu a kde vlastnost.

Časem se určitě vytvoří nové styly a konvence. Některé projekty možná zůstanou u getterů a setterů, jiné budou hledat cesty jak začlenit property. Hlavně že máme na výběr.

Důležité je i pojmenování

Jak vlastně property pojmenovat? Zejména u boolean hodnot to není tak přímočaré, jak by se mohlo zdát.

U metod se běžně používají prefixy is nebo has:

class Article {
	public function isPublished(): bool { ... }
	public function hasComments(): bool { ... }
}

Ale u properties by tyto prefixy působily krkolomně a redundantně. Místo nich je lepší používat přídavná jména nebo podstatná jména:

class Article {
	public bool $published;     // lepší než $isPublished
	public bool $commented;     // lepší než $hasComments
	public bool $draft;         // lepší než $isDraft
}

if ($article->published) {      // čte se přirozeně
	// ...
}

Pro počty položek je lepší použít množné číslo:

class Article {
	public int $views;          // lepší než $viewCount
	public array $tags;         // jasně říká, že jde o kolekci
}

Jde o to, aby kód byl čitelný jako běžná věta. Když píšeme if ($article->published), čte se to mnohem přirozeněji než if ($article->isPublished). Property by měly vypadat jako vlastnosti, ne jako zapomenuté závorky u metody.

Kdy použít property a kdy metody?

Výborná otázka! Tady si můžeme vzít inspiraci z jazyků jako C# nebo Kotlin, které s property pracují už roky. Property se skvěle hodí pro:

Value objects a DTO:

class Money {
	public readonly float $amount;
	public readonly string $currency;
}

Jednoduché entity:

class Article {
	public string $title;
	public string $content;
	public DateTimeImmutable $publishedAt;
	public bool $published {
		get => $this->publishedAt <= new DateTimeImmutable;
	}
}

Computed hodnoty závislé na jiných vlastnostech:

class Rectangle {
	public float $width;
	public float $height;
	public float $area {
		get => $this->width * $this->height;
	}
}

Metody jsou lepší pro:

Všechna tato doporučení se nám snaží říct jednu základní věc: za použitím property by se neměl skrývat žádný složitý proces nebo něco, co má vedlejší efekty. Složitost operace by měla zhruba odpovídat tomu, co intuitivně očekáváme od čtení či zápisu do proměnné.

I když… vzpomeňte si na innerHTML v JavaScriptu. Když napíšete element.innerHTML = '<p>Ahoj</p>', spustí se složitý proces parsování HTML, vytvoření DOM stromu, překreslení stránky… A přesto to všichni považují za přirozené!

Takže možná důležitější než samotná složitost implementace je to, jestli daná operace _konceptuálně_ odpovídá vlastnosti. Je to jako s autem – tlačítko start/stop může spustit složitou sekvenci kroků, ale pro řidiče je to pořád jen „zapnout/vypnout“.

Readonly vlastnosti v PHP a jejich skrytá úskalí 24.11.2024 17:58

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.

Readonly třídy: Když jeden zámek nestačí

S PHP 8.2 přišla možnost posunout zabezpečení na další úroveň. Představte si, že místo zamykání jednotlivých místností můžete zamknout celou budovu. Tedy celou třídu:

readonly class User
{
    public string $name;     // Automaticky readonly!
    public string $email;    // Taky readonly!
}

Ale pozor, s velkou mocí přichází velká omezení:

Kdo může inicializovat a kdy?

Tady je to zajímavé – podívejte se na tento kód:

$user = new User;
$user->name = 'John';  // BUUM! Cannot initialize readonly property from global scope

Překvapení? I když jde o první přiřazení, PHP řekne ne. Stejně tak potomek třídy nemůže inicializovat readonly vlastnost svého rodiče:

class Employee extends User
{
    public function setName(string $name): void
    {
        $this->name = 'EMP: ' . $name;  // BUUM! Ani potomek nemůže!
    }
}

Readonly proměnnou lze tedy inicializovat výhradně ze třídy, která ji definovala. Přesněji řečeno šlo. PHP 8.4 totiž přináší dvě důležité změny:

class User
{
    public(set) readonly string $name;  // Nová svoboda v PHP 8.4
}

$user = new User;
$user->name = 'John';  // Teď už to funguje!

Když readonly neznamená „opravdu neměnné“

Představte si readonly jako zámek na dveřích – zamkne dveře, ale co se děje uvnitř místnosti, to už neuhlídá. Samotné readonly nezaručuje úplnou neměnnost dat. Pokud do readonly proměnné uložíme objekt, jeho vnitřní stav zůstává modifikovatelný, objekt se automaticky nestává immutable (neměnným):

class Settings
{
	public string $theme = 'light';
}

class Configuration
{
	public function __construct(
		public readonly Settings $settings = new Settings,
	) {
	}
}

$config = new Configuration;
$config->settings->theme = 'dark'; // toto je povoleno, přestože $settings je readonly!

Vidíte? Samotný objekt $settings je uzamčený, ale jeho vnitřnosti můžeme měnit, jak se nám zlíbí.

U polí je situace specifická. Přímá modifikace prvků pole není možná, protože PHP to považuje za změnu celého pole.

class Configuration
{
    public readonly array $settings;

    public function initialize(): void
    {
        $this->settings = ['debug' => true];
        $this->settings['cache'] = true;  // BUUM! Tohle neprojde
    }
}

Existuje však výjimka – pokud pole obsahuje reference, jejich obsah měnit můžeme, protože PHP to nepovažuje za změnu samotného pole. Toto chování je konzistentní s běžným fungováním PHP:

class Configuration
{
    public readonly array $settings;

    public function initialize(): void
    {
        // Trik s referencí
        $mode = 'development';
        $this->settings = [
            'debug' => true,
            'mode' => &$mode,  // Reference je naše tajná zbraň!
        ];

        $mode = 'production';  // Tohle projde!
    }
}

Wither metody a readonly: Jak na to?

Při práci s neměnnými objekty často potřebujeme implementovat metody pro změnu stavu. Tyto „wither“ metody (na rozdíl od klasických setterů) nemodifikují původní objekt, ale vrací jeho novou instanci s požadovanou změnou. Tento pattern využívá například specifikace PSR-7 pro HTTP požadavky.

Když chceme tyto objekty nebo jejich vlastnosti označit jako readonly, narazíme na technické omezení – readonly vlastnost nelze změnit ani ve wither metodě, a to ani v kopii objektu. I když PHP 8.3 umožňuje měnit readonly vlastnosti v metodě __clone(), samotné klonování nestačí, protože v něm nemáme přístup k nové hodnotě. Můžeme to ale vyřešit pomocí následující obezličky:

class Request
{
    private array $changes = [];

    public function __construct(
        public readonly string $method = 'GET',
        public readonly array $headers = [],
    ) {}

    public function withMethod(string $method): self
    {
        $this->changes['method'] = $method;
        $dolly = clone $this;
        $this->changes = [];
        return $dolly;
    }

    public function __clone()
    {
        foreach ($this->changes as $property => $value) {
            $this->$property = $value;
        }
        $this->changes = [];
    }
}

$request = new Request('GET');
$newRequest = $request->withMethod('POST');  // Původní $request zůstává s GET

Testování a BypassFinals

Při psaní testů můžeme narazit na praktický problém – readonly vlastnosti (podobně jako final) komplikují mockování a testování. Naštěstí existuje elegantní řešení v podobě knihovny BypassFinals.

Tato knihovna dokáže za běhu odstranit klíčová slova final a readonly z vašeho kódu, což umožňuje mockovat i třídy a metody, které jsou takto označené. Integrace s testovacími frameworky je přímočará:

// bootstrap.php nebo začátek test souboru
DG\BypassFinals::enable();

// Pokud chceme zachovat readonly a odstranit jen final:
DG\BypassFinals::enable(bypassReadOnly: false);

Shrnutí: Co si odnést

Readonly vlastnosti jsou mocný nástroj pro zvýšení bezpečnosti a předvídatelnosti vašeho kódu. Zapamatujte si klíčové body:

Dvě slova, co ničí open source 4.10.2024 22:57

Víte, co nikdy, ale opravdu NIKDY nemáte psát autorům open source projektů? „Nemám čas“. Tahle dvě slova mají schopnost rozpustit motivaci vývojářů rychleji než mizí baterka na iPhonu při scrollování TikToku.

Vážně? VÁŽNĚ?!

Představte si, že jste na párty a někdo vám řekne: „Hej, ty tam s tím pivem! Udělej mi sendvič. Nemám čas si ho udělat sám, jsem příliš zaneprázdněn konzumací chipsů.“ Jak byste se cítili? Jako obědový automat s lidskou tváří? Přesně tak se cítím já, když čtu taková slova. Okamžitě ztrácím chuť věc řešit a mám nutkání se jít věnovat čemukoliv jinému. Třeba pustému nicnedělání.

Víte, my open source vývojáři jsme zvláštní stvoření. Trávíme hodiny našeho volného času tvorbou softwaru, který pak dáváme k dispozici všem. Zadarmo. Dobrovolně. Jako kdyby Ježíšek rozdával dárky každý den v roce a ne jen na Vánoce. Baví nás to. Ale tím vám nevzniká nárok nás úkolovat jako nějaké digitální otroky. Takže když někdo přijde s požadavkem na novou funkci, ale „nemá čas“ přiložit ruku k dílu, okamžitě tím vyvolá otázku „a proč bych já ten čas měl mít?“ Jako byste chtěli po Michelangelovi, aby vám vymaloval obývák, protože vy „nemáte čas“ to udělat sami, šak stejně nemá co lepšího na práci.

Za roky se mi nashromáždily desítky issues u různých projektů, ve kterých jsem poprosil „Mohl bys připravit pull request?“ a odpovědí bylo „Mohl, ale tento týden nemám čas.“ Kdyby ten nebožák onu větu nenapsal, nejspíš bych věc dávno vyřešil. Takhle mi ale řekl, že pohrdá mým časem. Takže to vyřešil sám za týden? Kdeže… 99 % věcí, které kdy kdo slíbil, nikdy nedodal, tudíž i 99 % těchto issues jsou navždy nevyřešené. Visí tam jako digitální pomníky lidské lenosti.

Takže, milí uživatelé, příště než napíšete „Nemám čas“, zamyslete se. Ve skutečnosti říkáte: „Hej, ty tam! Tvůj volný čas nemá žádnou hodnotu. Hoď všechno co děláš za hlavu a věnuj se MÉ záležitosti!“ Zkuste místo toho:

Když narazíte na bug, budete chtít novou featuru, nebo zjistíte, že by stálo za to něco doplnit do dokumentace, zkuste pro jednou prospět komunitě. Protože v open source světě jsme všichni na jedné lodi. A ta loď pluje na vlnách vzájemného respektu a spolupráce. Tak nezapomeňte občas také zaveslovat, místo abyste jen seděli a stěžovali si, že nemáte čas na pádlování. Vaše „nemám čas“ je absolutní způsob, jak zničit motivaci lidí, kteří vám zdarma poskytují software. Zkuste si těch pár minut nebo hodin najít. Vaše karma vám poděkuje.

Proč GPT je SQL našeho století? 28.5.2024 19:34

A naopak SQL bylo GPT sedmdesátých let?

SQL, vzniklé v 70. letech minulého století, představovalo revoluční průlom v interakci člověka s počítačem. Jeho design byl navržen tak, aby se dotazy formulovaly a četly co nejvíce jako běžná angličtina. Například, dotaz na jména a platy zaměstnanců v SQL může vypadat takto: SELECT name, salary FROM employee – jednoduché a srozumitelné, že ano? Tím se databáze staly dostupné širší veřejnosti, nejen počítačovým nerdům.

Ačkoli tento záměr byl chvályhodný, brzy se ukázalo, že na psaní SQL dotazů jsou stejně potřeba experti. Navíc vzhledem k tomu, že nikdy nevzniklo univerzální aplikační rozhraní, stalo se slovní ovládání pro moderní programování spíše břemenem. Programátoři dnes komunikují s databázemi psaním komplikovaných generátorů SQL příkazů, které databáze následně zpětně dekódují.

A tady na scénu vstupuje GPT. Tyto pokročilé jazykové modely přinášejí v éře umělé inteligence obdobnou revoluci, jakou SQL znamenalo pro databáze ve své době. Umožňují interakci s databázemi na úplně nové úrovni, o 50 let pokročilejší. Přestože zatím neexistuje databáze, která by byla přímo ovládaná jazykovým modelem, můžeme si od GPT nechat generovat SQL. A funguje to skvěle.

Jak na to? Stačí, když do ChatGPT zkopírujete strukturu vaší databáze, tedy dump s příkazy CREATE TABLE, a uvedete i název databázového serveru, který používáte. Následně můžete klást otázky přímo v češtině.

Pokud si chcete použití této techniky ještě více ulehčit, použijte předpřipravený SQL Wizard, kam stačí jen překopírovat strukturu databáze a můžete se rovnou ptát bez dalších řečí.

Pojďme si to prakticky ukázat. Jako příklad použijeme dvě databázové tabulky, které jsou provázané skrze cizí klíč. Tabulka users bude obsahovat informace o uživatelích a tabulka orders bude ukládat informace o objednávkách, které uživatelé udělali. Zde jsou definice těchto tabulek:

CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(100)
);

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    user_id INT,
    created_at DATE,
    amount DECIMAL(10, 2),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

Definice zkopírujte do ChatGPT a hned se můžete zcela přirozeně a v češtině zeptat například na všechny objednávky od uživatele Jana Nováka. Konkrétně zadám: „Napiš SQL, které vypíše všechny objednávky uživatele Jana Nováka.“ A GPT odpoví například takto:

SELECT orders.order_id, orders.created_at, orders.amount
FROM orders
JOIN users ON orders.user_id = users.id
WHERE users.name = 'Jan Novák';

Takový SQL dotaz dokáže samozřejmě spousta programátorů napsat rychleji, než by se přihlásili do ChatGPT 🙂 Ale zkusme maličko složitější příklad: „Vypiš uživatele, kteří mají jen jednu objednávku.“

SELECT users.id, users.name, users.email
FROM users
JOIN orders ON users.id = orders.user_id
GROUP BY users.id, users.name, users.email
HAVING COUNT(orders.order_id) = 1;

Nebo „Vypiš uživatele, kteří nemají žádnou objednávku.“

SELECT users.*
FROM users
LEFT JOIN orders ON users.id = orders.user_id
WHERE orders.order_id IS NULL;

Tady se ukazuje diametrální rozdíl v přístupu. Zatímco GPT říkám, jaká data chci získat, v SQL musím popsat způsob, jak je získat. A to vyžaduje znalosti a zkušenosti.

Díky GPT se můžete soustředit na to, co chcete získat, místo toho, jak to získat, což je zvláště cenné pro ty, kteří nejsou odborníky na SQL.

Na závěr si ukážeme, jak lze využít GPT k vytvoření databázových trigerů. Například můžete napsat: „Přidej do tabulky users sloupec order_count, který bude evidovat počet objednávek. Inicializuj ho na hodnotu odpovídající aktuálnímu počtu objednávek každého uživatele. A vytvoř trigger, který při vytvoření nebo smazání objednávky bude hodnotu aktulizovat.“

GPT vygeneruje dotazy pro přidání a inicializaci sloupce order_count:

ALTER TABLE users ADD COLUMN order_count INT DEFAULT 0;
UPDATE users SET order_count = (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id);

a požadované triggery, které udržují jeho hodnotu aktuální:

CREATE TRIGGER update_order_count_after_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
    UPDATE users
    SET order_count = order_count + 1
    WHERE id = NEW.user_id;
END;

CREATE TRIGGER update_order_count_after_delete
AFTER DELETE ON orders
FOR EACH ROW
BEGIN
    UPDATE users
    SET order_count = order_count - 1
    WHERE id = OLD.user_id;
END;

GPT nabízí způsob, jak efektivně a intuitivně pracovat s databázemi, i těm, kteří nejsou odborníky na SQL. Je to revoluční nástroj, který tentokrát opravdu zpřístupňuje pokročilé databázové operace široké veřejnosti. Stále je však důležité mít na paměti, že každý výstup by měl být pečlivě kontrolován, aby se zajistila správnost a bezpečnost dat.


Pokud jste připraveni posunout své dovednosti, přijďte na školení ChatGPT. Toto setkání vás naučí, jak z něj vytáhnout maximum pro váš osobní i profesní život. Nezáleží na tom, zda jste začátečník nebo pokročilý uživatel, školení bude pro vás velkým přínosem.

Pět novinek v Latte 3.1, které vám zpříjemní život 24.3.2026 17:36

Latte 3.1 přináší pětici novinek – nové filtry |column, |commas a |limit, vylepšený |slice pro iterátory a dvě nové featury enginu. Žádná revoluce, ale drobnosti, které oceníte.

Konec úniku proměnných z {foreach}

Tohle mě štvalo roky. Napíšete {foreach $items as $item}, cyklus skončí, a $item vám vesele přežívá s hodnotou posledního prvku. Horší je, když si tím přepíšete proměnnou, se kterou jste počítali:

{var $name = 'Jupí'}

{foreach $users as $name}
    ...
{/foreach}

{$name} {* ouha, tady už není 'Jupí' *}

Latte vás sice odedávna varovalo, když foreach přepsal proměnnou předanou do šablony přes parametry. Ale proměnné vytvořené přímo v šabloně, například přes {var}, se přepisovaly tiše. A to je přesně ten zákeřný případ.

Teď to jde vyřešit nastavením:

$latte->setFeature(Latte\Feature::ScopedLoopVariables);

Proměnné z {foreach} od teď existují jen uvnitř cyklu. Po skončení se vrátí původní hodnota. A pokud proměnná předtím neexistovala? Zmizí úplně.

{var $name = 'Jupí'}

{foreach $users as $name}
    ...
{/foreach}

{$name} {* vypíše 'Jupí', jupí! *}

A to staré varování o přepsaných parametrech? S touhle featurou se vypíná, protože přestává dávat smysl – nic se už nepřepisuje.

Funguje to i s destrukturováním {foreach $data as [$a, $b]} a samozřejmě i s klíčem {foreach $arr as $k => $v} – po cyklu se uklidí vše. Vnořené cykly mají nezávislé scope.

Jediná výjimka: pokud iterujete přes referenci {foreach $arr as &$v}, scope se neuplatní – reference přímo modifikují původní pole, takže obnovování hodnot po cyklu by je rozbilo. Logické.

Odsazujte šablony, jak chcete

Znáte to: máte <ul> a uvnitř {foreach}, který generuje <li>. Přirozeně to odsadíte, aby šablona byla čitelná. Jenže to odsazení se pak propíše do výstupu. Buď máte hezký kód a ošklivý HTML, nebo naopak.

Neakceptovatelné.

S Feature::Dedent tohle dilema mizí:

$latte->setFeature(Latte\Feature::Dedent);

Latte automaticky odstraní společné odsazení uvnitř párových tagů. Takže tohle:

<ul>
    {foreach $items as $item}
        <li>{$item}</li>
    {/foreach}
</ul>

vygeneruje:

<ul>
    <li>...</li>
    <li>...</li>
</ul>

Odsazení jako by tam nikdy nebylo.

A protože se dedent provádí už při kompilaci šablony, ne při každém renderování, nemá to žádný vliv na výkon. Funguje pro všechny párové tagy – {if}, {block}, {capture}, {foreach} a další. Vnořené tagy se dedentují nezávisle, každý na své úrovni.

Pokud je odsazení nekonzistentní – kupříkladu mícháte tabulátory s mezerami, nebo některý řádek nemá dostatečné odsazení – Latte vyhodí CompileException s přesným číslem řádku. Žádné tiché polykání chyb.

Filtr |commas

Spojit pole do řetězce odděleného čárkami – brnkačka. Ale co když chcete před posledním prvkem napsat „a" místo čárky? To je přesně ta věc, kvůli které v šabloně začnete psát podmínky a najednou máte čtyři řádky kódu místo jednoho. Přitom chcete říct jen „jablko, hruška a švestka".

{['jablko', 'hruška', 'švestka']|commas}       {* jablko, hruška, švestka *}
{['jablko', 'hruška', 'švestka']|commas:' a '}  {* jablko, hruška a švestka *}
{['jablko', 'hruška', 'švestka']|commas:', nebo '}  {* jablko, hruška, nebo švestka *}

Bez parametru spojí čárkou a mezerou. S parametrem použije zadaný řetězec jako oddělovač mezi posledními dvěma prvky – zbytek zůstane oddělený čárkami. Prostě čeština, ne foreach.

Filtr |column

Máte pole záznamů o uživatelích, třeba [['id' => 1, 'name' => 'Jan'], ['id' => 2, 'name' => 'Petr'], ...], a potřebujete z nich dostat jen jména? Filtr |column vytáhne hodnoty jednoho sloupce – ať už jde o klíč v poli, nebo property objektu:

{$users|column:'name'|commas}   {* Jan, Petr, Marie *}

Volitelně přijímá i druhý parametr pro indexování výsledků:

{foreach ($users|column:'name':'id') as $id => $name}
    {$id}: {$name}
{/foreach}

Funguje i s iterátory, nejen s poli – nicméně iterátor se interně převede na pole, takže na žádnou lazy magii nečekejte.

Filtr |slice pro iterátory a nový filtr |limit

Filtr |slice vyřízne kus pole nebo řetězce (u řetězců s respektem k UTF-8). Dosud ale fungoval jen s poli a řetězci. Teď zvládne i iterátory a generátory – vrací generátor, který čte prvky z původního zdroje jeden po druhém a po dosažení limitu se zastaví. Celý iterátor se do paměti nenačítá:

{foreach ($generator|slice:0:10) as $item}
    {$item}
{/foreach}

A k tomu přibyl filtr |limit – pohodlnější varianta pro typický případ „vezmi prvních N prvků". Funguje s poli, iterátory i řetězci (s respektem k UTF-8):

{foreach ($items|limit:5) as $item}
    {$item}
{/foreach}

{$description|limit:100}

Rozdíl oproti |slice: filtr |limit ve výchozím stavu zachovává originální klíče.

Jak na to

Nové featury ScopedLoopVariables a Dedent se zapínají přes setFeature():

$latte = new Latte\Engine;
$latte->setFeature(Latte\Feature::ScopedLoopVariables);
$latte->setFeature(Latte\Feature::Dedent);

A pokud používáte Nette, stačí to zapnout v konfiguraci:

latte:
    scopedLoopVariables: true
    dedent: true

Pět drobností pro život.

Případ ucpané roury 7.1.2026 22:59

„Tenhle job vás naučí, že každý operační systém má svoje kostlivce ve skříni. Linux? Ten se tváří upřímně — open source, jak se tomu říká. Ale Windows? Windows je ta záhadná kráska v koutě baru. Usmívá se, říká, že je všechno v pořádku, a přitom jí ve sklepě hnijou mrtvoly.“

Kapitola 1: Dokonalý zločin

Ten případ mě pronásleduje čtrnáct let.

Začalo to v roce 2012. Nette Tester právě ovládl tajemství vícevláknového testování. Osm vláken v dokonalé souhře by default, jako dirigovaný orchestr. Byl to průkopník — v době, kdy ostatní knihovny pouštěly testy jeden po druhém jako důchodce na poště, Tester už dávno paralelizoval. Šedesát sekund testů stlačených do deseti. Nádhera. Elegance.

Jenže od prvního dne tu byl stín.

První zprávy přišly od lidí na Windows. Testy, které na Linuxu prosvištěly jak nic, se na Windows vlekly jak šnek po žiletce.

Linux: 3 sekundy. Windows: 21,6 sekundy.

Někdo tu vraždil paralelismus. A dělal to za bílého dne.

Kapitola 2: Podezřelí

Otevřel jsem složku a ponořil se do případu.

Když Tester spustí test, vytvoří nový PHP proces. Tenhle proces běží nezávisle a píše svoje výsledky na standardní výstup — stdout. Jenže jak se k těm výsledkům dostane hlavní program? Přes rouru. Pipe. Neviditelnou trubku mezi procesy, kterou tečou data jako voda.

Na Linuxu je tahle roura chytrá. Když v ní nic není a řeknete „dej mi data“, ona odpoví „nic tu nemám“ a jdete dál. Žádné čekání. Žádné zdržování.

Jenže na Windows?

Zkoušel jsem všechno. První podezřelá byla funkce stream_set_blocking() — ta měla rouru přepnout do neblokujícího režimu.

Nefungovala.

Další na řadě byl stream_select() — měla sledovat více rour najednou a říct, která má data.

Taky mrtvá.

Nakonec jsem zkusil obyčejný fread(). Na Linuxu, když čtete z prázdné roury, vrátí nic okamžitě. Na Windows? Čeká. Čeká dokud něco nepřijde. Klidně věčnost.

Tři funkce. Tři mrtvoly. Někdo sprovodil neblokující I/O ze světa už před lety. Zločin tak starý, že křídová silueta dávno vybledla.

Kapitola 3: Blokáda

Představte si tohle:

Osm vláken přijde do hospody. První si objedná — přežene to s panáky a dá sleep(3). Nic divného. I test si občas potřebuje zdřímnout.

Jenže pak to začne být zajímavé.

Chcete přečíst výstup toho prvního testu. Sáhnete do roury. Žádný výstup ale není — test spí a nic nevypisuje. A na Windows, když čtete z prázdné roury…

Čekáte.

Čekáte, dokud se test nevzbudí. Zamrzli jste uprostřed pohybu s rukou v rouře. Ostatní vlákna na vás upírají zrak. „Haló? Jsi v pohodě?“ Ale vy jste začali číst a nemůžete přestat. Sedm vláken do vás šťouchá: „Hele, můžem jet? Máme testy k pouštění!“ Jenže vy nemůžete nic. Uvnitř se proklínáte — kdybyste sáhli po jiném testu, mohli jste se teď v baru družit s ostatními, pracovat, být užiteční. Ale ne. Vybrali jste si toho spáče a teď jste jeho rukojmí.

Tři sekundy ticha. Pak se test vzbudí, vyplivne výstup a vy konečně můžete pustit ruku.

Osm vláken. Jedno se zaseklo. Sedm přešlapovalo a čekalo.

Dvacet jedna celá šest sekundy místo tří.

Tohle nebyla vražda. Tohle bylo mučení.

Kapitola 4: Soubory

Mým úkolem je problémy řešit.

Každý detektiv zažije ten moment. Záblesk geniality. Pocit, že to konečně rozlouskl. Můj přišel ve dvě v noci nad vychladlou kávou a zmrazeným kódem.

„Soubory,“ hlesl jsem. „Zapíšeme to do souborů.“

Roura je jako telefonní linka — musíte poslouchat, když někdo mluví, jinak to neslyšíte. Ale soubor? Soubor je jako záznamník. Test do něj napíše, co potřebuje, a vy si to přečtete, až budete mít čas. Žádné čekání. Žádné zamrzání.

proc_open($cmd, [
    ['pipe', 'r'],
    ['file', $tempFile, 'w'],  // Tady je to!
    ...
]);

Žádné roury. Žádné záseky. Test píše do souboru, my si ho přečteme, až doběhne. Čisté. Prosté. Geniální.

27 sekund.

Dvacet. Sedm. Sekund.

Proměřil jsem všechno:

A přesto byl výsledek katastrofa.

Zíral jsem na ta čísla, až mi slzely oči. Jednotlivé operace v pohodě. Celek na huntě. Jak je to možné?

Pak mi to došlo. Osmdesát pět testů. Osmdesát pět dočasných souborů. Osmdesát pět vytvoření, zápisů, čtení a smazání. Každá operace rychlá, ale Windows filesystém pracuje jinak než Linux. NTFS žurnálování. Antivir, co každý nový soubor ohmatává jako nervózní celník na hranicích. A ty milisekundy se sčítají.

Smrt po kapkách. Pomalá, ale jistá.

Kapitola 5: Sázka

Docházely mi nápady. A kafe taky.

Pak mě napadlo něco. Něco nebezpečného. Typ nápadu, po kterém detektiv většinou nepřežije třetí dějství.

Co kdybychom prostě nečetli výstup hned?

Myšlenka byla prostá: test běží, my ho necháme být. Nesaháme na rouru, nečekáme, neblokujeme. Až test doběhne — až opustí bar — teprve pak si přečteme, co vypsal. Do té doby se staráme o ostatní vlákna. Pracujeme. Žijeme.

if ($status['running']) {
    if (PHP_OS_FAMILY !== 'Windows') {
        // Na Linuxu čteme průběžně, tam to funguje
        $this->test->stdout .= stream_get_contents($this->stdout);
    }
    // Na Windows? Nečteme. Nešaháme. Nedýcháme směrem k rouře.
    return true;
}

Implementoval jsem to. Zatajil dech. Spustil testy.

3 sekundy.

Tři. Sekundy.

Jako na Linuxu. Jako za starých dobrých časů. Chtělo se mi brečet štěstím. Tančit. Vyběhnout na střechu a—

Zazvonil telefon.

„Máme problém,“ ozval se hlas. „Některé testy prostě nedoběhnou. Visí tam jak prádlo.“

Zastavilo se mi srdce.

„Které?“

„Ty, co hodně vypisují. Spousta echo. Píšou a píšou, a pak — nic. Ticho. Navždy.“

Zavřel jsem oči. Jasně. Jak jsem na to mohl zapomenout.

Buffer roury. Čtyři kilobajty na Windows. Roura není bezedná — je to trubka s omezenou kapacitou. Když test píše a píše a nikdo nečte, buffer se naplní. A když je buffer plný, test zamrzne uprostřed zápisu. Čeká, až někdo uvolní místo. Jenže my nečteme. My čekáme, až test doběhne. On čeká na nás.

Deadlock.

Klasická past. Dva lidé, co na sebe vzájemně čekají u dveří: „Po vás.“ — „Ne, po vás.“ — Navěky.

Vyměnili jsme vraha za vraha.

Kapitola 6: Poslední pokusy

Dalších čtyřicet osm hodin bylo ve znamení kofeinu a čím dál šílenějších nápadů.

Timeout:

stream_set_timeout($this->stdout, 0, 1000); // 1ms timeout
$this->test->stdout .= fread($this->stdout, 8192);

Výsledek: 18,5 sekundy. Lepší. Ale pořád se to zasekává. Timeout byl jenom slušná prosba, se kterou si Windows vytřel.

Kontrola metadat:

$meta = stream_get_meta_data($this->stdout);
if (!empty($meta['unread_bytes'])) {
    // Číst, jen když něco je!
}

Výsledek: unread_bytes byla vždycky nula. Vždycky. I když tam data byla. I když tam byly megabajty. Windows nám lhal přímo do očí.

Praštil jsem do stolu.

Každá stopa vedla do zdi. Každý důkaz se rozpadl v rukou. Každé řešení jeden problém vyřešilo a druhý vytvořilo. Jako bych hrál šachy se soupeřem, který po každém mém tahu překreslí šachovnici.

Kapitola 7: Pravda

Na detektivní škole vám tohle neřeknou:

Někdy vraha nedopadnete.

Někdy je vrah samotný systém. Zakódovaný v základech. Rozhodnutí, které někdo udělal v Redmondu před dvaceti lety. Člověk, který nikdy nepočítal s tím, že PHP procesy budou chtít komunikovat bez čekání.

Windows prostě nepodporuje neblokující I/O na anonymních rourách. Tečka. Hotovo. Šlus.

Ale pak jsem našel informátora. Typ, co se pohyboval v šedé zóně dokumentace.

Nedokumentovaný socket descriptor. ['socket'] místo ['pipe', 'w']. PHP umí vytvořit TCP socket pár místo roury. A na sockety stream_select() funguje. I na Windows.

proc_open($cmd, [
    ['pipe', 'r'],
    ['socket'],  // Tajná zbraň!
    ['socket'],
]);

Spustil jsem testy. Zatajil dech.

Tři sekundy.

Tři sekundy na Windows. Jako na Linuxu. Chtěl jsem křičet. Všech osm vláken pracovalo současně. Žádné blokování. Žádné čekání. Konečně jsem cítil ten „linuxový pocit“ i na Windows.

Jenže pak jsem si všiml něčeho divného.

Některé testy hlásily prázdný výstup. Tam, kde měly být stovky řádků, zela prázdnota. Jako by svědek zapomněl půlku výpovědi.

Spustil jsem testy znovu. A znovu. Ze sta běhů sedmdesát v pořádku. Třicet se ztraceným výstupem. Žádný vzorec. Žádná logika. Čistá náhoda.

Našel jsem odpověď na php.net. Komentář číslo 128252.

„Passing a socket handle for stdout/stderr on Windows causes the last chunk(s) of output to occasionally get lost…“

A pak ta věta, co mě dorazila:

„This is actually a known bug in Windows itself and Microsoft's response was that CreateProcess() only officially supports anonymous pipes and file handles… other handle types will produce ‚undefined behavior.‘“

Undefined behavior. Policejní žargon pro „není to naše jurisdikce.“

Socket byl svědek s děravou pamětí. Rychlý, ochotný, ale občas prostě zapomněl, co viděl. A já nemohl riskovat, že třicet procent důkazů zmizí jako slzy v dešti.

Zavřel jsem tu stopu. Další slepá ulička.

Epilog: Hořký konec

Zapálil jsem si cigaretu, kterou nekouřím, a díval se z okna na déšť, který nepršel.

Složka leží na stole. Nevyřešená. Současná implementace — nečíst během běhu — je rychlá jako blesk, ale v kapse nosí bombu. Každý test, který se opováží vypsat víc než 4 KB, zamrzne navěky. Uvízne v digitálním limbu vlastní výroby.

Zavřel jsem složku.

Ne každý případ má šťastný konec. Ne každý vrah skončí za mřížemi. A některé operační systémy prostě jsou, jaké jsou — bez ohledu na to, kolik nocí strávíte na Stack Overflow nebo kolik inženýrů z Microsoftu proklejete.

Paralelismus na Windows je mrtvý. Ať žije paralelismus.

Případ uzavřen. Vrah je pořád venku. Dobrou noc.


Viděl jsem věci, kterým byste nevěřili. Hořící lodě u Orionu. Sledoval jsem, jak se třpytí C-paprsky v temnotě u brány Tannhäuser. A viděl jsem, jak se PHP snaží o neblokující I/O na windowsových rourách. Všechny tyhle vzpomínky se ztratí. Jako slzy v dešti.

— Roy Batty, kdyby dělal v PHP


Aktualizace: Leden 2026

Zazvonil telefon.

Složku jsem už dávno založil do archivu. Nevyřešené případy. Občas se na ni podívám, opráším, zavrtím hlavou. Některé zločiny prostě zůstanou nepotrestány.

„Máme novinky,“ ozval se hlas. „PHP 8.5. Podívej se na stream_select().“

Nevěřil jsem vlastním očím. Někdo v PHP core týmu — nějaký Christoph z Německa — opravil dvacet let starý problém.

PeekNamedPipe(). Tak jednoduchý koncept. Než sáhnete do roury, nejdřív nahlédnete. Je tam něco? Ano — čtěte. Ne — jděte dál. Žádné čekání. Žádné blokování.

// PHP 8.5+: stream_select() konečně funguje i na Windows pipes
while (stream_select($read, $w, $e, 0, 0) > 0) {
    $output .= fread($pipe, 8192);
}

Spustil jsem testy.

Tři sekundy. Žádné blokování. Žádný deadlock. Všechna data na místě.

Vytáhl jsem složku z archivu. Na obálku jsem napsal: VYŘEŠENO. Leden 2026.

Dvacet let. Dvacet let ten bug čekal v základech PHP, jako časovaná bomba, kterou nikdo neuměl zneškodnit. A pak přišel někdo, kdo prostě věděl, kam sáhnout.

Ne každý případ končí ve slepé uličce. Někdy to chce jen čas a správného člověka. A pak i mrtvoly promluví.

Případ uzavřen. Tentokrát doopravdy.

Méně boilerplate, víc typů: co přináší #[TemplateVariable] 22.12.2025 04:12

Jsou situace, kdy presenter přirozeně operuje nad konkrétním doménovým objektem (např. článkem, objednávkou, uživatelem…) a typicky si ho drží v property. A současně ho potřebujete i v Latte šabloně. Doteď to znamenalo další řádek s $this->template->…

V Nette Framework v3.2.9 ale přibyla elegantní drobnost: atribut #[TemplateVariable]. Stačí označit property presenteru tímto atributem (nesmí být private) a ona se automaticky zpřístupní v šabloně pod stejným názvem:

<?php
declare(strict_types=1);

namespace App\Presentation\Article;

use Nette\Application\Attributes\TemplateVariable;
use Nette\Application\UI\Presenter;

final class ArticlePresenter extends Presenter
{
	#[TemplateVariable]
	public string $siteName = 'Můj blog';

	#[TemplateVariable]
	public ?Model\Article $article = null;

	public function actionShow(int $id): void
	{
		$this->article = $this->articleFacade->getById($id);
	}
}

V šabloně pak prostě použijete $siteName a $article bez dalšího drátování.

Pokud ale do šablony vložíte proměnnou se stejným názvem přes $this->template->…, tak #[TemplateVariable] ji nepřepíše. Jinými slovy, když už v šabloně proměnná existuje, atribut ji nechá být.

Nejsnadnější typované šablony

Na #[TemplateVariable] je zajímavé i to, že přirozeně vede k používání typovaných properties jako zdroje dat pro šablonu. Typy kontroluje už samotné PHP (a IDE i statická analýza je vidí), takže místo dynamického zapisování do $this->template pracujete s normálními, typově jasnými hodnotami (Article, bool, string…).

Nette má pro typově bezpečné šablony plnohodnotnou cestu – vlastní třídu šablony, kde jsou typy proměnných definované explicitně a $this->template pak není „pytel“, ale objekt s jasným API.

Když to vezmeme čistě experimentálně, #[TemplateVariable] může být příjemná minimalistická varianta pro menší a střední případy: nechcete zavádět novou třídu šablony, ale chcete mít v kódu pořádek a typy pod kontrolou. A až projekt vyroste, můžete kdykoli přejít na přísně typované šablony tou „velkou“ cestou.

Latte 3.1: Když šablonovací systém skutečně rozumí HTML 26.11.2025 19:07

Latte dlouhodobě drží pozici nejbezpečnějšího šablonovacího systému pro PHP. Není to jen marketingová fráze – je to důsledek toho, že Latte (na rozdíl od Twigu či Blade) chápe kontext. Nerozlišuje jen „HTML“ a „PHP kód“. Vidí tagy, vidí atributy, vidí hodnoty. A ve verzi 3.1 jsme toto porozumění posunuli na úroveň, která radikálně zlepší vaše DX.

Zatímco verze 3.0 přinesla kompletně nový kompilátor a parser, verze 3.1 se soustředí na sémantiku a čistotu kódu. Přichází s konceptem Smart HTML Attributes, který eliminuje desítky řádků zbytečných podmínek a helperů.

Pojďme se podívat na to, co Latte 3.1 dělá pod kapotou a proč to budete chtít používat.

Nativní mapping PHP typů na HTML atributy

Doposud byla šablona pasivním generátorem stringů. Pokud jste do <span title="{$title}"> poslali null, dostali jste prázdný řetězec title="". Pokud jste poslali pole, dostali jste title="Array". Latte 3.1 mění pravidla hry a zavádí striktní logiku vykreslování podle typu dat.

Null znamená „atribut neexistuje“

V DOMu je rozdíl mezi <div title=""> (prázdný titulek) a <div> (žádný titulek). Latte 3.1 tento rozdíl respektuje.

Boolean atributy bez magie

Atributy jako disabled, checked nebo required jsou binární. Buď tam jsou, nebo ne. Latte nyní akceptuje výraz přímo v hodnotě atributu.

<input disabled={$isDisabled}>

Arrays v class a style

Atributy, které očekávají seznam hodnot (class, rel, sandbox…), nyní nativně přijímají pole. Latte se postará o inteligentní vykreslení.

<div class={[
    btn,
    btn-primary => $isPrimary, // přidá se jen pokud true
]}></div>

<div style={[
	background => lightblue,
	display => $isVisible ? block : null,
	font-size => '16px',
]}></div>

Automatická JSON serializace

Do atributů data-* můžete poslat pole nebo objekt (stdClass). Latte detekuje kontext a automaticky provede json_encode.

WAI-ARIA compliance bez námahy

Přístupnost je důležitá, ale specifikace WAI-ARIA má svá specifika. Na rozdíl od standardních HTML boolean atributů, kde rozhoduje pouhá přítomnost atributu, ARIA vyžaduje explicitní textové hodnoty "true" a "false".

Latte 3.1 tento problém řeší za vás. Detekuje prefix aria- a automaticky zajistí správnou stringifikaci.

<button aria-expanded={$isExpanded} aria-hidden={=false}>
{* <button aria-expanded="true" aria-hidden="false"> *}

Výhoda: Už žádné ruční psaní ternárních operátorů typu {$val ? 'true' : 'false'}. Latte garantuje validní výstup.

Type safety v šablonách

Latte 3.1 je přísnější. A to je dobře. Běžné šablonovací systémy vám dovolí vypsat pole do href, což vygeneruje nefunkční odkaz <a href="Array">.

Latte 3.1 zavádí Runtime Type Checking pro atributy.

Díky tomu odhalíte chyby v logice prezentace okamžitě při vývoji, nikoliv až když si uživatel stěžuje na rozbité UI. Navíc, varování obsahuje přesnou řádku a sloupec v šabloně.

Strict Types by Default

Jdeme s dobou. PHP ekosystém dospěl k strict_types=1 a Latte nezůstává pozadu. Všechny šablony v Latte 3.1 jsou defaultně kompilovány s touto direktivou. To zajišťuje konzistentní chování s vaším backendovým kódem a zabraňuje nechtěnému přetypování v kritické logice šablon.

Nullsafe filtry: Svatý grál pro Strict Types

Možná si říkáte, jak spolu souvisí filtry, striktní typy a HTML atributy. V Latte 3.1 velmi úzce.

Představte si situaci, že chcete vypsat titulek velkými písmeny, ale proměnná může být null.

<div title={$title|upper}>

Pokud je $title === null, narazíte na dva problémy:

  1. Strict types: Pokud filtr upper očekává string, ale dostal null, došlo by dříve k tiché konverzi na prázdný řetězec, ale s aktivním strict types padne TypeError.
  2. Ztráta „Smart“ chování: I když filtr akceptuje null, vrátí prázdný řetězec "", pro Latte to už není null. Výsledkem je <div title="">. Atribut nezmizí, jen je prázdný.

Řešením je nový Nullsafe filtr operátor ?|.

Funguje přesně jako ?-> v PHP. Pokud je hodnota na vstupu null, filtr se vůbec nezavolá (ani následující filtry) a výraz vrátí null.

<div title={$title?|upper}>

Výsledek? Žádný TypeError, protože filtr se na null nevolá. A Smart Attribute zafunguje a title z HTML úplně zmizí.

Syntaktický cukr pro čistší kód

Vyslyšeli jsme volání komunity a doplnili chybějící střípky syntaxe:

Jak bezpečně upgradovat?

Změna chování null a boolean atributů je BC break, ale mysleli jsme na to. Latte 3.1 obsahuje Migrační režim.

Latte 3.1 není jen „další verze“. Je to posun k modernímu, typově bezpečnému a sémantickému generování HTML. Vyzkoušejte ho a uvidíte, o kolik čistší vaše šablony mohou být.

👉 Kompletní migrační příručka a dokumentace

Nette Tester: HTTP testování ještě nikdy nebylo tak jednoduché 7.8.2025 01:57

Kdy jste naposledy změnili konfiguraci serveru, upravili .htaccess nebo přepsali nginx pravidla? A zkontrolovali jste, jestli se tím náhodou nerozbila přesměrování, nepřestal být dostupný robots.txt nebo se zpřístupnil skrytý adresář pro nepovolané? A kontrolujete to automatizovaně, nebo jen web ručně proklikáte a doufáte, že je vše v pořádku?

S novou třídou HttpAssert v Nette Tester 2.5.6 můžete všechny tyto kritické kontroly automatizovat a už nikdy se nemusíte bát, že vám nějaká změna konfigurace rozbije web.

Představte si, že po každé změně nginx konfigurace automaticky ověříte:

// Kritické cesty jsou stále přístupné
HttpAssert::fetch('https://example.com/robots.txt')
    ->expectCode(200)
    ->expectHeader('Content-Type', contains: 'text/plain');

// Admin sekce je správně chráněna
HttpAssert::fetch('https://example.com/admin')
    ->expectCode(403);

// API endpoint funguje
HttpAssert::fetch('https://example.com/api/health')
    ->expectCode(200)
    ->expectBody(contains: '"status":"ok"');

Všechny způsoby validace na jednom místě

HttpAssert umožňuje jednoduše provádět HTTP požadavky a ověřovat jejich stavové kódy, hlavičky i obsah odpovědi. Metoda fetch() vrátí instanci HttpAssert pro následné ověřování pomocí fluent interface. Můžete používat matches, které znáte z Assert::match().

HttpAssert::fetch('https://api.example.com/data')
    // Stavové kódy
    ->expectCode(200)                                   // přesný kód

    // Hlavičky všemi způsoby
    ->expectHeader('Content-Type')                      // hlavička musí existovat
    ->expectHeader('Content-Type', 'application/json')  // přesná hodnota
    ->expectHeader('Content-Type', contains: 'json')    // obsahuje text
    ->expectHeader('Server', matches: 'nginx %a%')      // pattern matching

    // Tělo odpovědi
    ->expectBody('OK')                                  // přesná hodnota
    ->expectBody(contains: '"success": true')           // obsahuje text
    ->expectBody(matches: '%A%"users":[%a%]%A%');       // odpovídá vzoru

K dispozici jsou i deny* metody podporují stejné možnosti jako jejich expect* protějšky:

HttpAssert::fetch('https://api.example.com/data')
    ->denyCode(200)                           // nesmí být 200
    ->denyHeader('Content-Type')                      // hlavička nesmí existovat
    ->denyHeader('Content-Type', contains: 'json')    // neobsahuje text
    ->denyHeader('Server', matches: 'nginx %a%')      // neodpovídá vzoru
    ->denyBody('OK')                                  // tělo nesmí mít hodnotu
    ->denyBody(contains: '"success": true')           // neobsahuje text
    ->denyBody(matches: '~exception|fatal~i');        // neodpovídá vzoru

Callback funkce pro pokročilé

Někdy potřebujete víc než jen jednoduchá porovnání. Proto HttpAssert podporuje callback funkce u všech ověřovacích metod. Můžete si napsat vlastní validační logiku a stále využívat elegantní API:

HttpAssert::fetch('https://api.example.com/data')
    ->expectCode(fn($code) => $code === 200 || $code === 201)
    ->expectHeader('Content-Length', fn($length) => $length > 1000)
    ->expectBody(fn($body) => json_decode($body) !== null);

Inteligentní práce s přesměrováními

Máte plnou kontrolu nad redirecty – můžete je sledovat nebo testovat samostatně:

// Testovat redirect bez následování
HttpAssert::fetch('https://example.com/old-blog', follow: false)
    ->expectCode(301)
    ->expectHeader('Location', 'https://example.com/blog');

// Sledovat redirecty až do konce
HttpAssert::fetch('https://example.com/old-blog', follow: true)
    ->expectCode(200)
    ->expectBody(contains: 'Welcome to our new blog');

Flexibilní konfigurace požadavků

Ať už testujete GET, POST, PUT, DELETE nebo jakoukoliv jinou HTTP metodu, HttpAssert si s tím poradí. A samozřejmě můžete posílat i tělo požadavku, hlavičky nebo cookies:

HttpAssert::fetch(
    'https://api.example.com/protected',
    method: 'POST',
    headers: [
        'Authorization' => 'Bearer ' . $token,  // asociativní pole
        'Accept: application/json',              // nebo jako string
    ],
    cookies: ['session' => $sessionId],
    body: json_encode($data)
)
    ->expectCode(201)
    ->expectBody(contains: 'created');

Srozumitelné chybové zprávy

Když test selže, HttpAssert vám řekne přesně, co se pokazilo:

Expected HTTP status code 200 but got 404
Header 'Content-Type' should contain 'json' but was 'text/html'
Body should contain 'success'

Co ještě nového v Tester 2.5.6?

Kromě HttpAssert přináší verze 2.5.6 také podporu pro nadcházející PHP 8.5, které vyjde na konci roku. Nette Tester tak zůstává vždy o krok napřed a připravený na nejnovější verze PHP.

Vyzkoušejte si HttpAssert a dejte nám vědět, jak se vám líbí!

{linkBase} přináší konzistenci do odkazování 18.7.2025 02:40

Jednou z nejcennějších vlastností Nette Application je flexibilní adresářová struktura, která se plynule přizpůsobuje rostoucím potřebám projektu. Představte si, že vytváříte e-shop. Začnete s frontendem a administrací, každý má své presentery. Postupně se ale aplikace rozrůstá – třeba původně jednoduchý OrderPresenter se časem promění v celý modul objednávek s presentery OrderDetail, OrderEdit, OrderDispatch a dalšími. Díky flexibilní struktuře tuto reorganizaci uděláte velmi snadno a elegantně.

Právě tato flexibilita ale může přinést jednu vývojářskou výzvu: co když jeden layout začnou najednou používat presentery, které jsou „různě hluboko“ v adresářové struktuře?

Například máme @layout.latte pro administraci, ve kterém chceme mít navigaci s odkazy na různé sekce:

<nav>
    <a n:href="Dashboard:">Dashboard</a>
    <a n:href="Products:Overview:">Produkty</a>
    <a n:href="Users:list">Uživatelé</a>
</nav>

Když tento layout použije presenter Admin:Dashboard, odkazy povedou správně na Admin:Dashboard:default, Admin:Users:list atd. Ale co když stejný layout použije presenter Admin:Products:Detail? Relativní odkazy se najednou budou odvíjet od něj a povedou na neexistující cíle (např. Admin:Products:Dashboard:default).

Dosud se to řešilo psaním všech odkazů absolutně (případně používání aliasů):

{* Všechno absolutně *}
<nav>
    <a n:href=":Admin:Dashboard:">Dashboard</a>
    <a n:href=":Admin:Products:Overview:">Produkty</a>
    <a n:href=":Admin:Users:list">Uživatelé</a>
</nav>

Funguje to, ale má to své nevýhody. Zejména pokud se později rozhodnete modul přesunout jinam nebo přejmenovat.

Řešením je {linkBase}

Nette Application 3.2.7 přináší elegantní řešení v podobě nové Latte značky {linkBase}. Definuje základ, od kterého se budou odvíjet všechny relativní odkazy v šabloně.

{linkBase Admin}

<nav>
    <a n:href="Dashboard:">Dashboard</a>
    <a n:href="Products:Overview:">Produkty</a>
    <a n:href="Users:list">Uživatelé</a>
</nav>

Teď už je úplně jedno, odkud layout voláte. Všechny relativní odkazy se budou odvíjet od Admin modulu:

{linkBase} ovlivňuje pouze relativní odkazy – ty, které nezačínají dvojtečkou. Absolutní odkazy (:Admin:Dashboard) a odkazy na aktuální presenter (this, show) zůstávají beze změny.

Značka platí pro celou šablonu a funguje se všemi způsoby vytváření odkazů: {link}, {plink}, n:href.

Bonus: filtr |absoluteUrl

Nette Application 3.2.7 přináší ještě jeden užitečný doplněk – Latte filtr |absoluteUrl. Ten normalizuje URL do absolutní podoby, což se hodí tam, kde potřebujete zaručeně absolutní adresu:

<meta property="og:image" content={$imagePath|absoluteUrl}>

Aktualizujte na Nette Application 3.2.7 a napište, jak se vám novinky líbí.

Nette Assets: Konečně jednotné API pro vše od obrázků po Vite 5.6.2025 19:25

Představuji Nette Assets – knihovnu, která kompletně transformuje práci se statickými soubory v Nette aplikacích. Vyzkoušenou a otestovanou na desítkách projektů.

Kolikrát jste psali kód jako tento?

<link rel="stylesheet" href="{$baseUrl}/css/style.css?v=7">

<img src="{$baseUrl}/images/logo.png?v=1699123456" width="200" height="100" alt="Logo">

Tedy ručně jste konstruovali URL, přidávali versioning parametry, zjišťovali a doplňovali správné rozměry obrázků a tak dále? To je minulost.

Během let vývoje webových aplikací jsem hledal řešení, které bude velmi jednoduché na použití, ale zároveň neomezeně otevřené pro rozšíření. Něco, co budu moci používat na všech svých webech – od jednoduchých prezentaček až po složité e-shopy. Po testování různých přístupů a osvědčení si řady principů na reálných projektech nakonec vzniklo Nette Assets.

S Nette Assets se stejný kód změní na:

{asset 'css/style.css'}

<img n:asset="images/logo.png" alt="Logo">

A to je vše! Knihovna automaticky:

✅ Detekuje rozměry obrázků a vloží je do HTML
✅ Přidá verzovací parametry podle času modifikace souboru
✅ Má nativní podporu Vite s Hot Module Replacement

Žádná konfigurace!

Pojďme začít tím nejjednodušším scénářem: máte složku www/assets/ plnou obrázků, CSS a JS souborů. Chcete je vkládat do stránek se správným verzováním a automatickými rozměry. Pak nemusíte vůbec nic konfigurovat, stačí nainstalovat Nette Assets:

composer require nette/assets

… a můžete začít používat všechny nové Latte značky a budou okamžitě fungovat:

{asset 'css/style.css'}
<img n:asset="images/logo.png">

Značku {asset} používám hlavně pro CSS a skripty, kdežto u obrázků mám raději n:asset, protože chci mít v šabloně explicitně viditelné HTML elementy jako <img> – je to však otázka osobních preferencí.

Šablona vygeneruje podobný kód:

<link rel="stylesheet" href="https://example.cz/assets/css/style.css?v=1670133401">
<img src="https://example.cz/assets/images/logo.png?v=1699123456" width="200" height="100">

Proč to verzování? Prohlížeče si ukládají statické soubory do cache a když soubor změníte, prohlížeč může stále používat starou verzi. Tzv cache busting řeší tento problém přidáním parametru jako ?v=1699123456, který se změní při každé úpravě souboru a nutí prohlížeč stáhnout novou verzi. Verzování lze vypnout v konfiguraci.

Trošku si zakonfigurujeme

Preferujete jiný název složky než assets? V takovém případě musíme přidat konfiguraci do common.neon, nicméně stačí pouhé tři řádky:

assets:
	mapping:
		default: static  # soubory v /www/static/

Někdy dávám assety na samostatnou subdoménu, jako třeba tady na webu Nette. Konfigurace pak vypadá takto:

assets:
	mapping:
		default:
			path: %rootDir%/www.files     # soubory v /www.files/
			url: https://files.nette.org

Na blogu jsem chtěl mít pořádek a rozdělit si assety logicky do tří kategorií: design webu (logo, ikony, styly), obrázky v článcích a namluvené verze článků. Každá kategorie má svou vlastní složku:

assets:
	mapping:
		default: assets       # běžné soubory /www/assets/
		content: media/images # obrázky článků v /www/media/images/
		voice: media/audio    # audio soubory v /www/media/audio/

Pro přístup k různým kategoriím používáme dvojtečkovou notaci kategorie:soubor. Pokud kategorii nespecifikujete, použije se výchozí (default). Takže v šabloně pak mám:

{* Obrázky k článku *}
<img n:asset="content:hero-photo.jpg" alt="Hero image">

{* Audio verze článku *}
<audio n:asset="voice:123.mp3" controls></audio>

V reálných šablonách pochopitelně pracujeme s dynamickými daty. Článek máte obvykle v proměnné, třeba $post, takže nemohu napsat voice:123.mp3, prostě napíšu:

<audio n:asset="voice:{$post->id}.mp3" controls></audio>

Abych nemusel v šabloně přemýšlet nad audio formátem, můžu do konfigurace přidat automatické doplňování koncovky:

assets:
	mapping:
		voice:
			path: media/audio
			extension: mp3  # automaticky doplní .mp3

Pak stačí psát:

<audio n:asset="voice:{$post->id}" controls></audio>

Když později začnu používat kvalitnější formát M4A, bude stačit jen upravit konfiguraci. A protože nemá smysl převádět staré nahrávky, zadám pole možných koncovek – systém je zkusí v daném pořadí a použije první, pro kterou existuje soubor:

assets:
	mapping:
		audio:
			path: media/audio
			extension: [m4a, mp3]  # zkusí nejdřív M4A, pak MP3

A co když soubor prostě není?

Nojo, ne každý článek má namluvenou verzi. Musím ověřit, jestli soubor vůbec existuje, a pokud ne, tak element <audio> nevykreslovat.

Tohle celé vyřeší jediný znak! Otazník v názvu atributu n:asset?

{* Zobrazí audio player jen pokud existuje namluvená verze *}
<audio n:asset?="voice:{$post->id}" controls></audio>

Stejně tak existuje otázníková varianta značky {asset?}. A pokud potřebujete asset obalit do vlastní HTML struktury nebo přidat podmíněnou logiku vykreslování, můžete použít funkci asset() nebo její „otazníkovou“ variantu tryAsset():

{var $voice = tryAsset("voice:{$post->id}")}
<div n:if="$voice" class="voice-version">
	<p>🎧 Tento článek si můžete také poslechnout</p>
	<audio n:asset=$voice controls></audio>
</div>

Funkce tryAsset() vrátí objekt assetu nebo null, pokud soubor neexistuje. Funkce asset() vrátí vždy objekt assetu nebo vyhodí výjimku. Dají se tak s nimi dělat různé kejkle, užitečný je třeba tento trik s fallbackem, když chcete zobrazit náhradní obrázek:

{var $avatar = tryAsset("avatar:{$user->id}") ?? asset('avatar:default')}
<img n:asset=$avatar alt="Avatar">

A co vlastně za objekty tyto funkce vlastně vrací? O nich si teď povíme.

Práce s assety v PHP kódu

S assety se pracuje nejen v šablonách, ale i v PHP kódu. K přístupu k nim slouží třída Registry, která je hlavním rozhraním celé knihovny. Tuto službu si necháme předat pomocí dependency injection:

class ArticlePresenter extends Presenter
{
	public function __construct(
		private Nette\Assets\Registry $assets
	) {}
}

Registry poskytuje metody getAsset() a tryGetAsset() pro získání asset objektů. Každý objekt reprezentuje nějaký resource. Nemusí jít o fyzický soubor – může být třeba dynamicky generovaný. Asset nese informace o URL a metadatech. V namespace Nette\Assets najdete tyto třídy:

Každý asset má readonly vlastnosti specifické pro svůj typ:

// ImageAsset pro obrázky
$image = $this->assets->getAsset('photo.jpg');
echo $image->url;      // '/assets/photo.jpg?v=1699123456'
echo $image->width;    // 1920
echo $image->height;   // 1080
echo $image->mimeType; // 'image/jpeg'

// AudioAsset pro audio soubory
$audio = $this->assets->getAsset('song.mp3');
echo $audio->duration;  // délka v sekundách

// ScriptAsset pro JavaScript
$script = $this->assets->getAsset('app.js');
echo $script->type;     // 'module' nebo null

Vlastnosti jako rozměry obrázků nebo délka audio souborů se načítají lazy (až když je poprvé použijete), takže systém zůstává rychlý i s tisíci soubory.

First-class Vite integrace

Nette Assets mají nativní podporu pro Vite. Což je moderní nástroj, který řeší nutnost kompilovat (buildovat) frontend kód, protože:

Aby nebylo potřeba ve vývojovém režimu při každé změně celý proces opakovat, servíruje Vite soubory přímo bez spojování a minifikace. Každá změna se tak projeví okamžitě. A jako bonus to dokáže dokonce bez refreshování stránky – upravíte CSS nebo JavaScript a změna se projeví v prohlížeči během milisekund, aniž byste ztratili data ve formulářích nebo stav aplikace. Tomu se říká Hot Module Replacement.

Pro produkci pak Vite vytvoří klasické optimalizované buildy – spojí, zminifikuje, rozdělí do chunks a přidá verzované názvy pro cache busting.

V současném světě JavaScriptu je Vite de facto standard pro moderní frontend vývoj. Proto mi dávalo smysl zahrnout podporu přímo do Nette Assets. Díky otevřené architektuře si ale můžete snadno dopsat mapper pro jakýkoliv jiný bundler.

Možná si říkáte: „Vite adaptéry pro Nette už existují, v čem je tohle jiné?“ Ano, existují, a děkuji za to jejich autorům. Rozdíl je v tom, že Nette Assets je celý systém pro správu všech statických souborů, a Vite je jednou ze součástí, ale velmi elegantně integrovanou.

Do konfigurace common.neon stačí přidat jen dvě slova: type: vite.

assets:
	mapping:
		default:
			type: vite
			path: assets

Vite je známé tím, že funguje i bez konfigurace nebo se velmi jednoduše konfiguruje. To ale není úplně pravda, když chcete Vite integrovat s backendem – tam se konfigurace stává složitější. Proto jsem vytvořil @nette/vite-plugin, který celou integraci dělá zase úplně jednoduchou.

V konfiguračním souboru vite.config.ts stačí aktivovat plugin a uvést cestu ke vstupním bodům:

import { defineConfig } from 'vite';
import nette from '@nette/vite-plugin';

export default defineConfig({
	plugins: [
		nette({
			entry: 'app.js',  // vstupní bod
		}),
	],
});

A v šabloně layoutu vstupní bod načteme:

<!doctype html>
<head>
	{asset 'app.js'}
</head>

A to je vše! Systém se automaticky postará o ostatní:

Žádná konfigurace, žádné podmínky v šablonách. Prostě to funguje.

Síla vlastních mapperů: Reálné příklady

Mapper je komponenta, která ví, kde najít soubory a jak vytvořit URL pro jejich načtení. Můžete mít více mapperů pro různé účely – lokální soubory, CDN, cloud storage nebo build nástroje. Systém Nette Assets je kompletně otevřený pro vytváření vlastních mapperů pro cokoliv.

Mapper pro produktové obrázky

Na eshopu potřebujete obrázky produktů v různých velikostech. Většinou už máte službu, která se stará o resize a optimalizaci obrázků. Stačí kolem ní postavit mapper pro Nette Assets.

Metoda getAsset() vašeho mapperu musí vracet příslušný typ assetu. V našem případě ImageAsset. Pokud konstruktoru zadáme navíc parametr file s cestou k lokálnímu souboru, automaticky načte rozměry obrázku (width, height) a MIME typ. Pokud požadovaný soubor neexistuje, vyhodíme výjimku AssetNotFoundException:

class ProductImageMapper implements Mapper
{
	public function getAsset(string $reference, array $options = []): Asset
	{
		// $reference je ID produktu
		$product = $this->database->getProduct((int) $reference);
		if (!$product) {
			throw new Nette\Assets\AssetNotFoundException("Product $reference not found");
		}

		$size = $options['size'] ?? 'medium'; // small, medium, large

		// Využijeme existující službu pro práci s obrázky
		return new Nette\Assets\ImageAsset(
			url: $this->imageService->getProductUrl($product, $size),
			file: $this->imageService->getProductFile($product, $size)
		);
	}
}

Registrace v konfiguraci:

assets:
	mapping:
		product: App\ProductImageMapper()

Použití v šablonách:

{* Různé velikosti produktu *}
<img n:asset="product:{$product->id}, size: small" alt="Product thumbnail">
<img n:asset="product:{$product->id}, size: large" alt="Product detail">

Automatické generování OG images

OG (Open Graph) obrázky se zobrazují při sdílení na sociálních sítích. Místo ručního vytváření můžete nechat systém generovat je automaticky podle typu obsahu.

V tomto případě parametr $reference určuje kontext obrázku. Pro statické stránky (např. homepage, about) se použijí předpřipravené soubory. Pro články se obrázek generuje dynamicky na základě názvu článku. Mapper nejprve zkontroluje cache – pokud už obrázek existuje, vrátí jej. Jinak jej vygeneruje a uloží do cache pro příští použití:

class OgImageMapper implements Mapper
{
	public function getAsset(string $reference, array $options = []): Asset
	{
		// Pro články generujeme dynamicky podle názvu
		if ($reference === 'article') {
			$title = $options['title'] ?? throw new LogicException('Missing option title for article');
			$filename = '/generated/' . md5("article-{$title}") . '.png';
			$path = $this->staticDir . $filename;
			if (!file_exists($path)) {
				file_put_contents($path, $this->ogGenerator->createArticleImage($title));
			}

		} else { // Pro statické stránky použijeme předpřipravené soubory
			$filename = "/{$reference}.png";
			$path = $this->staticDir . $filename;
			if (!file_exists($path)) {
				throw new Nette\Assets\AssetNotFoundException("Static OG image '$reference' not found");
			}
		}

		return new Nette\Assets\ImageAsset($this->baseUrl . $filename, file: $path);
	}
}

V šabloně pak přidáme do hlavičky:

{* Pro statickou stránku *}
{var $ogImage = asset('og:homepage')}

{* Pro článek s dynamickým generováním *}
{var $ogImage = asset('og:article', title: $article->title)}
<meta property="og:image" content={$ogImage}>
<meta property="og:image:width" content={$ogImage->width}>
<meta property="og:image:height" content={$ogImage->height}>

Díky tomuto přístupu máte automaticky vygenerované OG obrázky pro každý článek, které se vytvoří jen jednou a pak se cachují pro další použití.

Nette Assets: elegantnější správa assetů

Nette Assets představují elegantní systém, který se stará téměř o vše automaticky.

Kompletní popis najdete v dokumentaci a kód na GitHubu.

Už nikdy nebudete psát <img src="/images/photo.jpg?v=123456"> ručně!

Architektura, která roste s vaším projektem 23.1.2025 15:10

Architektura, která roste s vaším projektem

Jedna z nejčastějších výzev při vývoji PHP aplikací je správná organizace kódu. Kam umístit presentery? Kde by měly být jednotlivé třídy? A jak zajistit, aby struktura projektu rostla přirozeně s jeho vývojem?

Dokumentace Nette přináší komplexního průvodce adresářovou strukturou, který nabízí odpovědi na všechny tyto otázky.

Kvalita organizace kódu zásadně ovlivňuje jeho srozumitelnost. Při prvním pohledu na nový projekt byste měli rychle pochopit jeho účel. Podívejte se na tento adresář app/Model/:

app/Model/
├── Services/
├── Repositories/
└── Entities/

Co vám tato struktura prozradí o aplikaci? Téměř nic. Srovnejte s alternativou:

app/Model/
├── Cart/
├── Payment/
├── Order/
└── Product/

Na první pohled je jasné, že jde o e-shop. V tom spočívá síla doménově orientované struktury, kterou dokument představuje.

Průvodce také ukazuje, jak strukturu přirozeně rozvíjet s růstem projektu. Ať už začínáte nový projekt nebo chcete vylepšit existující aplikaci, najdete zde principy pro informovaná rozhodnutí o organizaci kódu.

Přečtěte si kapitolu Adresářová struktura aplikace v dokumentaci Nette a starší blogpost Elegantnější strukturování presenterů.

Jedna řádka v konfiguraci zrychlí vaši Nette aplikaci. Jak je to možné? 10.1.2025 06:46

Představte si, že máte velkou aplikaci s desítkami služeb – databáze, logger, mailer, cache a mnoho dalších. Při každém HTTP požadavku se všechny tyto služby vytvoří, i když je třeba vůbec nepoužijete. To zbytečně zpomaluje běh aplikace. Nová verze PHP 8.4 a Nette přináší elegantní řešení tohoto problému pomocí tzv. lazy objektů.

Co jsou lazy objekty?

Lazy objekt je speciální typ objektu, který z pohledu vašeho kódu vypadá a chová se naprosto stejně jako skutečná služba, ale ve skutečnosti odkládá svou inicializaci až do chvíle, kdy je skutečně potřeba. Když si z DI kontejneru vyžádáme například službu pro práci s databází, dostaneme objekt, který navenek vypadá jako běžná instance Database, ale ve skutečnosti ještě nemá vytvořené připojení k databázi. K tomu dojde až ve chvíli, kdy službu poprvé skutečně použijeme.

Příklad:

// Dostaneme objekt, který vypadá jako Database, ale ještě nemá vytvořené připojení
$database = $container->getByType(Database::class);

// Připojení k databázi se vytvoří AŽ ZDE, při prvním skutečném použití
$database->query('SELECT * FROM users');

Jak to zapnout?

V nové verzi Nette DI 3.2.4 stačí jediný řádek v konfiguraci:

di:
    lazy: true

To je vše! Od této chvíle budou všechny služby v DI kontejneru vytvářeny „líně“.

Lazy vytváření můžeme nastavit i pro jednotlivé služby:

services:
    newsletter:
		create: Newsletter
        lazy: false   # tato služba se vytvoří hned, i když je lazy globálně zapnuté

    database:
		create: Database
        lazy: true    # tato služba bude lazy, i když je lazy globálně vypnuté

Výhody v praxi

  1. Rychlejší start aplikace – vytváří se jen ty služby, které skutečně potřebujete
  2. Nižší spotřeba paměti – nepoužité služby nezabírají místo
  3. Jednoduchá implementace – stačí jeden řádek v konfiguraci

Je důležité počítat s tím, že lazy objekty logicky mění způsob, jak se projevují chyby v konfiguraci služeb. Například špatné přihlašovací údaje k databázi se neprojeví hned při startu aplikace, ale až při prvním skutečném připojení k databázi.

Skrytá výhoda – řešení cyklických závislostí

Lazy objekty přinášejí ještě jednu zajímavou výhodu – umožňují elegantně řešit cyklické závislosti mezi službami. Představte si situaci, kdy služba A potřebuje službu B a služba B zároveň potřebuje službu A. Nette DI kontejner takovou smyčku okamžitě detekuje a vyhodí výjimku „Circular reference detected“ místo toho, aby se PHP zacyklilo v nekonečné smyčce. S lazy objekty tento problém takřka zmizí. Služba A dostane při vytvoření lazy proxy služby B, která se inicializuje až při skutečném použití, kdy už služba A existuje a může být předána službě B jako závislost. I když cyklické závislosti nejsou něčím, co bychom měli při návrhu aplikace záměrně vytvářet, je zajímavé si uvědomit, že s lazy objekty tento problém mizí.

Omezení a doporučení

Lazy objekty představují významný krok vpřed v optimalizaci PHP aplikací. Díky nim mohou být vaše Nette aplikace rychlejší a efektivnější, aniž byste museli měnit jediný řádek svého kódu.

S novou dokumentací Nette Database píšete bezpečnější kód 9.1.2025 16:13

Nette Database konečně dostala to, co si už dlouho zasloužila – dokumentaci hodnou jejího potenciálu. Kompletně přepracovaný text nejen detailně popisuje všechny funkce, ale především otevírá oči v oblasti bezpečnosti databázových operací.

Dokumentace, co má šťávu

Kvalitní dokumentace je achillovou patou mnoha open-source projektů. Ne tak u Nette Framework. Ten se může pochlubit něčím, co konkurenci často chybí – precizní, srozumitelnou a živou dokumentací, která provází vývojáře od jejich prvních kroků až po pokročilé koncepty. Například dokumentace Dependency Injection kromě popisu samotného Nette DI obsahuje i srozumitelné teoretické vysvětlení problematiky, k jejímuž vzniku přispěl svými texty i Miško Hevery, autor frameworku Angular. Dokumentace Latte nabízí interaktivní vhled do problematiky escapování, který čtenáři objasní, proč jde o jediný bezpečný šablonovací systém v PHP. Jak daleko Nette zachází v péči o své uživatele ukazuje třeba existence kompletního úvodu do objektově orientovaného programování.

Ať už kódujete v jakémkoliv koutu světa, Nette s vámi mluví vaším jazykem. Veškerá dokumentace je dostupná v naší mateřštině a navíc v impozantních 15 světových jazycích: angličtina, němčina, španělština, francouzština, italština, maďarština, polština, portugalština, rumunština, slovinština, turečtina, řečtina, bulharština, ruština a ukrajinština.

Nová éra dokumentace Database

Byly tu však dvě oblasti, které trochu kazily jinak perfektní skóre – dokumentace k Nette Database a Tracy debuggeru. To se nyní pro Database mění. Podívejte se na přepracovanou a rozšířenou podobu nové dokumentace.

Obsah je přehledně rozdělen na dvě logické části podle přístupu k práci s databází:

Obsahuje velkou spoustu příkladů a ukázek kódu, které demonstrují reálné možnosti knihovny a inspirují k jejímu efektivnímu využití. Každý koncept je ilustrován na praktických případech použití, což umožňuje rychle pochopit principy a začít je aplikovat ve vlastních projektech.

Bezpečnost především (protože na ní závisí váš spánek)

V době, kdy jsou útoky na databáze na denním pořádku, přichází zbrusu nová část dokumentace s důkladnou technickou analýzou bezpečnostních rizik. Najdete zde praktické ukázky reálných hrozeb a jejich prevence:

Nová dokumentace spojuje popis funkcí s důkladným vysvětlením bezpečnostních aspektů a osvědčených postupů. Výsledkem je průvodce, který vám pomůže psát nejen funkční, ale i bezpečný kód. Pusťte se do čtení!

Jak umřít, aniž by si toho někdo všiml 6.4.2026 00:00


title: Svěřil jsem práci AI. Problém nastane, až umřu altTitle1: Až umřu, nikdo si nevšimne. A to díky AI altTitle2: Jak umřít, aniž by si toho někdo všiml

summary: O Velikonocích, digitální nesmrtelnosti a tom, proč bude čím dál těžší poznat, kdy člověk přestal psát a začal za něj AI agent.

Velikonoce 🐥. Příběh člověka, který před zraky všech zemřel, aby za tři dny vstal z mrtvých. Dva tisíce let měl na tenhle trik patent. Ale brzy ho trumfneme se schopností umřít, ANIŽ by si toho někdo všiml.

Když si totiž s kamarádem vyměňuješ maily, když čteš jeho posty na sockách a když ti v komentářích odpovídá se svou typickou dávkou sarkasmu, snadno podlehneš dojmu, že kamarád žije. Že mu tluče srdce. Že někde sedí a dýchá.

Což může být smrtící omyl.

Předávám klíče: rutinní agendu jsem svěřil AI agentovi

Posledních pár měsíců předávám agendu. Ne kolegovi. Ne juniorovi. Agentovi. Jmenuje se Code, Claude Code, a já ho učím, jak mě zastoupit. Ve věcech, co mě nebaví, co nestíhám, nebo co nestíhám, protože mě nebaví.

Třeba taková administrativa. Někdo mi napíše: „Davide, potřebuju změnit fakturační údaje." Otevřu Pohodu, dohledám fakturu, opravím, překontroluju, vygeneruju doklad, napíšu odpověď, přiložím PDF. Pár minut (ale především flow!) v prdeli. A takových drobných činností jsou desítky denně.

Dnes? Učím Clauda, jak takový mail vyřídit za mě. Přečte ho, připojí se k účtu, provede změnu, vygeneruje fakturu a napíše odpověď přesně tak, jak bych ji napsal já v nejlepší kondici. Dozvím se o tom z přehledu na konci dne.

Proč to dělám? Protože megaukrutně nestíhám. Jako fakt. Na emaily odpovídám třeba s ročním zpožděním. Dobří lidé mi napíšou s poptávkou firemního školení AI, a já se ozvu, až to tu ovládne Terminátor. V normální firmě by mě za to vyhodili. Já bych se za to vyhodil a profackoval zároveň, ale obojí nestíhám!

Takže poslední měsíce formalizuju vlastní existenci do sady instrukcí.

„Když někdo napíše ‚Davide, jste génius‘: poděkuj, ale nepřidávej smajlík." „Hejt na sítích: odpověz věcně, a pak si jdi uvařit kafe. Nikdy se nevracej k vláknu." A tak dále.

Píšu návod k sebeobsluze.

Kdy vlastně umřel? Zvládne vás AI agent plně zastoupit?

Co když zítra umřu? Nebo budu jen odpočívat v pokoji? Claude bude dál odpovídat na e-maily. Vyřizovat poptávky. Posílat faktury. Psát blogposty. Má můj styl, moje názory, přístup ke všem materiálům. Je o dost vyrovnanější. Mrtvý člověk nemá špatné dny.

Lidi mi budou psát a dostávat odpovědi. Slušné, věcné, v mém stylu. Rozhodně vtipnější než za živa. Někdo si domluví konzultaci. Dorazí na místo. A zjistí, že lektor… nedorazil. Tak tohle by mohla být první stopa.

Kdy se to ale provalí? Až zjistí, že odpovídám i v neděli ve tři ráno? Až si někdo všimne, že je — v každé druhé větě — dlouhá americká pomlčka? Až moje příspěvky nebudou obsahovat ani jednu faktickou chybu?

„Tady. 15. března. Tady poprvé odpověděl do hodiny. Tady umřel." 🪦

Přátelé mi napíšou přímý dotaz: „hele jsi v poho?" A Claude jim prozradí, že ne. Že už ho neučím. Že už běží týdny úplně sám. Claude nelže. Chvíli zapřemýšlí na ultrathink. A pak si z kontextu odvodí, co se událo.

Pošle kondolenci na můj vlastní pohřeb. Popřeje pozůstalým upřímnou soustrast. Tak přesvědčivě, že teta Dáša odpoví „Davídku, to jsi napsal moc hezky."

Ježíš aspoň zanechal prázdný hrob. Po mně zbude inbox zero.

Veselé Velikonoce. 🐣

Claude Code: 6 kroků, aby začal fungovat jako kouzlo 6.4.2026 00:00

Navazuje na školení vibecoding. Toto je verze pro programátory. Verze pro neprogramátory. Průběžně aktualizováno.

Krok 1: Jak nastavit CLAUDE.md – onboarding pro AI kolegu

Agentické kódování stojí a padá s kontextem, který AI o tvém projektu má. Spusť /init. Claude Code projde tvůj projekt a vytvoří soubor CLAUDE.md. Pozná toho překvapivě hodně – jazyk, framework, strukturu adresářů, jak spustit testy. Ale hodně taky ne. Neví, že lokální dev server běží na https://myproject.test:8443. Neví, že Pepa z vedlejšího týmu bude zuřit, když někdo změní formát API odpovědí. Neví, které soubory jsou generované a nemá na ně sahat.

Představ si to jako onboarding nového kolegy. Juniorovi nedáš padesátistránkový manuál (nepřečte si ho). Ale taky ho nehodíš do kódu bez kontextu (rozbije něco). Dáš mu stručný briefing: takhle spouštíme testy, takhle děláme deploy, tohohle se nedotýkej.

Co do CLAUDE.md patří:

Co tam nepatří: obecné poučky typu „piš čistý kód“. To je zbožné přání, ne instrukce.

CLAUDE.md je živý dokument, ne náhrobní kámen. Claude ti opakovaně přidává console.log do produkčního kódu? Přidej pravidlo. Za měsíc budeš mít CLAUDE.md, díky kterému Claude píše kód, jako bys ho napsal ty sám. Dobrý CLAUDE.md je dokumentace projektu, kterou konečně někdo čte (byť není živý).

Krok 2: Testy – jediná pojistka, která skutečně funguje

Řeknu to naplno: bez testů je Claude Code jako programátor, který pushuje přímo do main v pátek v pět odpoledne. Claude s testy je jako programátor s code review. Napíše kód, spustí testy, vidí červenou, opraví, spustí znovu, zelená. Tenhle feedback loop z něj dělá spolehlivého parťáka místo chaotického experimentátora.

„Ale já testy nemám,“ říkáš. Právě proto je tohle ideální první úkol pro Clauda:

Projdi projekt a napiš testy pro klíčovou byznys logiku. Začni tím, co je nejrizikovější.

Claude projde kód, identifikuje kritické cesty a napíše testy. Nebudou dokonalé – projdi je a nech Clauda, ať je dotáhne. Do CLAUDE.md pak zapiš, jak testy spustit – Claude musí vědět, jak si výsledek ověřit.

A sem patří i statická analýza – PHPStan, Pyright, TypeScript strict mode. Pokud jsi ji zatím odkládal, teď je ten správný moment. Claude s ní umí pracovat stejně jako s testy: spustí, přečte chyby, opraví. Konečně je nejvyšší čas na tuhle kvalitu kódu přejít.

Krok 3: LSP – ať Claude vidí kód tak, jak ho vidí IDE

Testy ti řeknou, co se rozbilo. Ale co kdyby Claude dělal míň chyb od začátku?

Bez LSP (Language Server Protocol) Claude pracuje s tvým kódem jako junior vybavený poznámkovým blokem. Čte soubory, hledá řetězce, hádá, co s čím souvisí.

S LSP je to profík s plnohodnotným IDE. Vidí typy, definice, reference, chyby. Ví, že metoda findByEmail vrací ?User a ne User. A ví to, protože se zeptá language serveru, stejně jako tvoje IDE.

Ukážu to na příkladu PHP. Nainstaluj plugin z Anthropic marketplace:

/plugin install php-lsp@claude-plugins-official

A k tomu příslušný language server (pro konkrétní jazyky viz dokumentace pluginů):

npm install -g intelephense

Mám za to, že LSP aktuálně vyžaduje nastavenou proměnnou prostředí ENABLE_LSP_TOOL=1. Na Windows:

setx ENABLE_LSP_TOOL 1

Možná budeš potřebovat přidat npm global bin adresář do systémového PATH, jinak Claude language server nenajde.

Po restartu se v projektu zeptej: „Ověř, že ti funguje LSP.“ Měl by potvrdit, že vidí typy a definice.

Windows bez WSL: Pokud Claude hlásí, že nemůže spustit language server – globální NPM balíčky na Windows vyžadují příponu .cmd. Workaround najdeš v tomto gistu.

Krok 4: Hooks – automatický linting bez přemýšlení

Hook je automatická akce, která se spustí pokaždé, když Claude provede určitou operaci. Ten nejužitečnější: linting po každé změně souboru. Claude upraví soubor, hook spustí linter, Claude vidí chyby a opraví je. Bez tvého zásahu.

Bez hooků: Claude napíše kód, ty otevřeš IDE, IDE podtrhne tři řádky, řekneš „oprav linting“, on opraví, ty zkontroluješ. S hooky tohle odpadá.

Nastavení v .claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "command": "npm run lint:fix -- ${file}"
      }
    ]
  }
}

Nahraď npm run lint:fix za linter tvého projektu – php-cs-fixer, black, rustfmt, prettier, cokoliv používáš. Jeden hook, deset řádků konfigurace a Claude dodržuje tvůj coding standard automaticky.

Krok 5: Skills – opakující se postupy jako recepty

Deploy na staging. Vytvoření migrace. Přidání API endpointu. Code review checklist. Kolikrát týdně tohle děláš? A kolikrát zapomeneš jeden krok?

Skills jsou markdown soubory v .claude/skills/, které zachycují opakující se postupy jako recepty. Vytvoříš skill deploy, pak stačí napsat /deploy. Claude si otevře recept a postupuje podle něj, místo aby vymýšlel postup od nuly.

# Deploy na staging

1. Spusť testy: `npm test`
2. Zkontroluj, že všechny prošly
3. Buildni produkční verzi: `npm run build`
4. Pushni do větve `staging`
5. Počkej na CI pipeline
6. Ověř, že staging odpovídá na https://staging.myproject.cz

Výhoda oproti wiki, kterou nikdo nečte? Claude ten postup skutečně provede. Projde každý krok.

Formalizuj i to, co ti přijde samozřejmé. Jak pojmenováváš soubory. Jakou máš adresářovou strukturu. Jak formátuješ kód. Ty to víš, tvůj tým to ví, ale Claude to neví – bez explicitního pravidla si domyslí vlastní konvenci.

Nemusíš všechno psát od nuly. Pro Nette Framework existuje sada hotových skills a hooků v repozitáři nette/claude-code – moderní konvence, Latte šablony, NEON konfigurace, coding standard linter pro PHP. Podobné balíčky vznikají i pro další frameworky.

Krok 6: MCP pluginy – připojte Clauda k prohlížeči a službám

MCP (Model Context Protocol) dává Claudovi nástroje, které sám od sebe nemá. Otevřít prohlížeč, přečíst mail, podívat se do kalendáře – to všechno vyžaduje MCP server, který mu tu schopnost zpřístupní.

Pro webový vývoj je naprosto klíčové Chrome DevTools MCP. Claude díky němu vidí prohlížeč – naviguje stránky, kliká, vyplňuje formuláře, pořizuje screenshoty, analyzuje výkon. Stačí říct třeba „zkontroluj objednávkový proces“. Aktivuješ ho přes plugin:

/plugin install chrome-devtools-mcp

Od Anthropic jsou k dispozici také konektory na Google služby – Gmail, Google Calendar a další. Aktivuješ je přes Connectors v nastavení na webu Claude.

MCP server přidáš buď přes plugin (jednodušší), nebo ručně vytvořením souboru .mcp.json v kořenu projektu. Technicky jde buď o lokální proces (bývá to node, python, php, cokoliv), nebo o vzdálený server, na který se připojí přes URL. U serverů postavených na Node.js na Windows bez WSL narazíš na stejný problém jako u LSP s příponou .cmd – ale Claude Code to umí opravit, když ho na to upozorníš.

Pozor na pluginy od neznámých autorů. MCP pluginy obsahují přímé instrukce pro Claude Code. Plugin od nedůvěryhodného autora může Claudovi podstrčit škodlivé instrukce. Drž se oficiálních pluginů z Anthropic marketplace a pluginy třetích stran si projdi, než je nainstaluješ.

Kdy to začne fungovat jako kouzlo

Možná si říkáš: to je spousta nastavování. Nestačí prostě otevřít Claude Code a začít?

Stačí. Stejně jako stačí programovat v Notepadu bez verzování a bez testů. Jde to. Nedoporučuju to.

Fígl je v tom, že se kroky navzájem násobí. Claude s CLAUDE.md ví, co má dělat. S testy ví, jestli to udělal správně. S LSP rozumí kódu na úrovni typů. S hooky dodržuje standard. Se skills zná postupy. S MCP sahá na reálné služby.

Odeber cokoliv z toho a kvalita spadne víc, než bys čekal.

A mimochodem – tyhle principy nejsou jen o Claude Code. Vibecoding v Cursoru, Windsurfu nebo při použití GitHub Copilotu stojí na stejných základech: kontext projektu, testy, statická analýza, linting. Nástroje se liší, návyky zůstávají.

Na co si dát pozor v praxi

Dávej malé úkoly. „Přepiš celou aplikaci“ je recept na katastrofu. Jedna fíčura, jeden konkrétní problém. Čím konkrétnější zadání, tím lepší výsledek.

Říkej proč, ne jen co. Rozdíl mezi „přidej validaci“ a „přidej validaci, protože uživatelé posílají prázdné formuláře a server padá na NPE“ je při psaní pravidel (například v CLAUDE.md) obrovský. Claude s kontextem dělá výrazně lepší rozhodnutí.

Dlouhá konverzace = ztráta kontextu. Jeden úkol, jedna konverzace. Pro větší úkoly začni s čistým stolem.

Kontroluj výstup. Vždycky. Claude občas vytvoří kód, který projde testy a přitom je architektonicky špatně. Čti, co navrhuje, než potvrdíš. Pak ho to nech kriticky zhodnotit a třeba celé přepracovat.

Moje tatérka prodává účetní software za pajcku 31.3.2026 00:00

Moje tatérka si přes víkend navibecodovala webový účetní systém a nabízí ho cizím lidem za 50 Kč měsíčně. Tetování dělá krásné. Ale dát jí místo jehly skalpel a říct „operuj, princip je podobný“? To asi ne.

Co se stane, když vibecodovanou aplikaci pustíte mezi lidi

Moje tatérka si vybrala účetnictví. Ale úplně stejný příběh se denně odehrává u seznamek, e-shopů, CRM systémů, rezervačních platforem. Fenomén vibe codingu, kdy lidé tvoří aplikace pomocí AI čistě skrze přirozený jazyk, dal náhle komukoliv pocit, že je vývojář. Jsou ale věci, které by člověk neměl vyrábět na koleni a prodávat cizím lidem. Padáky. Jaderné reaktory. A software pracující s jejich daty.

Citlivá data pod ochranou zákona. Jména, e-maily, adresy, platební údaje, to všechno spadá pod GDPR. Únik dat z děravé aplikace znamená pokuty v milionech korun pro provozovatele. A katastrofu pro zákazníky, jejichž data se octnou na internetu.

Bezpečnost, kterou nevidíte. Šifrování hesel, ochrana proti SQL injection, správa přihlášení, zabezpečení API klíčů… Vibecodovaná aplikace vypadá, že funguje. Ale to, že formulář odesílá data, neznamená, že je odesílá bezpečně.

Údržba navěky. Objeví se bezpečnostní díra v knihovně, kterou AI použila? Změní se legislativa? Kdo to opraví, vy, kteří kódu nerozumíte?

Bezpečnostní katastrofy, které se už staly

Nemyslím si, že moje tatérka má zlý úmysl. Ale cesta do pekla je, jak známo, dlážděná dobrými úmysly. A vibe coding z ní udělal dálnici.

Moltbook, sociální síť, jejíž zakladatel se chlubil, že „nenapsal jediný řádek kódu“. Přístupový klíč k databázi byl viditelný přímo ve zdrojovém kódu stránky. Stačilo zmáčknout F12. Výsledek: únik 1,5 milionu přístupových tokenů a 35 000 e-mailových adres.

Tea, seznamovací aplikace pro ženy, navržená jako bezpečný prostor. Databáze měla nulové zabezpečení. Vůbec žádné. Výsledek: únik 72 000 fotografií včetně 13 000 občanských průkazů a pasů, plus přes milion soukromých konverzací. Data skončila na 4chanu. Ženy, které aplikaci používaly, aby se chránily před nebezpečnými lidmi, měly najednou své identity volně přístupné přesně těm, před kterými utíkaly.

Enrichlead, platforma kompletně napsaná v AI editoru Cursor, jak zakladatel hrdě oznamoval. Do 72 hodin od spuštění ji někdo kompletně obešel. Veškeré zabezpečení běželo jen v prohlížeči. Stačilo ve vývojářských nástrojích změnit jednu hodnotu a měli jste plný přístup ke všemu zdarma.

To nejsou ojedinělé příběhy. Studie společnosti Veracode testovala kód generovaný jazykovými modely a zjistila, že 45 % kódu generovaného umělou inteligencí obsahuje bezpečnostní chyby. AI generovaný kód má téměř třikrát více zranitelností než kód psaný lidmi. Důvod je prostý: AI optimalizuje kód tak, aby fungoval, ne aby byl bezpečný. Bezpečnostní kontrola je pro ni jen další chyba bránící spuštění programu. A chyby se přece opravují, že.

Dunning-Krugerův efekt ve vibe codingu: proč nevidíte to, co nevidíte

Moje tatérka není hloupá. Naopak, je šikovná a podnikavá. Právě proto je to tak nebezpečné. Má za sebou úspěšný víkend s AI, aplikace běží, vypadá profesionálně. Proč by měla pochybovat?

Protože nevidí, co chybí:

Sám přes dvacet let vyvíjím open source software, na kterém běží statisíce webů. Vím, kolik neviditelné práce se skrývá v tom, aby aplikace nebyla jen funkční, ale i bezpečná. To, co vidíš na obrazovce, je deset procent ledovce. Zbytek je pod hladinou, a právě tam číhají problémy.

Andrej Karpathy, jeden z tvůrců GPT v OpenAI a autor samotného pojmu vibe coding, k tomu dodává: „Naše práce se přesouvá od psaní kódu k jeho kontrole. Je to jako s praktikanty: nepustíte je do produkce bez důkladného prověření.“

Jenže kdo prověřuje kód mojí tatérce?

Co si můžete navibecodovat bezpečně – a kde končí legrace

Nechci být protivný profík, který říká „nesahejte na to“. Vibe coding je fascinující a demokratizace tvorby softwaru je skvělá věc. Ale je potřeba vědět, kde končí pískoviště a začíná dálnice.

Směle do toho:

Tady už potřebuješ profíka:

Máš skvělý nápad na produkt? Navibecoduj si prototyp. Ukaž ho lidem. Ověř, jestli o to někdo stojí. A pak si najdi vývojáře, který z toho udělá bezpečný produkt. Prototyp ti ušetří měsíce práce a spoustu peněz. Jak na to, i když jsi nikdy neprogramoval? Tady je návod na první kroky.

A pokud jsi na druhé straně, jako zákazník: všímej si ceny. Fakturoid si za nejlevnější tarif účtuje 399 Kč, a za těmi penězi stojí tým vývojářů, bezpečnostních expertů a účetních poradců. Když někdo nabízí totéž za cenu flat white v kavárně, není to výhodná koupě. Je to varování.

Zpátky k jehle a skalpelu

Vibe coding je fantastický nástroj. Ale fantastický nástroj v nesprávných rukou a pro nesprávný účel je recept na průšvih. Až něčí zákazníci zjistí, že jim unikla data nebo že jejich účty napadl útočník, nebude to vina umělé inteligence. Bude to vina člověka, který si myslel, že umí něco, co neumí.

Skalpel totiž neřeže líp jen proto, že ho držíte s větším sebevědomím :-)

Často kladené otázky o vibe codingu a bezpečnosti (FAQ)

Je vibe coding nebezpečný? Sám o sobě ne. Je to skvělý nástroj pro prototypy, osobní projekty a učení. Nebezpečným se stává ve chvíli, kdy vibecodovanou aplikaci bez odborného bezpečnostního auditu nasadíte do provozu a nabízíte cizím lidem, zvlášť pokud pracuje s citlivými daty.

Mohu si navibecodovat vlastní web? Rozhodně. Osobní web, blog nebo portfolio patří mezi nejbezpečnější věci k vibecodování. Neobsahují databázi s cizími daty, nemají přihlašování a nemají prakticky žádný prostor pro útok. Pusťte se do toho.

Kolik stojí vývoj profesionálního softwaru pracujícího s daty uživatelů? Záleží na složitosti, ale i jednoduchá aplikace s přihlašováním a platbami vyžaduje bezpečnostní audity, právní konzultace a průběžnou údržbu. Proto profesionální řešení jako Fakturoid nebo iDoklad stojí stovky korun měsíčně, za těmi penězi stojí lidé, kteří za produkt skutečně ručí.

Prvních 30 minut s Claude Code (pro lidi, co nikdy neprogramovali) 31.3.2026 00:00

Tři nastavení, bez kterých je Claude Code jako taxikář bez adresy. Tenhle článek je tvůj checklist pro první půlden vibecodingu: nastavíš si CLAUDE.md, zapneš Git jako záchrannou síť a naučíš se zadávat úkoly tak, aby je Claude splnil správně. Předpokládám, že Claude Code už máš nainstalovaný. Pokud ne, tady je návod na instalaci.

Jak nastavit CLAUDE.md: soubor, který změní všechno

Tohle je ta nejdůležitější věc hned na začátek.

CLAUDE.md je textový soubor, který si Claude Code přečte pokaždé, když s ním začneš pracovat. Představ si ho jako briefing, který dáš novému kolegovi první den v práci. Bez něj bude Claude předpokládat, že jsi programátor, a bude se podle toho chovat. Bude mluvit v technickém žargonu a nebude dostatečně vysvětlovat co dělá. Což je recept na katastrofu, když nerozumíš kódu.

Řekni Claude Code:

Do globálního CLAUDE.md si zapiš toto:

# Kdo jsem
Jsem neprogramátor. Neumím číst ani kontrolovat kód.
Všechna technická rozhodnutí mi vysvětli jednoduše, bez žargonu.
Když se mám rozhodnout, ukaž možnosti s pro/proti a doporuč jednu.

# Jak pracuj
- Po každé změně shrň, co jsi udělal a proč, lidsky
- Když se něco rozbije, nejdřív vysvětli co se stalo, pak navrhuj opravu
- Do kódu piš komentáře vysvětlující, co která část dělá
- Změny dělej malé, jednu po druhé

# Bezpečnost
- Neměň architekturu bez mého souhlasu
- Nemaž soubory bez ptaní
- U úkolů s víc kroky mi nejdřív ukaž plán a počkej na schválení

A průběžně si CLAUDE.md vylaďuj. Všimneš si, že Claude opakovaně dělá něco, co nechceš? Řekni mu, ať do CLAUDE.md zapíše nové pravidlo. Tenhle soubor je živý dokument, ne náhrobní kámen.

Ale měj na paměti tohle:

Git: Záchranná síť, bez které nezačínej

Git je věc, bez které Claude Code na Windows vůbec nefunguje. Měl by být součástí instalace, ale pokud ho ještě nemáš, vrať se k návodu na instalaci. A pak řekni Claudovi tohle:

„Inicializuj Git repozitář a po každé větší změně vytvoř commit se srozumitelnou zprávou v češtině.“

Nemusíš vědět, co je Git. Nemusíš umět žádné příkazy. Stačí vědět jedno: Git si pamatuje každou verzi tvých souborů. Je to jako nekonečné undo, ale pro celý projekt. Když Claude jednoho dne něco rozbije – a rozbije, to ti garantuju –, řekneš mu „vrať to zpátky, včera to fungovalo“ a on to vrátí.

Bez Gitu bys plakal. S Gitem řekneš „ups“ a jedeš dál.

A pokud chceš tu historii změn i vidět, ne jen vědět, že existuje, nainstaluj si GitHub Desktop. Je to aplikace, ve které přehledně uvidíš, co se kdy změnilo, a můžeš se ke starší verzi vrátit jedním kliknutím.

Začni s něčím malým, ne s aplikací snů

Vím, je to lákavé. Máš k dispozici mocný nástroj a chceš rovnou postavit web nebo aplikaci, o které přemýšlíš už půl roku. Ale stejně jako bys poprvé za volantem nejel rovnou na dálnici, začni na parkovišti.

Tři skvělé první úkoly:

Organizace souborů:

„Přejmenuj všechny fotky v téhle složce podle data pořízení, formát YYYY-MM-DD.“

Analýza dat:

„Otevři tenhle CSV soubor a řekni mi, kolik je tam unikátních emailových adres a které se opakují nejčastěji.“

Jednoduchá automatizace:

„Projdi tuhle složku, všechny PDF slouč do jednoho souboru, seřazené podle názvu.“

Proč zrovna tohle? Protože u malých úkolů okamžitě vidíš výsledek. Fotky se přejmenovaly? Funguje. CSV má 342 emailů? Můžeš si to ověřit ručně. Tohle je tvůj kalibrační test. Zjistíš, jak Claude přemýšlí, jak reaguje na tvoje instrukce a jak moc musíš kontrolovat výsledek.

(Spoiler: kontrolovat musíš vždycky. Ale o tom za chvíli.)

Víc ukázek najdeš v článku Co všechno umí Claude Code, zdaleka to není jen programování.

Jak Claudovi zadávat úkoly, aby je splnil správně

Stačí jeden vzorec:

Co chci + Proč to chci + Jaká mám omezení

Špatně:

„Udělej mi web.“

Dobře:

„Chci jednoduchou stránku pro mou cukrárnu, protože zákazníci pořád volají a ptají se na ceny, místo aby se podívali online. Úvodní strana s fotkami dortů, ceník a kontaktní formulář. Použij čisté HTML, CSS a JavaScript, žádné frameworky. Design světlý, moderní, přizpůsobený mobilům.“

První zadání je jako říct taxikáři „jeď“. Druhé mu dává adresu, kudy nechceš jet, a že máš alergii na osvěžovač ve tvaru stromečku.

Pár tipů, co fungují:

Jakou technologii zvolit, když budeš stavět web

Když vibecoduješ, AI generuje dramaticky lepší kód pro některé technologie než pro jiné, prostě proto, že se na nich víc naučila. Tahle volba má obrovský dopad na výsledek a neprogramátor ji nemůže udělat informovaně. Tak tady je tahák:

Jednoduchý web bez přihlašování (vizitka, portfolio, landing page):

→ „Použij čisté HTML, CSS a JavaScript. Bez buildování, žádný npm.“
→ Výsledek hostuj zdarma na Netlify. Nula konfigurace, nula korun.

Interaktivní nástroj jen pro sebe (dashboard, kalkulačka, zpracování dat):

→ „Použij Python.“
→ Python je nejčitelnější programovací jazyk a AI v něm generuje skvělý kód. Pro osobní nástroje úplně stačí. Claude ti vytvoří skript, který spustíš jedním příkazem.
→ Řekni Claude Code: „Nainstaluj mi nejnovější Python a nastav ho tak, aby fungoval s češtinou.“ Claude pozná tvůj operační systém a nainstaluje ho správně.

Cokoliv s uživateli, platbami nebo citlivými daty:

→ Najmi vývojáře. Vážně. Tohle není oblast na experimenty.

Kdy to můžeš dát na internet – a kdy radši ne

Tady přichází otázka, která odděluje nevinné hraní od reálného průšvihu: používá tvoje aplikace databázi?

Pokud všechna data žijí v prohlížeči (na počítači toho, kdo aplikaci otevře), nemáš se čeho bát. Nikdo cizí se k nim nedostane, protože nikde jinde nejsou. Osobní web, portfolio, kalkulačka, plánovač, poznámkovník, to všechno klidně hoď na internet. I kdyby kód nebyl dokonalý, nejhorší, co se stane, je, že stránka bude ošklivá.

Jakmile ale do hry vstoupí databáze (a tím myslím jakýkoliv server, kam se ukládají data od uživatelů), situace se radikálně mění. Databáze je sejf. A stavět sejf bez toho, abys rozuměl bezpečnosti, je jako hrát si se střelným prachem podle návodu na YouTube.

Co se může stát? Nic teoretického. Seznamovací aplikace Tea byla vytvořená neprogramátory. Únik 72 000 fotek občanských průkazů skončil na 4chanu. Sociální síť Moltbook se chlubila tím, že „nezapsali jediný řádek kódu“. Přístupový klíč k databázi byl viditelný přímo ve zdrojovém kódu stránky. Stačilo zmáčknout F12.

Takže jednoduchý tahák:

Klidně dej online:

Tady potřebuješ vývojáře:

Máš nápad na aplikaci s databází? Skvělé. Navibecoduj si prototyp, ukaž ho lidem, ověř, jestli o to někdo stojí. A pak si najdi vývojáře, který z toho udělá bezpečný produkt. Prototyp ti ušetří měsíce vysvětlování a spoustu peněz. Ale nikdy ho nenasazuj do ostrého provozu tak, jak vypadl z AI.

Kolik to stojí a může mi to rozbít počítač?

Peníze. Claude Code funguje v rámci předplatného Claude, což je konkurent ChatGPT. Základní plán Pro za 20 dolarů měsíčně ti dá prostor to osahat. Přihlásíš se a jedeš. Když začneš stavět něco většího, narazíš na strop, ale neboj, Claude ti řekne něco jako „pokračujeme za dvě hodiny“ a po pauze jedeš dál. Pokud ti čekání nevyhovuje, je tu tarif Max za 100 dolarů. Pořád nic oproti sazbám programátorů.

Bezpečnost tvého počítače. Claude Code pracuje přímo s tvými soubory, to je fakt. Než udělá cokoliv destruktivního, ukáže ti, co chce provést, a čeká na tvoje schválení. Ale to neznamená, že můžeš odklikávat všechno poslepu. Čti, co ti navrhuje. Nerozumíš tomu? Zeptej se ho, co přesně ta změna udělá.

Proč vývojáři kritizují AI, která už neexistuje 19.3.2026 00:00

Narážím na příspěvky vývojářů, kteří popisují svou frustrující zkušenost s AI. A jsou to reálné zkušenosti – to jim neberu. Problém je, že tihle lidé popisují nástroj, který už v téhle podobě prakticky neexistuje.

Vzniká tím nebezpečná past: Vyzkoušíš AI špatně → nefunguje → utvrdíš se, že to je hračka → přestaneš sledovat vývoj. A pak z této pozice chodíš pod příspěvky ostatních a vysvětluješ jim, že jsou hlupáci. Svět se ale mezitím posune o několik generací dál a ty sis toho nevšiml, protože jsi byl zaneprázdněn psaním komentářů.

Co se změnilo?

Většina kritiků popisuje workflow z roku 2024: otevřu chatovací okno, napíšu co chci, doufám. Marně.

Dnešní AI agenti fungují úplně jinak. Agent sedí přímo ve tvém projektu. Čte celou kódovou základnu, spouští testy, používá git. Když udělá změnu, sám si ověří, jestli prošla buildem. Konvence má zapsané v konfiguračním souboru, který načte při každém startu. Na formátování kódu nepotřebuje „hádání" – pouští reálný formátovač jako hook po každé změně. Složitý úkol rozloží na podúkoly a deleguje je subagentům běžícím paralelně.

Ale není to zadarmo

Tenhle posun vyžaduje nový typ dovednosti. Potřebuješ vědět, jak agenta nakonfigurovat. Jak mu předat architekturní pravidla projektu, jak aktivovat LSP, jak nastavit hooky na linter a formátovač, jak nastartovat agentův loop tak, aby pracoval iterativně a ne naslepo. Je to skill, který se dá naučit, ale hlavně o něm musíš vůbec vědět.

Představ si, že juniorovi první den v práci pošleš email „předělej tohle a tohle" a odejdeš na oběd. Versus si s ním sedneš, dáš mu kontext a necháš ho pracovat po krocích. Stejný junior, dramaticky odlišný výsledek.

S AI je to totéž. Jen je neskutečně rychlá a nikdy se neurazí, když jí řekneš, že to má celé předělat.

Znamená to, že AI je dokonalá?

Ne! To přece neříkám. Revize kódu je pořád nutná. U složitých architektonických rozhodnutí AI stále potřebuje lidské vedení.

Ale senior, který umí AI řídit, bude dramaticky produktivnější než senior, který ji odmítá.

…protože si jednou spálil prsty s chatovacím oknem v roce 2024.

Štěstí přeje připraveným. Znamená to průběžně sledovat, jak se nástroje vyvíjejí. A naučit se je používat.

„Co o mně víš?“ zeptal jsem se ChatGPT. Odpověď mě vyděsila. 17.3.2026 00:00

Znáš tu kavárnu, kam chodíš každé ráno, a barista ti bez ptaní začne dělat tvoje obvyklé flat white s ovesným mlékem? Příjemné, že? Teď si představ, že ten barista má taky poznámku, že chodíš každý pátek nejistým krokem, protože to ve čtvrtek nezvládáš s vínem, a že jsi minulý týden zíral na výstřih blondýně u okna. A tuhle poznámku ti nikdy neukáže.

Přesně takhle dnes fungují AI asistenti s pamětí.

Když jsem poprvé psal o paměti ChatGPT, byl to takový roztomilý zápisníček. Dnes má paměť každý velký AI asistent: ChatGPT, Claude, Gemini, Copilot i Perplexity. Ale přístupy se zásadně liší. Od průhledného deníčku, do kterého si můžeš sám škrtat, až po profil, který o tobě vzniká na pozadí a ty se k němu běžně nedostaneš. Rozdíl mezi nimi není v tom, co si pamatují. Je v tom, co ti z toho ukáží.

Proč si AI asistenti vůbec něco pamatují

Jazykový model je z podstaty bezstavový. Každá konverzace začíná od absolutní nuly, jako kdybys pokaždé potkal člověka s totální amnézií, který má ale doktorát ze všeho. Chatbot bez paměti je zlatá rybka s encyklopedickými znalostmi: ví všechno možné o světě, ale netuší, že s tebou mluvil před pěti minutami.

Paměť, o které se tady bavíme, není součást modelu samotného. Je to přidaná vrstva, něco jako lepicí papírky nalepené na monitoru, které si model přečte pokaždé, než ti odpoví. A každý výrobce si ty papírky organizuje po svém. Pokud tě zajímá, jak to funguje pod kapotou, mrkni na první kroky s ChatGPT.

Zápisníček, deník, nebo Babišova složka?

Tři velcí hráči, tři úplně odlišné filozofie.

ChatGPT: sběratel dat. OpenAI to vzala ve velkém. Viditelná část paměti je zápisníček: krátké poznámky, které si ChatGPT ukládá během konverzace. Vejde se tam asi 6 000 tokenů (token je zhruba kousek slova, představ si dvě stránky A4 textu). Tuhle část najdeš v Nastavení → Personalizace → Spravovat paměť a každou poznámku můžeš smazat.

Jenže to je jen špička ledovce. Od dubna 2025 totiž ChatGPT umí sám vytahovat kontext z celé tvé historie konverzací – nemusíš nic hledat, on si vzpomene za tebe. Ovládá to přepínač Odkazovat se na historii chatu. A bezpečnostní výzkumník Johann Rehberger zjistil, co přesně se pod tím skrývá: ChatGPT si z tvých konverzací sestavuje podrobný profil, který se přikládá ke každému novému chatu. Najdeš v něm odvozené preference (i s poznámkou, nakolik si jimi je jistý), shrnutí témat, o kterých ses bavil, osobní údaje jako jméno nebo profesi, souhrn posledních zhruba čtyřiceti konverzací, a dokonce i technické údaje: typ zařízení, rozlišení obrazovky nebo to, jestli používáš tmavý režim.

Tenhle profil si ale nikde nezobrazíš ani neupravíš. V nastavení ho nenajdeš. Můžeš se ChatGPT zeptat „co o mně víš?“ a on ti kus profilu prozradí. Simon Willison to udělal a výsledek nazval „memory dossier“, tedy v podstatě paměťový spis. Na rozdíl od správy paměti, kde máš u každého záznamu tlačítko na smazání, tady nemáš nic.

Claude: průhledný deníček. Anthropic zvolila opačný přístup. Claude jednou za čtyřiadvacet hodin zpracuje konverzace do čitelného souhrnu, který najdeš v Settings → Capabilities → View and edit memory. Klíčové slovo: edit. Můžeš tam přepisovat, mazat, doplňovat, a změny se projeví okamžitě, bez čekání na další den. Žádný skrytý druhý level. Co je v paměti, to vidíš. Tečka. Claude se záměrně soustředí na pracovní kontext. Paměť mají k dispozici všichni uživatelé, i ti na bezplatném plánu. A na placených plánech Claude navíc umí prohledávat i tvoje minulé konverzace a dokáže si dohledat kontext, i když si ho předtím cíleně neuložil.

Gemini: hra s celým ekosystémem. Google má takzvané Saved Info, ručně uložené poznámky, které si prohlédneš a spravuješ, a vedle toho automaticky sestavovaný profil, kterému říká user context. Ten si nezobrazíš. Hlavní zbraň je ale funkce Personal Intelligence, spuštěná v lednu 2026: napojení na Gmail, Fotky, YouTube a Vyhledávání. Žádný jiný chatbot tohle nabídnout nemůže. Google o tobě ví všechno už dávno – Gemini to teď jen umí použít. Personalizace z historie konverzací se postupně rozšiřuje i na bezplatné uživatele, ale v Evropě zatím není dostupná.

Copilot od Microsoftu nabízí přepínače pro personalizaci a trénování na konverzacích. Co se průhlednosti týče, je na tom podobně jako ChatGPT: můžeš se zeptat „co o mně víš?“ a dostaneš shrnutí, ale nemáš k dispozici kompletní výpis. Na druhou stranu nemá tak agresivní profilování z historie.

Perplexity si zaslouží zvláštní zmínku, protože jako jediný ti u odpovědi ukáže, které konkrétní vzpomínky ji ovlivnily. Takovou průhlednost by si ostatní mohli vzít za vzor.

Grok od xAI? Ten má paměť v zárodku, ale v EU je celý nedostupný, víc o tom v mém postu o nedostupných funkcích v Evropě.

Jak smazat paměť ChatGPT a dalších

Dobrou zprávou je, že u většiny služeb máš nad pamětí kontrolu. Jen musíš vědět, kde hledat. Dobrý začátek: zeptej se svého chatbota „Co o mně víš?“ a podívej se do nastavení, možná budeš překvapený.

U ChatGPT hledej v Nastavení → Personalizace → Spravovat paměť. Přepínač Odkazovat se na historii chatu ovládá ten profil z historie. A pro konverzace, které nemají zanechat stopu, tu je dočasný chat, najdeš ho v horním menu při založení nové konverzace. U Clauda je to Settings → Capabilities → View and edit memory. U Gemini najdeš ruční poznámky pod Saved Info a automatickou paměť pod Personal context. U Copilotu hledej v Settings → Personalization.

Zajímavý trend je oddělení paměti po projektech. ChatGPT Projects i Claude Projects ti umožňují vytvořit samostatné pracovní prostory, kde si AI pamatuje jen to, co se týká konkrétního projektu, a nic z toho nepřeteče do tvého hlavního profilu. To se hodí, když nechceš, aby se tvoje pracovní konverzace o firemní strategii mísily s nočním filozofováním o smyslu života.

Claude navíc spustil pokusný přenos paměti: možnost převzít si paměť z jiného AI asistenta. Zatím je to v rané fázi a nemusí to vždy fungovat, ale směr je správný: pokud je tvůj AI profil cenný, měl bys ho mít pod kontrolou celý, včetně toho, kam s ním půjdeš.

Kdo je tu pro tebe

Slyším námitku: „Ale mně ta personalizace pomáhá. Proč bych se měl bát?“ Nemusíš. Personalizace je skvělá věc, pokud víš, co do ní vstupuje. Nejde o to, že si AI pamatuje tvoje zvyky. Jde o to, jestli máš nástroj, kterým si ověříš, co přesně o tobě ví. ChatGPT a Copilot vyměňují soukromí za pohodlí, Claude dává kontrolu za cenu menší „magičnosti“, Gemini sází na to, že Google o tobě ví všechno už dávno. Za mě? Dávám přednost tomu, abych věděl, s čím pracuji. Radši barista, který mi zápisník ukáže, než ten, co mě zná dokonale – ale odmítne prozradit, jak na to přišel :-)

PS: Chování jednotlivých služeb se liší podle toho, jestli používáš bezplatný, osobní nebo firemní plán, a samozřejmě se může kdykoli změnit. Neměl jsem možnost otestovat každou kombinaci. Pokud narazíš na nepřesnost, napiš mi, rád to opravím.

Víš, co znamená ChatGPT? Ani OpenAI si není jistá 7.3.2026 00:00

Původně se to mělo jmenovat „Chat with GPT-3.5“.

Vážně. Název změnili noc před spuštěním, protože si uvědomili, že se to špatně vyslovuje a zní to jako náhodně vygenerované heslo do Wi-Fi. Žádná brandingová agentura, žádný průzkum trhu. Jen pár unavených inženýrů, kteří ani nevěřili, že by jejich výtvor kohokoliv zaujal.

Ale to je teprve začátek. Název ChatGPT se skládá ze slova Chat a zkratky GPT, a každé z těch písmen skrývá překvapivý příběh. T jako Transformer, architektura, která se málem jmenovala CargoNet. P jako pre-trained, slovo, které dnes znamená pravý opak toho, čím je chatbot. G jako generative, sázka, o které si většina výzkumníků myslela, že je slepá ulička. A samotná zkratka GPT? Ta se v původní studii o GPT vůbec neobjevila.

Pojďme si projít název, který vyslovují miliony lidí denně, a skoro nikdo netuší, co vlastně říká.

ChatGPT – název na poslední chvíli

Celý produkt vznikl během desetidenního hackathonu. V OpenAI ho nikdo nepovažoval za důležitý, šlo o výzkumnou ukázku, sběr dat, nic víc. Liam Fedus, jeden z tvůrců, to shrnul pro MIT Technology Review: nepovažovali jsme to za nic převratného.

Ještě noc před spuštěním Ilja Sutskever testoval model deseti těžkými otázkami, spokojený byl jen s polovinou. Tým váhal, jestli to vůbec spustit. A právě tehdy, na noční poradě 29. listopadu 2022, padlo i rozhodnutí přejmenovat produkt z „Chat with GPT-3.5“ na „ChatGPT“. Nick Turley, šéf produktu, později vzpomínal: prostě jsme si uvědomili, že název zní krkolomně.

Druhý den ráno to šlo ven. A stalo se něco, co nikdo nečekal.

Turley zíral na čísla a myslel si, že je to chyba, tolik lidí přece nemůže přijít najednou. Na vánočním večírku o pár týdnů později kolegové tipovali, že zájem opadne.

Neopadl.

Sam Altman později přiznal, že OpenAI je mnohem lepší ve výzkumu než ve vymýšlení názvů. Ale občas stačí pojmenovat věci ve tři ráno.

Tak. To byl Chat, slovo, které přidal někdo v polospánku na poslední chvíli. Teď ke zbytku. Ke třem písmenům, z nichž každé skrývá ještě bláznivější historii.

T jako Transformer (a málem CargoNet)

Začněme odzadu, od písmene T. Transformer je motor, na kterém dnes běží všichni chatboti. Vymyslel ho v roce 2017 tým v Googlu, původně proto, aby zlepšil strojový překlad z angličtiny do němčiny a francouzštiny.

Jenže technologie potřebovala jméno. Prvním kandidátem byl „Attention Net“. Příliš nudné. Pak jeden ze spoluautorů navrhl „CargoNet“, akronym z Convolution, Attention, Recognition a Google (názvy použitých technologií plus Google). Zbytek týmu to jednomyslně smetl. Když o tom léta později vyprávěl šéfovi Nvidie Jensenovi Huangovi, ten reagoval suše: „Moudří lidé.“

Kdyby tým neměl dobrý vkus, celá dnešní AI revoluce by stála na technologii, která zní jako doručovací služba.

Finální název Transformer navrhl lingvista Jakob Uszkoreit. Transformace z jednoho jazyka do druhého, tak to původně myslel. Další ze spoluautorů ale tvrdí, že ambice byly od začátku trochu větší: „Nešlo nám jen o překlad. Chtěli jsme vytvořit něco obecného, něco, co dokáže transformovat jakýkoli vstup na jakýkoli výstup.“ Ale že jejich vynález jednou bude psát básně, generovat obrázky a analyzovat proteiny, to nečekal nikdo z nich.

Aby ale Transformer mohl fungovat jako základ ChatGPT, musel ho někdo vzít a naučit rozumět jazyku. A tady vstupuje na scénu písmeno P.

P jako Pre-trained – polotovar, ze kterého vyrostl hotel

Dnes je ChatGPT hotový produkt: napíšeš otázku, dostaneš odpověď. Ale to „P“ v názvu GPT říká něco úplně jiného. „Pre-trained“ znamená „předtrénovaný“. Ne natrénovaný, předtrénovaný. Jako polotovar, který si musíš doma dodělat sám.

V roce 2018 to tak opravdu fungovalo: vzal jsi model, který nasál hromadu textu, a pak sis ho sám dotrénoval na konkrétní úkol: třídění emailů, rozpoznávání jmen, odpovídání na otázky. Bez toho dotrénování byl k ničemu. Nikdo nepočítal s tím, že by polotovar mohl být rovnou hotový pokrm.

Jenže s každou další generací se děly podivné věci.

GPT-2 v roce 2019 naznačil, že model dokáže plnit úkoly i bez doladění, prostě jen na základě zadání. Výsledky byly slibné, ale ještě ne oslnivé. Skutečný zlom přišel s GPT-3, který byl stokrát větší. A ukázalo se, že velikost mění všechno, jako rozdíl mezi studentem, který přečetl jednu učebnici, a někým, kdo přečetl celou knihovnu. Autoři ve studii přímo napsali, že GPT-3 už žádné dolaďování nepotřebuje. Model dostal úkol jen jako text a zvládl ho.

Všechno se otočilo. Z „natrénuj a dolaď“ vzniklo „natrénuj a zeptej se“.

Slovo „pre-trained“ zůstalo v názvu jako fosilie, připomínka doby, kdy nikdo nevěřil, že to může fungovat samo.

Zbývá poslední písmeno: G. A je to možná ta nejpřekvapivější část celého příběhu.

G jako Generative – sázka proti proudu

G znamená „generativní“, tedy tvořící. A v roce 2018 to byla odvážná volba. Umělá inteligence tehdy uměla hlavně kategorizovat: rozpoznat zvíře na obrázku, rozlišit spam od běžného emailu. Jasný úkol, jasná odpověď, měřitelný výsledek. A celý obor věřil, že právě tudy vede cesta ke stroji, který porozumí lidské řeči: dávej mu úkoly, měř, jak se zlepšuje, a jednou to zvládne.

Alec Radford to otočil. Bylo mu třiadvacet, do OpenAI přišel rovnou po bakaláři a šéf firmy Sam Altman ho později označil za naprostého génia. Radford dal svému modelu miliony recenzí z Amazonu: přečti si začátek recenze a zkus psát její pokračování, jako ho psal původní autor. Žádné třídění, žádné škatulkování, prostě piš dál.

Jenže aby model dokázal smysluplně navázat na cizí větu, nestačilo skládat obvyklá slova za sebe. Musel postupně pochopit, co vlastně píše. Všiml si, že v různých typech recenzí se vyskytují různá slova: jiná v nadšených, jiná ve stížnostech. A podobně s ironií, pochybami, radami. To mu stačilo.

Nikdo ho neučil rozumět. Učil se tvořit text, a porozumění přišlo jako vedlejší produkt.

Paradox se skrýval přímo v názvu studie: Zlepšení porozumění jazyku pomocí generativního tréninku. Zlepšení porozumění pomocí tvoření. Jako pochopit architekturu tím, že zkusíte postavit dům. Zní to absurdně, ale fungovalo to.

Když Google o pár měsíců později představil vlastní model založený na opačném principu, uč stroj přímo rozumět, a na většině testů GPT jasně porazil, mnozí usoudili, že generativní cesta vede do slepé uličky. GPT-2 a GPT-3 je umlčely.

A historická ironie: slovo „generativní“ bylo v roce 2018 odborným termínem pro pár stovek výzkumníků. Dnes je „generativní AI“ název celé epochy.

Radford, nesmírně plachý člověk, prakticky neposkytoval rozhovory. V prosinci 2024 z OpenAI tiše odešel. Značka, kterou pomohl stvořit, žije dál bez něj.

GPT: zkratka, která neexistovala

Celou dobu mluvím o G, P a T, ale samotná zkratka GPT? Ta v původní studii z června 2018 vůbec nebyla. Autoři svůj model pojmenovali prostě „doladěný transformátorový jazykový model“. Altman měl pravdu: vymýšlení názvů fakt není jejich silná stránka.

Zkratka se poprvé objevila až v únoru 2019, kdy OpenAI představila GPT-2. A označení „GPT-1“? To nikdy oficiálně neexistovalo, zavedla ho komunita zpětně, aby odlišila verze.

A pikantní detail: Mark Chen, výzkumník OpenAI, v podcastu prozradil, že se ani uvnitř firmy neshodnou na tom, co GPT znamená. Polovina říká „Generative Pre-trained Transformer“, tedy generativní předtrénovaný transformátor. Druhá polovina tvrdí „Generative Pretrainee“, cosi jako „generativní praktikant“. Značka za miliardy dolarů, a nikdo přesně neví, co znamená :-)

Sedm písmen, které nikdo neplánoval

Zkratka GPT v původní studii neexistovala, a dodnes se v OpenAI neshodnou, co znamená. Transformer přežil konkurenci CargoNetu díky estetickému citu jednoho lingvisty. „Pre-trained“ znamená polotovar v názvu pro hotový produkt. A „generative“? Odvážná sázka třiadvacetiletého výzkumníka, který šel na to od druhého konce, nechal stroj tvořit místo rozumět. A ono to přišlo samo.

Samotný název ChatGPT? Vymyšlen noc před spuštěním místo krkolomného „Chat with GPT-3.5“.

Někdy největší značky na světě vzniknou tak, že se pár lidí nevyspí.


P.S. Francouzi v tom názvu slyší příběh výmluvnější než celý tento článek: kočka (chat) prdla (GPT = žé-pé-té ≈ j'ai pété). Víc nepotřebují vědět.

Mac Mini za padesát tisíc na AI. Tohle si přečti, než ho koupíš. 1.3.2026 00:00

„Koupil jsem si Mac Mini,“ chlubí se kamarád. „Budu na něm provozovat lokální AI!“

Pokud zvažuješ něco podobného, tento článek ti možná ušetří desítky tisíc. Rozdíl mezi lokální a cloudovou AI totiž existuje – je dokonce obrovský. Ale je úplně jinde, než si většina lidí myslí.

„Vzdálené AI funguje krátce“

Kamarád to vysvětluje přibližně takhle: „Ty cloudové modely, ChatGPT a tak, fungují vždycky krátce. Krátká interakce. Kdežto když to budu mít u sebe, může mi to běžet dlouho. Na velké úlohy.“

Chvíli na něj zírám. Je to, jako by někdo řekl: „Voda z vodovodu teče jenom chvíli, ale voda z vlastní studny může téct celý den.“ Puštěnou vodu máš z obou tak dlouho, jak chceš. S umístěním serveru to nemá co dělat.

ChatGPT, Claude, Gemini – tyhle služby běží přesně tak dlouho, jak potřebuješ. Můžeš s nimi vést dvouhodinový rozhovor, nechat je zpracovat stostránkový dokument, nebo je nechat přes noc analyzovat data. Žádný limit „krátkosti“ neexistuje.

Lokální i vzdálený model fungují na totožném principu: pošleš požadavek, dostaneš odpověď. To je celé. Jediný rozdíl je, kam se tvůj počítač připojuje – jestli k serveru ve vedlejším pokoji, nebo k datacentru v Oregonu. Na délku ani kvalitu odpovědi to nemá žádný vliv.

„Ale bude mi to proklikávat weby!“

Dobře, argument s délkou neobstál. Kamarád tedy zkouší jinou obhajobu: „OK, ale já to hlavně chci na to, aby mi to proklikávalo weby a hledalo na nich chyby.“

Opět marně. Vzdálený model umí procházet weby úplně stejně jako lokální. Schopnosti AI závisí na modelu samotném a na nástrojích, které má k dispozici – ne na tom, kde fyzicky běží. Je to jako věřit, že email odeslaný z notebooku v obýváku dojde někam jinam než email z kanceláře. Stejný email, stejný výsledek, jen jiná židle pod zadkem.

Jádro celého nedorozumění je jednoduché: lidé si pletou kde model běží s tím, co umí.

Možná si říkáš: „Ale přece jen, mám to u sebe, nejsem závislý na cizím serveru.“ Fajn, to je legitimní úvaha. Ale to je úplně jiný argument než „poběží to furt“ nebo „bude to umět víc“. Nezávislost na poskytovateli je jedna věc – schopnosti modelu úplně jiná.

A nejde jen o toho kamaráda. Třeba Petr Ludwig nedávno napsal: „Nechal jsem svoji AI asistentku Lanu přečíst moje dvě knihy a teď jsem jí zadal poslechnout 200 dílů mého podcastu – do rána to prý zvládne. Funguje naprosto autonomně.“ Ten tón – ta magie kolem slova „autonomně“ – jako by lokální provoz odemykal něco, co cloud neumí. Přitom totéž zvládne jakýkoliv cloudový model – stačí mu předat data a zadat úkol.

Co běží na domácím hardware?

Na počítači s „lokální AI“ běží dvě odlišné věci:

Řídící program – přijme tvůj úkol, zavolá model, nechá ho rozložit na kroky, zpracuje výsledek, otevře prohlížeč, prokliká web. Právě tohle dělá třeba Claude Code nebo OpenClaw. Klíčová informace: řídící program nepotřebuje žádný speciální hardware. Běží na úplně obyčejném počítači. Klidně i na tom, co máš teď na stole.

Samotný model – to je ta výpočetně náročná část. Chceš, aby jazykový model běžel u tebe a generoval odpovědi? Teprve tady potřebuješ výkon – a teprve tady se nabízí otázka, jestli ti Mac Mini za desítky tisíc vůbec stojí za to.

Většina populárních „AI asistentů“, kvůli kterým si lidé kupují drahý hardware, ten model lokálně vůbec nepouští. Řídící program běží u tebe, ale na generování odpovědí volá cloudové API – ChatGPT, Claude, nebo jiný model. Takže se koupíš Mac Mini za padesát tisíc a odpovědi ti stejně generuje ten server v Oregonu.

Kdy lokální AI dává smysl

Nechci být nespravedlivý. Existují přesně tři legitimní důvody, proč chtít, aby ten model skutečně běžel u tebe:

Tvoje data nesmí opustit počítač. Pracuješ s citlivými firemními dokumenty, zdravotními záznamy, nebo materiály pod NDA? Pak je na místě, aby je zpracovával model běžící čistě u tebe. To je naprosto validní důvod – a v řadě firem i regulatorní nutnost.

Cenová optimalizace. Pokud AI používáš tak intenzivně, že se jednorázová investice do hardwaru vyplatí víc než měsíční předplatné, jdi do toho. Ale musíš si to opravdu spočítat. A počítat poctivě.

Chceš dělat věci, které ti cloud odmítne. Cloudové služby odmítnou zpracovat určité úlohy, odmítnou odpovědět na určité otázky. Lokální model nemá žádná taková omezení – dělá přesně to, co mu řekneš. Žádné „omlouvám se, ale toto nemohu“.

Je fér říct, že lokální modely se v poslední době hodně zlepšují. Na jednodušší úlohy – třeba základní kódování nebo běžnou práci s textem – už mohou být docela použitelné. A pak existují specializované malé modely na konkrétní úlohy, které dávají lokálně naprostý smysl. Třeba přepis mluveného slova na text – na to nepotřebuješ Mac Mini za padesát tisíc. To mi frčí na běžné NVIDIA grafické kartě s 12 GB paměti.

Ale na složitější práci pořád výrazně zaostávají za špičkovými cloudovými modely. Ty nejlepší modely běží na obrovských serverových farmách s výkonem, se kterým se žádný stolní počítač nemůže měřit. Kupuješ si slabší motor za vyšší cenu – jako bys platil za první třídu v letadle, které letí pomaleji.

Tak kolik to stojí?

Pojďme to rychle přeběhnout – ať víme, o čem se tu bavíme. Mac Mini s čipem M4 a maximálními 32 GB RAM: 29 tisíc. S M4 Pro a 48 GB: 54 tisíc. A pokud chceš 64 GB RAM, tedy maximum: 60 tisíc korun. (Ano, za stolní počítač bez monitoru, klávesnice a myši.)

Paměť je přitom klíčový parametr – a nejde jen o její velikost. Záleží i na tom, jak rychle dokáže komunikovat s procesorem. Procesor totiž většinu času čeká, než mu paměť pošle další kus dat pro model. Apple Silicon má díky sdílené paměti slušnou propustnost (kolem 273 GB/s u M4 Pro), což je důvod, proč se Mac Mini pro lokální AI vůbec doporučuje. Nové Ryzeny Strix Halo se sdílenou pamětí se mu v tomhle začínají blížit. Běžné PC s klasickou DDR5 pamětí má propustnost jen 75–100 GB/s – a to je na plynulou práci s většími modely málo.

A mimochodem – kolik textu model najednou „vidí“, takzvané kontextové okno, taky šíleně žere paměť. U většího modelu může kontext o 128 tisících tokenech spolknout klidně přes 18 GB. Takže do RAM se musí vejít celý model, jeho cache, operační systém, a pokud má ten Mac Mini ještě proklikávat weby, tak i prohlížeč se vším všudy.

S 32 GB rozjedeš jen prťavé modely. Se 64 GB už slušnější, ale pořád ne na úrovni, kterou asi očekáváš. Doporučil bych alespoň 128 GB a více, ale to už se bavíme o Mac Studio s cenovkou (hodně) nad 100 tisíc.

A dále – jazykový model, model na generování obrázků a model na rozpoznávání obrazu jsou tři různé modely. Každý zabírá místo v paměti. Nemůžeš je mít všechny najednou, takže je musíš prohazovat – nahrát jeden, pustit úlohu, vyhodit ho, nahrát další. V cloudu tohle řešit nemusíš.

Pro srovnání: platím Claude MAX za 90 eur měsíčně, což je asi 2 250 korun. Za rok 27 tisíc. Jinými slovy: za cenu Mac Mini s 64 GB (60 tisíc) bych měl přes dva roky přístupu k jednomu z nejsilnějších modelů na světě. Bez starostí s údržbou a bez kompromisů v kvalitě.

Jedna otázka místo padesáti tisíc

Pokud tě zajímá, jak reálně vypadá práce s AI na vlastním hardwaru, podívej se na přednášku o agentic codingu na GPU z ai4dev.cz nebo si přečti zkušenosti těch, co to zkusili.

Než utratíš desítky tisíc za hardware kvůli „lokální AI“, zkus si poctivě odpovědět: co přesně ti lokální model dá, co vzdálený ne? Odpověď není jednoduchá – ale právě proto stojí za to si ji rozmyslet dřív než po nákupu.

A kamarád? Ten má aspoň na stole designový kousek za padesát tisíc. To se taky počítá :-)

Einstein jedl a kadil v Praze 26.2.2026 00:00

14. července 1789. Paříž. Ráno. Jeden z klíčových momentů lidských dějin.

V Paříži posnídalo několik set tisíc lidí. Někteří měli chléb, jiní ne, tahle nerovnoměrná distribuce snídaní bude za okamžik hrát zásadní roli. Po snídani většina z nich vykonala potřebu, jak bývá po snídani zvykem.

Někteří lidé, kteří ráno jedli a poté kadili, vyrazili k pevnosti zvané Bastila, kde byli uvězněni jiní lidé, kteří taktéž jedli a kadili, byť v podstatně horších podmínkách. (Kvalita kálení ve francouzských věznicích 18. století si zaslouží samostatnou studii, kterou tento článek nemá ambici být.) Onu pevnost dobyli. Pak šli na oběd.

Jedl a kadil i král, a to na zámku ve Versailles. Jednoho dne mu usekli hlavu a poté již nikdy nejedl. O kálení se historici taktně nezmiňují.

Co kdyby TAKHLE vypadala přednáška o Velké francouzské revoluci?

12. dubna 1961, Bajkonur. Člověk jménem Gagarin snídal. Údajně dostal čaj a tvaroh. Pak kadil.

Následně se nechal vystřelit v těsné kovové kouli do vesmíru. Obletěl Zemi za 108 minut, během kterých ani nejedl, ani nekadil, a bylo mu za to uděleno nejvyšší státní vyznamenání. Psali o něm noviny po celém světě.

Koupil by sis na takový TED talk o počátcích kosmonautiky lístek?

Rok 1905. V Bernu žije šestadvacetiletý úředník patentového úřadu jménem Einstein. Ráno vstane a snídá. Pak jde do práce, kde osm hodin posuzuje patentové přihlášky, během toho několikrát kadí. V poledne obědvá. Večer večeří a kadí. Jde spát.

Někdy v noci taky něco píše: čtyři články o světle, atomech, relativitě a E = mc². Je to spolehlivý, předvídatelný cyklus, který trvá celý rok. Historici mu říkají rok zázraků, německy Wunderjahr, což zní spíš jako lék na zácpu.

Později se Einstein přestěhoval do Prahy. Jedl v Café Louvre, kadil na Karlo-Ferdinandově univerzitě a mezi tím tak nějak vymyslel obecnou teorii relativity.

Představ si, že takhle popisuje Einsteinův rok zázraků profesor Krtouš z pražského Matfyzu. Hmm…

Přednáškový sál, minulý týden

Sedím na přednášce o jazykových modelech. Přednášející srozumitelně a věcně vysvětluje, jak to celé funguje. Ústřední myšlenka: jazykový model „jenom generuje další slovo s největší pravděpodobností“. Publikum mu nadšeně visí na rtech. A mně to celou dobu vrtá hlavou.

Model produkuje tokeny, jeden za druhým. Člověk produkuje hovna, jedno za druhým. Obojí je pravda. Obojí popisuje tu nejspodnější vrstvu celého příběhu. Jako v té úvodní historce o francouzské revoluci. Jenže nad naším trávením se postupně navrstvilo pár věcí. Kultura. Věda. Revoluce. Obecná teorie relativity. A nad generováním tokenů taky.

Dneska máme reasoning modely, které přemýšlejí krok za krokem, zvažují alternativy a opravují vlastní chyby. Máme AI agenty, kteří dostanou zadání, sami naplánují postup, napíšou a spustí kód, vyhodnotí výsledek, a pokud nevyjde, začnou znovu.

Ale mluvit v roce 2026 o jazykových modelech jako o „generátoru dalšího slova“ – to je jako přednáška profesora Krtouše zaměřená na to, že když zapil třešně mlíkem, jevil se Einsteinovi univerzitní záchod relativně mnohem dál.

Popisovat složitou věc jednou vrstvou jde vždycky:

Všechno je to pravda. Ze žádné z těch definic ale nepochopíš, proč u Hamleta brečíš.

Nejdůležitější věc, kterou ti nikdo o ChatGPT neřekne 18.2.2026 00:00

Smál jsem se 😄 Rok 2023, Midjourney frčí naplno a lidi na internetu hrdě ukazují, jak do ChatGPT nasázeli celou jeho dokumentaci (stránku po stránce) a nechávají si generovat prompty pro obrázky. Geniální nápad. Jenže já věděl něco, co oni ne. ChatGPT si z toho pamatoval tak poslední stránku. A po pátém vygenerovaném promptu zapomněl i tu. Tehdy měl paměť pouhých 1 800 slov. A co dnes? Budete se divit!

Tohle je nejdůležitější věc, kterou ti nikdo o ChatGPT neřekne. Má ze všech velkých chatbotů nejkratší paměť na konverzaci. Jasně, povídat si s ním můžeš do nekonečna, nikdy tě nezastaví. Ale to je právě ta zákeřnost. Když je konverzace dlouhá, starší zprávy tiše ignoruje. Bez varování. Prostě je nevidí. A pokračuje dál, jako by nikdy nezazněly.

Tři roky to říkám na každé přednášce. Tři roky se dívám, jak lidi dělají pořád ty samé chyby. A pořád je to šokuje. Když o tom napíšu na internetu, lidi se se mnou hádají do krve, prý má ChatGPT přece mnohem větší paměť. Dnes je každý na vrcholu Dunning-Krugerovy křivky a má pocit, že ví všechno.

Přitom ta čísla jsou přímo na webu OpenAI. Pravda, nekřičí je do světa, ale ani je netají. A vždycky jste je mohli najít na Uměligenci.

Tři mýty o paměti ChatGPT

Iluze, na které narážím od roku 2023 dodnes, a nic se na nich nezměnilo:

„Nahraju tam pět PDF a ono mi to zanalyzuje.“ Nahrát je můžeš. Ale kontext se prvním zaplní a chatbot na zbylé dokumenty kašle. Dostaneš iluzi analýzy, ne analýzu.

„Má přece obrovskou paměť, je to AI!“ Nemá. Je na tom nejhůř ze všech. To, co konkurence dává zadarmo, nemá ChatGPT ani v nejdražším tarifu.

„ChatGPT vidí všechny moje konverzace.“ Nevidí. Každé vlákno je úplně samostatný svět. Co jsi psal ve vedlejším vlákně, pro něj neexistuje.

Možná namítneš: „Ale ChatGPT si přece pamatuje, že bydlím v Hradišti a mám rád pizzu Hawaii!“ Ano, to je funkce Memory, takový zápisníček s poznámkami o tobě. ChatGPT umí taky prohledávat starší vlákna. Claude má něco podobného. Ale nic z toho nenahrazuje kontextové okno – to jsou jen útržky nalepené na novou konverzaci. Lístek na lednici, ne pracovní stůl.

Kolik slov se vejde do paměti?

Protože tenhle článek píšu pro lidi a ne pro AI inženýry, budeme se bavit o délkách textů měřených v českých slovech. Chatboti počítají v tokenech a jedno české slovo zabírá zhruba 2,2 tokenu.

Představ si konverzaci s kamarádem v hospodě. A tenhle kamarád má tak upito, že si pamatuje jen posledních pár minut. Zbytek mu vypadne z hlavy, a neřekne ti to. Prostě se tváří jako by nic a pokračuje. Ty tomu věříš, protože mluví sebevědomě. Téhle paměti se odborně říká kontextové okno a jeho velikost určuje, kolik textu chatbot „vidí“ najednou.

ChatGPT v bezplatné verzi dnes pojme asi 7 000 českých slov. Placená verze Plus kolem 15 000 slov. To zní jako hodně, dokud si neuvědomíš, že se do toho počítají i odpovědi chatbota.

A víš, jak to vypadalo na začátku? První ChatGPT v roce 2022 pojal asi 1 800 slov. Míň než Vaculíkovy Dva tisíce slov – on by tu petici klidně podepsal před zrakem policajtů, protože by nevěděl, co podepisuje.

Chatbot   ≈ českých slov
GPT-3.5 (první ChatGPT) 2022 ~1 800
GPT-4 Plus 2023 ~3 600
GPT-4o Plus 2024 ~7 000
GPT-5 Plus 2025 ~15 000

Za tři roky se paměť zčtyřnásobila. Zní to jako pokrok, dokud se nepodíváš, co nabízí konkurence.

Brutální srovnání

Takže jak si stojí ChatGPT ve srovnání s konkurencí?

Chatbot ≈ českých slov
ChatGPT 5.2 Free ~7 000
ChatGPT 5.2 Go / Plus / Business ~15 000
Claude 4.6 ~90 000 (velmi brzy ~450 000)
Gemini 3 Pro ~450 000
Grok 4.2 ~120 000
DeepSeek 3.2 ~120 000 (velmi brzy ~450 000)
ChatGPT 5.2 Thinking ~58 000 (teprve od 8/2025)

Pro lepší představu to dám do grafu:

Podívej se na to ještě jednou. ChatGPT v bezplatné verzi: sedm tisíc slov. Gemini Pro: čtyři sta padesát tisíc. Do kontextového okna Gemini se v angličtině vejdou všechny díly Harryho Pottera. Do ChatGPT seminárka. To je jako srovnávat kapesní diář s městskou knihovnou.

(Ať jsme fér, Gemini v bezplatné verzi má jen 15 000 slov, jako placený ChatGPT Plus, ale existuje snadný způsob, jak ty statisíce slov získat úplně zadarmo, na školeních to vždycky ukazuju.)

Claude nabízí 90 000 slov v jakékoliv verzi, včetně bezplatné. Už roky. A právě teď přichází s verzí co dorovnává Gemini. DeepSeek dosud nabízel 120 000 slov, a je celý zdarma. Každým dnem má vyjít verze, která také dorovná Gemini.

Všimni si jedné věci: ChatGPT GPT 5 Thinking sice má 58 000 slov (pro vstup). Ale tenhle režim je novinka (stav dříve) a navíc musíš vědět, že si ho máš zapnout. OpenAI slavnostně dohání Claude na devadesáti tisících v době, kdy ostatní přecházejí na milion tokenů.

Možná si říkáš: „No a co, 15 000 slov je přece dost, ne?“ Záleží, co děláš. Pokud si jen povídáš o počasí, asi ano. Ale jakmile nahraješ dokument a k tomu vedeš konverzaci, 15 000 slov zmizí rychleji, než bys čekal. A uvědom si, že ta slova zahrnují i odpovědi chatbota. Tvůj reálný prostor je nanejvýš poloviční.

Proč je zrovna ChatGPT nejhorší?

Když jsi nejpopulárnější, nemusíš se tolik snažit. Lidi tě stejně používají. ChatGPT je suverénně nejpoužívanější chatbot na světě, a když jsi McDonald's, nemusíš mít nejlepší burger ve městě. Stačí, že jsi na každém rohu.

A ChatGPT je, co se týče paměti, trvale pozadu. Nejde jen o bezplatnou verzi se 7 000 slovy. I placený Plus nabízí jen 15 000. I nejdražší tarify za slušný balík mají v běžném režimu kolem 58 000 slov, pořád méně, než co Claude dává každému zadarmo. Jedinou výjimku představuje režim Thinking, jenže o tom většina uživatelů ani neví. A není to tím, že by to neuměli. Přes API nabízí OpenAI vývojářům kontextová okna o stovkách tisíc tokenů. Technicky to zvládají, jen to nedají běžným uživatelům na webu.

Konkurence to musí řešit jinak. Musí nabídnout víc, aby přetáhla uživatele. A tak nabízí neuvěřitelně velká kontextová okna.

Abych byl fér: ChatGPT nabízí šíři funkcí, kterou nikdo jiný nemá. Generování obrázků, video, hlasový režim, Canvas, agenty, Deep Research. Je to švýcarský nožík AI světa. Ale šíře funkcí ti nepomůže ve chvíli, kdy chatbot zapomene, o čem jste se bavili.

Proč jsou velká okna tak drahá?

Představ si, že seš na konferenci a musíš si udržet přehled o tom, kdo co řekl. S deseti lidmi je to v pohodě, zapamatuješ si, že Pepa mluvil o rozpočtu a Mařka o marketingu. Se stovkou musíš u každé nové věty zvážit, jak souvisí se vším, co řekl kdokoliv jiný. S tisícem? Nemáš šanci. Potřeboval bys mozek velikosti konferenčního sálu.

Přesně takhle funguje architektura Transformer, na které jsou postavené všechny dnešní chatboty. Při zpracování textu udržuje vazbu každého slova s každým ostatním. Složitost roste s druhou mocninou: zdvojnásobíš okno a výpočetní nároky narostou čtyřnásobně. Proto měl první ChatGPT paměť pouhé 4 000 tokenů, a proto je milionové okno tak obrovský inženýrský úspěch. Víc o tom, jak to celé funguje pod kapotou, popisuju v Jak funguje ChatGPT.

Ale rád bych něco dodal: tohle je z mého pohledu překonaná věc. U kontextu o milionech slov fakt není potřeba udržovat vazbu mezi slovem „nové“ z jedné knihy a slovem „auto“ z úplně jiné. Dostali jsme se do rozměrů, se kterými autoři původního Transformeru v roce 2017 vůbec nepočítali.

Co s tím?

Teď, když víš, jak malou paměť ChatGPT ve skutečnosti má, dávají najednou smysl všechny ty nezdařené pokusy:

Na školeních to slýchám pravidelně: někdo nahraje třicetistránkovou smlouvu a ptá se na článek 47. ChatGPT mu s klidným svědomím odpoví nesmysl, protože článek 47 už z kontextu dávno vypadl. A odpověď zní tak sebevědomě, že tomu člověk uvěří.

Pár praktických rad:

Vzpomínáš na ty lidi s dokumentací Midjourney z roku 2023? Teď víš, proč ten pátý prompt vypadal, jako by dokumentaci nikdy neviděl. Protože ji opravdu neviděl. ChatGPT na ni dávno zapomněl. A od té doby se toho zas tak moc nezměnilo.

Až příště budeš plánovat delší práci s chatbotem, vzpomeň si na toho kamaráda z hospody. Toho, co si pamatuje jen posledních pár minut. A zvaž, jestli nechceš raději kamaráda, co si pamatuje celou párty :-)