Jak powinien być skonstruowany model w MVC?

Dopiero zaczynam rozumieć Framework MVC i często zastanawiam się, ile kodu powinno znaleźć się w modelu. Zazwyczaj mam klasę dostępu do danych, która ma metody takie jak Ta:

public function CheckUsername($connection, $username)
{
    try
    {
        $data = array();
        $data['Username'] = $username;

        //// SQL
        $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";

        //// Execute statement
        return $this->ExecuteObject($connection, $sql, $data);
    }
    catch(Exception $e)
    {
        throw $e;
    }
}

Moje modele są zazwyczaj klasą encji, która jest mapowana do tabeli bazy danych.

Czy obiekt modelu powinien mieć wszystkie właściwości zmapowane w bazie danych, jak również powyższy kod, czy może można oddzielić ten kod, który faktycznie działa w bazie danych?

Czy skończę mając cztery warstwy?
Author: i alarmed alien, 2011-05-03

5 answers

Zastrzeżenie: poniżej znajduje się opis tego, jak rozumiem wzorce podobne do MVC w kontekście aplikacji internetowych opartych na PHP. Wszystkie linki zewnętrzne, które są używane w treści są tam, aby wyjaśnić terminy i pojęcia, a nie, aby sugerować moją własną wiarygodność w tym temacie.

Pierwszą rzeczą, którą muszę wyjaśnić, jest: model jest warstwą.

Po drugie: istnieje różnica między klasycznym MVC i co wykorzystujemy w tworzeniu stron internetowych. oto nieco starsza odpowiedź, 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. Jest to bardzo częsty błąd, aby (ja też, choć oryginalna odpowiedź została napisana, gdy zacząłem uczyć się inaczej) {34]}, ponieważ większość frameworków utrwalić to błędne przekonanie.

Nie jest to ani Technika mapowania obiektowo-relacyjnego (ORM), ani abstrakcja tabel baz danych. Każdy, kto mówi inaczej, najprawdopodobniej próbuje "sprzedać" inny nowiutki ORM lub cały framework.

Czym jest model:

W odpowiedniej adaptacji MVC, M zawiera całą domenę logiki biznesowej, a warstwa modelu jest w większości zbudowana z trzech typów struktur:

  • Obiekty Domeny

    Obiekt domeny jest logicznym kontenerem czysto informacje o domenie; Zwykle reprezentuje logiczny byt w przestrzeni domeny problemowej. Powszechnie określane jako logika biznesowa .

    Tutaj definiujesz sposób walidacji danych przed wysłaniem faktury lub obliczasz całkowity koszt zamówienia. Jednocześnie obiekty domeny są całkowicie nieświadome przechowywania - ani z , gdzie (baza danych SQL, REST API, plik tekstowy itp.) ani nawet jeśli zostaną uratowani lub odzyskane.

  • Mapery Danych

    Te obiekty są odpowiedzialne tylko za przechowywanie. Jeśli przechowujesz informacje w bazie danych, będzie to miejsce, w którym mieszka SQL. A może używasz pliku XML do przechowywania danych, a twoi Maperzy danych parsują z I do plików XML.

  • usługi

    Można o nich myśleć jako o "obiektach domeny wyższego poziomu", ale zamiast logiki biznesowej, usługi są odpowiedzialne za interakcję pomiędzy obiektami domenyi Maperami . Struktury te tworzą "publiczny" interfejs do interakcji z logiką biznesową domeny. Można ich uniknąć, ale za karą wycieku logiki domeny do kontrolerów .

    Jest związana odpowiedź na ten temat w implementacja ACL pytanie - może się przydać.

[[19]}komunikacja pomiędzy warstwa modelu i inne części triady MVC powinny występować tylko za pośrednictwem usług . Wyraźne oddzielenie ma kilka dodatkowych korzyści:
    W 2007 roku, po raz pierwszy w historii, został wybrany do Izby Gmin.]} W przypadku zmiany logiki, w przypadku zmiany logiki dodaje się "wiggle room".]}
  • utrzymuje kontroler tak prosto, jak to możliwe
  • Jeśli kiedykolwiek potrzebujesz zewnętrznego API, możesz to zrobić w prosty sposób.]}

 

Jak współdziałać z modelka?

wymagania wstępne: obejrzyj wykłady "Global State and Singletons" i " nie szukaj rzeczy!" z rozmów o czystym kodzie.

Uzyskiwanie dostępu do instancji usług

Dla obu instancji View i Controller (co można nazwać: "UI layer"), aby mieć dostęp do tych usług, istnieją dwa ogólne podejścia:

  1. można wstrzyknąć wymagane usługi w konstruktorach Twoich widoków i sterownikach bezpośrednio, najlepiej za pomocą kontenera DI.
  2. 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łatwiejszym dla początkujących). Dwie biblioteki, które polecam rozważyć dla tej funkcjonalności, to samodzielny komponent DependencyInjection lub Auryn .

Zarówno rozwiązania wykorzystujące kontener fabryczny, jak i kontener DI pozwalają również na współdzielenie instancji różnych serwerów, które mają być współdzielone między wybranym kontrolerem i widokiem dla danego cyklu odpowiedzi na żądanie.

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: wziąć wkład użytkownika i, w oparciu o to wejście, zmiana aktualnego stanu logiki biznesowej. W tym przykładzie stany, które zostały zmienione, to "użytkownik anonimowy" i "użytkownik zalogowany".

Controller nie jest odpowiedzialny za walidację danych wejściowych użytkownika, ponieważ jest to część reguł biznesowych i controller na pewno nie wywołuje zapytań SQL, jak to, co można zobaczyć tutaj lub tutaj (proszę nie nienawidzić ich, są błędne, nie złe).

Pokazywanie użytkownikowi zmiany stanu.

Ok, użytkownik zalogował się (lub nie powiódł się). Co teraz? powiedział użytkownik nadal nie jest tego świadomy. Więc trzeba rzeczywiście stworzyć odpowiedź i to jest odpowiedzialność pogląd.

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, opartych na aktualnym stanie warstwy modelu. W innym przypadku użycia będziesz miał widok wybierając różne szablony do renderowania, w oparciu o coś w rodzaju "aktualny wybrany artykuł".

Warstwa prezentacji może rzeczywiście uzyskać dość rozbudowane, jak opisano tutaj: zrozumienie widoków MVC w PHP .

Ale właśnie robię REST API!

Oczywiście, są sytuacje, kiedy jest to przesada.

MVC jest tylko konkretnym rozwiązaniem dla zasady rozdziału obaw . MVC oddziela interfejs użytkownika od logiki biznesowej, a w interfejsie użytkownika oddziela obsługę wejścia użytkownika od prezentacji. to jest kluczowe. Podczas gdy często ludzie opisują ją jako " triadę", nie składa się z trzech niezależnych części. Struktura jest bardziej podobna do tej: {]}

Separacja MVC

Oznacza to, że gdy logika Twojej warstwy prezentacji jest zbliżona do nieistniejącej, pragmatycznym podejściem jest utrzymanie ich jako pojedynczej warstwy. Może również znacznie uprościć niektóre aspekty warstwy modelu.

Przy użyciu tego podejścia przykład logowania (dla 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, gdy masz skomplikowaną logikę dla renderując ciało odpowiedzi, to uproszczenie jest bardzo przydatne w bardziej trywialnych scenariuszach. Ale ostrzegam , to podejście stanie się koszmarem, gdy spróbuje używać w dużych bazach kodowych ze złożoną logiką prezentacji.

 

Jak zbudować model?

Ponieważ nie ma ani jednej klasy "modelu" (jak wyjaśniono powyżej), naprawdę nie "budujesz modelu". Zamiast tego zaczynasz od tworzenia usług , które są w stanie wykonywać określone metody. Oraz następnie zaimplementuj obiekty domeny i Mapery .

Przykład metody serwisowej:

W obu powyższych podejściach pojawiła się metoda logowania do usługi identyfikacji. Jak by to właściwie wyglądało. Używam nieco zmodyfikowanej wersji tej samej funkcjonalności z biblioteki , którą napisałem .. bo 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, nie ma wskazania, gdzie dane zostały pobrane od. Może to być baza danych, ale może to być również tylko obiekt pozorowany do celów testowych. Nawet mapery danych, które są faktycznie używane do tego celu, są ukryte w metodach private 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 mapperów

Aby zaimplementować abstrakcję trwałości, na najbardziej elastycznych podejściach jest tworzenie niestandardowych maperów danych {36]}.

Diagram mapera

From: PoEAA book

W praktyce są to zaimplementowane do interakcji z konkretnymi klasami lub superklasami. Powiedzmy, że masz Customer i Admin w kodzie (obie dziedziczą z User klasy nadrzędnej). Oba prawdopodobnie skończyłyby się na osobnym dopasowanym maperze, ponieważ zawierają różne pola. Ale skończysz również z operacjami współdzielonymi i powszechnie używanymi. Na przykład: aktualizowanie czasu "ostatnio widziany online". I zamiast robić istniejące mapery bardziej zawiłe, bardziej pragmatyczne podejście jest mieć ogólne "User Mapper", który aktualizuje tylko ten znacznik czasu.

Kilka dodatkowych komentarzy:

  1. Tabele bazy danych i model

    Podczas gdy czasami istnieje bezpośrednia relacja 1: 1: 1 pomiędzy tabelą bazy danych, Domain Object i Mapper , w większych projektach może to być mniej powszechne niż można się spodziewać:

    • Informacje używane przez pojedynczy obiekt Domain mogą być mapowane z różnych tabel, podczas gdy obiekt sama 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 nie ma magicznej tabeli MonthlyReport w bazie danych.

    • Pojedynczy maper może wpływać na wiele tabel.

      przykład: gdy przechowujesz dane z obiektu User, Ten obiekt Domain object może zawierać kolekcję innych obiektów domain - instancji Group. Jeśli zmieniaj je i przechowuj User, maper danych będzie musiał zaktualizować i/lub wstawić wpisy w wielu tabelach.

    • Dane z pojedynczego 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), pragmatyczne może być przechowywanie danych uwierzytelniania użytkowników i często dostępnych danych oddzielnie od większych fragmentów treści, co rzadko jest wymagane. W takim przypadku możesz nadal posiada pojedynczą klasę User, 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 maper

      przykład: masz stronę z wiadomościami ze współdzielonym kodem zarówno dla osób publicznych, jak i oprogramowania do zarządzania. Ale, podczas gdy oba interfejsy używają tej samej klasy Article, zarządzanie potrzebuje o wiele więcej informacji w niej zawartych. W tym przypadku będziesz miał dwa oddzielne mapery: "wewnętrzne" i "zewnętrzne". Każdy wykonuje różne zapytania, a nawet używa różnych baz danych(jak w master lub slave).

  2. Widok nie jest szablonem

    instancje View W MVC (jeśli nie używasz wariacji MVP wzorca) są odpowiedzialne za logikę prezentacyjną. Oznacza to, że każdy widok Zwykle żongluje co najmniej kilkoma szablonami. Pozyskuje dane z warstwy modelu , a następnie, na podstawie otrzymana informacja, wybiera szablon i ustawia wartości.

    Jedną z korzyści, jakie zyskujesz, jest ponowna użyteczność. Jeśli utworzysz klasę ListView, wtedy, z dobrze napisanym kodem, możesz mieć tę samą klasę przekazującą prezentację listy użytkowników i komentarzy pod artykułem. Ponieważ obie mają tę samą logikę prezentacji. Po prostu zmieniasz szablony.

    Możesz użyć natywnych szablonów PHP lub użyć zewnętrznego silnika szablonów. Może być też niektóre biblioteki innych firm, które są w stanie w pełni zastąpić instancje View.

  3. A co ze starą wersją odpowiedzi?

    Jedyną istotną zmianą jest to, że to, co nazywa się Model w starej wersji, jest w rzeczywistości usługą . Reszta "analogii bibliotecznej" trzyma się całkiem nieźle.

    Jedyną wadą, którą widzę, jest to, że byłaby to naprawdę dziwna biblioteka, ponieważ zwracałaby Ci informacje z książki, ale nie pozwól dotknąć samej książki, bo w przeciwnym razie abstrakcja zacznie "przeciekać". Będę musiał wymyślić bardziej odpowiednią analogię.

  4. Jaka jest zależność pomiędzy instancjami View i Controller ?

    Struktura MVC składa się z dwóch warstw: ui i modelu. Główne struktury w warstwie UI To widoki i kontroler.

    Kiedy masz do czynienia z witrynami, które używają wzorca projektowego MVC, najlepszym sposobem jest posiadanie relacji 1:1 pomiędzy widokami a kontrolerami. Każdy widok reprezentuje całą stronę w witrynie i ma dedykowany kontroler do obsługi wszystkich przychodzących żądań dla tego konkretnego widoku.

    Na przykład, aby reprezentować otwarty artykuł, należy mieć \Application\Controller\Document i \Application\View\Document. Zawiera ona wszystkie główne funkcje warstwy UI, jeśli chodzi o obsługę artykułów (oczywiście możesz mieć komponenty XHR , które nie są bezpośrednio związane z artykuły) .

 833
Author: tereško,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-18 07:54:33

Wszystko, co jest logiką biznesową należy do modelu, niezależnie od tego, czy jest to zapytanie do bazy danych, obliczenia, wywołanie REST itp.

Możesz mieć dostęp do danych w samym modelu, wzorzec MVC nie ogranicza cię do tego. Można go osłodzić usługami, maperami i czym innym, ale rzeczywista definicja modelu to warstwa, która obsługuje logikę biznesową, nic więcej, nic mniej. Może to być Klasa, funkcja lub kompletny moduł z gazillion obiektów, jeśli tego właśnie chcesz.

Zawsze łatwiej jest mieć osobny obiekt, który faktycznie wykonuje zapytania do bazy danych, zamiast wykonywać je bezpośrednio w modelu: jest to szczególnie przydatne podczas testów jednostkowych (ze względu na łatwość wstrzykiwania wzorcowej zależności od bazy danych w modelu):

class Database {
   protected $_conn;

   public function __construct($connection) {
       $this->_conn = $connection;
   }

   public function ExecuteObject($sql, $data) {
       // stuff
   }
}

abstract class Model {
   protected $_db;

   public function __construct(Database $db) {
       $this->_db = $db;
   }
}

class User extends Model {
   public function CheckUsername($username) {
       // ...
       $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ...";
       return $this->_db->ExecuteObject($sql, $data);
   }
}

$db = new Database($conn);
$model = new User($db);
$model->CheckUsername('foo');

Również w PHP rzadko trzeba łapać wyjątki / rethrow, ponieważ backtrace jest zachowane, szczególnie w przypadku takim jak twój przykład. Tylko Niech wyjątek będzie rzucony i złapać go w kontrolerze zamiast.

 33
Author: netcoder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-27 19:38:55

W Web - " MVC " możesz robić co chcesz.

Oryginalna koncepcja (1) opisywał model jako logikę biznesową. Powinien reprezentować stan aplikacji i wymuszać pewną spójność danych. Podejście to jest często określane jako "model tłuszczu".

Większość frameworków PHP stosuje bardziej płytkie podejście, gdzie model jest tylko interfejsem bazy danych. Ale przynajmniej te modele powinny nadal sprawdzać przychodzące dane i relacje.

Tak czy inaczej, nie jesteś zbyt daleko, jeśli oddzielisz rzeczy SQL lub wywołania bazy danych na inną warstwę. W ten sposób musisz tylko zająć się rzeczywistymi danymi/zachowaniem, a nie rzeczywistym interfejsem API pamięci masowej. (Jest to jednak nierozsądne, aby przesadzić. Na przykład nigdy nie będziesz w stanie zastąpić backendu bazy danych filestorage, jeśli nie został zaprojektowany z wyprzedzeniem.)

 19
Author: mario,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:34:45

Częściej większość aplikacji będzie mieć dane, wyświetlanie i przetwarzanie części i po prostu umieścić wszystkie te w literach M,V i C.

Model(M)-->posiada atrybuty, które utrzymują stan aplikacji i nie wie nic o V i C.

widok(V)-->ma format wyświetlania dla aplikacji i wie tylko o tym, jak przetrawić na niej model i nie martwi się o C.

Kontroler(C)---->ma część przetwarzania aplikacji i działa jako okablowanie między M I V i zależy od obu M,V w przeciwieństwie do M i V.

W sumie istnieje oddzielenie troski między każdym. W przyszłości wszelkie zmiany lub ulepszenia mogą być dodawane bardzo łatwo.

 5
Author: feel good and programming,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-08-19 14:44:27

W moim przypadku mam klasę bazy danych, która obsługuje wszystkie bezpośrednie interakcje z bazą danych, takie jak zapytania, pobieranie itp. Więc gdybym miał zmienić moją bazę danych z MySQL na PostgreSQL nie będzie żadnego problemu. Więc dodanie tej dodatkowej warstwy może być przydatne.

Każda tabela może mieć swoją własną klasę i swoje specyficzne metody, ale aby uzyskać dane, pozwala klasie bazodanowej obsługiwać je:

Plik Database.php

class Database {
    private static $connection;
    private static $current_query;
    ...

    public static function query($sql) {
        if (!self::$connection){
            self::open_connection();
        }
        self::$current_query = $sql;
        $result = mysql_query($sql,self::$connection);

        if (!$result){
            self::close_connection();
            // throw custom error
            // The query failed for some reason. here is query :: self::$current_query
            $error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n");
            $error->handleError();
        }
        return $result;
    }
 ....

    public static function find_by_sql($sql){
        if (!is_string($sql))
            return false;

        $result_set = self::query($sql);
        $obj_arr = array();
        while ($row = self::fetch_array($result_set))
        {
            $obj_arr[] = self::instantiate($row);
        }
        return $obj_arr;
    }
}

Obiekt tabeli classL

class DomainPeer extends Database {

    public static function getDomainInfoList() {
        $sql = 'SELECT ';
        $sql .='d.`id`,';
        $sql .='d.`name`,';
        $sql .='d.`shortName`,';
        $sql .='d.`created_at`,';
        $sql .='d.`updated_at`,';
        $sql .='count(q.id) as queries ';
        $sql .='FROM `domains` d ';
        $sql .='LEFT JOIN queries q on q.domainId = d.id ';
        $sql .='GROUP BY d.id';
        return self::find_by_sql($sql);
    }

    ....
}

Mam nadzieję, że ten przykład pomoże Ci stworzyć dobrą strukturę.

 0
Author: Ibu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-06-14 20:24:06