9. Servicio que devuelve JSON

Versión para imprimir.

A. Introducción

Este ejemplo recibe los datos JSON generados por un servicio.

Puedes probar el ejemplo en https://replit.com/@GilbertoPachec5/srvresultado?v=1. Hazle fork al proyecto y córrelo.

B. La función invocaServicio

C. La función ejecutaServicio

D. Las clases JsonResponse de JavaScript y PHP

E. Las clases ProblemDetails de JavaScript y PHP

F. La función muestraError

G. Diagrama de despliegue

Diagrama de despliegue

H. Funcionamiento

Versión para imprimir.

1. Iniciamos al ejecutar código en el cliente

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

2. Se invoca el servicio en el servidor

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Ejecuta invocaServicio y
envía request (solicitud).

Request

URL
srv/devuelve.php
Method
GET

srv/devuelve.php

return [
 "nombre" => "pp",
 "mensaje" => "Hola."
];

Despierta y recibe request.

3. El servicio procesa la request y genera la response

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Hace wait esperando response.

srv/devuelve.php

return [
 "nombre" => "pp",
 "mensaje" => "Hola."
];

Procesa la request y
genera response
(respuesta).

Response

code
200
body
{"nombre":"pp","mensaje":"Hola."}

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

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Despierta y recibe response.

Response

code
200
body
{"nombre":"pp","mensaje":"Hola."}

Memoria

respuesta
status
200
body
nombre
"pp"
mensaje
"Hola."

srv/devuelve.php

return [
 "nombre" => "pp",
 "mensaje" => "Hola."
];

Devuelve response y se duerme.

5. Se crea la constante body, que apunta al cuerpo de la respuesta

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Memoria

respuesta
status
200
body
nombre
"pp"
mensaje
"Hola."
body
ref->respuesta.body

6. Muestra el objeto recibido en un alert

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

Memoria

respuesta
status
200
body
nombre
"pp"
mensaje
"Hola."
body
ref->respuesta.body

Alert

Nombre: pp
Mensaje: Hola.

7. Al cerrar el alert, termina el evento

index.html

const respuesta =
 await invocaServicio(
  "srv/devuelve.php")
const body = respuesta.body
alert(`Nombre: ${body.nombre}
Mensaje: ${body.mensaje}`)

I. Hazlo funcionar

  1. Revisa el proyecto en Replit con la URL https://replit.com/@GilbertoPachec5/srvjson?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.

J. Archivos

Haz clic en los triángulos para expandir las carpetas

K. 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 devuelve un resultado</title>
10
11</head>
12
13<body>
14
15 <h1>Servicio que devuelve un resultado</h1>
16
17 <p><button onclick="resultado()">Resultado</button></p>
18
19 <script type="module">
20
21 import { invocaServicio } from "./lib/js/invocaServicio.js"
22 import { muestraError } from "./lib/js/muestraError.js"
23
24 async function resultado() {
25 try {
26 const respuesta =
27 await invocaServicio(
28 "srv/devuelve.php")
29 const body = respuesta.body
30 alert(`Nombre: ${body.nombre}
31Mensaje: ${body.mensaje}`)
32 } catch (error) {
33 muestraError(error)
34 }
35 }
36 // Permite que los eventos de html usen la función.
37 window["resultado"] = resultado
38
39 </script>
40
41</body>
42
43</html>

L. Carpeta « srv »

Versión para imprimir.

A. srv / devuelve.php

1<?php
2
3require_once __DIR__ . "/../lib/php/ejecutaServicio.php";
4
5ejecutaServicio(function () {
6 return [
7 "nombre" => "pp",
8 "mensaje" => "Hola."
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 / nojson.html

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

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

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

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

P. Resumen