Come dovrebbe essere strutturato un modello in MVC? [chiuso]


551

Sto solo capendo il framework MVC e spesso mi chiedo quanto codice dovrebbe andare nel modello. Tendo ad avere una classe di accesso ai dati che ha metodi come questo:

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;
    }
}

I miei modelli tendono ad essere una classe di entità mappata alla tabella del database.

L'oggetto modello dovrebbe avere tutte le proprietà mappate del database e il codice sopra o è corretto separare quel codice che effettivamente funziona il database?

Finirò per avere quattro livelli?


133
Perché stai rilevando delle eccezioni solo per gettarle di nuovo?
Bailey Parker,

9
@Elias Van Ootegem: hai perso il punto. Inutile prenderli in questo caso.
Karoly Horvath,

4
@Elias Van Ootegem: eh? se funziona con il rilancio, significa che uno strato superiore rileva l'eccezione. Ma se ce n'è uno, lo avrebbe catturato senza quel inutile ripensamento ... (se ancora non lo capisci, per favore prendi un piccolo codice di prova)
Karoly Horvath,

3
@Elias Van Ootegem: non ho idea di cosa tu stia parlando, non gestire un'eccezione su un livello specifico non significa che fermerà l'app. per favore costruisci (o più precisamente: non riesci a costruire) un esempio di codice in cui è necessario quel ripensamento. fermiamo questa conversazione offtopica, per favore
Karoly Horvath,

6
@drrcknlsn: questo è un argomento valido, ma in quel caso almeno cogli l'eccezione che ti aspetti di essere lanciato, il generico Exceptionnon ha molto valore di documentazione. Personalmente, se percorressi questa strada, sceglierei PHPDoc @exception, o un meccanismo simile, in modo che venga visualizzato nella documentazione generata.
Karoly Horvath,

Risposte:


903

Dichiarazione di non responsabilità: la seguente è una descrizione di come comprendo modelli simili a MVC nel contesto di applicazioni Web basate su PHP. Tutti i link esterni utilizzati nel contenuto sono lì per spiegare termini e concetti e non implicare la mia credibilità sull'argomento.

La prima cosa che devo chiarire è: il modello è un livello .

Secondo: c'è una differenza tra MVC classico e ciò che usiamo nello sviluppo web. Ecco una piccola risposta che ho scritto, che descrive brevemente come sono diversi.

Che modello NON è:

Il modello non è una classe o un singolo oggetto. È un errore molto comune da fare (anch'io l'ho fatto, anche se la risposta originale è stata scritta quando ho iniziato a imparare diversamente) , perché la maggior parte dei framework perpetua questo malinteso.

Né è una tecnica di mappatura relazionale di oggetti (ORM) né un'astrazione di tabelle di database. Chiunque ti dica altrimenti sta probabilmente cercando di "vendere" un altro ORM nuovo di zecca o un intero quadro.

Che cos'è un modello:

Nel corretto adattamento MVC, la M contiene tutta la logica aziendale del dominio e il livello del modello è composto principalmente da tre tipi di strutture:

  • Oggetti dominio

    Un oggetto dominio è un contenitore logico di informazioni puramente di dominio; di solito rappresenta un'entità logica nello spazio del dominio problematico. Comunemente indicato come logica aziendale .

    Qui è dove si definisce come convalidare i dati prima di inviare una fattura o calcolare il costo totale di un ordine. Allo stesso tempo, gli oggetti di dominio non sono completamente a conoscenza dell'archiviazione, né da dove (database SQL, API REST, file di testo, ecc.) Né anche se vengono salvati o recuperati.

  • Mapper dati

    Questi oggetti sono responsabili solo della memorizzazione. Se si memorizzano informazioni in un database, questo sarebbe dove risiede l'SQL. O forse usi un file XML per archiviare i dati e i tuoi mappatori di dati stanno analizzando da e verso file XML.

  • Servizi

    Puoi considerarli come "oggetti di dominio di livello superiore", ma invece della logica aziendale, i servizi sono responsabili dell'interazione tra oggetti di dominio e mappatori . Queste strutture finiscono per creare un'interfaccia "pubblica" per interagire con la logica aziendale del dominio. Puoi evitarli, ma a pena di perdere una certa logica di dominio nei controller .

    C'è una risposta correlata a questo argomento nella domanda di implementazione dell'ACL - potrebbe essere utile.

La comunicazione tra il livello del modello e altre parti della triade MVC dovrebbe avvenire solo attraverso i Servizi . La chiara separazione ha alcuni vantaggi aggiuntivi:

  • aiuta a far rispettare il principio della responsabilità singola (SRP)
  • fornisce ulteriore "spazio di manovra" nel caso in cui la logica cambi
  • mantiene il controller il più semplice possibile
  • fornisce un progetto chiaro, se mai hai bisogno di un'API esterna

 

Come interagire con un modello?

Prerequisiti: guardare le lezioni "Global State and Singletons" e "Don't Look For Things!" dai Clean Code Talks.

Ottenere l'accesso alle istanze del servizio

Per entrambe le istanze View e Controller (quello che potresti chiamare: "UI layer") per avere accesso a questi servizi, ci sono due approcci generali:

  1. È possibile iniettare direttamente i servizi richiesti nei costruttori delle viste e dei controller, preferibilmente utilizzando un contenitore DI.
  2. Utilizzo di una factory per i servizi come dipendenza obbligatoria per tutte le visualizzazioni e i controller.

Come potresti sospettare, il contenitore DI è una soluzione molto più elegante (pur non essendo la più semplice per un principiante). Le due librerie, che consiglio di prendere in considerazione per questa funzionalità, sarebbero il componente DependencyInjection autonomo di Syfmony o Auryn .

Entrambe le soluzioni che utilizzano un factory e un contenitore DI ti consentirebbero anche di condividere le istanze di vari server da condividere tra il controller selezionato e visualizzare per un determinato ciclo richiesta-risposta.

Alterazione dello stato del modello

Ora che puoi accedere al livello del modello nei controller, devi iniziare a usarli effettivamente:

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $identity = $this->identification->findIdentityByEmailAddress($email);
    $this->identification->loginWithPassword(
        $identity,
        $request->get('password')
    );
}

I controller hanno un compito molto chiaro: accettare l'input dell'utente e, in base a questo input, modificare lo stato corrente della logica aziendale. In questo esempio gli stati che vengono cambiati tra sono "utente anonimo" e "utente connesso".

Il controller non è responsabile per la convalida dell'input dell'utente, perché fa parte delle regole aziendali e il controller sicuramente non chiama query SQL, come quello che vedresti qui o qui (per favore, non odiarli, sono sbagliati, non malvagi).

Mostra all'utente il cambio di stato.

Ok, l'utente ha effettuato l'accesso (o non è riuscito). E adesso? Detto utente non ne è ancora a conoscenza. Quindi è necessario produrre effettivamente una risposta e questa è la responsabilità di una vista.

public function postLogin()
{
    $path = '/login';
    if ($this->identification->isUserLoggedIn()) {
        $path = '/dashboard';
    }
    return new RedirectResponse($path); 
}

In questo caso, la vista ha prodotto una delle due risposte possibili, in base allo stato corrente del livello del modello. Per un caso d'uso diverso avresti la vista che seleziona diversi modelli da renderizzare, in base a qualcosa come "selezionato attualmente dell'articolo".

Il livello di presentazione può effettivamente diventare piuttosto elaborato, come descritto qui: Comprensione delle visualizzazioni MVC in PHP .

Ma sto solo creando un'API REST!

Certo, ci sono situazioni in cui questo è eccessivo.

MVC è solo una soluzione concreta per il principio di separazione delle preoccupazioni . MVC separa l'interfaccia utente dalla logica aziendale e nell'interfaccia utente ha separato la gestione dell'input dell'utente e della presentazione. Questo è cruciale. Mentre spesso le persone lo descrivono come una "triade", in realtà non è composto da tre parti indipendenti. La struttura è più simile a questa:

Separazione MVC

Significa che, quando la logica del tuo livello di presentazione è vicina a nessuna inesistente, l'approccio pragmatico è di mantenerle come un singolo livello. Inoltre può semplificare sostanzialmente alcuni aspetti del livello del modello.

Utilizzando questo approccio, l'esempio di accesso (per un'API) può essere scritto come:

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);
}

Sebbene ciò non sia sostenibile, quando si ha una logica complicata per il rendering di un corpo di risposta, questa semplificazione è molto utile per scenari più banali. Ma attenzione , questo approccio diventerà un incubo, quando si tenta di utilizzare in grandi codebase con complesse logiche di presentazione.

 

Come costruire il modello?

Poiché non esiste una singola classe "Modello" (come spiegato sopra), in realtà non si "costruisce il modello". Invece inizi a creare servizi che sono in grado di eseguire determinati metodi. E quindi implementare oggetti e mapper di dominio .

Un esempio di un metodo di servizio:

In entrambi gli approcci sopra c'era questo metodo di accesso per il servizio di identificazione. Come sarebbe effettivamente. Sto usando una versione leggermente modificata della stessa funzionalità da una libreria , che ho scritto .. perché sono pigro:

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();
}

Come puoi vedere, a questo livello di astrazione, non vi è alcuna indicazione da dove siano stati recuperati i dati. Potrebbe essere un database, ma potrebbe anche essere solo un oggetto fittizio a scopo di test. Anche i mapper di dati, che sono effettivamente utilizzati per questo, sono nascosti nei privatemetodi di questo servizio.

private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
    $identity->setStatus($status);
    $identity->setLastUsed(time());
    $mapper = $this->mapperFactory->create(Mapper\Identity::class);
    $mapper->store($identity);
}

Modi per creare mappatori

Per implementare un'astrazione di persistenza, l'approccio più flessibile consiste nel creare mappatori di dati personalizzati .

Diagramma mappatore

Da: libro PoEAA

In pratica sono implementati per l'interazione con classi o superclassi specifiche. Diciamo che hai Customere Adminnel tuo codice (entrambi ereditati da una Usersuperclasse). Entrambi probabilmente finirebbero con un mappatore di corrispondenza separato, poiché contengono campi diversi. Ma finirai anche con operazioni condivise e di uso comune. Ad esempio: aggiornamento dell'ora dell'ultimo accesso online . E invece di rendere i mapper esistenti più contorti, l'approccio più pragmatico è quello di avere un "User Mapper" generale, che aggiorni solo quel timestamp.

Alcuni commenti aggiuntivi:

  1. Tabelle e modello di database

    Mentre a volte esiste una relazione diretta 1: 1: 1 tra una tabella di database, Domain Object e Mapper , in progetti più grandi potrebbe essere meno comune di quanto ci si aspetti:

    • Le informazioni utilizzate da un singolo oggetto dominio potrebbero essere mappate da tabelle diverse, mentre l'oggetto stesso non ha persistenza nel database.

      Esempio: se stai generando un rapporto mensile. Questo raccoglierebbe informazioni da diverse tabelle, ma non esiste una MonthlyReporttabella magica nel database.

    • Un singolo mappatore può influire su più tabelle.

      Esempio: quando si memorizzano dati Userdall'oggetto, questo oggetto dominio potrebbe contenere una raccolta di altri oggetti dominio - Groupistanze. Se li si modifica e si memorizza il User, il Mapper dati dovrà aggiornare e / o inserire voci in più tabelle.

    • I dati da un singolo oggetto dominio sono archiviati in più di una tabella.

      Esempio: nei sistemi di grandi dimensioni (si pensi a un social network di medie dimensioni), potrebbe essere pragmatico archiviare i dati di autenticazione dell'utente e quelli a cui si accede spesso separatamente da blocchi di contenuto più grandi, cosa che raramente è richiesta. In tal caso potresti avere ancora una singola Userclasse, ma le informazioni in essa contenute dipenderebbero dal recupero di tutti i dettagli.

    • Per ogni oggetto dominio può esserci più di un mappatore

      Esempio: disponi di un sito di notizie con un codice condiviso basato sia sul software pubblico che sul software di gestione. Ma, sebbene entrambe le interfacce utilizzino la stessa Articleclasse, la gestione ha bisogno di molte più informazioni popolate al suo interno. In questo caso avresti due mappatori separati: "interno" e "esterno". Ognuno esegue query diverse o addirittura utilizza database diversi (come in master o slave).

  2. Una vista non è un modello

    Le istanze di visualizzazione in MVC (se non si utilizza la variazione MVP del modello) sono responsabili della logica di presentazione. Ciò significa che ogni vista di solito manipola almeno alcuni modelli. Acquisisce i dati dal livello del modello e quindi, in base alle informazioni ricevute, sceglie un modello e imposta i valori.

    Uno dei vantaggi che ottieni da questo è la riutilizzabilità. Se crei una ListViewclasse, quindi, con un codice ben scritto, puoi avere la stessa classe che gestisce la presentazione dell'elenco utenti e dei commenti sotto un articolo. Perché entrambi hanno la stessa logica di presentazione. Basta passare da un modello all'altro.

    Puoi utilizzare modelli PHP nativi o utilizzare un motore di template di terze parti. Potrebbero esserci anche alcune librerie di terze parti, che sono in grado di sostituire completamente le istanze di View .

  3. E la vecchia versione della risposta?

    L'unico cambiamento importante è che, quello che viene chiamato Modello nella vecchia versione, è in realtà un Servizio . Il resto dell '"analogia della biblioteca" continua abbastanza bene.

    L'unico difetto che vedo è che questa sarebbe una biblioteca davvero strana, perché ti restituirebbe informazioni dal libro, ma non ti permetterebbe di toccare il libro stesso, perché altrimenti l'astrazione inizierebbe a "perdere". Potrei pensare a un'analogia più adatta.

  4. Qual è la relazione tra le istanze View e Controller ?

    La struttura MVC è composta da due livelli: interfaccia utente e modello. Le strutture principali nel livello dell'interfaccia utente sono viste e controller.

    Quando si ha a che fare con siti Web che utilizzano il modello di progettazione MVC, il modo migliore è avere una relazione 1: 1 tra visualizzazioni e controller. Ogni vista rappresenta un'intera pagina nel tuo sito Web e ha un controller dedicato per gestire tutte le richieste in arrivo per quella particolare vista.

    Ad esempio, per rappresentare un articolo aperto, avresti \Application\Controller\Documente \Application\View\Document. Ciò conterrebbe tutte le funzionalità principali per il livello dell'interfaccia utente, quando si tratta di gestire articoli (ovviamente potresti avere alcuni componenti XHR che non sono direttamente correlati agli articoli) .


4
@Rinzler, noterai che da nessuna parte in quel link, tutto ciò che è stato detto su Model (tranne in un commento). È solo "un'interfaccia orientata agli oggetti per le tabelle del database" . Se provi a modellarlo in una cosa simile a un modello, finisci per violare SRP e LSP .
tereško,

8
@hafichuk solo situazioni, quando è ragionevole impiegare il modello ActiveRecord è per la prototipazione. Quando si inizia a scrivere il codice che è medio per la produzione, diventa un anti-modello, perché mescola la logica di archiviazione e di business. E poiché Model Layer non è completamente a conoscenza delle altre parti MVC. Questo non cambia a seconda della variazione sul modello originale . Anche quando si utilizza MVVM. Non ci sono "modelli multipli" e non sono associati a nulla. Il modello è un livello.
tereško,

3
Versione breve: i modelli sono strutture di dati .
Eddie B,

9
Bene visto che ha inventato MVC l'articolo potrebbe avere qualche merito.
Eddie B,

3
... o anche solo un insieme di funzioni. MVC non richiede l'implementazione in uno stile OOP, sebbene sia implementato principalmente in questo modo. La cosa più importante è separare i livelli e stabilire i giusti dati e controllare il flusso
hek2mgl

37

Tutto ciò che è business logic appartiene a un modello, che si tratti di una query del database, calcoli, una chiamata REST, ecc.

Puoi avere l'accesso ai dati nel modello stesso, il modello MVC non ti impedisce di farlo. Puoi ricoprire di servizi, mapper e quant'altro, ma la definizione effettiva di un modello è un livello che gestisce la logica aziendale, niente di più, niente di meno. Può essere una classe, una funzione o un modulo completo con oggetti gazillion se è quello che vuoi.

È sempre più facile avere un oggetto separato che esegua effettivamente le query del database invece di eseguirle direttamente nel modello: questo sarà particolarmente utile durante il test dell'unità (a causa della facilità di iniettare una dipendenza del database finta nel modello):

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');

Inoltre, in PHP, raramente hai bisogno di catturare / ricodificare le eccezioni perché la backtrace viene preservata, specialmente in un caso come il tuo esempio. Lascia che venga generata l'eccezione e rilevala invece nel controller.


La mia struttura è molto simile, penso di separarla un po 'di più. Il motivo per cui stavo passando la connessione era perché avevo bisogno di avere blocchi eseguiti nelle transazioni. Volevo aggiungere un utente e quindi aggiungere l'utente a un ruolo, ma di nuovo il ruolo in caso di errore. L'unico modo per risolverlo era passare la connessione.
Dietpixel

10
-1: capita anche che sia completamente sbagliato. Il modello non è un'astrazione per una tabella.
tereško,

1
La Userclasse sostanzialmente estende il modello, ma non è un oggetto. L'utente dovrebbe essere un oggetto e avere proprietà come: id, name ... Stai distribuendo la Userclasse è un aiuto.
TomSawyer,

1
Penso che tu capisca MVC ma non capisci cos'è OOP. In questo scenario, come ho detto, Usersta per un oggetto e dovrebbe avere proprietà di un Utente, non metodi come CheckUsername, cosa dovresti fare se vuoi creare un nuovo Useroggetto? new User($db)
TomSawyer il

@TomSawyer OOP non significa che gli oggetti devono avere proprietà. Quello che stai descrivendo è un modello di progettazione, che è irrilevante per la domanda o una risposta a quella domanda. OOP è un modello di linguaggio, non un modello di progettazione.
netcoder

20

Nel Web "MVC" puoi fare quello che ti pare.

Il concetto originale (1) descriveva il modello come la logica aziendale. Dovrebbe rappresentare lo stato dell'applicazione e applicare una certa coerenza dei dati. Tale approccio è spesso descritto come "modello grasso".

La maggior parte dei framework PHP segue un approccio più superficiale, in cui il modello è solo un'interfaccia di database. Ma almeno questi modelli dovrebbero comunque convalidare i dati e le relazioni in arrivo.

Ad ogni modo, non sei molto lontano se separi le cose SQL o le chiamate del database in un altro livello. In questo modo devi preoccuparti solo dei dati / comportamenti reali, non dell'API di archiviazione effettiva. (È comunque irragionevole esagerare. Ad esempio, non sarai mai in grado di sostituire un back-end del database con un filestorage se non è stato progettato in anticipo.)


8
collegamento non valido (404)
Kyslik il


6

Più oftenly la maggior parte delle applicazioni avranno dati, visualizzazione e parte di elaborazione e abbiamo appena messo tutti coloro che nelle lettere M, Ve C.

Model ( M) -> Ha gli attributi che detengono lo stato dell'applicazione e non sa nulla di Ve C.

Visualizza ( V) -> Ha un formato di visualizzazione per l'applicazione e conosce solo il modello di digestione e non si preoccupa C.

Controller ( C) ----> Ha una parte di elaborazione dell'applicazione e funge da cablaggio tra M e V e dipende da entrambi M, Vdiversamente da Me V.

Complessivamente esiste una separazione di preoccupazione tra ciascuno. In futuro qualsiasi modifica o miglioramento può essere aggiunto molto facilmente.


0

Nel mio caso ho una classe di database che gestisce tutte le interazioni dirette del database come query, recupero e così via. Quindi se dovessi cambiare il mio database da MySQL a PostgreSQL non ci sarebbero problemi. Quindi aggiungere quel livello extra può essere utile.

Ogni tabella può avere una propria classe e metodi specifici, ma per ottenere effettivamente i dati, consente alla classe di database di gestirli:

File 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;
    }
}

Oggetto tabella 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);
    }

    ....
}

Spero che questo esempio ti aiuti a creare una buona struttura.


12
"Quindi se dovessi cambiare il mio database da MySQL a PostgreSQL non ci sarebbero problemi." Uhhhmmm con il codice sopra avresti un grosso problema a cambiare qualsiasi cosa imo.
PeeHaa,

Vedo che la mia risposta ha sempre meno senso dopo la modifica e con il passare del tempo. Ma dovrebbe rimanere qui
Ibu,

2
Databasenell'esempio non è una classe. È solo un wrapper per le funzioni. Inoltre, come si può avere una "classe di oggetti tabella" senza un oggetto?
tereško,

2
@ tereško Ho letto molti dei tuoi post e sono fantastici. Ma non riesco a trovare un quadro completo da nessuna parte per studiare. Ne conosci uno che "fa bene"? O almeno uno a cui piaci e alcuni altri qui su SO dicono di fare? Grazie.
johnny,

Potrei essere in ritardo, ma vorrei sottolineare che il PDO risolve quasi il problema di dover creare un "livello" di DB per facilitare i cambiamenti futuri.
Matthew Goulart
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.