Co musím udělat, aby se mi naplnila $basePath?

Polki
Člen | 553
+
0
-

Ahoj,
nevím, kam to patří, jestli do šablon, routingu, či kam, tak to dávám sem,

Prosím o radu.
routování mám takové:

$router = new RouteList;
$router->withModule('Admin')
    ->withPath('admin')
	->addRoute('nastenka', 'Homepage:default')
    ->addRoute('prihlaseni', 'Sign:in');
return $router;

Tak trochu jsem doufal, že díky tomu budu mít v šabloně v proměnné $basePath uložen řetězec ‚admin‘. Bohužel se tak nestalo. Ať dělám co dělám, vždy je v $basePath prázdný řetězec.

Díky.

Marek Bartoš
Nette Blogger | 1161
+
+1
-

base path je cesta, ve které běží celá aplikace, ne cesta k aktuálnímu modulu. Zda jsi v administraci mi nepřijde jako dobrý nápad odvíjet od url adresy

Editoval Marek Bartoš (21. 7. 2021 9:32)

leninzprahy
Člen | 150
+
0
-

Spíš bych vycházel z toho modulu, který používáš.

Jak získáš aktuální modul je vidět třeba v contributte/application

do šablony si ho pak v presenteru pošleš třeba

public function beforeRender()
{
	$this->template->module = $this->getModuleName();
}

v šabloně pak budeš mít v proměnné $module řetězec „Admin“.

Od Nette 3 jde použít i metoda isModuleCurrent

Polki
Člen | 553
+
0
-

@MarekBartoš

Nejde o modul, ale o ->withPath(‚admin‘) na modulu mi nezáleží, jelikož ten může být pojmenován jinak. Mě jde o to, že když mám FrontModule a pak AdminModule, tak zákazníci většinou chtějí v adminu mít prefix cesty. Takže všechny stránky v adminu začínají třeba takto: <domena>/administrace/
Jenže taky v 99% případů má administrace úplně jiný design, než frontend, takže css a js soubory pro fe a pro admin chci odděleně, aby mi to nekolidovalo. Proto fe má soubory klasicky v adresáři www/css/ a www/js/ a administrace v adresáři www/administrace/css/ a www/administrace/js/.
No a jde o to, aby i v url to vypadalo přívětivě. Tedy když jsem na stránce https://domain.name/…ace/nastenka tak bych rád styly načítal z této cesty https://domain.name/…css/main.css

Aktuálně to musím dělat takto ‚{$basePath}/administrace/css/main.css‘, což se mi nelíbí, protože musím všude opakovat slovo administrace, které je prostě natvrdo v url jako prefix všech admin stránek. Proto, když Nette podporuje uložení prefixu pro routy pomocí ->withPath(‚admin‘), tak jsem myslel, že tento prefix bude nějak získatelný i v aplikaci a to konkrétně v proměnné $basePath, což se nestalo. No a to je důvod mé otázky. O to, že jsem náhodou v modulu admin nejde. Klidně můžu být v jiném. Mě jde o ten prefix.

@leninzprahy
Tvoje řešení je dobré. Nicméně neřeší, že nechci používat název modulu, ale path, která je společná pro všechny routy.
Navíc, kdybych se uchýlil k nazývání složky podle modulu, tak sice by to vyřešilo problém, ale pořád bych musel psát místo tohoto:

{$basePath}/admin/css/main.css

toto:

{$basePath}/{$module}/css/main.css

kde jsem vyměnil duplicitní admin řetězec za module proměnnou, což můj problém přetvořilo z konstanty do proměnné, ale ten název proměnné tam pořád musím psát, takže se nic nevyřešilo.

Na jiných webech jsem viděl, že to borci řeší takto:
BaseAdminPresenter:

protected function beforeRender()
{
    parent::beforeRender();
    $this->template->basePath .= '/admin/';
}

To mi ale přijde jako špatné řešení protože:

  1. Zruší se tím lazy načítání šablon.
  2. Modifikuju proměnnou, kterou nějak nastavuje Nette a to se mi nezdá správné, protože kdyby na obsahu něco dalšího nějak záviselo, tak to můžu rozbít. Proto bych ji chtěl nějak nechat naplnit automaticky a myslel jsem podle dokumentace, že metoda ‚withPath‘ právě plní proměnnou $basePath. Chápu tedy, že význam proměnné $basePath je jiný, než jsem si myslel, ale mrzí mě, že se to nedá v aplikaci modifikovat, protože pro admin modul je v mém případě ten prefix ‚admin‘ právě cesta k ‚aplikaci‘

EDIT 1:
@MarekBartoš
A co prosím myslíš, že je dobrý způsob pro rozlišování, jestli jsem v administraci, když ne URL?
Já si až do teď totiž myslel, že URL je jednoznačný identifikátor cesty k dané stránce. Tedy doména že je něco jako adresa domu, který chci jít navštívit a zbytek za doménou, že je označní místnosti, do které chci jít.

No a když zadám tedy nějakou URL do adresního řádku, tak že se přes DNS vytáhne IP adresa serveru, kam se chci připojit a tomu se předá ta ‚cesta k místnosti v domě‘ a server potom vrátí požadovaný obsah dané místnosti.

V tomto popisu, jak to chápu já, nevidím jinou možnost, než zjišťovat, v jakém jsem modulu (části domu), případně místnosti jinak, než právě z té URL.

Tipy?

Editoval Polki (21. 7. 2021 15:35)

David Grudl
Nette Core | 8105
+
+3
-

Router by tě měl od URL odstínit, takže ve zbytku aplikace bys už neměl podle URL nic detekovat. Že jsi v adminu se prostě pozná podle modulu.

Všechny proměnné v šablonách jsou čistě uživatelské, můžeš je modifikovat dle libosti. Lazyloading šablony bych vůbec neřešil, vytvoří se tak jako tak.

Marek Bartoš
Nette Blogger | 1161
+
+1
-

Případ administrace to asi nebude, ale url adresy mohou být lokalizované a když už máme routování obousměrné, tak by mělo možné je změnit bez zásahu do kódu. Ty si v kódu zavádíš na url adresách závislost.

Do routy sice parametr admin můžeš přidat (<admin=admin>), ale musel bys pak filtrovat všechny routy, aby ti parametr nemohl poslat uživatel.

Osobně mám prostě pro administraci extra presentery, dědící BaseAdminPresenter. Presentery na frontendu a v administraci nejsou 1:1 a přijde mi zbytečný dělat pro všechny rozdíly podmínky a kontrolovat u každé akce, handlu a komponenty, zda je uživatel může použít. S tou tvou strukturou si říkáš o bezpečnostní díru.

Editoval Marek Bartoš (21. 7. 2021 17:03)

Polki
Člen | 553
+
0
-

@DavidGrudl @MarekBartoš
asi mám špatné vyjadřovací schopnosti, protože mi oba nějak odpovídáte na úplně jiné věci, než na které chci znát odpověď a taky si nejspíš myslíte, že dělám něco jinak, než to ve skutečnosti dělám.
Zkusím to rozepsat.

David Grudl napsal(a):

Router by tě měl od URL odstínit, takže ve zbytku aplikace bys už neměl podle URL nic detekovat. Že jsi v adminu se prostě pozná podle modulu.

Já hlavně ani nerozumím, proč bych někde měl něco detekovat?

  • Presenter je v daném modulu a nemám jeden presenter ve více modulech, takže zde je detekování k ničemu, protože prostě hned vím, že když jsem v presenteru s namespace ‚App\AdminModule\Presenters‘, tak jsem prostě v admin modulu. Čili moduly mají vlastní presentery.
  • Formuláře a komponenty mám udělané univerzálně tak, aby šly použít v jakémkoliv modulu. To, jestli je v daném modulu můžu použít rozhoduje rozhodovací vrstva, tedy Presenter (ve kterém už vím, do kterého náleží modulu) společně s ACL (které je opět na modulech nezávislé). Tedy nějaká detekce opět postrádá smysl.
  • Modelová vrstva je taktéž univerzální. Nemá smysl, aby model věděl, v jakém se nachází modulu. Prostě má jen vykonávat dané příkazy ne? A to jestli ukládám uživatele z FrontModule, nebo z AdminModule je Modelu úplně jedno. O tom, jestli tuto akci může někdo dělat rozhoduje zase Presenter (Který už ví, v jakém modulu je.)
  • View vrstva zase jen vykreslí to, co se po ní chce. A opět je jí úplně jedno, v jakém je modulu. O to, aby byla vykreslena na správném místě se stará ten, kdo ji chce vykreslit. (Presenter/Control)

Takže nerozumím tomu, proč si myslíš, že bych chtěl něco detekovat? Mě jde jen o to, aby se mi ve složce s šablonama v Admin modulu neopakoval řetězec ‚/admin/‘ a aby byly ty šablony opravdu univerzál, tedy abych ji mohl případně přesunout do jiného projektu, kde bude obsah $basePath jiný, ale v zápisu aby to prostě vypadalo takto: ‚{$basePath}/css/main.css‘, tedy abych po předělání nemusel ve všech šablonách měnit prefix ‚/admin/‘ na třeba ‚/backend/‘

David Grudl napsal(a):

Všechny proměnné v šablonách jsou čistě uživatelské, můžeš je modifikovat dle libosti.

Jsi schopen zaručit, že to bude pravda navždy a když podle toho navrhnu aplikaci, tak nebudu stejný problém za rok, či 2 řešit na všech aplikacích znovu, protože se změní chování?

David Grudl napsal(a):

Lazyloading šablony bych vůbec neřešil, vytvoří se tak jako tak.

Jo to jsem si říkal taky. A pak jednou tady na fóru jsem se o tom zmínil, že jsem pouze viděl, jak se registrují filtry takto:

protected function beforeRender()
{
    parent::beforeRender();
    $this->template->addFilter('name', $callback);
}

a tak mě sjeli, že to mám registrovat do konfigu, abych zachoval lazy šablon, že od té doby to jinak nedělám :D
Chápu, že v beforeRender metodě už je to asi jedno, protože redirectovat by si měl v action/startupu, ale když už to dělám nějak, tak chci to mít jednotné a ne dělat vyjímky kvůli každé ptákovině.

Marek Bartoš napsal(a):

Případ administrace to asi nebude, ale url adresy mohou být lokalizované a když už máme routování obousměrné, tak by mělo možné je změnit bez zásahu do kódu. Ty si v kódu zavádíš na url adresách závislost.

Tomu rozumím a máš pravdu. V mém případě ale jde o unikátní token v URL, která logicky odlišuje části.
Pokud bych to neměl jako jednu aplikaci, ale 2, tedy jedna by byla nette aplikace, která by byla uložená ve složce <documentRoot>/admin/ a druhá třeba ve složce <documentRoot>/front/, tak nette nemá jak změnit tu URL ne? Prostě musí respektovat <domain>/admin/prihlaseni, což spustí buď aplikaci ve složce admin, nebo aplikaci ve složce front. Obdobně by se pak daly tyto složky použít, jak doména 3 řádu. V takovém případě je tento token (admin|front|apod..) neměnný. V případě, že by to bylo jak popisuju rozdělené fyzicky do složek a 2 různých aplikací, pak ta cesta za documentRootem by byla obsažena v $basePath. Moje otázka zní, proč nejde nasimulovat, aby se to chovalo stejně (tedy v $basePath) bude uložen ten token admin|front|atd., když jsem se akorát rozhodl místo 2 aplikací udělat 1. A pokud se to dá takto nasimulovat, tak jak?

Marek Bartoš napsal(a):

Do routy sice parametr admin můžeš přidat (<admin=admin>), ale musel bys pak filtrovat všechny routy, aby ti parametr nemohl poslat uživatel.

Pravda, ale to nepotřebuju. Navíc, kdybych chtěl dělat překlad, tak bych si stejně vytvořil nějaký překladový slovník asi, takže by se to stejně bralo z povolených názvů, ale to je jedno. Toto není předmětem diskuze.

Marek Bartoš napsal(a):

Osobně mám prostě pro administraci extra presentery, dědící BaseAdminPresenter. Presentery na frontendu a v administraci nejsou 1:1 a přijde mi zbytečný dělat pro všechny rozdíly podmínky a kontrolovat u každé akce, handlu a komponenty, zda je uživatel může použít. S tou tvou strukturou si říkáš o bezpečnostní díru.

Tohle jsem moc nepochopil. Já mám strukturu stejnou, jak popisuješ. Z čeho jsi prosím vyčetl, že ji mám jinou a že dělám nějaké podmínky pro rozdíly mezi frontend modulem a backend modulem? Moduly jsou u mě nezávislé jednotky, která se každá stará sama o sebe. Kdybych vzal admin modul a vyextrahoval bych jej do samostatné aplikace, tak nemusím změnit ani čárku kódu.
Jak jsi to prosím přesně myslel?

Marek Bartoš
Nette Blogger | 1161
+
+2
-

Jestliže máš presentery oddělené, tak co řešíš? V base admin presenteru můžeš mít např. toto

$this->template->assetsPath = $this->template->basePath === ''
	? 'admin'
	: $this->template->basePath . '/admin';

Zapisovat do template v beforeRender() je imho naprosto okay, stejně by v ten okamžik už mělo být jasné, že jej vytvoříš

Marek Bartoš
Nette Blogger | 1161
+
0
-

Jo to jsem si říkal taky. A pak jednou tady na fóru jsem se o tom zmínil, že jsem pouze viděl, jak se registrují filtry takto:

protected function beforeRender()
{
    parent::beforeRender();
    $this->template->addFilter('name', $callback);
}

a tak mě sjeli, že to mám registrovat do konfigu, abych zachoval lazy šablon, že od té doby to jinak nedělám :D

Když je to v beforeRender(), tak je to pro laziness imho úplně fuk. Spíš se hodí nad filtry zamyslet a vytvářat je univerzální, to pak dává smysl je mít globálně.
Pro lazy načítání nejvíc udělá všechny komponenty vytvářet přes generované factories a každou závislost, která se nepoužívá v každé execution path získávat přes accessor. To už je reálná optimalizace.

Polki
Člen | 553
+
0
-

@MarekBartoš

Jestliže máš presentery oddělené, tak co řešíš?

No jde třeba o to, že když by se pak někdy stalo, a kvůli velikosti aplikace by se rozdělila opravdu do více menších aplikací, které by byly reálně ve více složkách, tak najednou by v tomto řešení bylo v cestě admin/admin/css/main.css, což je vlastně špatná cesta, jelikož cesta k admin assetům by se nezměnila.
Tedy cesta by byla pořád admin/css/main.css, ale kvůli rozdělení aplikace by se do $basePath ten prefix dával, což by vyústilo v zdvojení admin slovíčka a tím pádem už by to nebylo plně přenositelné, protože bych musel buď změnit i cestu k assetům, nebo modifikovat ten řádek co generuje assetsPath řetězec, čímž porušuju princip rozšířitelnosti.

Nicméně pokud to jinak nejde, tak je to teda i za mě ok řešení.

a každou závislost, která se nepoužívá v každé execution path získávat přes accessor.

Můžeš dát příklad? Nedokážu si to přesně představit.

Marek Bartoš
Nette Blogger | 1161
+
0
-

Co by kdyby~ Takových případů bych měl při rozdělení aplikace do dvou minimálně stovku a rozhodně je kvůli tomu nebudu řešit. Je fajn mít možnost vzít modul a udělat z něj samostatný balík třeba jen změnou namespace a dělám to tak, ale rozdělení aplikace se nikdy neobejde beze změn a je strašně zbytečný to řešit. A zrovna tohle je banální úprava, kterou máš hotovou za dvě minuty i s hledáním.

Accessor je stejný interface jako ten který vytváříš pro generovanou factory. Jen namísto vytváření nové instance vrací existující službu a místo create() metodu pojmenuješ get(). Takže se při vytvoření presenteru předá jen lightweight obálka nad containerem a služba samotná se nevytváří, dokud ji nepotřebuješ.

David k tomu má pěkný článek https://phpfashion.com/…lazy-loading

Editoval Marek Bartoš (21. 7. 2021 21:55)

Polki
Člen | 553
+
0
-

Jasně, takže:

interface Accessor
{
  public function get(): MyControl;
}

A dělá to vlastně to, že místo vytvoření nové instance MyControlu to počítá s tím, že MyControl je služba zaregistrovaná v configu a metoda get ji vrátí z DI?

To je pro mě novinka. Já myslel, že get a create jsou zaměnitelná synonyma s tím rozdílem, že get vytvoří instanci jen jednu a poté vrací vždy tu stejnou, ale že to taky tvoří pomocí new, proto jsem get nikdy nepoužil, jelikož jsem myslel, že o tvoření services by se měl starat jen DI.

To docela dost mění smysl používání. V případě, kde používám v komponentě třeba 3 modelové třídy, ale v ajax requestu využiju jen jeden, tak je možné díky accessorům vlastně načíst jen 3 accessory a službu reálně dostat jen tu jednu, kterou potřebuju a zbylé vůbec nevytvářet.

Chápu to správně?

Jestli jo, tak jsem zvědav, jaký vliv to v určitých situacích bude mít na výkon u mých aplikací.

EDIT 1:

Co by kdyby~ Takových případů bych měl při rozdělení aplikace do dvou minimálně stovku a rozhodně je kvůli tomu nebudu řešit.

Tak jasně. Každý máme u návrhu kódu jiné priority. Nicméně můj kód vždy opravdu vypadá tak, že transformace je ctrl+c a ctrl+v a přes ctrl+r změna namespaces. To je vše a kód běží. A ač lidi říkají, že se s tím hrozně prčím, že když to dělám univerzálně, tak že to zabere moc času, tak to tak není. Například vytvoření formuláře pro změnu produktu včetně databáze, modelu a komponenty, která form vykreslí mě stojí 8 souborů po cca 30 řádcích na soubor (průměr) a 5–10 minut práce.

Takový formulář pak můžu vzít c+c a c+v, replacnout namespacy a je v jiném projektu, jako služba bla bla. Plně přenositelný a daleko rychleji napsaný, než jak to píšou kolegové. Kdybych musel pořád někde něco při přenositelnosti upravovat, tak by to pro mě moc optimální nebylo. Ale jak píšu, to už je na každém, jakou má ideologii.

Editoval Polki (21. 7. 2021 22:34)

Marek Bartoš
Nette Blogger | 1161
+
0
-

Chápeš to správně. A výkon se zvýší v závislosti na tom jak moc dobře navržená appka je. Kamarád který měl masivní non-lazy závislosti a presentery s více akcemi mluvil o –20% na zpracování v php, pro mě to byly nižší jednotky procent maximálně.

Já to že chceš přenositelnost chápu. Já mám abstrakci i na html komponenty a mapování presenterů jsem změnil tak, abych se mohl odkazovat v odkazech přímo na třídy. Například vyčlenění modulu je pak fakt jen změna namespace. Ale když aplikaci rozdělíš na dvě, tak minimálně vytváření odkazů mezi rozdělenými částmi se imho změní.

Editoval Marek Bartoš (21. 7. 2021 22:58)

Polki
Člen | 553
+
0
-

Kamarád který měl masivní non-lazy závislosti a presentery s více akcemi mluvil o –20% na zpracování v php, pro mě to byly nižší jednotky procent maximálně.

No většina mých tříd má 1 max 2 závislosti (vyjma presenterů ty někdy mají více) a ty využívám, takže nejspíš moc zrychlení nepoznám. Navíc většina requestů je podle tracy odbavena mezi 20–100ms a toto rozpětí platí i když na jedné stránce dělám F5 za sebou. Prostě někdy to hodí 37ms někdy 80..

Nicméně převzal jsem jeden projekt, který je docela špatně napsaný a doba načítání je kolem 1s. Zkusím, co to udělá tam.

Ale když aplikaci rozdělíš na dvě, tak minimálně vytváření odkazů mezi rozdělenými částmi se imho změní.

Pravda, nicméně buď linky mezi Moduly nemám vůbec, protože je appka navržená tak, aby nebyly potřeba, nebo když potřeba jsou, tak pro takové případy mám třídu, která to obhospodařuje, takže si předávám takový ‚wrapper‘ nad LinkGeneratorem a vpřípadě nutnosti překopu jen ten wrapper, takže kód zůstane nezměněn, což mi umožňuje při takové změně měnit zase jen Namespace a při nejhorším 1 třídu.

Nicméně jsem to použil zatím jen 1× tuším.

Editoval Polki (21. 7. 2021 23:52)

Polki
Člen | 553
+
0
-

@MarekBartoš
Tak jsem si trošku prostudoval ten Accessor.

Mám pár nejasností. Rozumím, jak funguje a co dělá a koukal jsem se i do vygenerovaného kódu z Nette. Funguje přesně jak jsem doufal a jak jsem si myslel.

Ovšem s tím vyvstalo několik otázek, které, zdá se se motají v kruhu.

  1. Pokud nechci vytvořit objekt napřímo, například proto, že můj UserManager potřebuje na vstup Nette\Database\Explorer, tak použiju Accessor, jelikož se může stát, že ve vykreslování uživatelského účtu budu potřebovat načíst data z databáze, ale ve volání nějaké handle metody potřebovat UserManagera nebudu, například při aktualizaci notifikací a proto není nutné si UserManagera i s Explorerem atd. vytvářet a předávat do Presenteru, jelikož ho stejně nepoužiju. Accessor je tak jasná volba.

Ovšem vygenerovaný kód Nette vypadá cca nějak takto:

	public function createServiceAccessor(): App\Model\UserManagerAccessor
	{
		return new class ($this) implements App\Model\UserManagerAccessor{
			private $container;

			public function __construct(Container_a44a95ebf0 $container)
			{
				$this->container = $container;
			}

			public function get(): App\Model\UserManager
			{
				return $this->container->getService('02');
			}
		};
	}

Otázka zní, stejně jako u generovaných továrniček: Je v pořádku, že anonymní třída bere v konsruktoru celý container?

  1. Vzpomněl jsem si na článek o Service Locatoru, který říká, že každá třída by měla v konstruktoru brát všechny závislosti, které potřebuje a taktéž pouze jen ty a žádné navíc. Není tedy Container v tomto použití právě onen Service Locator?
  2. Pokud ano, což mi připadá že je, protože potřebuju získat jen instanci 1 třídy a předávám kvůli tomu Container, který zná všechny, není to tedy špatný návrh kódu?
  3. Pokud je to špatný návrh kódu a upravíme jej takto:
	public function createServiceAccessor(): App\Model\UserManagerAccessor
	{
		return new class ($this) implements App\Model\UserManagerAccessor{
			public function __construct(
				private App\Model\UserManager $manager,
			) { }

			public function get(): App\Model\UserManager
			{
				return $this->manager;
			}
		};
	}

Tak jsme přišli o Lazy načítání a tedy není třeba využívat Accessor ne? (Tady je ta cykličnost.)

  1. Dobře tak upravíme ještě trochu jinak ten kód:
	public function createServiceAccessor(): App\Model\UserManagerAccessor
	{
		return new class ($this) implements App\Model\UserManagerAccessor{
			private ?UserManager $manager = null;

			public function __construct(
				private App\Model\UserManagerFactory $factory,
			) { }

			public function get(): App\Model\UserManager
			{
				if (!$this->manager) {
					$this->manager = $this->factory->create();
				}
				return $this->manager;
			}
		};
	}

Tím jsme dostali Accessor, který vytvoří instanci UserManagera až tehdy, je-li zavolána metoda create a místo předávání celého Containeru jsme si předali jen továrnu, která jej umí vyrobit lazy až když potřebujeme ne a taky ta továrna neumí nic jiného, takže máme všechny závislosti v konstruktoru a jen ty, které potřebujeme ano? Takže tohle by mělo být správně napsán Accessor ne?

  1. Podíváme se teď na vygenerovanou továrnu:
	public function createService01(): App\Model\UserManagerFactory
	{
		return new class ($this) implements App\Model\UserManagerFactory {
			private $container;

			public function __construct(Container_a44a95ebf0 $container)
			{
				$this->container = $container;
			}

			public function create(): App\Model\UserManager
			{
				return new App\Model\UserManager($this->container->getService('database.default.context'));
			}
		};
	}

Ale tady vidíme ten stejný problém s předáváním celého kontejneru do třídy, která ke svému fungování potřebuje jen databázový kontext.

  1. Upravíme si tedy trochu kód:
	public function createService01(): App\Model\UserManagerFactory
	{
		return new class ($this) implements App\Model\UserManagerFactory {

			public function __construct(
				private Nette\Database\Explorer $explorer,
			) {	}

			public function create(): App\Model\UserManager
			{
				return new App\Model\UserManager($this->explorer);
			}
		};
	}

Tímto jsme vyřešili opět problém Service Locatoru. Ale.. Udělali jsme to, že Presenterem žádaný Accessor potřebuje tuto továrnu, která potřebuje Explorer, aby jej mohla předat do nové instance UserManagera a tedy se jen přetavila nutná závislost z UserManagera načítaného z jako přímou servicu do Presenteru do UserManagerFactory a tedy se stejně Explorer vytvoří i když UserManagera a tedy metodu get od Accessoru nepoužijeme a dosáhli jsme akorát zpomalení (navíc 2 třídy) a více jsme se napsali. (Zase cykličnost)

  1. Ok tak jaké je řešení? Můžeme Explorer taktéž obalit do nějakého Accessoru/Továrny:
	public function createService01(): App\Model\UserManagerFactory
	{
		return new class ($this) implements App\Model\UserManagerFactory {

			public function __construct(
				private Nette\Database\ExplorerAccessor $explorerAccessor,
			) {	}

			public function create(): App\Model\UserManager
			{
				return new App\Model\UserManager($this->explorer->get());
			}
		};
	}

Problém vyřešen. Zdánlivě. Kromě toho, že je nutno přegenerovat všechny služby, které načítá služba, která je dodávaná Accessorem tak, aby taky měly svůj Accessor, tak jsme ještě museli vygenerovat UserManagerFactory, který nikde jinde v aplikaci nevyužijeme a taky nám umožňuje vygenerovat více instancí UserManagera, což může být někdy nechtěné chování, takže továrničku nemusíme chtít. (továrna může evokovat, že je u služby v pohodě generovat tuny jejích instancí ±)

Hlavní otázka tedy zní, jestli je v případě Accessoru v pohodě používat Service Locator, nebo by správný návrh měl vypadat tak, jak jsem popsal výše a jestli to není spíše kontraproduktivní?
Díky.

EDIT 1:
Jde mi o to, kdybych někdy potřeboval z nějakého důvodu vytvořit Accessor ručně, tak jestli si můžu v klidu načíst celý Container, nebo to musím nějak složitě řešit, nebo jestli by nebylo lepší se v takovém případě na Accessor vyprdnout a neudělat nějaký wrapper/adapter či tak něco.

Editoval Polki (22. 7. 2021 23:16)

jiri.pudil
Nette Blogger | 1028
+
+3
-

Myslím, že to moc řešíš :) ano, service locator je hodně bad practice, pokud ho používáš v aplikačním kódu. Accessor ani továrna ale není tak docela aplikační kód, jsou to jenom pomocné třídy na úrovni kontejneru. Tam mi připadá takové použití úplně v pořádku, protože pořád zůstává zapouzdřené uvnitř a navenek do aplikace (tj. metoda get resp. create) je všechno transparentní.

Polki
Člen | 553
+
0
-

@jiripudil
Tak ale tohle tvrzení si dokážu představit aplikovat univerzálně skoro na všechno. Jako Když budu mít třeba třídu Customer a každý bude mít peněženku Wallet A zákazník bude mít v rozhraní jen metody getName a doPayment(float $amount), tak je ta peněženka v uživateli taky zapouzdřená, vně z aplikace nevidím, jak ji zákazník používá a jak ji uživatel získal může být zbytku aplikace jedno, pokud se o dodávání instance zákazníka stará nějaká služba. V ten moment o tom, jak zákazník získá peněženku ví jen služba co zákazníka tvoří (klidně továrna) a sám zákazník. Ale je to zapouzdřeno uvnitř a navenek aplikace co.

Znamená to, že si může zákazník taky tahat peněženku, nebo její továrnu apod. ze Service Locatoru?

Ale jinak máš pravdu, že to moc řeším. Nejsi první, kdo mi to říká/píše :D
Bohužel jsem perfekcionista a proto když vím, jak něco udělat správně, tak se to tak snažím dělat bez výjimek.
Dost často jsem dělal na projektech v týmech, kde lidi říkali, že danou věc moc řeším, že je to zbytečný overkill na kódování apod. a že to nemám dělat, protože to zabere o trochu času navíc. A když jsem je poslechl, tak bylo pravidlem, že nadešla chvíle, kdy se musela hromada kódu překopávat do mého původního řešení, nebo se musely dělat jiné hacky, protože kvůli nedodržování postupů se kód spatlal tak, že se s každou další přidanou věcí někde něco třeba rozpadlo, nebo něco nebylo možné udělat vůbec.

Typickým příkladem je neprogramovat oproti třídám, ale oproti rozhraní, kdy lidi jsou líní dělat rozhraní pro každou závislost každé třídy. Výsledkem je to, že na vstup berou třídy, které umění daleko více věcí, než daná třída potřebuje. Toto je ale ještě v pohodě. Prostě vyžaduješ něco, co nevyužiješ, ale to vem čert. Přece kdykoliv můžeš třídu změnit za rozhraní a upravit to ne?
Jasně…
Takže je napsána aplikace, kde všechny třídy jsou natvrdo svázané závislostmi s jinými třídami, protože se programuje proti třídám ne proti rozhraní. A teď jdeš psát testy. A zjistíš, že někteří devs v týmu uznávají pravidlo, že třída by měla být buď abstract, nebo final. A najednou začneš řešit problém.
Týjo třída Customer bere na vstup třídu Wallet, která se připojuje na bankovní účet strhává peníze. Já ale nechci si nechat strhnout peníze. Chci jen otestovat třídu Customer, jestli volá co má jak má a kdy má. Takže si chci třídu Wallet mocknout. Ups.. Třída Wallet je final…
A co teď? Budeš kvůli testům překopávat kód, aby nebyly třídy final a hádat se se zastáncem principu abstract/final, že prostě potřebuješ tu třídu mocknout, takže z ní dědit, nebo si nad všema takovejma třídama vytvořit interface/wrapper, který nebude final… Prostě najednou tuna práce navíc, která by nebyla, kdyby se kód navrhoval od začátku správně podle všech doporučení a striktně.
Nette toto obešlo pomocí Environment::bypassFinals() Ale to mi přijde jako vyhánět čerta ďáblem. Prostě ohnout si pravidlo, abych to mohl dělat špatně je podle mě špatný přístup.

Rozumím, že třeba konkrétně factory/accessor nebudu testovat, takže je jedno, že si tam předávám celý container, ale co když budu potřebovat nějakou abstractFactory/abstractAccessor, která/ý se až konkrétně podle nějakých podmínek bude rozhodovat, jakou konkrétní instanci mi vytvoří/předá? Tady už testy potřebovat možná budu, protože nechci, aby se stalo, že dostanu jinou instanci než chci. A tady už bych přemýšlel, jestli je Ok předat celý container a jestli je v pohodě ho mocknout.

David Grudl
Nette Core | 8105
+
+2
-

Znamená to, že si může zákazník taky tahat peněženku, nebo její továrnu apod. ze Service Locatoru?

Ne, nikde jako závislost nepoužívej kontejner nebo service locator. Od toho jsou accessory a továrny, aby nebyl žádný důvod jej používat.

Marek Bartoš
Nette Blogger | 1161
+
0
-

co když budu potřebovat nějakou abstractFactory/abstractAccessor, která/ý se až konkrétně podle nějakých podmínek bude rozhodovat, jakou konkrétní instanci mi vytvoří/předá?

Výš jsi psal, zda použití containeru v accessoru není service locator a teď polemizuješ, co když budeš potřebovat accessor použít jako service locator? :D Problém service locatoru vůbec není v tom, že by interně používal celý DIC, ale v tom, že se nekonfiguruju z vnější. Co ti accessor vrátí dovedeš ovlivnit v konfiguraci, nejde o SL.

Nerozumíš tomu, co service locator je a proč je špatně a bez toho lepší řešení prostě nenavrhneš.

jiri.pudil
Nette Blogger | 1028
+
0
-

Tak ale tohle tvrzení si dokážu představit aplikovat univerzálně skoro na všechno.

A to zase ne. Tím zapouzdřením jsem myslel „zapouzdřené uvnitř kontejneru“ spíš než „zapouzdřené uvnitř téhle jedné konkrétní třídy“.

Znamená to, že si může zákazník taky tahat peněženku, nebo její továrnu apod. ze Service Locatoru?

Podstatný rozdíl je v tom, že zákazník je aplikační/doménový/core kód. Továrna či accessor nikoliv, to je jenom wiring, assembly code, implementační detail kontejneru. Doporučuji přečíst tenhle článek, řekl bych, že popisuje tyhle koncepty lepším způsobem, než jsem schopný vyprodukovat takhle při pátečním odpoledni já :)

Polki
Člen | 553
+
0
-

David Grudl napsal(a):

Znamená to, že si může zákazník taky tahat peněženku, nebo její továrnu apod. ze Service Locatoru?

Ne, nikde jako závislost nepoužívej kontejner nebo service locator. Od toho jsou accessory a továrny, aby nebyl žádný důvod jej používat.

No jasně. Proto se ptám, jestli je v pohodě když budu muset napsat vlastní továrnu/accessor, tak v té továrně mít závislost na kontejneru.
Pokud ne, tak jak by vypadal accessor s metodou get(): DbDriver, kdy v případě, že je vše ok, tak vrátí instanci třídy class ElasticDbDriver implements DbDriver a když je server elasticu mimo provoz, tak vrátí instanci class MySqlDbDriver implements DbDriver. To, jestli je server mimo provoz pro jednoduchost můžeme mít předáváno jako flag do accessoru.

Marek Bartoš napsal(a):

Nerozumíš tomu, co service locator je a proč je špatně a bez toho lepší řešení prostě nenavrhneš.

Spíš mi přijde, že si navzájem nerozumíme my 2. To, co píšeš je úplně něco jiného, než na co jsem se ptal. Vůbec neřeším, jestli accessor není SL, ale jestli je v pohodě uvnitř accessoru/továrny SL používat a pokud ne, tak jak do Accessoru dostat závislosti, které má vracet. (Prosím nepiš, že to nemám řešit, protože od toho jsou generované továrničky. To už jsem právě řešil výše :D Nastanou situace, kdy si musíš factory napsat ručně a generovaná továrnička/accessor prostě nestačí.)

jiri.pudil napsal(a):

A to zase ne. Tím zapouzdřením jsem myslel “zapouzdřené uvnitř kontejneru” spíš než “zapouzdřené uvnitř téhle jedné konkrétní třídy”.

Jo to dává už větší smysl. Chápu, že uvnitř DIC je to jedno, protože jiná implementace by byla složitá a navíc je to autogenerated kód, který nemám jak ovlivnit a ani do něj přistoupit, takže v případě DIC je to OK. Mě jde ale o vlastní implementaci a tam pořád váhám.

Doporučuji přečíst tenhle článek

Mrknu na to díky.

Editoval Polki (23. 7. 2021 16:41)

David Grudl
Nette Core | 8105
+
0
-

No jasně. Proto se ptám, jestli je v pohodě když budu muset napsat vlastní továrnu/accessor, tak v té továrně mít závislost na kontejneru. Pokud ne, tak jak by vypadal accessor s metodou get(): DbDriver, kdy v případě, že je vše ok, tak vrátí instanci třídy class ElasticDbDriver implements DbDriver a když je server elasticu mimo provoz, tak vrátí instanci class MySqlDbDriver implements DbDriver. To, jestli je server mimo provoz pro jednoduchost můžeme mít předáváno jako flag do accessoru.

Uděláš továrnu, která má jako závislosti accessor pro elasic driver a accessor pro Mysql driver.

Polki
Člen | 553
+
0
-

A s Accessorem to bude stejně že? Prostě navrhnu Accessor, který bude brát jako závislosti tyto 2 další accessory, které už budou předgenerované Nette DI