Práctica: Crear una API con Slim 4

1. Introducción

¿Qué es Slim 4 y sus ventajas?

Slim 4 es un microframework de PHP diseñado para crear aplicaciones web simples y API RESTful. Es muy ligero, flexible y fácil de usar, lo que lo convierte en una excelente opción para crear servicios rápidos y eficientes sin sobrecargar el proyecto con características innecesarias.

Ventajas de Slim 4:

  • Ligero: Tiene una base muy pequeña, por lo que el rendimiento es óptimo.
  • Flexible: Puedes añadir solo lo que necesitas. Slim es muy configurable.
  • Fácil de aprender: Su sintaxis es sencilla y la documentación es clara.
  • Ideal para APIs: Slim facilita la creación de APIs RESTful, con soporte integrado para JSON, rutas, middleware, etc.

Requisitos previos

Para comenzar con Slim 4, necesitas tener los siguientes requisitos previos:

  • PHP 7.3 o superior.
  • Composer para la gestión de dependencias de PHP.
  • Docker (opcional) para crear un entorno de desarrollo aislado con PHP, Nginx y MySQL.
  • Visual Studio Code (VSCode) o cualquier editor de código de tu preferencia.

2. Instalación de Slim 4

Preparación

Mi recomendación es que crees un nuevo proyecto en github para ir subiendo los avances de la práctica. Le puedes llamar phpSlimApi. Luego clona el proyecto en tu máquina local.

git clone
git clone https://github.com/tu_usuario/phpSlimApi.git

De esta manera jugando con git y las ramas podrás ir guardando los avances de la práctica.

Instalación del entorno de desarrollo

Para el entorno de desarrollo vamos a mantener el que hemos creado para PHP. Recuerda que es un Docker compose con PHP, Nginx y MySQL. Importante antes de comeenar tener todo en funcionamiento y hacer una prueba con documento index.php que contenga la función: phpinfo();.

La estructura de carpetas es la siguiente:

mi_api_slim/
│├── docker-compose.yml
│├── nginx/
││   └── default.conf
│├── php/
││   └── Dockerfile
│├── mysql/
││   └── data/
││   └── tmp/
│├── src/
││   └── index.php
Puede faltar en el esquema algún fichero, te sugiero que vayas a la unidad 2 Configuración del entorno de desarrollo con Docker y sigas los pasos para crear el entorno.

Comprobaciones iniciales

A partir de ahora necesitaremos trabajar con erramientas como composer que no estan instaladas en nuestro equipo, están instaladas en el contenedor php. Por lo tanto, para ejecutar comandos de composer o php deberemos hacerlo a través del contenedor.

Lo primero será entrar en el contenedor php:

docker compose exec php bash
Una vez dentro del contenedor, podemos comprobar la versión de php y composer:

php -v
composer --version

resultado esperado

imagen

Si todo está correcto, ya podemos proceder a instalar Slim 4.

Instalación de Slim 4 via Composer

  1. Situarnos en docker php: Asegúrate de estar dentro del contenedor php como se explicó anteriormente.

  2. Instalar Slim 4: Slim 4 se gestiona mediante Composer, así que ejecuta el siguiente comando para instalarlo:

    composer require slim/slim
    

    Esto descargará e instalará Slim 4 y sus dependencias. Composer se encargará de gestionar las versiones y dependencias para ti.

    Atención

    Te aparecerá en el proyecto una nueva carpeta llamada vendor donde se encuentran todas las librerías instaladas por composer y un archivo composer.json que contiene la información del proyecto y las dependencias. Es importante no eliminar ni modificar estas carpetas y archivos. Composer es un gestor de paquetes y lo necesita para funcionar correctamente.

    Si vamos al sitio web de Slim Framework podemos ver la documentación oficial. En la guía de instalación podemos ver que también es necesario instalar Slim PSR-7 para manejar las peticiones y respuestas HTTP:

    composer require slim/psr7
    

    Configuración del servidor web (Nginx)

Ahora es necesario modificar la configuración de Nginx. Ahora composer nos ha creado una serie de carpetas y acrchivos en src a los que NO queremos acceder directamente desde el navegador. Por lo tanto, debemos modificar la configuración de Nginx para que el directorio raíz apunte a src/public.

Además también vamos a crear dentro de src una carpeta logs donde Nginx guardará los logs de acceso y errores (nos pueden ser útilies para depurar errores).

Ahora modificamos el contenido del archivo nginx/default.conf para que quede de la siguiente manera:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
server {
    listen 80;
    server_name example.com;
    index index.php;
    error_log /var/www/html/logs/example.error.log;
    access_log /var/www/html/logs/example.access.log;
    root /var/www/html/public;

    location / {
        try_files $uri /index.php$is_args$args;
    }

    location ~ \.php {
        try_files $uri =404;
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param SCRIPT_NAME $fastcgi_script_name;
        fastcgi_index index.php;
        fastcgi_pass php:9000;
    }
}
Reiniciar el contenedor nginx para que los cambios tengan efecto:

docker compose restart nginx

Esta configuración la puedes encontrar en la documentación oficial de Slim 4 en el apartado de Nginx Configuration. Aunque se ha modificado la última línea para que acceda al contenedor php que es donde tenemos instalado php-fpm, y el puerto 9000 que es el que utiliza php-fpm por defecto.

Otro cambio importante es la línea root /var/www/html/public; que indica que el directorio raíz es public dentro de src. Ahora debemos crear esa carpeta public dentro de src y mover el archivo index.php que tenemos en src a src/public.

Configuración básica de Slim 4

  1. Crear el archivo de inicio: Crea un archivo index.php dentro de src/public. Este será el punto de entrada de tu aplicación Slim.

    index.php
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    <?php
    
    require 'vendor/autoload.php';
    
    // Crear la aplicación Slim
    $app = \Slim\Factory\AppFactory::create();
    
    // Definir una ruta básica
    $app->get('/', function ($request, $response, $args) {
        $response->getBody()->write("<p>¡Hola, mundo!</p>");
        return $response;
    });
    
    // Ejecutar la aplicación
    $app->run();
    
  2. Comprobar funcionamiento:

    Esto hará que el servidor se ejecute en http://localhost:8080. Abre tu navegador y visita esa URL para ver el mensaje "¡Hola, mundo!" que definimos en la ruta.

    URL

    Fíjate que una vez instalado el FRAMEWORK la URL ha cambiado. Ahora es http://localhost:8080, ahora no hay que indicar la página index.php ya que Slim se encarga de gestionar las rutas. Si intentas acceder a http://localhost:8080/index.php te dará error ya que no existe esa ruta.

3. Creación de la Base de Datos

Configuración de la Base de Datos MySQL

  1. Crear la base de datos: Usando MySQL o MariaDB, crea una base de datos para la API. Por ejemplo, en MySQL:

    CREATE DATABASE mi_api;
    
  2. Crear una tabla de usuarios: Crea una tabla para almacenar usuarios. Aquí tienes un ejemplo de cómo crearla:

    CREATE TABLE usuarios (
       id INT AUTO_INCREMENT PRIMARY KEY,
       nombre VARCHAR(100) NOT NULL,
       email VARCHAR(100) NOT NULL UNIQUE,
       creado_en TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    

Conexión a la base de datos desde Slim

  1. Crear una clase para la conexión a la base de datos: Crea un archivo db.php que contendrá la lógica de conexión con la base de datos:

    db.php
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <?php 
    
    use PDO;
    
    class Database {
        private $host = 'mysql';
        private $db = 'mi_api';
        private $user = 'alumno'; // Cambia con tu usuario de MySQL
        private $pass = 'alumno'; // Cambia con tu contraseña de MySQL
        private $charset = 'utf8mb4';
    
        public $pdo = null;
    
        public function connect() {
            if ($this->pdo === null) {
                $dsn = "mysql:host=$this->host;dbname=$this->db;charset=$this->charset";
                try {
                    $this->pdo = new PDO($dsn, $this->user, $this->pass);
                    $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                    echo "<p>Connected to the database successfully!</p>";
                } catch (PDOException $e) {
                    echo "<p>Connection failed: " . $e->getMessage() . "</p>";
                }
            }
            return $this->pdo;
        }
    }
    

    Ahora llamaremos a este archivo desde index.php para establecer la conexión y comprobar que funciona. Hacerlo antes de crear la aplicación ya que las rutas las crearemos después y necesitaremos la conexión a la base de datos.

    ```php
    

    require 'db.php';

    $db = new Database(); $pdo = $db->connect(); ```

  2. Usar la conexión en la API: En el archivo index.php, añade el código para conectar la base de datos:

    require 'db.php';
    
    $db = new Database();
    $pdo = $db->connect();
    

    Volvemos a cargar la página en el navegador y si todo está correcto veremos el mensaje de conexión exitosa a la base de datos.

    Conexión exitosa

    Si ves el mensaje de conexión exitosa, ya puedes proceder a crear las rutas de la API para gestionar los usuarios.

    imagen


4. Creación de Rutas de la API

Para no tener todo el código en un solo archivo, vamos a crear un nuevo archivo llamado routes.php en una carpeta llamada 'routes/'donde definiremos todas las rutas de la API. Luego incluiremos este archivo en index.php.

Creamos el fichero y añadimos este código para probar que funciona:

routes.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request; 
use Slim\Factory\AppFactory;

/* Ruatas del api */


$app->get('/usuarios', function (Request $request, Response $response, $args) {
    $response->getBody()->write("<p>Lista de usuarios</p>");
    return $response;
});

ahora en index.php incluimos el archivo routes.php justo después de crear la aplicación y antes de ejecutar la aplicación:

index.php
1
2
3
4
5
$app = AppFactory::create();
...
require 'routes/routes.php';
...
$app->run();
Ahora cargará nuestra rutas antes de ejecutar la aplicación. Comprobamos que funciona accediendo a

http://localhost:8080/userarios

y veremos el mensaje "Users list". Si es así todo está correcto y podemos proceder a crear las rutas RESTful para gestionar usuarios.

Crear las rutas RESTful para gestionar usuarios

Primero hay una parte de conexión a la base de datos para obtener la información de los usuarios. Luego construimos en $data un array con el número total de usuarios y los datos de los usuarios. Finalmente, codificamos el resultado en JSON y lo devolvemos en la respuesta.

  1. Ruta GET para obtener todos los usuarios:

    GET /usuarios
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    $app->get('/usuarios', function ($request, $response, $args) use ($pdo) {
    // Realizamos la consulta para obtener los usuarios
    $stmt = $pdo->query("SELECT * FROM usuarios");
    $usuarios = $stmt->fetchAll(PDO::FETCH_ASSOC);
    
    // Creamos un array con los usuarios y el número total de usuarios
    $data = [
        'total' => count($usuarios),  // Contamos el número de usuarios
        'usuarios' => $usuarios       // Los datos de los usuarios
    ];
    
    // Codificamos el resultado en JSON
    $response->getBody()->write(json_encode($data));
    
    // Establecemos el header Content-Type a application/json
    return $response->withHeader('Content-Type', 'application/json');
    });
    

    Aunque al ser un GET podemos seguir utiliando el navegador para probar la ruta, es mejor usar una herramienta como Postman o REST Client en VSCode que veremos más adelante.

    Respuesta JSON

    La respuesta de esta ruta será un JSON con la lista de usuarios y el número total de usuarios. Por ejemplo:

    imagen

    En este caso hay 0 usuarios, pero si damos de alta usuarios veremos los datos en el JSON.

  2. Ruta POST para crear un nuevo usuario:

    A diferencia de una ruta GET, para crear un nuevo usuario necesitamos pasar información al servidor (nombre y email por ejemplo). Por lo tanto, usaremos una ruta POST que recibirá los datos en el cuerpo de la petición. Luego veremos como pasar esta información usando REST Client en VSCode.

    Validaciones

    En este ejemplo no se incluyen validaciones de datos. En un entorno real, es importante validar los datos recibidos para evitar errores y problemas de seguridad. Las veremos más adelante.

    POST /usuarios
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    $app->post('/usuarios', function ($request, $response, $args) use ($pdo) {
    //$data = $request->getParsedBody();
    $data = json_decode($request->getBody(), true);
    $nombre = $data['nombre'];
    $email = $data['email'];
    
    $stmt = $pdo->prepare("INSERT INTO usuarios (nombre, email) VALUES (?, ?)");
    $stmt->execute([$nombre, $email]);
    
    $output  = [
        'id' => $pdo->lastInsertId(),
        'nombre' => $nombre,
        'email' => $email,
    ];
    
    $response->getBody()->write(json_encode($output));
    return $response->withHeader('Content-Type', 'application/json');
    
    });
    

    Un ejemplo de petición POST para crear un usuario sería:

    POST http://localhost:8080/usuarios
    Content-Type: application/json
    
    {
        "nombre": "Juan Pérez",
        "email": "juan.perez@example.com"
    }
    

    Respuesta

    La respuesta de esta ruta será un mensaje indicando que el usuario ha sido creado correctamente.

    imagen

  3. Ruta PUT para actualizar un usuario:

    PUT /usuarios/{id}
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    $app->put('/usuarios/{id}', function ($request, $response, $args) use ($pdo) {
        $id = $args['id'];
        $data= json_decode($request->getBody(), true);
        $nombre = $data['nombre'];
        $email = $data['email'];
    
        $stmt = $pdo->prepare("UPDATE usuarios SET nombre = ?, email = ? WHERE id = ?");
        $stmt->execute([$nombre, $email, $id]);
    
        $output  = [
            'id' => $id,
            'nombre' => $nombre,
            'email' => $email,
            'mensaje' => 'Usuario actualizado correctamente'
        ];
    
        $response->getBody()->write(json_encode($output));
        return $response->withHeader('Content-Type', 'application/json');
    
    });
    

    La petición PUT para actualizar un usuario sería:

    PUT http://localhost:8080/usuarios/1
    Content-Type: application/json  
    
    {
        "nombre": "Juan Pérez Actualizado",
        "email": "juan.perez.actualizado@example.com"
    }
    

    Respuesta

    La respuesta de esta ruta será un mensaje indicando que el usuario ha sido actualizado correctamente. imagen

  4. Ruta DELETE para eliminar un usuario:

    DELETE /usuarios/{id}
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    $app->delete('/usuarios/{id}', function ($request, $response, $args) use ($pdo) {
        $id = $args['id'];
    
        $stmt = $pdo->prepare("DELETE FROM usuarios WHERE id = ?");
        $stmt->execute([$id]);
    
        $output  = [
            'id' => $id,
            'mensaje' => 'Usuario eliminado correctamente'
        ];
    
        $response->getBody()->write(json_encode($output));
        return $response->withHeader('Content-Type', 'application/json');
    });
    

    Para eliminar un usuario, la petición DELETE sería:

    DELETE http://localhost:8080/usuarios/1
    

    Respuesta

    La respuesta de esta ruta será un mensaje indicando que el usuario ha sido eliminado correctamente. imagen


5. Acceso a las Rutas con REST Client

Ya lo hemos visto en el punto anterior en las pruebas de las rutas, pero vamos a verlo con más detalle.

Usar REST Client en Visual Studio Code

  1. Instalar la extensión REST Client en VSCode desde el marketplace de extensiones.
  2. Realizar peticiones a la API: Crea un archivo de texto con extensión .http o .rest y agrega las siguientes peticiones:

  3. GET para obtener usuarios:

    GET http://localhost:8080/usuarios
    
  4. POST para crear un usuario:

    POST http://localhost:8080/usuarios
    Content-Type: application/json
    
    {
        "nombre": "Carlos Gómez",
        "email": "carlos@example.com"
    }
    
  5. PUT para actualizar un usuario:

    PUT http://localhost:8080/usuarios/1
    Content-Type: application/json
    
    {
        "nombre": "Carlos Gómez Actualizado",
        "email": "carlos_actualizado@example.com"
    }
    
  6. DELETE para eliminar un usuario:

    DELETE http://localhost:8080/usuarios/1
    

Ver las respuestas

Cuando estamos ofreciendo un servicio RESTful, las respuestas deben ser en formato JSON. En REST Client, al hacer clic en "Send Request", la respuesta se mostrará en una nueva pestaña con el formato adecuado. Es importante dar informacion suficiente en las respuestas para que el cliente pueda entender el resultado de la operación.


6. Validación de Datos y Seguridad

Validación Básica de Datos

En este punto, se explica cómo realizar una validación simple para asegurarse de que los datos ingresados por el usuario sean correctos antes de ser procesados o almacenados en la base de datos.

Ejemplo de Validación en la Ruta POST para Crear un Usuario

Vemoa un ejemplo de validación básica en la ruta POST para crear un usuario, luego podemos aplicar validaciones similares en las otras rutas.

En la ruta de POST /usuarios, validamos que el nombre y el correo sean proporcionados y que el correo tenga un formato válido:

POST /usuarios con validación
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
$app->post('/usuarios', function ($request, $response, $args) use ($pdo) {
    // Obtener los datos del cuerpo de la solicitud (JSON)
    $data = json_decode($request->getBody(), true);  // Decodificar el JSON a un array asociativo
    $nombre = $data['nombre'] ?? '';  // Si 'nombre' no está presente, usar una cadena vacía
    $email = $data['email'] ?? '';    // Si 'email' no está presente, usar una cadena vacía

    // Comprobación combinada: si 'nombre' o 'email' están vacíos
    if (empty($nombre) || empty($email)) {
        return $response->withJson([
            'error' => true,
            'message' => 'El nombre y el email son requeridos.'
        ], 400);  // Error 400 (Bad Request)
    }

    // Validación del email (si no es válido)
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        return $response->withJson([
            'error' => true,
            'message' => 'El email es inválido.'
        ], 400);  // Error 400 (Bad Request)
    }

    // Si la validación pasa, insertamos el usuario
    try {
        $stmt = $pdo->prepare("INSERT INTO usuarios (nombre, email) VALUES (?, ?)");
        $stmt->execute([$nombre, $email]);

        // Obtener el ID del nuevo usuario insertado
        $id = $pdo->lastInsertId();

        // Respuesta exitosa con los datos del usuario creado
        $responseData = [
            'error' => false,
            'message' => 'Usuario creado correctamente.',
            'usuario' => [
                'id' => $id,
                'nombre' => $nombre,
                'email' => $email
            ]
        ];

        return $response->withJson($responseData, 201);  // Código 201 (Created)
    } catch (PDOException $e) {
        // Si ocurre un error en la base de datos, devolver un error 500
        return $response->withJson([
            'error' => true,
            'message' => 'Error en la base de datos: ' . $e->getMessage()
        ], 500);  // Error 500 (Internal Server Error)
    }
});

Es importante fijarse además de los mensajes de error, los códigos de estado HTTP que se devuelven en cada caso. Esto ayuda al cliente a entender el resultado de la operación.

Código Significado
200 OK
201 Created (Recurso creado)
400 Bad Request (Solicitud incorrecta)
404 Not Found (No encontrado)
500 Internal Server Error (Error interno del servidor)

Es común usar estos códigos para indicar el resultado de las operaciones en una API RESTful.

Seguridad Básica

La seguridad básica puede incluir la autenticación y la validación de datos. Aquí te muestro un ejemplo de cómo usar un middleware para proteger las rutas.

Middleware para Autenticación Básica

Este punto lo dejamos de momento para más adelante, para no alargar más el tema y por su complejidad, ahora estamos intentando asimilar el funcionamiento básico de Slim 4.

Solo adelantar que para algunas funciones añadidas los frameworks suelen utilizar middleware que son funciones que se ejecutan antes de que una petición llegue al su controlador (o manejador de ruta). Un ejemplo común es un middleware de autenticación que verifica si el usuario está autenticado antes de permitirle acceder a ciertas rutas.

Tanto Slim 4 como otros frameworks como Laravelpermiten crear y usar middleware de manera sencilla.

Aquí os dejo un par de enlaces para que podáis investigar más sobre este tema:


7. Conclusión y Próximos Pasos

Resumen

En esta práctica, hemos aprendido a crear una API básica usando Slim 4, cubriendo los aspectos fundamentales como la configuración, la creación de rutas RESTful, la conexión con una base de datos MySQL y la validación de datos. También hemos explorado cómo realizar pruebas con REST Client y cómo hacer configuraciones básicas de seguridad.

Ampliaciones

  1. Autenticación y autorización: Implementa autenticación basada en JWT para proteger tus rutas.
  2. Control de errores avanzado: Usa un sistema de manejo de errores centralizado para responder de manera adecuada a los problemas.
  3. Paginación y filtros: Añade funcionalidades como paginación y filtros en las rutas GET.

Consejos para la Implementación en Producción

  • Utiliza siempre HTTPS para la transmisión segura de datos.
  • Asegúrate de que las credenciales de la base de datos estén correctamente almacenadas en un archivo .env y no en el código fuente.
  • Considera usar un servidor como Nginx o Apache para servir la API de forma eficiente.