Imported from martinbailetti/baseapi (
AGENTS.md). Install upstream withnpx skills add martinbailetti/baseapi. Copyright stays with the author.
AGENTS.md — Guía para agentes de programación
Instrucciones de referencia rápida para trabajar en este proyecto sin cometer errores comunes.
Restricciones de PHP 7.4 — OBLIGATORIO
Este proyecto usa PHP 7.4. Está prohibido usar cualquier sintaxis de PHP 8+:
| ❌ Prohibido (PHP 8+) | ✅ Alternativa PHP 7.4 |
|---|---|
fn($x) => $x * 2 (arrow function) |
function($x) { return $x * 2; } |
$obj?->method() (nullsafe operator) |
isset($obj) ? $obj->method() : null |
match($x) { ... } |
switch o cadena de if/elseif |
named arguments: foo(a: 1) |
argumentos posicionales |
str_contains(), str_starts_with() |
strpos(), substr() |
array_is_list() |
comprobar manualmente |
Tipos de unión int|string |
PHPDoc @param int|string |
throw como expresión |
envolver en bloque |
Trampa crítica: is_callable en el Router
Problema conocido y ya corregido. No revertir.
En PHP 7.4, is_callable(['ClassName', 'method']) devuelve true aunque el método
no sea estático. Si se usa call_user_func sobre ese array, PHP invoca el método
estáticamente y lanza:
Deprecated: Non-static method Foo::bar() should not be called statically
Solución aplicada en src/Router.php: detectar el array [string, string] antes
de llamar a is_callable, e instanciar siempre el controlador:
// CORRECTO — primero array de clase, luego callable genérico
if (is_array($handler) && count($handler) === 2 && is_string($handler[0])) {
$controller = new $handler[0]();
$controller->{$handler[1]}($params);
return;
}
if (is_callable($handler)) {
call_user_func($handler, $params);
}
Estructura rápida
index.php ← bootstrap, CORS, dispatch (sin rutas inline)
routes/
api.php ← registerApiRoutes() — incluye los demás
admin.php ← activity-log, users, db-dump
user.php ← user-prefs
push.php ← qr + push
cinema.php ← movies, actors, directors
auth.php ← login, refresh, logout, me
config/Config.php ← Config::get('KEY', 'default')
config/Database.php ← Database::getInstance() → PDO
src/Router.php ← $router->get('/ruta/{id}', ['Ctrl', 'metodo'])
src/Response.php ← Response::success($data) / ::error($msg, $code, $errors)
src/Cors.php ← Cabeceras CORS compartidas (index, Router 404, errores)
src/AuthException.php ← Excepción de auth (usada por JwtService / BaseController)
src/Services/JwtService.php ← Emisión/verificación JWT HS256 (roles, sub)
src/Controllers/BaseController.php ← requireAuth(), requireRole(), jsonValidationError()
src/Models/BaseModel.php ← all(), find(), count(), paginate()
src/ActivityLogger.php ← Auditoría (usa JwtService para email)
src/Mail/MailMessage.php ← DTO de correo (to, subject, html/text)
src/Mail/MailLayout.php ← Layout HTML/texto compartido
src/Mail/MailTemplate.php ← Base para plantillas reutilizables
src/Mail/ErrorMail.php ← Plantilla de errores (error/warning/critical)
src/Mail/NotificationMail.php ← Plantilla de notificaciones (info/success/warning)
src/Mail/SimpleMail.php ← Correo genérico ad-hoc
src/Mail/SystemFailureMail.php ← Alias de ErrorMail (compatibilidad)
src/Services/MailService.php ← Envío SMTP (PHPMailer + Config)
src/Services/AlertNotifier.php ← Alertas por correo con cooldown
src/Services/NotificationService.php ← Notificaciones informativas (NotificationMail)
tests/ ← PHPUnit (composer test)
cache/ ← caché runtime (alertas, etc.)
Autenticación — OBLIGATORIO en endpoints nuevos
La API emite y verifica JWT HS256 propios en JwtService (login en /api/auth/login):
| Método en BaseController | Uso |
|---|---|
requireAuth() |
JWT válido obligatorio |
requireAuthSub() |
JWT válido + devuelve sub |
requireRole('super') |
JWT válido + rol en el claim roles |
optionalAuthSub() |
sub si hay token válido; null si no hay token o es inválido |
Reglas actuales:
| Endpoints | Auth |
|---|---|
POST /api/auth/login, /api/auth/refresh |
Público |
GET /api/auth/me |
JWT obligatorio |
GET movies/actors/directors |
Público (JWT opcional para favoritos) |
POST/PUT/DELETE movies/actors/directors |
JWT obligatorio |
/api/activity-log, /api/activity-log/filters, /api/users |
Rol super |
/api/qr/push |
JWT obligatorio |
/api/user-prefs, push subscribe/unsubscribe |
JWT obligatorio |
/api/push/send, /api/db-dump |
Rol super |
/api/push/public-key |
Público |
Variables .env relevantes:
JWT_SECRET=change-me-in-production
JWT_ISSUER=basekit
JWT_TTL=900
JWT_REFRESH_TTL=604800
JWT_REFRESH_TTL_REMEMBER=2592000
JWT_VERIFY=true
JWT_VERIFY=falsesolo para tests locales (composer testlo desactiva).- No duplicar lógica JWT en controladores; usar siempre
BaseController/JwtService.
Correo SMTP (alertas y notificaciones):
MAIL_ENABLED=true
MAIL_HOST=mail.smi2000.net
MAIL_PORT=587
MAIL_USERNAME=smisendemail
MAIL_PASSWORD=***
MAIL_ENCRYPTION=null
MAIL_VERIFY_SSL=false
MAIL_FROM_ADDRESS=noreply@smi2000.net
MAIL_FROM_NAME="${APP_NAME}"
MAIL_ALERT_RECIPIENTS=admin@example.com,ops@example.com
MAIL_ALERT_SOURCES=db_connection,dispatch_pdo,dispatch_throwable,php_fatal,bootstrap
MAIL_ALERT_COOLDOWN_SECONDS=300
MAIL_ENCRYPTION:tls,ssl,noneonull(sin cifrado explícito).MAIL_VERIFY_SSL=falsesi el servidor SMTP usa certificado autofirmado o interno.MAIL_ALERT_SOURCES=*alerta ante cualquier origen registrado en_logApiError.- Errores:
ErrorMail(severidadeserror,warning,critical). - Notificaciones:
NotificationMail(tiposinfo,success,warning). - Para otros casos: crear clase en
src/Mail/extendsMailTemplateo usarSimpleMail.
Cómo añadir un endpoint nuevo (checklist mínimo)
- Model (si es tabla nueva) →
src/Models/NombreModel.phpextendsBaseModel, definir$table. - Controller →
src/Controllers/NombreController.phpextendsBaseController. - Auth →
$this->requireAuth()o$this->requireRole('super')según corresponda. - Validación → errores por campo con
$this->jsonValidationError(['campo' => ['mensaje']]). - Ruta → en el archivo de dominio bajo
routes/(p.ej.routes/cinema.php) o crearroutes/nombre.phpe incluirlo desderoutes/api.php - Test → añadir caso en
tests/si la lógica es crítica.
Configuración por hostname
- Archivo:
.env.{gethostname()}(p.ej..env.ONLINE1) - Si no existe, intenta
.envgenérico; si tampoco existe → excepción visible. - Nunca hardcodear credenciales en el código.
- Agregar al
.gitignoretodos los.env.*excepto el de ejemplo.
Ejecutar en desarrollo
cd c:\Projects\BaseKit\api
php -S localhost:8888 index.php
El segundo argumento index.php es el router script; sin él, las rutas que no sean
archivos físicos devuelven 404 porque el built-in server no lee .htaccess.
IMPORTANTE — archivos estáticos con router script:
Cuando se usa un router, el built-in server pasa todas las peticiones al script,
incluidas las de .js, .css, imágenes, etc. Para que los sirva directamente
hay que añadir al inicio de index.php el bloque cli-server que retorna false.
Tests
cd c:\Projects\BaseKit\api
composer test
PHPUnit 9.x en tests/. El bootstrap carga JWT_VERIFY=false para no exigir firma HMAC en CI.
Convenciones
| Concepto | Convención |
|---|---|
| Rutas API | /api/{recurso} → JSON |
| Respuesta éxito | Response::success($data) → {"success":true,"data":[...]} |
| Respuesta error | Response::error($msg, $code, $errors) → {"success":false,"message":"...","errors":{...}} |
| Paginación | parámetros ?page= y ?per_page= (máx 200) |
| Acceso a DB | siempre a través del Model, nunca PDO directo en controladores |
| Queries con input | siempre prepared statements con ?, nunca concatenar variables en SQL |
| JSON en producción | sin JSON_PRETTY_PRINT (solo con APP_DEBUG=true) |