In MVC un modello dovrebbe gestire la convalida?


25

Sto cercando di riprogettare un'applicazione Web sviluppata per utilizzare il modello MVC, ma non sono sicuro che la convalida debba essere gestita o meno nel modello. Ad esempio, sto configurando uno dei miei modelli in questo modo:

class AM_Products extends AM_Object 
{
    public function save( $new_data = array() ) 
    {
        // Save code
    }
}

Prima domanda: quindi mi chiedo se il mio metodo di salvataggio debba chiamare una funzione di validazione su $ new_data o supporre che i dati siano già stati validati?

Inoltre, se fosse per offrire la convalida, sto pensando che alcuni dei codici modello per definire i tipi di dati sarebbero così:

class AM_Products extends AM_Object
{
    protected function init() // Called by __construct in AM_Object
    {
        // This would match up to the database column `age`
        register_property( 'age', 'Age', array( 'type' => 'int', 'min' => 10, 'max' => 30 ) ); 
    }
}

Seconda domanda: ogni classe figlio di AM_Object eseguirà register_property per ogni colonna nel database di quell'oggetto specifico. Non sono sicuro se questo è un buon modo di farlo o no.

Terza domanda: se il modello deve gestire la convalida, dovrebbe restituire un messaggio di errore o un codice di errore e la vista deve utilizzare il codice per visualizzare un messaggio appropriato?

Risposte:


30

Prima risposta: un ruolo chiave del modello è mantenere l'integrità. Tuttavia l'elaborazione dell'input dell'utente è una responsabilità di un controller.

Cioè, il controller deve tradurre i dati utente (che il più delle volte sono solo stringhe) in qualcosa di significativo. Ciò richiede l'analisi (e può dipendere da cose come la localizzazione, dato che ad esempio ci sono diversi operatori decimali ecc.).
Quindi la validazione effettiva, come in "i dati sono ben formati?", Dovrebbe essere eseguita dal controller. Tuttavia, la verifica, come in "hanno senso i dati?" dovrebbe essere eseguito all'interno del modello.

Per chiarire questo con un esempio: si
supponga che l'applicazione ti consenta di aggiungere alcune entità, con una data (ad esempio un problema con una scadenza). Potresti avere un'API, in cui le date potrebbero essere rappresentate come semplici timestamp Unix, mentre quando provengono da una pagina HTML, sarà un insieme di valori diversi o una stringa nel formato MM / GG / AAAA. Non vuoi queste informazioni nel modello. Volete che ciascun controller provi individualmente a capire la data. Tuttavia, quando la data viene quindi passata al modello, il modello deve mantenere l'integrità. Ad esempio, potrebbe essere logico non consentire date in passato o date in ferie / domenica, ecc.

Il controller contiene regole di input (elaborazione). Il modello contiene regole commerciali. Volete che le vostre regole aziendali vengano sempre applicate, qualunque cosa accada. Supponendo che tu avessi regole di business nel controller, quindi dovresti duplicarle, se dovessi mai creare un controller diverso.

Seconda risposta: l'approccio ha un senso, tuttavia il metodo potrebbe essere reso più potente. Invece che l'ultimo parametro sia un array, dovrebbe essere un'istanza di IContstraintcui è definito come:

interface IConstraint {
     function test($value);//returns bool
}

E per i numeri potresti avere qualcosa come

class NumConstraint {
    var $grain;
    var $min;
    var $max;
    function __construct($grain = 1, $min = NULL, $max = NULL) {
         if ($min === NULL) $min = INT_MIN;
         if ($max === NULL) $max = INT_MAX;
         $this->min = $min;
         $this->max = $max;
         $this->grain = $grain;
    }
    function test($value) {
         return ($value % $this->grain == 0 && $value >= $min && $value <= $max);
    }
}

Inoltre, non vedo cosa 'Age'significhi rappresentare, a dire il vero. È il nome della proprietà attuale? Supponendo che ci sia una convenzione per impostazione predefinita, il parametro potrebbe semplicemente andare alla fine della funzione ed essere facoltativo. Se non impostato, verrà impostato per default su to_camel_case del nome della colonna DB.

Quindi la chiamata di esempio sarebbe simile a:

register_property('age', new NumConstraint(1, 10, 30));

Il punto di usare le interfacce è che puoi aggiungere sempre più vincoli mentre vai e possono essere complicati quanto vuoi. Affinché una stringa corrisponda a un'espressione regolare. Per un appuntamento con almeno 7 giorni di anticipo. E così via.

Terza risposta: ogni entità del modello dovrebbe avere un metodo simile Result checkValue(string property, mixed value). Il controller dovrebbe chiamarlo prima di impostare i dati. L' Resultdovrebbe avere tutte le informazioni sul fatto che il controllo non è riuscita, e nel caso in cui lo ha fatto, motivare, in modo che il controller può propagarsi quelli alla vista di conseguenza.
Se al modello viene passato un valore errato, il modello dovrebbe semplicemente rispondere sollevando un'eccezione.


Grazie per questo commento. Ha chiarito molte cose su MVC.
Amadeus Dr Zaius,

5

Non sono completamente d'accordo con "back2dos": la mia raccomandazione è di utilizzare sempre un modulo / livello di convalida separato, che il controller può utilizzare per convalidare i dati di input prima che vengano inviati al modello.

Da un punto di vista teorico, la convalida del modello opera su dati attendibili (stato del sistema interno) e dovrebbe idealmente essere ripetibile in qualsiasi momento mentre la convalida dell'input opera esplicitamente una volta su dati che provengono da fonti non attendibili (a seconda del caso d'uso e dei privilegi dell'utente).

Questa separazione consente di costruire modelli, controller e moduli riutilizzabili che possono essere accoppiati liberamente attraverso l'iniezione di dipendenza. Pensa alla convalida dell'input come convalida della whitelist ("accetta il bene noto") e la convalida del modello come convalida della lista nera ("rifiuta il male noto"). La convalida della lista bianca è più sicura mentre la convalida della lista nera impedisce al livello del modello di essere eccessivamente vincolato a casi d'uso molto specifici.

Dati del modello non validi dovrebbero sempre generare un'eccezione (altrimenti l'applicazione può continuare a funzionare senza notare l'errore) mentre i valori di input non validi provenienti da fonti esterne non sono imprevisti, ma piuttosto comuni (a meno che non si ottengano utenti che non commettono mai errori).

Vedi anche: https://lastzero.net/2015/11/why-im-using-a-separate-layer-for-input-data-validation/


Per semplicità, supponiamo che esista una famiglia di classi Validator e che tutte le convalide vengano eseguite con una gerarchia strategica. I bambini con validatore concreto possono anche essere composti da validatori speciali: e-mail, numero di telefono, token di modulo, captcha, password e altri. La convalida dell'input del controller è di due tipi: 1) Verifica dell'esistenza di un controller e metodo / comando e 2) un esame preliminare dei dati (ovvero il metodo di richiesta HTTP, quanti input di dati (Troppi? Troppi?).
Anthony Rutledge,

Dopo aver verificato la quantità di input, devi sapere che sono stati inviati i controlli HTML corretti, per nome, tenendo presente che il numero di input per richiesta può variare, poiché non tutti i controlli di un modulo HTML inviano qualcosa se lasciato vuoto ( in particolare le caselle di controllo). Successivamente, l'ultimo controllo preliminare è un test delle dimensioni di input. Secondo me, dovrebbe essere presto , non tardi. Fare la quantità, il nome del controllo e il controllo delle dimensioni di input di base in un validatore del controller significherebbe avere un validatore per ciascun comando / metodo nel controller. Ritengo che ciò renda la tua applicazione più sicura.
Anthony Rutledge,

Sì, il validatore del controller per un comando sarà strettamente accoppiato agli argomenti (se presenti) richiesti per un metodo modello , ma il controller stesso non lo sarà, salvo il riferimento a detto validatore del controller . Questo è un degno compromesso, in quanto non si deve andare avanti con il presupposto che la maggior parte degli input sarà legittima. Prima puoi interrompere l'accesso illegittimo alla tua applicazione, meglio è. Farlo in una classe di validatore del controller (quantità, nome e dimensione massima degli input) evita di dover istanziare l'intero modello per rifiutare richieste HTTP chiaramente dannose.
Anthony Rutledge,

Detto questo, prima di affrontare i problemi relativi alle dimensioni massime dell'input, è necessario assicurarsi che la codifica sia corretta. Tutto considerato, questo è troppo per il modello da fare, anche se il lavoro è incapsulato. Diventa inutilmente costoso rifiutare richieste dannose. In breve, il controller deve assumersi maggiori responsabilità per ciò che invia al modello. L'errore a livello di controller dovrebbe essere fatale, senza informazioni di ritorno al richiedente diverse da 200 OK. Registra l'attività. Lancia un'eccezione fatale. Termina tutte le attività. Arresta tutti i processi il più presto possibile.
Anthony Rutledge,

Controlli minimi, controlli massimi, controlli corretti, codifica di input e dimensione massima di input riguardano tutti la natura della richiesta (in un modo o nell'altro). Alcune persone non hanno identificato queste cinque cose fondamentali nel determinare se una richiesta debba essere onorata. Se tutte queste cose non sono soddisfatte, perché invii queste informazioni al modello? Buona domanda.
Anthony Rutledge,

3

Sì, il modello deve eseguire la convalida. Anche l'interfaccia utente dovrebbe convalidare l'input.

È chiaramente responsabilità del modello determinare valori e stati validi. A volte tali regole cambiano spesso. In tal caso, nutro il modello dai metadati e / o lo decoro.


Che dire dei casi in cui l'intento dell'utente è chiaramente dannoso o in errore? Ad esempio, si suppone che una determinata richiesta HTTP non abbia più di sette (7) valori di input, ma il controller ne ottenga settanta (70). Consentirai davvero dieci volte (10 volte) il numero di valori consentiti per colpire il modello quando la richiesta è chiaramente corrotta? In questo caso, è lo stato dell'intera richiesta in questione, non lo stato di alcun valore particolare. Una strategia di difesa approfondita suggerirebbe di esaminare la natura della richiesta HTTP prima di inviare i dati al modello.
Anthony Rutledge,

(continua) In questo modo, non si sta verificando che determinati valori e stati forniti dall'utente siano validi, ma che la totalità della richiesta sia valida. Non è ancora necessario eseguire il drill down fino a quel punto. L'olio è già in superficie.
Anthony Rutledge,

(continua) Non è possibile forzare la convalida del front-end. Bisogna considerare che gli strumenti automatizzati possono essere usati come interfaccia con la tua applicazione web.
Anthony Rutledge,

(Dopo aver riflettuto) I valori e gli stati dei dati validi nel modello sono importanti, ma ciò che ho descritto colpisce all'intento della richiesta che arriva attraverso il controller. Omettere la verifica dell'intento rende l'applicazione più vulnerabile. L'intento può essere solo buono (giocando secondo le tue regole) o cattivo (andando fuori dalle tue regole). L'intento può essere verificato mediante controlli di base sugli input: controlli minimi, controlli massimi, controlli corretti, codifica degli input e dimensioni massime degli input. È una proposta tutto o niente. Tutto passa o la richiesta non è valida. Non è necessario inviare nulla al modello.
Anthony Rutledge,

2

Ottima domanda!

In termini di sviluppo del World Wide Web, cosa succederebbe anche se si chiedesse quanto segue.

"Se un input utente errato viene fornito a un controller da un'interfaccia utente, il controller dovrebbe aggiornare la vista in una sorta di ciclo ciclico, costringendo i comandi e i dati di input a essere precisi prima di elaborarli ? Come? Come viene aggiornata la vista normalmente? condizioni? Una vista è strettamente accoppiata a un modello? La logica di business principale della convalida dell'input dell'utente è il modello o è preliminare ad esso e quindi dovrebbe avvenire all'interno del controller (perché i dati di input dell'utente fanno parte della richiesta)?

(In effetti, si può e si dovrebbe ritardare l'istanza di un modello fino a quando non si acquisisce un input valido?)

La mia opinione è che i modelli dovrebbero gestire una circostanza pura e incontaminata (per quanto possibile), non ostacolata dalla convalida dell'input della richiesta HTTP di base che dovrebbe avvenire prima dell'istanza del modello (e sicuramente prima che il modello ottenga i dati di input). Poiché la gestione dei dati di stato (persistenti o meno) e delle relazioni API è il mondo del modello, consentire al controller la convalida dell'input della richiesta HTTP di base .

Riassumendo.

1) Convalida il tuo percorso (analizzato dall'URL), poiché il controller e il metodo devono esistere prima che qualsiasi altra cosa possa andare avanti. Questo dovrebbe sicuramente accadere nel regno del controller anteriore (classe Router), prima di arrivare al controller vero. Duh. :-)

2) Un modello può avere molte fonti di dati di input: una richiesta HTTP, un database, un file, un'API e sì, una rete. Se hai intenzione di inserire tutta la convalida dell'input nel modello, consideri la convalida dell'input della richiesta HTTP parte dei requisiti aziendali per il programma. Caso chiuso.

3) Tuttavia, è miope sostenere le spese di creare un'istanza di molti oggetti se l' input della richiesta HTTP non va bene! Puoi sapere se ** l'input della richiesta HTTP ** è buono ( fornito con la richiesta ) convalidandolo prima di creare un'istanza del modello e di tutte le sue complessità (sì, forse anche più validatori per i dati di input / output API e DB).

Prova quanto segue:

a) Il metodo di richiesta HTTP (GET, POST, PUT, PATCH, DELETE ...)

b) Controlli HTML minimi (ne hai abbastanza?).

c) Numero massimo di controlli HTML (ne hai troppi?).

d) Controlli HTML corretti (hai quelli giusti?).

e) Codifica di input (in genere, è la codifica UTF-8?).

f) Dimensione massima dell'input (uno degli input è fuori limite?).

Ricorda che potresti ottenere stringhe e file, quindi attendere che il modello crei un'istanza potrebbe diventare molto costoso quando le richieste colpiscono il tuo server.

Quello che ho descritto qui colpisce nell'intento della richiesta che arriva attraverso il controller. Omettere la verifica dell'intento rende l'applicazione più vulnerabile. L'intento può essere solo buono (giocando secondo le tue regole fondamentali) o cattivo (andando al di fuori delle tue regole fondamentali).

L'intento per una richiesta HTTP è una proposta tutto o niente. Tutto passa o la richiesta non è valida . Non è necessario inviare nulla al modello.

Questo livello di base di HTTP richiedere intenti non ha nulla a che fare con gli errori di input utente normale e convalida. Nelle mie applicazioni, una richiesta HTTP deve essere valida nei cinque modi sopra indicati per onorarla. In un modo di difesa approfondito , non si arriva mai alla convalida dell'input dell'utente sul lato server se una di queste cinque cose fallisce.

Sì, questo significa che anche l'input di file deve essere conforme ai tuoi tentativi di front-end per verificare e comunicare all'utente la dimensione massima del file accettata. Solo HTML? Nessun JavaScript? Bene, ma l'utente deve essere informato delle conseguenze del caricamento di file troppo grandi (principalmente, che perderanno tutti i dati del modulo e verranno espulsi dal sistema).

4) Ciò significa che i dati di input della richiesta HTTP non fanno parte della logica aziendale dell'applicazione? No, significa solo che i computer sono dispositivi limitati e che le risorse devono essere utilizzate con saggezza. Ha senso interrompere l'attività dannosa prima, non dopo. Paghi di più in risorse di calcolo per aspettare di fermarlo in seguito.

5) Se l' input della richiesta HTTP è errato, l'intera richiesta è errata . È così che lo guardo. La definizione di un buon input di richiesta HTTP deriva dai requisiti aziendali del modello, ma deve esserci un certo punto di demarcazione delle risorse. Per quanto tempo lascerai vivere una cattiva richiesta prima di ucciderla e dire: "Oh, ehi, non importa. Cattiva richiesta."

Il giudizio non è semplicemente che l'utente ha commesso un errore di input ragionevole, ma che una richiesta HTTP è così fuori limite che deve essere dichiarata dannosa e interrotta immediatamente.

6) Quindi, per i miei soldi, la richiesta HTTP (METODO, URL / percorso e dati) è TUTTA buona, oppure NIENTE può procedere. Un modello robusto ha già compiti di convalida di cui occuparsi, ma un buon pastore di risorse dice "La mia strada, o la strada più alta. Vieni, corretto o non venire affatto".

È il tuo programma, però. "C'è più di un modo per farlo." Alcuni modi costano di più in termini di tempo e denaro rispetto ad altri. La convalida dei dati delle richieste HTTP in un secondo momento (nel modello) dovrebbe costare di più nel corso della vita di un'applicazione (soprattutto se il ridimensionamento o il ridimensionamento).

Se i tuoi validatori sono modulari, la convalida dell'input di richiesta * HTTP di base ** nel controller non dovrebbe essere un problema. Usa solo una classe di convalida strategica, in cui i validatori sono talvolta composti anche da validatori specializzati (e-mail, telefono, token di modulo, captcha, ...).

Alcuni lo vedono come completamente sbagliato, ma HTTP era agli inizi quando la Gang of Four scrisse Design Patterns: Elements of Re-usable Object-Oriented Software .

================================================== ========================

Ora, per quanto riguarda la normale convalida dell'input dell'utente (dopo che la richiesta HTTP è stata considerata valida), sta aggiornando la vista quando l'utente incasina a cui devi pensare! Questo tipo di convalida dell'input dell'utente dovrebbe avvenire nel modello.

Non hai alcuna garanzia di JavaScript sul front-end. Ciò significa che non è possibile garantire l'aggiornamento asincrono dell'interfaccia utente dell'applicazione con stati di errore. Il vero miglioramento progressivo coprirebbe anche il caso d'uso sincrono.

La contabilità per il caso d'uso sincrono è un'arte che si perde sempre più perché alcune persone non vogliono passare attraverso il tempo e il fastidio di tracciare lo stato di tutti i loro trucchi dell'interfaccia utente (mostra / nascondi controlli, disabilita / abilita i controlli , indicazioni di errore, messaggi di errore) sul back-end (in genere monitorando lo stato negli array).

Aggiornamento : Nel diagramma, dico che Viewdovrebbe fare riferimento a Model. No. Dovresti trasmettere i dati al Viewda Modelper preservare l'accoppiamento lento. inserisci qui la descrizione dell'immagine

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.