En esta lección se muestra el almacenamiento de un archivo por registro de la base de datos, usando servicios.
Puedes probar el ejemplo en http://srvarchivos.rf.gd/.
Este ejemplo toma como base la lección de asociaciones a uno, pero en caso de necesitar varios archivos por producto, puedes tomar como base la lección de asociaciones a muchos.
Prueba el ejemplo en http://srvarchivos.rf.gd/.
Descarga el archivo /src/srvarchivos.zip y descompáctalo.
Crea tu proyecto en GitHub:
Crea una cuenta de email, por ejemplo, pepito@google.com
Crea una cuenta de GitHub usando el email anterior y selecciona el nombre de usuario unsando la parte inicial del correo electrónico, por ejemplo pepito.
Crea un repositorio nuevo. En la página principal de GitHub cliquea 📘 New.
En la página Create a new repository introduce los siguientes datos:
Proporciona el nombre de tu repositorio debajo de donde dice Repository name *.
Mantén la selección Public para que otros usuarios puedan ver tu proyecto.
Verifica la casilla Add a README file. En este archivo se muestra información sobre tu proyecto.
Cliquea License: None. y selecciona la licencia que consideres más adecuada para tu proyecto.
Cliquea Create repository.
Importa el proyecto en GitHub:
En la página principal de tu proyecto en GitHub, en la pestaña < > Code, cliquea < > Code y en la sección Branches y copia la dirección que está en HTTPS, debajo de Clone.
En Visual Studio Code, usa el botón de la izquierda para Source Control.
Cliquea el botón Clone Repository.
Pega la url que copiaste anteriormente hasta arriba, donde dice algo como Provide repository URL y presiona la teclea Intro.
Selecciona la carpeta donde se guardará la carpeta del proyecto.
Abre la carpeta del proyecto importado.
Añade el contenido de la carpeta descompactada que contiene el código del ejemplo.
Edita los archivos que desees.
Haz clic derecho en index.html
, selecciona
PHP Server: serve project y se abre el navegador para que puedas
probar localmente el ejemplo.
Para depurar paso a paso haz lo siguiente:
En el navegador, haz clic derecho en la página que deseas depurar y selecciona inspeccionar.
Recarga la página, de preferencia haciendo clic derecho en el ícono de volver a cargar la página y seleccionando vaciar caché y volver a cargar de manera forzada (o algo parecido). Si no aparece un menú emergente, simplemente cliquea volver a cargar la página . Revisa que no aparezca ningún error ni en la pestañas Consola, ni en Red.
Selecciona la pestaña Fuentes (o Sources si tu navegador está en Inglés).
Selecciona el archivo donde vas a empezar a depurar.
Haz clic en el número de la línea donde vas a empezar a depurar.
En Visual Studio Code, abre el archivo de PHP donde vas a empezar a depurar.
Haz clic en Run and Debug .
Si no está configurada la depuración, haz clic en create a launch json file.
Haz clic en la flechita RUN AND DEBUG, al lado de la cual debe decir Listen for Xdebug .
Aparece un cuadro con los controles de depuración
Selecciona otra vez el archivo de PHP y haz clic en el número de la línea donde vas a empezar a depurar.
Regresa al navegador, recarga la página y empieza a usarla.
Si se ejecuta alguna de las líneas de código seleccionadas, aparece resaltada en la pestaña de fuentes. Usa los controles de depuración para avanzar, como se muestra en este video.
Sube el proyecto al hosting que elijas. En algunos casos puedes usar filezilla (https://filezilla-project.org/)
Abre un navegador y prueba el proyecto en tu hosting.
En el hosting InfinityFree, la primera ves que corres la página, puede marcar un mensaje de error, pero al recargar funciona correctamente. Puedes evitar este problema usando un dominio propio.
Para subir el código a GitHub, en la sección de SOURCE CONTROL, en Message introduce un mensaje sobre los cambios que hiciste, por ejemplo index.html corregido, selecciona v y luego Commit & Push.
Haz clic en los triángulos para expandir las carpetas
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/consumeJson.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="consumeJson('srv/productos.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> |
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/submitForm.js"></script> |
12 | <script type="module" src="lib/js/muestraError.js"></script> |
13 | <script type="module" src="lib/js/muestraImagenSeleccionada.js"></script> |
14 | |
15 | </head> |
16 | |
17 | <body onload="muestraImagenSeleccionada(forma, forma.imagen) |
18 | .catch(muestraError)"> |
19 | |
20 | <form id="forma" onsubmit="submitForm('srv/producto-agrega.php', event) |
21 | .then(respuesta => 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="imagen" type="file" accept="image/*" data-img="preview" |
39 | oninput="muestraImagenSeleccionada(forma, this).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="preview" hidden alt="Imagen del producto" style="max-width: 100%;"> |
49 | </figure> |
50 | |
51 | </form> |
52 | |
53 | </body> |
54 | |
55 | </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/consumeJson.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/muestraImagenSeleccionada.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 | consumeJson('srv/producto.php?' + params) |
26 | .then(modelo => muestraObjeto(document, modelo.body)) |
27 | .catch(muestraError) |
28 | }"> |
29 | |
30 | <form onsubmit="submitForm('srv/producto-modifica.php', event) |
31 | .then(modelo => location.href = 'index.html') |
32 | .catch(muestraError)"> |
33 | |
34 | <h1>Modificar</h1> |
35 | |
36 | <p><a href="index.html">Cancelar</a></p> |
37 | |
38 | <input type="hidden" name="id"> |
39 | |
40 | <p> |
41 | <label> |
42 | Nombre * |
43 | <input name="nombre" value="Cargando…"> |
44 | </label> |
45 | </p> |
46 | |
47 | <p> |
48 | <label> |
49 | Imagen |
50 | <input name="imagen" type="file" accept="image/*" data-img="preview" |
51 | oninput="muestraImagenSeleccionada(document, this).catch(muestraError)"> |
52 | </label> |
53 | </p> |
54 | |
55 | <p>* Obligatorio</p> |
56 | |
57 | <p> |
58 | |
59 | <button type="submit">Guardar</button> |
60 | |
61 | <button type="button" onclick=" |
62 | if (params.size > 0 && confirm('Confirma la eliminación')) { |
63 | consumeJson('srv/producto-elimina.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="preview" hidden alt="Imagen del producto" style="max-width: 100%;"> |
74 | </figure> |
75 | |
76 | </form> |
77 | |
78 | </body> |
79 | |
80 | </html> |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/NOT_FOUND.php"; |
4 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
5 | require_once __DIR__ . "/../lib/php/recuperaIdEntero.php"; |
6 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
7 | require_once __DIR__ . "/../lib/php/selectFirst.php"; |
8 | require_once __DIR__ . "/Bd.php"; |
9 | require_once __DIR__ . "/TABLA_ARCHIVO.php"; |
10 | |
11 | ejecutaServicio(function () { |
12 | |
13 | // Evita que la imagen se cargue en el caché del navegador. |
14 | header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0"); |
15 | header("Cache-Control: post-check=0, pre-check=0", false); |
16 | header("Pragma: no-cache"); |
17 | |
18 | $archId = recuperaIdEntero("id"); |
19 | |
20 | $archivo = |
21 | selectFirst(pdo: Bd::pdo(), from: ARCHIVO, where: [ARCH_ID => $archId]); |
22 | |
23 | if ($archivo === false) { |
24 | $idHtml = htmlentities($archId); |
25 | throw new ProblemDetails( |
26 | status: NOT_FOUND, |
27 | title: "Archivo no encontrado.", |
28 | type: "/error/archivonoencontrado.html", |
29 | detail: "No se encontró ningún archivo con el id $idHtml.", |
30 | ); |
31 | } |
32 | |
33 | $bytes = $archivo[ARCH_BYTES]; |
34 | $contentType = (new finfo(FILEINFO_MIME_TYPE))->buffer($bytes); |
35 | header("Content-Type: $contentType"); |
36 | echo $bytes; |
37 | }); |
38 |
1 | <?php |
2 | |
3 | class Bd |
4 | { |
5 | private static ?PDO $pdo = null; |
6 | |
7 | static function pdo(): PDO |
8 | { |
9 | if (self::$pdo === null) { |
10 | |
11 | self::$pdo = new PDO( |
12 | // cadena de conexión |
13 | "sqlite:srvarchivos.db", |
14 | // usuario |
15 | null, |
16 | // contraseña |
17 | null, |
18 | // Opciones: pdos no persistentes y lanza excepciones. |
19 | [PDO::ATTR_PERSISTENT => false, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] |
20 | ); |
21 | |
22 | self::$pdo->exec( |
23 | 'CREATE TABLE IF NOT EXISTS ARCHIVO ( |
24 | ARCH_ID INTEGER, |
25 | ARCH_BYTES BLOB NOT NULL, |
26 | CONSTRAINT ARCH_PK |
27 | PRIMARY KEY(ARCH_ID) |
28 | )' |
29 | ); |
30 | self::$pdo->exec( |
31 | 'CREATE TABLE IF NOT EXISTS PRODUCTO ( |
32 | PROD_ID INTEGER, |
33 | PROD_NOMBRE TEXT NOT NULL, |
34 | ARCH_ID INTEGER NOT NULL, |
35 | CONSTRAINT PROD_PK |
36 | PRIMARY KEY(PROD_ID), |
37 | CONSTRAINT PROD_NOM_UNQ |
38 | UNIQUE(PROD_NOMBRE), |
39 | CONSTRAINT PROD_NOM_NV |
40 | CHECK(LENGTH(PROD_NOMBRE) > 0), |
41 | CONSTRAINT PROD_ARCH_FK |
42 | FOREIGN KEY (ARCH_ID) REFERENCES ARCHIVO(ARCH_ID) |
43 | )' |
44 | ); |
45 | } |
46 | |
47 | return self::$pdo; |
48 | } |
49 | } |
50 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
5 | require_once __DIR__ . "/../lib/php/recuperaBytes.php"; |
6 | require_once __DIR__ . "/../lib/php/recuperaTexto.php"; |
7 | require_once __DIR__ . "/../lib/php/validaNombre.php"; |
8 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
9 | require_once __DIR__ . "/../lib/php/insert.php"; |
10 | require_once __DIR__ . "/../lib/php/devuelveCreated.php"; |
11 | require_once __DIR__ . "/Bd.php"; |
12 | require_once __DIR__ . "/TABLA_PRODUCTO.php"; |
13 | require_once __DIR__ . "/TABLA_ARCHIVO.php"; |
14 | require_once __DIR__ . "/validaImagen.php"; |
15 | |
16 | ejecutaServicio(function () { |
17 | |
18 | $nombre = recuperaTexto("nombre"); |
19 | $bytes = recuperaBytes("imagen"); |
20 | |
21 | $nombre = validaNombre($nombre); |
22 | $bytes = validaImagen($bytes); |
23 | |
24 | if ($bytes === "") { |
25 | throw new ProblemDetails( |
26 | status: BAD_REQUEST, |
27 | title: "Imagen vacía.", |
28 | type: "/error/imagenvacia.html", |
29 | detail: "Selecciona un archivo que no esté vacío." |
30 | ); |
31 | } |
32 | |
33 | $pdo = Bd::pdo(); |
34 | $pdo->beginTransaction(); |
35 | |
36 | insert(pdo: $pdo, into: ARCHIVO, values: [ARCH_BYTES => $bytes]); |
37 | $archId = $pdo->lastInsertId(); |
38 | |
39 | insert( |
40 | pdo: $pdo, |
41 | into: PRODUCTO, |
42 | values: [PROD_NOMBRE => $nombre, ARCH_ID => $archId] |
43 | ); |
44 | $id = $pdo->lastInsertId(); |
45 | |
46 | $pdo->commit(); |
47 | |
48 | $encodeId = urlencode($id); |
49 | $encodeArchId = urlencode($archId); |
50 | $htmlEncodeArchId = htmlentities($encodeArchId); |
51 | // Los bytes se descargan con "archivo.php"; no desde aquí. |
52 | devuelveCreated("/srv/producto.php?id=$encodeId", [ |
53 | "id" => ["value" => $id], |
54 | "nombre" => ["value" => $nombre], |
55 | "imagen" => ["data-file" => "srv/archivo.php?id=$htmlEncodeArchId"] |
56 | ]); |
57 | }); |
58 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/recuperaIdEntero.php"; |
5 | require_once __DIR__ . "/../lib/php/selectFirst.php"; |
6 | require_once __DIR__ . "/../lib/php/delete.php"; |
7 | require_once __DIR__ . "/../lib/php/devuelveNoContent.php"; |
8 | require_once __DIR__ . "/Bd.php"; |
9 | require_once __DIR__ . "/TABLA_PRODUCTO.php"; |
10 | require_once __DIR__ . "/TABLA_ARCHIVO.php"; |
11 | |
12 | ejecutaServicio(function () { |
13 | |
14 | $prodId = recuperaIdEntero("id"); |
15 | |
16 | $pdo = Bd::pdo(); |
17 | $pdo->beginTransaction(); |
18 | |
19 | $producto = |
20 | selectFirst(pdo: $pdo, from: PRODUCTO, where: [PROD_ID => $prodId]); |
21 | if ($producto !== false) { |
22 | delete(pdo: $pdo, from: PRODUCTO, where: [PROD_ID => $prodId]); |
23 | if ($producto[ARCH_ID] !== null) { |
24 | delete(pdo: $pdo, from: ARCHIVO, where: [ARCH_ID => $producto[ARCH_ID]]); |
25 | } |
26 | } |
27 | |
28 | $pdo->commit(); |
29 | |
30 | devuelveNoContent(); |
31 | }); |
32 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/NOT_FOUND.php"; |
4 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
5 | require_once __DIR__ . "/../lib/php/recuperaIdEntero.php"; |
6 | require_once __DIR__ . "/../lib/php/recuperaTexto.php"; |
7 | require_once __DIR__ . "/../lib/php/recuperaBytes.php"; |
8 | require_once __DIR__ . "/../lib/php/validaNombre.php"; |
9 | require_once __DIR__ . "/../lib/php/selectFirst.php"; |
10 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
11 | require_once __DIR__ . "/../lib/php/insert.php"; |
12 | require_once __DIR__ . "/../lib/php/update.php"; |
13 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
14 | require_once __DIR__ . "/Bd.php"; |
15 | require_once __DIR__ . "/TABLA_PRODUCTO.php"; |
16 | require_once __DIR__ . "/TABLA_ARCHIVO.php"; |
17 | require_once __DIR__ . "/validaImagen.php"; |
18 | |
19 | ejecutaServicio(function () { |
20 | |
21 | $prodId = recuperaIdEntero("id"); |
22 | $nombre = recuperaTexto("nombre"); |
23 | $bytes = recuperaBytes("imagen"); |
24 | |
25 | $nombre = validaNombre($nombre); |
26 | $bytes = validaImagen($bytes); |
27 | |
28 | $pdo = Bd::pdo(); |
29 | $pdo->beginTransaction(); |
30 | |
31 | $producto = |
32 | selectFirst(pdo: $pdo, from: PRODUCTO, where: [PROD_ID => $prodId]); |
33 | |
34 | if ($producto === false) { |
35 | $prodIdHtml = htmlentities($prodId); |
36 | throw new ProblemDetails( |
37 | status: NOT_FOUND, |
38 | title: "Producto no encontrado.", |
39 | type: "/error/productonoencontrado.html", |
40 | detail: "No se encontró ningún producto con el id $prodIdHtml.", |
41 | ); |
42 | } |
43 | |
44 | $archId = $producto[ARCH_ID]; |
45 | |
46 | if ($bytes !== "") { |
47 | if ($archId === null) { |
48 | insert(pdo: $pdo, into: ARCHIVO, values: [ARCH_BYTES => $bytes]); |
49 | $archId = $pdo->lastInsertId(); |
50 | } else { |
51 | update( |
52 | pdo: $pdo, |
53 | table: ARCHIVO, |
54 | set: [ARCH_BYTES => $bytes], |
55 | where: [ARCH_ID => $archId] |
56 | ); |
57 | } |
58 | } |
59 | |
60 | update( |
61 | pdo: $pdo, |
62 | table: PRODUCTO, |
63 | set: [PROD_NOMBRE => $nombre, ARCH_ID => $archId], |
64 | where: [PROD_ID => $prodId] |
65 | ); |
66 | |
67 | $pdo->commit(); |
68 | |
69 | $encodeArchId = $archId === null ? "" : urlencode($archId); |
70 | $htmlEncodeArchId = htmlentities($encodeArchId); |
71 | // Los bytes se descargan con "archivo.php"; no desde aquí. |
72 | devuelveJson([ |
73 | "id" => ["value" => $prodId], |
74 | "nombre" => ["value" => $nombre], |
75 | "imagen" => [ |
76 | "data-file" => $htmlEncodeArchId === "" |
77 | ? "" |
78 | : "srv/archivo.php?id=$htmlEncodeArchId" |
79 | ] |
80 | ]); |
81 | }); |
82 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/NOT_FOUND.php"; |
4 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
5 | require_once __DIR__ . "/../lib/php/recuperaIdEntero.php"; |
6 | require_once __DIR__ . "/../lib/php/selectFirst.php"; |
7 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
8 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
9 | require_once __DIR__ . "/Bd.php"; |
10 | require_once __DIR__ . "/TABLA_PRODUCTO.php"; |
11 | require_once __DIR__ . "/TABLA_ARCHIVO.php"; |
12 | |
13 | ejecutaServicio(function () { |
14 | |
15 | $prodId = recuperaIdEntero("id"); |
16 | |
17 | $modelo = |
18 | selectFirst(pdo: Bd::pdo(), from: PRODUCTO, where: [PROD_ID => $prodId]); |
19 | |
20 | if ($modelo === false) { |
21 | $prodIdHtml = htmlentities($prodId); |
22 | throw new ProblemDetails( |
23 | status: NOT_FOUND, |
24 | title: "Producto no encontrado.", |
25 | type: "/error/productonoencontrado.html", |
26 | detail: "No se encontró ningún producto con el id $prodIdHtml.", |
27 | ); |
28 | } |
29 | |
30 | $encodeArchId = $modelo[ARCH_ID] === null ? "" : urlencode($modelo[ARCH_ID]); |
31 | $htmlEncodeArchId = htmlentities($encodeArchId); |
32 | devuelveJson([ |
33 | "id" => ["value" => $prodId], |
34 | "nombre" => ["value" => $modelo[PROD_NOMBRE]], |
35 | "imagen" => [ |
36 | "data-file" => $htmlEncodeArchId === "" |
37 | ? "" |
38 | : "srv/archivo.php?id=$htmlEncodeArchId" |
39 | ] |
40 | ]); |
41 | }); |
42 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/ejecutaServicio.php"; |
4 | require_once __DIR__ . "/../lib/php/select.php"; |
5 | require_once __DIR__ . "/../lib/php/devuelveJson.php"; |
6 | require_once __DIR__ . "/Bd.php"; |
7 | require_once __DIR__ . "/TABLA_PRODUCTO.php"; |
8 | require_once __DIR__ . "/TABLA_ARCHIVO.php"; |
9 | |
10 | ejecutaServicio(function () { |
11 | |
12 | $lista = select(pdo: Bd::pdo(), from: PRODUCTO, orderBy: PROD_NOMBRE); |
13 | |
14 | $render = ""; |
15 | foreach ($lista as $modelo) { |
16 | $prodId = htmlentities($modelo[PROD_ID]); |
17 | $prodNombre = htmlentities($modelo[PROD_NOMBRE]); |
18 | $encodeArchId = $modelo[ARCH_ID] === null ? "" : urlencode($modelo[ARCH_ID]); |
19 | $archId = $encodeArchId === "" ? "" : htmlentities($encodeArchId); |
20 | $src = $archId === "" ? "" : "srv/archivo.php?id=$archId"; |
21 | $render .= |
22 | "<div style='display: flex; flex-direction: row-reverse; |
23 | align-items: center; gap: 0.5rem'> |
24 | <dt style='flex: 1 1 0'> |
25 | <a href='modifica.html?id=$prodId'>$prodNombre</a> |
26 | </dt> |
27 | <dd style='flex: 1 1 0; margin: 0'> |
28 | <a href='modifica.html?id=$prodId'><img |
29 | style='width: 100%; aspect-ratio:16/9; object-fit: cover' |
30 | alt='Imagen del producto' src='$src'></a> |
31 | </dd> |
32 | </div>"; |
33 | } |
34 | |
35 | devuelveJson(["lista" => ["innerHTML" => $render]]); |
36 | }); |
37 |
1 | <?php |
2 | |
3 | const ARCHIVO = "ARCHIVO"; |
4 | const ARCH_ID = "ARCH_ID"; |
5 | const ARCH_BYTES = "ARCH_BYTES"; |
6 |
1 | <?php |
2 | |
3 | const PRODUCTO = "PRODUCTO"; |
4 | const PROD_ID = "PROD_ID"; |
5 | const PROD_NOMBRE = "PROD_NOMBRE"; |
6 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/../lib/php/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/../lib/php/ProblemDetails.php"; |
5 | |
6 | function validaImagen(false|string $bytes) |
7 | { |
8 | |
9 | if ($bytes === false) { |
10 | throw new ProblemDetails( |
11 | status: BAD_REQUEST, |
12 | title: "Falta la imagen.", |
13 | type: "/error/faltaimagen.html", |
14 | detail: "La solicitud no tiene el valor de imagen." |
15 | ); |
16 | } |
17 | |
18 | return $bytes; |
19 | } |
20 |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Espera a que la promesa de un fetch termine. Si |
6 | * hay error, lanza una excepción. Si no hay error, |
7 | * interpreta la respuesta del servidor como JSON y |
8 | * la convierte en una literal de objeto. |
9 | * |
10 | * @param { string | Promise<Response> } servicio |
11 | */ |
12 | export async function consumeJson(servicio) { |
13 | |
14 | if (typeof servicio === "string") { |
15 | servicio = fetch(servicio, { |
16 | headers: { "Accept": "application/json, application/problem+json" } |
17 | }) |
18 | } else if (!(servicio instanceof Promise)) { |
19 | throw new Error("Servicio de tipo incorrecto.") |
20 | } |
21 | |
22 | const respuesta = await servicio |
23 | |
24 | const headers = respuesta.headers |
25 | |
26 | if (respuesta.ok) { |
27 | // Aparentemente el servidor tuvo éxito. |
28 | |
29 | if (respuesta.status === 204) { |
30 | // No contiene texto de respuesta. |
31 | |
32 | return { headers, body: {} } |
33 | |
34 | } else { |
35 | |
36 | const texto = await respuesta.text() |
37 | |
38 | try { |
39 | |
40 | return { headers, body: JSON.parse(texto) } |
41 | |
42 | } catch (error) { |
43 | |
44 | // El contenido no es JSON. Probablemente sea texto de un error. |
45 | throw new ProblemDetails(respuesta.status, headers, texto, |
46 | "/error/errorinterno.html") |
47 | |
48 | } |
49 | |
50 | } |
51 | |
52 | } else { |
53 | // Hay un error. |
54 | |
55 | const texto = await respuesta.text() |
56 | |
57 | if (texto === "") { |
58 | |
59 | // No hay texto. Se usa el texto predeterminado. |
60 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText) |
61 | |
62 | } else { |
63 | // Debiera se un ProblemDetails en JSON. |
64 | |
65 | try { |
66 | |
67 | const { title, type, detail } = JSON.parse(texto) |
68 | |
69 | throw new ProblemDetails(respuesta.status, headers, |
70 | typeof title === "string" ? title : respuesta.statusText, |
71 | typeof type === "string" ? type : undefined, |
72 | typeof detail === "string" ? detail : undefined) |
73 | |
74 | } catch (error) { |
75 | |
76 | if (error instanceof ProblemDetails) { |
77 | // El error si era un ProblemDetails |
78 | |
79 | throw error |
80 | |
81 | } else { |
82 | |
83 | throw new ProblemDetails(respuesta.status, headers, respuesta.statusText, |
84 | undefined, texto) |
85 | |
86 | } |
87 | |
88 | } |
89 | |
90 | } |
91 | |
92 | } |
93 | |
94 | } |
95 | |
96 | exportaAHtml(consumeJson) |
1 | /** |
2 | * Permite que los eventos de html usen la función. |
3 | * @param {function} functionInstance |
4 | */ |
5 | export function exportaAHtml(functionInstance) { |
6 | window[nombreDeFuncionParaHtml(functionInstance)] = functionInstance |
7 | } |
8 | |
9 | /** |
10 | * @param {function} valor |
11 | */ |
12 | export function nombreDeFuncionParaHtml(valor) { |
13 | const names = valor.name.split(/\s+/g) |
14 | return names[names.length - 1] |
15 | } |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { ProblemDetails } from "./ProblemDetails.js" |
3 | |
4 | /** |
5 | * Muestra un error en la consola y en un cuadro de |
6 | * alerta el mensaje de una excepción. |
7 | * @param { ProblemDetails | Error | null } error descripción del error. |
8 | */ |
9 | export function muestraError(error) { |
10 | |
11 | if (error === null) { |
12 | |
13 | console.error("Error") |
14 | alert("Error") |
15 | |
16 | } else if (error instanceof ProblemDetails) { |
17 | |
18 | let mensaje = error.title |
19 | if (error.detail) { |
20 | mensaje += `\n\n${error.detail}` |
21 | } |
22 | mensaje += `\n\nCódigo: ${error.status}` |
23 | if (error.type) { |
24 | mensaje += ` ${error.type}` |
25 | } |
26 | |
27 | console.error(mensaje) |
28 | console.error(error) |
29 | console.error("Headers:") |
30 | error.headers.forEach((valor, llave) => console.error(llave, "=", valor)) |
31 | alert(mensaje) |
32 | |
33 | } else { |
34 | |
35 | console.error(error) |
36 | alert(error.message) |
37 | |
38 | } |
39 | |
40 | } |
41 | |
42 | exportaAHtml(muestraError) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | import { getImgParaElementoHtml } from "./muestraObjeto.js" |
3 | |
4 | /** |
5 | * @param { Document | HTMLElement } raizHtml |
6 | * @param {HTMLInputElement} input |
7 | */ |
8 | export function muestraImagenSeleccionada(raizHtml, input) { |
9 | return new Promise((resolve, reject) => { |
10 | setTimeout(() => { |
11 | |
12 | const img = getImgParaElementoHtml(raizHtml, input) |
13 | if (img !== null) { |
14 | try { |
15 | |
16 | const dataUrl = getDataUrlDeSeleccion(input) |
17 | if (dataUrl === "") { |
18 | |
19 | const file = input.dataset.file |
20 | if (file === undefined || file === "") { |
21 | img.hidden = true |
22 | img.src = "" |
23 | } else { |
24 | img.hidden = false |
25 | img.src = file |
26 | } |
27 | |
28 | } else { |
29 | |
30 | img.hidden = false |
31 | img.src = dataUrl |
32 | |
33 | } |
34 | |
35 | resolve(true) |
36 | |
37 | } catch (error) { |
38 | |
39 | img.hidden = true |
40 | |
41 | reject(error) |
42 | |
43 | } |
44 | } |
45 | |
46 | }, |
47 | 500) |
48 | }) |
49 | } |
50 | exportaAHtml(muestraImagenSeleccionada) |
51 | |
52 | /** |
53 | * @param {HTMLInputElement} input |
54 | */ |
55 | export function getDataUrlDeSeleccion(input) { |
56 | const seleccion = getArchivoSeleccionado(input) |
57 | if (seleccion === null) { |
58 | return "" |
59 | } else { |
60 | return URL.createObjectURL(seleccion) |
61 | } |
62 | } |
63 | exportaAHtml(getDataUrlDeSeleccion) |
64 | |
65 | |
66 | /** |
67 | * @param { HTMLInputElement } input |
68 | */ |
69 | export function getArchivoSeleccionado(input) { |
70 | const seleccion = input.files |
71 | if (seleccion === null || seleccion.length === 0) { |
72 | return null |
73 | } else { |
74 | return seleccion.item(0) |
75 | } |
76 | } |
77 | exportaAHtml(getArchivoSeleccionado) |
1 | import { exportaAHtml } from "./exportaAHtml.js" |
2 | |
3 | /** |
4 | * @param { Document | HTMLElement } raizHtml |
5 | * @param { any } objeto |
6 | */ |
7 | export function muestraObjeto(raizHtml, objeto) { |
8 | |
9 | for (const [nombre, definiciones] of Object.entries(objeto)) { |
10 | |
11 | if (Array.isArray(definiciones)) { |
12 | |
13 | muestraArray(raizHtml, nombre, definiciones) |
14 | |
15 | } else if (definiciones !== undefined && definiciones !== null) { |
16 | |
17 | const elementoHtml = buscaElementoHtml(raizHtml, nombre) |
18 | |
19 | if (elementoHtml instanceof HTMLInputElement) { |
20 | |
21 | muestraInput(raizHtml, elementoHtml, definiciones) |
22 | |
23 | } else if (elementoHtml !== null) { |
24 | |
25 | for (const [atributo, valor] of Object.entries(definiciones)) { |
26 | if (atributo in elementoHtml) { |
27 | elementoHtml[atributo] = valor |
28 | } |
29 | } |
30 | |
31 | } |
32 | |
33 | } |
34 | |
35 | } |
36 | |
37 | } |
38 | exportaAHtml(muestraObjeto) |
39 | |
40 | /** |
41 | * @param { Document | HTMLElement } raizHtml |
42 | * @param { string } nombre |
43 | */ |
44 | export function buscaElementoHtml(raizHtml, nombre) { |
45 | return raizHtml.querySelector( |
46 | `#${nombre},[name="${nombre}"],[data-name="${nombre}"]`) |
47 | } |
48 | |
49 | /** |
50 | * @param { Document | HTMLElement } raizHtml |
51 | * @param { string } propiedad |
52 | * @param {any[]} valores |
53 | */ |
54 | function muestraArray(raizHtml, propiedad, valores) { |
55 | |
56 | const conjunto = new Set(valores) |
57 | const elementos = |
58 | raizHtml.querySelectorAll(`[name="${propiedad}"],[data-name="${propiedad}"]`) |
59 | |
60 | if (elementos.length === 1) { |
61 | const elemento = elementos[0] |
62 | |
63 | if (elemento instanceof HTMLSelectElement) { |
64 | const options = elemento.options |
65 | for (let i = 0, len = options.length; i < len; i++) { |
66 | const option = options[i] |
67 | option.selected = conjunto.has(option.value) |
68 | } |
69 | return |
70 | } |
71 | |
72 | } |
73 | |
74 | for (let i = 0, len = elementos.length; i < len; i++) { |
75 | const elemento = elementos[i] |
76 | if (elemento instanceof HTMLInputElement) { |
77 | elemento.checked = conjunto.has(elemento.value) |
78 | } |
79 | } |
80 | |
81 | } |
82 | |
83 | /** |
84 | * @param { Document | HTMLElement } raizHtml |
85 | * @param { HTMLInputElement } input |
86 | * @param { any } definiciones |
87 | */ |
88 | function muestraInput(raizHtml, input, definiciones) { |
89 | |
90 | for (const [atributo, valor] of Object.entries(definiciones)) { |
91 | |
92 | if (atributo == "data-file") { |
93 | |
94 | const img = getImgParaElementoHtml(raizHtml, input) |
95 | if (img !== null) { |
96 | input.dataset.file = valor |
97 | input.value = "" |
98 | if (valor === "") { |
99 | img.src = "" |
100 | img.hidden = true |
101 | } else { |
102 | img.src = valor |
103 | img.hidden = false |
104 | } |
105 | } |
106 | |
107 | } else if (atributo in input) { |
108 | |
109 | input[atributo] = valor |
110 | |
111 | } |
112 | } |
113 | |
114 | } |
115 | |
116 | /** |
117 | * @param { Document | HTMLElement } raizHtml |
118 | * @param { HTMLElement } elementoHtml |
119 | */ |
120 | export function getImgParaElementoHtml(raizHtml, elementoHtml) { |
121 | const imgId = elementoHtml.getAttribute("data-img") |
122 | if (imgId === null) { |
123 | return null |
124 | } else { |
125 | const input = buscaElementoHtml(raizHtml, imgId) |
126 | if (input instanceof HTMLImageElement) { |
127 | return input |
128 | } else { |
129 | return null |
130 | } |
131 | } |
132 | } |
1 | /** |
2 | * Detalle de los errores devueltos por un servicio. |
3 | */ |
4 | export class ProblemDetails extends Error { |
5 | |
6 | /** |
7 | * @param {number} status |
8 | * @param {Headers} headers |
9 | * @param {string} title |
10 | * @param {string} [type] |
11 | * @param {string} [detail] |
12 | */ |
13 | constructor(status, headers, title, type, detail) { |
14 | super(title) |
15 | /** |
16 | * @readonly |
17 | */ |
18 | this.status = status |
19 | /** |
20 | * @readonly |
21 | */ |
22 | this.headers = headers |
23 | /** |
24 | * @readonly |
25 | */ |
26 | this.type = type |
27 | /** |
28 | * @readonly |
29 | */ |
30 | this.detail = detail |
31 | /** |
32 | * @readonly |
33 | */ |
34 | this.title = title |
35 | } |
36 | |
37 | } |
1 | import { consumeJson } from "./consumeJson.js" |
2 | import { exportaAHtml } from "./exportaAHtml.js" |
3 | |
4 | /** |
5 | * Envía los datos de la forma a la url usando la codificación |
6 | * multipart/form-data. |
7 | * @param {string} url |
8 | * @param {Event} event |
9 | * @param { "GET" | "POST"| "PUT" | "PATCH" | "DELETE" | "TRACE" | "OPTIONS" |
10 | * | "CONNECT" | "HEAD" } metodoHttp |
11 | */ |
12 | export function submitForm(url, event, metodoHttp = "POST") { |
13 | |
14 | event.preventDefault() |
15 | |
16 | const form = event.target |
17 | |
18 | if (!(form instanceof HTMLFormElement)) |
19 | throw new Error("event.target no es un elemento de tipo form.") |
20 | |
21 | return consumeJson(fetch(url, { |
22 | method: metodoHttp, |
23 | headers: { "Accept": "application/json, application/problem+json" }, |
24 | body: new FormData(form) |
25 | })) |
26 | |
27 | } |
28 | |
29 | exportaAHtml(submitForm) |
1 | <?php |
2 | |
3 | const BAD_REQUEST = 400; |
4 |
1 | <?php |
2 | |
3 | function calculaArregloDeParametros(array $arreglo) |
4 | { |
5 | $parametros = []; |
6 | foreach ($arreglo as $llave => $valor) { |
7 | $parametros[":$llave"] = $valor; |
8 | } |
9 | return $parametros; |
10 | } |
11 |
1 | <?php |
2 | |
3 | function calculaSqlDeAsignaciones(string $separador, array $arreglo) |
4 | { |
5 | $primerElemento = true; |
6 | $sqlDeAsignacion = ""; |
7 | foreach ($arreglo as $llave => $valor) { |
8 | $sqlDeAsignacion .= |
9 | ($primerElemento === true ? "" : $separador) . "$llave=:$llave"; |
10 | $primerElemento = false; |
11 | } |
12 | return $sqlDeAsignacion; |
13 | } |
14 |
1 | <?php |
2 | |
3 | function calculaSqlDeCamposDeInsert(array $values) |
4 | { |
5 | $primerCampo = true; |
6 | $sqlDeCampos = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeCampos .= ($primerCampo === true ? "" : ",") . "$nombreDeValue"; |
9 | $primerCampo = false; |
10 | } |
11 | return $sqlDeCampos; |
12 | } |
13 |
1 | <?php |
2 | |
3 | function calculaSqlDeValues(array $values) |
4 | { |
5 | $primerValue = true; |
6 | $sqlDeValues = ""; |
7 | foreach ($values as $nombreDeValue => $valorDeValue) { |
8 | $sqlDeValues .= ($primerValue === true ? "" : ",") . ":$nombreDeValue"; |
9 | $primerValue = false; |
10 | } |
11 | return $sqlDeValues; |
12 | } |
13 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | function delete(PDO $pdo, string $from, array $where) |
7 | { |
8 | $sql = "DELETE FROM $from"; |
9 | |
10 | if (sizeof($where) === 0) { |
11 | $pdo->exec($sql); |
12 | } else { |
13 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
14 | $sql .= " WHERE $sqlDeWhere"; |
15 | |
16 | $statement = $pdo->prepare($sql); |
17 | $parametros = calculaArregloDeParametros($where); |
18 | $statement->execute($parametros); |
19 | } |
20 | } |
21 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | |
5 | function devuelveCreated($urlDelNuevo, $resultado) |
6 | { |
7 | |
8 | $json = json_encode($resultado); |
9 | |
10 | if ($json === false) { |
11 | |
12 | devuelveResultadoNoJson(); |
13 | } else { |
14 | |
15 | http_response_code(201); |
16 | header("Location: {$urlDelNuevo}"); |
17 | header("Content-Type: application/json"); |
18 | echo $json; |
19 | } |
20 | } |
21 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
6 | |
7 | function devuelveErrorInterno(Throwable $error) |
8 | { |
9 | devuelveProblemDetails(new ProblemDetails( |
10 | status: INTERNAL_SERVER_ERROR, |
11 | title: $error->getMessage(), |
12 | type: "/error/errorinterno.html" |
13 | )); |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | |
5 | function devuelveJson($resultado) |
6 | { |
7 | |
8 | $json = json_encode($resultado); |
9 | |
10 | if ($json === false) { |
11 | |
12 | devuelveResultadoNoJson(); |
13 | } else { |
14 | |
15 | http_response_code(200); |
16 | header("Content-Type: application/json"); |
17 | echo $json; |
18 | } |
19 | } |
20 |
1 | <?php |
2 | |
3 | function devuelveNoContent() |
4 | { |
5 | http_response_code(204); |
6 | } |
7 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/devuelveResultadoNoJson.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function devuelveProblemDetails(ProblemDetails $details) |
7 | { |
8 | |
9 | $body = ["title" => $details->title]; |
10 | if ($details->type !== null) { |
11 | $body["type"] = $details->type; |
12 | } |
13 | if ($details->detail !== null) { |
14 | $body["detail"] = $details->detail; |
15 | } |
16 | |
17 | $json = json_encode($body); |
18 | |
19 | if ($json === false) { |
20 | |
21 | devuelveResultadoNoJson(); |
22 | } else { |
23 | |
24 | http_response_code($details->status); |
25 | header("Content-Type: application/problem+json"); |
26 | echo $json; |
27 | } |
28 | } |
29 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
4 | |
5 | function devuelveResultadoNoJson() |
6 | { |
7 | |
8 | http_response_code(INTERNAL_SERVER_ERROR); |
9 | header("Content-Type: application/problem+json"); |
10 | echo '{' . |
11 | '"title": "El resultado no puede representarse como JSON."' . |
12 | '"type": "/error/resultadonojson.html"' . |
13 | '}'; |
14 | } |
15 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/ProblemDetails.php"; |
4 | require_once __DIR__ . "/devuelveProblemDetails.php"; |
5 | require_once __DIR__ . "/devuelveErrorInterno.php"; |
6 | |
7 | function ejecutaServicio(callable $codigo) |
8 | { |
9 | try { |
10 | $codigo(); |
11 | } catch (ProblemDetails $details) { |
12 | devuelveProblemDetails($details); |
13 | } catch (Throwable $error) { |
14 | devuelveErrorInterno($error); |
15 | } |
16 | } |
17 |
1 | <?php |
2 | |
3 | function fetch( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ) { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return false; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | if ($opcional === null) { |
20 | return $statement->fetch($mode); |
21 | } else { |
22 | $statement->setFetchMode($mode, $opcional); |
23 | return $statement->fetch(); |
24 | } |
25 | } |
26 | } |
27 |
1 | <?php |
2 | |
3 | function fetchAll( |
4 | PDOStatement|false $statement, |
5 | $parametros = [], |
6 | int $mode = PDO::FETCH_ASSOC, |
7 | $opcional = null |
8 | ): array { |
9 | |
10 | if ($statement === false) { |
11 | |
12 | return []; |
13 | } else { |
14 | |
15 | if (sizeof($parametros) > 0) { |
16 | $statement->execute($parametros); |
17 | } |
18 | |
19 | $resultado = $opcional === null |
20 | ? $statement->fetchAll($mode) |
21 | : $statement->fetchAll($mode, $opcional); |
22 | |
23 | if ($resultado === false) { |
24 | return []; |
25 | } else { |
26 | return $resultado; |
27 | } |
28 | } |
29 | } |
30 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaSqlDeCamposDeInsert.php"; |
4 | require_once __DIR__ . "/calculaSqlDeValues.php"; |
5 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
6 | |
7 | function insert(PDO $pdo, string $into, array $values) |
8 | { |
9 | $sqlDeCampos = calculaSqlDeCamposDeInsert($values); |
10 | $sqlDeValues = calculaSqlDeValues($values); |
11 | $sql = "INSERT INTO $into ($sqlDeCampos) VALUES ($sqlDeValues)"; |
12 | $parametros = calculaArregloDeParametros($values); |
13 | $pdo->prepare($sql)->execute($parametros); |
14 | } |
15 |
1 | <?php |
2 | |
3 | const INTERNAL_SERVER_ERROR = 500; |
1 | <?php |
2 | |
3 | const NOT_FOUND = 404; |
4 |
1 | <?php |
2 | |
3 | /** Detalle de los errores devueltos por un servicio. */ |
4 | class ProblemDetails extends Exception |
5 | { |
6 | |
7 | public int $status; |
8 | public string $title; |
9 | public ?string $type; |
10 | public ?string $detail; |
11 | |
12 | public function __construct( |
13 | int $status, |
14 | string $title, |
15 | ?string $type = null, |
16 | ?string $detail = null, |
17 | Throwable $previous = null |
18 | ) { |
19 | parent::__construct($title, $status, $previous); |
20 | $this->status = $status; |
21 | $this->type = $type; |
22 | $this->title = $title; |
23 | $this->detail = $detail; |
24 | } |
25 | } |
26 |
1 | <?php |
2 | |
3 | use function PHPSTORM_META\type; |
4 | |
5 | require_once __DIR__ . "/BAD_REQUEST.php"; |
6 | require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php"; |
7 | require_once __DIR__ . "/ProblemDetails.php"; |
8 | |
9 | function recuperaBytes(string $parametro): false|string |
10 | { |
11 | if (isset($_FILES[$parametro])) { |
12 | $path = $_FILES[$parametro]["tmp_name"]; |
13 | |
14 | if ($path === "") { |
15 | return ""; |
16 | } elseif (is_uploaded_file($path)) { |
17 | |
18 | $contents = file_get_contents($path); |
19 | |
20 | if ($contents === false) { |
21 | |
22 | switch ($_FILES[$parametro]['error']) { |
23 | |
24 | case UPLOAD_ERR_OK: |
25 | |
26 | return $contents; |
27 | |
28 | case UPLOAD_ERR_INI_SIZE: |
29 | case UPLOAD_ERR_FORM_SIZE: |
30 | |
31 | throw new ProblemDetails( |
32 | status: BAD_REQUEST, |
33 | title: "Archivo demasiado largo.", |
34 | type: "/error/archivodemasiadolargo.html", |
35 | detail: "El archivo " - $parametro . |
36 | " excede el tamaño máximo que el servidor puede recibir." |
37 | ); |
38 | |
39 | case UPLOAD_ERR_PARTIAL: |
40 | |
41 | throw new ProblemDetails( |
42 | status: INTERNAL_SERVER_ERROR, |
43 | title: "Carga incompleta de archivo.", |
44 | type: "/error/archivocargaincompleta.html", |
45 | detail: "Por una razón desconocida, el archivo " - $parametro . |
46 | " no se cargó completamente." |
47 | ); |
48 | |
49 | case UPLOAD_ERR_NO_FILE: |
50 | |
51 | throw creaArchivoNoEnviado($parametro); |
52 | |
53 | case UPLOAD_ERR_NO_TMP_DIR: |
54 | |
55 | throw new ProblemDetails( |
56 | status: INTERNAL_SERVER_ERROR, |
57 | title: "Falta la carpeta temporal.", |
58 | type: "/error/faltacarpetatemporal.html", |
59 | detail: "Por una razón desconocida, falta la carpeta temporal " . |
60 | "para cargar el archivo $parametro.", |
61 | ); |
62 | |
63 | case UPLOAD_ERR_CANT_WRITE: |
64 | |
65 | throw new ProblemDetails( |
66 | status: INTERNAL_SERVER_ERROR, |
67 | title: "El archivo no se guardó.", |
68 | type: "/error/archivonoguardado.html", |
69 | detail: "Por una razón desconocida, el archivo " - $parametro . |
70 | " no se pudo guardar en disco.", |
71 | ); |
72 | |
73 | case UPLOAD_ERR_EXTENSION: |
74 | |
75 | throw new ProblemDetails( |
76 | status: BAD_REQUEST, |
77 | title: "Extensión no permitida.", |
78 | type: "/error/extensionprohibida.html", |
79 | detail: "La extensión del archivo " - $parametro . |
80 | " no está permitida en el servidor." |
81 | ); |
82 | |
83 | default: |
84 | |
85 | throw new Exception("Error no identificado recuperando el archivo " . |
86 | $parametro . "."); |
87 | } |
88 | } else { |
89 | |
90 | return $contents; |
91 | } |
92 | } else { |
93 | |
94 | throw creaArchivoNoEnviado($parametro); |
95 | } |
96 | } else { |
97 | return false; |
98 | } |
99 | } |
100 | |
101 | function creaArchivoNoEnviado(string $parametro) |
102 | { |
103 | return new ProblemDetails( |
104 | status: BAD_REQUEST, |
105 | title: "Archivo no enviado.", |
106 | type: "/error/archivonoenviado.html", |
107 | detail: "El archivo $parametro no fué recibido por el servidor." |
108 | ); |
109 | } |
110 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/recuperaTexto.php"; |
4 | |
5 | /** |
6 | * Devuelve el valor entero de un parámetro recibido en el |
7 | * servidor por medio de GET, POST o cookie. |
8 | * |
9 | * Si el parámetro no se recibe, devuekve false |
10 | * |
11 | * Si se recibe una cadena vacía, se devuelve null. |
12 | * |
13 | * Si parámetro no se puede convertir a entero, se genera |
14 | * un error. |
15 | */ |
16 | function recuperaEntero(string $parametro): false|null|int |
17 | { |
18 | $valor = recuperaTexto($parametro); |
19 | if ($valor === false) { |
20 | return false; |
21 | } elseif ($valor === "") { |
22 | return null; |
23 | } else { |
24 | return (int) trim($valor); |
25 | } |
26 | } |
27 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/recuperaEntero.php"; |
5 | require_once __DIR__ . "/ProblemDetails.php"; |
6 | |
7 | function recuperaIdEntero(string $parametro): int |
8 | { |
9 | |
10 | $id = recuperaEntero($parametro); |
11 | |
12 | if ($id === false) |
13 | throw new ProblemDetails( |
14 | status: BAD_REQUEST, |
15 | title: "Falta el id.", |
16 | type: "/error/faltaid.html", |
17 | detail: "La solicitud no tiene el valor de id.", |
18 | ); |
19 | |
20 | if ($id === null) |
21 | throw new ProblemDetails( |
22 | status: BAD_REQUEST, |
23 | title: "Id en blanco.", |
24 | type: "/error/idenblanco.html", |
25 | ); |
26 | |
27 | return $id; |
28 | } |
29 |
1 | <?php |
2 | |
3 | /** |
4 | * Recupera el texto de un parámetro enviado al |
5 | * servidor por medio de GET, POST o cookie. |
6 | * |
7 | * Si el parámetro no se recibe, devuelve false. |
8 | */ |
9 | function recuperaTexto(string $parametro): false|string |
10 | { |
11 | /* Si el parámetro está asignado en $_REQUEST, |
12 | * devuelve su valor; de lo contrario, devuelve false. |
13 | */ |
14 | $valor = isset($_REQUEST[$parametro]) |
15 | ? $_REQUEST[$parametro] |
16 | : false; |
17 | return $valor; |
18 | } |
19 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetchAll.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | function select( |
7 | PDO $pdo, |
8 | string $from, |
9 | array $where = [], |
10 | string $orderBy = "", |
11 | int $mode = PDO::FETCH_ASSOC, |
12 | $opcional = null |
13 | ) { |
14 | $sql = "SELECT * FROM $from"; |
15 | |
16 | if (sizeof($where) > 0) { |
17 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
18 | $sql .= " WHERE $sqlDeWhere"; |
19 | } |
20 | |
21 | if ($orderBy !== "") { |
22 | $sql .= " ORDER BY $orderBy"; |
23 | } |
24 | |
25 | if (sizeof($where) === 0) { |
26 | $statement = $pdo->query($sql); |
27 | return fetchAll($statement, [], $mode, $opcional); |
28 | } else { |
29 | $statement = $pdo->prepare($sql); |
30 | $parametros = calculaArregloDeParametros($where); |
31 | return fetchAll($statement, $parametros, $mode, $opcional); |
32 | } |
33 | } |
34 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/fetch.php"; |
4 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
5 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
6 | |
7 | function selectFirst( |
8 | PDO $pdo, |
9 | string $from, |
10 | array $where = [], |
11 | string $orderBy = "", |
12 | int $mode = PDO::FETCH_ASSOC, |
13 | $opcional = null |
14 | ) { |
15 | $sql = "SELECT * FROM $from"; |
16 | |
17 | if (sizeof($where) > 0) { |
18 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
19 | $sql .= " WHERE $sqlDeWhere"; |
20 | } |
21 | |
22 | if ($orderBy !== "") { |
23 | $sql .= " ORDER BY $orderBy"; |
24 | } |
25 | |
26 | if (sizeof($where) === 0) { |
27 | $statement = $pdo->query($sql); |
28 | return fetch($statement, [], $mode, $opcional); |
29 | } else { |
30 | $statement = $pdo->prepare($sql); |
31 | $parametros = calculaArregloDeParametros($where); |
32 | return fetch($statement, $parametros, $mode, $opcional); |
33 | } |
34 | } |
35 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/calculaArregloDeParametros.php"; |
4 | require_once __DIR__ . "/calculaSqlDeAsignaciones.php"; |
5 | |
6 | |
7 | function update(PDO $pdo, string $table, array $set, array $where) |
8 | { |
9 | $sqlDeSet = calculaSqlDeAsignaciones(",", $set); |
10 | $sqlDeWhere = calculaSqlDeAsignaciones(" AND ", $where); |
11 | $sql = "UPDATE $table SET $sqlDeSet WHERE $sqlDeWhere"; |
12 | |
13 | $parametros = calculaArregloDeParametros($set); |
14 | foreach ($where as $nombreDeWhere => $valorDeWhere) { |
15 | $parametros[":$nombreDeWhere"] = $valorDeWhere; |
16 | } |
17 | $statement = $pdo->prepare($sql); |
18 | $statement->execute($parametros); |
19 | } |
20 |
1 | <?php |
2 | |
3 | require_once __DIR__ . "/BAD_REQUEST.php"; |
4 | require_once __DIR__ . "/ProblemDetails.php"; |
5 | |
6 | function validaNombre(false|string $nombre) |
7 | { |
8 | |
9 | if ($nombre === false) |
10 | throw new ProblemDetails( |
11 | status: BAD_REQUEST, |
12 | title: "Falta el nombre.", |
13 | type: "/error/faltanombre.html", |
14 | detail: "La solicitud no tiene el valor de nombre." |
15 | ); |
16 | |
17 | $trimNombre = trim($nombre); |
18 | |
19 | if ($trimNombre === "") |
20 | throw new ProblemDetails( |
21 | status: BAD_REQUEST, |
22 | title: "Nombre en blanco.", |
23 | type: "/error/nombreenblanco.html", |
24 | detail: "Pon texto en el campo nombre.", |
25 | ); |
26 | |
27 | return $trimNombre; |
28 | } |
29 |
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>Carga incompleta de archivo</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Carga incompleta de archivo</h1> |
16 | |
17 | <p>Por una razón desconocida, un archivo no se cargó completamente.</p> |
18 | |
19 | </body> |
20 | |
21 | </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 demasiado largo</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Archivo demasiado largo</h1> |
16 | |
17 | <p>Un archivo excede el tamaño máximo que el servidor puede recibir.</p> |
18 | |
19 | </body> |
20 | |
21 | </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 no enviado</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Archivo no enviado</h1> |
16 | |
17 | <p>Un archivo no fué recibido por el servidor.</p> |
18 | |
19 | </body> |
20 | |
21 | </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 archivo no se guardó</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>El archivo no se guardó</h1> |
16 | |
17 | <p>Por una razón desconocida, un archivo no se pudo guardar en disco.</p> |
18 | |
19 | </body> |
20 | |
21 | </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> |
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>Extensión no permitida</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Extensión no permitida</h1> |
16 | |
17 | <p>La extensión de un archivo no está permitida en el servidor.</p> |
18 | |
19 | </body> |
20 | |
21 | </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 carpeta temporal</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Falta la carpeta temporal</h1> |
16 | |
17 | <p> |
18 | Por una razón desconocida, falta la carpeta temporal para cargar archivos. |
19 | </p> |
20 | |
21 | </body> |
22 | |
23 | </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>La solicitud no tiene el valor de id.</p> |
18 | |
19 | </body> |
20 | |
21 | </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 imagen</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Falta la imagen</h1> |
16 | |
17 | <p>La solicitud no tiene el valor de imagen.</p> |
18 | |
19 | </body> |
20 | |
21 | </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 | <p>La solicitud no tiene el valor de nombre.</p> |
18 | |
19 | </body> |
20 | |
21 | </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>Id en blanco</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Id en blanco</h1> |
16 | |
17 | </body> |
18 | |
19 | </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>Imagen vacía</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Imagen vacía</h1> |
16 | |
17 | <p>Selecciona un archivo que no esté vacío.</p> |
18 | |
19 | </body> |
20 | |
21 | </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>Nombre en blanco</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>Nombre en blanco</h1> |
16 | |
17 | <p>Pon texto en el campo nombre.</p> |
18 | |
19 | </body> |
20 | |
21 | </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> |
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 resultado no puede representarse como JSON</title> |
10 | |
11 | </head> |
12 | |
13 | <body> |
14 | |
15 | <h1>El resultado no puede representarse como JSON</h1> |
16 | |
17 | <p> |
18 | Debido a un error interno del servidor, el resultado generado, no se puede |
19 | recuperar. |
20 | </p> |
21 | |
22 | </body> |
23 | |
24 | </html> |
Este archivo ayuda a detectar errores en los archivos del proyecto.
Lo utiliza principalmente Visual Studio Code.
No se explica aquí su estructura, pero puede encontrarse la explicación de todo en la documentación del sitio de Visual Studio Code.
1 | { |
2 | "compilerOptions": { |
3 | "checkJs": true, |
4 | "strictNullChecks": true, |
5 | "target": "ES6", |
6 | "module": "Node16", |
7 | "moduleResolution": "Node16", |
8 | "lib": [ |
9 | "ES2017", |
10 | "WebWorker", |
11 | "DOM" |
12 | ] |
13 | } |
14 | } |
En esta lección se mostró el almacenamiento de un archivo por registro de la base de datos, usando servicios.