11. Servicio que procesa una forma

Versión para imprimir.

A. Introducción

B. La función submitForm

C. La función leeTexto

D. Diagrama de despliegue

Diagrama de despliegue

E. Funcionamiento

Versión para imprimir.

1. El usuario captura datos y activa la forma

Forma

2. Se activa el código del evento submit.

Forma

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

3. Se invoca el servicio, incluyendo los datos de la forma

Forma

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Request

URL
srv/procesa.php
Method
POST
body
saludo
hola
nombre
pp

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Despierta y recibe request.

4. El servicio lee los datos

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Hace wait esperando response.

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Request

URL
srv/procesa.php
Method
POST
body
saludo
hola
nombre
pp

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"

5. El servicio procesa los datos

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Hace wait esperando response.

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"
$resultado
"hola pp"

6. El servicio genera la response

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Hace wait esperando response.

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Memoria (Servidor)

$saludo
"hola"
$nombre
"pp"
$resultado
"hola pp"

Response

code
200
body
"hola pp"

7. El servicio devuelve la response, que es recibida en el cliente

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Despierta y recibe response.

Response

code
200
body
"hola pp"

Memoria

respuesta
status
200
body
"Hola pp."

srv/procesa.php

$saludo = leeTexto("saludo");
$nombre = leeTexto("nombre");
$resultado =
 "{$saludo} {$nombre}.";
return $resultado;

Devuelve response y se duerme.

8. Muestra el texto recibido en un alert

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

Memoria

respuesta
status
200
body
"Hola pp."

Alert

hola pp

9. Al cerrar el alert, termina el evento

index.html

const respuesta =
 await submitForm(
  "srv/procesa.php", event)
alert(respuesta.body)

F. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/srvform?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>Servicio que procesa un formulario</title>
10
11</head>
12
13<body>
14
15 <h1>Servicio que procesa un formulario</h1>
16
17 <form onsubmit="procesaForm(event) ">
18
19 <p>
20 <label>
21 Saludo:
22 <!-- Como este input tiene name="saludo", su valor se recupera en el
23 servidor con leeTexto("saludo") -->
24 <input name="saludo">
25 </label>
26 </p>
27
28 <p>
29 <label>
30 Nombre:
31 <!-- Como este input tiene name="nombre", su valor se recupera en el
32 servidor con leeTexto("nombre") -->
33 <input name="nombre">
34 </label>
35 </p>
36
37 <p><button type="submit">Procesa</button></p>
38
39 </form>
40
41 <script type="module">
42
43 import { muestraError } from "./lib/js/muestraError.js"
44 import { submitForm } from "./lib/js/submitForm.js"
45
46 /**
47 * @param {Event} event
48 */
49 async function procesaForm(event) {
50 try {
51 const respuesta =
52 await submitForm(
53 "srv/procesa.php", event)
54 alert(respuesta.body)
55 } catch (error) {
56 muestraError(error)
57 }
58 }
59 // Permite que los eventos de html usen la función.
60 window["procesaForm"] = procesaForm
61
62 </script>
63
64</body>
65
66</html>

I. Carpeta « srv »

Versión para imprimir.

A. srv / procesa.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4require_once __DIR__ . "/../lib/php/leeTexto.php";
5
6ejecutaServicio(function () {
7 $saludo = leeTexto("saludo");
8 $nombre = leeTexto("nombre");
9 $resultado =
10 "{$saludo} {$nombre}.";
11 return $resultado;
12});
13

J. Carpeta « lib »

Versión para imprimir.

A. Carpeta « lib / js »

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

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

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

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

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

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

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

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

M. Resumen