N. Carpeta « php »

Versión para imprimir.

A. php / Bd.php

1
<?php
2
3
class Bd
4
{
5
6
 private static ?PDO $pdo = null;
7
8
 static function pdo(): PDO
9
 {
10
  if (self::$pdo === null) {
11
   self::$pdo = new PDO(
12
    // cadena de conexión
13
    "sqlite:" . __DIR__ . "/sincro.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 PASATIEMPO (
24
      PAS_ID TEXT NOT NULL,
25
      PAS_NOMBRE TEXT NOT NULL,
26
      PAS_MODIFICACION INTEGER NOT NULL,
27
      PAS_ELIMINADO INTEGER NOT NULL,
28
      CONSTRAINT PAS_PK
29
       PRIMARY KEY(PAS_ID),
30
      CONSTRAINT PAS_ID_NV
31
       CHECK(LENGTH(PAS_ID) > 0),
32
      CONSTRAINT PAS_NOM_NV
33
       CHECK(LENGTH(PAS_NOMBRE) > 0)
34
     )'
35
   );
36
  }
37
38
  return self::$pdo;
39
 }
40
}
41

B. php / pasatiempoAgrega.php

1
<?php
2
3
require_once __DIR__ . "/Bd.php";
4
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
5
6
/**
7
 * @param array{
8
 *   PAS_ID: string,
9
 *   PAS_NOMBRE: string,
10
 *   PAS_MODIFICACION: int,
11
 *   PAS_ELIMINADO: int
12
 *  } $modelo
13
 */
14
function pasatiempoAgrega(array $modelo)
15
{
16
 $bd = Bd::pdo();
17
 $stmt = $bd->prepare(
18
  "INSERT INTO PASATIEMPO (
19
    PAS_ID, PAS_NOMBRE, PAS_MODIFICACION, PAS_ELIMINADO
20
   ) values (
21
    :PAS_ID, :PAS_NOMBRE, :PAS_MODIFICACION, :PAS_ELIMINADO
22
   )"
23
 );
24
 $stmt->execute([
25
  ":PAS_ID" => $modelo[PAS_ID],
26
  ":PAS_NOMBRE" => $modelo[PAS_NOMBRE],
27
  ":PAS_MODIFICACION" => $modelo[PAS_MODIFICACION],
28
  ":PAS_ELIMINADO" => $modelo[PAS_ELIMINADO],
29
 ]);
30
}
31

C. php / pasatiempoBusca.php

1
<?php
2
3
require_once __DIR__ . "/Bd.php";
4
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
5
6
/**
7
 * @return false | array{
8
 *   PAS_ID: string,
9
 *   PAS_NOMBRE: string,
10
 *   PAS_MODIFICACION: int,
11
 *   PAS_ELIMINADO: int
12
 *  }
13
 */
14
function pasatiempoBusca(string $id): false|array
15
{
16
 $bd = Bd::pdo();
17
 $stmt = $bd->prepare("SELECT * FROM PASATIEMPO WHERE PAS_ID = :PAS_ID");
18
 $stmt->execute([":PAS_ID" => $id]);
19
 $modelo = $stmt->fetch(PDO::FETCH_ASSOC);
20
 return $modelo;
21
}
22

D. php / pasatiempoConsultaNoEliminados.php

1
<?php
2
3
require_once __DIR__ . "/Bd.php";
4
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
5
6
/**
7
 * @return array{
8
 *   PAS_ID: string,
9
 *   PAS_NOMBRE: string,
10
 *   PAS_MODIFICACION: int,
11
 *   PAS_ELIMINADO: int
12
 *  }[]
13
 */
14
function pasatiempoConsultaNoEliminados()
15
{
16
 $bd = Bd::pdo();
17
$stmt = $bd->query(
18
 "SELECT * FROM PASATIEMPO WHERE PAS_ELIMINADO = 0 ORDER BY PAS_NOMBRE"
19
);
20
$lista = $stmt->fetchAll(PDO::FETCH_ASSOC);
21
return $lista;
22
}
23

E. php / pasatiempoModifica.php

1
<?php
2
3
require_once __DIR__ . "/Bd.php";
4
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
5
6
/**
7
 * @param array{
8
 *   PAS_ID: string,
9
 *   PAS_NOMBRE: string,
10
 *   PAS_MODIFICACION: int,
11
 *   PAS_ELIMINADO: int
12
 *  } $modelo
13
 */
14
function pasatiempoModifica(array $modelo)
15
{
16
 $bd = Bd::pdo();
17
 $stmt = $bd->prepare(
18
  "UPDATE PASATIEMPO
19
   SET
20
    PAS_NOMBRE = :PAS_NOMBRE,
21
    PAS_MODIFICACION = :PAS_MODIFICACION,
22
    PAS_ELIMINADO = :PAS_ELIMINADO
23
   WHERE
24
    PAS_ID = :PAS_ID"
25
 );
26
 $stmt->execute([
27
  ":PAS_ID" => $modelo[PAS_ID],
28
  ":PAS_NOMBRE" => $modelo[PAS_NOMBRE],
29
  ":PAS_MODIFICACION" => $modelo[PAS_MODIFICACION],
30
  ":PAS_ELIMINADO" => $modelo[PAS_ELIMINADO],
31
 ]);
32
}
33

F. php / sincroniza.php

1
<?php
2
3
require_once __DIR__ . "/lib/manejaErrores.php";
4
require_once __DIR__ . "/lib/recibeJson.php";
5
require_once __DIR__ . "/lib/devuelveJson.php";
6
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
7
require_once __DIR__ . "/validaPasatiempo.php";
8
require_once __DIR__ . "/pasatiempoAgrega.php";
9
require_once __DIR__ . "/pasatiempoBusca.php";
10
require_once __DIR__ . "/pasatiempoConsultaNoEliminados.php";
11
require_once __DIR__ . "/pasatiempoModifica.php";
12
13
 $lista = recibeJson();
14
15
 if (!is_array($lista)) {
16
  $lista = [];
17
 }
18
19
 foreach ($lista as $modelo) {
20
  $modeloEnElCliente = validaPasatiempo($modelo);
21
  $modeloEnElServidor = pasatiempoBusca($modeloEnElCliente[PAS_ID]);
22
23
  if ($modeloEnElServidor === false) {
24
25
   /* CONFLICTO: El modelo no ha estado en el servidor.
26
    * AGREGARLO solamente si no está eliminado. */
27
   if ($modeloEnElCliente[PAS_ELIMINADO] === 0) {
28
    pasatiempoAgrega($modeloEnElCliente);
29
   }
30
  } elseif (
31
   $modeloEnElServidor[PAS_ELIMINADO] === 0
32
   && $modeloEnElCliente[PAS_ELIMINADO] === 1
33
  ) {
34
35
   /* CONFLICTO: El registro está en el servidor, donde no se ha eliminado, pero
36
    * ha sido eliminado en el cliente.
37
    * Gana el cliente, porque optamos por no revivir lo eliminado. */
38
   pasatiempoModifica($modeloEnElCliente);
39
  } else if (
40
   $modeloEnElCliente[PAS_ELIMINADO] === 0
41
   && $modeloEnElServidor[PAS_ELIMINADO] === 0
42
  ) {
43
44
   /* CONFLICTO: Registros en el servidor y en el cliente. Pueden ser
45
    * diferentes.
46
    * GANA FECHA MÁS GRANDE. Cuando gana el servidor, no se hace nada. */
47
   if (
48
    $modeloEnElCliente[PAS_MODIFICACION] >
49
    $modeloEnElServidor[PAS_MODIFICACION]
50
   ) {
51
    // La versión del cliente es más nueva y prevalece.
52
    pasatiempoModifica($modeloEnElCliente);
53
   }
54
  }
55
 }
56
57
 $lista = pasatiempoConsultaNoEliminados();
58
59
 devuelveJson($lista);
60

G. php / TABLA_PASATIEMPO.php

1
<?php
2
3
const PASATIEMPO = "PASATIEMPO";
4
const PAS_ID = "PAS_ID";
5
const PAS_NOMBRE = "PAS_NOMBRE";
6
const PAS_MODIFICACION = "PAS_MODIFICACION";
7
const PAS_ELIMINADO = "PAS_ELIMINADO";
8

H. php / validaPasatiempo.php

1
<?php
2
3
require_once __DIR__ . "/lib/BAD_REQUEST.php";
4
require_once __DIR__ . "/lib/ProblemDetailsException.php";
5
require_once __DIR__ . "/TABLA_PASATIEMPO.php";
6
7
function validaPasatiempo($objeto)
8
{
9
 if (!isset($objeto->PAS_ELIMINADO) || !is_int($objeto->PAS_ELIMINADO))
10
  throw new ProblemDetailsException([
11
   "status" => BAD_REQUEST,
12
   "title" => "El campo eliminado debe ser entero.",
13
   "type" => "/errors/eliminadoincorrecto.html",
14
  ]);
15
16
 if (
17
  !isset($objeto->PAS_ID)
18
  || !is_string($objeto->PAS_ID)
19
  || $objeto->PAS_ID === ""
20
 )
21
  throw new ProblemDetailsException([
22
   "status" => BAD_REQUEST,
23
   "title" => "El id debe ser texto que no esté en blanco.",
24
   "type" => "/errors/idincorrecto.html",
25
  ]);
26
27
 if (!isset($objeto->PAS_MODIFICACION) || !is_int($objeto->PAS_MODIFICACION))
28
  throw new ProblemDetailsException([
29
   "status" => BAD_REQUEST,
30
   "title" => "La modificacion debe ser número.",
31
   "type" => "/errors/modificacionincorrecta.html",
32
  ]);
33
34
 if (
35
  !isset($objeto->PAS_NOMBRE)
36
  || !is_string($objeto->PAS_NOMBRE)
37
  || $objeto->PAS_NOMBRE === ""
38
 )
39
  throw new ProblemDetailsException([
40
   "status" => BAD_REQUEST,
41
   "title" => "El nombre debe ser texto que no esté en blanco.",
42
   "type" => "/errors/nombreincorrecto.html",
43
  ]);
44
45
 return [
46
  PAS_ELIMINADO => $objeto->PAS_ELIMINADO,
47
  PAS_ID => $objeto->PAS_ID,
48
  PAS_NOMBRE => $objeto->PAS_NOMBRE,
49
  PAS_MODIFICACION => $objeto->PAS_MODIFICACION,
50
 ];
51
}
52

I. Carpeta « php / lib »

Versión para imprimir.

1. php / lib / BAD_REQUEST.php

1
<?php
2
3
const BAD_REQUEST = 400;
4

2. php / lib / devuelveJson.php

1
<?php
2
3
require_once __DIR__ . "/devuelveResultadoNoJson.php";
4
5
function devuelveJson($resultado)
6
{
7
 $json = json_encode($resultado);
8
 if ($json === false) {
9
  devuelveResultadoNoJson();
10
 } else {
11
  header("Content-Type: application/json; charset=utf-8");
12
  echo $json;
13
 }
14
 exit();
15
}
16

3. php / lib / devuelveResultadoNoJson.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5
function devuelveResultadoNoJson()
6
{
7
 http_response_code(INTERNAL_SERVER_ERROR);
8
 header("Content-Type: application/problem+json; charset=utf-8");
9
10
 echo '{' .
11
  "status: " . INTERNAL_SERVER_ERROR .
12
  '"title": "El resultado no puede representarse como JSON."' .
13
  '"type": "/errors/resultadonojson.html"' .
14
  '}';
15
}
16

4. php / lib / INTERNAL_SERVER_ERROR.php

1
<?php
2
3
const INTERNAL_SERVER_ERROR = 500;

5. php / lib / manejaErrores.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
require_once __DIR__ . "/ProblemDetailsException.php";
5
6
// Hace que se lance una excepción automáticamente cuando se genere un error.
7
set_error_handler(function ($severity, $message, $file, $line) {
8
 throw new ErrorException($message, 0, $severity, $file, $line);
9
});
10
11
// Código cuando una excepción no es atrapada.
12
set_exception_handler(function (Throwable $excepcion) {
13
 if ($excepcion instanceof ProblemDetailsException) {
14
  devuelveProblemDetails($excepcion->problemDetails);
15
 } else {
16
  devuelveProblemDetails([
17
   "status" => INTERNAL_SERVER_ERROR,
18
   "title" => "Error interno del servidor",
19
   "detail" => $excepcion->getMessage(),
20
   "type" => "/errors/errorinterno.html",
21
  ]);
22
 }
23
 exit();
24
});
25
26
function devuelveProblemDetails(array $array)
27
{
28
 $json = json_encode($array);
29
 if ($json === false) {
30
  devuelveResultadoNoJson();
31
 } else {
32
  http_response_code(isset($array["status"]) ? $array["status"] : 500);
33
  header("Content-Type: application/problem+json; charset=utf-8");
34
  echo $json;
35
 }
36
}
37

6. php / lib / ProblemDetailsException.php

1
<?php
2
3
require_once __DIR__ . "/INTERNAL_SERVER_ERROR.php";
4
5
/**
6
 * Detalle de los errores devueltos por un servicio.
7
 */
8
class ProblemDetailsException extends Exception
9
{
10
11
 public array $problemDetails;
12
13
 public function __construct(
14
  array $problemDetails,
15
 ) {
16
  
17
  parent::__construct(
18
   isset($problemDetails["detail"])
19
    ? $problemDetails["detail"]
20
    : (isset($problemDetails["title"])
21
     ? $problemDetails["title"]
22
     : "Error"),
23
   $problemDetails["status"]
24
    ? $problemDetails["status"]
25
    : INTERNAL_SERVER_ERROR
26
  );
27
28
  $this->problemDetails = $problemDetails;
29
 }
30
}
31

7. php / lib / recibeJson.php

1
<?php
2
3
require_once __DIR__ . "/BAD_REQUEST.php";
4
5
function recibeJson()
6
{
7
 $json = json_decode(file_get_contents("php://input"));
8
9
 if ($json === null) {
10
11
  http_response_code(BAD_REQUEST);
12
  header("Content-Type: application/problem+json; charset=utf-8");
13
14
  echo '{' .
15
   "status: " . BAD_REQUEST .
16
   '"title": "Los datos recibidos no están en formato JSON."' .
17
   '"type": "/errors/datosnojson.html"' .
18
   '}';
19
20
  exit();
21
 }
22
23
 return $json;
24
}
25