modelo - mvc php pdf
¿Cómo se debe estructurar un modelo en MVC? (5)
Descargo de responsabilidad: la siguiente es una descripción de cómo entiendo los patrones tipo MVC en el contexto de las aplicaciones web basadas en PHP. Todos los enlaces externos que se utilizan en el contenido están allí para explicar términos y conceptos, y no para implicar mi propia credibilidad en el tema.
Lo primero que debo aclarar es: el modelo es una capa .
Segundo: existe una diferencia entre el MVC clásico y lo que usamos en el desarrollo web. Here''s un poco de una vieja respuesta que escribí, que describe brevemente cómo son diferentes.
Lo que un modelo NO es:
El modelo no es una clase o un objeto único. Es un error muy común (yo también lo hice, aunque la respuesta original se escribió cuando empecé a aprender de otra manera) , porque la mayoría de los marcos perpetúan este concepto erróneo.
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 es muy probable que 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 negocios del dominio y la Capa de modelo se compone principalmente de tres tipos de estructuras:
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 referido como lógica de negocios .
Aquí es donde debe definir 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 incluso si se guardan o recuperan.
Estos objetos son los únicos responsables del almacenamiento. Si almacena información en una base de datos, aquí es donde vive el SQL. O tal vez utilice un archivo XML para almacenar datos, y sus asignadores de datos están analizando desde y hacia archivos XML.
Puede considerarlos como "objetos de dominio de nivel superior", pero en lugar de lógica empresarial, los servicios son responsables de la interacción entre los objetos de dominio y los asignadores . Estas estructuras terminan creando una interfaz "pública" para interactuar con la lógica de negocios del dominio. Puede evitarlos, pero a riesgo de filtrar algo de lógica de dominio en los Controladores .
Hay una respuesta relacionada con este tema en la pregunta de implementación de ACL : podría ser útil.
La comunicación entre la capa del modelo y otras partes de la tríada MVC debe realizarse solo 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 ''espacio de maniobra'' adicional en caso de que la lógica cambie
- mantiene el controlador lo más simple posible
- da un plano claro, si alguna vez necesita una API externa
¿Cómo interactuar con un modelo?
Prerrequisitos: ver conferencias "Global State and Singletons" y "¡No busques cosas!" De las conversaciones de código limpio.
Obteniendo acceso a instancias de servicio
Tanto para las instancias de Vista como de Controlador (lo que podríamos llamar: "capa UI") 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 para 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 independiente DependencyInjection de Syfmony o Auryn .
Tanto las soluciones que utilizan una fábrica como un contenedor DI le permitirían compartir también las instancias de varios servidores que se compartirán entre el controlador seleccionado y la vista para un ciclo dado de solicitud-respuesta.
Alteración del estado del modelo.
Ahora que puede acceder a la capa de modelo en los controladores, debe comenzar a usarlos realmente:
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: tome la entrada del usuario y, en función de esta entrada, cambie el estado actual de la lógica empresarial. En este ejemplo, los estados que se cambian son "usuario anónimo" y "usuario registrado".
El controlador no es responsable de validar la entrada del usuario, ya que forma parte de las reglas comerciales y el controlador definitivamente no llama las consultas de SQL, como lo que vería here o here (por favor, no las odie, están mal orientadas, no son malas).
Mostrando al usuario el cambio de estado.
Ok, el usuario ha iniciado sesión (o fallado). ¿Ahora que? Dicho usuario aún 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 respuestas posibles, en función del estado actual de la capa del modelo. Para un caso de uso diferente, tendría la vista que seleccionaría diferentes plantillas para representar, en función de algo así como "seleccionado actualmente del artículo".
La capa de presentación puede llegar a ser bastante elaborada, como se describe aquí: Descripción de 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, separó 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 esto:
Esto significa que, cuando la lógica de la capa de presentación es casi inexistente, el enfoque pragmático es mantenerlos como capa única. También puede simplificar sustancialmente algunos aspectos de la capa del modelo.
Usando 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 cuidado , este enfoque se convertirá en una pesadilla cuando intente utilizarlo en grandes bases de código 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 se "construye el modelo". En lugar de eso, comienzas a hacer servicios , que son capaces de 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 hubo este método de inicio de sesión para el servicio de identificación. ¿Cómo sería realmente? Estoy usando una versión ligeramente modificada de la misma funcionalidad de una biblioteca , que escribí ... porque soy perezoso:
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 ninguna 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 propósitos de prueba. Incluso los mapeadores de datos, que en realidad se utilizan para ello, están ocultos en los métodos private
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 asignadores de datos personalizados.
De: libro de PoEAA
En la práctica se implementan para la interacción con clases específicas o superclases. Digamos que tiene un Customer
y un Admin
en su código (ambos heredados de una superclase de User
). Es probable que ambos terminen teniendo un asignador coincidente separado, ya que contienen campos diferentes. Pero también terminarás con operaciones compartidas y de uso común. Por ejemplo: actualizando el tiempo "ú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 Usuario" general, que solo actualice esa marca de tiempo.
Algunos comentarios adicionales:
Tablas de base de datos y modelo
Aunque a veces hay una relación directa 1: 1: 1 entre una tabla de base de datos, un objeto de dominio y un asignador , 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 recopilaría información de diferentes tablas, pero no hay
MonthlyReport
tabla deMonthlyReport
mágica en la base de datos.Un solo Mapper puede afectar varias tablas.
Ejemplo: cuando está almacenando datos del objeto
User
, este Objeto de dominio podría contener una colección de otros objetos de dominio: instancias deGroup
. Si los modifica y almacena elUser
, el asignador de datos 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 en una red social de tamaño mediano), podría ser pragmático almacenar datos de autenticación de usuarios y datos a los que se accede a menudo por separado de grandes porciones de contenido, lo que rara vez se requiere. En ese caso, es posible que aún tenga una única clase de
User
, pero la información que contiene dependerá de si se obtuvieron todos los detalles.Para cada objeto de dominio puede haber más de un mapeador
Ejemplo: tiene un sitio de noticias con un código compartido basado tanto en el software de gestión pública como para el público. Pero, mientras que ambas interfaces utilizan la misma clase de
Article
, la administración necesita mucha más información en ella. En este caso, tendría dos asignadores 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
Las instancias de vista en MVC (si no está utilizando la variación MVP del patrón) son responsables de la lógica de presentación. Esto significa que cada Vista usualmente combinará al menos unas pocas plantillas. Adquiere datos de la capa de modelo y luego, en función de la información recibida, elige una plantilla y establece valores.
Uno de los beneficios que obtienes de esto es la reutilización. Si crea una clase
ListView
, entonces, con un código bien escrito, puede tener la misma clase que entrega la lista de usuarios y los comentarios debajo de un artículo. Porque ambos tienen la misma lógica de presentación. Usted acaba de cambiar las plantillas.Puede usar cualquiera de las plantillas PHP nativas o usar algún 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 antigua 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.
El único defecto que veo es que esta sería una biblioteca realmente extraña, porque le devolvería información del libro, pero no le dejaría tocar el libro en sí, porque de lo contrario la abstracción comenzaría a "filtrarse". Puede que tenga que pensar en una analogía más apropiada.
¿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 UI son vistas y controlador.
Cuando se trata de sitios web que utilizan un 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, tendría
/Application/Controller/Document
y/Application/View/Document
. Esto contendría toda la funcionalidad principal para la capa UI, cuando se trata de artículos (por supuesto, puede tener algunos componentes XHR que no están directamente relacionados con los artículos) .
Acabo de familiarizarme con el marco MVC y a menudo me pregunto cuánto código debería incluir el modelo. Tiendo a tener una clase de acceso a datos que tiene 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;
}
}
Mis modelos tienden a ser una clase de entidad que se asigna a la tabla de base de datos.
¿Debería el objeto modelo tener todas las propiedades de la base de datos asignadas, así como el código anterior, o está bien separar ese código que realmente funciona con la base de datos?
¿Terminaré teniendo cuatro capas?
En Web- "MVC" puedes hacer lo que quieras.
El concepto original (1) describía el modelo como la lógica empresarial. Debe representar el estado de la aplicación y aplicar cierta consistencia de datos. Ese enfoque a menudo se describe como "modelo gordo".
La mayoría de los marcos PHP siguen un enfoque más superficial, donde el modelo es solo una interfaz de base de datos. Pero al menos estos modelos todavía deben validar los datos y las relaciones entrantes.
De cualquier manera, no estás muy lejos si separas las cosas de SQL o las llamadas a la base de datos en otra capa. De esta manera, solo tiene que preocuparse por los datos / comportamiento reales, no por la API de almacenamiento real. (Sin embargo, no es razonable exagerar. Por ejemplo, nunca podrá reemplazar un back-end de la base de datos con un almacenamiento de archivos si no se diseñó con anticipación).
En mi caso, tengo una clase de base de datos que maneja toda la interacción directa de la base de datos, como consultas, búsquedas y demás. Entonces, si tuviera que cambiar mi base de datos de MySQL a PostgreSQL , no habrá ningún problema. Así que agregar esa capa extra puede ser útil.
Cada tabla puede tener su propia clase y sus métodos específicos, pero para obtener los datos, permite que la clase de base de datos lo maneje:
Archivo 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 tabla 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 ejemplo te ayude a crear una buena estructura.
Más a menudo, la mayoría de las aplicaciones tendrán datos, visualización y procesamiento, y solo colocamos todas las letras M
, V
y C
Modelo ( M
) -> Tiene los atributos que mantienen el estado de la aplicación y no sabe nada acerca de V
y C
Vista ( V
) -> Tiene formato de visualización para la aplicación y solo conoce el modelo de cómo hacerlo y no se preocupa por C
Controlador ( C
) ----> Procesa parte de la aplicación y actúa como cableado entre M y V y depende de M
, V
diferencia de M
y V
En total hay separación de preocupación entre cada uno. En el futuro, cualquier cambio o mejoras se pueden agregar muy fácilmente.
Todo lo que es lógica de negocios pertenece a un modelo, ya sea una consulta de base de datos, cálculos, una llamada REST, etc.
Puede tener acceso a los datos en el propio modelo, el patrón MVC no le impide hacerlo. Usted puede cubrirlo con servicios, mapeadores y lo que no, pero la definición real de un modelo es una capa que maneja la lógica de negocios, nada más y nada menos. Puede ser una clase, una función o un módulo completo con un millón de objetos si así lo desea.
Siempre es más fácil tener un objeto separado que realmente ejecute las consultas de la base de datos en lugar de ejecutarlas directamente en el modelo: esto será especialmente útil cuando se realicen pruebas unitarias (debido a la facilidad de inyectar una dependencia de base de datos simulada en su 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'');
Además, en PHP, rara vez es necesario capturar / volver a generar excepciones porque el retroceso se conserva, especialmente en un caso como su ejemplo. Simplemente deje que se lance la excepción y cójala en el controlador.