Descargo de responsabilidad: la siguiente es una descripción de cómo entiendo los patrones similares a MVC en el contexto de las aplicaciones web basadas en PHP. Todos los enlaces externos que se utilizan en el contenido están ahí para explicar los términos y conceptos, y no para implicar mi propia credibilidad sobre el tema.
Lo primero que debo aclarar es: el modelo es una capa .
Segundo: hay una diferencia entre el MVC clásico y lo que usamos en el desarrollo web. Aquí hay una respuesta un poco más antigua que escribí, que describe brevemente cómo son diferentes.
Qué modelo NO es:
El modelo no es una clase ni ningún objeto individual. Es un error muy común de cometer (también lo hice, aunque la respuesta original fue escrita cuando comencé a aprender lo contrario) , porque la mayoría de los marcos perpetúan esta idea errónea.
Tampoco es una técnica de mapeo relacional de objetos (ORM) ni una abstracción de tablas de bases de datos. Cualquiera que le diga lo contrario probablemente intente 'vender' otro ORM nuevo o un marco completo.
Qué modelo es:
En la adaptación adecuada de MVC, la M contiene toda la lógica de negocio del dominio y la Capa de modelo se compone principalmente de tres tipos de estructuras:
Objetos de dominio
Un objeto de dominio es un contenedor lógico de información puramente de dominio; Por lo general, representa una entidad lógica en el espacio del dominio del problema. Comúnmente conocido como lógica de negocios .
Aquí es donde definiría cómo validar los datos antes de enviar una factura, o calcular el costo total de un pedido. Al mismo tiempo, los objetos de dominio desconocen por completo el almacenamiento, ni desde dónde (base de datos SQL, API REST, archivo de texto, etc.) ni siquiera si se guardan o recuperan.
Mapeadores de datos
Estos objetos solo son responsables del almacenamiento. Si almacena información en una base de datos, este sería el lugar donde vive el SQL. O tal vez use un archivo XML para almacenar datos, y sus Data Mappers están analizando desde y hacia archivos XML.
Servicios
Puede pensar en ellos como "Objetos de dominio de nivel superior", pero en lugar de la lógica empresarial, los Servicios son responsables de la interacción entre Objetos de dominio y Mapeadores . Estas estructuras terminan creando una interfaz "pública" para interactuar con la lógica comercial del dominio. Puede evitarlos, pero con la penalidad de filtrar cierta lógica de dominio en los Controladores .
Hay una respuesta relacionada con este tema en la pregunta de implementación de ACL : puede ser útil.
La comunicación entre la capa del modelo y otras partes de la tríada MVC solo debe realizarse a través de los Servicios . La separación clara tiene algunos beneficios adicionales:
- ayuda a hacer cumplir el principio de responsabilidad única (SRP)
- proporciona 'margen de maniobra' adicional en caso de que la lógica cambie
- mantiene el controlador lo más simple posible
- proporciona un plan claro, si alguna vez necesita una API externa
¿Cómo interactuar con una modelo?
Prerrequisitos: mire las conferencias "Estado global y Singletons" y "¡No busque cosas!" de las conversaciones de código limpio.
Obtener acceso a instancias de servicio
Para las instancias de Vista y Controlador (lo que se podría llamar: "capa de interfaz de usuario") para tener acceso a estos servicios, hay dos enfoques generales:
- Puede inyectar los servicios requeridos en los constructores de sus vistas y controladores directamente, preferiblemente utilizando un contenedor DI.
- Usar una fábrica de servicios como una dependencia obligatoria para todas sus vistas y controladores.
Como puede sospechar, el contenedor DI es una solución mucho más elegante (aunque no es la más fácil para un principiante). Las dos bibliotecas, que recomiendo considerar para esta funcionalidad, serían el componente de inyección de dependencia independiente de Syfmony o Auryn .
Tanto las soluciones que usan una fábrica como un contenedor DI también le permitirían compartir las instancias de varios servidores que se compartirán entre el controlador seleccionado y la vista para un ciclo de solicitud-respuesta dado.
Alteración del estado del modelo.
Ahora que puede acceder a la capa del modelo en los controladores, debe comenzar a usarlos:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
Sus controladores tienen una tarea muy clara: tomar la entrada del usuario y, en base a esta entrada, cambiar el estado actual de la lógica empresarial. En este ejemplo, los estados que se cambian entre "usuario anónimo" y "usuario conectado".
El controlador no es responsable de validar la entrada del usuario, porque eso es parte de las reglas de negocio y el controlador definitivamente no está llamando a consultas SQL, como lo que vería aquí o aquí (por favor, no las odie, están equivocadas, no son malas).
Mostrando al usuario el cambio de estado.
Ok, el usuario ha iniciado sesión (o ha fallado). ¿Ahora que? Dicho usuario todavía no lo sabe. Por lo tanto, debe producir una respuesta y esa es la responsabilidad de una vista.
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
En este caso, la vista produjo una de dos posibles respuestas, en función del estado actual de la capa del modelo. Para un caso de uso diferente, tendría la vista seleccionando diferentes plantillas para representar, en base a algo como "artículo seleccionado actualmente".
La capa de presentación en realidad puede ser bastante elaborada, como se describe aquí: Comprender las vistas MVC en PHP .
¡Pero solo estoy haciendo una API REST!
Por supuesto, hay situaciones, cuando esto es una exageración.
MVC es solo una solución concreta para el principio de separación de preocupaciones . MVC separa la interfaz de usuario de la lógica de negocios y, en la interfaz de usuario, separa el manejo de la entrada del usuario y la presentación. Esto es crucial Aunque a menudo la gente lo describe como una "tríada", en realidad no se compone de tres partes independientes. La estructura es más como esta:
Significa que, cuando la lógica de su capa de presentación es casi inexistente, el enfoque pragmático es mantenerlos como una sola capa. También puede simplificar sustancialmente algunos aspectos de la capa del modelo.
Con este enfoque, el ejemplo de inicio de sesión (para una API) se puede escribir 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);
}
Si bien esto no es sostenible, cuando tiene una lógica complicada para representar un cuerpo de respuesta, esta simplificación es muy útil para escenarios más triviales. Pero tenga en cuenta que este enfoque se convertirá en una pesadilla cuando intente utilizarlo en bases de código grandes con una lógica de presentación compleja.
¿Cómo construir el modelo?
Como no hay una sola clase de "Modelo" (como se explicó anteriormente), realmente no "construye el modelo". En su lugar, comienza por hacer Servicios , que pueden realizar ciertos métodos. Y luego implementar objetos de dominio y mapeadores .
Un ejemplo de un método de servicio:
En los dos enfoques anteriores había este método de inicio de sesión para el servicio de identificación. ¿Cómo se vería realmente? Estoy usando una versión ligeramente modificada de la misma funcionalidad de una biblioteca que escribí ... porque soy vago:
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 puede ver, en este nivel de abstracción, no hay indicación de dónde se obtuvieron los datos. Puede ser una base de datos, pero también puede ser solo un objeto simulado para fines de prueba. Incluso los mapeadores de datos, que realmente se utilizan para ello, están ocultos en los private
métodos de este servicio.
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
Formas de crear mapeadores
Para implementar una abstracción de persistencia, en los enfoques más flexibles es crear mapeadores de datos personalizados .
De: libro de PoEAA
En la práctica, se implementan para la interacción con clases o superclases específicas. Digamos que tiene Customer
y Admin
en su código (ambos heredan de una User
superclase). Ambos probablemente terminarían teniendo un mapeador coincidente separado, ya que contienen diferentes campos. Pero también terminará con operaciones compartidas y de uso común. Por ejemplo: actualizar el "último visto en línea" . Y en lugar de hacer que los mapeadores existentes sean más complicados, el enfoque más pragmático es tener un "Mapeador de usuarios" general, que solo actualiza esa marca de tiempo.
Algunos comentarios adicionales:
Tablas de base de datos y modelo
Si bien a veces hay una relación directa 1: 1: 1 entre una tabla de base de datos, un Objeto de dominio y un Mapeador , en proyectos más grandes puede ser menos común de lo que espera:
La información utilizada por un solo Objeto de dominio puede asignarse desde diferentes tablas, mientras que el objeto en sí no tiene persistencia en la base de datos.
Ejemplo: si está generando un informe mensual. Esto recolectaría información de diferentes tablas, pero no hay una MonthlyReport
tabla mágica en la base de datos.
Un solo Mapper puede afectar múltiples tablas.
Ejemplo: cuando está almacenando datos del User
objeto, este Objeto de dominio podría contener una colección de otros objetos de dominio: Group
instancias. Si los modifica y almacena User
, el Data Mapper tendrá que actualizar y / o insertar entradas en varias tablas.
Los datos de un solo objeto de dominio se almacenan en más de una tabla.
Ejemplo: en sistemas grandes (piense: una red social de tamaño mediano), podría ser pragmático almacenar los datos de autenticación del usuario y los datos a los que se accede con frecuencia por separado de grandes cantidades de contenido, lo que rara vez se requiere. En ese caso, es posible que aún tenga una sola User
clase, pero la información que contiene dependerá de si se obtuvieron todos los detalles.
Por cada objeto de dominio puede haber más de un mapeador
Ejemplo: tiene un sitio de noticias con un código compartido basado tanto para el público como para el software de administración. Pero, si bien ambas interfaces usan la misma Article
clase, la administración necesita mucha más información. En este caso, tendría dos mapeadores separados: "interno" y "externo". Cada uno realiza diferentes consultas, o incluso utiliza diferentes bases de datos (como en maestro o esclavo).
Una vista no es una plantilla
Ver instancias en MVC (si no está utilizando la variación MVP del patrón) es responsable de la lógica de presentación. Esto significa que cada vista generalmente hará malabares con al menos algunas plantillas. Adquiere datos de la capa modelo y luego, en función de la información recibida, elige una plantilla y establece valores.
Uno de los beneficios que obtiene de esto es la reutilización. Si crea una ListView
clase, entonces, con un código bien escrito, puede tener la misma clase entregando la presentación de la lista de usuarios y los comentarios debajo de un artículo. Porque ambos tienen la misma lógica de presentación. Simplemente cambia de plantilla.
Puede usar plantillas PHP nativas o usar un motor de plantillas de terceros. También puede haber algunas bibliotecas de terceros, que pueden reemplazar completamente las instancias de View .
¿Qué pasa con la versión anterior de la respuesta?
El único cambio importante es que, lo que se llama Modelo en la versión anterior, es en realidad un Servicio . El resto de la "analogía de la biblioteca" se mantiene bastante bien.
La única falla que veo es que esta sería una biblioteca realmente extraña, porque le devolvería información del libro, pero no le permitiría tocar el libro en sí, porque de lo contrario la abstracción comenzaría a "filtrarse". Podría tener que pensar en una analogía más adecuada.
¿Cuál es la relación entre las instancias de View y Controller ?
La estructura MVC se compone de dos capas: ui y modelo. Las estructuras principales en la capa de IU son vistas y controlador.
Cuando se trata de sitios web que utilizan el patrón de diseño MVC, la mejor manera es tener una relación 1: 1 entre las vistas y los controladores. Cada vista representa una página completa en su sitio web y tiene un controlador dedicado para manejar todas las solicitudes entrantes para esa vista en particular.
Por ejemplo, para representar un artículo abierto, debería tener \Application\Controller\Document
y \Application\View\Document
. Esto contendría toda la funcionalidad principal para la capa de interfaz de usuario, cuando se trata de tratar con artículos (por supuesto, puede tener algunos componentes XHR que no están directamente relacionados con los artículos) .