19. Relaciones a uno 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/srvauno?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>Relaciones a uno</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('srv/srvAmigoConsulta.php')
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Relaciones a uno</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/muestraObjeto.js"></script>
14 <script type="module" src="lib/js/muestraError.js"></script>
15
16</head>
17
18<body onload="invocaServicio('srv/srvPasatiempoOptions.php')
19 .then(options => muestraObjeto(document, options.body))
20 .catch(muestraError)">
21
22 <form onsubmit="submitForm('srv/srvAmigoAgrega.php', event)
23 .then(modelo => location.href = 'index.html')
24 .catch(muestraError)">
25
26 <h1>Agregar</h1>
27
28 <p><a href="index.html">Cancelar</a></p>
29
30 <p>
31 <label>
32 Nombre *
33 <input name="nombre">
34 </label>
35 </p>
36
37 <p>
38 <label>
39 Pasatiempo
40 <select name="pasId">
41 <option value="">Cargando…</option>
42 </select>
43 </label>
44 </p>
45
46 <p>* Obligatorio</p>
47
48 <p><button type="submit">Agregar</button></p>
49
50 </form>
51
52</body>
53
54</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
17 <script>
18 // Obtiene los parámetros de la página.
19 const params = new URL(location.href).searchParams
20 </script>
21
22</head>
23
24<body onload="if (params.size > 0) {
25 invocaServicio('srv/srvPasatiempoOptions.php')
26 .then(async options => {
27 const modelo = await invocaServicio('srv/srvAmigoBusca.php?' + params)
28 await muestraObjeto(document, options.body)
29 await muestraObjeto(document, modelo.body)
30 })
31 .catch(muestraError)
32 }">
33
34 <form onsubmit="submitForm('srv/srvAmigoModifica.php', event)
35 .then(modelo => location.href = 'index.html')
36 .catch(muestraError)">
37
38 <h1>Modificar</h1>
39
40 <p><a href="index.html">Cancelar</a></p>
41
42 <input type="hidden" name="id">
43
44 <p>
45 <label>
46 Nombre *
47 <input name="nombre" value="Cargando…">
48 </label>
49 </p>
50
51 <p>
52 <label>
53 Pasatiempo
54 <select name="pasId">
55 <option value="">Cargando…</option>
56 </select>
57 </label>
58 </p>
59
60 <p>* Obligatorio</p>
61
62 <p>
63
64 <button type="submit">Guardar</button>
65
66 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
67 invocaServicio('srv/srvAmigoElimina.php?' + params)
68 .then(() => location.href = 'index.html')
69 .catch(muestraError)
70 }">
71 Eliminar
72 </button>
73
74 </p>
75
76 </form>
77
78</body>
79
80</html>

K. Carpeta « srv »

Versión para imprimir.

A. Carpeta « srv / modelo »

1. srv / modelo / Amigo.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/validaNombre.php";
4
5class Amigo
6{
7
8 public int $id;
9 public string $nombre;
10 public ?Pasatiempo $pasatiempo;
11
12 public function __construct(
13 string $nombre = "",
14 ?Pasatiempo $pasatiempo = null,
15 int $id = 0
16 ) {
17 $this->id = $id;
18 $this->nombre = $nombre;
19 $this->pasatiempo = $pasatiempo;
20 }
21
22 public function valida()
23 {
24 validaNombre($this->nombre);
25 }
26}
27

2. srv / modelo / Pasatiempo.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/validaNombre.php";
4
5class Pasatiempo
6{
7
8 public int $id;
9 public string $nombre;
10
11 public function __construct(string $nombre = "", int $id = 0)
12 {
13 $this->id = $id;
14 $this->nombre = $nombre;
15 }
16
17 public function valida()
18 {
19 validaNombre($this->nombre);
20 }
21}
22

B. srv / srvAmigoAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeEntero.php";
6require_once __DIR__ . "/../lib/php/leeTexto.php";
7require_once __DIR__ . "/modelo/Amigo.php";
8require_once __DIR__ . "/modelo/Pasatiempo.php";
9require_once __DIR__ . "/bd/amigoAgrega.php";
10
11ejecutaServicio(function () {
12 $nombre = leeTexto("nombre");
13 $pasId = leeEntero("pasId");
14 $pasatiempo = $pasId === null
15 ? null
16 : new Pasatiempo(id: $pasId);
17 $modelo = new Amigo(
18 nombre: $nombre === null ? "" : trim($nombre),
19 pasatiempo: $pasatiempo
20 );
21 amigoAgrega($modelo);
22 $id = htmlentities($modelo->id);
23 $pasatiempo = $modelo->pasatiempo;
24 return JsonResponse::created("/srv/srvAmigoBusca.php?id=$id", [
25 "id" => ["value" => $modelo->id],
26 "nombre" => ["value" => $modelo->nombre],
27 "pasId" => ["value" => $pasatiempo === null ? "" : $pasatiempo->id]
28 ]);
29});
30

C. srv / srvAmigoBusca.php

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

D. srv / srvAmigoConsulta.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/amigoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = amigoConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $amiId = htmlentities($modelo->amiId);
11 $amiNombre = htmlentities($modelo->amiNombre);
12 $pasNombre = $modelo->pasNombre === null
13 ? "<em>-- Sin pasatiempo --</em>"
14 : htmlentities($modelo->pasNombre);
15 $render .=
16 "<dt><a href='modifica.html?id=$amiId'>$amiNombre</a></dt>
17 <dd><a href='modifica.html?id=$amiId'>$pasNombre</a></dd>";
18 }
19 return ["lista" => ["innerHTML" => $render]];
20});
21

E. srv / srvAmigoElimina.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/amigoElimina.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 amigoElimina($id);
13 return JsonResponse::noContent();
14});
15

F. srv / srvAmigoModifica.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/leeTexto.php";
7require_once __DIR__ . "/modelo/Amigo.php";
8require_once __DIR__ . "/modelo/Pasatiempo.php";
9require_once __DIR__ . "/bd/amigoModifica.php";
10
11ejecutaServicio(function () {
12 $id = leeEntero("id");
13 if ($id === null) throw pdFaltaId();
14 $nombre = leeTexto("nombre");
15 $pasId = leeEntero("pasId");
16 $pasatiempo = $pasId === null
17 ? null
18 : new Pasatiempo(id: $pasId);
19 $modelo = new Amigo(
20 nombre: $nombre === null ? "" : trim($nombre),
21 pasatiempo: $pasatiempo,
22 id: $id
23 );
24 amigoModifica($modelo);
25 return [
26 "id" => ["value" => $modelo->id],
27 "nombre" => ["value" => $modelo->nombre],
28 "pasId" => ["value" => $pasatiempo === null ? "" : $pasatiempo->id]
29 ];
30});
31

G. srv / srvPasatiempoOptions.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/pasatiempoConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = pasatiempoConsulta();
8 $render = "<option value=''>-- Sin pasatiempo --</option>";
9 foreach ($lista as $modelo) {
10 $id = htmlentities($modelo->id);
11 $nombre = htmlentities($modelo->nombre);
12 $render .= "<option value='$id'>{$nombre}</option>";
13 }
14 return ["pasId" => ["innerHTML" => $render]];
15});
16

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 PASATIEMPO (
7 PAS_ID INTEGER,
8 PAS_NOMBRE TEXT NOT NULL,
9 CONSTRAINT PAS_PK
10 PRIMARY KEY(PAS_ID),
11 CONSTRAINT PAS_NOM_UNQ
12 UNIQUE(PAS_NOMBRE)
13 )'
14 );
15 $con->exec(
16 'CREATE TABLE IF NOT EXISTS AMIGO (
17 AMI_ID INTEGER,
18 AMI_NOMBRE TEXT NOT NULL,
19 PAS_ID INTEGER,
20 CONSTRAINT AMI_PK
21 PRIMARY KEY(AMI_ID),
22 CONSTRAINT AMI_NOM_UNQ
23 UNIQUE(AMI_NOMBRE)
24 CONSTRAINT AMI_PAS_FK
25 FOREIGN KEY (PAS_ID) REFERENCES PASATIEMPO(PAS_ID)
26 )'
27 );
28}
29

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../modelo/Pasatiempo.php";
4require_once __DIR__ . "/bdCrea.php";
5require_once __DIR__ . "/pasatiempoConsulta.php";
6require_once __DIR__ . "/pasatiempoAgrega.php";
7
8class Bd
9{
10
11 private static ?PDO $conexion = null;
12
13 static function getConexion(): PDO
14 {
15 if (self::$conexion === null) {
16
17 self::$conexion = new PDO(
18 // cadena de conexión
19 "sqlite:srvauno.db",
20 // usuario
21 null,
22 // contraseña
23 null,
24 // Opciones: conexiones persistentes y lanza excepciones.
25 [PDO::ATTR_PERSISTENT => true, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
26 );
27
28 bdCrea(self::$conexion);
29 $pasatiempo = pasatiempoConsulta();
30 if (count($pasatiempo) === 0) {
31 $pasatiempo = new Pasatiempo(nombre: "Futbol");
32 pasatiempoAgrega($pasatiempo);
33
34 $pasatiempo = new Pasatiempo(nombre: "Videojuegos");
35 pasatiempoAgrega($pasatiempo);
36 }
37 }
38
39 return self::$conexion;
40 }
41}
42

3. srv / bd / amigoAgrega.php

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

4. srv / bd / amigoBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Amigo.php";
4require_once __DIR__ . "/../modelo/Pasatiempo.php";
5require_once __DIR__ . "/Bd.php";
6
7function amigoBusca(int $amiId)
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 A.AMI_ID AS amiId,
13 A.AMI_NOMBRE AS amiNombre,
14 A.PAS_ID AS pasId,
15 P.PAS_NOMBRE AS pasNombre
16 FROM AMIGO A
17 LEFT JOIN PASATIEMPO P
18 ON A.PAS_ID = P.PAS_ID
19 WHERE A.AMI_ID = :amiId"
20 );
21 $stmt->execute([":amiId" => $amiId]);
22 $stmt->setFetchMode(PDO::FETCH_OBJ);
23 $obj = $stmt->fetch();
24 if ($obj === false) {
25 return false;
26 } else {
27 $pasatiempo = $obj->pasId === null
28 ? null
29 : new Pasatiempo(nombre: $obj->pasNombre, id: $obj->pasId);
30 $amigo = new Amigo(
31 nombre: $obj->amiNombre,
32 pasatiempo: $pasatiempo,
33 id: $obj->amiId
34 );
35 return $amigo;
36 }
37}
38

5. srv / bd / amigoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/Bd.php";
5
6function amigoConsulta()
7{
8 $con = Bd::getConexion();
9 $stmt = $con->query(
10 "SELECT
11 A.AMI_ID AS amiId,
12 A.AMI_NOMBRE AS amiNombre,
13 P.PAS_NOMBRE AS pasNombre
14 FROM AMIGO A
15 LEFT JOIN PASATIEMPO P
16 ON A.PAS_ID = P.PAS_ID
17 ORDER BY A.AMI_NOMBRE"
18 );
19 $resultado = $stmt->fetchAll(PDO::FETCH_OBJ);
20 return recibeFetchAll($resultado);
21}
22

6. srv / bd / amigoElimina.php

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

7. srv / bd / amigoModifica.php

1<?php
2
3require_once __DIR__ . "/../modelo/Amigo.php";
4require_once __DIR__ . "/Bd.php";
5
6function amigoModifica(Amigo $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "UPDATE AMIGO
12 SET
13 AMI_NOMBRE = :nombre,
14 PAS_ID = :pasId
15 WHERE AMI_ID = :id"
16 );
17 $stmt->execute([
18 ":id" => $modelo->id,
19 ":nombre" => $modelo->nombre,
20 ":pasId" => $modelo->pasatiempo === null
21 ? null
22 : $modelo->pasatiempo->id
23 ]);
24}
25

8. srv / bd / pasatiempoAgrega.php

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

9. srv / bd / pasatiempoConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Pasatiempo.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Pasatiempo[] */
8function pasatiempoConsulta()
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 PAS_ID AS id,
14 PAS_NOMBRE AS nombre
15 FROM PASATIEMPO
16 ORDER BY PAS_NOMBRE"
17 );
18 $resultado = $stmt->fetchAll(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Pasatiempo::class
21 );
22 return recibeFetchAll($resultado);
23}
24

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

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

4. 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

5. 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}

6. 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}

7. 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 / 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

4. 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

5. 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

6. 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

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

8. 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 / 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>

C. 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>

D. 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>

E. 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>

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