Nebezpečné používanie konštánt v PHP

📅   22. 06. 2021
👤   Jan Barášek

Pri používaní konštánt v PHP je potrebné mať na pamäti dve záludnosti.

Dynamické a statické konštanty

Konštantu možno v PHP definovať buď staticky priamo v triede (najlepšie riešenie), napríklad takto:

class Region
{
	public const PREFIX = 420;
}

A použitie je celkom jasné. V čase kompilácie triedy je hodnota konštanty určená a môžeme k nej pristupovať volaním názvu triedy a samotnej konštanty. Najčastejšie zápisom Region::PREFIX.

Druhý (oveľa horší spôsob) je definovať konštantu dynamicky za behu (najčastejšie niekde v konfiguračnom skripte), kde je potom niečo ako:

define('BASE_DIR', __DIR__ . '/../');

Hlavnou nevýhodou definovania konštanty pomocou funkcie define je, že skript, ktorý definuje konštantu, nemusí byť zavolaný, takže konštanta nebude existovať, keď sa ju pokúsite prečítať.

V kombinácii s použitím dynamickej konštanty v rámci definície statickej konštanty v triede to môže dokonca viesť k fatálnej chybe reflexie:

class InvoiceGenerator
{
	// To je úplne nesprávne!
	public const DATA_DIR = BASE_DIR . '/data/invoice';
}

Vysvetlenie:

Použitie dynamickej konštanty v rámci statickej konštanty má tú hlavnú nevýhodu, že hodnotu dynamickej konštanty nemožno prečítať v čase kompilácie. Tento skript sa preto musí pri každej požiadavke spracovať znova (t. j. nemôže sa uložiť do vyrovnávacej pamäte OPCache kvôli optimalizácii rýchlosti), a ak konštanta vôbec neexistovala, vyhodí sa fatálna chyba kompilácie a aplikácia sa vôbec nemôže spustiť.

Ak používate program PhpStan, môže vás na tento problém automaticky upozorniť:

Reflection error: Could not locate constant "BASE_DIR" while evaluating expression in InvoiceGenerator at line 6

Učenie:

Hodnota všetkých konštánt by mala byť vždy konštantná.

Dedičnosť konštánt pri použití statických

V niektorých prípadoch má zmysel použiť dedičnosť na prepísanie hodnoty konštanty. V takom prípade však predok nemôže (alebo by nemal) čítať hodnotu z potomka.

Príkladom je definovanie krajín a regiónov:

abstract class Region
{
	public function getPrefix(): int
	{
		// Osudová chyba!
		return static::REGION;
	}
}

final class CzechRepublic extends Region
{
	public const REGION = 420;
}

Paradoxom je, že uvedený kód nemusí nutne vyhodiť chybu, ale môže byť vyhodený nevhodným použitím dedičnosti.

Ak zavoláme metódu getPrefix() na potomkovi CzechRepublic, všetko bude správne, pretože hodnota konštanty bude načítaná správne. Ak by však potomok nenastavil hodnotu konštanty, vyhodila by sa fatálna chyba neexistujúcej konštanty. Najhoršie na celej veci je, že ide o skrytú závislosť, ktorá sa vytvára v implementácii metódy, a vývojár, ktorý triedu zdedí, o tejto závislosti nemusí ani vedieť.

Najlepším riešením v tomto prípade je buď definovať konštantu priamo v predkovi s predvolenou hodnotou (aby logika vždy prešla), alebo aspoň vyhodiť výnimku v getteri.

abstract class Region
{
	public const REGION = null;

	public function getPrefix(): int
	{
		if (static::REGION === null) {
			throw new \LogicException("Región nebol definovaný.);
		}
		return static::REGION;
	}
}

final class CzechRepublic extends Region
{
	public const REGION = 420;
}

PhpStan reaguje na túto chybu takto:

Access to undefined constant static(Region):REGION.

Jan Barášek     Viac o autorovi

Autor pracuje ako senior vývojár a softvérový architekt v Prahe. Navrhuje a spravuje veľké webové aplikácie, ktoré poznáte a používate. Od roku 2009 získal bohaté skúsenosti, ktoré odovzdáva prostredníctvom tejto webovej stránky.

Rád vám pomôžem:

Kontakt