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
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
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
ohome
.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
Ya hemos creado unos controladores de prueba: home, user y login.
Vamos a crear una vista para el index de los tres.
Empezamos con el index y nos vamos a basar en una plantilla Bootstrap para darle un poco de estilo:
https://getbootstrap.com/docs/4.0/examples/sticky-footer-navbar/
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.
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.
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 delproduct_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)
__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?