Czy w MVC model powinien obsługiwać sprawdzanie poprawności?

25

Usiłuję ponownie zaprojektować opracowaną przeze mnie aplikację internetową, aby używać wzorca MVC, ale nie jestem pewien, czy sprawdzanie poprawności powinno być obsługiwane w modelu, czy nie. Na przykład konfiguruję jeden z moich modeli:

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

Pierwsze pytanie: zastanawiam się więc, czy moja metoda zapisu powinna wywoływać funkcję sprawdzania poprawności na $ new_data, czy zakładać, że dane zostały już sprawdzone?

Ponadto, gdyby miał oferować sprawdzanie poprawności, myślę, że część kodu modelu do definiowania typów danych wyglądałaby następująco:

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

Drugie pytanie: Każda klasa potomna AM_Object uruchomiłaby właściwość register_property dla każdej kolumny w bazie danych tego konkretnego obiektu. Nie jestem pewien, czy to dobry sposób, czy nie.

Trzecie pytanie: czy model powinien obsługiwać sprawdzanie poprawności, czy powinien zwrócić komunikat o błędzie lub kod błędu, a widok powinien użyć tego kodu do wyświetlenia odpowiedniego komunikatu?

Brandon Wamboldt
źródło

Odpowiedzi:

30

Pierwsza odpowiedź: Kluczową rolą modelu jest utrzymanie integralności. Jednak przetwarzanie danych wejściowych użytkownika jest obowiązkiem administratora.

Oznacza to, że administrator musi przetłumaczyć dane użytkownika (które przez większość czasu są tylko ciągami znaków) na coś znaczącego. Wymaga to parsowania (i może zależeć od takich rzeczy jak lokalizacja, biorąc pod uwagę, że na przykład istnieją różne operatory dziesiętne itp.).
Tak więc faktyczna walidacja, jak w „czy dane są dobrze uformowane?”, Powinna być wykonana przez kontrolera. Jednak weryfikacja, jak w „czy dane mają sens?” należy wykonać w ramach modelu.

Aby wyjaśnić to na przykładzie:
Załóżmy, że Twoja aplikacja pozwala na dodanie niektórych encji z datą (na przykład problem z terminem). Możesz mieć interfejs API, w którym daty mogą być reprezentowane jako zwykłe uniksowe znaczniki czasu, a gdy pochodzą ze strony HTML, będzie to zestaw różnych wartości lub ciąg znaków w formacie MM / DD / RRRR. Nie chcesz tych informacji w modelu. Chcesz, aby każdy kontroler indywidualnie próbował ustalić datę. Jednak gdy data jest następnie przekazywana do modelu, model musi zachować integralność. Na przykład rozsądne może być niedopuszczanie dat z przeszłości lub dat przypadających w święta / niedziele itp.

Twój kontroler zawiera reguły wprowadzania (przetwarzania). Twój model zawiera reguły biznesowe. Chcesz, aby reguły biznesowe były zawsze egzekwowane, bez względu na to, co się stanie. Zakładając, że masz reguły biznesowe w kontrolerze, musisz je zduplikować, jeśli kiedykolwiek stworzysz inny kontroler.

Druga odpowiedź: Podejście to ma sens, jednak metoda może zostać wzmocniona. Zamiast ostatniego parametru będącego tablicą, powinna to być instancja IContstraintzdefiniowana jako:

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

A dla liczb możesz mieć coś takiego

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

Szczerze mówiąc, nie rozumiem, co 'Age'ma reprezentować. Czy to rzeczywista nazwa nieruchomości? Zakładając, że istnieje domyślna konwencja, parametr może po prostu przejść do końca funkcji i być opcjonalny. Jeśli nie zostanie ustawiony, domyślnie będzie to_nazwa_kameli w nazwie kolumny DB.

Zatem przykładowe wywołanie wyglądałoby następująco:

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

Korzystanie z interfejsów polega na tym, że możesz dodawać coraz więcej ograniczeń w miarę upływu czasu i mogą one być tak skomplikowane, jak chcesz. Dla ciągu pasującego do wyrażenia regularnego. Aby data była co najmniej 7 dni wcześniej. I tak dalej.

Trzecia odpowiedź: każda jednostka modelu powinna mieć taką metodę Result checkValue(string property, mixed value). Administrator powinien zadzwonić przed ustawieniem danych. ResultPowinny mieć wszystkie informacje na temat tego, czy kontrola zawiodła, aw przypadku tak było, uzasadnienia, więc sterownik może propagować tych do widoku odpowiednio.
Jeśli do modelu zostanie przekazana niewłaściwa wartość, model powinien po prostu zareagować, zgłaszając wyjątek.

back2dos
źródło
Dziękuję za ten napis. Wyjaśniło wiele rzeczy na temat MVC.
AmadeusDrZaius
5

Nie zgadzam się całkowicie z „back2dos”: zalecam, aby zawsze używać osobnej warstwy formularza / sprawdzania poprawności, której kontroler może użyć do sprawdzania poprawności danych wejściowych przed przesłaniem ich do modelu.

Z teoretycznego punktu widzenia sprawdzanie poprawności modelu działa na zaufanych danych (wewnętrzny stan systemu) i idealnie powinno być powtarzalne w dowolnym momencie, podczas gdy sprawdzanie poprawności danych jednoznacznie działa raz na danych pochodzących z niezaufanych źródeł (w zależności od przypadku użycia i uprawnień użytkownika).

Ta separacja umożliwia budowanie modeli, kontrolerów i formularzy wielokrotnego użytku, które można luźno połączyć poprzez wstrzykiwanie zależności. Potraktuj walidację danych wejściowych jako walidację białej listy („zaakceptuj znane dobro”), a walidację modelu jako walidację czarnej listy („odrzuć znane złe”). Sprawdzanie poprawności na białej liście jest bezpieczniejsze, podczas gdy sprawdzanie poprawności na czarnej liście zapobiega nadmiernemu ograniczeniu warstwy modelu do bardzo szczególnych przypadków użycia.

Niepoprawne dane modelu powinny zawsze powodować zgłoszenie wyjątku (w przeciwnym razie aplikacja może kontynuować działanie bez zauważenia błędu), podczas gdy nieprawidłowe wartości wejściowe pochodzące ze źródeł zewnętrznych nie są nieoczekiwane, ale raczej powszechne (chyba że masz użytkowników, którzy nigdy nie popełniają błędów).

Zobacz także: https://lastzero.net/2015/11/why-im-using-a-separate-layer-for-input-data-validation/

lastzero
źródło
Dla uproszczenia załóżmy, że istnieje rodzina klasy Validator i że wszystkie walidacje są wykonywane ze strategiczną hierarchią. Konkretne dzieci walidatora mogą również składać się ze specjalistycznych walidatorów: e-mail, numer telefonu, tokeny formularza, captcha, hasło i inne. Walidacja wejście regulatora jest dwojakiego rodzaju:?. 1) sprawdzenie istnienia kontrolera i metody / komendy, oraz 2) wstępnej analizie danych (czyli metoda żądania HTTP, ile wejść danych (zbyt wiele zbyt mało)
Anthony Rutledge
Po zweryfikowaniu liczby danych wejściowych musisz wiedzieć, że przesłano poprawne formanty HTML według nazwy, pamiętając, że liczba danych wejściowych na żądanie może się różnić, ponieważ nie wszystkie formanty formularza HTML przesyłają coś, gdy są puste ( zwłaszcza pola wyboru). Następnie ostatnia kontrola wstępna jest testem wielkości wejściowej. Moim zdaniem powinno to być wcześnie , a nie późno. Wykonanie kontroli ilości, nazwy kontrolnej i podstawowego rozmiaru wejściowego w walidatorze kontrolera oznaczałoby posiadanie walidatora dla każdej komendy / metody w kontrolerze. Wydaje mi się, że dzięki temu Twoja aplikacja jest bezpieczniejsza.
Anthony Rutledge
Tak, walidator kontrolera dla polecenia będzie ściśle powiązany z argumentami (jeśli istnieją) wymaganymi dla metody modelowej , ale sam kontroler nie będzie, z wyjątkiem odniesienia do wspomnianego walidatora kontrolera . Jest to godny kompromis, ponieważ nie można iść do przodu z założeniem, że większość danych wejściowych będzie zgodna z prawem. Im szybciej przerwiesz nielegalny dostęp do aplikacji, tym lepiej. Robienie tego w klasie walidatora kontrolera (ilość, nazwa i maksymalny rozmiar danych wejściowych) pozwala uniknąć tworzenia instancji całego modelu w celu odrzucenia wyraźnie złośliwych żądań HTTP.
Anthony Rutledge
Biorąc to pod uwagę, przed rozwiązaniem problemów z maksymalnym rozmiarem wejściowym należy upewnić się, że kodowanie jest dobre. Biorąc wszystko pod uwagę, jest to zbyt wiele, aby model mógł działać, nawet jeśli praca jest zamknięta w sobie. Odrzucanie złośliwych żądań staje się niepotrzebnie kosztowne. Podsumowując, kontroler musi wziąć na siebie większą odpowiedzialność za to, co wysyła do modelu. Awaria poziomu kontrolera powinna być śmiertelna, bez informacji zwrotnych do requestera innych niż 200 OK. Zaloguj aktywność. Rzuć śmiertelny wyjątek. Zakończ wszystkie działania. Zatrzymaj wszystkie procesy tak szybko, jak to możliwe.
Anthony Rutledge
Minimalne elementy sterujące, maksymalne elementy sterujące, prawidłowe elementy sterujące, kodowanie wejściowe i maksymalny rozmiar wejściowy odnoszą się do charakteru żądania (w taki czy inny sposób). Niektóre osoby nie zidentyfikowały tych pięciu podstawowych rzeczy jako decydujących o tym, czy prośba powinna zostać spełniona. Jeśli wszystkie te rzeczy nie są spełnione, dlaczego wysyłasz te informacje do modelu? Dobre pytanie.
Anthony Rutledge
3

Tak, model powinien przeprowadzić walidację. Interfejs użytkownika powinien również zweryfikować dane wejściowe.

Wyraźnie obowiązkiem modelu jest określenie prawidłowych wartości i stanów. Czasami takie zasady często się zmieniają. W takim przypadku nakarmiłbym model z metadanych i / lub udekorowałem go.

Sokół
źródło
Co z przypadkami, w których zamiar użytkownika jest wyraźnie złośliwy lub błędny? Na przykład określone żądanie HTTP powinno mieć nie więcej niż siedem (7) wartości wejściowych, ale kontroler otrzymuje siedemdziesiąt (70). Czy naprawdę zamierzasz pozwolić dziesięciokrotności (10x) liczby dozwolonych wartości trafić w model, gdy żądanie jest wyraźnie uszkodzone? W takim przypadku kwestionowany jest stan całego żądania, a nie stan jakiejkolwiek konkretnej wartości. Strategia dogłębnej obrony sugerowałaby, że charakter żądania HTTP powinien zostać zbadany przed wysłaniem danych do modelu.
Anthony Rutledge
(ciąg dalszy) W ten sposób nie sprawdzasz, czy podane przez użytkownika wartości i stany są poprawne, ale czy całość żądania jest poprawna. Jak dotąd nie ma potrzeby drążenia w dół. Olej jest już na powierzchni.
Anthony Rutledge
(ciąg dalszy) Nie ma możliwości wymuszenia weryfikacji frontonu. Należy wziąć pod uwagę, że zautomatyzowanych narzędzi można używać interfejsu z aplikacją internetową.
Anthony Rutledge,
(Po myślowe) Prawidłowe wartości i stany danych w modelu są ważne, ale to, co opisałem hity u intencją wniosku wpadającym przez kontrolera. Pominięcie weryfikacji zamiaru pozostawia twoją aplikację bardziej narażoną. Cel może być dobry (gra według twoich zasad) lub zły (wykraczanie poza twoje zasady). Cel można zweryfikować za pomocą podstawowych kontroli danych wejściowych: kontroli minimalnej, kontroli maksymalnej, prawidłowej kontroli, kodowania wejściowego i maksymalnego rozmiaru wejściowego. To propozycja wszystkiego albo nic. Wszystko mija lub żądanie jest nieprawidłowe. Nie musisz niczego wysyłać do modelu.
Anthony Rutledge
2

Świetne pytanie!

Jeśli chodzi o rozwój sieci na całym świecie, co jeśli zapytasz również o następujące rzeczy.

„Jeśli dane wejściowe złego użytkownika są dostarczane do kontrolera z interfejsu użytkownika, czy sterownik powinien aktualizować widok w rodzaju cyklicznej pętli, wymuszając dokładność poleceń i danych wejściowych przed ich przetworzeniem ? W jaki sposób? Jak widok jest aktualizowany w normalny sposób? warunki? Czy widok jest ściśle powiązany z modelem? Czy podstawowa logika biznesowa walidacji danych wejściowych wprowadzana przez użytkownika , czy też jest ona wstępna i dlatego powinna mieć miejsce w kontrolerze (ponieważ dane wejściowe użytkownika są częścią żądania)?

(Czy w efekcie można i należy opóźnić utworzenie modelu, dopóki nie uzyska się dobrych danych wejściowych?)

Moim zdaniem modele powinny radzić sobie z czystą i nieskazitelną okolicznością (na tyle, na ile to możliwe), nieobciążoną podstawową weryfikacją danych wejściowych żądania HTTP, która powinna nastąpić przed utworzeniem instancji modelu (i na pewno zanim model otrzyma dane wejściowe). Ponieważ zarządzanie danymi stanu (trwałymi lub innymi) i relacjami API jest światem modelu, pozwól, aby podstawowe sprawdzanie poprawności danych wejściowych żądania HTTP miało miejsce w kontrolerze.

Zreasumowanie.

1) Sprawdź poprawność trasy (parsowane z adresu URL), ponieważ kontroler i metoda muszą istnieć, zanim cokolwiek innego będzie mogło być kontynuowane. Zdecydowanie powinno się to zdarzyć w dziedzinie kontrolera frontowego (klasa routera), zanim dojdzie do prawdziwego kontrolera. Duh. :-)

2) Model może mieć wiele źródeł danych wejściowych: żądanie HTTP, bazę danych, plik, interfejs API i tak, sieć. Jeśli zamierzasz umieścić całą weryfikację danych wejściowych w modelu, wówczas uznajesz, że weryfikacja danych wejściowych żądania HTTP stanowi część wymagań biznesowych dla programu. Sprawa zamknięta.

3) Jednak krótkowzroczny jest koszt tworzenia instancji wielu obiektów, jeśli wprowadzanie żądania HTTP nie jest dobre! Możesz wiedzieć, czy ** wejście żądania HTTP ** jest dobre ( które przychodziło z żądaniem ), sprawdzając je przed utworzeniem instancji modelu i wszystkich jego złożoności (tak, być może jeszcze więcej walidatorów dla danych wejściowych / wyjściowych API i DB).

Przetestuj następujące:

a) Metoda żądania HTTP (GET, POST, PUT, PATCH, DELETE ...)

b) Minimalna kontrola HTML (czy masz dość?).

c) Maksymalna kontrola HTML (czy masz ich zbyt wiele?).

d) Prawidłowe formanty HTML (czy masz odpowiednie?).

e) Kodowanie wejściowe (zazwyczaj jest to kodowanie UTF-8?).

f) Maksymalny rozmiar wejściowy (czy którykolwiek z danych wejściowych jest całkowicie poza zakresem?).

Pamiętaj, że możesz otrzymać ciągi i pliki, więc oczekiwanie na utworzenie modelu może być bardzo kosztowne, gdy żądania trafią na twój serwer.

To, co tu opisałem, uderza w intencji żądania przychodzącego przez kontroler. Pominięcie weryfikacji zamiaru pozostawia twoją aplikację bardziej narażoną. Cel może być dobry (gra według twoich podstawowych zasad) lub zły (wykraczanie poza twoje podstawowe zasady).

Zamiarem żądania HTTP jest propozycja „wszystko albo nic”. Wszystko mija lub żądanie jest nieprawidłowe . Nie musisz niczego wysyłać do modelu.

Ten podstawowy poziom żądania HTTP intencją ma nic wspólnego z regularnych błędów wejściowych użytkownika i walidacji. W moich aplikacjach żądanie HTTP musi być ważne na pięć powyższych sposobów, aby je honorować. Mówiąc bardziej szczegółowo o obronie , nigdy nie przejdziesz do sprawdzania poprawności danych wejściowych przez użytkownika po stronie serwera, jeśli którakolwiek z tych pięciu rzeczy zawiedzie.

Tak, oznacza to, że nawet dane wejściowe pliku muszą być zgodne z twoimi frontonowymi próbami weryfikacji i poinformowania użytkownika o maksymalnym zaakceptowanym rozmiarze pliku. Tylko HTML? Brak JavaScript? W porządku, ale użytkownik musi zostać poinformowany o konsekwencjach przesyłania plików, które są zbyt duże (przede wszystkim, że stracą wszystkie dane formularza i zostaną wyrzuceni z systemu).

4) Czy to oznacza, że dane wejściowe żądania HTTP nie są częścią logiki biznesowej aplikacji? Nie, oznacza to po prostu, że komputery są urządzeniami skończonymi, a zasoby muszą być mądrze wykorzystywane. Sensowne jest zatrzymanie złośliwej aktywności wcześniej, a nie później. Płacisz więcej za zasoby obliczeniowe za czekanie, aby zatrzymać je później.

5) Jeśli dane wejściowe żądania HTTP są złe, całe żądanie jest złe . Tak na to patrzę. Definicja dobrych danych wejściowych żądania HTTP pochodzi z wymagań biznesowych modelu, ale musi istnieć pewien punkt rozgraniczenia zasobów. Jak długo pozwolisz żyć złej prośbie, zanim ją zabijesz i powiesz: „Och, hej, nieważne. Zła prośba”.

Wyrok nie polega po prostu na tym, że użytkownik popełnił rozsądny błąd przy wprowadzaniu danych, ale że żądanie HTTP jest tak poza zasięgiem, że musi zostać uznane za złośliwe i natychmiast zatrzymane.

6) Tak więc, dla moich pieniędzy, żądanie HTTP (METODA, adres URL / trasa i dane) jest WSZYSTKIE dobre, albo NIC nie może kontynuować. Solidny model ma już zadania sprawdzania poprawności, ale dobry pasterz zasobów mówi: „Moja droga lub wysoka droga. Przyjdź dobrze, albo wcale nie przychodź”.

Ale to twój program. „Jest na to więcej niż jeden sposób”. Niektóre sposoby kosztują więcej czasu i pieniędzy niż inne. Sprawdzanie poprawności danych żądań HTTP później (w modelu) powinno kosztować więcej w całym okresie użytkowania aplikacji (szczególnie w przypadku skalowania w górę lub w dół).

Jeśli walidatory są modułowe, sprawdzanie poprawności * podstawowych danych wejściowych żądań HTTP ** w kontrolerze nie powinno stanowić problemu. Wystarczy użyć strategicznej klasy Validator, w której walidatory czasami składają się również ze specjalistycznych walidatorów (e-mail, telefon, token formularza, captcha, ...).

Niektórzy uważają to za całkowicie błędne, ale HTTP był w powijakach, gdy Gang of Four napisał Design Patterns: Elements of Re-useable Object-Oriented Software .

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

Teraz, ponieważ dotyczy normalnego sprawdzania poprawności danych wprowadzanych przez użytkownika (po tym, jak żądanie HTTP zostało uznane za prawidłowe), aktualizuje widok, gdy użytkownik się zorientuje, o czym należy pomyśleć! Ten rodzaj sprawdzania poprawności danych wejściowych przez użytkownika powinien mieć miejsce w modelu.

Nie masz gwarancji JavaScript w interfejsie. Oznacza to, że nie ma możliwości zagwarantowania asynchronicznej aktualizacji interfejsu użytkownika aplikacji o statusie błędu. Prawdziwe progresywne ulepszenie obejmowałoby również przypadek użycia synchronicznego.

Uwzględnianie przypadku użycia synchronicznego to sztuka, która jest coraz bardziej zagubiona, ponieważ niektórzy ludzie nie chcą tracić czasu i problemów związanych ze śledzeniem stanu wszystkich swoich sztuczek interfejsu użytkownika (pokaż / ukryj elementy sterujące, wyłącz / włącz elementy sterujące , wskazania błędów, komunikaty o błędach) na zapleczu (zwykle poprzez śledzenie stanu w tablicach).

Aktualizacja : Na schemacie mówię, że Viewpowinien odnosić się do Model. Nie. Powinieneś przekazać dane Viewz, Modelaby zachować luźne połączenie. wprowadź opis zdjęcia tutaj

Anthony Rutledge
źródło