# Día 3, Bienes Raíces-POO: crear productos Siguiendo Active records ya hemos creado nuestra clase, con su constructor y reflejando perfectamente los campos de la base de datos. ``` <?php namespace App; class Propiedad{ public $id; public $titulo; public $precio; public $imagen; public $descripcion; public $habitaciones; public $wc; public $estacionamiento; public $creado; public $vendedorId; public function __construct($args = []) { $this->id = $args['id'] ?? null; $this->titulo = $args['titulo'] ?? ''; $this->precio = $args['precio'] ?? ''; $this->imagen = $args['imagen'] ?? ''; $this->descripcion = $args['descripcion'] ?? ''; $this->habitaciones = $args['habitaciones'] ?? ''; $this->wc = $args['wc'] ?? ''; $this->estacionamiento = $args['estacionamiento'] ?? ''; $this->creado = date('Y/m/d'); $this->vendedorId = $args['vendedorId'] ?? ''; } ``` Ahora vamos a productos/crear.php y vemos cómo modificarlo para usar lo que hemos creado. Las primeras líneas del fichero deben cambiar, promero porque sólo deben incluir app.php, segundo porque tenemos que usar el namespace que hemos creado para nuestra aplicación y el objeto Propiedad, y tercero porque lo primero que tengo que ``` <?php require '../../includes/app.php'; use App\Propiedad; $propiedad=new Propiedad(); echo '<pre>'; var_dump($propiedad); echo '</pre>'; exit; ``` Ya hemos escrito esto último lo suficiente, vamos a crear unas funciones helpers que nos ayuden a realizar el desarrollo aunque se supone que en este punto ya tienes instalado el xdebug que también te puede ayudar con esto. Dentro de funciones.php vamos a crear una función con ese código. ``` function debuguear($variable){ echo '<pre>'; var_dump($variable); echo '</pre>'; exit; } ``` A partir de ahora cuando la necesitemos la usaremos de esta manera, ahorrando escribir una y otra vez este código. Lo probamos! Vamos también a mejorar la función estaAutenticado de funciones.php ``` function estaAutenticado() { session_start(); if(!$_SESSION['login']) { header ('Location: /'); } } ``` De tal manera que ahora cada vez que queramos proteger una ruta usaremos esta función, el comienzo del código de crear.php quedará: ``` <?php require '../../includes/app.php'; use App\Propiedad; $propiedad=new Propiedad(); debuguear($propiedad); // Proteger esta ruta. estaAutenticado(); ``` Realmente la nueva propiedad sólo se crea cuando hacemos un post así que para crear el nuevo objeto lo hacemos como sigue: ``` if ($_SERVER['REQUEST_METHOD'] === 'POST') { $propiedad=new Propiedad($_POST); debuguear($propiedad); ``` Comprobamos qué pasa? Si hay errores, para para solucionarlo. Lo que nos interesa es después de asignar los valores hacer algo como $propiedad->guardar() Así que en Propiedad.php creamos este método: ``` public function guardar(){ $query = "INSERT INTO propiedades (titulo, precio, imagen, descripcion, habitaciones, wc, estacionamiento, vendedorId, creado ) VALUES ( '$this->titulo', '$this->precio', '$this->imagen', '$this->descripcion', '$this->habitaciones', '$this->wc', '$this->estacionamiento', '$this->vendedores_id', '$this->creado' )"; debuguear($query); } ``` Lo llamamos desde crear: ``` $propiedad=new Propiedad($_POST); $propiedad->guardar(); debuguear($propiedad); ``` Vemos que nos devuelve algo como: ``` string(345) "INSERT INTO propiedades (titulo, precio, imagen, descripcion, habitaciones, wc, estacionamiento, vendedorId, creado ) VALUES ( 'casita en la playa', '4444', '', 'casita en la playacasita en la playacasita en la playacasita en la playacasita en la playacasita en la playacasita en la playacasita en la playa', '2', '1', '2', '1', '2023/11/28' )" ``` Nos falta la imagen pero como eso lo trabajaremos más adelante vamos a insertar una por defecto poniendo en el constructor algo como: `$this->imagen = $args['imagen'] ?? 'imagen.jpg';` Cómo hacemos para ejecutarlo dentro de este método, habrá que tener una conexión! Vamos a ver cómo hacemos para que este objeto tenga su conexión. Si dentro del constructor pusiéramos algo como $this->db=new mysqli con cada objeto propiedad abriría una nueva conexión a la base de datos. Para esto sirven los atributos estáticos! Así que creamos un atributo static $db y una función para setear la base de datos: public static function setDB($database){ self::$db=$database; } en app.php incluimos esta conexión ya que va a ser común para todos: ``` $db=conectarDb(); Propiedad::setDB($db); ``` Y en database.php cambiamos el método de conexión al formato de POO y además mejoramos la respuesta en caso de error mostrando el código del error: ``` function conectarDb(): mysqli { $db = new mysqli('localhost', 'root', '', 'bienesraices_crud'); if (!$db) { echo "Error: No se pudo conectar a MySQL."; echo "errno de depuración: " . mysqli_connect_errno(); echo "error de depuración: " . mysqli_connect_error(); exit; } return $db; } ``` Si vemos en el código original después de tener el query se hace: `$resultado = mysqli_query($db, $query)` Pero en nuestro método guardar ya está todo orientado a objetos así que quedaría: ``` public function guardar(){ $query = "INSERT INTO propiedades (titulo, precio, imagen, descripcion, habitaciones, wc, estacionamiento, vendedores_id, creado ) VALUES ( '$this->titulo', '$this->precio', '$this->imagen', '$this->descripcion', '$this->habitaciones', '$this->wc', '$this->estacionamiento', '$this->vendedores_id', '$this->creado' )"; $resultado=self::$db->query($query); debuguear($resultado); } ``` ## Sanitizar datos Si ves el código ves que vamos recorriendo cada atributo de nuestra propiedad, para simplificar y generalizar vamos a crear un vector con los campos a sanitizar: ` protected static $columnasDB = ['id', 'titulo', 'precio', 'imagen', 'descripcion', 'habitaciones', 'wc', 'estacionamiento', 'creado', 'vendedores_id'];` Como ves, es un static porque al igual que $db será común a todos los objetos de la clase. Creamos un método atributos que mapee nuestro atributos en un vector y vamos a sanitizarlos en el método sanitizarAtributos, recorriendo y usando el método orientado a objetos. ``` public function guardar(){ //Sanitizar los datos $atributos=$this->sanitizarAtributos(); //insertar en la base de datos $query = "INSERT INTO propiedades (titulo, precio, imagen, descripcion, habitaciones, wc, estacionamiento, vendedores_id, creado ) VALUES ( '$this->titulo', '$this->precio', '$this->imagen', '$this->descripcion', '$this->habitaciones', '$this->wc', '$this->estacionamiento', '$this->vendedores_id', '$this->creado' )"; $resultado=self::$db->query($query); } //identifica y une los atributos de la bd con sus valores en forma de vector public function atributos(){ $atributos=[]; foreach(self::$columnasDB as $columna){ if ($columna==='id') continue; $atributos[$columna]=$this->$columna; } return $atributos; } public function sanitizarAtributos(){ $atributos=$this->atributos(); $sanitizado=[]; //este vector se recorre como asociativo foreach ($atributos as $key=>$value){ $sanitizado[$key]= self::$db->escape_string($value); } debuguear($sanitizado); return $sanitizado; } } ``` Como ves en el método guardar tenemos en $atributos, los datos sanitizados, pero no son los que estamos introduciendo en la base de datos. Para cambiar ese código es complicado...vamos a automatizarlo! ``` public function guardar(){ //Sanitizar los datos $atributos=$this->sanitizarAtributos(); //insertar en la base de datos $query = "INSERT INTO propiedades ("; $query.=join(',', array_keys($atributos)); $query.=" ) VALUES (' "; $query.=join(',',array_values($atributos)); $query.= " ' ) "; debuguear($query); $resultado=self::$db->query($query); } ``` Si intentas hacerlo dará un error. Comprueba por qué!!! Como te darás cuenta ya podemos borrar del crear, mucho código que ya no hace falta al estarse encargando la clase Propiedad de hacer lo necesario. Vete borrando el código que creas conveniente. ## Añadiendo validación Tenemos ahora que hacer que sea el mismo objeto Propiedades quien se encargue de la validación y vamos a volver a insistir, no es lo mismo validar que sanitizar, este es un código explicativo, no hay que incluirlo en el proyecto: ``` // Sanitizar va a hacer eso, limpiar los datos $estacionamiento = filter_var($numero, FILTER_SANITIZE_NUMBER_INT); // Validar va a revisar que sea un tipo de dato valido. $estacionamiento = filter_var($numero, FILTER_VALIDATE_INT); // Existe otra opción llamada mysqli_real_escape_string, esta función va a eliminar los caracteres especiales o escaparlos para hacerlos compatibles con la base de datos. $titulo = mysqli_real_escape_string( $db, $_POST['titulo'] ); // Todo esto de escapar datos y asegurarlos se puede evitar con Sentencias preparadas y PDO ``` Lo primero es crear en Propiedad.php el vector vacío con los errores y un getter de errores: ` protected static $errores=[]; //Validaciones public static function getErrores(){ return self::$errores; } ` En crear.php borramos la inicialización del vector de errores y lo sustituimos por el getter: `$errores = Propiedad::getErrores();` Cortamos todo el código de validación y lo llevamos hasta la clase dentro del método Propiedad->validar, como ves quedará igual salvo que ahora tendremos que acceder a los atributos de la clase: ``` public function validar(){ if (!$this->titulo) { self::$errores[] = 'Debes añadir un Titulo'; } if (!$this->precio) { self::$errores[] = 'El Precio es Obligatorio'; } if (strlen($this->descripcion) < 50) { self::$errores[] = 'La Descripción es obligatoria y debe tener al menos 50 caracteres'; } if (!$this->habitaciones) { self::$errores[] = 'La Cantidad de Habitaciones es obligatoria'; } if (!$this->wc) { self::$errores[] = 'La cantidad de WC es obligatoria'; } if (!$this->estacionamiento) { self::$errores[] = 'La cantidad de lugares de estacionamiento es obligatoria'; } if (!$this->vendedores_id) { self::$errores[] = 'Elige un vendedor'; } if (!$this->imagen) { self::$errores[] = 'Imagen no válida'; } return self::$errores; } ``` En crear.php vamos a seguir sustituyendo código: ``` if ($_SERVER['REQUEST_METHOD'] === 'POST') { $propiedad=new Propiedad($_POST); $errores= $propiedad->validar(); // El array de errores esta vacio if (empty($errores)) { $propiedad->guardar(); ``` Si queremos hacer pruebas tendremos que comentar todo lo referente a las imágenes o dará fallo. Así que... ## Subir imágenes con InterventionImage Vamos a instalar una librería específica de manipulación de imágenes. https://image.intervention.io/v2 Como tenemos el composer instalado mejor seguimos las instrucciones de instalación en https://packagist.org/packages/intervention/image Donde nos dice que se instala con: `composer require intervention/image` Una vez instalado y gracias a que tenemos instalado el composer y el autoload en el app.php que hemos incluido todos los ficheros, no tenemos que hacer nada sino poner lo siguiente donde queremos usarla: `use Intervention\Image\ImageManagerStatic as Image;` (hemos puesto un alias para no tener que escribir tanto!) COn esta librería podemos hacer efectos sobre nuestras imágenes, por ejemplo usaremos muchon FIT: https://image.intervention.io/v2/api/fit Que es para recortar imágenes. Vamos a reemplazar la línea en la que copiamos la imagen a la carpeta: `move_uploaded_file($imagen['tmp_name'], $carpetaImagenes.$nombreImagen);` Por ``` //Realiza resize $image= Image::make($_FILES['imagen']['tmp_name']); $image->fit(800,600); ``` Que lo que hará será recortar la imagen y prepararla para subirla al server. En el objeto Propiedad.php vamos a setear las imágenes: ``` public function setImagen($imagen){ if ($imagen){ $this->imagen=$imagen; } } ``` Y nuestro crear.php se reorganizaría para quedar: ``` <?php require '../../includes/app.php'; use App\Propiedad; use Intervention\Image\ImageManagerStatic as Image; // Proteger esta ruta. //estaAutenticado(); $consulta = "SELECT * FROM vendedores"; $resultado = mysqli_query($db, $consulta); // Validar $errores = Propiedad::getErrores(); $titulo= ''; $precio= ''; $descripcion= ''; $habitaciones= ''; $wc= ''; $estacionamiento=''; $vendedores_id= ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { $propiedad=new Propiedad($_POST); $carpetaImagenes = '../../imagenes/'; if (!is_dir($carpetaImagenes)) { mkdir($carpetaImagenes); } $nombreImagen = md5(uniqid(rand(), true)).".jpg"; if($_FILES['imagen']['tmp_name']){ //Realiza resize $image= Image::make($_FILES['imagen']['tmp_name']); $image->fit(800,600); $propiedad->setImagen($nombreImagen); } $errores= $propiedad->validar(); // El array de errores esta vacio if (empty($errores)) { //Subir la imagen $image->save($carpetaImagenes.$nombreImagen); //guardar en la bd $resultado= $propiedad->guardar(); if ($resultado) { header('location: /admin/index.php?mensaje=1'); } } // Insertar en la BD. } ?> ``` Y la clase Propiedades.php quedaría: ``` <?php namespace App; class Propiedad { // Base DE DATOS protected static $db; protected static $tabla = 'propiedades'; protected static $columnasDB = ['id', 'titulo', 'precio', 'imagen', 'descripcion', 'habitaciones', 'wc', 'estacionamiento', 'creado', 'vendedores_id']; protected static $errores=[]; public $id; public $titulo; public $precio; public $imagen; public $descripcion; public $habitaciones; public $wc; public $estacionamiento; public $creado; public $vendedores_id; public function __construct($args = []) { $this->id = $args['id'] ?? null; $this->titulo = $args['titulo'] ?? ''; $this->precio = $args['precio'] ?? ''; $this->imagen = $args['imagen'] ?? 'imagen.jpg'; $this->descripcion = $args['descripcion'] ?? ''; $this->habitaciones = $args['habitaciones'] ?? ''; $this->wc = $args['wc'] ?? ''; $this->estacionamiento = $args['estacionamiento'] ?? ''; $this->creado = date('Y/m/d'); $this->vendedores_id = $args['vendedores_id'] ?? ''; } public static function setDB($database){ self::$db=$database; } public function guardar(){ //Sanitizar los datos $atributos=$this->sanitizarAtributos(); //insertar en la base de datos $query = "INSERT INTO propiedades ("; $query.=join(',', array_keys($atributos)); $query.=" ) VALUES ('"; $query.=join("' , '",array_values($atributos)); $query.= "')"; $resultado=self::$db->query($query); return $resultado; } //identifica y une los atributos de la bd con sus valores en forma de vector public function atributos(){ $atributos=[]; foreach(self::$columnasDB as $columna){ if ($columna==='id') continue; $atributos[$columna]=$this->$columna; } return $atributos; } public function sanitizarAtributos(){ $atributos=$this->atributos(); $sanitizado=[]; //este vector se recorre como asociativo foreach ($atributos as $key=>$value){ $sanitizado[$key]= self::$db->escape_string($value); } return $sanitizado; } //Validaciones public static function getErrores(){ return self::$errores; } public function validar(){ if (!$this->titulo) { self::$errores[] = 'Debes añadir un Titulo'; } if (!$this->precio) { self::$errores[] = 'El Precio es Obligatorio'; } if (strlen($this->descripcion) < 50) { self::$errores[] = 'La Descripción es obligatoria y debe tener al menos 50 caracteres'; } if (!$this->habitaciones) { self::$errores[] = 'La Cantidad de Habitaciones es obligatoria'; } if (!$this->wc) { self::$errores[] = 'La cantidad de WC es obligatoria'; } if (!$this->estacionamiento) { self::$errores[] = 'La cantidad de lugares de estacionamiento es obligatoria'; } if (!$this->vendedores_id) { self::$errores[] = 'Elige un vendedor'; } if (!$this->imagen) { self::$errores[] = 'Imagen no válida'; } return self::$errores; } public function setImagen($imagen){ if ($imagen){ $this->imagen=$imagen; } } } ``` Ahora mismo el único fallo es que si hay errores se borran los datos que ya hayas introducido en el formulario. Vamos a arreglarlo. Como te has dado cuenta el código del crear es exactamente igual que el del actualizar, vamos a crearlo como plantilla, cortamos todo desde el form de apertura hasta el imput y lo vamos a meter en templates/formulario_propiedades.php En el crear.php el formulario debe quedar como sigue: ``` <form class="formulario" method="POST" enctype="multipart/form-data"> <?php include '../../includes/templates/formulario_propiedades.php'; ?> <input type="submit" value="Crear Propiedad" class="boton boton-verde"> </form> ``` Y en el template podremos utilizar nuestra clase para acceder a los valores iniciales: ``` <legend>Información General</legend> <label for="titulo">Titulo:</label> <input name="titulo" type="text" id="titulo" placeholder="Titulo Propiedad" value="<?php echo $propiedad->titulo; ?>"> ``` Lo podríamos hacer así para todos los campos pero el problema es que esos datos no estarían sanitizados al mostrarse tras un error, por lo que en funciones creamos: ``` function s($html):string{ $s=htmlspecialchars($html); return $s; } ``` Y todos los valores del formulario irían tal que así: ``` <fieldset> <legend>Información General</legend> <label for="titulo">Titulo:</label> <input name="titulo" type="text" id="titulo" placeholder="Titulo Propiedad" value="<?php echo s($propiedad->titulo); ?>"> ```