Nette\DI\ServiceCreationException – neumím založit službu presenteru

m.brecher
Generous Backer | 736
+
0
-

Ahoj,

v modelové třídě jsem chtěl přidat do metody pro získání atributu src tagu <img> i cestu $basePath a tu jsem chtěl získat z presenteru. Zkusil jsem injektnout presenter do modelové třídy pomocí DI jako službu, ale nejde mě to:

Presenter:

namespace App\AdminModule\Presenters;

final class ProjectPresenter extends AdminPresenter
{
    public function __construct(
        private ImageModel $imageModel,
    )
    {}

Model:

class ImageModel
{
    public function __construct(
        private Explorer $database,
        private ProjectPresenter $presenter,
    )
    {}

Konfigurace:

services:
	- App\Router\RouterFactory::createRouter
	- App\AdminModule\Presenters\ProjectPresenter

search:
	model:
		in: %appDir%/Model

	adminModel:
		in: %appDir%/AdminModule/Model

Přidání presenteru do služeb jsem viděl někde na fóru, doporučoval to myslím @MarekBartoš a fungovat by měl. Mě ale výše uvedený kód vyhazuje chybovou hlášku:

Nette\DI\ServiceCreationException

Service of type App\AdminModule\Model\ImageModel: Service of type App\AdminModule\Presenters\ProjectPresenter required by $presenter in ImageModel::__construct() not found. Did you add it to configuration file?

Ve službách presenter přidán je, tak bude něco vadit vytvoření služby, napadlo mě třeba:

  • presenter injektuje model a model injektuje presenter – není to nepovolená smyčka?
  • presenter očekává v konstruktoru nějaké závislosti, které je potřeba mu dodat v konfiguraci

Díky za jakékoli tipy co dělám špatně.

Vím, že $basePath mohu pohodlně přidat v šabloně, že to asi není úplně nejlepší nápad injektovat presenter do modelu, ba co víc dokonce asi antipattern, ale i když se asi použití budu snažit vyhnout, rád bych věděl co je špatně, abych konfiguraci konečně trochu porozuměl.

Marek Bartoš
Nette Blogger | 1165
+
0
-

ApplicationExtension u presenterů vypíná autowiring, protože není žádný důvod, proč bys je měl vyžadovat jako závislost ve svém kódu.

Nette base path získává z Nette\Http\IRequest a kód vypadá takto:

$baseUrl = rtrim($this->httpRequest->getUrl()->withoutUserInfo()->getBaseUrl(), '/')`
$basePath = preg_replace('#https?://[^/]+#A', '', $baseUrl);

Pro to chceš bych si spíš vytvořil presenter, napsal pro něj routu a odkazy na obrázky generoval pomocí této routy. Pokud obrázek existuje pod danou url, tak ho webserver rovnou odešle. Pokud ne, můžeš na to zareagovat – třeba i vygenerováním onoho obrázku.
A nebudeš tak muset řešit kde získat base path.


Registraci presenterů do služeb jsem doporučoval kvůli edge cases, které u automatické registrace existují. Presenter v Nette běžně služba je, ale Nette se nemusí povést všechny presentery nalézt (v závislosti na nastavení autoloadingu a dohledávání presenterů) a tak je žádoucí je registrovat do služeb manuálně.

Editoval Marek Bartoš (28. 8. 2022 14:51)

m.brecher
Generous Backer | 736
+
0
-

@MarekBartoš

Díky za podrobný návod, ano, není vhodné v modelové třídě používat presenter a dá se to udělat i bez něj. Nicméně je zde otázka proč můj výše uvedený kód skončí chybou, že služba presenteru není vytvořena. Kde jsem udělal chybu?

mystik
Člen | 289
+
0
-

Sluzba presenteru je vytvorena ale protoze ma vypnuty autowiring (coz se u presenteru udela automaticky) tak je to z pohledu injectovani zavislosti jako kdyby neexistovala. Prsnejsi by bylo ze nebyla nalezena injectovatelna service.

mystik
Člen | 289
+
0
-

A i kdyby injectovatelna byla tak by to nejspis skoncilo chybou kvuli cyklicke zavislosti mezi modelem a presenterem

m.brecher
Generous Backer | 736
+
0
-

@mystik Díky za vysvětlení, takže stačí autowiring nějak zapnout, zkusím to.

Zkusil jsem v modelových třídách cyklické injektování a skutečně to není povoleno:

Nette\InvalidStateException
Circular reference detected for services: application.5, 09, 08.

Takže když se mě podaří autowiring zapnout, mělo by to vyhodit podobnou hlášku.

mystik
Člen | 289
+
+2
-

Ja bych se o to vubec nepokousel. Nette to vypina z dobreho duvodu. Obejit to myslim nebude vubec jednoduche (nejspis by to vyzadovalo napsat vlastni DI extension) a vysledek stejne bude cyklicka zavislost a i kdyby ne tak to je hodne spatna architektura.

Sel bych se radou co napsal Marek Bartos.

Bulldog
Člen | 110
+
0
-

Kompletní souhlas s mystik

Nicméně obejít to lze velmi jednoduše a to i cyklickou závislost a i explicitní vypnutí autowiringu.
Díky tomu, že NetteDI umí krom registrace služeb je i rovnou decorator, tak můžeš zapsat něco jako:

services:
	-
		create: App\Model\Authenticator
		setup:
			- setPresenter(@application.2)  # application.2 je u mě v test appce ErrorPresenter

Nevýhoda je akorát ta, že nemáš servicu nastavenou už v konstruktoru, ale až kdo ví kdy :) (většinou těsně po vytvoření, ale u komponent kdo ví, jestli před attachem, nebo po?)

Autowiring to obejde, protože to je explicitní žádost a ne právě autowiring a ~~circular reference taky, jelikož to není povinná závislost, takže se nejdřív vše vytvoří a pak se zavolá ten set. :)~~ Tak hovno neumí to obejít circular reference :D Ale na to jsou ještě jiné metody. Každopádně mi to přijde jako chyba :D Decorator by to měl zvládat… :D

Nicméně zůstává pravda pravdou a to, že se tohle prostě nedělá. Presenter je prezentační vrstva ne modelová, takže tak jak ji chceš použít to nemá co dělat.
(pořadí presenterů se může měnit během vývoje. Tak bacha na číselko. :) )

Editoval Bulldog (29. 8. 2022 19:58)

mystik
Člen | 289
+
0
-

@Bulldog Takhle jednoduche to prave neni. Nette DI vzdy sluzbu vytvari kompletne vcetne setupu takze rozbit circular dependency neni vubec jednoduche. Kdysi se resilo jestli kvuli tomu nemit nejaky odlozen setup, ktery se vola az kdyz sluzby existuji, ale nerealizovalo se to. Takze kdyz chces rozbit circular dependency musis trochu kouzlit s DI extension.

Marek Bartoš
Nette Blogger | 1165
+
+1
-

Ani nemusíš kouzlit s DI extension, pokud se obejdeš bez kontroly při kompilaci, že služba existuje. Ale záměrně neřeknu jak. Nedávno jsem si vytvořil dost komplikovanou cyklickou závislost v DI a v logování, a i když přesně vím, co se tam děje, tak vyřešit problém správně mi zabralo několik dní. Není to nic, co by se mělo objevit jako návod na fóru.


@Bulldog Jestli dekorátorem myslíš DecoratorExtension, tak to je jen způsob, jak setup aplikovat na více služeb, vygenerovaný kód je stejný jako při konfiguraci jednotlivých služeb. Klasický decorator by problém mohl (ale nutně nemusel) vyřešit.

Presentery jsou jen nepojmenované služby. Jestli nechceš číslo, není nic snazšího než presenter do služeb registrovat sám a pojmenovat jej.

Bulldog
Člen | 110
+
0
-

mystik napsal(a):

@Bulldog Takhle jednoduche to prave neni. Nette DI vzdy sluzbu vytvari kompletne vcetne setupu takze rozbit circular dependency neni vubec jednoduche. Kdysi se resilo jestli kvuli tomu nemit nejaky odlozen setup, ktery se vola az kdyz sluzby existuji, ale nerealizovalo se to. Takze kdyz chces rozbit circular dependency musis trochu kouzlit s DI extension.

Tak obecně se dá cyklická závislost řešit velice jednoduše a to třeba accessorem:
Pokud ClassA je závislá na ClassB a naopak, tak pak třeba takto:

ClassA:

class ClassA {
	public function __construct(ClassB $classB) { }
}

ClassB:

class ClassB {
	public function __construct(ClassAAccessor $classAAccessor) { }
}

ClassAAccessor:

interface ClassAAccessor {
	public function get(): ClassA;
}

A tohle už projde (Zrovna odzkoušeno)
Taky je samozřejmě možné aby obě třídy měly takovýto accessor.
Jenom je nutné nezapomenout v tomto případě, že ClassB nesmí nad ClassAAccessorem volat metodu get už v konstruktoru, jinak by se opět jednalo o cyklickou závislost, kterou DI nepustí.

Prosím hoši je tu někde návod na to, jak ovládat fórum? Nenašel jsem nikde jak přeškrtnou text.

EDIT:
Pozor, accessor kvůli autowiringu nelze použít na Presenter. Pokud tedy je nutné to udělat s presenterem, tak tak, že presenter bude mít jako závislost accessor na službu a služba bude získávat presenter přes set metodu viz můj minulý koment..

Editoval Bulldog (29. 8. 2022 21:35)

mystik
Člen | 289
+
+1
-

@Bulldog Jo vidis mas pravdu Accessor to resi a vlastne podobne jak to my delame v DI extension. Jen to vyzaduje upravy jedne z cyklicky zavislych trid.

Bulldog
Člen | 110
+
0
-

mystik napsal(a):

@Bulldog Jo vidis mas pravdu Accessor to resi a vlastne podobne jak to my delame v DI extension. Jen to vyzaduje upravy jedne z cyklicky zavislych trid.

Přesně

m.brecher
Generous Backer | 736
+
0
-

Děkuji všem za podrobný náhled do problematiky DI. Ano, presenter je top level manager a modelové třídy musí být schopné pracovat nezávisle na něm. BasePath nakonec je asi nejsprávnější řešit v šabloně a nezatěžovat tím model.

Presentery Nette k „zneužívání“ tak trochu svádějí – umožňují přímý přístup k řadě dalších objektů a přes ně k dalším atd. Tedy tak trochu se dají „zneužít“ jako Service Lokator, což je Bad Practice. Člověk prostě musí servis locator pokušení odolat a hledat koncepčně správné řešení a dodržovat hierarchii řízení.