Pequeña API para gestión de libros con CRUD, búsqueda, caché de respuestas en Redis, rate limiting, autenticación JWT y descarga local de portadas desde OpenLibrary. Incluye OpenAPI (Swagger), tests con PHPUnit y análisis estático con PHPStan.
- PHP 8.3 (FPM) + Nginx
- MySQL 8 (datos)
- Redis (rate limit + caché)
- PHPUnit (tests)
- PHPStan (análisis estático)
- Guzzle (HTTP client)
- OpenAPI (docs en
docs/openapi.yaml) - Docker Compose para orquestación
- Copia
.env.examplea.envy revisa valores. - Levanta los servicios:
docker compose up -d --build
docker compose exec app composer install
docker compose exec app php -v- Healthcheck:
curl -i http://localhost:8080/healthDebe devolver 200 OK y un JSON con ok: true.
Si usas PowerShell, sustituye
curlporInvoke-RestMethoddonde prefieras.
Get-Content src/Infrastructure/Persistence/Migrations/2025_10_17_000001_create_libros.sql | docker compose exec -T mysql mysql -uroot -proot booksGet-Content src/Infrastructure/Persistence/Migrations/2025_10_19_000002_add_portada_path.sql | docker compose exec -T mysql mysql -uroot -proot booksVerifica columnas:
docker compose exec -T mysql mysql -uroot -proot -e "USE books; SHOW COLUMNS FROM libros;"Login (usuario demo admin/admin123):
TOKEN=$(curl -s -X POST http://localhost:8080/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}' | jq -r '.data.access_token')
echo $TOKENPowerShell:
$auth = Invoke-RestMethod -Uri "http://localhost:8080/auth/login" -Method Post -ContentType "application/json" -Body '{"username":"admin","password":"admin123"}'
$TOKEN = $auth.data.access_token
$TOKENIncluye el header: Authorization: Bearer <token>
GET /api/v1/libros— listado + búsqueda (q,titulo,autor,sort,direction,page,per_page)GET /api/v1/libros/{id}— detallePOST /api/v1/libros— crear (requieretitulo,autor,isbnválidos)PUT /api/v1/libros/{id}— actualizar (param opcionalrefresh_cover=1fuerza re-descarga de portada)DELETE /api/v1/libros/{id}— eliminar (soft/hard segúnDELETE_MODE)
Formato de respuesta estándar: { "data": ..., "meta": ..., "errors": ... }
- Al crear/editar, se consulta OpenLibrary (con caché en Redis).
- Se guarda
portada_url(remota) y siSTORE_COVERS=true, se descarga la imagen a/storage/coversy se exponeportada_path(vía Nginx/storage/...).
Forzar refresco de portada en un update:
curl -X PUT "http://localhost:8080/api/v1/libros/<ID>?refresh_cover=1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"titulo":"Nuevo título"}'APP_ENV=local
APP_DEBUG=true
PAGINATION_PER_PAGE=20
DELETE_MODE=soft
# MySQL
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=books
DB_USERNAME=root
DB_PASSWORD=root
DB_TIMEZONE=Europe/Madrid
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
RATE_LIMIT_MAX=60
RATE_LIMIT_WINDOW=60
CACHE_ENABLED=true
API_CACHE_TTL_SECONDS=30
# Portadas locales
STORE_COVERS=true
COVER_STORAGE_PATH=/var/www/html/storage/covers
COVER_BASE_URL=/storage/covers
COVER_TIMEOUT=10
# JWT
JWT_SECRET=devsecret
JWT_ISSUER=BooksAPI
JWT_TTL=3600
JWT_REFRESH_TTL=1209600docker compose exec app composer test
# o:
docker compose exec app ./vendor/bin/phpunit --display-deprecations --testdoxdocker compose exec app composer stan
# o:
docker compose exec app ./vendor/bin/phpstan analyse --memory-limit=512Mdocker compose exec app composer fix- Response Cache: middleware con Redis para GET (cabecera
X-Cache: HIT/MISS). - Invalidación: tras POST/PUT/DELETE se invalidan claves relevantes.
- Rate Limit: límites por IP/token con cabeceras
X-RateLimit-*y429si se excede.
El contrato está en: docs/openapi.yaml (actualizado).
Usa el archivo docker-compose.swagger.yml incluido. Arranca Swagger UI en http://localhost:8081:
docker compose -f docker-compose.swagger.yml up -d swagger
# Abrir en el navegador:
# http://localhost:8081/public # index.php (front controller)
/src
/Domain
/Application
/Interfaces # Controllers HTTP
/Infrastructure # DB, HTTP, Cache, Middlewares, etc.
/Persistence/Migrations
/Services
/Storage
/tests # unit e integración
/docs # openapi.yaml
/docker # nginx conf, etc.
/storage # covers (sirviendo por Nginx /storage)
- Flujo:
feature/*→ PR →develop→main(releases). - Convencional commits (sugerido):
feat: ...,fix: ...,test: ...,docs: ...,chore: ...,refactor: ...,db: ...,ops: ...
- JWT en header
Authorization(stateless). - SQL seguro mediante consultas preparadas.
- Cabeceras de seguridad habilitadas (CSP, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection).
- Router: asegúrate de tener
Router::middleware()yRequest::capture(). - Nginx: recuerda el alias
/storage/endocker/nginx/nginx.confy reiniciar:docker compose restart nginx
- Redis no disponible → se usan fallbacks (sin cache/ratelimit reales).
- Permisos: si no puedes escribir en
storage/covers, revisa permisos del contenedor. - Windows: usa rutas y comillas adecuadas en PowerShell (usa backticks para multilínea).