MVC

MVC y Conexión a BBDD

Objetivo:

  • Vamos a refinar la arquitectura de nuestra aplicación

    • Aplicaremos una arquitectura MVC

    • Aplicaremos el patrón de controlador frontal.

  • Vamos a ver como se hace la conexión y acceso a una base de datos. Usaremos un patrón Active Record

  • Completaremos una pequeña aplicación que refleje todo esto

Configuración

  • Vamos a usar un nuevo sitio web: mvc.local

  • Debes incluirlo en /etc/hosts

  • Debes añadir la configuracion en el dockercompose. Directamente puedes usar la rama mvc

    git checkout --track origin/mvc
  • Debes crear la carpeta data/mvc. Allí puedes ir construyenco tu entorno MVC.

Carpeta public:

  • En una aplicación Web servimos el contenido de un directorio.

  • Este directorio es conocido como Document Root en Apache.

  • Todo lo que hay en él es accesible desde el exterior

  • Resulta interesante separar nuestro código php y otros recursos de lo que realmente queremos publicar.

  • Por defecto el document root de Apache es /var/www/html

  • Nosotros queremos cambiarlo a /var/www/html/public

  • Debemos crear nuestra carpeta public: es decir data/mvc/public.

  • El resto de contenido dentro de html será inaccesible desde el exterior

  • Para lograrlo hay que modificar la configuración de Apache.

  • En una instalación común, en el fichero de configuración:

  • Nosotros estamos usando docker y en la rama mvc ya lo tenemos.

  • La modificación la realizamos desde nuestro Dockerfile. Debemos añadir:

Resumen de qué debemos hacer:

  • Actualizar entornods y pasar a la rama mvc:

  • Añadir mvc.local a fichero /etc/hosts:

¿Funciona?

  • Crea estos ficheros y conecta tu navegador a mvc.local

MVC

mvc
  • El controlador es quien responde a cada petición

    • Recibe ordenes como: quiero la lista de usuarios, añadir uno nuevo, modificarlo, ...

  • El modelo se ocupa de obtener los datos, normalmente de una BBDD. También es la clase que los contiene. Típicamente, un registro, un objeto.

  • El controlador tras obtener los datos del modelo invoca una vista.

Ejemplo simplificado sin BBDD

  • Rama mvc00.

  • Un fichero "cargador": start.php

  • Un controlador: Controller.php

  • Un modelo: User.php

  • Dos vistas: index y show

El fichero start.php

El controlador:

El modelo:

Vista del listado:

Vista del detalle:

Front Controller y Enrutamiento:

Situación hasta el momento:

  • Rama mvc01

  • Inconvenientes de la arquitectura anterior:

    • Si hay múltiples recursos o tablas tendremos múltiples controladores

    • Cada uno de ellos supone un punto de entrada a la aplicación.

Front Controller: entrada única.

  • La existencia de una entrada única:

    • Permite filtrar cualquier petición.

    • Permite realizar tareas sistemáticas:

      • Iniciar sesión

      • Comprobar si el usuario está autorizado

¿Qué es Front Controller?

  • Se trata de la clase que recibe (casi) todas las peticiones.

  • Vamos a hacer que cualquier petición que no responda a un fichero real (css, js, imagen, php,...) llegue a este controlador frontal.

  • De acuerdo a la ruta recibida (URI), puede cargar un controlador u otro y ejecutar el método necesario.

  • Esta funcionalidad se conoce como enrutamiento.

  • Es interesante que el enutamiento sea user friendly, es decir que evite los parámetros GET:

Rutas amigables:

  • Vamos a hacer que todas las peticiones lleguen a una única clase App de entrada.

  • Nosotros escribiremos:

    http://mvc.local/user/show

  • Apache interpretará:

    http://mvc.local/index.php?url=user/show

Módulo rewrite

  • Para lograr esto necesitamos que Apache tenga activo el módulo rewrite.

  • Mira tu Dockerfile de mvc, ya está activo.

  • Además necesitas un fichero .htaccess en el public

Procesar la ruta:

  • Nuestro controlador frontal va a ser una clase llamada App.

  • Nuestro start.ph debe crear un objeto App.

  • El constructor de App llevará toda la ejecución de la aplicación, de acuerdo a la ruta recibida:

  • En nuestro proyecto de clase el front controller carga un controlador de manera fija de acuerdo a la ruta y después ejecuta uno de sus métodos:

  • Esquema que usaremos:

  • Por ejemplo la ruta /user/index:

    • Carga el controlador UserController

    • Y ejecuta su método index()

  • Además tomaremos un controlador y método por defecto:

    • Si no existe controlador tomamos uno por defecto, por ejemplo index o home .

    • Si no existe método tomamos uno por defecto, por ejemplo index

  • Veamos:

Un poco de orden:

  • Necesitamos usar clases controladoras, modelos y vistas.

  • Vamos a borrar las clases Controller y User de días anteriores, y el directorio views.

  • Vamos a crear los siguientes directorios:

Probando un controlador

  • Ejercicio: Crea y prueba UserController y LoginController

Primeros controladores y organizar vistas

  • Vamos a crear tres controladores básicos que sólo saluden.

  • Vamos a añadir un poco de estilo: Bootstrap

  • Vamos a dar cierta estructura a nuestras vistas:

Controladores básicos

Ejercicio

  • Copia el código del enlace en una vista llamada app/views/home.php.

  • Limpia el texto y ajústalo al contenido de nuestra aplicación.

  • Separa la vista en tres ficheros:

    • Extrae <header> .... </header> al fichero header.php.

    • Extree <footer> ... </html> al fichero footer.php.

  • Añade los header y footer al home.php con require y rutas relativas o aboslutas:

    • <?php require __DIR__ . "/header.php" ?>

    • <?php require "../app/views/header.php" ?>

  • Para terminar podríamos separar incluso más ficheros:

    • head.php: El contenido de la eiqueta <head>

    • scripts.php: El contenido de los scripts colocados al final del body.

Namespaces

  • Rama mvc02

  • El código usado hasta aquí es correcto pero ...

  • Si usamos librerías externas podemos tener problemas de colisión de nombres.

  • Esto es, que dos clases se llamen igual.

  • Para solucionar este problema se usan los namespaces, concepto análogo a los paquetes de java.

  • Es como si organizaramos las clases en directorios.

  • Dos ficheros con el mismo nombre pueden estar en directorios distintos. Dos clases con el mismo nombre pueden estar en namespaces distintos.

  • Es habitual nombrar los directorios (minúsculas: models) y namespaces con el mismo nombre (primera mayúscula: Models)

  • Los namespaces tambien se pueden anidar unos dentro de otros

  • En un namespace pueden englobarse: constantes, funciones, clases y otros elementos de POO.

Declaración

  • Al principio del fichero debe declararse:

  • Si no se declara el namespace los elementos pertenecen al namespace global (\)

  • Espacios anidados serían:

Acceso.

  • Para acceder a un elemento (constante, función, clase ...):

    • Primero hemos de hacerlo disponible: include/require.

    • Después nos podemos acceder a él.

  • El acceso puede ser:

    • Sin cualificar

    • Cualificado

    • Totalmente cualificado

  • Acceso sin cualificar:

  • Las búsquedas son en cualquier fichero de el namespace actual

  • Ruta relativa

  • Acceso cualificado, el elemento va referido a un namespace (Namespace\Elemento)

  • Comienza sin barra: ruta relativa

  • Acceso totalmento cualificado.

    • El elemento se refiere a un namespace desde el global.

    • Equivale a una ruta absoluta.

  • Para evitar la referencia cualificada podemos declarar el uso

  • Palabra reservada use

  • Suele hacerse en la cabecera del fichero

  • Cuando utilizamos use podemos renombrar elmentos:

  • Por ejemplo:

Ejercicio

  • Modifica el código de nuestro proyecto para que todas las clases estén contenidas en un namespace (App\Controllers)

  • Idem para la clase App (namespace Core)

  • Modifica la clase App para que los controladores sean referidos a su namespace

Modelo: Active Record y herencia.

  • Rama mvc03

  • Vamos a acceder a bases de datos.

  • Vamos a usar el conector PDO (está incluído en el Dockerfile de mvc).

  • Vamos a usar herencia para que la conexión a la BBDD sea definida en un único sitio.

PDO

  • PHP cuenta con multitud de extensiones para gestionar bases de datos.

  • Existen extensiones exclusivas de muchos proveedores.

  • Existen extensiones abstractas que permiten la conexión a múltiples bases de datos.

    • PDO es uno de estos casos.

    • Permite migrar de sistema de BBDD sin modificar nuestro código.

Active Record.

Esquema Active Record
  • El patrón Active Record definde clases que permiten el mapeo objeto relacional.

  • La misma clase:

    • Contiene los atributos correspondientes a las columnas de un registro.

    • Define los métodos necesarios para la consulta y modificación de registros.

Conexión y herencia

  • Podemos definir la conexión en todas las clases de modelo: User, Product, Order, ...

  • Parece más interesante definir la conexión dentro de una superclase modelo y usar herencia

  • Refactorizando:

  • Sería más conveniente usar un fichero de configuración para sacar los parámtros de configuración fuera del código en sí.

El modelo de Usuario

  • Vamos a analizar cómo podría ser nuestro modelo de usuario

  • Namespace App\Models

  • Definimos herencia de model

  • Usamos herencia para usar una conexión ya definida en Model.php

  • Los atributos podrían definirse en nuestra clase User pero no es necesario, php permite definir en ejecución los atributos.

CRUD

  • Create

  • Read

  • Update

  • Delete

  • ABMC: Alta, Baja, Modificación y Consulta

CRUD (I): READ

  • Primer método: all() para buscar todos los registros.

  • Usamos la conexión del Model

  • Usamos su función query() para ejecutar SELECT

  • El resultado puede ser tomado usando las funciónes de PDO fetch y fetch_all

  • fetch recoge registro a registro. Si hay muchos requiere un bucle.

  • fetch_all recoge arrays. Ojo si hay un solo registro.

El modelo:

El controlador, método index:

La vista:

  • Segundo método find(), funciones preparadas.

  • Este método lo usamos para cargar un registro a partir de su id.

  • Importante: usamos sentencias preparadas para evitar Sql injection

  • También se obtiene más velocidad si se ejecuta varias veces la misma sentencia

  • Para cargar un objeto User debemos usar setFetchMode y fetch.

  • Paso de Parámetros:

    • Puede realizarse pasando un array en el execute (ejemplos 1 y 2 del manual)

    • Puede realizarse mediante el método bindParam. En el se pasa una variable. Ideal para bucles.

    • Puede realizarse mediante el método bindValue. Se pasa un literal o el valor de una variable.

El controlador, método show:

La vista:

Fechas

  • Si echamos un ojo a las fechas veremos que no están bien formateadas.

  • Si no lo hacemos las fechas se mostrarán con el parseo de mysql:

    • Si es tipo Date: año-mes-dia

    • Si es DateTime: año:mes-dia h:m:s

  • Php puede manejar de forma nativa datos fecha.

  • Se trata de campos numéricos que miden los segundos dede 1970 (fecha unix);

  • Funciones adecuadas son: date() o strtotime()

  • Pero resulta más práctico usar la clase DateTime

  • Debemos decirle al modelo User que birthdate es una fecha.

  • Cómo?:

  • ¿Dónde?: en el constructor. Este método se invoca cada vez que leamos un registro con fetch o con fetchAll.

  • Después en la vista: $user->birthdate->format('Y-m-d')

CRUD (II): CREATE

  • La inserción requiere dos métodos del controlador:

    • Método insert que genera un formulario de alta.

    • Método store que recibe los datos de dicho formulario.

  • El método store concluye con un reenvío a la lista, index(), o al detalle, show() del nuevo registro;

  • Controlador:

  • Vista formulario de alta. Ojo de momento fecha tipo "txt" y en formato "Y-m-d":

  • Modelo User, método insert()

CRUD (III): UPDATE

  • La actualización requiere dos métodos del controlador:

    • Método edit que genera un formulario de modificación con los datos del registro. Este método implica buscar en la base de datos antes de construir el formulario.

    • Método update que recibe los datos de dicho formulario. Igualmente, debemos buscar el registro en la base de datos y después modificarlo.

  • El método update también concluye con un reenvío;

Controlador:

  • Vista formulario de edición:

  • Modelo User, método save()

CRUD (IV): DELETE

  • El método más sencillo

  • Sólo requiere borrar y reenviar.

  • En el modelo la cosa es sencilla:

Composer y autoload

  • rama mvc06

  • Composer es un gestor de dependencias en proyectos php.

  • Permite definir las librerías de las que depende un proyecto.

  • Controlar las versiones y mantener las mismas en diferentes instalaciones.

  • Permite autocargar todas las clases sin usar require.

  • Instalamos:

  • Iniciamos el proyecto:

  • El comando init crea un fichero llamado composer.json.

Ejemplo

  • Un proyecto y sus dependencias se define en el fichero composer.json.

  • Ejemplo con name, authors, require y autoload:

  • En require definimos librerías a usar por el proyecto.

  • En autoload definimos los namespaces que vamos a usar y los directorios donde está cada uno de ellos.

  • Debemos ejecutar en el proyecto:

  • Esto crea unos scripts dentro de vendor/composer

  • Después debemos añadir un único require, por ejemplo en star.php

Nota: no te preocupes por tener muchas dependencias y solo usar unas pocas en cada página. Este archivo autoload no carga nada especialmente, solo tiene el script de autocarga de clases. PHP no resultará más pesado. Solamente se irán cargando las clases que vayas usando en tu código.

Ejercicio

  • Haz lo indicado en nuestro proyecto MVC y elimina todos los require de clases.

NOTA: cada clase que añadas al proyecto require ejecutar de nuevo composer dump-autoload

  • Para usar librerías de terceros también usamos composer

  • Podemos editar el composer.json o ejecutar composer require nombrelibreria

  • Observa:

    • El contenido de composer.json

    • El nuevo fichero composer.lock

Comandos principales

  • Para instalar las dependencias al clonar un repositorio o tras editar el composer.json:

  • Para actualizar las dependencias a la versión más actual:

Login: sesión, contraseñas

  • Hacer login supone decirle al servidor que guarde en sesión la información del usuario actual.

  • Lo habitual es usar nombre (o email) + contraseña.

  • La contraseña debe ir cifrada. Ningún sistema serio debe guardar la contraseña sin cifrar.

  • Existen multitud de sistemas de cifrado.

  • Podríamos usar un hash md5 o sha, disponibles en cualquier sistema.

  • Estos sistemas son indescifrables pero vulnerables a ataques por diccionario.

  • Ejercicio:

    • Ejecuta md5('secret')

    • Busca el resultado obtenido en internet

    • ¿Qué vulnerabilidad encuentras?

  • Usaremos password_hash.

  • Vamos a asignar contraseña a nuestros usuarios de forma masiva usando la consola

  • Hasta ahora siempre corríamos php desde el navegador.

  • También podemos hacerlo desde la consola.

  • Ejemplo:

  • Vamos a crear dos métodos en User:

    • setPassword($password) para asignarle contraseña.

    • passwordVerify($password) para comprobarla.

  • Script de asignación de contraseñas masivas:

Ejercicio:

  • Login de usuario.

  • En la ruta /login debe mostrarse una vista: email + contraseña

  • Al enviar el formulario debe comprobarse que la contraseña es válida

  • Si es válida se envía a home

  • Si no lo es se regresa a la vista de login.

  • Si el usuario está logueado, el botón login debe cambiarse por otro de cerrar sesión (login/logout).

    • El método out debe eliminar el usuario de la sesión.

  • Haz que las rutas de usuarios, productos y tipos de producto sean exclusivas de usuarios logueados.

    • Usa reenvío a login en caso contrario.

  • Uso de la sesión para guardar el email: si falla el login haz que no se olvide el email.

    • Guarda antes de reenviar de vuelta.

    • Elimina tras cargar la vista.

Fechas

Relaciones 1:n entre modelos

$hijo->padre->atributo

  • En una relación 1:N, un padre tiene muchos hijos, un hijo pertenece a un padre.

  • En nuestra ruta /product/index mostramos la lista de productos.

  • En ella mostramos el type_id

  • Sería más interesante mostrar el nombre del tipo de produto:

  • Vamos a añadir un método type() que cargue un atributo con ese nombre y que contenga toda la información del product_type

  • Ahora ya podemos mostrar el nombre del tipo de producto:

  • Pero sería más eleganta tratar type como un atributo.

¿Cómo hacer esta magia? La función __get($atributo)

  • Hemos visto algunos métodos mágicos:

    • __construct(), el constructor

    • __toString(), la conversión de un objeto a texto

  • Vamos ahora a usar el metodo __get($nombreAtributo)

  • La función __get se ejecuta siempre que intentamos acceder a un atributo inexistente.

  • Vamos a decirle al sistema lo siguiente:

    • Si piden un atributo desconocido pero hay un método con ese nombre:

    • Primero ejecuta el método para que cree ese atributo.

    • Después devuelve el atributo ya existente.

  • Ejemplo:

$padre->hijos array!!

  • En la ruta /producttype/show/id me puede interesar mostrar la lista de productos de cada tipo.

  • Por ejemplo al ver el tipo refresco, quiero ver la lista de todos los refrescos.

  • Con lo ya explicado parece fácil

  • En el caso de los productos, en el modelo Producttype definimos un método products:

Ejercicio:

  • En el show de tipos de producto añade la lista de productos asociados.

  • Una pista:

Etiqueta select en creación y actualización

  • Alta y modificación:

    • En el alta debe aparecer un select en vez de una caja de texto

    • En el select deben cargarse todas las opciones de acuerdo a la tabla product_types

    • En la modificación debemos marcar la opción correspondiente con el parámetro "selected".

  • ¿Cómo construir el select de creación?

  • En el controlador:

  • En la vista:

  • ¿y en el formulario de actualización?

  • En el controlador hacemos como en el método create

  • En la vista de nuevo usamos foreach para el select

  • Ojo, ahora debemos añadir el atributo selected dentro del option que corresponda

Paginación

  • Cuando manejamos tablas con numerosos registros resulta fundamental mostrarlos de forma paginada.

  • Vamos a ver cómo realizar la paginación:

El modelo

El controlador

La vista

Trabajo

  • Vamos a poner en práctica lo que hemos visto y alguna cosa más como trabajo de este tema.

  • Buena parte de este trabajo puede haberse realizado en el seguimiento de lo explicado hasta aquí, o tal vez se ha hecho algo parecido.

  • En la rama trabajo tienes el sql de las tablas

Fase 1

HISTORIA 1:

  • El sistema debe permitir mantener una lista de usuarios:

  • Ejecuta el fichero users.sql desde phpmyadmin. Crearás la BBDD mvc18trabajo con una tabla users y 7 registros iniciales.

Se espera acceder a las siguientes rutas:

  • /user y /user/index Lista de usuarios

  • /user/show/{id} Detalles del usuario con id {id}. La contraseña ni se modifica ni se muestra.

  • /register y /register/index Formulario de registro (alta de usuario).

  • /register/register Tomar los datos del registro

Fase 2

  • Crea un controlador LoginController (ya existe)

    • Método index: muestra la vista de login

    • Método login: comprueba los datos del formulario de login.

      • Si son válidos, guarda el usuario en sesión.

      • Si no son válidos reenvía al usuario a la ruta anterior con un mensaje de error (usa $_SESSION['error'])

      • A partir de ahí debes mostrar en la cabecera la información de usuario (header.php)

      • Si el usuario existe: nombre + link de logout

      • Si no existe link a login.

    • El método logout debe cerrar la sesión y reenviar a login.

Fase 3

  • Ejecuta los sql 02 y 03

  • Crea un controlador ProductController responsable de un CRUD completo sobre la tabla "products".

    • lista: /product/index

    • detalle: /product/show/{id}

    • formulario de alta: /product/create

    • alta del producto: /product/store (action del formulario anterior)

    • edición de un producto: /product/edit/{id}

    • actualización del producto: /product/update (action del formulario anterior)

    • borrado de producto: /product/delete/{id}

Fase 4

  • Modifica el CRUD sobre la tabla de productos:

    • En la lista de productos debe aparecer el nombre del tipo y no su id

    • En el alta y modificación de producto debe aparecer un select con la lista de tipos para elegir el adecuado.

  • Además debes crear el controlador ProducttypeController y los métodos index y show:

    • Añade la ruta /producttype que muestra todos los tipos de productos.

    • Añade la lista /producttype/show/{id} que muestra el detalle de un tipo de producto y, debajo, la lista de productos asocidados disponibles.

Fase 5

  • Vamos a gestionar la realización de "pedidos" en la peña. Se trata de que cada usuario registre aquellos productos que consume.

  • Añade las siguientes rutas:

    /basket/add/{id} Este enlace debe estar en la lista de productos. Al hacer click sobre el se añade a la lista de productos en la cesta.

    Si un producto se clica y ya existe en la cesta se aumenta la cantidad.

  • /basket Muestra el contenido de la cesta en cada momento (producto, cantidad, precio).

  • /basket/remove/{id}

  • /basket/up/{id} y /basket/down/{id} aumenta o disminuye en 1 la cantidad de cada producto. (enlaces en la vista de /basket).

  • Al cerrar sesión se debe perder el contenido de la cesta. El próximo día veremos como guardarla en BBDD.

Fase 6

  • Ruta /basket/store Guarda el contenido de la cesta en la tabla orders y order_product. La cesta se vacía. Usa transacciones.

  • Ruta /order muestra la lista de pedidos: fecha en formato d/m/yyy y nombre completo del comprador.

  • Ruta /order/show/{id} muestra información completa del pedido, productos, cantidades y precio (el del pedido, no el del producto).

Last updated

Was this helpful?