Model—view—controller: uvod u MVC arhitekturu, na primeru Magenta

Šta je Model—view—controller arhitektura? Odgovaramo na ovo pitanje sadržajem koji vam može pomoći da pišete aplikacije koje su dosta lakše za održavanje.

Mladen Ilić - 25. Maj, 2018.

U ovom i seriji članaka u narednom periodu ćemo razmatrati različite fundamentalne teme vezane za programiranje, a kroz prizmu alata koji dobro znamo i volimo — Magenta. U ovom ćemo se pozabaviti Model-View-Controller arhitekturom.

Većina nas koja je pokušala da istraži MVC prvo je postavila pitanje „Šta je MVC?“. I verovatno je naišla na odgovor da to znači Model — View — Controller i da je to Architectual pattern.

Ako vama, kao i meni, ovo nije značilo puno, nastavili ste da istražujete dalje i došli do nekih dijagrama poput ova dva.

Međutim, oni nisu previše jasni, čak nisu ni među sobom potpuno konzistentni. Prvi pokazuje Controller na View, a na drugom nema ni toga.

Zato nam je nada da ćemo ovim člankom uspeti da pojasnimo stvar, da bolje odgovorimo na prethodno postavljeno pitanje, i da usput vidimo i kako se ovaj patern primenjuje u Magentu.

Počećemo primerom — zamislićemo jednostavnu aplikaciju i probleme s kojima se možemo susresti u implementaciji, i osvrnuti se na to kako nam MVC pomaže da rešimo te probleme.

Ukoliko imamo zadatak da napravimo blog i pravimo post stranu gde se prikazuje sadržaj, ovo bi bio standardan markup.

Prva sledeća logična stvar koju hoćemo da uradimo je da iz baze izvučemo sadržaj za ovu stranu. Standardan PHP način da ovo uradimo bi izgledao ovako: imamo konekciju ka bazi, zatim query koji izvršavamo, dovodimo rezultate ina kraju sve to zajedno prikažemo.

Za ovako jednostavan primer ovo možda deluje OK.

Međutim, prvi problemi na koje nailazimo nastaju kada počnemo da dobijamo nove zahteve, kada ova aplikacija krene da raste. Na primer, prvi sledeći logičan zahtev bio bi da implementiramo komentare, tj. da ih prikažemo na strani sa postovima. I onda ovaj query na početku, koji već nije bio reprezentativan, sad je postao još gori, a dole imamo opet deo neke logike koji ispisuje taj komentar.

Možemo da zamislimo da naša aplikacija prestaje da koristi MySQL i želimo da migriramo bazu na Mongo. To bi bilo nemoguće u našoj aplikaciji, pošto u svim View-ovima imamo implementiranu logiku i to bi podrazumevalo da moramo manje-više da pišemo aplikaciju iz početka.

Ideja iza Model-View-Controllera je baš to da — ne moramo.

View

Svrha svih Architectual paterna je da nam pomogne u tome kako organizujemo aplikaciju, na koji način implementiramo nove funkcinalnosti koje gradimo u njoj, kako bi ona mogla da bude skalabilna, da bi održavanje bilo jednostavno i, na kraju, da nam omogući i pisanje automatskih testova.

Možemo da kažemo da ovaj dijagram prikazuje View:

deo aplikacije koji je zadužen da našem krajnjem korisniku prezentuje konačan rezultat.

Sledeći korak koji ćemo napraviti kako bismo ovu stranu refaktorisali pomoću MVC paterna je da implementiramo model.

Model

Ako pratimo MVC, sledi nam da ovo nije mesto za bilo kakvu logiku vezanu za našu aplikaciju. Prvi korak koji ćemo raditi u refaktorisanju ove aplikacije je da implementiramo model koji će preuzeti tu logiku.

Model je dakle deo naše aplikacije, jedan segment MVC paterna, koji bi trebalo da, između ostalog, bude zadužen za manipulisanje podacima. Ovo je jedan primer ORM-a, osnove našeg modela. Svaki model bi trebalo da ima implementirane CRUD akcije, tj. akcije koje će mu omogućavati kreiranje (create) novih podataka, čitanje (read), ažuriranje (update) i brisanje (delete). U  našem modelu to izgledalo ovako, pri čemu je opet vredno imati na umu da je to jedan apstraktni primer koji nam pomaže da bolje shvatimo MVC.

Ako zamislimo da u našoj aplikaciji imamo ovaj model implementiran, onda bismo mogli da kažemo da bi i naš View trebao da bude sličan. Cela logika koja je vezana za query može da se izmesti, i umesto nje imamo jedan find request. Find je nešto na osnovu čega će naš model da pronađe i prikaže podatke u tom postu. Izvan toga, ostalo nam je manje-više isto.

Ono što je takođe bitno da razumemo je to da, pored toga što modeli implementiraju CRUD akcije, omogućavaju dohvatanje i manipulaciju podacima, imaju još jednu bitnu ulogu, a to je da su mesto gde smeštamo biznis logiku naše aplikacije.

Ako pogledamo sledeći korak, u kojem dohvatamo komentare, to je nešto što bi takođe trebalo da bude implementirano u modelu. Sa našim primerom MVC-a to bi moglo da izgleda ovako — imamo comment model i where gde dohvatamo sve komentare sa ovim post ID-jem.

Iz ovog koda deluje da će se samo u nekom trenutku pretvoriti u SQL i izvršiti, ali modeli nisu samo to — vrlo lako može da znači da se konvertuje u REST request, ka nekom REST service-u, koji dohvata post. Upravo u tome je elegantnost ovog paterna, to što iz View dela naše aplikacije izbacujemo svu logiku vezanu za dohvatanje.

Kada bismo imali zahtev da ovo refaktorišemo i da ovde migriramo aplikaciju sa MySQL-a na Mongo, to bi u ovoj novoj situaciji, sa primenjenim MVC-om, bilo dosta jednostavnije. Ceo View segment bi ostao nepromenjen, jer naš View ne zanima šta ovo post find radi, već je sve to inkapsulirano unutar modela.

Ovo je naš primer bez dela za postove.

Sada naša instanca posta može da dohvati komentare.

Controller

Sledeći segment MVC-a su kontroleri, koji su u MVC-ju ključan deo. Shvatili smo da view-ovi prikazuju, modeli imaju podatke i biznis logiku, a gde su sad tu uklapaju kontroleri?

Suština kontrolera je da probamo da spojimo prave modele sa pravim View-ovima na određenim stranama, tako da izbegnemo zavisnost između tih segmenata, na koji način poboljšavamo testiranje. Možemo da zamislimo situaciju u kojoj želimo nešto da testiramo, naš View ili naš kontroler, vrlo lako možemo da ubacimo neke podatke primere, tako da u pozadini nije potrebna ni baza niti bilo šta drugo, već odmah dobijamo rezultate koji pokrivaju testove. To je nešto sa prvobitnom verzijom naše aplikacije nije moguće.

Da bismo još bolje razumeli kontrolere, pogledaćemo kako Magento to sve implementira, a pre toga pravimo brzu digresiju na temu strukture Magenta.

Struktura Magenta

Magento kao platforma ima jedan glavni cilj — fleksibilnost. Ideja je da platforma omogući izmene, dodavanje funkcionalnosti i prilagođavanje bilo kom ecommerce biznis modelu. Način na koji Magento sa tehničke strane to omogućava su moduli. Moduli su nešto za šta možemo reći da liči na neki add-on ili ekstenziju i na taj način vi dodajete svoj kod u Magentu.

Ovako taj modul izgleda na Magentu 1, odnosno na  Magentu 2:

Za ovu priču o MVC-ju i Magentu, jako je bitno da shvatimo da verzija Magenta nije previše bitna. Ideje su iste, one koje su bile u jedinici, su zadržane u Magentu 2. Opet imamo standardne segmente: Model, View i Controller. U okviru Magenta oni su složeniji i imaju podsegmente u sebi.

Primera radi, View je kod nas bio statičan file, dok Magento, kako bi bio fleksibilniji, nastoji da View-ove podeli u podsegmente. Oni dosta liče na jedan MVC pattern za sebe, gde imamo template-e, što je naš HTML kod, kod bez logike — gde ćemo imati samo prikazivanje podataka.

Zatim imamo blokove. U njima se nalazi minimalna logika za prikazivanje tih podataka i imamo nešto što Magento zove Layout, a oni su jako slični Kontrolerima za View-ove. Ideja je da Magento omogući da se različiti blokovi povezuju sa različitim template-ima i da na taj način budu fleksibilniji, da vi za jedan proizvod možete imati različite prikaze bez izmene logike bloka. Sva logika za prikazivanje bloka ostaje ista, dok se samo menjaju template-i. Dva template-a iz različitog input-a mogu drugačije da se prikažu.

U Magentu su Modeli takođe podeljeni u tri segmenta. Imamo standardne modele — njihova namena je vrlo slična standardnom MVC modelu; sadrže CRUD akcije i omogućavaju manipulaciju podacima.

Zatim imamo još dva dodatna modela — jedan je Resource model, a drugi Collection model, i ovaj pristup je specifičan za Magento. Resource model u sebi sadrži logiku koja odlučuje na koji način će taj konkretni model dohvatati podatke. Tu se nalazi low level kod. Za naš blog, na primer, u okviru modela bismo imali metode koje bi nam pomagale da ga snimimo, da ga učitamo, obrišemo… Resource model bi definisao šta to znači — da li to znači konekcija ka bazi, da li to znači API request, Mongo….

Collection modeli u Magentu isto imaju specifičnu ulogu. Magento kao platforma često ima potrebu za manipulacijom, ne samo jednog entiteta, nego kolekcije, tačnije skupa entiteta, tako da postoje posebni modeli koji to omogućavaju. Oni omogućavaju da manipulišete, što ćemo videti kasnije na primeru komentara.

Nazad na kontrolere

Vraćamo se na kontrolere i razumevanje njihove ulogu. Kontroler bi trebalo da obradi ulaz u našu aplikaciju i da na osnovu tog ulaza odluči šta se dalje dešava. Kada pričamo o Magentu web aplikaciji, ulazi neki HTTP request sa nekim URL-om.

U ovom slučaju Magento obrađuje taj request preko front kontrolera, koji će parsati URL, podeliti ga u relevantne segmente i onda ga proslediti router-ima, koji će odlučiti šta treba uraditi sa request-om, tj. kuda ga proslediti.

Action kontroler je deo gde se nalazi naša kontroler logika — nešto što ćemo mi u našim modulima pisati i negde gde ćemo implementirati tu logiku.

Evo jednog primera Magento kontrolera — za naš blog post.

U našem kontroleru imamo View akciju, koja nešto radi. Rekli smo da je taj URL, taj HTTP request, ulaz u našu aplikaciju i MVC kao pattern ne definiše šta znači URL, šta treba da se radi, već Magento, kao jednu implementaciju framework-a radi konkretno određenu stvar. Magento ima sledeću URL strukturu:

Deli se u tri segmenta, pri čemu svaki URL ima svoj modul deo, svoj kontroler deo i svoju akciju. Za naš primer URL bi izgledao ovako — imali bismo blog post koji bi se obradio u ovakvu strukturu, gde je modul blog, post je kontroler i view je akcija. Ostatak su parametri koji se prosleđuju njoj, što bi naš router odradio, kako bi request došao do naše akcije, tako da ako biste poslali requst baš na ovaj URL očekivano je da taj request dođe do ove akcije.

U akciji prvo inicijalizujemo post, tako što iz request-a uzmemo ID i učitamo taj post. Ovde je sada veza između kontrolera i našeg modela, tako da se u postu nalazi to što učitamo. Nakon toga iz layout-a dohvatimo relevantan blok i tom bloku prosledimo naš post.

To je sada taj kritičan deo za ulogu kontrolera u našoj aplikaciji. Jedan model prosleđujemo jednom bloku, tako što kažemo bloku šta su podaci koje on treba da prikazuje. Taj blok u sebi nema logike budući da je deo View-a.

Ovde je posebno relevantna Get-Content funkcija koja treba da prosledi sadržaj našem View-u. Ovde možemo da vidimo podelu MVC odgovornosti između View-a i Modela. View nije “svestan” toga šta je sadržaj, funkcioniše nezavisno od toga to post ili šta god da može biti. Ako sutra imamo zahtev kako bismo dodatno implementirali page ili neki treći CMS entitet, ovo vrlo lako može da ostane i View za naš page.

Kada je layout u pitanju, ovako izgledaju layout update-ovi u Magentu:

Koncept je takav da želimo da povežemo konkretan blok sa konkretnim template-om i stoga imamo fizičku putanju do tog template-a, a tu je i parametar za Magentovu factory metodu, koja vraća instancu ovog bloka. To je način na koji, bez direktnog povezivanja između bloka i View-a, između template-a i bloka, mi možemo u naš template da dopremimo odgovarajuće podatke.

Pred sam kraj, osvrnućemo se na modele u Magentu. Ako pogledamo početni primer framework-a MVC-a i uporedimo ga sa Magentom videćemo da izgleda malo drugačije. U Magentu nema direktne razlike između Create-a i Update-a, već to zavisi od podataka na samom objektu modela, tako da ukoliko je na objektu modela set-ovan primary key to bi značilo da on treba da update-uje taj post, dok ukoliko nije to znači da treba da kreira novi.

Prilično je jednostavno rešen i find, tako što je Magentu to samo load, što znači da od Resource modela treba da dobije relevantne podatke na osnovu ovog parametra i load-ovani model može da se obriše.

Ostalo je da za našu aplikaciju implementiramo dohvatanje komentara za naše postove. To bi izgledalo ovako.

Ovde vidimo resource collection, odnosno Collection model kojim dohvatimo instancu, koja predstavlja skup više modela, više entiteta i onda ga filtriramo po post ID-ju.