¿Cómo debe estructurarse un modelo en MVC?


Solo estoy captando el framework MVC y a menudo me pregunto cuánto código debería ir en 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 la base de datos.

¿Debe el objeto modelo tener todas las propiedades asignadas a la base de datos, así como el código anterior o está bien separar ese código que realmente funciona la base de datos?

¿Terminaré teniendo cuatro capas?

Author: i alarmed alien, 2011-05-03

5 answers

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 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 MVC clásico y lo que usamos en el desarrollo web. Aquí está una respuesta un poco más antigua que escribí, que describe brevemente cómo son diferentes.

Qué NO es un modelo:

El modelo no es una clase ni un solo objeto. Es un error muy común cometer (yo también lo hice, aunque la respuesta original fue escrita cuando empecé a aprender lo contrario) , porque la mayoría de los marcos perpetúan este concepto erróneo.

Ni es una técnica de Mapeo Objeto-Relacional (Object) ni una abstracción de tablas de base de datos. Cualquiera que te diga lo contrario probablemente esté tratando de 'vender' otro OR completamente nuevo o un framework completo.

Qué es un modelo:

En la adaptación MVC adecuada, la M contiene toda la lógica de negocio de dominio y la Capa de modelo es en su mayoría hecha de tres tipos de estructuras:{[26]]}

  • Objetos de Dominio

    Un objeto de dominio es un contenedor lógico de información de dominio; generalmente representa una entidad lógica en el espacio de dominio del problema. Comúnmente conocido como lógica de negocios .

    Aquí es donde se define cómo validar los datos antes de enviar una factura, o para calcular el costo total de un pedido. Al mismo tiempo, Los objetos de dominio no son completamente conscientes del almacenamiento, ni de donde (base de datos SQL, API REST, archivo de texto, etc.) ni siquiera si se salvan o recuperar.

  • Mapeadores de Datos

    Estos objetos solo son responsables del almacenamiento. Si almacena información en una base de datos, aquí es donde reside SQL. O tal vez utilice un archivo XML para almacenar datos, y sus Mapeadores de datos están analizando desde y hacia archivos XML.

  • Servicios

    Puede pensar en ellos como "objetos de dominio de nivel superior", pero en lugar de lógica de negocios, Servicios son responsables de la interacción entre Objetos de Dominio y Mappers. Estas estructuras terminan creando una interfaz "pública" para interactuar con la lógica de negocio del dominio. Puede evitarlos, pero con la pena de filtrar alguna lógica de dominio en Controladores.

    Hay una respuesta relacionada a este tema en la pregunta implementación de ACL - podría ser útil.

La comunicación entre la la capa del modelo y otras partes de la tríada MVC deben ocurrir solo a través de 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 plano claro, si alguna vez necesita una API externa

 

Cómo interactuar con un modelo?

Requisitos previos: ver conferencias "Estado Global y Singletons" y "¡No Busques Cosas!" de las Conversaciones de Código Limpio.

Obtener acceso a instancias de servicio

Para las instancias View y Controller (lo que podría llamarse: "UI layer") para tener acceso a estos servicios, hay dos enfoques generales: {[26]]}

  1. Puede inyectar el servicios en los constructores de sus vistas y controladores directamente, preferiblemente utilizando un contenedor DI.
  2. Usando 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 usan una fábrica como un contenedor DI también le permitirán 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 de modelo en los controladores, necesita 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 esto entrada, cambiar el estado actual de la lógica de negocio. En este ejemplo, los estados que se cambian entre son "usuario anónimo"y" usuario registrado".

Controller no es responsable de validar la entrada del usuario, porque eso es parte de las reglas de negocio y controller definitivamente no está llamando consultas SQL, como lo que verías aquío aquí (por favor, no las odies, están equivocadas, no son malvadas).

Mostrando al usuario el cambio de estado.

Ok, el usuario ha iniciado sesión (o ha fallado). ¿Ahora qué? Dicho usuario aún no es consciente de ello. Por lo tanto, es necesario producir realmente una respuesta y esa es la responsabilidad de un punto de vista.

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

En este caso, la vista produjo una de las dos posibles respuestas, basadas en el estado actual de la capa del modelo. Para un caso de uso diferente, la vista elegiría diferentes plantillas para renderizar, basadas en algo como "current selected of article" .

La capa de presentación puede obtener bastante elaborado, como se describe aquí: Entendiendo las vistas MVC en PHP.

¡Pero solo estoy haciendo una API REST!

Por supuesto, hay situaciones, cuando esto es un exceso.

MVC es solo una solución concreta para Separación de Preocupaciones principio. MVC separa la interfaz de usuario de la lógica de negocio, y en la interfaz de usuario separa el manejo de la entrada del usuario y la presentación. Esto es crucial. Mientras que 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:

Separación MVC

Significa, que, cuando la lógica de su capa de presentación está cerca de none-existent, el enfoque pragmático es mantenerlos como una sola capa. También puede simplificar sustancialmente algunos aspectos de la capa de 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 se ha complicado la lógica para renderizando un cuerpo de respuesta, esta simplificación es muy útil para escenarios más triviales. Pero tenga en cuenta, este enfoque se convertirá en una pesadilla, cuando se intenta utilizar en grandes bases de código con lógica de presentación compleja.

 

Cómo construir el modelo?

Dado que no hay una sola clase "Model" (como se explicó anteriormente), realmente no "construye el modelo". En su lugar, comienza por hacer Servicios, que son capaces de realizar ciertos métodos. Y luego implementa Objetos de dominioy 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 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 de. Podría ser una base de datos, pero también podría 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 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 mapeadores de datos personalizados .

Diagrama de mapeador

De: PoEAA libro

En la práctica son implementado para la interacción con clases o superclases específicas. Digamos que tienes Customer y Admin en tu código (ambos heredando de una superclase User). Ambos probablemente terminarían teniendo un mapeador coincidente separado, ya que contienen campos diferentes. Pero también terminarás con operaciones compartidas y de uso común. Por ejemplo: actualizando la hora "last seen online". Y en lugar de hacer que los mapeadores existentes sean más enrevesados, el enfoque más pragmático es tener un enfoque general "User Mapper", que solo actualiza esa marca de tiempo.

Algunos comentarios adicionales:

  1. Tablas y modelos de base de datos

    Mientras que a veces hay una relación directa 1:1:1 entre una tabla de base de datos, Objeto de Dominio , y Mapper , en proyectos más grandes podría ser menos común de lo esperado:

    • La información utilizada por un único Objeto de Dominio puede mapearse desde diferentes tablas, mientras que el objeto 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 una tabla mágica MonthlyReport en la base de datos.

    • Un único mapeador puede afectar a varias tablas.

      Ejemplo: cuando está almacenando datos del objeto User, este Objeto Domain podría contener una colección de otros objetos de dominio: instancias Group. Si alterarlos y almacenar el User, el Data Mapper tendrá que actualizar y/o insertar entradas en varias tablas.

    • Los datos de un único 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 de usuario y los datos a los que se accede a menudo por separado de trozos más grandes de contenido, lo que rara vez se requiere. En ese caso usted podría todavía tener una única clase User, pero la información que contiene dependería de si se obtuvieron todos los detalles.

    • Por cada Objeto de dominio puede haber más de un mapeador

      Ejemplo: usted tiene un sitio de noticias con un código compartido basado tanto para el público y el software de gestión. Pero, si bien ambas interfaces utilizan la misma clase Article, 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).

  2. Una vista no es una plantilla

    Ver las instancias 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 generalmente hará malabares con al menos algunas plantillas. Adquiere datos de la Capa de modelo y luego, basado en 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 hacer que la misma clase entregue 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. Solo cambia de plantilla.

    Puede usar plantillas PHP nativas o usar algún motor de plantillas de terceros. También podría haber algunas bibliotecas de terceros, que son capaces de reemplazar completamente Ver instancias.

  3. ¿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 antigua, 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 te devolvería información del libro, pero no dejar que toque 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.

  4. ¿Cuál es la relación entre Viewy Controller instances?

    La estructura MVC se compone de dos capas: ui y model. Las estructuras principales en la capa de interfaz son vistas y controlador.

    Cuando se trata de sitios web que utilizan el patrón de diseño MVC, el la mejor manera es tener una relación 1: 1 entre vistas y 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ías \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, podría tener algunos componentes XHR que no están directamente relacionados con artículos) .

 833
Author: tereško,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-10-18 07:54:33

Todo lo que es business logic pertenece a un modelo, ya sea una consulta de base de datos, cálculos, una llamada REST, etc.

Puede tener el acceso a los datos en el modelo en sí, el patrón MVC no le impide hacerlo. Puede endulzarlo 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, nada menos. Puede ser una clase, una función, o un módulo completo con un tropecientos objetos si eso es lo que quieres.

Siempre es más fácil tener un objeto separado que realmente ejecute las consultas de la base de datos en lugar de que se ejecuten 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');

También, en PHP, rara vez necesita capturar/repensar excepciones porque la traza inversa se conserva, especialmente en un caso como su ejemplo. Deja que la excepción sea lanzado y atraparlo en el controlador en su lugar.

 33
Author: netcoder,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-27 19:38:55

En Web-"MVC" puedes hacer lo que quieras.

El concepto original(1) describe el modelo como la lógica de negocio. Debe representar el estado de la aplicación e imponer cierta consistencia de los datos. Ese enfoque se describe a menudo como"modelo fat".

La mayoría de los frameworks PHP siguen un enfoque más superficial, donde el modelo es solo una interfaz de base de datos. Pero por lo menos estos modelos aún deben validar los datos y relaciones entrantes.

De cualquier manera, no estás muy lejos si separas las cosas SQL o las llamadas a la base de datos en otra capa. De esta manera, solo necesita preocuparse por los datos/comportamientos reales, no por la API de almacenamiento real. (Sin embargo, no es razonable exagerar. Por ejemplo, nunca podrá reemplazar un backend de base de datos con un filestorage si no se diseñó previamente.)

 19
Author: mario,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:45

Más a menudo la mayoría de las aplicaciones tendrán datos, pantalla y parte de procesamiento y solo ponemos todos los que en las letras M,V y C.

Modelo(M)-->Tiene los atributos que mantiene el estado de aplicación y no sabe nada sobre V y C.

Ver(V)-->Tiene formato de visualización para la aplicación y y solo sabe sobre el modelo how-to-digest en él y no se preocupa por C.

Contralor(C)---->Tiene parte de procesamiento de la aplicación y actúa como cableado entre M y V y depende de ambos M,V a diferencia de M y V.

En conjunto hay separación de preocupación entre cada uno. En el futuro, cualquier cambio o mejora se puede agregar muy fácilmente.

 5
Author: feel good and programming,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-08-19 14:44:27

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, etc. Así que si tuviera que cambiar mi base de datos de MySQL a PostgreSQL no habrá ningún problema. Así que agregar esa capa adicional puede ser útil.

Cada tabla puede tener su propia clase y tener sus métodos específicos, pero para obtener realmente los datos, permite que la clase de la base de datos los maneje:

File 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.

 0
Author: Ibu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-14 20:24:06