Lazy objekty v PHP 8.4 a jejich využití v Nette DI

David Grudl
Nette Core | 8258
+
+16
-

PHP 8.4 přináší nativní podporu lazy objektů. Lazy objekt je speciální typ objektu, který odkládá svoji skutečnou inicializaci až do chvíle, kdy je potřeba – konkrétně když poprvé přistoupíme k některé jeho vlastnosti. Do té doby existuje jen jako „prázdná“ instance, která nezabírá zbytečné prostředky ani nevykonává potenciálně náročné operace z konstruktoru.

A právě tato vlastnost je skvělá pro DI kontejner v Nette. Ten běžně při startu aplikace vytváří celý strom závislostí – tedy všechny služby a jejich závislosti. To ale může být zbytečně náročné, protože ne všechny služby jsou v každém requestu skutečně potřeba.

V nové testovací verzi nette/di 3.2-dev lze zapnout lazy vytváření služeb pomocí jednoduché konfigurace:

di:
    lazy: true

Co to znamená v praxi? Když si necháte z kontejneru vytáhnout nějakou službu, třeba:

$database = $container->getByType(Database::class);

Dostanete lazy objekt, který vypadá jako normální instance Database, nepoznáte rozdíl, ale ve skutečnosti ještě nemá vytvořené připojení k databázi. K tomu dojde až v momentě, kdy s databází začnete skutečně pracovat:

$database->query('SELECT ...');  // zde teprve dojde k vytvoření připojení

Pokud v daném requestu databázi vůbec nepoužijete, ušetříte prostředky za její inicializaci. To může být významná optimalizace, zejména u větších aplikací s komplexním stromem závislostí.

Samozřejmě to má i své důsledky – případné chyby při vytváření služeb (třeba špatné přihlašovací údaje k databázi) se projeví až při prvním použití, ne hned při startu aplikace. To může být v některých případech výhoda, v jiných nevýhoda, podle konkrétního use-case.

Bude zajímavé sledovat, jaký dopad bude mít tato novinka na výkon aplikací v produkčním prostředí. Potenciál pro optimalizaci je zde značný.


Při implementaci jsem nejprve zvažoval, že by se lazy zapínalo pro jednotlivé služby. Pak mi došlo, že opravdu bomba to bude, když se to naopak zapne pro úplně všechny služby a následně umožní selektivě vypínat. Na interní PHP třídy to vliv nemá, ty nelze lazyloadovat.

Marek Bartoš
Nette Blogger | 1298
+
+5
-

Super funkce, těším se na smazání všech accesorů

Pak mi došlo, že opravdu bomba to bude, když se to naopak zapne pro úplně všechny služby a následně umožní selektivě vypínat.

A jak se to dá vypnout? V konfiguraci a v extension. Některé služby (např. obousměrných bridge pro Tracy a Monolog) nejspíš budu muset inicializovat ihned, aby se správně propojily.

Editoval Marek Bartoš (25. 11. 2024 23:33)

David Grudl
Nette Core | 8258
+
+4
-

Dá se v definici služby uvést lazy: false

ViPEr*CZ*
Člen | 821
+
0
-

nasazeno… yeah :-) good job

jerrx
Člen | 1
+
+5
-

Šlo by do tracy lišty přidat info o tom, která služba se načítá jako lazy? Teď tam tu informaci nikde nevidím a vypadá to, jako by to nefungovalo a musím to kontrolovat ve vygenerovaném containeru

David Grudl
Nette Core | 8258
+
+6
-

Uvědomil jsem si jeden zajímavý side efekt. Díky lazy loadingu jde vytvářet cyklické závislosti mezi službami, aniž by to způsobovalo problémy.

Představte si klasický případ: Služba A potřebuje službu B a služba B zároveň potřebuje službu A. V běžném DI containeru by to vedlo k vyhození výjimky „Circular reference detected“ při inicializaci. DI container smyčku detekuje, aby se PHP nezacyklilo, ale výsledkem je, že takové závislosti prostě nemůžete vytvořit.

S lazy objekty je to ale jinak. Služba A dostane při vytvoření lazy proxy služby B, která se inicializuje až v momentě, kdy A skutečně přistoupí k metodám nebo vlastnostem B. V té době už služba A existuje a může být předána službě B jako závislost.

V praxi to znamená, že můžete najednou klidně vytvořit container, který takové závislosti má a vůbec si toho nevšimnout. Je to dobře nebo špatně? Nechávám na vás. Ale může nastat problém, pokud takovou aplikaci nahrajete na server, kde je PHP < 8.4 a lazy objekty nejsou podporovány – cyklické závislosti, které dosud fungovaly, vyhodí výjimku.

MajklNajt
Člen | 513
+
0
-

David Grudl napsal(a):

Uvědomil jsem si jeden zajímavý side efekt. Díky lazy loadingu jde vytvářet cyklické závislosti mezi službami, aniž by to způsobovalo problémy.

Představte si klasický případ: Služba A potřebuje službu B a služba B zároveň potřebuje službu A. V běžném DI containeru by to vedlo k vyhození výjimky „Circular reference detected“ při inicializaci. DI container smyčku detekuje, aby se PHP nezacyklilo, ale výsledkem je, že takové závislosti prostě nemůžete vytvořit.

S lazy objekty je to ale jinak. Služba A dostane při vytvoření lazy proxy služby B, která se inicializuje až v momentě, kdy A skutečně přistoupí k metodám nebo vlastnostem B. V té době už služba A existuje a může být předána službě B jako závislost.

V praxi to znamená, že můžete najednou klidně vytvořit container, který takové závislosti má a vůbec si toho nevšimnout. Je to dobře nebo špatně? Nechávám na vás. Ale může nastat problém, pokud takovou aplikaci nahrajete na server, kde je PHP < 8.4 a lazy objekty nejsou podporovány – cyklické závislosti, které dosud fungovaly, vyhodí výjimku.

Podľa mňa tento side efekt nie je nejako zásadný, pretože

  1. ak vyvíjam aplikáciu pre PHP 8.4 a nahrám to na 8.3, a priori by som mal dostať chybu od composeru Composer detected issues in your platform: Your Composer dependencies require a PHP version ">= 8.4.0".
  2. osobne si myslím, že cyklická závislosť v správne navrhnutej architektúre nemá čo robiť

Editoval MajklNajt (17. 3. 8:20)

David Grudl
Nette Core | 8258
+
0
-

Mluvím o situaci, kdy vyvyjis aplikaci pro PHP < 8.4 a máš na vývojářském počítači PHP 8.4

MajklNajt
Člen | 513
+
0
-

@DavidGrudl Kým nezapnem lazy: true, tak sa kontajner vygeneruje bez lazy služieb a teda cyklickú závislosť / chybu uvidím, či nie?