Co musím udělat, aby se mi naplnila $basePath?
- Polki
- Člen | 553
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 | 1280
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
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
@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:
- Zruší se tím lazy načítání šablon.
- 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 | 8239
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 | 1280
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
@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 | 1280
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 | 1280
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
@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 | 1280
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
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 | 1280
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
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
@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.
- 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?
- 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?
- 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?
- 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.)
- 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?
- 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.
- 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)
- 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 | 1032
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
@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 | 8239
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 | 1280
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 | 1032
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
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 | 8239
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.