Uwaga: poniżej znajduje się opis tego, jak rozumiem wzorce podobne do MVC w kontekście aplikacji internetowych opartych na PHP. Wszystkie linki zewnętrzne użyte w treści mają na celu wyjaśnienie terminów i pojęć, a nie sugerowanie mojej wiarygodności w tym temacie.
Pierwszą rzeczą, którą muszę wyjaśnić, jest: model jest warstwą .
Po drugie: istnieje różnica między klasycznym MVC a tym, czego używamy do tworzenia stron internetowych. Oto trochę starszej odpowiedzi, którą napisałem, która krótko opisuje, jak się różnią.
Czym NIE jest model:
Model nie jest klasą ani żadnym pojedynczym obiektem. Bardzo często popełniany jest błąd (ja też to zrobiłem, chociaż oryginalna odpowiedź została napisana, gdy zacząłem uczyć się inaczej) , ponieważ większość ram utrwala to błędne przekonanie.
Nie jest to również technika mapowania obiektowo-relacyjnego (ORM) ani abstrakcja tabel baz danych. Każdy, kto mówi inaczej, najprawdopodobniej próbuje „sprzedać” inną zupełnie nową ORM lub całą platformę.
Co to jest model:
Przy odpowiedniej adaptacji MVC, M zawiera całą logikę biznesową domeny, a Warstwa Modelowa składa się głównie z trzech rodzajów struktur:
Obiekty Domeny
Obiekt domeny jest logicznym kontenerem zawierającym wyłącznie informacje o domenie; zazwyczaj reprezentuje logiczny byt w obszarze problemowym. Powszechnie nazywane logiką biznesową .
W tym miejscu możesz zdefiniować sposób sprawdzania poprawności danych przed wysłaniem faktury lub obliczenia całkowitego kosztu zamówienia. Jednocześnie obiekty domenowe są całkowicie nieświadome miejsca do przechowywania - ani skąd (baza danych SQL, interfejs API REST, plik tekstowy itp.), Ani nawet jeśli zostaną zapisane lub odzyskane.
Mapujący dane
Te obiekty są odpowiedzialne tylko za przechowywanie. Jeśli przechowujesz informacje w bazie danych, to właśnie tam mieszka SQL. A może używasz pliku XML do przechowywania danych, a twoi maperzy parsują zi do plików XML.
Usługi
Możesz myśleć o nich jako o „obiektach domeny wyższego poziomu”, ale zamiast logiki biznesowej, Usługi są odpowiedzialne za interakcję między obiektami domeny a Mapperami . Struktury te tworzą w końcu „publiczny” interfejs do interakcji z logiką biznesową domeny. Możesz ich uniknąć, ale pod groźbą wycieku logiki domeny do kontrolerów .
W pytaniu dotyczącym implementacji ACL znajduje się odpowiednia odpowiedź na ten temat - może być przydatna.
Komunikacja między warstwą modelu a innymi częściami triady MVC powinna odbywać się tylko za pośrednictwem Usług . Wyraźna separacja ma kilka dodatkowych zalet:
- pomaga egzekwować zasadę jednej odpowiedzialności (SRP)
- zapewnia dodatkowy „pokój poruszania się” na wypadek zmiany logiki
- sprawia, że kontroler jest tak prosty, jak to możliwe
- daje jasny plan, jeśli kiedykolwiek potrzebujesz zewnętrznego interfejsu API
Jak wchodzić w interakcje z modelem?
Wymagania wstępne: obejrzyj wykłady „Global State and Singletons” i „Don't Look For Things!” z rozmów na temat czystego kodu.
Uzyskiwanie dostępu do wystąpień usług
Zarówno w przypadku widoków, jak i kontrolerów (które można nazwać „warstwą interfejsu użytkownika”) w celu uzyskania dostępu do tych usług, istnieją dwa ogólne podejścia:
- Możesz wstrzyknąć wymagane usługi bezpośrednio do konstruktorów twoich widoków i kontrolerów, najlepiej używając kontenera DI.
- Używanie fabryki dla usług jako obowiązkowej zależności dla wszystkich twoich widoków i kontrolerów.
Jak można podejrzewać, pojemnik DI jest o wiele bardziej eleganckim rozwiązaniem (choć nie jest najłatwiejszy dla początkującego). Dwie biblioteki, które polecam rozważyć pod kątem tej funkcjonalności, to samodzielny komponent DependencyInjection firmy Syfmony lub Auryn .
Zarówno rozwiązania wykorzystujące fabrykę, jak i kontener DI pozwoliłyby również na udostępnianie wystąpień różnych serwerów, które mają być współużytkowane przez wybrany kontroler i podgląd dla danego cyklu żądanie-odpowiedź.
Zmiana stanu modelu
Teraz, gdy masz dostęp do warstwy modelu w kontrolerach, musisz zacząć z nich korzystać:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
Twoje kontrolery mają bardzo jasne zadanie: weź dane wejściowe użytkownika i, na podstawie tych danych wejściowych, zmień aktualny stan logiki biznesowej. W tym przykładzie zmieniane są stany: „użytkownik anonimowy” i „użytkownik zalogowany”.
Kontroler nie jest odpowiedzialny za sprawdzanie poprawności danych wejściowych użytkownika, ponieważ jest to część reguł biznesowych, a kontroler zdecydowanie nie wywołuje zapytań SQL, takich jak to, co zobaczysz tutaj lub tutaj (proszę ich nie nienawidzić, są wprowadzane w błąd, nie są złe).
Pokazuje użytkownikowi zmianę stanu.
Ok, użytkownik się zalogował (lub nie powiódł się). Co teraz? Ten użytkownik nadal nie jest tego świadomy. Musisz więc właściwie zareagować i to jest odpowiedzialność za widok.
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
W tym przypadku widok wygenerował jedną z dwóch możliwych odpowiedzi, w oparciu o bieżący stan warstwy modelu. W przypadku innego przypadku użycia widok wybierałby różne szablony do renderowania, na podstawie czegoś takiego jak „aktualnie wybrany artykuł”.
Warstwa prezentacji może być dość skomplikowana, jak opisano tutaj: Zrozumienie widoków MVC w PHP .
Ale właśnie tworzę interfejs API REST!
Oczywiście zdarzają się sytuacje, w których jest to przesada.
MVC jest tylko konkretnym rozwiązaniem dla zasady separacji problemów . MVC oddziela interfejs użytkownika od logiki biznesowej, aw interfejsie użytkownika oddzielił obsługę danych wejściowych i prezentacji użytkownika. To jest kluczowe. Chociaż często ludzie określają to jako „triadę”, tak naprawdę nie składa się ona z trzech niezależnych części. Struktura jest bardziej taka:
Oznacza to, że gdy logika warstwy prezentacji jest prawie nieistniejąca, pragmatyczne podejście polega na zachowaniu jej jako pojedynczej warstwy. Może także znacznie uprościć niektóre aspekty warstwy modelu.
Korzystając z tego podejścia, przykład logowania (dla interfejsu API) można zapisać jako:
public function postLogin(Request $request)
{
$email = $request->get('email');
$data = [
'status' => 'ok',
];
try {
$identity = $this->identification->findIdentityByEmailAddress($email);
$token = $this->identification->loginWithPassword(
$identity,
$request->get('password')
);
} catch (FailedIdentification $exception) {
$data = [
'status' => 'error',
'message' => 'Login failed!',
]
}
return new JsonResponse($data);
}
Chociaż nie jest to trwałe, jeśli masz skomplikowaną logikę renderowania treści odpowiedzi, to uproszczenie jest bardzo przydatne w przypadku bardziej trywialnych scenariuszy. Ale uwaga , takie podejście stanie się koszmarem, gdy spróbujesz użyć go w dużych bazach kodowych ze złożoną logiką prezentacji.
Jak zbudować model?
Ponieważ nie ma jednej klasy „Model” (jak wyjaśniono powyżej), tak naprawdę nie „buduje się modelu”. Zamiast tego zaczynasz od tworzenia Usług , które są w stanie wykonywać określone metody. A następnie zaimplementuj Obiekty Domeny i Maperów .
Przykład metody usługi:
W obu powyższych podejściach zastosowano tę metodę logowania do usługi identyfikacji. Jak by to faktycznie wyglądało. Używam nieco zmodyfikowanej wersji tej samej funkcjonalności z biblioteki , którą napisałem ... ponieważ jestem leniwy:
public function loginWithPassword(Identity $identity, string $password): string
{
if ($identity->matchPassword($password) === false) {
$this->logWrongPasswordNotice($identity, [
'email' => $identity->getEmailAddress(),
'key' => $password, // this is the wrong password
]);
throw new PasswordMismatch;
}
$identity->setPassword($password);
$this->updateIdentityOnUse($identity);
$cookie = $this->createCookieIdentity($identity);
$this->logger->info('login successful', [
'input' => [
'email' => $identity->getEmailAddress(),
],
'user' => [
'account' => $identity->getAccountId(),
'identity' => $identity->getId(),
],
]);
return $cookie->getToken();
}
Jak widać, na tym poziomie abstrakcji nic nie wskazuje na to, skąd dane zostały pobrane. Może to być baza danych, ale może to być również próbny obiekt do celów testowych. Nawet osoby mapujące dane, które są do tego faktycznie używane, są ukryte w private
metodach tej usługi.
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
Sposoby tworzenia twórców map
Aby wdrożyć abstrakcję trwałości, najbardziej elastycznym podejściem jest stworzenie niestandardowych maperów danych .
Od: Książka PoEAA
W praktyce są one implementowane do interakcji z określonymi klasami lub nadklasami. Powiedzmy, że masz Customer
, a Admin
w kodzie (zarówno dziedziczenie z User
klasy nadrzędnej). Oba prawdopodobnie miałyby osobne pasujące mapowanie, ponieważ zawierają one różne pola. Ale skończysz także na wspólnych i często używanych operacjach. Na przykład: aktualizacja czasu „ostatni raz online” . I zamiast uczynić obecnych twórców map bardziej skomplikowanymi, bardziej pragmatycznym podejściem jest stworzenie ogólnego „User Mapper”, który aktualizuje tylko ten znacznik czasu.
Kilka dodatkowych komentarzy:
Tabele i model bazy danych
Podczas gdy czasami istnieje bezpośredni związek 1: 1: 1 między tabelą bazy danych, obiektem domeny i maperem , w większych projektach może być mniej powszechny niż się spodziewasz:
Informacje używane przez pojedynczy obiekt domeny mogą być mapowane z różnych tabel, podczas gdy sam obiekt nie ma trwałości w bazie danych.
Przykład: jeśli generujesz raport miesięczny. To zbierałoby informacje z różnych tabel, ale MonthlyReport
w bazie danych nie ma magicznej tabeli.
Pojedynczy program mapujący może wpływać na wiele tabel.
Przykład: gdy przechowujesz dane z User
obiektu, ten obiekt domeny może zawierać kolekcję innych obiektów domeny - Group
instancji. Jeśli je zmienisz i zapiszesz User
, program mapujący dane będzie musiał zaktualizować i / lub wstawić wpisy w wielu tabelach.
Dane z jednego obiektu domeny są przechowywane w więcej niż jednej tabeli.
Przykład: w dużych systemach (pomyśl: średniej wielkości sieć społecznościowa) przechowywanie danych uwierzytelniających użytkownika i często używanych danych osobno od większych fragmentów treści może być pragmatyczne, co jest rzadko wymagane. W takim przypadku nadal możesz mieć jedną User
klasę, ale informacje w niej zawarte zależą od tego, czy zostały pobrane pełne szczegóły.
Dla każdego obiektu domeny może być więcej niż jeden program odwzorowujący
Przykład: masz witrynę z wiadomościami ze wspólnym kodem opartym zarówno na oprogramowaniu publicznym, jak i oprogramowaniu do zarządzania. Ale chociaż oba interfejsy używają tej samej Article
klasy, zarządzanie potrzebuje znacznie więcej informacji. W takim przypadku mielibyśmy dwa oddzielne elementy mapujące: „wewnętrzny” i „zewnętrzny”. Każde z nich wykonuje inne zapytania, a nawet korzysta z różnych baz danych (jak w trybie master lub slave).
Widok nie jest szablonem
Wyświetl instancje w MVC (jeśli nie używasz wariantu wzorca MVP) są odpowiedzialne za logikę prezentacji. Oznacza to, że każdy widok zwykle żongluje co najmniej kilkoma szablonami. Pozyskuje dane z warstwy modelu a następnie na podstawie otrzymanych informacji wybiera szablon i ustawia wartości.
Jedną z korzyści, jakie z tego zyskujesz, jest możliwość ponownego użycia. Jeśli utworzysz ListView
klasę, to z dobrze napisanym kodem możesz mieć tę samą klasę, która przekazuje listę użytkowników i komentarze poniżej artykułu. Ponieważ oba mają tę samą logikę prezentacji. Po prostu zmieniasz szablony.
Możesz użyć natywnych szablonów PHP lub innego silnika szablonów. Mogą też istnieć biblioteki innych firm, które mogą całkowicie zastąpić wystąpienia programu View .
Co ze starą wersją odpowiedzi?
Jedyną istotną zmianą jest to, co nazywa się Modelem w starej wersji jest w rzeczywistości Usługą . Reszta „analogii bibliotecznej” nieźle sobie radzi.
Jedyną wadą, jaką widzę, jest to, że byłaby to naprawdę dziwna biblioteka, ponieważ zwróciłaby ci informacje z książki, ale nie pozwoliłaby ci dotknąć samej książki, ponieważ w przeciwnym razie abstrakcja zacząłaby „wyciekać”. Być może będę musiał wymyślić bardziej odpowiednią analogię.
Jaki jest związek między widokami a instancjami kontrolera ?
Struktura MVC składa się z dwóch warstw: interfejsu użytkownika i modelu. Głównymi strukturami w warstwie interfejsu użytkownika są widoki i kontroler.
Kiedy masz do czynienia ze stronami internetowymi, które używają wzorca projektowego MVC, najlepszym sposobem jest uzyskanie stosunku 1: 1 między widokami a kontrolerami. Każdy widok reprezentuje całą stronę w Twojej witrynie i ma dedykowany kontroler do obsługi wszystkich przychodzących żądań dla tego konkretnego widoku.
Na przykład, aby przedstawić otwarty artykuł, będziesz mieć \Application\Controller\Document
i \Application\View\Document
. Zawierałoby to wszystkie główne funkcje warstwy interfejsu użytkownika, jeśli chodzi o obsługę artykułów (oczywiście możesz mieć niektóre komponenty XHR , które nie są bezpośrednio związane z artykułami) .