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:
- 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.
- 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:
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 private
mé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 .
De: livro PoEAA
Na prática, eles são implementados para interação com classes ou superclasses específicas. Digamos que você tenha Customer
e Admin
em seu código (ambos herdados de uma User
superclasse). 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:
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á MonthlyReport
tabela mágica no banco de dados.
Um único mapeador pode afetar várias tabelas.
Exemplo: ao armazenar dados do User
objeto, este Objeto de Domínio pode conter uma coleção de outros objetos de domínio - Group
instâ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 User
classe, 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 Article
classe, 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).
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 ListView
classe, 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 .
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.
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\Document
e \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) .