20. Relaciones a muchos - Usuarios y roles

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/srvamuchos?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 muchos</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/srvUsuarioConsulta.php')
18 .then(render => muestraObjeto(document, render.body))
19 .catch(muestraError)">
20
21 <h1>Relaciones a muchos</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/srvRolCheckBoxes.php')
19 .then(chackBoxes => muestraObjeto(document, chackBoxes.body))
20 .catch(muestraError)">
21
22 <form onsubmit="submitForm('srv/srvUsuarioAgrega.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 <!-- Usamos cue para que los navegadores no bloqueen la página. -->
33 Cue *
34 <input name="cue">
35 </label>
36 </p>
37
38 <fieldset>
39
40 <legend>Roles</legend>
41
42 <div id="roles">
43 <progress max="100">Cargando…</progress>
44 </div>
45
46 </fieldset>
47
48 <p>* Obligatorio</p>
49
50 <p><button type="submit">Agregar</button></p>
51
52 </form>
53
54</body>
55
56</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/srvRolCheckBoxes.php')
26 .then(async checkBoxes => {
27 const modelo = await invocaServicio('srv/srvUsuarioBusca.php?' + params)
28 await muestraObjeto(document, checkBoxes.body)
29 await muestraObjeto(document, modelo.body)
30 })
31 .catch(muestraError)
32 }">
33
34 <form onsubmit="submitForm('srv/srvUsuarioModifica.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 <!-- Usamos cue para que los navegadores no bloqueen la página. -->
47 Cue *
48 <input name="cue" value="Cargando…">
49 </label>
50 </p>
51
52 <fieldset>
53 <legend>Roles</legend>
54
55 <div id="roles">
56 <progress max="100">Cargando…</progress>
57 </div>
58
59 </fieldset>
60
61 <p>* Obligatorio</p>
62
63 <p>
64
65 <button type="submit">Guardar</button>
66
67 <button type="button" onclick="if (params.size > 0 && confirmaEliminar()) {
68 invocaServicio('srv/srvUsuarioElimina.php?' + params)
69 .then(() => location.href = 'index.html')
70 .catch(muestraError)
71 }">
72 Eliminar
73 </button>
74
75 </p>
76
77 </form>
78
79</body>
80
81</html>

K. Carpeta « srv »

Versión para imprimir.

A. Carpeta « srv / modelo »

1. srv / modelo / Rol.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4
5class Rol
6{
7
8 public string $id;
9 public string $descripcion;
10
11 public function __construct(string $descripcion = "", string $id = "")
12 {
13 $this->id = $id;
14 $this->descripcion = $descripcion;
15 }
16
17 public function valida()
18 {
19
20 if ($this->id === "")
21 throw new ProblemDetails(
22 status: ProblemDetails::BadRequest,
23 type: "/error/faltaid.html",
24 title: "Falta el id.",
25 );
26
27 if ($this->descripcion === "")
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/faltadescripcion.html",
31 title: "Falta la descripción.",
32 );
33 }
34}
35

2. srv / modelo / Usuario.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/ProblemDetails.php";
4require_once __DIR__ . "/Rol.php";
5
6class Usuario
7{
8
9 public int $id;
10 public string $cue;
11 /** @var Rol[] */
12 public array $roles;
13
14 public function __construct(
15 string $cue = "",
16 array $roles = [],
17 int $id = 0
18 ) {
19 $this->id = $id;
20 $this->cue = $cue;
21 $this->roles = $roles;
22 }
23
24 public function valida()
25 {
26
27 if ($this->cue === "")
28 throw new ProblemDetails(
29 status: ProblemDetails::BadRequest,
30 type: "/error/faltacue.html",
31 title: "Falta el cue.",
32 );
33
34 foreach ($this->roles as $rol) {
35 if (!($rol instanceof Rol))
36 throw new ProblemDetails(
37 status: ProblemDetails::BadRequest,
38 type: "/error/rolincorrecto.html",
39 title: "Tipo incorrecto para un rol.",
40 );
41 }
42 }
43}
44

B. srv / srvRolCheckBoxes.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/bd/rolConsulta.php";
5
6ejecutaServicio(function () {
7 $lista = rolConsulta();
8 $render = "";
9 foreach ($lista as $modelo) {
10 $id = htmlentities($modelo->id);
11 $descripcion = htmlentities($modelo->descripcion);
12 $render .=
13 "<p>
14 <label style='display: flex'>
15 <input type='checkbox' name='rolIds[]' value='$id'>
16 <span>
17 <strong>$id</strong>
18 <br>$descripcion
19 </span>
20 </label>
21 </p>";
22 }
23 return ["roles" => ["innerHTML" => $render]];
24});
25

C. srv / srvUsuarioAgrega.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/JsonResponse.php";
5require_once __DIR__ . "/../lib/php/leeTexto.php";
6require_once __DIR__ . "/../lib/php/leeArray.php";
7require_once __DIR__ . "/modelo/Rol.php";
8require_once __DIR__ . "/modelo/Usuario.php";
9require_once __DIR__ . "/bd/usuarioAgrega.php";
10
11ejecutaServicio(function () {
12 $cue = leeTexto("cue");
13 $rolIds = leeArray("rolIds");
14 /** @var Rol[] $roles */
15 $roles = [];
16 if ($rolIds !== null) {
17 foreach ($rolIds as $rolId) {
18 $roles[] = new Rol(id: $rolId);
19 }
20 }
21 $modelo = new Usuario(cue: $cue === null ? "" : trim($cue), roles: $roles);
22 usuarioAgrega($modelo);
23 $id = htmlentities($modelo->id);
24 return JsonResponse::created("/srv/srvUsuarioBusca.php?id=$id", $modelo);
25});
26

D. srv / srvUsuarioBusca.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/usuarioBusca.php";
8
9ejecutaServicio(function () {
10 $id = leeEntero("id");
11 if ($id === null) throw pdFaltaId();
12 $modelo = usuarioBusca($id);
13 if ($modelo === false) {
14 $htmlId = htmlentities($id);
15 throw new ProblemDetails(
16 status: ProblemDetails::NotFound,
17 type: "/error/usuarionoencontrado.html",
18 title: "Usuario no encontrado.",
19 detail: "No se encontró ningún usuario con el id $htmlId.",
20 );
21 }
22 $rolIds = [];
23 foreach ($modelo->roles as $rol) {
24 $rolIds[] = $rol->id;
25 }
26 return [
27 "id" => ["value" => $modelo->id],
28 "cue" => ["value" => $modelo->cue],
29 "rolIds[]" => $rolIds
30 ];
31});
32

E. srv / srvUsuarioConsulta.php

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

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

G. srv / srvUsuarioModifica.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__ . "/../lib/php/leeArray.php";
8require_once __DIR__ . "/modelo/Usuario.php";
9require_once __DIR__ . "/bd/usuarioModifica.php";
10
11ejecutaServicio(function () {
12 $id = leeEntero("id");
13 if ($id === null) throw pdFaltaId();
14 $cue = leeTexto("cue");
15 $rolIds = leeArray("rolIds");
16 /** @var Rol[] $roles */
17 $roles = [];
18 if ($rolIds !== null) {
19 foreach ($rolIds as $rolId) {
20 $roles[] = new Rol(id: $rolId);
21 }
22 }
23 $usuario = new Usuario(
24 cue: $cue === null ? "" : trim($cue),
25 roles: $roles,
26 id: $id
27 );
28 usuarioModifica($usuario);
29 return $usuario;
30});
31

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 USUARIO (
7 USU_ID INTEGER,
8 USU_CUE TEXT NOT NULL,
9 CONSTRAINT USU_PK
10 PRIMARY KEY(USU_ID),
11 CONSTRAINT USU_CUE_UNQ
12 UNIQUE(USU_CUE)
13 )'
14 );
15 $con->exec(
16 'CREATE TABLE IF NOT EXISTS ROL (
17 ROL_ID TEXT NOT NULL,
18 ROL_DESCRIPCION TEXT NOT NULL,
19 CONSTRAINT ROL_PK
20 PRIMARY KEY(ROL_ID),
21 CONSTRAINT ROL_DESCR_UNQ
22 UNIQUE(ROL_DESCRIPCION)
23 )'
24 );
25 $con->exec(
26 'CREATE TABLE IF NOT EXISTS USU_ROL (
27 USU_ID INTEGER NOT NULL,
28 ROL_ID TEXT NOT NULL,
29 CONSTRAINT USU_ROL_PK
30 PRIMARY KEY(USU_ID, ROL_ID),
31 CONSTRAINT USU_ROL_USU_FK
32 FOREIGN KEY (USU_ID) REFERENCES USUARIO(USU_ID),
33 CONSTRAINT USU_ROL_ROL_FK
34 FOREIGN KEY (ROL_ID) REFERENCES ROL(ROL_ID)
35 )'
36 );
37}
38

2. srv / bd / Bd.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/bdCrea.php";
5require_once __DIR__ . "/rolBusca.php";
6require_once __DIR__ . "/rolAgrega.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:srvamuchos.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
30 if (rolBusca("Administrador") === false) {
31 $administrador = new Rol(
32 id: "Administrador",
33 descripcion: "Administra el sistema."
34 );
35 rolAgrega($administrador);
36 }
37
38 if (rolBusca("Cliente") === false) {
39 $cliente = new Rol(
40 id: "Cliente",
41 descripcion: "Realiza compras."
42 );
43 rolAgrega($cliente);
44 }
45 }
46
47 return self::$conexion;
48 }
49}
50

3. srv / bd / rolAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/Bd.php";
5
6function rolAgrega(Rol $modelo)
7{
8 $modelo->valida();
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO ROL
12 (ROL_ID, ROL_DESCRIPCION)
13 VALUES
14 (:id, :descripcion)"
15 );
16 $stmt->execute([
17 ":id" => $modelo->id,
18 ":descripcion" => $modelo->descripcion
19 ]);
20}
21

4. srv / bd / rolBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Rol.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolConsulta.php";
6
7function rolBusca(string $id) : false|Rol
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 ROL_ID as id,
13 ROL_DESCRIPCION as descripcion
14 FROM ROL
15 WHERE ROL_ID = :id"
16 );
17 $stmt->execute([":id" => $id]);
18 $stmt->setFetchMode(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Rol::class
21 );
22 return $stmt->fetch();
23}
24

5. srv / bd / rolConsulta.php

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

6. srv / bd / usuarioAgrega.php

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

7. srv / bd / usuarioBusca.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5require_once __DIR__ . "/usuRolConsulta.php";
6
7function usuarioBusca(int $usuId)
8{
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "SELECT
12 USU_ID as id,
13 USU_CUE as cue
14 FROM USUARIO
15 WHERE USU_ID = :usuId"
16 );
17 $stmt->execute([":usuId" => $usuId]);
18 $stmt->setFetchMode(
19 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
20 Usuario::class
21 );
22 /** @var false|Usuario */
23 $usuario = $stmt->fetch();
24 if ($usuario === false) {
25 return false;
26 } else {
27 $usuario->roles = usuRolConsulta($usuId);
28 return $usuario;
29 }
30}
31

8. srv / bd / usuarioConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/Bd.php";
5
6function usuarioConsulta()
7{
8 $con = Bd::getConexion();
9 $stmt = $con->query(
10 "SELECT
11 U.USU_ID AS usuId,
12 U.USU_CUE AS usuCue,
13 GROUP_CONCAT(R.ROL_ID, ', ') AS roles
14 FROM USUARIO U
15 LEFT JOIN USU_ROL UR
16 ON U.USU_ID = UR.USU_ID
17 LEFT JOIN ROL R
18 ON UR.ROL_ID = R.ROL_ID
19 GROUP BY U.USU_CUE
20 ORDER BY U.USU_CUE"
21 );
22 $resultado = $stmt->fetchAll(PDO::FETCH_OBJ);
23 return recibeFetchAll($resultado);
24}
25

9. srv / bd / usuarioElimina.php

1<?php
2
3require_once __DIR__ . "/Bd.php";
4require_once __DIR__ . "/usuRolElimina.php";
5
6function usuarioElimina(int $id)
7{
8 $con = Bd::getConexion();
9 $con->beginTransaction();
10 usuRolElimina($id);
11 $stmt = $con->prepare(
12 "DELETE FROM USUARIO
13 WHERE USU_ID = :id"
14 );
15 $stmt->execute([":id" => $id]);
16 $con->commit();
17}
18

10. srv / bd / usuarioModifica.php

1<?php
2
3 require_once __DIR__ . "/../modelo/Usuario.php";
4 require_once __DIR__ . "/Bd.php";
5 require_once __DIR__ . "/usuRolAgrega.php";
6 require_once __DIR__ . "/usuRolElimina.php";
7
8function usuarioModifica(Usuario $modelo) {
9 $modelo->valida();
10 $con = Bd::getConexion();
11 $con->beginTransaction();
12 $stmt = $con->prepare(
13 "UPDATE USUARIO
14 SET USU_CUE = :cue
15 WHERE USU_ID = :id"
16 );
17 $stmt->execute([
18 ":id" => $modelo->id,
19 ":cue" => $modelo->cue
20 ]);
21 usuRolElimina($modelo->id);
22 usuRolAgrega($modelo);
23 $con->commit();
24}
25

11. srv / bd / usuRolAgrega.php

1<?php
2
3require_once __DIR__ . "/../modelo/Usuario.php";
4require_once __DIR__ . "/Bd.php";
5
6function usuRolAgrega(Usuario $usuario) {
7 $roles = $usuario->roles;
8 if (sizeof($roles) > 0) {
9 $con = Bd::getConexion();
10 $stmt = $con->prepare(
11 "INSERT INTO USU_ROL
12 (USU_ID, ROL_ID)
13 VALUES
14 (:usuId, :rolId)"
15 );
16 foreach ($roles as $rol) {
17 $stmt->execute(
18 [
19 ":usuId" => $usuario->id,
20 ":rolId" => $rol->id
21 ]
22 );
23 }
24 }
25}
26

12. srv / bd / usuRolConsulta.php

1<?php
2
3require_once __DIR__ . "/../../lib/php/recibeFetchAll.php";
4require_once __DIR__ . "/../modelo/Rol.php";
5require_once __DIR__ . "/Bd.php";
6
7/** @return Rol[] */
8function usuRolConsulta(int $usuId)
9{
10 $con = Bd::getConexion();
11 $stmt = $con->query(
12 "SELECT
13 UR.ROL_ID AS id,
14 R.ROL_DESCRIPCION AS descripcion
15 FROM USU_ROL UR, ROL R
16 WHERE
17 UR.ROL_ID = R.ROL_ID
18 AND UR.USU_ID = :usuId
19 ORDER BY UR.ROL_ID"
20 );
21 $stmt->execute([":usuId" => $usuId]);
22 $resultado = $stmt->fetchAll(
23 PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE,
24 Rol::class
25 );
26 return recibeFetchAll($resultado);
27}
28

13. srv / bd / usuRolElimina.php

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

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

1<?php
2
3/**
4 * Devuelve los valores asociados a un
5 * parámetro multivaluado; por ejemplo, un
6 * grupo de checkbox, recibido en el servidor
7 * por medio de GET, POST o cookie. Si no se
8 * recibe el parámetro, devuelve null. Si el
9 * valor recibido no es un arreglo, lo coloca
10 * dentro de uno.
11 */
12function leeArray(string $parametro)
13{
14 if (isset($_REQUEST[$parametro])) {
15 $valor = $_REQUEST[$parametro];
16 return is_array($valor)
17 ? $valor
18 : [$valor];
19 } else {
20 return null;
21 }
22}
23

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

M. Carpeta « error »

Versión para imprimir.

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

B. error / faltacue.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 cue</title>
10
11</head>
12
13<body>
14
15 <h1>Falta el cue</h1>
16
17</body>
18
19</html>

C. error / faltadescripcion.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 la descripción</title>
10
11</head>
12
13<body>
14
15 <h1>Falta la descripción</h1>
16
17</body>
18
19</html>

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

E. error / idincorrecto.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>Valor de id incorrecto</title>
10
11</head>
12
13<body>
14
15 <h1>Valor de id incorrecto</h1>
16
17 <p>
18 O no se ha proporcionado el valor para id, o el valor proporcionado no es un
19 número entero.
20 </p>
21
22</body>
23
24</html>

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

G. error / rolincorrecto.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>Tipo incorrecto para un rol</title>
10
11</head>
12
13<body>
14
15 <h1>Tipo incorrecto para un rol</h1>
16
17</body>
18
19</html>

H. error / usuarionoencontrado.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>Usuario no encontrado</title>
10
11</head>
12
13<body>
14
15 <h1>Usuario no encontrado</h1>
16
17 <p>No se encontró ninguna usuario 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