Cómo construir una API RESTful eficiente y segura en PHP 8 con Slim, Eloquent y JWT

php

Cómo construir una API RESTful eficiente y segura en PHP 8 con Slim, Eloquent y JWT

En esta guía práctica crearás una API RESTful mínima y profesional usando PHP 8, Slim Framework v4, Eloquent (ORM de Laravel) y JSON Web Tokens (JWT) para autenticación. Te mostraré la estructura de carpetas, dependencias, código completo y las decisiones por qué se toman. Al final tendrás una base lista para producción (con margen de mejora).

Por qué esta stack

  • PHP 8: mejoras en rendimiento, tipos y JIT opcional.
  • Slim: microframework ligero, middleware-friendly y fácil de usar.
  • Eloquent: ORM robusto y productivo incluso fuera de Laravel.
  • JWT: estándar para tokens stateless, fácil integración con SPAs o móviles.

Estructura de carpetas

project-root/
├─ public/
│  └─ index.php          # Front controller
├─ src/
│  ├─ Controllers/
│  │  └─ AuthController.php
│  ├─ Middleware/
│  │  └─ JwtMiddleware.php
│  ├─ Models/
│  │  └─ User.php
│  ├─ config.php
│  └─ dependencies.php
├─ migrations/
├─ .env
├─ composer.json
└─ README.md

Instalación rápida

composer require slim/slim:^4 slim/psr7 nyholm/psr7
composer require illuminate/database:^8 vlucas/phpdotenv firebase/php-jwt
composer require respect/validation

Usamos nyholm/psr7 para PSR-7, illuminate/database para Eloquent, phpdotenv para configuración y firebase/php-jwt para tokens.

Archivo de configuración: .env

APP_ENV=development
APP_DEBUG=true
DB_DRIVER=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=api_db
DB_USERNAME=root
DB_PASSWORD=secret
JWT_SECRET=tu_secreto_muy_largo_y_seguro

Bootstrap: public/index.php

<?php
require __DIR__ . '/../vendor/autoload.php';

use Dotenv\Dotenv;
use Slim\Factory\AppFactory;

$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->safeLoad();

$app = AppFactory::create();

// Carga dependencias y config
(require __DIR__ . '/../src/dependencies.php')($app);
(require __DIR__ . '/../src/config.php')($app);

// Rutas
(require __DIR__ . '/../src/routes.php')($app);

$app->run();

Dependencias y Eloquent: src/dependencies.php

<?php
use Psr\Container\ContainerInterface;
use Illuminate\Database\Capsule\Manager as Capsule;

return function($app) {
    // Configuración Eloquent
    $capsule = new Capsule;
    $capsule->addConnection([
        'driver'    => getenv('DB_DRIVER'),
        'host'      => getenv('DB_HOST'),
        'database'  => getenv('DB_DATABASE'),
        'username'  => getenv('DB_USERNAME'),
        'password'  => getenv('DB_PASSWORD'),
        'charset'   => 'utf8',
        'collation' => 'utf8_unicode_ci',
        'prefix'    => '',
    ]);
    $capsule->setAsGlobal();
    $capsule->bootEloquent();

    // Registrar en el container si lo necesitas
    $container = $app->getContainer();
    if ($container) {
        $container->set('db', function() use ($capsule) {
            return $capsule;
        });
    }
};

Configuración adicional: src/config.php

<?php
return function($app) {
    // Error middleware básico
    $displayErrorDetails = getenv('APP_DEBUG') === 'true';
    $errorMiddleware = $app->addErrorMiddleware($displayErrorDetails, true, true);
};

Modelo User: src/Models/User.php

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected $table = 'users';
    protected $fillable = ['name', 'email', 'password'];
    protected $hidden = ['password'];
    public $timestamps = true;
}

Por qué: ocultamos el password en serializaciones y permitimos fillable para asignación masiva controlada.

Middleware JWT: src/Middleware/JwtMiddleware.php

<?php
namespace App\Middleware;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class JwtMiddleware
{
    public function __invoke(Request $request, Response $response, $next)
    {
        $authHeader = $request->getHeaderLine('Authorization');
        if (!$authHeader || !preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) {
            $response->getBody()->write(json_encode(['error' => 'Token not provided']));
            return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
        }

        $token = $matches[1];
        try {
            $secret = getenv('JWT_SECRET');
            $decoded = JWT::decode($token, new Key($secret, 'HS256'));
            // attach user data to request (puedes consultar DB aquí)
            $request = $request->withAttribute('jwt', $decoded);
            return $next($request, $response);
        } catch (\Exception $e) {
            $response->getBody()->write(json_encode(['error' => 'Invalid token', 'message' => $e->getMessage()]));
            return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
        }
    }
}

Por qué: el middleware verifica y descodifica el JWT y añade la información al request para los controladores. Usar Key con firebase/php-jwt para compatibilidad con HS256 y futuros algoritmos.

Controlador de autenticación: src/Controllers/AuthController.php

<?php
namespace App\Controllers;

use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use App\Models\User;
use Firebase\JWT\JWT;
use Respect\Validation\Validator as v;

class AuthController
{
    public function login(Request $request, Response $response)
    {
        $data = (array)$request->getParsedBody();

        $email = $data['email'] ?? '';
        $password = $data['password'] ?? '';

        // Validación básica
        $emailValidator = v::email()->notEmpty();
        $passwordValidator = v::stringType()->notEmpty();

        if (!$emailValidator->validate($email) || !$passwordValidator->validate($password)) {
            $response->getBody()->write(json_encode(['error' => 'Invalid credentials']));
            return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
        }

        $user = User::where('email', $email)->first();
        if (!$user || !password_verify($password, $user->password)) {
            $response->getBody()->write(json_encode(['error' => 'Invalid email or password']));
            return $response->withHeader('Content-Type', 'application/json')->withStatus(401);
        }

        $payload = [
            'sub' => $user->id,
            'email' => $user->email,
            'iat' => time(),
            'exp' => time() + 3600
        ];

        $jwt = JWT::encode($payload, getenv('JWT_SECRET'), 'HS256');

        $response->getBody()->write(json_encode(['token' => $jwt]));
        return $response->withHeader('Content-Type', 'application/json');
    }

    public function me(Request $request, Response $response)
    {
        $jwt = $request->getAttribute('jwt');
        $user = User::find($jwt->sub);
        $response->getBody()->write(json_encode(['user' => $user]));
        return $response->withHeader('Content-Type', 'application/json');
    }
}

Rutas: src/routes.php

<?php
use App\Controllers\AuthController;
use App\Middleware\JwtMiddleware;

return function($app) {
    $app->post('/login', [AuthController::class, 'login']);

    $app->get('/me', function($request, $response) {
        // route-level closure ejemplo
    });

    // Rutas protegidas
    $app->group('/api', function($group) {
        $group->get('/me', [AuthController::class, 'me']);
        // Más rutas CRUD
    })->add(new JwtMiddleware());
};

Migraciones y seguridad de contraseñas

Usa migrations para crear la tabla users. Guarda contraseñas con password_hash(..., PASSWORD_DEFAULT). Nunca guardes JWT secrets en el repositorio. Rotación periódica de claves y uso de KMS (AWS KMS, HashiCorp Vault) en producción es recomendable.

Buenas prácticas y performance

  • Usa middleware para autenticación, rate limiting (ej. symfony/rate-limiter o middleware personalizado) y logging.
  • Serializa respuestas consistentemente: usa un response factory que envuelva la respuesta en { data: ..., error: ... }.
  • Evita consultas N+1 con Eloquent: usa with() para relaciones.
  • Cachea respuestas públicas con Redis o Varnish para endpoints intensivos en lectura.
  • Activa OPcache y, si aplicable, RoadRunner o Swoole para mejorar latencia y throughput.

Ejemplo de petición

POST /login
Content-Type: application/json

{"email":"user@example.com","password":"secret"}

Respuesta 200:
{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."}

Uso en rutas protegidas:
Authorization: Bearer <token>

Errores comunes y cómo evitarlos

  • No validar entrada: usa Respect/Validation o DTOs con argumentos tipados.
  • Exponer toda la excepción en producción: habilita displayErrorDetails solo en dev.
  • Tokens demasiado largos sin expiración: establece exp corta y refresh tokens si hace falta.
  • Conexiones DB sin pool en entornos serverless: usa drivers y estrategias adecuados.

Por qué evitar: estas fallas llevan a fugas de datos, mala experiencia y problemas de escalabilidad.

Pruebas y validación

  • Escribe tests para rutas con phpunit y requests simuladas.
  • Prueba la expiración y revocación de tokens.
  • Fuzzing de entrada para endpoints críticos.

Implementa monitors (Sentry) y métricas (Prometheus) para detectar regresiones y latencias.

Si quieres aplicar esto en producción: agrega HTTPS obligatorio, CSP, CORS configurado solo para orígenes permitidos y auditing de dependencias (composer audit / SensioLabs Security Checker si disponible).

Próximo paso avanzado: considera dividir la capa de autenticación en un microservicio separado y usar OAuth2 / OpenID Connect (Keycloak, Authelia) si necesitas SSO, refresh tokens y scopes granulares.

Advertencia: nunca uses JWT sin expiración o sin un mecanismo de revocación si tu aplicación requiere invalidar tokens inmediatamente (logout forzado, cambio de permisos). Implementa short-lived access tokens + refresh tokens seguros con revocación server-side.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión