En MVC, ¿debería un modelo manejar la validación?

25

Estoy tratando de rediseñar una aplicación web que desarrollé para usar el patrón MVC, pero no estoy seguro de si la validación debe manejarse en el modelo o no. Por ejemplo, estoy configurando uno de mis modelos así:

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

Primera pregunta: ¿Entonces me pregunto si mi método de guardar debería llamar a una función de validación en $ new_data o asumir que los datos ya han sido validados?

Además, si ofreciera validación, estoy pensando que parte del código del modelo para definir los tipos de datos se vería así:

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

Segunda pregunta: Cada clase secundaria de AM_Object ejecutaría register_property para cada columna en la base de datos de ese objeto específico. No estoy seguro de si esta es una buena manera de hacerlo o no.

Tercera pregunta: si el modelo debe manejar la validación, ¿debería devolver un mensaje de error o un código de error y hacer que la vista use el código para mostrar un mensaje apropiado?

Brandon Wamboldt
fuente

Respuestas:

30

Primera respuesta: Un papel clave del modelo es mantener la integridad. Sin embargo, procesar la entrada del usuario es responsabilidad de un controlador.

Es decir, el controlador debe traducir los datos del usuario (que la mayoría de las veces son solo cadenas) en algo significativo. Esto requiere análisis (y puede depender de cosas como la configuración regional, dado que, por ejemplo, hay diferentes operadores decimales, etc.).
Entonces la validación real, como en "¿los datos están bien formados?", Debe ser realizada por el controlador Sin embargo, la verificación, como en "¿tienen sentido los datos?" debe realizarse dentro del modelo.

Para aclarar esto con un ejemplo:
suponga que su aplicación le permite agregar algunas entidades, con una fecha (un problema con una fecha límite, por ejemplo). Es posible que tenga una API, donde las fechas pueden representarse como simples marcas de tiempo de Unix, mientras que cuando proviene de una página HTML, será un conjunto de valores diferentes o una cadena en el formato de MM / DD / AAAA. No desea esta información en el modelo. Desea que cada controlador intente individualmente averiguar la fecha. Sin embargo, cuando la fecha se pasa al modelo, el modelo debe mantener la integridad. Por ejemplo, podría tener sentido no permitir fechas en el pasado, o fechas que sean feriados / domingos, etc.

Su controlador contiene reglas de entrada (procesamiento). Su modelo contiene reglas comerciales. Desea que sus reglas comerciales se apliquen siempre, pase lo que pase. Suponiendo que tuviera reglas comerciales en el controlador, tendría que duplicarlas si alguna vez creara un controlador diferente.

Segunda respuesta: El enfoque tiene sentido, sin embargo, el método podría hacerse más poderoso. En lugar de que el último parámetro sea una matriz, debería ser una instancia IContstraintque se define como:

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

Y para los números podrías tener algo como

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

Además, no veo lo que 'Age'se supone que representa, para ser honesto. ¿Es el nombre real de la propiedad? Asumiendo que hay una convención por defecto, el parámetro podría simplemente ir al final de la función y ser opcional. Si no se establece, el valor predeterminado sería to_camel_case del nombre de la columna DB.

Por lo tanto, la llamada de ejemplo se vería así:

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

El objetivo del uso de interfaces es que puede agregar más y más restricciones a medida que avanza y pueden ser tan complicadas como desee. Para que una cadena coincida con una expresión regular. Para una fecha con al menos 7 días de anticipación. Y así.

Tercera respuesta: Cada entidad modelo debe tener un método como Result checkValue(string property, mixed value). El controlador debe llamarlo antes de configurar los datos. El Resultdebe tener toda la información acerca de si el cheque no, y en caso de que así fuera, dar razones, por lo que el controlador puede propagar aquellos a la vista en consecuencia.
Si se pasa un valor incorrecto al modelo, el modelo simplemente debería responder planteando una excepción.

back2dos
fuente
Gracias por este artículo. Aclaró muchas cosas sobre MVC.
AmadeusDrZaius
5

No estoy completamente de acuerdo con "back2dos": mi recomendación es usar siempre una capa de formulario / validación separada, que el controlador puede usar para validar los datos de entrada antes de enviarlos al modelo.

Desde un punto de vista teórico, la validación del modelo opera en datos confiables (estado interno del sistema) y, de manera ideal, debe ser repetible en cualquier momento, mientras que la validación de entrada opera explícitamente una vez en los datos que provienen de fuentes no confiables (dependiendo del caso de uso y los privilegios del usuario).

Esta separación permite construir modelos, controladores y formas reutilizables que pueden acoplarse libremente mediante la inyección de dependencia. Piense en la validación de entrada como validación de la lista blanca ("aceptar lo bueno conocido") y la validación del modelo como validación de lista negra ("rechazar lo malo conocido"). La validación de la lista blanca es más segura, mientras que la validación de la lista negra evita que la capa de su modelo esté demasiado limitada a casos de uso muy específicos.

Los datos de modelo no válidos siempre deben generar una excepción (de lo contrario, la aplicación puede continuar ejecutándose sin darse cuenta del error), mientras que los valores de entrada no válidos provenientes de fuentes externas no son inesperados, sino más bien comunes (a menos que haya usuarios que nunca cometan errores).

Ver también: https://lastzero.net/2015/11/why-im-using-a-separate-layer-for-input-data-validation/

lastzero
fuente
Por simplicidad, supongamos que hay una familia de clase Validator, y que todas las validaciones se realizan con una jerarquía estratégica. Los niños validadores concretos también pueden estar compuestos por validadores especializados: correo electrónico, número de teléfono, fichas de formulario, captcha, contraseña y otros. La validación de entrada del controlador es de dos tipos:??. 1) verificación de la existencia de un controlador y un método / sistema, y 2) un examen preliminar de los datos (es decir, el método de solicitud HTTP, el número de entradas de datos (también muchos Demasiado pocos)
Anthony Rutledge
Después de verificar la cantidad de entradas, debe saber que se enviaron los controles HTML correctos, por nombre, teniendo en cuenta que el número de entradas por solicitud puede variar, ya que no todos los controles de un formulario HTML envían algo cuando se deja en blanco ( especialmente casillas de verificación). Después de esto, la última verificación preliminar es una prueba del tamaño de entrada. En mi opinión, esto debería ser temprano , no tarde. Hacer la cantidad, el nombre del control y la verificación del tamaño de entrada básico en un validador del controlador significaría tener un Validador para cada comando / método en el controlador. Creo que esto hace que su aplicación sea más segura.
Anthony Rutledge
Sí, el validador del controlador para un comando estará estrechamente acoplado a los argumentos (si los hay) requeridos para un método modelo , pero el controlador en sí no lo será, salvo la referencia a dicho validador del controlador . Este es un compromiso digno, ya que uno no debe avanzar con el supuesto de que la mayoría de los aportes serán legítimos. Cuanto antes pueda detener el acceso ilegítimo a su aplicación, mejor. Hacerlo en una clase de validador de controlador (cantidad, nombre y tamaño máximo de entradas) le ahorra tener que instanciar todo el modelo para rechazar solicitudes HTTP claramente maliciosas.
Anthony Rutledge
Dicho esto, antes de abordar los problemas de tamaño máximo de entrada, uno debe asegurarse de que la codificación sea buena. A fin de cuentas, esto es demasiado para el modelo, incluso si el trabajo está encapsulado. Se vuelve innecesariamente costoso rechazar solicitudes maliciosas. En resumen, el controlador debe asumir más responsabilidad por lo que envía al modelo. La falla en el nivel del controlador debe ser fatal, sin información de devolución para el solicitante que no sea 200 OK. Registra la actividad. Lanza una excepción fatal. Terminar toda actividad. Detenga todos los procesos lo antes posible.
Anthony Rutledge
Los controles mínimos, los controles máximos, los controles correctos, la codificación de entrada y el tamaño máximo de entrada pertenecen a la naturaleza de la solicitud (de una forma u otra). Algunas personas no han identificado estas cinco cosas centrales como determinantes de si una solicitud debe cumplirse. Si todas estas cosas no están satisfechas, ¿por qué envía esta información al modelo? Buena pregunta.
Anthony Rutledge
3

Sí, el modelo debe realizar la validación. La interfaz de usuario también debe validar la entrada.

Es claramente responsabilidad del modelo determinar valores y estados válidos. A veces, tales reglas cambian a menudo. En ese caso, alimentaría el modelo a partir de metadatos y / o lo decoraría.

Halcón
fuente
¿Qué sucede con los casos en que la intención del usuario es claramente maliciosa o errónea? Por ejemplo, se supone que una solicitud HTTP particular no tiene más de siete (7) valores de entrada, pero su controlador obtiene setenta (70). ¿Realmente va a permitir diez veces (10x) el número de valores permitidos para alcanzar el modelo cuando la solicitud está claramente corrupta? En este caso, lo que está en cuestión es el estado de toda la solicitud, no el estado de ningún valor en particular. Una estrategia de defensa en profundidad sugeriría que se debe examinar la naturaleza de la solicitud HTTP antes de enviar los datos al modelo.
Anthony Rutledge
(continuación) De esta manera, no verifica que los valores y estados proporcionados por el usuario en particular sean válidos, sino que la totalidad de la solicitud es válida. No hay necesidad de profundizar tanto, todavía. El petróleo ya está en la superficie.
Anthony Rutledge
(continuación) No hay forma de forzar la validación frontal. Hay que tener en cuenta que las herramientas automatizadas se pueden utilizar para interactuar con su aplicación web.
Anthony Rutledge
(Después de pensarlo) Los valores válidos y los estados de datos en el modelo son importantes, pero lo que he descrito coincide con la intención de que la solicitud llegue a través del controlador. Omitir la verificación de intención deja a su aplicación más vulnerable. La intención solo puede ser buena (según tus reglas) o mala (ir fuera de tus reglas). La intención puede verificarse mediante comprobaciones básicas en la entrada: controles mínimos, controles máximos, controles correctos, codificación de entrada y tamaño máximo de entrada. Es una propuesta de todo o nada. Todo pasa o la solicitud no es válida. No es necesario enviar nada al modelo.
Anthony Rutledge
2

Gran pregunta!

En términos de desarrollo de la red mundial, ¿qué pasaría si preguntaras lo siguiente también?

"Si se suministra una entrada de usuario incorrecta a un controlador desde una interfaz de usuario, ¿ debería el controlador actualizar la Vista en una especie de bucle cíclico, obligando a los comandos y datos de entrada a ser precisos antes de procesarlos ? ¿Cómo? ¿Cómo se actualiza la vista en condiciones normales? ¿Es una vista estrechamente acoplada a un modelo? ¿La validación de entrada del usuario es la lógica comercial central del modelo, o es preliminar y, por lo tanto, debe ocurrir dentro del controlador (porque los datos de entrada del usuario son parte de la solicitud)?

(En efecto, ¿se puede y se debe retrasar la creación de instancias de un modelo hasta que se obtenga una buena entrada?)

Mi opinión es que los modelos deben manejar una circunstancia pura y prístina (tanto como sea posible), sin la carga de una validación de entrada de solicitud HTTP básica que debe ocurrir antes de la creación de instancias del modelo (y definitivamente antes de que el modelo obtenga datos de entrada). Dado que la gestión de datos de estado (persistentes o de otro tipo) y las relaciones API es el mundo del modelo, permita que la validación de entrada de solicitud HTTP básica ocurra en el controlador.

Resumiendo

1) Valide su ruta (analizada desde la URL), ya que el controlador y el método deben existir antes de que cualquier otra cosa pueda avanzar. Esto definitivamente debería suceder en el reino del controlador frontal (clase de enrutador), antes de llegar al controlador verdadero. Duh :-)

2) Un modelo puede tener muchas fuentes de datos de entrada: una solicitud HTTP, una base de datos, un archivo, una API y, sí, una red. Si va a colocar toda su validación de entrada en el modelo, entonces considera que la validación de entrada de solicitud HTTP forma parte de los requisitos comerciales para el programa. Caso cerrado.

3) ¡Sin embargo, es miope pasar por el gasto de crear instancias de muchos objetos si la entrada de solicitud HTTP no es buena! Puede saber si ** la entrada de solicitud HTTP ** es buena ( que vino con la solicitud ) al validarla antes de instanciar el modelo y todas sus complejidades (sí, quizás incluso más validadores para datos de entrada / salida de API y DB).

Prueba lo siguiente:

a) El método de solicitud HTTP (GET, POST, PUT, PATCH, DELETE ...)

b) Controles mínimos de HTML (¿tiene suficiente?).

c) Controles HTML máximos (¿tienes demasiados?).

d) Controles HTML correctos (¿tiene los correctos?).

e) Codificación de entrada (típicamente, ¿es la codificación UTF-8?).

f) Tamaño máximo de entrada (¿alguna de las entradas está fuera de límites?).

Recuerde, puede obtener cadenas y archivos, por lo que esperar a que se instale el modelo podría ser muy costoso a medida que las solicitudes lleguen a su servidor.

Lo que he descrito aquí afecta a la intención de que la solicitud llegue a través del controlador. Omitir la verificación de intención deja a su aplicación más vulnerable. La intención solo puede ser buena (siguiendo tus reglas fundamentales) o mala (yendo fuera de tus reglas fundamentales).

La intención de una solicitud HTTP es una propuesta de todo o nada. Todo pasa o la solicitud no es válida . No es necesario enviar nada al modelo.

Este nivel básico de intención de solicitud HTTP no tiene nada que ver con los errores de entrada y validación de los usuarios regulares. En mis aplicaciones, una solicitud HTTP debe ser válida en las cinco formas anteriores para que pueda cumplirla. En una forma de hablar de defensa en profundidad , nunca puede obtener la validación de entrada del usuario en el lado del servidor si alguna de estas cinco cosas falla.

Sí, esto significa que incluso la entrada del archivo debe ajustarse a sus intentos de front-end para verificar y decirle al usuario el tamaño máximo de archivo aceptado. ¿Solo HTML? ¿Sin JavaScript? Bien, pero el usuario debe ser informado de las consecuencias de cargar archivos que son demasiado grandes (principalmente, que perderán todos los datos del formulario y serán expulsados ​​del sistema).

4) ¿Esto significa que los datos de entrada de la solicitud HTTP no son parte de la lógica empresarial de la aplicación? No, solo significa que las computadoras son dispositivos finitos y los recursos deben usarse con prudencia. Tiene sentido detener la actividad maliciosa antes, no más tarde. Paga más en recursos de cómputo por esperar para detenerlo más tarde.

5) Si la entrada de la solicitud HTTP es incorrecta, toda la solicitud es incorrecta . Así es como lo veo. La definición de una buena entrada de solicitud HTTP se deriva de los requisitos comerciales del modelo, pero debe haber algún punto de demarcación de recursos. ¿Cuánto tiempo dejarás vivir una mala solicitud antes de matarla y decir: "Oh, oye, no importa. Mala solicitud".

El juicio no es simplemente que el usuario ha cometido un error de entrada razonable, sino que una solicitud HTTP está tan fuera de los límites que debe declararse maliciosa y detenerse de inmediato.

6) Por lo tanto, por mi dinero, la solicitud HTTP (MÉTODO, URL / ruta y datos) es TODO buena, o NADA más puede continuar. Un modelo robusto ya tiene tareas de validación con las que preocuparse, pero un buen pastor de recursos dice "Mi camino o el camino alto. Ven a corregir o no vengas".

Sin embargo, es su programa. "Hay más de una forma de hacerlo". Algunas formas cuestan más en tiempo y dinero que otras. Validar los datos de la solicitud HTTP más adelante (en el modelo) debería costar más durante la vida útil de una aplicación (especialmente si se amplía o reduce).

Si sus validadores son modulares, validar la entrada de solicitud HTTP básica * en el controlador no debería ser un problema. Simplemente use una clase Validator estratégica, donde los validadores a veces también se componen de validadores especializados (correo electrónico, teléfono, token de formulario, captcha, ...).

Algunos ven esto como algo completamente equivocado, pero HTTP estaba en su infancia cuando la Banda de los Cuatro escribió Patrones de diseño: Elementos de software orientado a objetos reutilizables .

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

Ahora, en lo que respecta a la validación de entrada de usuario normal (después de que la solicitud HTTP se haya considerado válida), ¡está actualizando la vista cuando el usuario comete un error en el que debe pensar! Este tipo de validación de entrada del usuario debe ocurrir en el modelo.

No tiene garantía de JavaScript en el front-end. Esto significa que no tiene forma de garantizar la actualización asincrónica de la interfaz de usuario de su aplicación con estados de error. La verdadera mejora progresiva también cubriría el caso de uso sincrónico.

Tener en cuenta el caso de uso síncrono es un arte que se pierde cada vez más porque algunas personas no quieren pasar por el tiempo y las molestias de rastrear el estado de todos sus trucos de interfaz de usuario (mostrar / ocultar controles, deshabilitar / habilitar controles , indicaciones de error, mensajes de error) en el back-end (generalmente mediante el seguimiento del estado en matrices).

Actualización : en el diagrama, digo que Viewdeberían hacer referencia a Model. No. Debe pasar los datos Viewdesde el Modelpara preservar el acoplamiento suelto. ingrese la descripción de la imagen aquí

Anthony Rutledge
fuente