Oznámení
Lean Mapper, DI, formuláře – vzorový projekt
před 6 lety
- Šaman
- Člen | 2275
Ahoj, jak jsem slíbil, zveřejňuji svůj ukázkový projekt s využitím LM. Stahujte jen otagované verze, ty jsou spustitelné a relativně stabilní.
Je to obligátní Todolist z Quickstartu, i když došlo k drobým
změnám v ER diagramu a tedy i v databázi. LM je zatím použit ve velmi
jednoduché podobě (žádné filtry, enumy, apod.), další rozšiřování
bude probíhat postupně.
Všechny závislosti jsou řešené pomocí DI, coding standard je podobný
Nette, snažím se vytvořit jakýsi best practise pro jednoduché Nette
aplikace.
Do tohoto vlákna pište prosím jak dotazy, tak hlavně připomínky, nejen k LM, ale i k celkové architektuře aplikace, od namespaců po configy.
před 6 lety
- Šaman
- Člen | 2275
Pár poznámek k akruální verzi 0.52:
- Jediný uživatel je ‚admin‘ a heslo libovolné (kontrola je zatím vypnutá). Časem se budou dát vytvářet další.
- Authenticator je první na řade s refaktoringem (bude závislý na UserRepository, nikoliv přímo na Connection).
- Jediný formulář vytvářený jako komponenta pomocí továrničky je LoginForm (ostatní jsou zatím postaru, jednodušším způsobem).
- Držím konvenci, že
getXxx()
vrací entitu, nebo NULL,findXxx()
vrací kolekci. - Při použití magických
getByFoo()
afindByFooAndBar()
je velmi doporučeno vyjmenovat použité metody v anotacích jednotlivých repository. Zajistíte si tak napovídání a odmagičtíte tyto metody.
před 6 lety
- Jan Suchánek
- Backer | 403
@Šaman: pěkné hned du vyzkoušet, jen drobnost nebylo by lepší mezi UserRepository dát ještě UserManager?
před 6 lety
- Casper
- Člen | 253
Do tohoto vlákna pište prosím jak dotazy, tak hlavně připomínky, nejen k LM, ale i k celkové architektuře aplikace, od namespaců po configy.
Když vidím, že tam nemáš žádné fasády, rád bych věděl, kde bude
později posílání emailů, transakce a vůbec složitější use-casy, kde
ukládáš více entit? Tedy například kam přijde metoda
registerUser()
?
před 6 lety
- Šaman
- Člen | 2275
@Casper: Zatím je to poměrně prázdné a fasády mi připadají zbytečné. Nechci, aby to vypadalo zbytečně složité. Na druhou stranu pokud by se aplikace rozrostla a v repozitářích by začaly vznikat složitější metody, tak samozřejmě fasády přidám. Nebo ty bys je tvořil hned v základu?
@jenicek: Čemu říkáš manager? Té fasádě? Ta by byla nad repozitory.
Snažím se o implementaci návrhového vzoru DAO a Repository, i když spojené dohromady a pojmenované trochu nepřesně Repository. Celkově v aplikaci máme spodní 4 vrstvy pětivrstvého modelu, servisní vrstva/fasáda je podle mě už spíš záležitost aplikační logiky, než ORM. Takže dokud aplikace nic neumí, tak nemohu říct jaké fasády budu potřebovat.
Editoval Šaman (25. 10. 2013 15:11)
před 6 lety
- Jan Suchánek
- Backer | 403
@Šaman: ok, manager = fasáda.
Editoval jenicek (25. 10. 2013 12:01)
před 6 lety
- Casper
- Člen | 253
@Šaman:
No já když jsem s Nette začínal, tak jsem se řídil quickstarty apod, ale
všude se používala nějaká zjednodušená architektura (protože prostě pro
quickstart víc abstrakce nepotřebuješ), nicméně jsem na to pak trochu
doplácel. Takže ano, kdybych já dělal nějaký ukázkový projekt, použiju
ty fasády rovnou.
Celkově v aplikaci máme spodní 4 vrstvy pětivrstvého modelu, servisní vrstva/fasáda je podle mě už spíš záležitost aplikační logiky, než ORM. Takže dokud aplikace nic neumí, tak nemohu říct jaké fasády budu potřebovat.
Máš pravdu, že fasáda je aplikační logika, nicméně i primitivní aplikace nějakou má. Sice tam budeš mít pravděpodobně jen převolání nějaké metody Repository, nicméně už tam tu abstrakci máš a aplikace je připravená na snadné rozšíření. Což se rozhodně stane, pokud z ní někdo bude vycházet ve svém projektu. A pak se ti nestane, že začátečníci budou rvát aplikační logiku do Repository/DAO, kam rozhodně nepatří (a tím vytvářet například takové zrůdnosti jako závislosti mezi Repository).
Editoval Casper (25. 10. 2013 12:04)
před 6 lety
- Šaman
- Člen | 2275
@Casper: Máš pravdu, došlo mi to během výuky.
(Je zajímavý, kolik věcí si člověk ujasní, když se je snaží vysvětlit
někomu jinému). Jako první fasádu bych už měl vytvořit
TaskService
a hodit do ní metodu setDone()
.
Protože DAO/Repository (i když se definice různí) dohromady mají řešit
jen základní CRUD operace. Naopak presenter (nebo komponenty) by
neměly pomocí Repository provádět složitější operace, než čtení
s nastavením nějakého řazení/limitů. Takže už metoda
setDone()
musí být někde mezi Repository a aplikační
vrstvou.
Zjednodušeně se dá říct, že aplikace (presentery, komponenty) může
pomocí čistých Repository získávat data, ale ukládat by je měla pomocí
servisní třídy.
Otázka zní, jak bychom těmto třídám servisní vrstvy/fasády měli
říkat:
TaskService, TaskFacade, TaskManager, TaskModel,…? Můj favorit je
TaskService. Protože u složitějších aplikací může být takováto
třída i k jedné konkrétní činnosti nad více repozitáři (např.
PublishService).
před 6 lety
- Šaman
- Člen | 2275
Tak jsem na poslední chvíli ještě stihl aktualizaci –
refaktoring Authenticatoru (zrušení závislosti na Connection), s tím
souvisí ukázka virtuální metody v anotaci UserRepository.
A druhá věc je zavedení servisní třídy TaskService. Aspoň jako první
nástřel.
Teď jedu na víkend, vrátím se v pondělí. LM prý bude na poslední sobotě, tak doufám, že bude nějaký záznam přednášky.
před 6 lety
- Casper
- Člen | 253
@Šaman:
Zjednodušeně se dá říct, že aplikace (presentery, komponenty) může pomocí čistých Repository získávat data, ale ukládat by je měla pomocí servisní třídy.
Máš pravdu, nicméně osobně kvůli jednotnému přístupu používám všude v aplikaci fasády. K Repository/DAO přistupuji jen z fasád. Nelíbilo se mi totiž, že by aplikace měla přístup k tak obecné metodě jako je persist. Ukládání entit vyžaduje většinou dost práce okolo (navázání jiných entit, transakce, emailing, …) a to by rozhodně neměl dělat nějaký presenter. Samozřejmě můžeš mít konvenci, že nic takového dělat nebudeš, ale preferuji trochu defenzivnější přístup :)
Otázka zní, jak bychom těmto třídám servisní vrstvy/fasády měli říkat.
Osobně mám vše pojmenované *Facade, nicméně vše kromě *Model mi přijde použitelné.
Možná bych spíše zvážil co vyžadovat ve fasádních metodách za parametry. Zda entity, či jejich id. Osobně používám Entity, abych měl jistotu typové kontroly a nemohl nechtěně předat například id jiné entity.
A ještě jsem si u různých komponent (např. CatalogControl) všiml, že na jejich vytvoření nepoužíváš generované továrničky i přesto, že ti to běží na Nette 2.1. Máš k tomu nějaký důvod? Vždyť takhle musíš zbytečně vyžadovat cizí závislosti v presenteru jen proto, abys je předal této komponentě. Tohle za tebe může vyřešit DIC.
A ještě taková technická. Měl jsem za to, že při použití composeru se složka s knihovnami necommituje. Máš nějaký důvod, proč máš na githubu i složku libs? … a vůbec, jak se ti podařilo přes composer nainstalovat dev nette bez testů apod?
před 6 lety
- tsusanka
- Člen | 23
Měl bych ještě dotaz na Collection. Např. v UserRepository máš getByUsername($username), předpokládejme, že tam je ještě findByFirstName a vracelo by to kolekci, protože jméno není unikátní. Jak bys nyní udělal pokud bys tento výsledek chtěl seřadit? To by asi ideálně mělo být možné na Collection, ne? Ale ta už má onen výsledek, takže tam se k fluentu nijak nedostanu. Pletu se? Díky
před 6 lety
- Šaman
- Člen | 2275
tsusanka: Collection vychází z nativní LM Entity getCollection(), která jako parametr obdrží čisté pole dat a já jen místo pole vytvářím ArayObject. Případné řazení a limity je tedy nutné řešit ručně a pokud chceš řadit už na úrovni databáze, tak musíš v mapperu. Na ‚objektové straně‘ ORM už by nemělo být žádné DibiFluent, repository ta data mohla čistě teoreticky získat třeba přes MemcacheMapper a tedy bez nějakého dibi datasource.
Casper:
- Fasády – zkusím to, ale pak bude potřeba přidat nějakou podporu anotací, která by virtuální metody zapsané v anotaci delegovala na Repository. Zastávám názor, že cokoliv nemůž být napovídáno, to je špatně zdokumentováno, nebo příliš magické. A psát „trubičky“ – metody které jen předají informace repozitáři, to se mi nechce. Jinak mnohdy je zbytečné vytvářet Entity jen pro navázání na jinou entitu, proto akceptuji jak Entitu, tak její id. (Např. při vytvoření nového katalogu ho musím přiřadit nějakému uživateli. Id máme jednoduše v identitě, ale na vytvoření entoty User bych potřeboval i UserRepository, nebo UserService).
- Generované továrničky – máš pravdu, jsou lepší, jen jsem zatím nestihl převést všechny komponenty (ukázkou je LoginForm), bude co nejdříve.
- Nepoužívám Composer, právě třeba proto, že Nette mi to stahuje i se spoustou balastu. Navíc z minula mám zkušenosti s tím, že aplikace odladěná na jedné verzi nemusí šlapat v nové verzi. A 2.1 je stále dev a mění se. Takže v repozitáři je všechno, kromě souboru config.local.neon, který je v gitignore a je potřeba ho vytvořit zkopírováním a přejmenováním souboru config.local.neon.example. A pak ho samozřejmě upravit podle sebe.
před 6 lety
- Jan Tvrdík
- Nette guru | 2550
Šaman wrote: Nepoužívám Composer, právě třeba proto, že Nette mi to stahuje i se spoustou balastu.
Musíš to instalovat s přepínačem --prefer-dist
. Pak se
např. nestahují testy. Viz .gitattributes.
před 6 lety
- tsusanka
- Člen | 23
@Šaman myslíš, že je tedy okej to řadit (limitovat) až v rámci Collection phpkem? To při větším oběmu dat přeci smrdí, ne? Jinak v repu bych to musel napsat celý znovu, např:
public function findByPrice($price)
{
$data = $this->connection->select('*')
->from($this->getTable())
->where('[price] = %i', $price)
->orderBy('[price] DESC')
->fetchAll();
}
a nemohu zde použít magické findByPrice a pak až seřadit. Říkám to
dobře?
(sory za nechápavost, jen si to chci ujasnit..)
před 6 lety
- Šaman
- Člen | 2275
tsusanka:
- LM vytvoří array, metoda getCollection ho jenom obalí objektem. V současné verzi LM není možné vracet Datasource (Dibi fluent). To nesouvisí s moji ukázkovou aplikací, ale je to jedna z vlastností použitého ORM.
Datasource je totiž stále dost blízko databázi, v datasource např.
není možné řadit podle vypočítané property (které neodpovídá žádný
sloupec v DB), případně řadit podle číselníků (třeba abecední
řazení podle typu, pokud je ve sloupci typ uloženo jen jeho id). Tohle
ORM nám vrací pole hotových objektů, zatímco
DibiFluent je jen připravený SQL dotaz. Proto již není
možné výsledek na objektové straně nějak filtrovat pomocí SQL.
Je to jeden z důvodů, proč jsem vytvořil třídu Collection – chci do
ní přidat i metody pro řazení, aby se to nemuselo dělat na úrovni
presenteru pomocí procedurálních metod pro řazení polí.
Vzhledem k tomu, že nejsem autorem LM, tak tento bod bez trochu
s rezervou. Neznám všechny možnosti LeanMapperu.
- Problém s nepoužitelností magických get a find metod pro další modifikaci ještě v repository je dobrá připomínka a zkusím se nad tím ještě zamyslet. Myslím, že uvnitř modelu (aspoň uvnitř repository) by se dotazy skládat mohly.
Editoval Šaman (28. 10. 2013 13:32)
před 6 lety
- duke
- Člen | 637
@Šaman Obávám se, že tsusanka má pravdu. V situaci kdy máš v tabulce větší počet záznamů, musíš prostě řadit záznamy na úrovni SQL. Nechat si vytvořit tisíce objektů, abys je pak mohl setřídit v nějakém speciálním třídícím Collection objektu, za účelem toho, abys mohl např. 10 z nich zobrazit, je cesta do pekel. Rozhodně bych takováto řešení do vzorového projektu nedával. Spíše bych se přimlouval za nějaký jednoduchý query objekt, který taková třídění a stránkování na úrovni SQL umožní, a ukáže tak začátečníkům s LM, jak problém vhodně řešit.
před 6 lety
- Šaman
- Člen | 2275
@duke: Obávám se, že si nerozumíme.
- ORM vrací objekty, nikoliv datasource. To je jedna z jeho klíčových vlastností pravých ORM a s tím se musí pracovat. Má to už v názvu – objektově relační mapper.
- Datasource (často používaný třeba v různých datagridech) je často query builder, tedy dělá podobnou činnost jako ORM, ale s jinou filosofií.
- Nad ORM můžeme pracovat s property jednotlivých entit (třeba řadit podle vypočtené hodnotou, která v db není – zkuste si jinak vyřešit problém vyhledávání a řazení obdélníků podle obsahu, když v databázi máte jen rozměry stran). Nad datasourcem naopak můžeme efektivně modifikovat přímo SQL dotaz – datasource má daleko blíže k databázi. Takže je často vhodnější pro různé tabulkové výpisy a stránkování.
- Každý přístup má tedy určité nevýhody a samozřejmě existují
způsoby, jak se s nimi vypořádat. Nevýhoda datasourců se dá řešit
joiny, nebo výpočty na úrovni databáze (čímž se nám ale trochu rozplizne
aplikační logika mezi PHP a databázi a navíc se snižuje přenositelnost
mezi různými db enginy). Nevýhoda ORMů se dá řešit tak, že v repository
(nebo jiné ORM vrstvě) máme dotazy pro náš konkrétní dotaz. Takže např.
$userRepository->findByName($name, $order = 'ASC', $limit = NULL);
. To, že to ukázkový příklad zatím neumí, je jiná věc a podle této diskuze to asi má prioritu.
⇒ Rozhodně nebudeme hledat a hlavně VRACET spoustu zbytečných záznamů, to by byla školácká chyba. Jen Collection není datasource, takže již není možné ovlivňovat dotaz do db (to musíme udělat dříve) a řadící funkce jsou jen pomocné. Třeba pokud chceme všechny studenty nějaké třídy (v dotazu nebudeme ani řadit, ani limitovat), tak je před výpisem můžeme seřadit. A nebo seřadíme všechny vrácené obdélníky podle obsahu, protože již máme objekty, které příslušnou propery mají.
Koukal jsem na nějaké datagridy a třeba DoctrineDataSource jen využívá její nízkoúrovňový přístup k databázi. V tom případě my můžeme použít na běžnou práci LeanMapper a třeba pro NiftyGrid použijeme DibiFluentDataSource (ale pozor, je to trochu obejití LM a proto se nemůžeme spolehnout na pravidla, která jsou definovaná v LM entitách (můžeme třeba vložit do db neexistující hodnotu výčtu)).
Je to teď trochu srozumitelnější?
před 6 lety
- Jan Suchánek
- Backer | 403
@duke: Tzn.: nepoužívat/používat Query object jak o něm mluví Tharos na PS a píše tady?
před 6 lety
- Šaman
- Člen | 2275
Tak jsem viděl přednášku z #PoSoboty a pár věcí časem nejspíš
předělám. QueryObject pravděpodobně slouží k tomu, na co se ptal
@tsusanka.
Nicméně do teď jsem o QueryObjectu neslyšel a základní práce se obejde
bez něho.
Složitější záležitosti, třeba komunikace s DataGridem pomocí
LeanMapperu (tedy bez obcházení jak jsem psal výše) budou muset být nejprve
zdokumentovány od Tharose, nebo si je musím vyzkoušet.
před 6 lety
- Jan Suchánek
- Backer | 403
@Šaman: Gridu předat jen co potřebuje Connection nebo DataSource. Casper o tom píše tady.
Jen nevím jak případnou službu pojmenovat (možná \Datagrid\Repository). Slušně v tom plavu.
Beru to tak, že s Gridem se netrápit, popřípadě si vyrobit vlastní, ale na ostatní používat jen LM.
Casper napsal(a):
@jenicek:
Nevidím moc důvod proč aplikaci zprostředkovávat DibiRows či dokonce celý Fluent. Od toho chceš používat ORM, aby jsi pracoval čistě s Entitami ne? Pokud chceš používat ORM, napiš si model tak, aby ti vracel jen entity a v aplikaci pak pracuj jen s nimi. Výjimkami snad můžou být jen Datagridy, které Fluent vyžadují jako nějaké své DataSource. Nicméně těm už můžeš injectnout celé DibiConnection, to s ORM (a Repository) moc nesouvisí (případně nechť mě zkušenější opraví jak to řeší oni).
před 6 lety
- duke
- Člen | 637
Šaman napsal:
@duke: Obávám se, že si nerozumíme.
- ORM vrací objekty, nikoliv datasource. To je jedna z jeho klíčových vlastností pravých ORM a s tím se musí pracovat. Má to už v názvu – objektově relační mapper.
- Datasource (často používaný třeba v různých datagridech) je často query builder, tedy dělá podobnou činnost jako ORM, ale s jinou filosofií.
V těchto bodech si naprosto rozumíme. Nikdy jsem nepředpokládal ani netvrdil opak.
- Nad ORM můžeme pracovat s property jednotlivých entit (třeba řadit podle vypočtené hodnotou, která v db není – zkuste si jinak vyřešit problém vyhledávání a řazení obdélníků podle obsahu, když v databázi máte jen rozměry stran). Nad datasourcem naopak můžeme efektivně modifikovat přímo SQL dotaz – datasource má daleko blíže k databázi. Takže je často vhodnější pro různé tabulkové výpisy a stránkování.
Toto je pravda pouze ve speciálních případech, kdy v php skriptu pracujeme se všemi záznamy z tabulky (tj. veškeré řazení a stránkování pak místo v DB děláme v PHP). Pokud je ale záznamů hodně, bez databázového řazení a stránkování se neobejdeme, chceme-li aby aplikace pracovala efektivně. Navíc databázové řazení lze použít i v případě menšího počtu záznamů (a i zde nejspíš efektivněji než v PHP), takže řazení na úrovni ORM je něco, co považuji za nadbytečné. Co se týče zmíněného příkladu řazení obdélníků podle obsahu, platí, že pokud předem vím, že budu potřebovat řadit tímto způsobem, dává smysl mít obsah těchto obdélníku uložený v databázi a mít také nad příslušným sloupcem vytvořený index. Řadit to až v PHP může být praktické pouze pokud mám v takové tabulce jen malé množství obdélníků.
- Každý přístup má tedy určité nevýhody a samozřejmě existují způsoby, jak se s nimi vypořádat. Nevýhoda datasourců se dá řešit joiny, nebo výpočty na úrovni databáze (čímž se nám ale trochu rozplizne aplikační logika mezi PHP a databázi a navíc se snižuje přenositelnost mezi různými db enginy). Nevýhoda ORMů se dá řešit tak, že v repository (nebo jiné ORM vrstvě) máme dotazy pro náš konkrétní dotaz. Takže např.
$userRepository->findByName($name, $order = 'ASC', $limit = NULL);
. To, že to ukázkový příklad zatím neumí, je jiná věc a podle této diskuze to asi má prioritu.
To, že si uložím předvypočtené obsahy obdélníků do databáze nepovažuji za rozpliznutí aplikační logiky. Prostě si ukládám pomocná data, která využiji v budoucnu (pro rychlejší řazení). Možná ale mluvíš o něčem jiném, alespoň ten argument se snížením přenositelnosti tomu nasvědčuje, neboť já tu nic takového nevidím.
⇒ Rozhodně nebudeme hledat a hlavně VRACET spoustu zbytečných záznamů, to by byla školácká chyba. Jen Collection není datasource, takže již není možné ovlivňovat dotaz do db (to musíme udělat dříve) a řadící funkce jsou jen pomocné. Třeba pokud chceme všechny studenty nějaké třídy (v dotazu nebudeme ani řadit, ani limitovat), tak je před výpisem můžeme seřadit. A nebo seřadíme všechny vrácené obdélníky podle obsahu, protože již máme objekty, které příslušnou propery mají.
Ten pomocný charakter řadících funkcí mi pořád nějak není jasný. Pokud tedy nemluvíš jen o tom speciálním případu, kdy mám jen několik málo záznamů v tabulce a řadím je podle vypočítané property, kterou se mi nechtělo ukládat do databáze. A to mi přijde natolik specifické, že bych to do vzorové aplikace nedával. Spíše to nováčky svede z cesty a budou tím chtít řešit i situace s větším počtem záznamů…
jenicek napsal:
@duke: Tzn.: nepoužívat/používat Query object jak o něm mluví Tharos na PS a píše tady?
Nevím, co říkal Tharos na PS (existuje nějaký záznam?), ale to, o čem píše na uvedeném odkazu, zní rozumně. Já jsem spíš pro query objekt než pro větší počet speciálních metod. Mám za to, že s tím bude méně psaní a méně nutnosti si pamatovat pořadí parametrů atp. (což ale nemusí být problém pro někoho, kdo používá nějaké lepší IDE s napovídáním).
Editoval duke (30. 10. 2013 18:23)
před 6 lety
- Šaman
- Člen | 2275
Tharosova
přednáška na Poslední říjnové sobotě. (QueryBuiler se řeší až
v závěru v dotazech po skončení přednášky.)
Ostatní budu řešit až večer po tréninku.
Editoval Šaman (30. 10. 2013 18:33)
před 6 lety
- Jan Suchánek
- Backer | 403
@Šaman: Šlo by do quickstartu přidat nástin řešení propojení dvou tabulek s nějakým tím Query modelem a sortováním? Nebo je správné vše vyřešit na úrovni dibi + Query objectu a vypsat jen Entity z LM a až následně si standartně tahat další data?