Potlačení kruhovych závislostí při vytváří služeb v DI
- hAssassin
- Člen | 293
Ahoj, napsal jsem si vlastní jednoduchou extenzi pro config. Je to extenze pro model a repozitáře.
Jelikož v repozitářích zpracovávám vše
(vytvoření/aktualizace/mazání entity) tak občas potřebuju injektnout jeden
repozitář do druhýho. A extenze proto, abych pořád nemusel v configu
psát setup: [injectRepos1, injectRepos2, ...]
.
Do teď všechno funguje super. Problém ale nastane, pokud vytvořím
kruhovou závislost mezi repozitářema. Pak vyskakuje „nečekaná“ vyjímka
Circular reference detected for services. Je to logický
protože se setup()
volá hned při vytvoření instance první
třídy, ale v té době ještě není vytvořena instance druhé třídy,
která se má injektnout do té první. A tak se začne vytvářet, ale chce
instanci první třídy, aby si ji mohla injektnout k sobě, ale ta ještě
není hotová… Je to jasný proč to dělá. Otázka z ní: jde
z toho nějak ven? Ideálně rozšířením extenze:
protected function createServiceRepos1()
{
$service = new Repository1(...);
if (!$service instanceof Repository1) {
throw new Nette\UnexpectedValueException('...');
}
//>> TADY neco udelat aby se prave vytvorena sluzba pridala do stromu,
// aby volani $this->getService('repos1') nehodilo vyjimku.
$service->injectRepository2($this->getService('repos2'));
return $service;
}
Co sem koukal, tak to asi moc nepůjde, protože si to kontejner hlídá,
takže volání addService()
nepomůže.
Jediný rozumný na co sem přišel by bylo nevolat addSetup()
,
ale přesunout to až do afterCompile() do metody initialize()
. To
je ovšem problém, protože sice budu mít „cyklický“ repozitáře, ale za
cenu toho, že se mi všechny vytvoří najednou, takže nejen že to nebude
lazy, bude to megaNElazy :-(
Nedá se to tedy pořešit nějak inteligentně?
- Jan Tvrdík
- Nette guru | 2595
@hAssassin: Vidím dvě cesty, jak z toho ven:
- Akademická – uprav si model, aby neměl kruhové závislosti
- Pragmatická – vytvoř si service locator a injektni ho do všech repozitářů (při injektnutí se repozitář může automaticky do service locatoru zaregistrovat)
- hAssassin
- Člen | 293
@Jan Tvrdík > ahoj, díky za odpověď. Model bez kruhových závislotí by byl sice pěkný, ale momentálně to nevidím reálně. Ta druhá možnost je lepší. Ale nevím jestli to chápu přesně:
- Byl by service locator, který by se předával do všech repozitářů třeba v konstruktoru a hnedle by se do něho repozitář registroval.
- Pokud byl bylo potřeba pracovat s jiným repozitářem, tahal by se ze service locatoru.
- Úplně by tedy z repozitářů zmizely inject* metody.
<?php
class Repository1 {
protected $dbconn;
protected $locator;
public function __construct($dbconn, $locator) {
$this->dbconn = $dbconn;
$this->locator = $locator->add($this);
}
public function getSomethingFromAnotherRepos() {
return $this->locator['anotherRepos']->something();
}
Chápu to správně? Pokud ano, tak je to zajímavá myšlenka, a tady by asi měl service locator svoje opodstatnění.
Každopádně jsem to prozatím vyřešil tak, že jsem kritickým službám
přidal tag a v afterCompile()
projdu právě všechny služby,
které tento tag mají a v initialize()
jim injektnu poźadovaný
repos. Problém z natáhováním všech služeb jsem omezil pouze na ty, který
mají ten tag (ona ta kruhová závislost určitě bude jen u nějakých a bude
jich menší množina, takže zatím snad OK, ale uvidím).
- hAssassin
- Člen | 293
Jinak ješte abych to uvedl na konkrétním případě. Mám produkty a
kategorie. Přičemž vazba mezi nimi je m:n
. Mám tedy
ProductRepository
a CategoryRepository
a chtěl bych
volat nad ProductRepository
metodu
getCategories($productId)
a současně nad
CategoryRepository
volat getProducts($catId)
.
Současně si ale myslím, že by nebylo správný hrabat v
ProductMapperu
do tabulky category
ani v
CategoryMapperu
do tabulky product
. Mapper at si
pracuje pouze nad svojí tabulkou, resp. několika tabulkami, které tvoří
logický celek (což zde už neplatí).
Pak ale potřebuju mít kruhovou závislost…
- Jan Tvrdík
- Nette guru | 2595
Chápu to správně?
Ano, a už vím, kdy to nebude fungovat :) Když si třeba do presenteru
injektneš ProductRepository
, tak dokud nebude vytvořena instance
CategoryRepository
, tak ty metody, co ho potřebují nebudou
fungovat.
- hAssassin
- Člen | 293
@enumag > díky za odpověď. Ano, nad tím sem taky uvažoval, ale nejsem si jistý jestli by to vlastně vubec něco řešilo, protoze by se ta kruhová závislost přenesla jen z repozitářů do fasád. Nebo si teď nejsem jistý jak to přesně myslíš:
- fasáda přímo pracuje z několika repositáři, které se pouze injektnou do ní při jejím vytvoření, nebo
- fasáda pracuje s ostatními fasádami a přes ně přistupuje až k repozitáři (čili injektují se zase fasády).
Předpokládám, že myslíš asi a), protože b) vlastně nic neřeší?
- hAssassin
- Člen | 293
@enumag > ano chápu a asi to nejčistčí
řešení jak z toho ven. Já mám problém ten, že chci mít možnost přes
entitu přitupovat k jiným entitám přes cizí klíče. Čili když se entita
(např produkt) inicializuje, nastaví si categoryId
na id daný
kategorie (za předpokladu, že je tam vazba 1:n
). A pokud někde
(třeba až v šabloně) zavolám $entity->getCategory()
natáhne a vrátí mi to právě danou kategorii (pokud existuje). K tomu sice
musí být entita závislá na repozitáři, což mi zase tak nevadí, ale
repozitář (produkt) je pak závislí na jiném (category).
Možnost, že by entita byla závislá místo toho nebo současne přímo na facádě, mi nepříjde jako zrovna nejlepší.
- enumag
- Člen | 2118
V případě YetORM entita na získání těch souvisejících dat z jiné tabulky imho nepotřebuje repository (imho už to by bylo špatně). Pokud je získávání souvisejících dat složitější a jsou potřeba cizí repository, nemělo by to být implementované přímo v entitě, ale pouze v Facade.
Editoval enumag (20. 4. 2013 15:12)
- hAssassin
- Člen | 293
Aha, YetORM presně neznám, každopádně to přímo v entitě nemám, ta
metoda getCategories()
je pouze zkratka, uvnitř se volá
repozitář pro danou entitu a v něm ses volá repozitář cizí, proto ho tam
potřebuju. Představa, že bych z repozitáře nebo z entity volal fasádu a
ta by volala cizí repo se mi nelíbí a nelíbí se mi ani fakt, že bych
o tuto funkcionalitu přisel :-)
- castamir
- Člen | 629
Napadá mě určitá struktura modelu (zatím opravdu jen nápad): Mějme nějaký modelContainer (něco jako service locator), který bude zastřešovat všechny repository a k nim příslušné entity a mappery. Zároveň všechny entity budou striktně propojené s repozitářem např. nějak takto:
$entity = $repository->createEntity();
//nebo
$entity = new Entity();
$repository->attach($entity);
pak požadavek volaní metod ekvivalentních metodám ref resp. related z NDB by probublal z dané entity/kolekce přes repozitář do modelContainer, kde by se zavolal patřičný repozitář, který by to zpracoval a vrátil. A teď se do mě pusťte :D
Edit: vyřešilo by to např. teoreticky neomezené odkazování přes cizí klíč.
Editoval castamir (20. 4. 2013 16:36)
- hAssassin
- Člen | 293
@castamir > ano, o něco takového se pokouším, akorát že bez service lokatoru. Tady by se však celkem mohl hodit. A ano, chci mimojiné dosáhnout nekonečné odkazování přes cizí klíč :-)
Každopádně mě teda ještě napadlo povolit možnost, aby fasáda mohla
přistupovat k více repozitářům, tak jak psal enumag,
potom sama fasáda musí všechny tyto repozitáře vyžadovat a ty pak sama
rozstrkat do repozitářů dál. Např. ProductFacade
vyžaduje
ProductRepos
a CategoryRepos
, ty se do ní injektnou a
ona pak ví že CategoryRepos
musí předat do
ProductRepos
a ProductRepos
do
CategoryRepos
. Címž se sice kruhová závislost vytvoří, ale
až po tom, co jsou inicilizovaný všechny potřebný repozitáře.
Má to dvě nevýhody:
- porušuje to SRP, protože fasáda částečně přebírá činnost kontejneru/service locatoru,
- porušuje to DRY, protože teoreticky bude potřeba totéž udělat i v
CategoryFacade
(stejný kód, šlo by to ale asi pořešit automaticky už vCompilerExtension
).
Co myslíte?
- David Grudl
- Nette Core | 8218
Konfigurační soubor by neměl nahrazovat programování. Takže úvaha by se měla odvíjet od toho, jak bych stejný problém řešit čistě v PHP. No a vyrobil bych si na to továrničku.
Tedy místo původního
services:
a: ClassA
b: ClassB
kde potřebujeme objekty @a a @b nějak propojit, si vytvořím továrnu:
class ABFactory
{
function __construct(A $a, B $b)
{
// zašmodrchám je
$a->setB($this->b);
$b->setA($this->a);
$this->a = $a;
$this->b = $b;
}
function getA() {
return $this->a;
}
function getB() {
return $this->b;
}
}
A využiju jejích služeb:
services:
abFactory: ABFactory(ClassA, ClassB)
a: @abFactory::getA
b: @abFactory::getB
- hAssassin
- Člen | 293
No, jak sem říkal, blbý dotaz a vlastně to odporuje tomu, co jsi psal hned v úvodu svého prvního příspěvku. Takže beru zpět.
Jinak sem měl na mysli automatizovat ono vytváření, případně konfiguraci těch továrniček např. v rozšíření (což by asi šlo). A úplně ideálně i jejich kód (což už zavání příliš velkou leností :-).
- miler
- Člen | 75
Snažím se implementovat Davidovo řešení, ale dostávám se do fáze "Reference to missing service @abFactory::getA", v jaké verzi Nette tenhle postup funguje, prosím? Jsem s aktuálním 2.0 v pytli?
Jak bych pak prosím nahradil službu definovanou takto?
a:
class: A
setup:
- setXY(%xy%)
Edit:
Možná je to tím že neumím továrničce předat ty původní modely?
testFactory: Models\TestFactory(Models\A, Models\B)
mi vygeneruje
/**
* @return Models\TestFactory
*/
protected function createServiceTestFactory()
{
$service = new Models\TestFactory('Models\\A', 'Models\\B'');
return $service;
}
Ale ostatní služby, které dřív vyžadovaly službu A mi po přepsání a na:
a:
@testFactory::getA
hlásí že služba typu Models\A
neexistuje a že mám
zkontrolovat typehint.
TestFactory vypadá takto:
<?php
namespace Models;
class TestFactory {
public function __construct(\Models\A $a, \Models\B $b) {
$this->a = $a;
$this->b = $b;
}
public function getA() {
return $this->a;
}
public static function getB() {
return $this->b;
}
}
Editoval miler (22. 10. 2013 11:17)
- miler
- Člen | 75
Přidal jsem těm getterům ve factory return type do phpdoc komentáře a posunul jsem se k:
Argument 1 passed to Models\TestFactory::__construct() must be an instance of Models\A,
string given...
tzn. že asi opravdu „jen“ neumím předat správně ty modely jako parametry pro továrničku?
Díky
Edit:
Ty parametry tam vlastně možná nejsou vůbec potřeba? Nicméně teď se dostanu do dalšího kroku, kde vymrznu na statickém volání getA() z configu.
Non-static method Models\TestFactory::getA() should not be called statically,
assuming $this from incompatible context
Edit 2:
Tak tam asi potřeba jsou, jinak se mi nevytvoří žádná služba pro TestFactory.
Už jsem se zamotal a nevím kudy kam :-)
Editoval miler (23. 10. 2013 12:42)
- miler
- Člen | 75
Nedokázal by mi prosím někdo zkušenější poradit jak se z této šlamastiky vymotat? Už jsem prošel forum 4× tam a zpět, vyzkoušel spousty tipů a triků ale ne a ne s tím hnout. Díky moc.
Dočasně jsem to vyřešil tak že jsem ty metody co potřebují jiný model dal do jiného spolčného modelu.
Editoval miler (24. 10. 2013 12:18)