phpFashion

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

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?

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:

  • Změny v aplikační logice v průběhu času (např. přechod z jednoho ORM na jiné)
  • Různé týmy nebo programátoři používající různé konvence
  • Buggy migrace dat při slučování databází
  • Legacy kód, který se chová jinak než nový
  • Chyby v aplikaci, které občas propustí prázdný řetězec místo NULL nebo naopak

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

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?

Představte si, že by vaše PHP objekty mohly být čistší, přehlednější a lépe použitelné. Dobrá zpráva – už nemusíte snít! PHP 8.4 přichází s revoluční novinkou v podobě property hooks a asymetrické viditelnosti, které kompletně mění pravidla hry v objektově orientovaném programování. Zapomeňte na neohrabané gettery a settery – konečně máme k dispozici moderní a intuitivní způsob, jak kontrolovat přístup k datům objektů. Pojďme se podívat na to, jak tyto novinky mohou změnit váš kód k nepoznání.

Property hooks představují promyšlený způsob, jak definovat chování při čtení a zápisu vlastností objektu – a to mnohem čistěji a výkonněji než dosavadní magické metody __get/__set. Je to jako byste dostali k dispozici sílu magických metod, ale bez jejich typických nevýhod.

Podívejme se na jednoduchý příklad z praxe, který vám ukáže, proč jsou property hooks tak užitečné. Představme si běžnou třídu Person s veřejnou property age:

class Person
{
	public int $age = 0;
}

$person = new Person;
$person->age = 25;  // OK
$person->age = -5;  // OK, ale to je přece nesmysl!

PHP sice díky typu int zajistí, že věk bude celé číslo (to lze od PHP 7.4), ale co s tím záporným věkem? Dříve bychom museli sáhnout po getterech a setterech, property by musela být private, museli bychom doplnit spoustu kódu… S hooks to vyřešíme elegantně:

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

$person->age = -5;  // Ups! InvalidArgumentException nás upozorní na nesmysl

Krása tohoto řešení spočívá v jeho jednoduchosti – navenek se property chová úplně stejně jako dřív, můžeme číst i zapisovat přímo přes $person->age. Ale máme plnou kontrolu nad tím, co se při zápisu děje. A to je teprve začátek!

Můžeme jít ještě dál a vytvořit třeba hook pro čtení. Hookům lze přidat atributy. A samozřejmě mohou obsahovat složitější logiku než jednoduchý výraz. Podívejte se na tento příklad práce se jménem:

class Person
{
	public string $first;
	public string $last;
	public string $fullName {
		get {
			return "$this->first $this->last";
		}
		set(string $value) {
			[$this->first, $this->last] = explode(' ', $value, 2);
		}
	}
}

$person = new Person;
$person->fullName = 'James Bond';
echo $person->first;  // vypíše 'James'
echo $person->last;   // vypíše 'Bond'

A něco důležitého: kdykoliv se přistupuje k proměnné (i uvnitř samotné třídy Person), vždy se využijí hooks. Jediná výjimka je přímý přístup k reálné proměnné uvnitř kódu samotného hooku.

Ohlédnutí do minulosti: Co nás naučil SmartObject?

Pro uživatele Nette může být zajímavé ohlédnout se do minulosti. Framework totiž podobnou funkcionalitu nabízel už před 17 lety ve formě SmartObject, který výrazně vylepšoval práci s objekty v době, kdy PHP v této oblasti značně zaostávalo.

Pamatuju si, že tehdy přišla vlna bezbřehého nadšení, kdy se properties používaly prakticky všude. Tu pak vystřídala vlna opačná – nepoužívat je nikde. Důvod? Chybělo jasné vodítko, kdy je lepší použít metody a kdy property. Ale dnešní nativní řešení je kvalitativně úplně jinde.Property hooks a asymetrická viditelnost jsou plnohodnotné nástroje, které nám dávají stejnou úroveň kontroly jako máme u metod. Proto dnes můžeme mnohem lépe rozlišit, kdy je property skutečně tím správným řešením.

…pokračování


Readonly vlastnosti v PHP a jejich skrytá úskalí

Představte si, že byste mohli svým datům dát pevnou půdu pod nohama – jednou je nastavíte a pak si můžete být jistí, že je nikdo nezmění. Přesně to přineslo PHP 8.1 s readonly vlastnostmi. Je to jako dát vašim objektům neprůstřelnou vestu – chrání jejich data před nechtěnými změnami. Pojďme se podívat, jak vám tento mocný nástroj může usnadnit život a na co si při jeho používání dát pozor.

Začněme jednoduchým příkladem:

class User
{
    public readonly string $name;

    public function setName(string $name): void
    {
        $this->name = $name;  // První nastavení - vše OK
    }
}

$user = new User;
$user->setName('John');      // Paráda, máme jméno
echo $user->name;            // "John"
$user->setName('Jane');      // BOOM! Výjimka: Cannot modify readonly property

Jakmile jednou jméno nastavíte, je to jako vytesané do kamene. Žádné náhodné přepsání, žádné nechtěné změny.

Kdy je uninitialized opravdu uninitialized?

Často se setkávám s mýtem, že readonly vlastnosti musí být nastaveny v konstruktoru. Ve skutečnosti je PHP mnohem flexibilnější – můžete je inicializovat kdykoliv během života objektu, ale pouze jednou! Před prvním přiřazením jsou ve speciálním stavu ‚uninitialized‘, což je takový limbo stav mezi nebytím a bytím.

A tady přichází zajímavý detail – readonly vlastnosti nemohou mít výchozí hodnotu. A proč? Kdyby měly výchozí hodnotu, staly by se de facto konstantami – hodnota by byla nastavena při vytvoření objektu a už by nešla změnit.

Vyžadují se typy

Readonly proměnné vyžadují explicitní definici datového typu. Je to proto, že stav ‚uninitialized‘, který využívají, existuje pouze u typovaných proměnných. Bez uvedení typu tedy readonly proměnnou nelze definovat. Pokud si nejste jistí typem, můžete použít mixed.

…pokračování


Dvě slova, co ničí open source

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.

  • „Nemám čas na to napsat opravu.“
  • „Nemám čas připravit ukázku s chybou.“
  • „Tohle by mělo být v dokumentaci, ale nemám čas to napsat.“

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:

  • Najít ten čas. Věřte mi, existuje. Možná je schovaný mezi epizodami vašeho oblíbeného seriálu nebo mezi scrollováním na sociálních sítích.
  • Nabídnout řešení. Nemusíte psát rovnou patch. Stačí ukázat, že jste řešení problému fakt promýšleli.
  • Motivovat správce open source, aby se vaším issue zabývali. Třeba tím, že ukážete, jak bude úprava užitečná nejen pro vás, ale i pro celé lidstvo a přilehlý vesmír.

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

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.


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

Vývoj softwaru často přináší dilema. Například jak řešit situace, kdy getter nemá co vrátit. V tomto článku prozkoumáme tři strategie pro implementaci getterů v PHP, které ovlivňují strukturu a čitelnost kódu, a každá má své specifické výhody i nevýhody. Pojďme se na ně podrobněji podívat.

Univerzální getter s parametrem

Prvním a v Nette používaným řešením je vytvoření jediné getter metody, která, pokud hodnota není dostupná, může dle potřeby vrátit buď null nebo vyhodit výjimku. O chování rozhoduje volitelný parametr. Zde je příklad, jak by mohla metoda vypadat:

public function getFoo(bool $need = true): ?Foo
{
    if (!$this->foo && $need) {
        throw new Exception("Foo not available");
    }
    return $this->foo;
}

Hlavní výhodou tohoto přístupu je, že eliminuje potřebu mít několik verzí getteru pro různé scénáře použití. Někdejší nevýhodou byla horší srozumitelnost uživatelského kódu používajícího booleovské parametry, ale ta padla s příchodem pojmenovaných parametrů, kdy lze psát getFoo(need: false).

Dále tento přístup může způsobit komplikace v oblasti statické analýzy, jelikož dle signatury se zdá, že getFoo() může vrátit null v každé situaci. Nicméně nástroje jako PHPStan umožňují explicitní dokumentaci chování metody pomocí speciálních anotací, které zlepšují porozumění kódu a jeho správnou analýzu:

/** @return ($need is true ? Foo : ?Foo) */
public function getFoo(bool $need = true): ?Foo
{
}

Tato anotace jasně určuje, jaké návratové typy může metoda getFoo() generovat v závislosti na hodnotě parametru $need. Ale například PhpStorm jí nerozumí.

Dvojice metod: hasFoo() a getFoo()

Další možností je rozdělit zodpovědnost na dvě metody: hasFoo() pro ověření existence hodnoty a getFoo() pro její získání. Tento přístup zvyšuje přehlednost kódu a je intuitivně srozumitelný.

public function hasFoo(): bool
{
    return (bool) $this->foo;
}

public function getFoo(): Foo
{
    return $this->foo ?? throw new Exception("Foo not available");

Hlavním problémem je redundance, zvláště v případech, kdy je kontrola dostupnosti hodnoty sama o sobě náročným procesem. Pokud hasFoo() provádí složité operace k ověření, zda je hodnota dostupná, a tato hodnota je poté opět získávána pomocí getFoo(), dojde k jejich opětovnému provedení. Hypoteticky může být stav objektu nebo dat změněn mezi voláním hasFoo() a getFoo(), což může vést k nesrovnalostem. Z uživatelského pohledu může být tento přístup méně pohodlný, protože nás nutí volat dvojici metod s opakujícím se parametrem. A nemůžeme využít například null-coalescing operátor.

Výhodou je, že některé nástroje pro statickou analýzu umožňují definovat pravidlo, že po úspěšném volání hasFoo() nedojde v getFoo() k vyhození výjimky.

Metody getFoo() a getFooOrNull()

Třetí strategií pro je rozdělení funkcionality na dvě metody: getFoo() pro vyhození výjimky, pokud hodnota neexistuje, a getFooOrNull() pro vrácení null. Tento přístup minimalizuje redundanci a zjednodušuje logiku.

public function getFoo(): Foo
{
    return $this->getFooOrNull() ?? throw new Exception("Foo not available");
}

public function getFooOrNull(): ?Foo
{
    return $this->foo;
}

Alternativou je dvojice getFoo() a getFooIfExists(), ale v tomto případě nemusí být zcela intuitivní pochopit, která metoda vyhazuje výjimku a která vrací null. O trošku výstižnější by byla dvojice getFooOrThrow() a getFoo(). Další možností je getFoo() a tryGetFoo().

Každý z představených přístupů k implementaci getterů v PHP má své místo v závislosti na specifických potřebách projektu a preferencích vývojářského týmu. Při výběru vhodné strategie je důležité zvážit, jaký dopad bude mít na čitelnost, údržbu a výkon aplikace. Volba by odrážet snahu o co nejsrozumitelnější a nejefektivnější kód.


Lze pomocí regulárních výrazů parsovat HTML?

Pojďme jednou provždy rozlousknout tuhle věčnou otázku, která rozděluje komunitu programátorů. Rozhodl jsem se ponořit do temných vod regulárních výrazů, abych přinesl odpověď (spoiler: ano, je to možné).

Takže, co vlastně HTML dokument obsahuje? Jde o mix textu, entit, značek, komentářů a speciální značky doctype. Prozkoumejme nejprve každou ingredienci zvlášť.

Entity

Základem HTML stránky je text, který tvoří obyčejné znaky a speciální sekvence zvané HTML entity. Ty mohou být buď pojmenované, jako je &nbsp; pro nezlomitelnou mezeru, nebo číselné, a to buď v desítkovém &#160; nebo šestnáctkovém &#xA0; formátu. Regulární výraz, který zachytí HTML entitu, by vypadal takto:

(?<entity>
	&
	(
		[a-z][a-z0-9]+     # pojmenovaná entita
		|
		\#\d+              # desítkové číslo
		|
		\#x[0-9a-f]+       # hexadecimální číslo
	)
	;
)

Všechny regulární výrazy jsou zapsány v režimu extended, ignorují velikost písmen a tečka představuje jakýkoliv znak. Tj. modifikátor six.

Značky

Tyto ikonické prvky dělají HTML tím čím je. Tag začíná <, následuje název tagu, možná sada atributů a uzavírá se > nebo />. Atributy mohou mít volitelnou hodnotu a ta může být uvozena do dvojitých, jednoduchých nebo žádných uvozovek. Regulární výraz zachytávající atribut by vypadal takto:

(?<attribute>
	\s+                         # alespoň jeden bílý znak před atributem
	<a href="#fns" class="footnote">[\s&quot;&#039;&lt;&gt;=`/]</a>+               # název atributu
	(
		\s* = \s*               # rovnítko před hodnotou
		(
			"                   # hodnota uzavřená ve dvojitých uvozovkách
				(
					<a href="#fn" class="footnote">[&quot;]</a>        # libovolný znak kromě dvojité uvozovky
					|
					(?&entity)  # nebo HTML entita
				)*
			"
			|
			'                   # hodnota uzavřená v jednoduchých uvozovkách
				(
					<a href="#fn" class="footnote">[&#039;]</a>        # libovolný znak kromě uvozovky
					|
					(?&entity)  # nebo HTML entita
				)*
			'
			|
			<a href="#fns" class="footnote">[\s&quot;&#039;&lt;&gt;=`]</a>+         # hodnota bez uvozovek
		)
	)?                           # hodnota je volitelná
)

Všimněte si, že se odvolávám na pojmenovanou skupinu entity definovanou dříve.

Elementy

Element může představovat jak samostatná značka (tzv. prázdný element), tak značky párové. Existuje pevný výčet jmen prázdných elementů, podle kterých je rozeznáme. Regulární výraz pro jejich zachytávání by vypadal takto:

(?<void_element>
	<                  # začátek značky
	(                  # název elementu
		img|hr|br|input|meta|area|embed|keygen|source|base|col
		|link|param|basefont|frame|isindex|wbr|command|track
	)
	(?&attribute)*     # volitelné atributy
	\s*
	/?                 # volitelné /
	>                  # konec značky
)

Ostatní značky jsou tedy párové a zachytí je tento regulární výraz (používám v něm odvolávku na skupinu content, kterou teprve nadefinujeme):

(?<element>
	<                  # počáteční značka
	(?<element_name>
		[a-z]<a href="#fns" class="footnote">[\s/&gt;]</a>*  # název elementu
	)
	(?&attribute)*     # volitelné atributy
	\s*
	>                  # konec počáteční značky
	(?&content)*
	</                 # koncová značka
	(?P=element_name)  # zopakujeme název elementu
	\s*
	>                  # konec koncové značky
)

Speciálním případem jsou elementy jako <script>, jejichž obsah se musí zpracovávat odlišně od ostatních elementů:

(?<special_element>
	<                  # počáteční značka
	(?<special_element_name>
		script|style|textarea|title  # název elementu
	)
	(?&attribute)*     # volitelné atributy
	\s*
	>                  # konec počáteční značky
	(?>                # atomická skupina
		.*?            # nejmenší možný počet jakýchkoliv znaků
		</             # koncová značka
		(?P=special_element_name)
	)
	\s*
	>                  # konec koncové značky
)

Líný kvantifikátor .*? zajistí, že se výraz zastaví u první ukončovací sekvence, a atomická skupina zajistí, že toto zastavení bude definitivní.

Komentáře

Typický HTML komentář začíná sekvencí <!-- a končí sekvencí -->. Regulární výraz pro HTML komentáře může vypadat takto:

(?<comment>
	<!--
	(?>           # atomická skupina
		.*?       # nejmenší možný počet jakýchkoliv znaků
		-->
	)
)

Líný kvantifikátor .*? opět zajistí, že se výraz zastaví u první ukončovací sekvence, a atomická skupina zajistí, že toto zastavení bude definitivní.

Doctype

Jde o historický relikt, který dnes existuje jen proto, aby přepnul prohlížeč do tzv. standardního režimu. Obvykle vypadá jako <!doctype html>, ale může obsahovat i další znaky. Zde je regulární výraz, který jej zachytí:

(?<doctype>
	<!doctype
	\s
	<a href="#fn" class="footnote">[&gt;]</a>*         # jakékoliv znaky kromě '>'
	>
)

Dejme to dohromady

Když máme hotové regulární výrazy zachytávající každou část HTML, je čas vytvořit výraz pro celý HTML 5 dokument:

\s*
(?&doctype)?              # volitelný doctype
(?<content>
	(?&void_element)      # prázdný element
	|
	(?&special_element)   # speciální element
	|
	(?&element)           # párový element
	|
	(?&comment)           # komentář
	|
	(?&entity)            # entita
	|
	<a href="#fn" class="footnote">[&lt;]</a>                  # znak
)*

Všechny části můžeme spojit do jednoho komplexního regulárního výrazu. Tohle je on, superhrdina mezi regulárními výrazy se schopností parsovat HTML 5.

Závěrečné poznámky

I když jsme si ukázali, že HTML 5 lze parsovat pomocí regulárních výrazů, uvedený příklad k ničemu užitečný není. Nepomůže vám se zpracováním HTML dokumentu. Vyláme si zuby u nevalidního dokumentu. Bude pomalý. A tak dále. V praxi se používají spíš reguláry jako je tento (pro hledání URL obrázků):

<img.+?src=["'](.+?)["'].*?>

Ale to je opravdu velmi nespolehlivé řešení, která vede k chybám. Tento regexp chybně matchuje třeba custom tagy jako například <imgs-tag src="image.jpg">, custom atributy jako <img data-src="custom info">, nebo selže, když atribut bude obsahovat uvozovku <img src="mcdonald's.jpg">. Proto je doporučeno používat specializované knihovny. Ve světě PHP máme smolíčka, protože rozšíření DOM podporuje pouze pravěké ztrouchnivělé HTML 4. Naštěstí PHP 8.4 slibuje parser pro HTML 5.


Když Copilot ztratí směr aneb oslava fušeřiny

Video od Microsoftu, které mělo být oslnivou ukázkou možností Copilota, je spíš tragikomickou prezentací úpadku programátorského řemesla.

Mluvím o tomto videu. Má demonstrovat možnosti GitHub Copilota, mimo jiné jak pomocí něj napsat regulární výraz pro vyhledávání značek <img> s třídou hero-image. Jenže už původní kód, který upravují, je děravý jako švýcarský sýr, já bych se za něj styděl. Copilot se nechá strhnout a místo opravy pokračuje ve stejném duchu.

Výsledkem je regulární výraz, který nezamýšleně matchuje i jiné třídy, jiné značky, jiné atributy a tak dále. Ba co víc, selže, pokud je atribut src uveden před class.

Píšu o tom, protože tato demonstrace fušeřiny, zejména vzhledem k oficiální povaze videa, je zarážející. Jak je možné, že si toho nevšiml ani jeden z prezentujících či jejich kolegů? Nebo si toho všimli a řekli si, že o nic nejde? To by bylo ještě smutnější. Výuka programování vyžaduje preciznost a důslednost, bez nichž se snadno mohou propagovat nesprávné praktiky. Video mělo být oslavou programátorského umění, ale já v něm vidím ponurou ukázku, jak se úroveň programátorského řemesla propadá do propasti nedbalosti.

Ale ať nejsem jenom negativní: je tam hezky ukázáno, jak funguje Copilot a v něm GPT, takže si je určitě pusťte 🙂


phpFashion © 2004, 2025 David Grudl | o blogu

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