22. Archivos en la base de datos

Versión para imprimir.

A. Introducción

B. Diagrama entidad relación

Diagrama entidad relación

C. Diagrama relacional

Diagrama relacional

D. Diagrama de paquetes

Diagrama de paquetes

E. Diagrama de despliegue

Diagrama de despliegue

F. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/srvarchivos?v=1. Hazle fork al proyecto y córrelo. En el ambiente de desarrollo tienes la opción de descargar el proyecto en un zip.

  2. Usa o crea una cuenta de Google.

  3. Crea una cuenta de Replit usando la cuenta de Google.

  4. Crea un proyecto PHP Web Server en Replit y edita o sube los archivos de este proyecto.

  5. Depura el proyecto.

  6. Crea la cover page o página de spotlight del proyecto.

G. Archivos

Haz clic en los triángulos para expandir las carpetas

H. index.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Archivos</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/muestraObjeto.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14
15</head>
16
17<body onload="invocaServicio(fetch('srv/srvProductoConsulta.php'))
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Archivos</h1>
22
23 <p><a href="agrega.html">Agregar</a></p>
24
25 <dl id="lista">
26 <dt>Cargando…</dt>
27 <dd><progress max="100">Cargando…</progress></dd>
28 </dl>
29
30</body>
31
32</html>

I. agrega.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Agregar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14 <script type="module" src="lib/js/imagenNuevaSeleccionada.js"></script>
15
16</head>
17
18<body onload="imagenNuevaSeleccionada(forma.bytes, imagen).catch(muestraError)">
19
20 <form id="forma" onsubmit="submitForm('srv/srvProductoAgrega.php', event)
21 .then(modelo => location.href = 'index.html')
22 .catch(muestraError)">
23
24 <h1>Agregar</h1>
25
26 <p><a href="index.html">Cancelar</a></p>
27
28 <p>
29 <label>
30 Nombre *
31 <input name="nombre">
32 </label>
33 </p>
34
35 <p>
36 <label>
37 Imagen *
38 <input name="bytes" type="file" accept="image/*"
39 oninput="imagenNuevaSeleccionada(this, imagen).catch(muestraError)">
40 </label>
41 </p>
42
43 <p>* Obligatorio</p>
44
45 <p><button type="submit">Agregar</button></p>
46
47 <figure>
48 <img id="imagen" hidden alt="Imagen del producto" style="max-width: 100%;">
49 </figure>
50
51 </form>
52
53</body>
54
55</html>

J. modifica.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Modificar</title>
10
11 <script type="module" src="lib/js/invocaServicio.js"></script>
12 <script type="module" src="lib/js/submitForm.js"></script>
13 <script type="module" src="lib/js/muestraError.js"></script>
14 <script type="module" src="lib/js/muestraObjeto.js"></script>
15 <script type="module" src="lib/js/confirmaEliminar.js"></script>
16 <script type="module" src="lib/js/imagenSeleccionada.js"></script>
17
18 <script>
19 // Obtiene los parámetros de la página.
20 const params = new URL(location.href).searchParams
21 </script>
22
23</head>
24
25<body onload="if (params.size > 0) {
26 invocaServicio('srv/srvProductoBusca.php?' + params)
27 .then(modelo => muestraObjeto(document, modelo.body))
28 .catch(muestraError)
29 }">
30
31 <form onsubmit="submitForm('srv/srvProductoModifica.php', event)
32 .then(modelo => location.href = 'index.html')
33 .catch(muestraError)">
34
35 <h1>Modificar</h1>
36
37 <p><a href="index.html">Cancelar</a></p>
38
39 <input type="hidden" name="id">
40
41 <p>
42 <label>
43 Nombre *
44 <input name="nombre" value="Cargando…">
45 </label>
46 </p>
47
48 <p>
49 <label>
50 Imagen
51 <input name="bytes" type="file" accept="image/*"
52 oninput="imagenSeleccionada(this, imagen).catch(muestraError)">
53 </label>
54 </p>
55
56 <p>* Obligatorio</p>
57
58 <p>
59
60 <button type="submit">Guardar</button>
61
62 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
63 invocaServicio('srv/srvProductoElimina.php?' + params)
64 .then(() => location.href = 'index.html')
65 .catch(muestraError)
66 }">
67 Eliminar
68 </button>
69
70 </p>
71
72 <figure>
73 <img id="imagen" hidden alt="Imagen del producto" style="max-width: 100%;"
74 data-input="bytes">
75 </figure>
76
77 </form>
78
79</body>
80
81</html>

K. Carpeta « srv »

Versión para imprimir.

A. Carpeta « srv / modelo »

1. srv / modelo / Archivo.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4
5class Archivo
6{
7
8 public int $id;
9 public string $bytes;
10
11 public function __construct(string $bytes = "", int $id = 0)
12 {
13 $this->id = $id;
14 $this->bytes = $bytes;
15 }
16
17 public function valida()
18 {
19 if ($this->bytes === "")
20 throw new ProblemDetails(
21 status: ProblemDetails::BadRequest,
22 type: "/error/archivovacio.html",
23 title: "Archivo vacío.",
24 detail: "Selecciona un archivo que no esté vacío."
25 );
26 }
27}
28

2. srv / modelo / Producto.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/validaNombre.php";
4require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
5require_once __DIR__ . "/Archivo.php";
6
7class Producto
8{
9
10 public int $id;
11 public string $nombre;
12 public ?Archivo $archivo;
13
14 public function __construct(
15 string $nombre = "",
16 Archivo $archivo = null,
17 int $id = 0,
18 ) {
19 $this->id = $id;
20 $this->nombre = $nombre;
21 $this->archivo = $archivo;
22 }
23
24 public function validaNuevo()
25 {
26 validaNombre($this->nombre);
27 if ($this->archivo === null)
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/faltaarchivo.html",
31 title: "Falta el archivo.",
32 detail: "Selecciona un archivo que no esté vacío."
33 );
34 $this->archivo->valida();
35 }
36
37 public function valida()
38 {
39 validaNombre($this->nombre);
40 }
41}
42

B. srv / srvArchivo.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/ProblemDetails.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/archivoBusca.php";
8
9mb_internal_encoding("UTF-8");
10try {
11 // Evita que la imagen se cargue en el caché de la computadora.
12 header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
13 header("Cache-Control: post-check=0, pre-check=0", false);
14 header("Pragma: no-cache");
15 $id = leeEntero("id");
16 if ($id === null) throw pdFaltaId();
17 $archivo = archivoBusca($id);
18 if ($archivo === false) {
19 throw new ProblemDetails(
20 status: ProblemDetails::BadRequest,
21 type: "/error/archivonoencontrado.html",
22 title: "Archivo no encontrado.",
23 detail: "No se encontró ningún archivo con el id solicitado."
24 );
25 }
26 echo $archivo->bytes;
27} catch (ProblemDetails $details) {
28 procesa_problem_details($details);
29} catch (Throwable $throwable) {
30 procesa_problem_details(new ProblemDetails(
31 status: ProblemDetails::InternalServerError,
32 type: "/error/errorinterno.html",
33 title: "Error interno del servidor.",
34 detail: $throwable->getMessage()
35 ));
36}
37

C. srv / srvProductoAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeBytes.php";
6require_once __DIR__ . "/../lib/php/leeTexto.php";
7require_once __DIR__ . "/modelo/Archivo.php";
8require_once __DIR__ . "/modelo/Producto.php";
9require_once __DIR__ . "/bd/productoAgrega.php";
10
11ejecutaServicio(function () {
12 $bytes = leeBytes("bytes");
13 $archivo = $bytes === "" ? null : new Archivo(bytes: $bytes);
14
15 $nombre = leeTexto("nombre");
16 $modelo = new Producto(
17 nombre: $nombre === null ? "" : trim($nombre),
18 archivo: $archivo
19 );
20
21 productoAgrega($modelo);
22
23 $id = htmlentities($modelo->id);
24 // Los bytes se descargan con SrvArchivo; no desde aquí.
25 return JsonResponse::created("/srv/srvProductoBusca.php?id=$id", [
26 "id" => ["value" => $modelo->id],
27 "nombre" => ["value" => $modelo->nombre],
28 "imagen" => [
29 "src" => $archivo === null
30 ? ""
31 : "srv/srvArchivo.php?id=" . $archivo->id,
32 ]
33 ]);
34});
35

D. srv / srvProductoBusca.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/ProblemDetails.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/productoBusca.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 $modelo = productoBusca($id);
13 if ($modelo === false) {
14 $htmlId = htmlentities($id);
15 throw new ProblemDetails(
16 status: ProblemDetails::NotFound,
17 type: "/error/productonoencontrado.html",
18 title: "Producto no encontrado.",
19 detail: "No se encontró ningún producto con el id $htmlId.",
20 );
21 } else {
22 $archivo = $modelo->archivo;
23 return [
24 "id" => ["value" => $modelo->id],
25 "nombre" => ["value" => $modelo->nombre],
26 "imagen" => [
27 "src" => $archivo === null
28 ? ""
29 : "srv/srvArchivo.php?id=" . $archivo->id
30 ]
31 ];
32 }
33});
34

E. srv / srvProductoConsulta.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/productoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = productoConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $prodId = htmlentities($modelo->prodId);
11 $prodNombre = htmlentities($modelo->prodNombre);
12 $archId = $modelo->archId === null
13 ? ""
14 : htmlentities($modelo->archId);
15 $render .=
16 "<div style='display: flex; flex-direction: row-reverse;
17 align-items: center; gap: 0.5rem'>
18 <dt style='flex: 1 1 0'>
19 <a href='modifica.html?id=$prodId'>$prodNombre</a>
20 </dt>
21 <dd style='flex: 1 1 0; margin: 0'>
22 <a href='modifica.html?id=$prodId'><img style='width: 100%'
23 alt='Imagen del producto' src='srv/srvArchivo.php?id=$archId'></a>
24 </dd>
25 </div>";
26 }
27 return ["lista" => ["innerHTML" => $render]];
28});
29

F. srv / srvProductoElimina.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/pdFaltaId.php";
6require_once __DIR__ . "/../lib/php/leeEntero.php";
7require_once __DIR__ . "/bd/productoElimina.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 productoElimina($id);
13 return JsonResponse::noContent();
14});
15

G. srv / srvProductoModifica.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/pdFaltaId.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeBytes.php";
7require_once __DIR__ . "/../lib/php/leeTexto.php";
8require_once __DIR__ . "/modelo/Archivo.php";
9require_once __DIR__ . "/modelo/Producto.php";
10require_once __DIR__ . "/bd/productoModifica.php";
11
12ejecutaServicio(function () {
13 $id = leeEntero("id");
14 if ($id === null) throw pdFaltaId();
15 $bytes = leeBytes("bytes");
16 $archivo = $bytes === "" ? null : new Archivo(bytes: $bytes);
17
18 $nombre = leeTexto("nombre");
19 $modelo = new Producto(
20 $nombre === null ? "" : trim($nombre),
21 archivo: $archivo,
22 id: $id
23 );
24
25 productoModifica($modelo);
26
27 // Los bytes se descargan con SrvArchivo; no desde aquí.
28 $archivo = $modelo->archivo;
29 return [
30 "id" => ["value" => $modelo->id],
31 "nombre" => ["value" => $modelo->nombre],
32 "imagen" => [
33 "src" => $archivo === null
34 ? ""
35 : "srv/srvArchivo.php?id=" . $archivo->id,
36 ]
37 ];
38});
39

H. Carpeta « srv / bd »

1. srv / bd / bdCrea.php

1<?php
2
3function bdCrea(PDO $con)
4{
5 $con->exec(
6 'CREATE TABLE IF NOT EXISTS ARCHIVO (
7 ARCH_ID INTEGER,
8 ARCH_BYTES BLOB NOT NULL,
9 CONSTRAINT ARCH_PK
10 PRIMARY KEY(ARCH_ID)
11 )'
12 );
13 $con->exec(
14 'CREATE TABLE IF NOT EXISTS PRODUCTO (
15 PROD_ID INTEGER,
16 PROD_NOMBRE TEXT NOT NULL,
17 ARCH_ID INTEGER NOT NULL,
18 CONSTRAINT PROD_PK
19 PRIMARY KEY(PROD_ID),
20 CONSTRAINT PROD_NOM_UNQ
21 UNIQUE(PROD_NOMBRE)
22 CONSTRAINT PROD_ARCH_FK
23 FOREIGN KEY (ARCH_ID) REFERENCES ARCHIVO(ARCH_ID)
24 )'
25 );
26}
27

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/bdCrea.php";
4
5class Bd
6{
7
8 private static ?PDO $conexion = null;
9
10 static function getConexion(): PDO
11 {
12 if (self::$conexion === null) {
13
14 self::$conexion = new PDO(
15 // cadena de conexión
16 "sqlite:srvarchivos.db",
17 // usuario
18 null,
19 // contraseña
20 null,
21 // Opciones: conexiones persistentes y lanza excepciones.
22 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
23 );
24
25 bdCrea(self::$conexion);
26 }
27
28 return self::$conexion;
29 }
30}
31

3. srv / bd / archivoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/Bd.php";
5
6function archivoAgrega(Archivo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO ARCHIVO
12 (ARCH_BYTES)
13 VALUES
14 (:bytes)"
15 );
16 $stmt->execute([
17 ":bytes" => $modelo->bytes
18 ]);
19 /* Si usas una secuencia para generar el id,
20 * pasa como parámetro de lastInsertId el
21 * nombre de dicha secuencia, debes
22 * ejecutarlo antes del INSERT y pasarle el
23 * id generado al SQL. */
24 $modelo->id = $con->lastInsertId();
25}
26

4. srv / bd / archivoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/Bd.php";
5
6function archivoBusca(int $id): false|Archivo
7{
8 $con = Bd::getConexion();
9 $stmt = $con->prepare(
10 "SELECT
11 ARCH_ID AS id,
12 ARCH_BYTES AS bytes
13 FROM ARCHIVO
14 WHERE ARCH_ID = :id"
15 );
16 $stmt->execute([":id" => $id]);
17 $stmt->setFetchMode(
18 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
19 Archivo::class
20 );
21 return $stmt->fetch();
22}
23

5. srv / bd / archivoElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4
5function archivoElimina(int $id)
6{
7 $con = Bd::getConexion();
8 $stmt = $con->prepare(
9 "DELETE FROM ARCHIVO
10 WHERE ARCH_ID = :id"
11 );
12 $stmt->execute([":id" => $id]);
13}
14

6. srv / bd / archivoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/Bd.php";
5
6function archivoModifica(Archivo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "UPDATE ARCHIVO
12 SET ARCH_BYTES = :bytes
13 WHERE ARCH_ID = :id"
14 );
15 $stmt->execute([
16 ":bytes" => $modelo->bytes,
17 ":id" => $modelo->id
18 ]);
19}
20

7. srv / bd / productoAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/archivoAgrega.php";
6
7function productoAgrega(Producto $modelo)
8{
9 $modelo->validaNuevo();
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 archivoAgrega($modelo->archivo);
13 $stmt = $con->prepare(
14 "INSERT INTO PRODUCTO
15 (PROD_NOMBRE, ARCH_ID)
16 VALUES
17 (:nombre, :archId)"
18 );
19 $stmt->execute([
20 ":nombre" => $modelo->nombre,
21 ":archId" => $modelo->archivo->id
22 ]);
23 /* Si usas una secuencia para generar el id,
24 * pasa como parámetro de lastInsertId el
25 * nombre de dicha secuencia, debes
26 * ejecutarlo antes del INSERT y pasarle el
27 * id generado al SQL. */
28 $modelo->id = $con->lastInsertId();
29 $con->commit();
30}
31

8. srv / bd / productoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Archivo.php";
4require_once __DIR__ . "/../modelo/Producto.php";
5require_once __DIR__ . "/Bd.php";
6
7function productoBusca(int $prodId)
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 P.PROD_ID AS prodId,
13 P.PROD_NOMBRE AS prodNombre,
14 A.ARCH_ID AS archId
15 FROM PRODUCTO P
16 LEFT JOIN ARCHIVO A
17 ON P.ARCH_ID = A.ARCH_ID
18 WHERE P.PROD_ID = :prodId"
19 );
20 $stmt->execute([
21 ":prodId" => $prodId
22 ]);
23 $stmt->setFetchMode(PDO::FETCH_OBJ);
24 $obj = $stmt->fetch();
25 if ($obj === false) {
26 return false;
27 } else {
28 $id = $obj->prodId;
29 $nombre = $obj->prodNombre;
30 $archId = $obj->archId;
31 $archivo = $archId === null ? null : new Archivo(id: $archId);
32 $producto = new Producto(
33 id: $id,
34 nombre: $nombre,
35 archivo: $archivo
36 );
37 return $producto;
38 }
39}
40

9. srv / bd / productoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Producto.php";
5require_once __DIR__ . "/Bd.php";
6
7function productoConsulta()
8{
9 $con = Bd::getConexion();
10 $stmt = $con->query(
11 "SELECT
12 P.PROD_ID AS prodId,
13 P.PROD_NOMBRE AS prodNombre,
14 A.ARCH_ID AS archId
15 FROM PRODUCTO P
16 LEFT JOIN ARCHIVO A
17 ON P.ARCH_ID = A.ARCH_ID
18 ORDER BY P.PROD_NOMBRE"
19 );
20 $resultado = $stmt->fetchAll(PDO::FETCH_OBJ);
21 return recibeFetchAll($resultado);
22}
23

10. srv / bd / productoElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/archivoElimina.php";
5require_once __DIR__ . "/productoBusca.php";
6
7function productoElimina(int $id)
8{
9 $con = Bd::getConexion();
10 $con->beginTransaction();
11 $modelo = productoBusca($id);
12 if ($modelo === false) {
13 $con->rollBack();
14 } else {
15 archivoElimina($modelo->archivo->id);
16 $stmt = $con->prepare(
17 "DELETE FROM PRODUCTO
18 WHERE PROD_ID = :id"
19 );
20 $stmt->execute([":id" => $modelo->id]);
21 $con->commit();
22 }
23}
24

11. srv / bd / productoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Producto.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/productoBusca.php";
6require_once __DIR__ . "/archivoModifica.php";
7
8function productoModifica(Producto $modelo)
9{
10 $modelo->valida();
11 $con = Bd::getConexion();
12 $con->beginTransaction();
13 $archivo = $modelo->archivo;
14 $anterior = productoBusca($modelo->id);
15 if ($anterior === false) {
16 throw new Exception("Producto no encontrado.");
17 }
18 if ($anterior->archivo === null) {
19 throw new Exception("Falta el archivo anterior.");
20 }
21 if ($archivo === null) {
22 $archivo = $anterior->archivo;
23 $modelo->archivo = $archivo;
24 } else {
25 $archivo->id = $anterior->archivo->id;
26 archivoModifica($archivo);
27 }
28 $stmt = $con->prepare(
29 "UPDATE PRODUCTO
30 SET
31 PROD_NOMBRE = :nombre,
32 ARCH_ID = :archId
33 WHERE PROD_ID = :id"
34 );
35 $stmt->execute([
36 ":id" => $modelo->id,
37 ":nombre" => $modelo->nombre,
38 ":archId" => $archivo->id
39 ]);
40 $con->commit();
41}
42

L. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / js »

1. lib / js / confirmaEliminar.js

1export function confirmaEliminar() {
2 return confirm("Confirma la eliminación")
3}
4
5// Permite que los eventos de html usen la función.
6window["confirmaEliminar"] = confirmaEliminar

2. lib / js / imagenNuevaSeleccionada.js

1import { getDataUrlDeSeleccion } from "./muestraObjeto.js"
2
3/**
4 * @param {HTMLInputElement} input
5 * @param {HTMLImageElement} img
6 */
7export function imagenNuevaSeleccionada(input, img) {
8 return new Promise((resolve, reject) => {
9 setTimeout(async () => {
10 try {
11 const dataUrl = await getDataUrlDeSeleccion(input)
12 if (dataUrl === "") {
13 img.hidden = true
14 img.src = ""
15 } else {
16 img.hidden = false
17 img.src = dataUrl
18 }
19 resolve(true)
20 } catch (error) {
21 img.hidden = true
22 reject(error)
23 }
24 },
25 500)
26 })
27}
28
29// Permite que los eventos de html usen la función.
30window["imagenNuevaSeleccionada"] = imagenNuevaSeleccionada

3. lib / js / imagenSeleccionada.js

1import { getDataUrlDeSeleccion } from "./muestraObjeto.js"
2
3/**
4 * @param {HTMLInputElement} input
5 * @param {HTMLImageElement} img
6 */
7export async function imagenSeleccionada(input, img) {
8 try {
9 const dataUrl = await getDataUrlDeSeleccion(input)
10 if (dataUrl === '') {
11 const imagenInicial = img.dataset.inicial
12 if (imagenInicial === undefined || imagenInicial === '') {
13 img.hidden = true
14 img.src = ""
15 } else {
16 img.hidden = false
17 img.src = imagenInicial
18 }
19 } else {
20 img.hidden = false
21 img.src = dataUrl
22 }
23 } catch (error) {
24 img.hidden = true
25 throw error
26 }
27}
28
29// Permite que los eventos de html usen la función.
30window["imagenSeleccionada"] = imagenSeleccionada

4. lib / js / invocaServicio.js

1import {
2 JsonResponse, JsonResponse_Created, JsonResponse_NoContent, JsonResponse_OK
3} from "./JsonResponse.js"
4import {
5 ProblemDetails, ProblemDetails_InternalServerError
6} from "./ProblemDetails.js"
7
8/**
9 * Espera a que la promesa de un fetch termine. Si
10 * hay error, lanza una excepción. Si no hay error,
11 * interpreta la respuesta del servidor como JSON y
12 * la convierte en una literal de objeto.
13 * @param { string | Promise<Response> } servicio
14 */
15export async function invocaServicio(servicio) {
16 let f = servicio
17 if (typeof servicio === "string") {
18 f = fetch(servicio, {
19 headers: { "Accept": "application/json, application/problem+json" }
20 })
21 } else if (!(f instanceof Promise)) {
22 throw new Error("Servicio de tipo incorrecto.")
23 }
24 const respuesta = await f
25 if (respuesta.ok) {
26 if (respuesta.status === JsonResponse_NoContent) {
27 return new JsonResponse(JsonResponse_NoContent)
28 }
29 const texto = await respuesta.text()
30 try {
31 const body = JSON.parse(texto)
32 if (respuesta.status === JsonResponse_Created) {
33 const location = respuesta.headers.get("location")
34 return new JsonResponse(JsonResponse_Created, body,
35 location === null ? undefined : location)
36 } else {
37 return new JsonResponse(JsonResponse_OK, body)
38 }
39 } catch (error) {
40 // El contenido no es JSON. Probablemente sea texto.
41 throw new ProblemDetails(ProblemDetails_InternalServerError,
42 "Problema interno en el servidor.", texto)
43 }
44 } else {
45 const texto = await respuesta.text()
46 try {
47 const { type, title, detail } = JSON.parse(texto)
48 throw new ProblemDetails(respuesta.status,
49 typeof title === "string" ? title : "",
50 typeof detail === "string" ? detail : undefined,
51 typeof type === "string" ? type : undefined)
52 } catch (error) {
53 if (error instanceof ProblemDetails) {
54 throw error
55 } else {
56 // El contenido no es JSON. Probablemente sea texto.
57 throw new ProblemDetails(respuesta.status, respuesta.statusText, texto)
58 }
59 }
60 }
61}
62
63// Permite que los eventos de html usen la función.
64window["invocaServicio"] = invocaServicio

5. lib / js / JsonResponse.js

1export const JsonResponse_OK = 200
2export const JsonResponse_Created = 201
3export const JsonResponse_NoContent = 204
4
5export class JsonResponse {
6
7 /**
8 * @param {number} status
9 * @param {any} [body]
10 * @param {string} [location]
11 */
12 constructor(status, body, location) {
13 /** @readonly */
14 this.status = status
15 /** @readonly */
16 this.body = body
17 /** @readonly */
18 this.location = location
19 }
20
21}

6. lib / js / muestraError.js

1import { ProblemDetails } from "./ProblemDetails.js"
2
3/**
4 * Muestra un error en la consola y en un cuadro de
5 * alerta el mensaje de una excepción.
6 * @param { ProblemDetails | Error | null } error descripción del error.
7 */
8export function muestraError(error) {
9 if (error === null) {
10 console.log("Error")
11 alert("Error")
12 } else if (error instanceof ProblemDetails) {
13 let mensaje = error.title
14 if (error.detail) {
15 mensaje += `\n\n${error.detail}`
16 }
17 mensaje += `\n\nCódigo: ${error.status}`
18 if (error.type) {
19 mensaje += ` ${error.type}`
20 }
21 console.error(mensaje)
22 console.error(error)
23 alert(mensaje)
24 } else {
25 console.error(error)
26 alert(error.message)
27 }
28}
29
30// Permite que los eventos de html usen la función.
31window["muestraError"] = muestraError

7. lib / js / muestraObjeto.js

1/**
2 * @param { Document | HTMLElement } raizHtml
3 * @param { any } objeto
4 */
5export async function muestraObjeto(raizHtml, objeto) {
6 for (const [nombre, definiciones] of Object.entries(objeto)) {
7 if (Array.isArray(definiciones)) {
8 muestraArray(raizHtml, nombre, definiciones)
9 } else if (definiciones !== undefined && definiciones !== null) {
10 const elementoHtml = buscaElementoHtml(raizHtml, nombre)
11 if (elementoHtml instanceof HTMLImageElement) {
12 await muestraImagen(raizHtml, elementoHtml, definiciones)
13 } else if (elementoHtml !== null) {
14 for (const [atributo, valor] of Object.entries(definiciones)) {
15 if (atributo in elementoHtml) {
16 elementoHtml[atributo] = valor
17 }
18 }
19 }
20 }
21 }
22}
23// Permite que los eventos de html usen la función.
24window["muestraObjeto"] = muestraObjeto
25
26/**
27 * @param { Document | HTMLElement } raizHtml
28 * @param { string } nombre
29 */
30export function buscaElementoHtml(raizHtml, nombre) {
31 return raizHtml.querySelector(
32 `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`)
33}
34
35/**
36 * @param { Document | HTMLElement } raizHtml
37 * @param { string } propiedad
38 * @param {any[]} valores
39 */
40function muestraArray(raizHtml, propiedad, valores) {
41 const conjunto = new Set(valores)
42 const elementos =
43 raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`)
44 if (elementos.length === 1) {
45 const elemento = elementos[0]
46 if (elemento instanceof HTMLSelectElement) {
47 const options = elemento.options
48 for (let i = 0, len = options.length; i < len; i++) {
49 const option = options[i]
50 option.selected = conjunto.has(option.value)
51 }
52 return
53 }
54 }
55 for (let i = 0, len = elementos.length; i < len; i++) {
56 const elemento = elementos[i]
57 if (elemento instanceof HTMLInputElement) {
58 elemento.checked = conjunto.has(elemento.value)
59 }
60 }
61}
62
63/**
64 * @param { Document | HTMLElement } raizHtml
65 * @param { HTMLImageElement } img
66 * @param { any } definiciones
67 */
68async function muestraImagen(raizHtml, img, definiciones) {
69 const input = getInputParaElementoHtml(raizHtml, img)
70 const src = definiciones.src
71 if (src !== undefined) {
72 img.dataset.inicial = src
73 if (input === null) {
74 img.src = src
75 if (src === "") {
76 img.hidden = true
77 } else {
78 img.hidden = false
79 }
80 } else {
81 const dataUrl = await getDataUrlDeSeleccion(input)
82 if (dataUrl !== "") {
83 img.hidden = false
84 img.src = dataUrl
85 } else if (src === "") {
86 img.src = ""
87 img.hidden = true
88 } else {
89 img.src = src
90 img.hidden = false
91 }
92 }
93 }
94 for (const [atributo, valor] of Object.entries(definiciones)) {
95 if (atributo !== "src" && atributo in img) {
96 img[atributo] = valor
97 }
98 }
99}
100
101/**
102 * @param { HTMLInputElement } input
103 */
104export function getArchivoSeleccionado(input) {
105 const seleccion = input.files
106 if (seleccion === null || seleccion.length === 0) {
107 return null
108 } else {
109 return seleccion.item(0)
110 }
111}
112// Permite que los eventos de html usen la función.
113window["getArchivoSeleccionado"] = getArchivoSeleccionado
114
115
116/**
117 * @param {HTMLInputElement} input
118 * @returns {Promise<string>}
119 */
120export function getDataUrlDeSeleccion(input) {
121 return new Promise((resolve, reject) => {
122 try {
123 const seleccion = getArchivoSeleccionado(input)
124 if (seleccion === null) {
125 resolve("")
126 } else {
127 const reader = new FileReader()
128 reader.onload = () => {
129 const dataUrl = reader.result
130 if (typeof dataUrl === "string") {
131 resolve(dataUrl)
132 } else {
133 resolve("")
134 }
135 }
136 reader.onerror = () => reject(reader.error)
137 reader.readAsDataURL(seleccion)
138 }
139 } catch (error) {
140 resolve(error)
141 }
142 })
143}
144// Permite que los eventos de html usen la función.
145window["getDataUrlDeSeleccion"] = getDataUrlDeSeleccion
146
147/**
148 * @param { Document | HTMLElement } raizHtml
149 * @param { HTMLElement } elementoHtml
150 */
151export function getInputParaElementoHtml(raizHtml, elementoHtml) {
152 const inputId = elementoHtml.getAttribute("data-input")
153 if (inputId === null) {
154 return null
155 } else {
156 const input = buscaElementoHtml(raizHtml, inputId)
157 if (input instanceof HTMLInputElement) {
158 return input
159 } else {
160 return null
161 }
162 }
163}

8. lib / js / ProblemDetails.js

1export const ProblemDetails_BadRequest = 400
2export const ProblemDetails_NotFound = 404
3export const ProblemDetails_InternalServerError = 500
4
5export class ProblemDetails extends Error {
6
7 /**
8 * @param {number} status
9 * @param {string} title
10 * @param {string} [detail]
11 * @param {string} [type]
12 */
13 constructor(status, title, detail, type) {
14 super(title)
15 /** @readonly */
16 this.status = status
17 /** @readonly */
18 this.type = type
19 /** @readonly */
20 this.title = title
21 /** @readonly */
22 this.detail = detail
23 }
24
25}

9. lib / js / submitForm.js

1import { invocaServicio } from "./invocaServicio.js"
2
3/**
4 * Envía los datos de la forma a la url usando la codificación
5 * multipart/form-data.
6 * @param {string} url
7 * @param {Event} event
8 * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS"
9 * | "CONNECT" | "HEAD" } metodoHttp
10 */
11export function submitForm(url, event, metodoHttp = "POST") {
12 event.preventDefault()
13 const form = event.target
14 if (!(form instanceof HTMLFormElement))
15 throw new Error("event.target no es una form.")
16 return invocaServicio(fetch(url, {
17 method: metodoHttp,
18 headers: { "Accept": "application/json, application/problem+json" },
19 body: new FormData(form)
20 }))
21}
22
23// Permite que los eventos de html usen la función.
24window["submitForm"] = submitForm

B. Carpeta « lib / php »

1. lib / php / ejecutaServicio.php

1<?php
2
3require_once __DIR__ . "/JsonResponse.php";
4require_once __DIR__ . "/ProblemDetails.php";
5
6/**
7 * Ejecuta una funcion que implementa un servicio.
8 */
9function ejecutaServicio($servicio)
10{
11 try {
12 $resultado = $servicio();
13 if (!($resultado instanceof JsonResponse)) {
14 $resultado = JsonResponse::ok($resultado);
15 }
16 procesa_json_response($resultado);
17 } catch (ProblemDetails $details) {
18 procesa_problem_details($details);
19 } catch (Throwable $throwable) {
20 procesa_problem_details(new ProblemDetails(
21 status: ProblemDetails::InternalServerError,
22 type: "/error/errorinterno.html",
23 title: "Error interno del servidor.",
24 detail: $throwable->getMessage()
25 ));
26 }
27}
28
29function procesa_json_response(JsonResponse $response)
30{
31 $json = "";
32 $body = $response->body;
33 if ($response->status !== JsonResponse_NoContent) {
34 $json = json_encode($body);
35 if ($json === false) {
36 no_puede_generar_json();
37 return;
38 }
39 }
40 http_response_code($response->status);
41 if ($response->location !== null) {
42 header("Location: {$response->location}");
43 }
44 if ($response->status !== JsonResponse_NoContent) {
45 header("Content-Type: application/json");
46 echo $json;
47 }
48}
49
50function procesa_problem_details(ProblemDetails $details)
51{
52 $body = ["title" => $details->title];
53 if ($details->type !== null) {
54 $body["type"] = $details->type;
55 }
56 if ($details->detail !== null) {
57 $body["detail"] = $details->detail;
58 }
59 $json = json_encode($body);
60 if ($json === false) {
61 no_puede_generar_json();
62 } else {
63 http_response_code($details->status);
64 header("Content-Type: application/problem+json");
65 echo $json;
66 }
67}
68
69function no_puede_generar_json()
70{
71 http_response_code(ProblemDetails::InternalServerError);
72 header("Content-Type: application/problem+json");
73 echo '{"type":"/error/nojson.html"'
74 . ',"title":"El valor devuelto no puede representarse como JSON."}';
75}
76

2. lib / php / JsonResponse.php

1<?php
2
3const JsonResponse_OK = 200;
4const JsonResponse_Created = 201;
5const JsonResponse_NoContent = 204;
6
7class JsonResponse
8{
9
10 public int $status;
11 public $body;
12 public ?string $location;
13
14 public function __construct(
15 int $status = JsonResponse_OK,
16 $body = null,
17 ?string $location = null
18 ) {
19 $this->status = $status;
20 $this->body = $body;
21 $this->location = $location;
22 }
23
24 public static function ok($body)
25 {
26 return new JsonResponse(body: $body);
27 }
28
29 public static function created(string $location, $body)
30 {
31 return new JsonResponse(JsonResponse_Created, $body, $location);
32 }
33
34 public static function noContent()
35 {
36 return new JsonResponse(JsonResponse_NoContent, null);
37 }
38}
39

3. lib / php / leeBytes.php

1<?php
2
3function leeBytes(string $parametro): string
4{
5 /* Si el archivo se recibió y contiene bytes, los recupera;
6 * en caso contrario, devuelve false. */
7 $contents = isset($_FILES[$parametro]) && $_FILES[$parametro]["size"] > 0
8 ? file_get_contents($_FILES[$parametro]["tmp_name"])
9 : false;
10 return $contents === false
11 ? ""
12 : $contents;
13}
14

4. lib / php / leeEntero.php

1<?php
2
3require_once __DIR__ . "/leeTexto.php";
4
5/**
6 * Devuelve el valor entero de un parámetro
7 * recibido en el servidor por medio de GET,
8 * POST o cookie. Si parámetro no se puede
9 * convertir a entero, se genera un error.
10 * Si el parámetro no se recibe, devuelve
11 * null.
12 * Para que las llaves foráneas fucionen bien,
13 * si se recibe una cadena vacía, se devuelve
14 * null.
15 */
16function leeEntero(string $parametro): ?int
17{
18 $valor = leeTexto($parametro);
19 return $valor === null || $valor === ""
20 ? null
21 : trim($valor);
22}
23

5. lib / php / leeTexto.php

1<?php
2
3/**
4 * Recupera el texto de un parámetro enviado al
5 * servidor por medio de GET, POST o cookie.
6 * Si el parámetro no se recibe, devuelve null.
7 */
8function leeTexto(string $parametro): ?string
9{
10 /* Si el parámetro está asignado en $_REQUEST,
11 * devuelve su valor; de lo contrario,
12 * devuelve null. */
13 $valor = isset($_REQUEST[$parametro])
14 ? $_REQUEST[$parametro]
15 : null;
16 return $valor;
17}
18

6. lib / php / pdFaltaId.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function pdFaltaId()
6{
7 return new ProblemDetails(
8 status: ProblemDetails::BadRequest,
9 type: "/error/faltaid.html",
10 title: "No se ha proporcionado el valor de id.",
11 );
12}
13

7. lib / php / ProblemDetails.php

1<?php
2
3class ProblemDetails extends Exception
4{
5
6 public const BadRequest = 400;
7 public const NotFound = 404;
8 public const InternalServerError = 500;
9
10 public int $status;
11 public string $title;
12 public ?string $type;
13 public ?string $detail;
14
15 public function __construct(
16 int $status,
17 string $title,
18 ?string $type = null,
19 ?string $detail = null,
20 Throwable $previous = null
21 ) {
22 parent::__construct($title, $status, $previous);
23 $this->status = $status;
24 $this->type = $type;
25 $this->title = $title;
26 $this->detail = $detail;
27 }
28}
29

8. lib / php / recibeFetchAll.php

1<?php
2
3function recibeFetchAll(false|array $resultado): array
4{
5 if ($resultado === false) {
6 return [];
7 } else {
8 return $resultado;
9 }
10}
11

9. lib / php / validaNombre.php

1<?php
2
3require_once __DIR__ . "/ProblemDetails.php";
4
5function validaNombre(string $nombre)
6{
7 if ($nombre === "")
8 throw new ProblemDetails(
9 status: ProblemDetails::BadRequest,
10 type: "/error/faltanombre.html",
11 title: "Falta el nombre.",
12 );
13}
14

M. Carpeta « error »

Versión para imprimir.

A. error / amigonoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Amigo no encontrada</title>
10
11</head>
12
13<body>
14
15 <h1>Amigo no encontrada</h1>
16
17 <p>No se encontró ningún amigo con el id solicitado.</p>
18
19</body>
20
21</html>

B. error / archivovacio.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Archivo vacío</title>
10
11</head>
12
13<body>
14
15 <h1>Archivo vacío</h1>
16
17 <p>Selecciona un archivo que no esté vacío.</p>
18
19</body>
20
21</html>

C. error / errorinterno.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Error interno del servidor</title>
10
11</head>
12
13<body>
14
15 <h1>Error interno del servidor</h1>
16
17 <p>Se presentó de forma inesperada un error interno del servidor.</p>
18
19</body>
20
21</html>

D. error / faltaarchivo.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el archivo</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el archivo</h1>
16
17 <p>Selecciona un archivo que no esté vacío.</p>
18
19</body>
20
21</html>

E. error / faltaid.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el id</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el id</h1>
16
17 <p>No se ha proporcionado el valor de id.</p>
18
19</body>
20
21</html>

F. error / faltanombre.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Falta el nombre</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el nombre</h1>
16
17</body>
18
19</html>

G. error / nojson.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>El valor devuelto no puede representarse como JSON</title>
10
11</head>
12
13<body>
14
15 <h1>El valor devuelto no puede representarse como JSON</h1>
16
17 <p>
18 Debido a un error interno del servidor, la respuesta generada, no se puede
19 recuperar.
20 </p>
21
22</body>
23
24</html>

H. error / productonoencontrado.html

1<!DOCTYPE html>
2<html lang="es">
3
4<head>
5
6 <meta charset="UTF-8">
7 <meta name="viewport" content="width=device-width">
8
9 <title>Producto no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Producto no encontrado</h1>
16
17 <p>No se encontró ningún producto con el id solicitado.</p>
18
19</body>
20
21</html>

N. jsconfig.json

1{
2 "compilerOptions": {
3 "checkJs": true,
4 "strictNullChecks": true,
5 "target": "ES6",
6 "module": "ES6",
7 "moduleResolution": "classic",
8 "lib": [
9 "ES2017",
10 "WebWorker",
11 "DOM"
12 ]
13 }
14}

O. Resumen