php software Come dovrebbe essere strutturato un modello in MVC?




software mvc (4)

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

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

Secondo: c'è una differenza tra il classico MVC e ciò che usiamo nello sviluppo web. Here's una risposta più antica 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 (l'ho fatto anch'io, anche se la risposta originale è stata scritta quando ho iniziato a imparare diversamente) , perché la maggior parte delle strutture perpetuano questo equivoco.

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

Che modello è:

Nell'adatto adattamento MVC, la M contiene tutta la logica di business del dominio e il livello del modello è costituito principalmente da tre tipi di strutture:

  • Oggetti di dominio

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

    Questo sarebbe dove si definisce come convalidare i dati prima di inviare una fattura o per calcolare il costo totale di un ordine. Allo stesso tempo, Domain Objects è completamente inconsapevole dello storage - né da dove (database SQL, API REST, file di testo, ecc.) Né se vengono salvati o recuperati.

  • Data Mappers

    Questi oggetti sono solo responsabili della memorizzazione. Se memorizzi le informazioni in un database, questo sarebbe dove vive l'SQL. O forse usi un file XML per archiviare i dati, e i tuoi Data Mapper stanno analizzando da e verso i file XML.

  • Services

    Puoi considerarli come "oggetti di dominio di livello superiore", ma invece di 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 di business del dominio. Puoi evitarli, ma con la penalità di perdere qualche logica di dominio in Controller .

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

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

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

Come interagire con un modello?

Prerequisiti: seguire le lezioni "Global State and Singletons" e "Do not Look For Things!" dai discorsi sul codice pulito.

Ottenere l'accesso alle istanze di servizio

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

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

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

Entrambe le soluzioni che utilizzano un factory e un contenitore DI consentono di condividere anche le istanze di vari server da condividere tra il controller selezionato e la visualizzazione 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 tuoi controllori hanno un compito molto chiaro: prendi l'input dell'utente e, in base a questo input, cambia lo stato corrente della logica aziendale. In questo esempio, gli stati che vengono modificati sono "utente anonimo" e "utente connesso".

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

Mostra all'utente lo stato di modifica.

Ok, l'utente ha effettuato l'accesso (o non è riuscito). Ora cosa? Detto utente è ancora inconsapevole di esso. Quindi è necessario produrre 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 possibili risposte, in base allo stato corrente del livello del modello. Per un caso d'uso diverso, la vista potrebbe essere selezionata scegliendo diversi modelli per il rendering, in base a qualcosa come "l'attuale selezionato dell'articolo".

Il livello di presentazione può effettivamente diventare piuttosto elaborato, come descritto qui: Comprendere le viste MVC in PHP .

Ma sto solo facendo un'API REST!

Certo, ci sono situazioni in cui questo è eccessivo.

MVC è solo una soluzione concreta per il principio Separation of Concerns . 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 la gente la descrive come una "triade", in realtà non è composta da tre parti indipendenti. La struttura è più simile a questa:

Significa che, quando la logica del tuo livello di presentazione è quasi inesistente, l'approccio pragmatico è di mantenerli come singoli livelli. Inoltre, può semplificare in modo sostanziale 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 tenterà di utilizzare in grandi codebase con logica di presentazione complessa.

Come costruire il modello?

Poiché non esiste una singola classe "Modello" (come spiegato sopra), non si "costruisce il modello" realmente. Invece si inizia dalla creazione di servizi , che sono in grado di eseguire determinati metodi. E quindi implementare Domain Objects and Mappers .

Un esempio di un metodo di servizio:

In entrambi gli approcci di cui sopra c'era questo metodo di accesso per il servizio di identificazione. Come sarebbe in realtà? 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 su dove i dati sono stati recuperati. Potrebbe essere un database, ma potrebbe anche essere solo un oggetto fittizio a scopo di test. Persino i mappatori di dati, che sono effettivamente utilizzati per questo, sono nascosti nei metodi private 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 mapper

Per implementare un'astrazione di persistenza, gli approcci più flessibili sono la creazione di mapper dei dati personalizzati.

Da: libro PoEAA

In pratica sono implementati per l'interazione con classi o superclassi specifici. Diciamo che hai il Customer e l' Admin nel tuo codice (entrambi ereditati da una superclasse User ). 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 "visto l'ultima volta online" . E invece di rendere più complicati i mappatori esistenti, l'approccio più pragmatico è quello di avere un "Mapper utente" generale, che aggiorna solo il timestamp.

Alcuni commenti aggiuntivi:

  1. Tabelle e modello del database

    Mentre a volte esiste una relazione diretta 1: 1: 1 tra una tabella di database, un oggetto dominio e un mapper , nei 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 alcuna persistenza nel database.

      Esempio: se stai generando un rapporto mensile. Ciò raccoglierebbe informazioni da diverse tabelle, ma non esiste una tabella MonthlyReport nel database.

    • Un singolo Mapper può influenzare più tabelle.

      Esempio: quando si memorizzano i dati dall'oggetto User , questo Oggetto dominio potrebbe contenere la raccolta di altri oggetti dominio - Istanze di Group . Se si modificano e si memorizzano l' User , il Data Mapper dovrà aggiornare e / o inserire voci in più tabelle.

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

      Esempio: nei sistemi di grandi dimensioni (si pensi: un social network di medie dimensioni), potrebbe essere pragmatico archiviare i dati di autenticazione utente e i dati a cui si accede spesso separatamente da blocchi di contenuto più grandi, che è raramente necessario. In tal caso potresti avere ancora una singola classe User , ma le informazioni che contiene dipendono dal fatto che siano stati recuperati tutti i dettagli.

    • Per ogni oggetto dominio ci possono essere più di un mappatore

      Esempio: si dispone di un sito di notizie con un codice condiviso per il software di gestione e per il pubblico. Ma, mentre entrambe le interfacce usano la stessa classe Article , la gestione ha bisogno di molte più informazioni popolate in essa. In questo caso avresti due mapper separati: "interno" ed "esterno". Ognuno esegue query diverse o utilizza anche database diversi (come 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 è la riutilizzabilità. Se si crea una classe ListView , quindi, con un codice ben scritto, è possibile avere la stessa classe che consegna la presentazione di user-list e commenti sotto un articolo. Perché entrambi hanno la stessa logica di presentazione. Basta cambiare i modelli.

    Puoi utilizzare i modelli nativi di PHP 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 View .

  3. E la vecchia versione della risposta?

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

    L'unico difetto che vedo è che questa sarebbe una libreria davvero strana, perché ti restituirebbe le informazioni dal libro, ma non ti lascerebbe toccare il libro stesso, perché altrimenti l'astrazione comincerebbe a "perdere". Potrei dover pensare ad un'analogia più appropriata.

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

    La struttura MVC è composta da due livelli: ui 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 pattern di progettazione MVC, il modo migliore è avere una relazione 1: 1 tra viste e controllori. 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\Document e \Application\View\Document . Ciò conterrebbe tutte le funzionalità principali per il livello dell'interfaccia utente, quando si tratta di gestire gli articoli (ovviamente potresti avere alcuni componenti XHR che non sono direttamente correlati agli articoli) .

https://code.i-harness.com

Sto solo facendo conoscenza del framework MVC e spesso mi chiedo quanto codice debba 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 del modello deve avere tutte le proprietà mappate del database e il codice sopra o è OK separare quel codice che effettivamente funziona il database?

Finirò per avere quattro strati?


In Web- "MVC" puoi fare tutto ciò che vuoi.

Il concetto originale (1) descriveva il modello come la logica aziendale. Dovrebbe rappresentare lo stato dell'applicazione e rafforzare la coerenza dei dati. Questo 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 per lo meno questi modelli dovrebbero ancora convalidare i dati in entrata e le relazioni.

In entrambi i casi, non sei molto lontano se separi la roba SQL o le chiamate al database in un altro livello. In questo modo devi solo occuparti dei dati / comportamenti reali, non con l'effettiva API di archiviazione. (È comunque irragionevole esagerare. Ad esempio, non sarà mai possibile sostituire un back-end del database con una filestoria se non è stato progettato in anticipo.)


Più spesso la maggior parte delle applicazioni avrà una parte di dati, di visualizzazione e di elaborazione e inseriamo semplicemente tutti quelli nelle lettere M , V e C

Model ( M ) -> Ha gli attributi che mantengono lo stato di applicazione e non sa nulla di V e C

Visualizza ( V ) -> Ha un formato di visualizzazione per l'applicazione e conosce solo il modello how-to-digest su di esso e non si preoccupa di C

Controller ( C ) ----> Ha una parte di applicazione di elaborazione e funge da cablaggio tra M e V e dipende da M , V differenza di M e V

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


Tutto ciò che è logico aziendale appartiene a un modello, sia che si tratti di una query di database, di calcoli, di una chiamata REST, ecc.

Puoi avere l'accesso ai dati nel modello stesso, il pattern MVC non ti impedisce di farlo. Puoi rivestirlo di zucchero con servizi, mappatori e cosa no, ma la definizione attuale di un modello è un livello che gestisce la logica di business, niente di più, niente di meno. Può essere una classe, una funzione o un modulo completo con un oggetto di miliardi se è quello che vuoi.

È sempre più semplice avere un oggetto separato che esegua effettivamente le query del database anziché eseguirle direttamente nel modello: questo sarà particolarmente utile quando si esegue il test delle unità (a causa della facilità di iniettare una dipendenza del database fittizio 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 devi catturare / rilanciare le eccezioni perché il backtrace è preservato, specialmente in un caso come il tuo esempio. Fai in modo che l'eccezione sia lanciata e catturala nel controller.





model