APIs con PHP

Creación de APIs con PHP

Objetivos de Aprendizaje
  • Diseñar y crear una API REST con PHP nativo
  • Implementar operaciones CRUD en la API
  • Comprender la estructura de una API profesional
  • Manejar respuestas y errores en el formato adecuado
  • Implementar seguridad básica en APIs

Arquitectura de una API REST con PHP

Antes de crear nuestra API, vamos a entender la estructura de archivos que utilizaremos:

Estructura de archivos
  • api/
    • index.php (punto de entrada)
    • db.php (conexión a la BD)
    • UsuarioController.php (lógica)
    • config.php (configuración)
Flujo de la API
  1. index.php - Recibe todas las peticiones HTTP y actúa como enrutador
  2. db.php - Establece la conexión con la base de datos
  3. UsuarioController.php - Contiene las funciones para manejar las operaciones CRUD
  4. Los controladores ejecutan consultas en la base de datos
  5. Se genera y devuelve una respuesta JSON con código HTTP apropiado

Conexión a la base de datos (db.php)

Primero, creamos el archivo de conexión a la base de datos:

db.php

<?php
$host = 'localhost';
$dbname = 'prueba';
$user = 'root';
$pass = '';

try {
    // Crear conexión PDO
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $user, $pass);
    
    // Configurar PDO para mostrar errores
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
    // En caso de error, devolver respuesta de error en JSON
    http_response_code(500);
    echo json_encode(["error" => "Error de conexión: " . $e->getMessage()]);
    exit;
}
?>

Controlador de usuarios (UsuarioController.php)

A continuación, creamos el controlador que manejará las operaciones CRUD para los usuarios:

UsuarioController.php

<?php
// Incluir archivo de conexión a la BD
require_once 'db.php';

/**
 * Obtener todos los usuarios
 * Método: GET
 * URL: /usuarios
 */
function obtenerUsuarios() {
    global $pdo;
    try {
        $stmt = $pdo->query("SELECT * FROM usuarios");
        $usuarios = $stmt->fetchAll(PDO::FETCH_ASSOC);
        echo json_encode($usuarios);
    } catch (PDOException $e) {
        http_response_code(500);
        echo json_encode(["error" => "Error al obtener usuarios"]);
    }
}

/**
 * Obtener un usuario por ID
 * Método: GET
 * URL: /usuarios/{id}
 * @param int $id ID del usuario
 */
function obtenerUsuario($id) {
    global $pdo;
    try {
        $stmt = $pdo->prepare("SELECT * FROM usuarios WHERE id = ?");
        $stmt->execute([$id]);
        $usuario = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($usuario) {
            echo json_encode($usuario);
        } else {
            http_response_code(404);
            echo json_encode(["error" => "Usuario no encontrado"]);
        }
    } catch (PDOException $e) {
        http_response_code(500);
        echo json_encode(["error" => "Error al obtener usuario"]);
    }
}

/**
 * Crear un nuevo usuario
 * Método: POST
 * URL: /usuarios
 * @param array $data Datos del usuario a crear
 */
function crearUsuario($data) {
    global $pdo;

    // Validar datos necesarios
    if (!isset($data['nombre']) || !isset($data['correo'])) {
        http_response_code(400);
        echo json_encode(["error" => "Nombre y correo son obligatorios"]);
        return;
    }

    try {
        $stmt = $pdo->prepare("INSERT INTO usuarios (nombre, correo) VALUES (?, ?)");
        $stmt->execute([$data['nombre'], $data['correo']]);
        http_response_code(201); // 201 Created
        echo json_encode(["mensaje" => "Usuario creado"]);
    } catch (PDOException $e) {
        http_response_code(500);
        echo json_encode(["error" => "Error al crear usuario"]);
    }
}

/**
 * Actualizar un usuario existente
 * Método: PUT
 * URL: /usuarios/{id}
 * @param int $id ID del usuario
 * @param array $data Datos actualizados
 */
function actualizarUsuario($id, $data) {
    global $pdo;

    // Validar datos necesarios
    if (!isset($data['nombre']) || !isset($data['correo'])) {
        http_response_code(400);
        echo json_encode(["error" => "Nombre y correo son obligatorios"]);
        return;
    }

    try {
        $stmt = $pdo->prepare("UPDATE usuarios SET nombre = ?, correo = ? WHERE id = ?");
        $stmt->execute([$data['nombre'], $data['correo'], $id]);

        if ($stmt->rowCount() > 0) {
            echo json_encode(["mensaje" => "Usuario actualizado"]);
        } else {
            http_response_code(404);
            echo json_encode(["error" => "Usuario no encontrado o sin cambios"]);
        }
    } catch (PDOException $e) {
        http_response_code(500);
        echo json_encode(["error" => "Error al actualizar usuario"]);
    }
}

/**
 * Eliminar un usuario
 * Método: DELETE
 * URL: /usuarios/{id}
 * @param int $id ID del usuario
 */
function eliminarUsuario($id) {
    global $pdo;
    try {
        $stmt = $pdo->prepare("DELETE FROM usuarios WHERE id = ?");
        $stmt->execute([$id]);

        if ($stmt->rowCount() > 0) {
            echo json_encode(["mensaje" => "Usuario eliminado"]);
        } else {
            http_response_code(404);
            echo json_encode(["error" => "Usuario no encontrado"]);
        }
    } catch (PDOException $e) {
        http_response_code(500);
        echo json_encode(["error" => "Error al eliminar usuario"]);
    }
}
?>

Punto de entrada de la API (index.php)

Finalmente, creamos el archivo index.php que actuará como enrutador para todas las peticiones:

index.php

<?php
// Configurar cabeceras para API REST
header("Content-Type: application/json");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type, Authorization");

// Para peticiones OPTIONS (CORS preflight)
if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
    exit(0);
}

// Incluir el controlador
require_once 'UsuarioController.php';

// Obtener método HTTP
$method = $_SERVER['REQUEST_METHOD'];

// Analizar la ruta solicitada
$path = explode('/', trim($_SERVER['PATH_INFO'] ?? '', '/'));
$resource = $path[0] ?? null;
$id = $path[1] ?? null;

// Enrutamiento basado en método HTTP y recurso
switch ($method) {
    case 'GET':
        if ($resource === 'usuarios') {
            $id ? obtenerUsuario($id) : obtenerUsuarios();
        }
        break;

    case 'POST':
        if ($resource === 'usuarios') {
            $data = json_decode(file_get_contents("php://input"), true);
            crearUsuario($data);
        }
        break;

    case 'PUT':
        if ($resource === 'usuarios' && $id) {
            $data = json_decode(file_get_contents("php://input"), true);
            actualizarUsuario($id, $data);
        }
        break;

    case 'DELETE':
        if ($resource === 'usuarios' && $id) {
            eliminarUsuario($id);
        }
        break;

    default:
        http_response_code(405); // Method Not Allowed
        echo json_encode(["error" => "Método no permitido"]);
}
?>

Creación de la Base de Datos

Para que nuestra API funcione, necesitamos crear la tabla usuarios en la base de datos:

crear_tabla.sql

-- Crear la base de datos si no existe
CREATE DATABASE IF NOT EXISTS prueba;

-- Usar la base de datos
USE prueba;

-- Crear la tabla usuarios
CREATE TABLE IF NOT EXISTS usuarios (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nombre VARCHAR(100) NOT NULL,
  correo VARCHAR(100) NOT NULL UNIQUE,
  fecha_registro TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insertar algunos usuarios de prueba
INSERT INTO usuarios (nombre, correo) VALUES
('Juan Pérez', 'juan@example.com'),
('María García', 'maria@example.com');

Probando la API

Una vez que hayamos creado todos los archivos y la base de datos, podemos probar nuestra API con herramientas como Postman o utilizando cURL desde la línea de comandos:

Probar con cURL

# GET - Obtener todos los usuarios
curl http://localhost/api/usuarios

# GET - Obtener un usuario por ID
curl http://localhost/api/usuarios/1

# POST - Crear un nuevo usuario
curl -X POST http://localhost/api/usuarios \
  -H "Content-Type: application/json" \
  -d '{"nombre":"Ana Rodríguez","correo":"ana@example.com"}'

# PUT - Actualizar un usuario
curl -X PUT http://localhost/api/usuarios/1 \
  -H "Content-Type: application/json" \
  -d '{"nombre":"Juan Pérez Actualizado","correo":"juan.nuevo@example.com"}'

# DELETE - Eliminar un usuario
curl -X DELETE http://localhost/api/usuarios/2
                                    
Ejemplo de respuesta JSON

// Respuesta GET /usuarios
[
  {
    "id": "1",
    "nombre": "Juan Pérez",
    "correo": "juan@example.com",
    "fecha_registro": "2023-05-15 10:30:45"
  },
  {
    "id": "2",
    "nombre": "María García",
    "correo": "maria@example.com",
    "fecha_registro": "2023-05-15 10:30:45"
  }
]

// Respuesta POST /usuarios (201 Created)
{
  "mensaje": "Usuario creado"
}

// Respuesta de error (404 Not Found)
{
  "error": "Usuario no encontrado"
}
                                    

Seguridad en APIs

Para una API en producción, es importante implementar medidas de seguridad:

Medidas de seguridad recomendadas
  1. Autenticación: Implementar JWT, API Keys o OAuth 2.0
  2. HTTPS: Asegurar todas las comunicaciones con cifrado SSL/TLS
  3. Validación de entradas: Sanitizar y validar todos los datos de entrada
  4. Rate limiting: Limitar el número de solicitudes por usuario/IP
  5. Logs de auditoría: Registrar todas las operaciones importantes
  6. CORS: Configurar correctamente las políticas de acceso entre dominios
  7. Protección contra SQL Injection: Usar consultas preparadas (como ya hicimos)
  8. Gestión adecuada de errores: No revelar información sensible en errores
api_key_auth.php

<?php
// Ejemplo simple de autenticación por API Key
function verificarApiKey() {
    // En producción, estas claves deberían almacenarse de forma segura
    $apiKeys = [
        'abcd1234xyz' => 'usuario_cliente',
        'efgh5678abc' => 'usuario_admin'
    ];
    
    // Obtener encabezados
    $headers = getallheaders();
    
    // Verificar si existe el encabezado X-API-Key
    if (!isset($headers['X-API-Key'])) {
        http_response_code(401); // Unauthorized
        echo json_encode(["error" => "Se requiere API Key"]);
        exit;
    }
    
    $apiKey = $headers['X-API-Key'];
    
    // Verificar si la API Key es válida
    if (!array_key_exists($apiKey, $apiKeys)) {
        http_response_code(403); // Forbidden
        echo json_encode(["error" => "API Key inválida"]);
        exit;
    }
    
    // Aquí podríamos verificar permisos específicos según el usuario
    $usuario = $apiKeys[$apiKey];
    
    return $usuario;
}

// Esta función se llamaría al inicio de index.php
$usuario = verificarApiKey();
?>
En resumen

Crear una API REST con PHP nativo es relativamente sencillo y no requiere frameworks adicionales. En esta lección hemos visto cómo estructurar los archivos, crear un sistema de enrutamiento básico y manejar diferentes tipos de peticiones HTTP. Hemos implementado operaciones CRUD para una entidad Usuario y añadido un ejemplo básico de seguridad. Estos conceptos pueden expandirse para crear APIs más complejas y robustas.