Pregunta ¿Cómo debería estructurarse un modelo en MVC?


Solo estoy entendiendo el framework MVC y muchas veces 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 está asignada a la tabla de la base de datos.

¿Debería el objeto modelo tener todas las propiedades mapeadas de la base de datos así como también el código anterior o está bien separar ese código que realmente funciona?

¿Terminaré teniendo cuatro capas?


510
2018-05-03 00:28


origen


Respuestas:


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

Lo que un modelo NO es:

El modelo no es una clase o un solo objeto. Es un error muy común de hacer (Yo 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 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 probablemente intentará 'vender' otro ORM nuevo o un marco completo.

Qué es un modelo:

En una adecuada adaptación de MVC, la M contiene toda la lógica comercial de dominio y la Modelo de capa es principalmente hecho de tres tipos de estructuras:

  • Objetos de dominio

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

    Aquí debería definir cómo validar los datos antes de enviar una factura o calcular el costo total de una orden. Al mismo tiempo, 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

  • Data Mappers

    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 su Data Mappers están analizando desde y hacia archivos XML.

  • Servicios

    Puedes pensar en ellos como "objetos de dominio de nivel superior", pero en lugar de lógica comercial, 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 negocios del dominio. Puedes evitarlos, pero a la pena de filtrar alguna lógica de dominio en Controladores.

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

La comunicación entre la capa modelo y otras partes de la tríada MVC debería ocurrir solo a través de Servicios. La separación clara tiene algunos beneficios adicionales:

  • ayuda a hacer cumplir la 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 plan claro, si alguna vez necesitas una API externa

¿Cómo interactuar con un modelo?

Prerrequisitos: ver conferencias "Estado global y Singletons" y "¡No busques cosas!" de las Charlas de Código Limpio.

Obtener acceso a instancias de servicio

Para ambos Ver y Controlador instancias (lo que se podría llamar: "capa de interfaz de usuario") para tener acceso a estos servicios, hay dos enfoques generales:

  1. Puede inyectar los servicios requeridos en los constructores de sus vistas y controladores directamente, preferiblemente utilizando un contenedor DI.
  2. Usar una fábrica para servicios como una dependencia obligatoria para todas sus vistas y controladores.

Como podría 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 independientes de Syfmony Componente DependencyInjection o Auryn.

Ambas soluciones, que utilizan una fábrica y 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 determinado.

Alteración del estado del modelo

Ahora que puede acceder a la capa del modelo en los controladores, necesita 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: tomar la entrada del usuario y, en función de esta entrada, cambiar el estado actual de la lógica comercial. En este ejemplo, los estados que se cambian entre "usuario anónimo" y "usuario registrado".

El controlador no es responsable de validar la entrada del usuario, porque eso es parte de las reglas del negocio y el controlador definitivamente no llama a las consultas SQL, como lo que vería aquí o aquí (por favor no los odies, están equivocados, no malvados).

Mostrando al usuario el cambio de estado.

Ok, el usuario ha iniciado sesión (o ha fallado). ¿Ahora que? Dicho usuario aún no lo sabe. Entonces, realmente necesita 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, basadas en el estado actual de la capa del modelo. Para un caso de uso diferente, tendría la vista de elegir diferentes plantillas para procesar, basadas en algo así como "actual seleccionado de artículo".

La capa de presentación en realidad puede ser bastante elaborada, como se describe aquí: Comprender las vistas de 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 Separación de intereses principio. 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 las personas lo describen como una "tríada", en realidad no se compone de tres partes independientes. La estructura es más como esto:

MVC separation

Significa que, cuando la lógica de la capa de presentación es casi nula, el enfoque pragmático es mantenerlos como una sola capa. 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 se ha complicado la lógica para generar un cuerpo de respuesta, esta simplificación es muy útil para escenarios más triviales. Pero ser advertido, este enfoque se convertirá en una pesadilla cuando se intente utilizar en grandes bases de código con 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 cambio, comienzas haciendo Servicios, que son capaces de realizar ciertos métodos. Y luego implementar Objetos de dominio y Mappers.

Un ejemplo de un método de servicio:

En ambos 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 flojo:

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 extrajeron los datos. Puede 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 en realidad se utilizan para ello, están ocultos en el 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 la persistencia, en los enfoques más flexibles es crear personalizado mapeadores de datos.

Mapper diagram

De: PoEAA libro

En la práctica, se implementan para interactuar con clases específicas o superclases. Digamos que tienes Customer y Admin en su código (ambos heredan de un User superclase). Ambos probablemente terminen teniendo un asignador de correspondencia por separado, ya que contienen diferentes campos. Pero también terminará con operaciones compartidas y de uso común. Por ejemplo: actualizar el "visto por última vez en línea" hora. Y en lugar de hacer que los mapeadores existentes sean más complejos, el enfoque más pragmático es tener un "User Mapper" general, que solo actualice esa marca de tiempo.

Algunos comentarios adicionales:

  1. Tablas de base de datos y modelo

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

    • Información utilizada por un solo Objeto de dominio se puede mapear 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 información mágica MonthlyReport tabla en la base de datos.

    • Una sola Mapper puede afectar a varias tablas

      Ejemplo: cuando está almacenando datos de User objeto, esto Objeto de dominio podría contener una colección de otros objetos de dominio Group instancias. Si los modifica y almacena el User, el Data Mapper tendrá que actualizar y / o insertar entradas en múltiples tablas.

    • Datos de un solo Objeto de dominio se almacena en más de una tabla.

      Ejemplo: en sistemas grandes (piénsese: una red social de tamaño medio), podría ser práctico almacenar los datos de autenticación de usuario y los datos a los que se accede a menudo de forma separada de grandes fragmentos de contenido, lo que rara vez se requiere. En ese caso, es posible que todavía tenga un solo User clase, 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 en el publicitario. Pero, si bien ambas interfaces usan el mismo Article clase, la administración necesita mucha más información poblada en ella. En este caso, tendrías dos mapeadores separados: "interno" y "externo". Cada uno realiza diferentes consultas, o incluso utiliza diferentes bases de datos (como en el maestro o esclavo).

  2. Una vista no es una plantilla

    Ver las instancias en MVC (si no está usando la variación MVP del patrón) son responsables de la lógica de presentación. Esto significa que cada Ver por lo general hará malabares con al menos algunas plantillas. Adquiere datos de la Modelo de capa 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 creas un 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. Solo cambias las plantillas.

    Puedes usar cualquiera plantillas PHP nativas o use un motor de plantillas de terceros. También puede haber algunas bibliotecas de terceros, que son capaces de reemplazar por completo Ver instancias.

  3. ¿Qué hay de la versión anterior de la respuesta?

    El único cambio importante es eso, lo que se llama Modelo en la versión anterior, es en realidad una 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 te dejaría tocar el libro en sí, porque de lo contrario la abstracción comenzaría a "filtrarse". Debería pensar en una analogía más apropiada.

  4. ¿Cuál es la relación entre Ver y Controlador ¿instancias?

    La estructura MVC se compone de dos capas: ui y modelo. Las estructuras principales en el Capa UI 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 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 todas las funcionalidades principales para la capa UI, cuando se trata de tratar con artículos (por supuesto, puede tener algunos XHR componentes que no están directamente relacionados con los artículos).


820
2018-05-03 00:56



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 modelo en sí, el patrón MVC no le impide hacerlo. Puedes endulzarlo con servicios, mapeadores y otras cosas, 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 montón de objetos si eso es lo que quieres.

Siempre es más fácil tener un objeto separado que ejecute las consultas de la base de datos en lugar de ejecutarlas en el modelo directamente: esto será especialmente útil cuando se realicen pruebas unitarias (debido a la facilidad de inyectar una dependencia falsa de la base de datos 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 necesita capturar / reinventar excepciones porque se conserva la traza inversa, especialmente en un caso como el de su ejemplo. Solo deja que se emita la excepción y captúrala en el controlador.


32
2018-05-03 00:42



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

El concepto original (1) describió el modelo como la lógica de negocios. Debe representar el estado de la aplicación y hacer cumplir la coherencia de algunos datos. Ese enfoque a menudo se describe como "modelo gordo".

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

De cualquier manera, no está muy lejos si separa las cosas SQL o las llamadas a la base de datos en otra capa. De esta forma, solo debe preocuparse por los datos / comportamientos reales, no con la API de almacenamiento real. (Sin embargo, no es razonable exagerar. Por ejemplo, nunca podrá reemplazar un back-end de base de datos con un almacén de archivos si eso no se diseñó antes).


18
2018-05-03 00:41



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

Modelo(M)-> Tiene los atributos que mantienen 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 solo sabe sobre el modelo de cómo digerirlo y no se preocupa por C.

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

En total, existe separación de intereses entre cada uno. En el futuro, cualquier cambio o mejora se puede agregar muy fácilmente.


4
2018-06-30 13:09



En mi caso, tengo una clase de base de datos que maneja toda la interacción directa con la base de datos, como consultas, búsqueda, etc. Entonces, si tuviera que cambiar mi base de datos de MySQL a PostgreSQL no habrá ningún problema Entonces, 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 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;
    }
}

Clase de objeto de tabla

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
2018-05-03 00:47