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:
- È possibile iniettare direttamente i servizi richiesti nei costruttori delle viste e dei controller, preferibilmente utilizzando un contenitore DI.
- 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:
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 private
metodi 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 .
Da: libro PoEAA
In pratica sono implementati per l'interazione con classi o superclassi specifiche. Diciamo che hai Customer
e Admin
nel tuo codice (entrambi ereditati da una User
superclasse). 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:
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 MonthlyReport
tabella magica nel database.
Un singolo mappatore può influire su più tabelle.
Esempio: quando si memorizzano dati User
dall'oggetto, questo oggetto dominio potrebbe contenere una raccolta di altri oggetti dominio - Group
istanze. 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 User
classe, 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 Article
classe, 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).
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 ListView
classe, 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 .
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.
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\Document
e \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) .