Como um modelo deve ser estruturado no MVC? [fechadas]

551

Estou apenas entendendo a estrutura do MVC e sempre me pergunto quanto código deve haver no modelo. Eu costumo ter uma classe de acesso a dados que possui métodos como este:

public function CheckUsername($connection, $username)
{
    try
    {
        $data = array();
        $data['Username'] = $username;

        //// SQL
        $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";

        //// Execute statement
        return $this->ExecuteObject($connection, $sql, $data);
    }
    catch(Exception $e)
    {
        throw $e;
    }
}

Meus modelos tendem a ser uma classe de entidade que é mapeada para a tabela do banco de dados.

O objeto de modelo deve ter todas as propriedades mapeadas do banco de dados, bem como o código acima, ou é correto separar esse código que realmente funciona no banco de dados?

Vou acabar tendo quatro camadas?

Dietpixel
fonte
133
Por que você está capturando exceções apenas para lançá-las novamente?
Bailey Parker
9
@Elias Van Ootegem: você perdeu o ponto. não faz sentido pegá-los nesse caso.
Karoly Horvath
4
@Elias Van Ootegem: hein? se funcionar com relançamento, significa que uma camada superior captura a exceção. Mas se há um, em seguida, ele teria catched-lo sem que rethrow inútil ... (se você ainda não obtê-lo, por favor, mock up de um código de teste pequeno)
Karoly Horvath
3
@Elias Van Ootegem: Não faço ideia do que você está falando, não lidar com uma exceção em uma camada específica não significa que interromperá o aplicativo. construa (ou mais precisamente: falha ao construir) um exemplo de código onde essa releitura é necessária. vamos parar essa conversa offtopic, por favor
Karoly Horvath
6
@drrcknlsn: esse é um argumento válido, mas nesse caso pelo menos pegue a exceção que você espera que seja lançada, o genérico Exceptionnão tem muito valor de documentação. Pessoalmente, se eu seguisse esse caminho, escolheria o PHPDoc @exception, ou algum mecanismo semelhante, para que apareça na documentação gerada.
Karoly Horvath

Respostas:

903

Isenção de responsabilidade: a seguir, é apresentada uma descrição de como eu entendo padrões semelhantes ao MVC no contexto de aplicativos Web baseados em PHP. Todos os links externos usados ​​no conteúdo existem para explicar termos e conceitos, e não para implicar minha própria credibilidade no assunto.

A primeira coisa que devo esclarecer é: o modelo é uma camada .

Segundo: existe uma diferença entre o MVC clássico e o que usamos no desenvolvimento da web. Aqui está uma resposta mais antiga que escrevi, que descreve brevemente como elas são diferentes.

O que um modelo NÃO é:

O modelo não é uma classe ou nenhum objeto único. É um erro muito comum de cometer (também o fiz, embora a resposta original tenha sido escrita quando comecei a aprender o contrário) , porque a maioria das estruturas perpetua esse equívoco.

Nem é uma técnica de mapeamento objeto-relacional (ORM), nem uma abstração de tabelas de banco de dados. Qualquer pessoa que diga o contrário provavelmente tentará "vender" outro ORM novinho em folha ou uma estrutura inteira.

O que é um modelo:

Na adaptação adequada do MVC, o M contém toda a lógica de negócios do domínio e a Camada de Modelo é composta principalmente de três tipos de estruturas:

  • Objetos de domínio

    Um objeto de domínio é um contêiner lógico de informações puramente de domínio; geralmente representa uma entidade lógica no espaço do domínio do problema. Geralmente chamado de lógica de negócios .

    É aqui que você define como validar os dados antes de enviar uma fatura ou calcular o custo total de um pedido. Ao mesmo tempo, os Objetos de Domínio desconhecem completamente o armazenamento - nem de onde (banco de dados SQL, API REST, arquivo de texto etc.) nem mesmo se forem salvos ou recuperados.

  • Mapeadores de dados

    Esses objetos são responsáveis ​​apenas pelo armazenamento. Se você armazenar informações em um banco de dados, é onde o SQL mora. Ou talvez você use um arquivo XML para armazenar dados, e seus Mapeadores de Dados estão analisando de e para arquivos XML.

  • Serviços

    Você pode pensar neles como "Objetos de Domínio de nível superior", mas, em vez da lógica de negócios, os Serviços são responsáveis ​​pela interação entre Objetos de Domínio e Mapeadores . Essas estruturas acabam criando uma interface "pública" para interagir com a lógica de negócios do domínio. Você pode evitá-los, mas sob pena de vazar alguma lógica de domínio nos Controladores .

    Há uma resposta relacionada a esse assunto na pergunta de implementação da ACL - pode ser útil.

A comunicação entre a camada do modelo e outras partes da tríade MVC deve ocorrer apenas através dos Serviços . A separação clara tem alguns benefícios adicionais:

  • ajuda a aplicar o princípio da responsabilidade única (SRP)
  • fornece 'espaço de manobra' adicional caso a lógica mude
  • mantém o controlador o mais simples possível
  • fornece um plano claro, se você precisar de uma API externa

 

Como interagir com um modelo?

Pré-requisitos: assista às palestras "Estado Global e Singletons" e "Não procure coisas!" das conversas sobre código limpo.

Obtendo acesso a instâncias de serviço

Para as instâncias View e Controller (o que você poderia chamar de "camada da interface do usuário") para acessar esses serviços, há duas abordagens gerais:

  1. Você pode injetar os serviços necessários nos construtores de suas visualizações e controladores diretamente, de preferência usando um contêiner DI.
  2. Usando uma fábrica de serviços como uma dependência obrigatória para todos os seus modos de exibição e controladores.

Como você pode suspeitar, o contêiner DI é uma solução muito mais elegante (embora não seja a mais fácil para iniciantes). As duas bibliotecas que recomendo considerar para essa funcionalidade seriam o componente DependencyInjection autônomo da Syfmony ou Auryn .

As soluções que usam uma fábrica e um contêiner de DI também permitem compartilhar as instâncias de vários servidores a serem compartilhadas entre o controlador selecionado e exibir um determinado ciclo de solicitação-resposta.

Alteração do estado do modelo

Agora que você pode acessar a camada de modelo nos controladores, é necessário começar a usá-los:

public function postLogin(Request $request)
{
    $email = $request->get('email');
    $identity = $this->identification->findIdentityByEmailAddress($email);
    $this->identification->loginWithPassword(
        $identity,
        $request->get('password')
    );
}

Seus controladores têm uma tarefa muito clara: pegue a entrada do usuário e, com base nessa entrada, altere o estado atual da lógica de negócios. Neste exemplo, os estados que são alterados são "usuário anônimo" e "usuário conectado".

O Controller não é responsável por validar a entrada do usuário, porque isso faz parte das regras de negócios e o controlador definitivamente não está chamando consultas SQL, como o que você veria aqui ou aqui (por favor, não as odeie, elas são mal orientadas, não são más).

Mostrando ao usuário a mudança de estado.

Ok, o usuário efetuou login (ou falhou). O que agora? O referido usuário ainda não o conhece. Então, você precisa realmente produzir uma resposta e essa é a responsabilidade de uma visão.

public function postLogin()
{
    $path = '/login';
    if ($this->identification->isUserLoggedIn()) {
        $path = '/dashboard';
    }
    return new RedirectResponse($path); 
}

Nesse caso, a visualização produziu uma das duas respostas possíveis, com base no estado atual da camada do modelo. Para um caso de uso diferente, você teria a visualização escolhendo modelos diferentes para renderizar, com base em algo como "atual selecionado do artigo".

A camada de apresentação pode ficar bastante elaborada, como descrito aqui: Entendendo as visualizações MVC no PHP .

Mas estou apenas criando uma API REST!

Claro, existem situações em que isso é um exagero.

O MVC é apenas uma solução concreta para o princípio da Separação de Preocupações . O MVC separa a interface do usuário da lógica de negócios e, na interface do usuário, separou a manipulação da entrada do usuário e a apresentação. Isto é crucial. Embora muitas vezes as pessoas o descrevam como uma "tríade", na verdade não é composta de três partes independentes. A estrutura é mais ou menos assim:

Separação MVC

Isso significa que, quando a lógica da sua camada de apresentação é quase inexistente, a abordagem pragmática é mantê-la como uma única camada. Também pode simplificar substancialmente alguns aspectos da camada do modelo.

Usando essa abordagem, o exemplo de login (para uma API) pode ser escrito como:

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

Embora isso não seja sustentável, quando você tem uma lógica complicada para renderizar um corpo de resposta, essa simplificação é muito útil para cenários mais triviais. Mas esteja avisado , essa abordagem se tornará um pesadelo, ao tentar usar em grandes bases de código com lógica de apresentação complexa.

 

Como construir o modelo?

Como não há uma única classe "Modelo" (como explicado acima), você realmente não "constrói o modelo". Em vez disso, você começa a criar serviços , capazes de executar determinados métodos. E, em seguida, implemente objetos de domínio e mapeadores .

Um exemplo de um método de serviço:

Nas duas abordagens acima, havia esse método de login para o serviço de identificação. Como seria realmente. Estou usando uma versão ligeiramente modificada da mesma funcionalidade de uma biblioteca , que escrevi .. porque sou preguiçoso:

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

Como você pode ver, nesse nível de abstração, não há indicação de onde os dados foram buscados. Pode ser um banco de dados, mas também pode ser apenas um objeto falso para fins de teste. Até os mapeadores de dados, que são realmente usados ​​para isso, ficam ocultos nos privatemétodos desse serviço.

private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
    $identity->setStatus($status);
    $identity->setLastUsed(time());
    $mapper = $this->mapperFactory->create(Mapper\Identity::class);
    $mapper->store($identity);
}

Maneiras de criar mapeadores

Para implementar uma abstração de persistência, nas abordagens mais flexíveis é criar mapeadores de dados personalizados .

Diagrama do mapeador

De: livro PoEAA

Na prática, eles são implementados para interação com classes ou superclasses específicas. Digamos que você tenha Customere Adminem seu código (ambos herdados de uma Usersuperclasse). Ambos provavelmente acabariam tendo um mapeador correspondente separado, pois eles contêm campos diferentes. Mas você também terminará com operações compartilhadas e comumente usadas. Por exemplo: atualizando o horário da "última vez online" . E, em vez de tornar os mapeadores existentes mais complicados, a abordagem mais pragmática é ter um "Mapeador de Usuários" geral, que apenas atualiza esse carimbo de data / hora.

Alguns comentários adicionais:

  1. Tabelas e modelo de banco de dados

    Embora, às vezes, exista um relacionamento direto 1: 1: 1 entre uma tabela de banco de dados, objeto de domínio e mapeador , em projetos maiores, pode ser menos comum do que o esperado:

    • As informações usadas por um único Objeto de Domínio podem ser mapeadas de diferentes tabelas, enquanto o próprio objeto não tem persistência no banco de dados.

      Exemplo: se você estiver gerando um relatório mensal. Isso coletaria informações de diferentes tabelas, mas não há MonthlyReporttabela mágica no banco de dados.

    • Um único mapeador pode afetar várias tabelas.

      Exemplo: ao armazenar dados do Userobjeto, este Objeto de Domínio pode conter uma coleção de outros objetos de domínio - Groupinstâncias. Se você os alterar e armazenar User, o Mapeador de Dados precisará atualizar e / ou inserir entradas em várias tabelas.

    • Os dados de um único objeto de domínio são armazenados em mais de uma tabela.

      Exemplo: em sistemas grandes (pense: uma rede social de tamanho médio), pode ser pragmático armazenar dados de autenticação do usuário e dados frequentemente acessados ​​separadamente de grandes partes do conteúdo, o que raramente é necessário. Nesse caso, você ainda pode ter uma única Userclasse, mas as informações que ela contém dependerão da obtenção de todos os detalhes.

    • Para cada objeto de domínio , pode haver mais de um mapeador

      Exemplo: você tem um site de notícias com um código compartilhado baseado no software de gerenciamento e público. Mas, embora ambas as interfaces usem a mesma Articleclasse, o gerenciamento precisa de muito mais informações nela preenchidas. Nesse caso, você teria dois mapeadores separados: "interno" e "externo". Cada um deles realiza consultas diferentes ou usa bancos de dados diferentes (como no mestre ou no escravo).

  2. Uma visualização não é um modelo

    As instâncias de exibição no MVC (se você não estiver usando a variação do padrão MVP) são responsáveis ​​pela lógica de apresentação. Isso significa que cada modo de exibição geralmente manipula pelo menos alguns modelos. Ele adquire dados da Camada de Modelo e, com base nas informações recebidas, escolhe um modelo e define valores.

    Um dos benefícios que você ganha com isso é a reutilização. Se você criar uma ListViewclasse, então, com código bem escrito, poderá ter a mesma classe entregando a apresentação da lista de usuários e comentários abaixo de um artigo. Porque ambos têm a mesma lógica de apresentação. Você acabou de mudar de modelo.

    Você pode usar modelos PHP nativos ou algum mecanismo de modelagem de terceiros. Também pode haver algumas bibliotecas de terceiros, capazes de substituir completamente as instâncias do View .

  3. E a versão antiga da resposta?

    A única grande mudança é que, o que é chamado de Modelo na versão antiga, é na verdade um Serviço . O restante da "analogia da biblioteca" mantém-se muito bem.

    A única falha que vejo é que essa seria uma biblioteca realmente estranha, porque retornaria informações do livro, mas não permitiria que você tocasse no livro em si, porque, caso contrário, a abstração começaria a "vazar". Talvez eu precise pensar em uma analogia mais adequada.

  4. Qual é o relacionamento entre as instâncias do View e do Controller ?

    A estrutura MVC é composta de duas camadas: interface do usuário e modelo. As principais estruturas na camada da interface do usuário são visualizações e controlador.

    Quando você lida com sites que usam o padrão de design MVC, a melhor maneira é ter uma relação 1: 1 entre visualizações e controladores. Cada visualização representa uma página inteira no seu site e possui um controlador dedicado para lidar com todas as solicitações recebidas para essa visualização específica.

    Por exemplo, para representar um artigo aberto, você teria \Application\Controller\Documente \Application\View\Document. Isso conteria todas as principais funcionalidades da camada de interface do usuário, quando se trata de artigos (é claro que você pode ter alguns componentes XHR que não estão diretamente relacionados aos artigos) .

tereško
fonte
4
@Rinzler, você notará que, em nenhum lugar desse link, nada foi dito sobre o Model (exceto em um comentário). É apenas "uma interface orientada a objetos para tabelas de banco de dados" . Se você tentar moldar isso em algo semelhante ao modelo, acabará violando o SRP e o LSP .
tereško 12/06/12
8
@hafichuk apenas situações em que é razoável empregar o padrão ActiveRecord é para prototipagem. Quando você começa a escrever o código destinado à produção, ele se torna um antipadrão, porque combina armazenamento e lógica de negócios. E como o Model Layer não tem conhecimento das outras partes do MVC. Isso não muda dependendo da variação no padrão original . Mesmo ao usar o MVVM. Não há "vários modelos" e eles não são mapeados para nada. Modelo é uma camada.
tereško
3
Versão curta - Os modelos são estruturas de dados .
Eddie B
9
Bem, vendo que ele inventou o MVC, o artigo pode ter algum mérito.
Eddie B
3
... ou mesmo apenas um conjunto de funções. O MVC não precisa ser implementado no estilo OOP, embora seja principalmente implementado dessa maneira. A coisa mais importante é camadas separadas e que cria o fluxo de dados e controle de certas
hek2mgl
37

Tudo o que é lógica de negócios pertence a um modelo, seja uma consulta ao banco de dados, cálculos, uma chamada REST, etc.

Você pode ter acesso a dados no próprio modelo, o padrão MVC não o impede de fazer isso. Você pode usá-lo com serviços, mapeadores e o que não, mas a definição real de um modelo é uma camada que lida com a lógica de negócios, nada mais, nada menos. Pode ser uma classe, uma função ou um módulo completo com um zilhão de objetos, se é isso que você deseja.

É sempre mais fácil ter um objeto separado que efetivamente executa as consultas ao banco de dados, em vez de executá-las diretamente no modelo: isso será especialmente útil no teste de unidade (devido à facilidade de injetar uma dependência de banco de dados simulada no seu modelo):

class Database {
   protected $_conn;

   public function __construct($connection) {
       $this->_conn = $connection;
   }

   public function ExecuteObject($sql, $data) {
       // stuff
   }
}

abstract class Model {
   protected $_db;

   public function __construct(Database $db) {
       $this->_db = $db;
   }
}

class User extends Model {
   public function CheckUsername($username) {
       // ...
       $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ...";
       return $this->_db->ExecuteObject($sql, $data);
   }
}

$db = new Database($conn);
$model = new User($db);
$model->CheckUsername('foo');

Além disso, no PHP, você raramente precisa capturar / repetir exceções porque o backtrace é preservado, especialmente em um caso como o seu exemplo. Apenas deixe a exceção ser lançada e pegue-a no controlador.

codificador de rede
fonte
Minha estrutura é muito parecida, acho que apenas a separo um pouco mais. A razão pela qual eu estava passando a conexão foi porque eu precisava ter pedaços executados nas transações. Eu queria adicionar um usuário e, em seguida, adicioná-lo a uma função, mas a função retornaria se um falhasse. A única maneira de resolver isso era passar a conexão.
Dietpixel
10
-1: também acontece completamente errado. O modelo não é uma abstração para uma tabela.
22414 Tereško
1
A Userclasse basicamente estende o modelo, mas não é um objeto. O usuário deve ser um objeto e possuir propriedades como: id, nome ... Você está implementando a Userclasse como auxiliar.
TomSawyer
1
Eu acho que você entende MVC, mas não entende o que é OOP. Nesse cenário, como eu disse, Usersignifica um objeto e deve ter propriedades de um usuário, não métodos como CheckUsername, o que você deve fazer se quiser criar um novo Userobjeto? new User($db)
TomSawyer 02/12/16
@TomSawyer OOP não significa que os objetos precisam ter propriedades. O que você está descrevendo é um padrão de design, que é irrelevante para a pergunta ou uma resposta para ela. OOP é um modelo de linguagem, não um padrão de design.
Netcoder
20

Na Web "MVC", você pode fazer o que quiser.

O conceito original (1) descreveu o modelo como a lógica de negócios. Ele deve representar o estado do aplicativo e aplicar alguma consistência dos dados. Essa abordagem é frequentemente descrita como "modelo de gordura".

A maioria das estruturas PHP segue uma abordagem mais superficial, onde o modelo é apenas uma interface de banco de dados. Mas, no mínimo, esses modelos ainda devem validar os dados e as relações recebidas.

De qualquer forma, você não estará muito longe se você separar as chamadas SQL ou de banco de dados em outra camada. Dessa forma, você só precisa se preocupar com os dados / comportamento reais, não com a API de armazenamento real. (No entanto, não é razoável exagerar. Você nunca poderá substituir um back-end de banco de dados por um armazenamento de arquivos, se isso não tiver sido projetado adiante.)

mario
fonte
8
o link é inválido (404)
Kyslik
6

Mais oftenly a maioria dos aplicativos terá dados, exibição e parte de processamento e nós apenas colocar todos aqueles nas cartas M, Ve C.

Model ( M) -> Possui os atributos que mantêm o estado de aplicação e não sabe nada sobre Ve C.

View ( V) -> Possui formato de exibição para o aplicativo e apenas conhece o modelo de como digerir e não se preocupa C.

Controller ( C) ----> Possui parte de processamento do aplicativo e atua como fiação entre M e V e depende de ambos M, Vdiferente de Me V.

No total, há uma separação de preocupações entre cada um. No futuro, qualquer alteração ou aprimoramento poderá ser adicionada com muita facilidade.

se sentir bem e programação
fonte
0

No meu caso, eu tenho uma classe de banco de dados que lida com toda a interação direta com o banco de dados, como consultas, buscas e outras coisas. Portanto, se eu tivesse que mudar meu banco de dados do MySQL para PostgreSQL , não haveria nenhum problema. Portanto, adicionar essa camada extra pode ser útil.

Cada tabela pode ter sua própria classe e métodos específicos, mas, para obter os dados, ela permite que a classe do banco de dados lide com isso:

Arquivo Database.php

class Database {
    private static $connection;
    private static $current_query;
    ...

    public static function query($sql) {
        if (!self::$connection){
            self::open_connection();
        }
        self::$current_query = $sql;
        $result = mysql_query($sql,self::$connection);

        if (!$result){
            self::close_connection();
            // throw custom error
            // The query failed for some reason. here is query :: self::$current_query
            $error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n");
            $error->handleError();
        }
        return $result;
    }
 ....

    public static function find_by_sql($sql){
        if (!is_string($sql))
            return false;

        $result_set = self::query($sql);
        $obj_arr = array();
        while ($row = self::fetch_array($result_set))
        {
            $obj_arr[] = self::instantiate($row);
        }
        return $obj_arr;
    }
}

Objeto de tabela classL

class DomainPeer extends Database {

    public static function getDomainInfoList() {
        $sql = 'SELECT ';
        $sql .='d.`id`,';
        $sql .='d.`name`,';
        $sql .='d.`shortName`,';
        $sql .='d.`created_at`,';
        $sql .='d.`updated_at`,';
        $sql .='count(q.id) as queries ';
        $sql .='FROM `domains` d ';
        $sql .='LEFT JOIN queries q on q.domainId = d.id ';
        $sql .='GROUP BY d.id';
        return self::find_by_sql($sql);
    }

    ....
}

Espero que este exemplo ajude você a criar uma boa estrutura.

Ibu
fonte
12
"Então, se eu tivesse que mudar meu banco de dados do MySQL para PostgreSQL, não haveria nenhum problema." Uhhhmmm com o código acima, você teria um enorme problema ao alterar qualquer coisa imo.
PeeHaa 04/10/2012
Vejo que minha resposta faz cada vez menos sentido após a edição e com o passar do tempo. Mas ele deve ficar aqui
Ibu
2
Databaseno exemplo não é uma classe. É apenas um invólucro para funções. Além disso, como você pode ter "classe de objeto de tabela" sem um objeto?
tereško
2
@ tereško Eu li muitos de seus posts e eles são ótimos. Mas não consigo encontrar nenhuma estrutura completa em nenhum lugar para estudar. Você conhece alguém que "faz certo"? Ou pelo menos um que goste de você e de outras pessoas aqui no SO dizerem fazer? Obrigado.
johnny
Posso estar atrasado, mas gostaria de salientar que a DOP quase resolve o problema de ter que criar uma 'camada' de banco de dados para facilitar mudanças futuras.
Matthew Goulart