No MVC, um modelo deve lidar com a validação?

25

Estou tentando re-arquitetar um aplicativo Web que desenvolvi para usar o padrão MVC, mas não tenho certeza se a validação deve ser tratada no modelo ou não. Por exemplo, estou configurando um dos meus modelos assim:

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

Primeira pergunta: Então, eu estou querendo saber se meu método save deve chamar uma função de validação em $ new_data ou assumir que os dados já foram validados?

Além disso, se fosse oferecer validação, estou pensando que alguns dos códigos de modelo para definir tipos de dados ficariam assim:

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 pergunta: Toda classe filho de AM_Object executaria register_property para cada coluna no banco de dados desse objeto específico. Não tenho certeza se essa é uma boa maneira de fazê-lo ou não.

Terceira pergunta: Se a validação deve ser tratada pelo modelo, ela deve retornar uma mensagem de erro ou um código de erro e fazer com que a visualização use o código para exibir uma mensagem apropriada?

Brandon Wamboldt
fonte

Respostas:

30

Primeira resposta: Um papel fundamental do modelo é manter a integridade. Entretanto, o processamento da entrada do usuário é de responsabilidade de um controlador.

Ou seja, o controlador deve converter os dados do usuário (que na maioria das vezes são apenas strings) em algo significativo. Isso requer análise (e pode depender de coisas como a localidade, pois, por exemplo, existem diferentes operadores decimais etc.).
Portanto, a validação real, como em "os dados estão bem formados?", Deve ser realizada pelo controlador. No entanto, a verificação, como em "os dados fazem sentido?" deve ser realizado dentro do modelo.

Para esclarecer isso com um exemplo:
Suponha que seu aplicativo permita adicionar algumas entidades, com uma data (um problema com um prazo final, por exemplo). Você pode ter uma API, na qual as datas podem ser representadas como meros carimbos de data e hora do Unix, enquanto, quando proveniente de uma página HTML, será um conjunto de valores diferentes ou uma sequência no formato MM / DD / AAAA. Você não deseja essas informações no modelo. Você deseja que cada controlador tente individualmente descobrir a data. No entanto, quando a data é passada para o modelo, o modelo deve manter a integridade. Por exemplo, pode fazer sentido não permitir datas no passado ou datas de feriados / domingos etc.

Seu controlador contém regras de entrada (processamento). Seu modelo contém regras de negócios. Você deseja que suas regras de negócios sejam sempre aplicadas, não importa o que aconteça. Supondo que você tenha regras de negócios no controlador, será necessário duplicá-las, caso crie um controlador diferente.

Segunda resposta: A abordagem faz sentido, no entanto, o método pode ser mais poderoso. Em vez de o último parâmetro ser uma matriz, deve ser uma instância IContstraintdefinida como:

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

E para números, você pode ter 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);
    }
}

Também não vejo o que 'Age'se pretende representar, para ser honesto. É o nome da propriedade real? Supondo que exista uma convenção por padrão, o parâmetro pode simplesmente ir até o final da função e ser opcional. Se não estiver definido, o padrão será o to_camel_case do nome da coluna DB.

Assim, a chamada de exemplo seria semelhante a:

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

O objetivo do uso de interfaces é que você pode adicionar mais e mais restrições à medida que avança e elas podem ser tão complicadas quanto você desejar. Para uma string corresponder a uma expressão regular. Para uma data com pelo menos 7 dias de antecedência. E assim por diante.

Terceira resposta: Toda entidade Modelo deve ter um método como Result checkValue(string property, mixed value). O controlador deve chamá-lo antes de definir os dados. Ele Resultdeve ter todas as informações sobre se a verificação falhou e, caso tenha ocorrido, forneça razões, para que o controlador possa propagar essas para a visualização de acordo.
Se um valor errado for passado para o modelo, o modelo deve simplesmente responder levantando uma exceção.

back2dos
fonte
Obrigado por este artigo. Esclareceu muitas coisas sobre o MVC.
AmadeusDrZaius
5

Não concordo totalmente com o "back2dos": minha recomendação é sempre usar uma camada de formulário / validação separada, que o controlador possa usar para validar os dados de entrada antes de serem enviados ao modelo.

Do ponto de vista teórico, a validação do modelo opera com dados confiáveis ​​(estado interno do sistema) e, idealmente, pode ser repetida a qualquer momento, enquanto a validação de entrada opera explicitamente uma vez em dados provenientes de fontes não confiáveis ​​(dependendo do caso de uso e dos privilégios do usuário).

Essa separação possibilita a criação de modelos, controladores e formulários reutilizáveis ​​que podem ser acoplados livremente através da injeção de dependência. Pense na validação de entrada como validação da lista de permissões ("aceite bom conhecido") e validação de modelo como validação de lista negra ("rejeite conhecido ruim"). A validação da lista de permissões é mais segura, enquanto a validação da lista negra impede que sua camada de modelo seja excessivamente restrita a casos de uso muito específicos.

Dados de modelo inválidos sempre devem gerar uma exceção (caso contrário, o aplicativo pode continuar em execução sem perceber o erro), enquanto valores de entrada inválidos provenientes de fontes externas não são inesperados, mas sim comuns (a menos que você tenha usuários que nunca cometerão erros).

Veja também: https://lastzero.net/2015/11/why-im-using-a-separate-layer-for-input-data-validation/

lastzero
fonte
Para simplificar, vamos supor que exista uma família de classes Validator e que todas as validações sejam feitas com uma hierarquia estratégica. Os filhos de validadores concretos também podem ser compostos por validadores especializados: email, número de telefone, tokens de formulário, captcha, senha e outros. Validação de entrada do controlador é de dois tipos:?. 1) Verificar a existência de um controlador e método / comando, e 2) um exame preliminar dos dados (ou seja, o método de solicitação HTTP, quantas entradas de dados (muitas Muito poucos)
Anthony Rutledge
Após a verificação da quantidade de entradas, você precisa saber que os controles HTML corretos foram enviados, por nome, tendo em mente que o número de entradas por solicitação pode variar, pois nem todos os controles de um formulário HTML enviam algo quando são deixados em branco ( especialmente caixas de seleção). Depois disso, a última verificação preliminar é um teste do tamanho da entrada. Na minha opinião, isso deve ser cedo , não tarde. Fazer a verificação da quantidade, nome do controle e tamanho básico da entrada em um validador do controlador significaria ter um Validator para cada comando / método no controlador. Eu sinto que isso torna seu aplicativo mais seguro.
Anthony Rutledge
Sim, o validador do controlador para um comando será fortemente acoplado aos argumentos (se houver) necessários para um método de modelo , mas o próprio controlador não será, exceto a referência ao referido validador do controlador . Este é um compromisso digno, pois não se deve avançar com a suposição de que a maioria dos insumos será legítima. Quanto mais cedo você puder interromper o acesso ilegítimo ao seu aplicativo, melhor. Fazer isso em uma classe de validador de controlador (quantidade, nome e tamanho máximo de entradas) evita que você precise instanciar o modelo inteiro para rejeitar solicitações HTTP claramente maliciosas.
Anthony Rutledge
Dito isto, antes de abordar questões de tamanho máximo de entrada, é preciso garantir que a codificação seja boa. Tudo considerado, isso é demais para o modelo, mesmo que o trabalho seja encapsulado. Torna-se desnecessariamente caro rejeitar solicitações maliciosas. Em resumo, o controlador precisa assumir mais responsabilidade pelo que envia ao modelo. A falha no nível do controlador deve ser fatal, sem informações de retorno ao solicitante que não sejam 200 OK. Registre a atividade. Lance uma exceção fatal. Encerre todas as atividades. Pare todos os processos o mais rápido possível.
Anthony Rutledge
Controles mínimos, controles máximos, controles corretos, codificação de entrada e tamanho máximo de entrada pertencem à natureza da solicitação (de uma maneira ou de outra). Algumas pessoas não identificaram essas cinco coisas principais como determinando se uma solicitação deve ser atendida. Se todas essas coisas não forem satisfeitas, por que você está enviando essas informações para o modelo? Boa pergunta.
Anthony Rutledge
3

Sim, o modelo deve executar a validação. A interface do usuário deve validar a entrada também.

É claramente da responsabilidade do modelo determinar valores e estados válidos. Às vezes, essas regras mudam com frequência. Nesse caso, eu alimentaria o modelo a partir de metadados e / ou o decoraria.

Falcão
fonte
E os casos em que a intenção do usuário é claramente maliciosa ou está errada? Por exemplo, uma requisição HTTP específica deve ter no máximo sete (7) valores de entrada, mas seu controlador obtém setenta (70). Você realmente permitirá que dez vezes (10x) o número de valores permitidos atinja o modelo quando a solicitação estiver claramente corrompida? Nesse caso, é o estado de toda a solicitação que está em questão, não o estado de qualquer valor específico. Uma estratégia de defesa em profundidade sugeriria que a natureza da solicitação HTTP fosse examinada antes de enviar dados para o modelo.
Anthony Rutledge
(continuação) Dessa forma, você não está verificando se determinados valores e estados fornecidos pelo usuário são válidos, mas se a totalidade da solicitação é válida. Ainda não há necessidade de detalhar até agora. O óleo já está na superfície.
Anthony Rutledge
(continuação) Não há como forçar a validação do front-end. É preciso considerar que ferramentas automatizadas podem ser usadas em interface com seu aplicativo da web.
Anthony Rutledge
(Após uma reflexão) Valores e estados de dados válidos no modelo são importantes, mas o que descrevi ocorre com a intenção da solicitação que chega pelo controlador. Omitir a verificação de intenção deixa seu aplicativo mais vulnerável. A intenção só pode ser boa (jogando de acordo com suas regras) ou ruim (sair de suas regras). A intenção pode ser verificada por verificações básicas na entrada: controles mínimos, controles máximos, controles corretos, codificação de entrada e tamanho máximo de entrada. É uma proposta de tudo ou nada. Tudo passa ou a solicitação é inválida. Não há necessidade de enviar nada para o modelo.
Anthony Rutledge
2

Ótima pergunta!

Em termos de desenvolvimento na World Wide Web, e se você perguntasse o seguinte, também.

"Se uma entrada incorreta do usuário for fornecida a um controlador a partir de uma interface com o usuário, o controlador deve atualizar o View em um tipo de loop cíclico, forçando comandos e dados de entrada a serem precisos antes de processá-los ? Como? Como? Como o modo de exibição é atualizado normalmente? É uma visão fortemente acoplada a um modelo? A validação de entrada do usuário é a lógica de negócios principal do modelo, ou é preliminar a ele e, portanto, deve ocorrer dentro do controlador (porque os dados de entrada do usuário fazem parte da solicitação)?

(Com efeito, pode e deve-se atrasar a instanciação de um modelo até que boas informações sejam adquiridas?)

Minha opinião é que os modelos devem gerenciar uma circunstância pura e pura (o máximo possível), livre da validação básica de entrada de solicitação HTTP que deve ocorrer antes da instanciação do modelo (e definitivamente antes que o modelo obtenha dados de entrada). Como gerenciar dados de estado (persistentes ou não) e relacionamentos de API é o mundo do modelo, permita que a validação básica de entrada de solicitação HTTP ocorra no controlador.

Resumindo.

1) Valide sua rota (analisada a partir da URL), pois o controlador e o método devem existir antes que qualquer outra coisa possa avançar. Definitivamente, isso deve acontecer no domínio do controlador frontal (classe Router), antes de chegar ao verdadeiro controlador. Duh. :-)

2) Um modelo pode ter muitas fontes de dados de entrada: uma solicitação HTTP, um banco de dados, um arquivo, uma API e, sim, uma rede. Se você deseja colocar toda a validação de entrada no modelo, considere a validação de entrada de solicitação HTTP parte dos requisitos de negócios do programa. Caso encerrado.

3) No entanto, é míope passar pela despesa de instanciar muitos objetos se a entrada da solicitação HTTP não for boa! Você pode saber se ** entrada de solicitação HTTP ** é boa ( que veio com a solicitação ) validando-a antes de instanciar o modelo e todas as suas complexidades (sim, talvez ainda mais validadores para API e DB de dados de entrada / saída).

Teste o seguinte:

a) O método de solicitação HTTP (GET, POST, PUT, PATCH, DELETE ...)

b) Controles HTML mínimos (você tem o suficiente?).

c) Máximo de controles HTML (você tem muitos?).

d) Controles HTML corretos (você tem os corretos?).

e) Codificação de entrada (normalmente, é a codificação UTF-8?).

f) Tamanho máximo da entrada (alguma das entradas está muito fora dos limites?).

Lembre-se, você pode obter seqüências de caracteres e arquivos, portanto, aguardar o modelo instanciar pode ficar muito caro à medida que as solicitações atingirem o servidor.

O que descrevi aqui ocorre com a intenção da solicitação que chega pelo controlador. Omitir a verificação de intenção deixa seu aplicativo mais vulnerável. A intenção só pode ser boa (seguindo suas regras fundamentais) ou ruim (indo além de suas regras fundamentais).

A intenção de uma solicitação HTTP é uma proposta de tudo ou nada. Tudo passa ou a solicitação é inválida . Não há necessidade de enviar nada para o modelo.

Esse nível básico de intenção de solicitação HTTP não tem nada a ver com erros e validação regulares de entrada do usuário. Nos meus aplicativos, uma solicitação HTTP deve ser válida das cinco maneiras acima para que eu a honre. Em uma defesa em profundidade maneira de falar, você nunca chegar a validação de entrada do usuário no lado do servidor, se nenhum destes cinco coisas falham.

Sim, isso significa que mesmo a entrada do arquivo deve estar em conformidade com as suas tentativas de front-end para verificar e informar ao usuário o tamanho máximo do arquivo aceito. Apenas HTML? Sem JavaScript? Tudo bem, mas o usuário deve ser informado das consequências do upload de arquivos muito grandes (principalmente, que eles perderão todos os dados do formulário e serão expulsos do sistema).

4) Isso significa que os dados de entrada da solicitação HTTP não fazem parte da lógica de negócios do aplicativo? Não, apenas significa que os computadores são dispositivos finitos e os recursos devem ser usados ​​com sabedoria. Faz sentido interromper atividades maliciosas mais cedo ou mais tarde. Você paga mais em recursos de computação pela espera para interrompê-lo mais tarde.

5) Se a entrada da solicitação HTTP estiver incorreta, a solicitação inteira estará incorreta . É assim que eu olho para isso. A definição de boa entrada de solicitação HTTP é derivada dos requisitos de negócios do modelo, mas deve haver algum ponto de demarcação de recurso. Por quanto tempo você deixará um pedido ruim permanecer antes de matá-lo e dizer: "Ei, não importa. Pedido ruim".

O julgamento não é simplesmente que o usuário cometeu um erro razoável de entrada, mas que uma solicitação HTTP é tão fora dos limites que deve ser declarada maliciosa e interrompida imediatamente.

6) Portanto, para meu dinheiro, a solicitação HTTP (MÉTODO, URL / rota e dados) é TUDO boa ou NADA mais pode prosseguir. Um modelo robusto já tem tarefas de validação com as quais se preocupar, mas um bom pastor de recursos diz: "Meu caminho, ou o caminho mais alto. Seja correto, ou nem venha".

É o seu programa, no entanto. "Há mais de uma maneira de fazer isso". Algumas formas custam mais tempo e dinheiro do que outras. A validação dos dados da solicitação HTTP posteriormente (no modelo) deve custar mais ao longo da vida útil de um aplicativo (especialmente se estiver aumentando ou diminuindo).

Se seus validadores forem modulares, a validação da entrada básica * de solicitação HTTP ** no controlador não deve ser um problema. Basta usar uma classe Validator estrategizada, onde os validadores às vezes também são compostos por validadores especializados (email, telefone, token de formulário, captcha, ...).

Alguns vêem isso completamente errado, mas o HTTP estava em sua infância quando o Gang of Four escreveu Design Patterns: Elements of Re-usable Oriented Object Oriented .

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

Agora, no que se refere à validação normal de entrada do usuário (depois que a solicitação HTTP for considerada válida), ela está atualizando a visualização quando o usuário faz uma bagunça que você precisa pensar! Esse tipo de validação de entrada do usuário deve ocorrer no modelo.

Você não tem garantia de JavaScript no front-end. Isso significa que você não tem como garantir a atualização assíncrona da interface do usuário do seu aplicativo com status de erro. O verdadeiro aprimoramento progressivo também abrangeria o caso de uso síncrono.

Contabilizar o caso de uso síncrono é uma arte que está se perdendo cada vez mais, porque algumas pessoas não desejam passar pelo tempo e pelos problemas de rastrear o estado de todos os seus truques de interface do usuário (mostrar / ocultar controles, desativar / ativar controles , indicações de erro, mensagens de erro) no back-end (geralmente acompanhando o estado nas matrizes).

Atualização : no diagrama, eu digo que o Viewdeve fazer referência a Model. Não. Você deve passar os dados Viewdo Modelpara preservar o acoplamento solto. insira a descrição da imagem aqui

Anthony Rutledge
fonte