| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 1 | <?php |
| 2 | |
| 3 | const BAD_REQUEST = 400; |
| 4 |
| 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 |
| 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 |
| 1 | <?php |
| 2 | |
| 3 | const INTERNAL_SERVER_ERROR = 500; |
| 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 |
| 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 |
| 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 |