¿Cómo debe estructurarse un modelo en MVC? [cerrado]

551

Me estoy familiarizando 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 la base de datos.

¿Debería el objeto modelo tener todas las propiedades asignadas de 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?

Dietpixel
fuente
133
¿Por qué estás capturando excepciones solo para lanzarlas de nuevo?
Bailey Parker
99
@Elias Van Ootegem: te perdiste el punto. no tiene sentido atraparlos en este caso.
Karoly Horvath
44
@Elias Van Ootegem: ¿eh? Si funciona con rethrow, significa que una capa superior atrapa la excepción. Pero si hay uno, entonces lo habría atrapado sin esa repetición sin sentido ... (si aún no lo obtiene, simule un pequeño código de prueba)
Karoly Horvath
3
@ Elias Van Ootegem: No tengo idea de lo que estás hablando, no manejar una excepción en una capa específica no significa que detendrá la aplicación. construya (o más precisamente: no construya) un ejemplo de código donde sea necesario volver a lanzar. Detengamos esta conversación fuera del tema, por favor
Karoly Horvath
66
@drrcknlsn: ese es un argumento válido, pero en ese caso al menos captura la excepción que espera que se lance, el genérico Exceptionno tiene mucho valor de documentación. Personalmente, si seguía ese camino, elegiría PHPDoc @exceptiono algún mecanismo similar, por lo que se muestra en la documentación generada.
Karoly Horvath

Respuestas:

903

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:

  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 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:

Separación MVC

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 privatemé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 .

Diagrama del mapeador

De: libro de PoEAA

En la práctica, se implementan para la interacción con clases o superclases específicas. Digamos que tiene Customery Adminen su código (ambos heredan de una Usersuperclase). 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:

  1. 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 MonthlyReporttabla mágica en la base de datos.

    • Un solo Mapper puede afectar múltiples tablas.

      Ejemplo: cuando está almacenando datos del Userobjeto, este Objeto de dominio podría contener una colección de otros objetos de dominio: Groupinstancias. 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 Userclase, 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 Articleclase, 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 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 ListViewclase, 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 .

  3. ¿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.

  4. ¿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\Documenty \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) .

tereško
fuente
44
@Rinzler, notará, que en ninguna parte de ese enlace, nada se dice sobre el Modelo (excepto en un comentario). Es solo "una interfaz orientada a objetos para las tablas de la base de datos" . Si intentas moldear esto en un modelo, terminas violando SRP y LSP .
tereško
8
@hafichuk solo situaciones, cuando es razonable emplear el patrón ActiveRecord es para la creación de prototipos. Cuando comienzas a escribir el código que significa la producción, se convierte en un antipatrón, ya que combina almacenamiento y lógica empresarial. Y dado que Model Layer desconoce por completo las otras partes de MVC. Esto no cambia dependiendo de la variación en el patrón original . Incluso cuando se usa MVVM. No hay "modelos múltiples" y no están asignados a nada. El modelo es una capa.
tereško
3
Versión corta: los modelos son estructuras de datos .
Eddie B el
99
Bueno, viendo que inventó MVC, el artículo puede tener algún mérito.
Eddie B
3
... o incluso solo un conjunto de funciones. MVC no requiere implementarse en un estilo OOP, aunque se implementa principalmente de esa manera. Lo más importante es separar las capas y establecer el flujo correcto de datos y control
hek2mgl
37

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 mismo, el patrón MVC no le impide hacerlo. Puede endulzarlo con servicios, mapeadores y demás, pero la definición real de un modelo es una capa que maneja la lógica empresarial, nada más y nada menos. Puede ser una clase, una función o un módulo completo con millones de 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 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 necesita capturar / volver a generar excepciones porque se conserva la traza inversa, especialmente en un caso como su ejemplo. Simplemente deje que se lance la excepción y en su lugar póngala en el controlador.

netcoder
fuente
Mi estructura es muy similar, creo que la separo un poco más. La razón por la que estaba pasando la conexión era porque necesitaba ejecutar fragmentos en las transacciones. Quería agregar un usuario y luego agregar el usuario a un rol, pero el rol volvería si uno fallaba. La única forma en que podía resolver eso era pasar la conexión.
Dietpixel
10
-1: también sucede que está completamente equivocado. El modelo no es una abstracción para una tabla.
tereško
1
La Userclase básicamente extiende el modelo, pero no es un objeto. El usuario debe ser un objeto y tiene propiedades como: id, name ... Estás implementando Userclass es un ayudante.
TomSawyer
1
Creo que entiendes MVC pero no entiendes qué es POO. En este escenario, como dije, Userrepresenta un objeto, y debe tener propiedades de un Usuario, no métodos como CheckUsername, ¿qué debe hacer si desea crear un nuevo Userobjeto? new User($db)
TomSawyer
@TomSawyer OOP no significa que los objetos deben tener propiedades. Lo que está describiendo es un patrón de diseño, uno que es irrelevante para la pregunta o una respuesta a esa pregunta. OOP es un modelo de lenguaje, no un patrón de diseño.
netcoder
20

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

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

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 al menos estos modelos aún deberían validar los datos y las relaciones entrantes.

De cualquier manera, no está muy lejos si separa las cosas de SQL o las llamadas a la base de datos en otra capa. De esta manera, solo necesita 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 backend de base de datos con un almacenamiento de archivos si no se diseñó de antemano).

mario
fuente
8
enlace no válido (404)
Kyslik
6

Con mayor frecuencia, la mayoría de las aplicaciones tendrán una parte de datos, visualización y procesamiento, y solo ponemos todas esas en las letras M, Vy C.

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

Ver ( V) -> Tiene un formato de visualización para la aplicación y solo conoce el modelo de cómo digerir y no se molesta C.

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

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

sentirse bien y programar
fuente
0

En mi caso, tengo una clase de base de datos que maneja todas las interacciones directas de la base de datos, como las consultas, la obtención y demás. Entonces, si tuviera que cambiar mi base de datos de MySQL a PostgreSQL , no habría ningún problema. Por lo tanto, agregar esa capa adicional puede ser útil.

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

Expediente 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;
    }
}

Tabla objeto clase L

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.

Ibu
fuente
12
"Entonces, si tuviera que cambiar mi base de datos de MySQL a PostgreSQL, no habría ningún problema". Uhhhmmm con el código anterior, tendría un gran problema para cambiar cualquier cosa.
PeeHaa
Veo que mi respuesta tiene cada vez menos sentido después de la edición, y a medida que pasa el tiempo. Pero debería quedarse aquí
Ibu
2
Databaseen el ejemplo no es una clase. Es solo un contenedor para funciones. Además, ¿cómo puede tener una "clase de objeto de tabla" sin un objeto?
tereško
2
@ tereško He leído muchas de tus publicaciones y son geniales. Pero no puedo encontrar ningún marco completo en ningún lugar para estudiar. ¿Conoces uno que "lo hace bien"? ¿O al menos uno que le guste a usted y a otros aquí en SO decir? Gracias.
Johnny
Puede que llegue tarde, pero me gustaría señalar que PDO casi resuelve el problema de tener que crear una 'capa' de DB para facilitar futuros cambios.
Matthew Goulart