Architektura nad Doctrine2
- jasin755
- Člen | 116
Zdravím předem se omlouvám, že zakládám vlákno, které tu už je, ale na to staré už nejde reagovat.
Chtěl jsem si upřesnit některé věci v architektuře nad Doctrine2.
Entity
U entit, asi není co řešit. Gettery/settery bez logiky.
Repository
Všechny moje repositáře dědí od Doctrine\ORM\EntityRepository. Jsou v nich
jenom DQL dotazy, bez constructoru, vše potřebné přes parametry metod.
Vrací entitu nebo kolekce entit.
A teď se dostávám do částí, kde fakt nevím kam co patří.
Facade
Pokud dobře chápu fasáda přebere skalární hodnoty z presenteru, vytvoří
nad nimi entity + validace zda dostalo co potřebuje. To případně pošle
service, která data upraví dle potřeby a následně se persistuje. Pokud to
tak je tak to chápu dobře, ale co když je situace opačná, že potřebuji
data poslat z fasady do presenteru a následně do šablony? Co je výstupem
fasady? Skalár, value object, entita?
Service
Jak jsem zmiňoval ve fasade, při ukládání dat chápu, že service
modifikuje entity, setuje jim různé hodnoty, validuje atd… Ale opět co
když je situace opačná. Mám entitu Product ve které je property
supplier_price. Getterem getSupplierPrice() získam nákupní cenu
v originální měně, ale co když mě price absolutně nezajíma, ale chci
vždy finální nákupní cenu přepočítanou na potřebnou měnu?
Předem děkuji za rady :)
- enumag
- Člen | 2118
Nepatřím mezi nejzkušenější uživatele Doctrine v komunitě takže můj způsob nemusí být úplně správný. Zajímá mne i názor ostatních jestli to dělají podobně.
Facade používám v případě kdy si v presenteru nevystačím s jednoduchým voláním find* metod z EntityRepository. Výstupem facade je co je zrovna potřeba, tedy skalár (pokud tahám např. nějakou složitější SUM), entita (pokud se hledá složitými podmínkami) nebo iterátor nad kolekcí entit (když jich může být více). Pokud je to něco ještě jiného tak to zpravidla dávám do samostatné service.
EDIT: Na složité queries dost často používám query objecty z Kdyby\Doctrine, ve Facade mi toho tedy moc nezbude.
Pokud jde o uvedený případ s měnami tak bych měl nějakou service
CurrencyCalculator která by měny převáděla. Pak bych používal
$currencyCalculator->calculatePrice($product->getPrice(), $this->currency)
kde $this->currency
je typicky perzistentní parametr
s aktuální měnou. Pro použití v šabloně bych z toho udělal
helper.
Editoval enumag (4. 12. 2014 21:47)
- jasin755
- Člen | 116
Nenapsal si co by bylo vystupem $currencyCalculator->calculatePrice přímo skalár vypočítané ceny? Jinak si osobně myslim, že find a ostatně entity_manager do presenteru nepatří. Veškeré persisence by měli být ve fasadě.
Mě jde hlavně o to, že každý tvrdí, že fasady by měli být znovu použitelné, tak se ptám jak. Protože když mi fasáda vrátí cokoli jiného než entitu nebo kolekci, tak znovu použitelnost je diskutabilní.
- enumag
- Člen | 2118
Jasně, důvod proč to tak nedělám je, že mi připadá otravné psát proxy metody (zavolám fasádu která jen předá dotaz EM a vrátí mi přesně to co vrátí EM). A ještě otravnější je když v jednom presenteru pracuji s entitami více typů najednou a potřebuji injectovat 5 facade tříd. Proto raději v presenteru nechávám EM a používám queryobjecty (to nejsou služby takže je nemusím injectovat).
Feel free to prove me wrong. ;-)
Editoval enumag (4. 12. 2014 22:52)
- Zax
- Člen | 370
Já tomu sice úplně nerozumím, ale fasády přeci nejsou proxy objekty nebo jo? Příklad: mám formulář pro přidání nového záznamu, který chci oživit. Pro oživení můžu:
- namatlat kód do presenteru
function formSubmitted(Form $form, $values) {
$entity = new Entity;
// .. přiřazení odeslaných hodnot do vlastností
$this->entityManager->persist($entity);
$this->entityManager->flush();
$this->flashMessage('Entita ' . $entity->id . ' vytvořena');
}
- vytvořit si na to třídu (fasádu – nebo to je asi jedno jak se tomu říká ne? Prostě služba, která má jasně dané API pro nějakou konkrétní akci), jejíž jediná zodpovědnost bude ze zaslaných údajů vyrobit novou entitu.
function formSubmitted(Form $form, $values) {
// nevím, jestli "persistentEntityFactory" je dobrý název, lepší jsem nevymyslel :-P
$entity = $this->persistentEntityFactory->create($values->name, $values->whatever);
$this->flashMessage('Entita ' . $entity->id . ' vytvořena');
}
A z toho pak vyplývá ta znovupoužitelnost, protože té třídě je jedno, kdo ji volá. Můžu třeba využít Kdyby/Console a použít tuto třídu ve vlastním příkazu. Také mi přijde čistší si injektnout službu, jejíž jediná zodpovědnost je to, co chci, ne nějaký monster-object, který dělá vše.
BTW myslím si, že je asi docela rozumné si na tyto fasádní třídy psát i interfacy. Je to sice trochu psaní navíc, ale pak má každá fasádní třída jasně dané API, které musí splnit. Navíc, vyžadováním interfacu nezávisíš na konkrétní implementaci – občas je třeba něco někde přiohnout, ale jinde nechat výchozí chování, není problém si napsat další implementaci, zaregistrovat v configu s „autowired: false“ a ručně injektovat kam je třeba.
Pokud jde o dolování dat: +1 pro QueryObject, pomocí kterého se dá krásně zapouzdřit dolovací logika do čitelného API. Ale stejně bych si na něj vždycky udělal aspoň továrnu a tu vyžadoval, než mastit „new“ do presenteru ;-) (hint: automaticky generované továrny fungují na všechno, nejen na komponenty)
Znovu opakuji, že tomu sám moc nerozumím, hlavně pokud jde o pojmy – ale i se samotnou Doctrinou dělám spíš krátce. K této architektuře jsem dospěl tak nějak „evolučně“ a zatím mi to dává takto smysl, ale je samozřejmě možné, že budu někdy za měsíc opět přepisovat komplet celý model :-P
- jasin755
- Člen | 116
@Tharos :
Tak nedokážu si představit co by tam mělo být. Resp. mám pouze
věci typu:
public function getProductReference(){
if(isset($this->supplier_code) && isset($this->product_number)){
//vrací nový složený kód produktu
return $this->supplier_code.'-'.$this->product_number;
}else{
//vrací starý kód produktu protože ješte nedostal nový
return $this->reference;
}
}
a nebo v abstrakní třídě Entity, kterou dědí všechny entity mám invalidaci cache podle primárního klíče. Resp je tam jen metoda preUpdate() ve které se zavolá fasáda, které se předá entita a primární klíč o zbytek se stará fasáda, ale to jsem uplně rozebírat nechtěl.
@enumag:
QueryObjecty myslíte přímo Doctrine\ORM\Query? Tzn. až někde v presenteru voláte getResult() a nějakým způsobem provádíte potřebnou Hydrataci ?
Editoval jasin755 (5. 12. 2014 8:36)
- Jiří Nápravník
- Člen | 710
Já souhlasím s tím, že podle mě EntityManager apod. nemá v presenteru co dělat a mám to ve fasádě. Na druhou stranu, zase nepoužívám Query Objecty a púšu to také v té fasáde. Do services (podle pětivrstvého modelu) jsem nepochopil moc co patří, tak vše nazývám facade. Samozřejmě Těch facade mam vice, podle logik, aby to nebyla jedna megatřída.
Paginator: Předpokládám, že používáš Kdyby\Doctrine – pak si vytvoříš, třeba dotaz a vrátíš ho jako new ResultSet($query); no a v presenteru nad tím objektem zavoláš ->applyPaginator() kam šoupneš Nettí Paginator…
- Jiří Nápravník
- Člen | 710
Taky jsem chtěl být takový hipster, až mě někdo řekl, ať tomu dám šanci, dal jsem a nemůžu si to vynachválit. V podstatě, to co bys dělal s čistou Doctrine můžeš i tady, a navíc tam můžeš (ale nemusíš) používat různé vychtávky.
- enumag
- Člen | 2118
@jasin755:
Mám na mysli třídy, které dědí Kdyby\Doctrine\QueryObject.
Použití Kdyby\Doctrine mohu jen a jen doporučit, tím že ji nepoužíváš si dle mého názoru jen zbytečně komplikuješ život. ;-)
Jinak by tě mohl zajímat tenhle dnešní twít od @FilipProcházka.
Editoval enumag (5. 12. 2014 12:58)
- mkoubik
- Člen | 728
@Tharos cokoliv se ti opakuje v kódu mimo entity je dobrý
kandidát.
Naposledy jsem přepisoval (jen typově, ponechme stranou že to patří
do DQL)
$product->availableCount = $product->availableCount - $count;
$product->reservedCount = $product->reservedCount + $count;
na
$product->reserveCount($count);
a kód tím docela prokouknul.
- Filip Procházka
- Moderator | 4668
@jasin755 nenahrazuje, rozšiřuje. Viz dokumentace
DAO ani používat nemusíš, můžeš si nakonfigurovat, že chceš používat jen vylepšené repository
doctrine:
defaultRepositoryClassName: Kdyby\Doctrine\EntityRepository
Ty věci co přidává EntityDAO už zase tak zásadní nejsou, v podstatě to samé dělá EntityManager (a často i lépe). Ovšem ta vylepšení co přidává „můj“ EntityRepository jsou super :)
- Filip Procházka
- Moderator | 4668
Já to zpochybnil už dávno :) (tenhle commit bylo moje definitivní rozhodnutí, že dao není ideální, pochybnosti jsem měl už dlouho předtím).
Dao::save()
vypadal jako super nápad, než se v praxi ukázalo
že to není super nápad a v ten moment EntityDAO
trochu ztratil
smysl. Stále ho používám na prototypování, ale vždycky použití nakonec
přepíšu tak, aby využívalo jenom metody z EntityRepository a na všechny
ty save operace využívám EntityManager.
Mazat ho rozhodně v dohledné době neplánuju, na zpětné kompatibilitě si zakládám :)
- Filip Procházka
- Moderator | 4668
@jasin755 o QueryObjektech mám v docce celý článek, přečti si ho :)
- jasin755
- Člen | 116
Díval jsem se ještě na Kdyby/DoctrineCache ke které dokumentaci nemáš dá se říct žádnout. Zajímalo by mě vlastně co to je, protože implementaci resultCache jsem již řešil a moc dobře jsem nevyřešil. Resp problém byl s jeji invalidaci. Chtěl jsem ji rozsáhle používat po celém projektu a cachovat do Memcache, ale nakonec se mi to podařilo jenom u routeru, kde se celkem dobře cachuji i invaliduji objekty, které používám.
- Filip Procházka
- Moderator | 4668
Kdyby/DoctrineCache je v podstatě jenom bridge, který dokáže do doctrine podstrčit IStorage z Nette. Doctrina sama má zabudovanou result cache přímo v sobě a rozhodně nedoporučuji psát si vlastní.
V configu si můžeš cache přepnout na nějakou jinou implementaci z Doctrine.
- Kdyby/Doctrine/DI/OrmExtension.php tyhle typy cache si můžeš nastavit v configu
- Kdyby/DoctrineCache/DI/Helpers.php –
tohle jsou jenom aliasy, kdybys chtěl použít driver který tam není, tak
místo aliasu prostě napíšeš název třídy, nebo odkaz na službu
(
@myCacheService
)
Na produkci doporučuji použít apcu na metadata a DQL => SQL
překlady
doctrine:
metadataCache: apc
queryCache: apc
- Tharos
- Člen | 1030
Vydestilovat nějaké minimum z kódu, který jsem měl na mysli, mi docela dává zabrat, a tak zkusím slovní popis:
- Mám v e-shopu entitu
Order
s metodouaddProduct
, ta metoda při přidání produktu kontroluje, zda je daný produkt na prodej (metodaProduct::isOnSale
) a když ho přidá do kolekce produktů, spustí privátní metoduOrder::calculatePrices
, která podle aktuálních položek objednávky, dopravy atp. vypočte ceny objednávky (cena za dopravu, celková cena atp. – to vše chci mít spočtené přímo v objednávce mimo jiné z důvodu změn cen produktů v čase). - Ekvivalentně ten přepočet ceny vyvolává i odebrání produktu, změna typu dopravy, metody platby atp.
- V entitě
Order
mám metodusetFinished
, která při zavolání zkontroluje, že v objednávce jsou nějaké položky (jinak vyhodí výjimku) a také zkontroluje, zda se objednávka aktuálně nachází ve stavu, ze kterého je možné do stavufinished
přejít. Pokud je vše OK, nastaví se stav objednávky nafinished
. - Pokud nastavuji objednávce způsob platby „kartou online“, kontroluji, že všechny položky objednávky jsou skladem, a pokud nějaká není, vyhazuji výjimku. Jde o to, že nechci, aby zákazník zaplatil kartou nějaký produkt, který mám jen u externího dodavatele a v krajním případě mu jej nebudu schopný dodat (například ani externí dodavatel jej nemá skladem a zároveň výrobce už produkt nevyrábí).
- Při potvrzení objednávky zajistím přímo z entity zarezervování potřebného množství kusů produktů ve skladě (rezervace je reprezentována další entitou, takže de facto jen zajistím vznik těchto entit).
- To, zda je produkt skladem, zjišťuji přímo z entity
Product
tak, že „dotraverzováním“ do skladu zjistím, kolik je v něm dostupných kusů, odečtu zarezervované kusy atp. a vše vyhodnotím.
Jak je z mého popisu vidět, nejedná se o nějaké zásadní úkony (nevkládám přímo z entity zprávy do message queue atp.), ale ve výsledku mám v entitách nemalé množství doménové logiky. Dalo by se říct, že to je validace tady, validace tam, vytvoření entity tady, vytvoření entity jinde, přepočet nějaké skalární hodnoty… Ale ve výsledku to vytváří krásný OOP model, který je zvnějšku téměř nerozbitný (nelze jej uvést do neplatného stavu voláním veřejných metod) a práce s ním je jedna radost.
Pokud bych tuto logiku neměl přímo v entitách, musel bych ji mít jinde. V lepším příadě v nějakých service třídách, ale už to je taková berlička, anebo v horším případě rozsetou úplně všude (v presenterech, ba i v šablonách). Poslední varianta je cesta do pekel mimo jiné proto, že ta logika pak má velkou tendenci různě se v aplikaci duplikovat.
Editoval Tharos (6. 12. 2014 11:22)
- Tharos
- Člen | 1030
Co by si povedal na nahradenie setFinished() za finish()?
Dobrý dotaz. :) Záměrně ji mám pojmenovanou setFinished
, a
to proto, protože klasické finish
ve mně evokuje, že
zavoláním takové metody se provedou všechny úkony, které je
zapotřebí při dokončení objednávky provést. Takových úkonů je
hodně: nasypání messages do front (aby se odeslaly e-maily), odebrání vazby
na objednávku z uživatelovo košíku (práce se session), flush modelu… a
to řešit v entitě by už bylo nevhodné. Stručně řečeno proto, protože
to je práce s vrstvami, o kterých je zbytečné, aby entita vůbec věděla
(sessions, messages, flush).
Proto tomu říkám setFinished
(upozorňuji, že ta metoda
nepřijímá žádný parametr, v praxi je to vážně jenom
$order->setFinished()
), čímž se čtenářovi kódu snažím
říct, že to volání nějakým způsobem nastaví stav modelu, ale neprovede
všechny potřebné úkony.
Přemýšlím, jestli se mnou nastíněné myšlenkové pochody dají nějak zobecnit a napadá mě zhruba následující…
- Jedná se o validaci dat?
Rozhodně to udělám v entitě, protože ty přímo obsahují data, která k té validaci potřebuji. Plus chci, aby byla zvnějšku „vrstva entit“ nerozbitná.
- Jedná se o dopočet nějakých skalárních hodnot v entitě/entitách?
Rozhodně to udělám v entitě, protože přímo obsahují data, která k tomu potřebuji.
- Jedná se o vytvoření/odstranění/úpravu entit, na které má nějaká
„hlavní“ vazbu?
Rozhodně to udělám v entitách, protože opět vše, co k tomu potřebuji, mám právě v entitách.
- Jedná se o nějakou práci se session, messages, view a tak podobně?
Nebudu to dělat v entitách, protože je kontraproduktivní, aby entity o těchto částech aplikace vůbec věděly.
Není to vyčerpávající souhrn, ale věřím, že je z toho patrný jistý přístup. Bonusem pak je, že v praxi zjistíte, že spoustu public getterů a setterů vůbec nepotřebujete a můžete zahodit. :) Právě proto, protože danou logiku neřešíte mimo entity, nýbrž v entitách.
Editoval Tharos (6. 12. 2014 14:21)
- jasin755
- Člen | 116
Edit:
Jak dostanu anotation reader z Kdyby ?
Edit2:
Narazil jsem na zváštní chování BaseEntity. Když zavolám:
\Kdyby\Doctrine\EntityManager $em->getMetadataFactory()->getAllMetadata();
Následně si to projíždím v cyklu:
foreach($meta_datas AS $meta_data){
$fields = $meta_data->getFieldNames();
if(empty(($fields))){
throw new UrlFatalError(gettext(sprintf('Nalezená prázdná entita %s',$meta_data->getName())));
}
}
Tak mi to vyhodí:
Nalezená prázdná entita Kdyby\Doctrine\Entities\BaseEntity
Moc dobře nerozumím jak se tam ta entita vůbec dostane.
Editoval jasin755 (8. 12. 2014 11:26)
- Filip Procházka
- Moderator | 4668
@jasin755 hned pro začátek bych tě s dovolením pokáral, že jsi nezaložil nové téma.
Když si přečteš dokumentaci Doctrine tak zjistíš, že existuje věc které se říká „mapped superclass“ – a ta žádné property mít vůbec nemusí. Všechno je v pořádku, jen ty to špatně používáš, protože jsi si nepřečetl dokumentaci.
- jasin755
- Člen | 116
Jan Endel napsal(a):
Vždyť entita ani nemá vědět že nějaká url existuje, co když budeš mít proces v command lajně?
Ano i né. Za ideální situace by entita neměla vědět o ničem, ale tohle elegantněji řešit podle mě nejde. Entitě nastavím jenom anotaci @URL ve které je jenom jméno nic vic. Samotná entita s tím ani nepracuje. Jenom nese tuto informaci. O zbytek se starají fasady routeru, které hledají tyto entity a z DB si už načítaji vazby, které mezi sebou mají. Následně se rekurzivne magicky skládá QueryBuilder podle vazeb.
Je to z důvodu, že řešení je multisite a každá site může mít jinou strukturu adres a struktury jsou např.:
example.com/category
example.com/subcategory
example.com/product
example.com/subcategory/8
example.com/subcategory/100/8
example.com/product/8
example.com/product/8/100
Tzn. se do URL ukladá cesta odkud se uživatel proklikal do daného produktu či kategorie s tím, že se kontroluji vazby mezi sebou (zda ten produkt v kategorii opravdu je, zda ta subkategorie je ve všech vyjmenovanych nadřazených kategoriích + pořadí atd..). Takže držet tuto malou informaci u Entity mě nepřipadne nijak špatné.
@FilipProcházka
Za nové téma se asi omlouvám, ale kdbych to měl brát do důsledku tak by se
tohle vlákno mělo rozseknout min na 3 další. Takže mě snad omluvíš.
Jinak jsem si následně všiml, že tvoje BaseEntity je opravdu Mapped
SupperClass. Na to správně používání bych se rád zeptal.
- enumag
- Člen | 2118
jasin755 napsal(a):
Tzn. se do URL ukladá cesta odkud se uživatel proklikal do daného produktu či kategorie s tím, že se kontroluji vazby mezi sebou (zda ten produkt v kategorii opravdu je, zda ta subkategorie je ve všech vyjmenovanych nadřazených kategoriích + pořadí atd..). Takže držet tuto malou informaci u Entity mě nepřipadne nijak špatné.
Imho je mnohem lepší než tyhle nesmysly k entitě jednoduše uložit celou její url (včetně kategorií) a udržovat to aktuální pomocí Kdyby/Events.
Editoval enumag (9. 12. 2014 12:55)
- Azathoth
- Člen | 495
Hm, architektura nad doctrine…já mám rozsekaný model podle domén na jednotlivé namespacy (model\payments, model\carPooling, apod.) a uvnitř toho mám vždycky třídu, která si drží Dao a entityManager a má metody, které potřebuji na crud operace. A potom mám třídu, která má k této přístup a obsahuje aplikační logiku. Entity mám jenom na validaci dat, zapouzdření a nějaké jednoduché operace nad jejich properties.
Ale zajímala by mne jedna věc: jak elegantně dělat modulární aplikaci nad doctrine? abych měl například entitu user, extension na platby, extension na správu souborů…jak té jsou best practise, aby entita user měla jako properties peněženku (v případě plateb) soubory, které vlastní (v případě správy souborů) a nemusel tu entitu moc upravovat, tedy aby šly tyto extensions snadno přidávat/odebírat bez velkého zásahu do entit, ale přitom aby byly entity provázané (user měl referenci na svou peněženku apod.)?
- jasin755
- Člen | 116
matej21 napsal(a):
@jasin755 Query(Object) vyzaduje ve svych metodach Queryable (to prave implementuje EntityDao (resp. EntityRepository))
Chvilku jsem si s tím hrál a funguje mi to i když to nemá moc hlavu a patu.
Mám QueryObject Product, který obsahuje různé metody na dolování dat k produktu. ResultSet jsem z toho získal pomoci:
$this->entity_manager->getRepository('Entity\Product')->fetch($query_object);
ale zároveň funguje:
$this->entity_manager->getRepository('Entity\NejakaUplneJinaEntita')->fetch($query_object);
a dokonce i:
$this->entity_manager->getDao('Entity\NejakaUplneJinaEntita')->fetch($query_object);
Je to trošku WTF. Proč neudělat rovnou:
$query_object->fetch()
(Popravdě jsem se i o to pokoušel tím, že tomu přehodím $this, ale pak jsem zjistil, že to implementuje jiný interface :X)
Nebo aspoň:
$this->entity_manager->fetch($query_object);
Editoval jasin755 (9. 12. 2014 22:51)
- David Matějka
- Moderator | 6445
@jasin755 metody select a createQueryBuilder pracuji s konkretnim typem entity
- norbe
- Backer | 405
Azathoth napsal(a):
Ale zajímala by mne jedna věc: jak elegantně dělat modulární aplikaci nad doctrine? abych měl například entitu user, extension na platby, extension na správu souborů…jak té jsou best practise, aby entita user měla jako properties peněženku (v případě plateb) soubory, které vlastní (v případě správy souborů) a nemusel tu entitu moc upravovat, tedy aby šly tyto extensions snadno přidávat/odebírat bez velkého zásahu do entit, ale přitom aby byly entity provázané (user měl referenci na svou peněženku apod.)?
Tohle řeším tak, že mám generátor abstraktních tříd, který si z konfigu vytáhne dostupná rozšíření – traity které obsahují potřebný kód (akorát nepoužíváme doctrine, ale předpokládám že ta bude s traity také kompatibilní). Z těchto vygenerovaných tříd potom dědím vlastní entity:
class Product extends App\Generated\Product {
...
}
Vygenerovaný kód vypadá zhruba takhle:
namespace App\Generated;
class Product extends Entity {
use ProductEntityExtension1;
use ProductEntityExtension2;
}
- Jiří Nápravník
- Člen | 710
Azathoth napsal(a):
Ale zajímala by mne jedna věc: jak elegantně dělat modulární aplikaci nad doctrine? abych měl například entitu user, extension na platby, extension na správu souborů…jak té jsou best practise, aby entita user měla jako properties peněženku (v případě plateb) soubory, které vlastní (v případě správy souborů) a nemusel tu entitu moc upravovat, tedy aby šly tyto extensions snadno přidávat/odebírat bez velkého zásahu do entit, ale přitom aby byly entity provázané (user měl referenci na svou peněženku apod.)?
Tak tohle by me taky zajimalo. Mam modlarni aplikaci sice, ale ty Entity porad nemam nijak rozumne vyresene a musim to pravovat, takze uplne modularita vyndam tohle a strcim tohle nefunguje. Hledal jsem jednim casem a nenasel. Napadli mě jedině nějaký Doctriní listenery, které by kontrolovali, že když bude tahle extensiona povolena, tak se přidají properties. Ale s tím se mi nechtělo dělat, protože by to bylo asi dost složité…
- Azathoth
- Člen | 495
Jiří Nápravník napsal(a):
Azathoth napsal(a):
Ale zajímala by mne jedna věc: jak elegantně dělat modulární aplikaci nad doctrine? abych měl například entitu user, extension na platby, extension na správu souborů…jak té jsou best practise, aby entita user měla jako properties peněženku (v případě plateb) soubory, které vlastní (v případě správy souborů) a nemusel tu entitu moc upravovat, tedy aby šly tyto extensions snadno přidávat/odebírat bez velkého zásahu do entit, ale přitom aby byly entity provázané (user měl referenci na svou peněženku apod.)?Tak tohle by me taky zajimalo. Mam modlarni aplikaci sice, ale ty Entity porad nemam nijak rozumne vyresene a musim to pravovat, takze uplne modularita vyndam tohle a strcim tohle nefunguje. Hledal jsem jednim casem a nenasel. Napadli mě jedině nějaký Doctriní listenery, které by kontrolovali, že když bude tahle extensiona povolena, tak se přidají properties. Ale s tím se mi nechtělo dělat, protože by to bylo asi dost složité…
mě to napadlo (zatím jsem neměl čas na realizaci, tak to je možná blbost)udělat to pomocí trait, takže entita user by měla navíc 1 řádek, ten s traitou.
Pokud si dobře vzpomínám, tak @stekycz říkal něco o tom, že nejlepší by bylo udělat extension, který by nějak zařídil spojení usera a plateb tak, aby se nemuselo sáhnout ani na usera, ani na platby, ale nevím, jak přesně by se to dalo implementovat.
Možná by to šlo přes dědičnost…že by byl potomek usera, co by měl navíc traitu. Jinak nevím, jak by se to dalo elegantně udělat tak, aby se nemusel přidáním téhle extension měnit kód jinde než ve factory třídě, která produkuje usery.
- Filip Procházka
- Moderator | 4668
@jasin755
Chvilku jsem si s tím hrál a funguje mi to i když to nemá moc hlavu a patu.
Mám QueryObject Product, který obsahuje různé metody na dolování dat k produktu. ResultSet jsem z toho získal pomoci … ale zároveň funguje … a dokonce i:
To je jednoduché, createQueryBuilder() nad Queriable vždy vytvoří „základní“ query builder nad danou entitou ke které patří.
Pokud bys udělal
protected function doCreateQuery($queriable)
{
$qb = $queriable->createQueryBuilder()
->select('e')->from(Entity\Product::class);
// ...
return $qb;
}
tak už to bude pracovat vždy jenom nad Entity\Product
.
Nevidím moc důvod to svazovat pro konkrétní entitu, na druhou stranu když se liší „root“ entita toho DQL (ta co je ve from) od entity toho DAO, tak by to možná mohlo nějakou výjimku hodit, což?
Je to trošku WTF. Proč neudělat rovnou:
$query_object->fetch()
(Popravdě jsem se i o to pokoušel tím, že tomu přehodím $this, ale pak jsem zjistil, že to implementuje jiný interface :X)
Protože něco takového by byla neskutečná prasárna,
$query = new MyCustomQuery();
$resultSet = $query->fetch();
Odkud se tam vezme connection do db a entity manager? Ze vzduchu?
Nebo aspoň:
$this->entity_manager->fetch($query_object);
To by tereticky nebyl problém dodělat, ale muselo by se striktně vyžadovat, že výsledné DQL musí definovat uvnitř z jaké entity selectuje. Nevidím v tom ale nijak zásadní rozdíl.
- Filip Procházka
- Moderator | 4668
@Azathoth ty je nepotřebuješ spojovat a když jo, tak ti stejně stačí ta vazba z té strany, ktera je navíc. Tedy bys udělal v platbě property user, ale v userovy už bys neudělal property platby.
Pořád půjde napsat DQL které ti vytáhne plaby daného usera a nebo usera dané platby. A pokud bys tam fakt potřeboval nějak ty entity propojovat, tak se to dělá pomocí interfaců a listenerů.
- Azathoth
- Člen | 495
@FilipProcházka takže navrhuješ, aby tyhle extensions byly
závislé na userovi a ten o nich vůbec nevěděl? To zní hodně dobře.
Ale kdybych chtěl připojit odkaz na nějaký objekt do více entit, tak by to
chtělo jednostrannou vazbu na straně těch entit…nevíš, jak by se dalo
elegantně udělat tohle?
- jasin755
- Člen | 116
@FilipProcházka
Nevím co dělám špatně, ale podle mě to mám uplně stejně:
$query = (new ProductShop())
->withShop()
->byIdProduct(1573);
$product_shops = $this->entity_manager->getRepository('Exsys\Entity\ProductShop')->fetch($query);
protected function doCreateQuery(\Kdyby\Persistence\Queryable $repository) {
$qb = $repository->createQueryBuilder();
$this->basicQuery($qb);
foreach($this->select AS $select){
$select($qb);
}
foreach($this->filter AS $filter){
$filter($qb);
}
return $qb;
}
protected function basicQuery(\Kdyby\Doctrine\QueryBuilder $qb) {
$qb->select('ps')
->from(\Exsys\Entity\ProductShop::class,'ps');
}
To vše funguje, ale funguje to i když zaměním na:
$product_shops = $this->entity_manager->getRepository('Exsys\Entity\Articles')->fetch($query);
- jasin755
- Člen | 116
Zkusil jsem to ze vzědavosti, jestli to nějakou magii pak nevytvoří alias a bohužel. Bez from(…) to vrací Undefined offset 0 ve vendor/doctrine/orm/lib/Doctrine/ORM/QueryBuilder.php:292
public function getRootAlias(){
$aliases = $this->getRootAliases();
return $aliases[0];
}
Editoval jasin755 (10. 12. 2014 14:52)