Inicio rápido
- Descarga y descomprime oxira-micromvc.zip en tu carpeta o hosting (cPanel: apunta el dominio a
public/, o usa los .htaccess incluidos). - Copia
.env.examplea.envy configura tu base de datos (opcional: el framework corre sin BD). - Limpia la demo y verifica:
php oxi fresh --confirmar # borra vistas/carpetas demo (no toca lo tuyo)
php oxi check # verifica extensiones, permisos y configuración
php oxi make:controller Admin/ProductosController
php oxi migrate && php oxi seed
Estructura
app/
Core/ ← framework (NO EDITAR: se reemplaza al actualizar)
Support/ ← helpers en español, Imagen (WebP), Crypto (AES)
Drivers/ ← integraciones: Sunat/, Documento, Whatsapp, Correo, TipoCambio
Controllers/ ← tus controladores (Admin/, Api/, ...)
Models/ ← tus modelos
Views/ ← vistas por área: front/, admin/, errors/
config/ ← app, database, security, services (credenciales de drivers), routes
database/ ← migrations/ y seeders/ (callables PHP puros)
public/ ← ÚNICO directorio expuesto (index.php + assets)
storage/ ← logs, cache, uploads, sunat (XML/CDR)
tools/oxi.php ← CLI
Auto-routing: la URL manda
No registres rutas. La URL se traduce sola a controlador y método (kebab-case → PascalCase/camelCase):
/productos → ProductosController::index()
/productos/15 → ProductosController::index(15)
/productos/create → ProductosController::create()
/productos/15/edit → ProductosController::edit(15)
/blog-posts/ultimas → BlogPostsController::ultimas()
/admin/productos → Admin\ProductosController::index()
/admin/ajustes/monedas → Admin\Ajustes\MonedasController::index()
Solo métodos públicos son alcanzables; los mágicos (__construct…) se rechazan.
Rutas explícitas solo para casos especiales (webhooks, {parámetros} nombrados) en config/routes.php.
Áreas y autenticación
Un área es un prefijo con su carpeta de controladores, su layout.php y sus reglas. Se declara en config/security.php:
'reserved_areas' => ['admin', 'api', 'pos'],
'protected_areas' => ['admin' => 'admin', 'pos' => 'cashier'],
'auth_redirects' => ['admin' => '/admin/login'],
'auth_exceptions' => ['admin' => ['login']], // controladores públicos del área
Login por rol con Auth::loginRole('admin', $id) · Auth::logoutRole() · Auth::requireRole().
Model y base de datos
final class Producto extends Model {
protected string $table = 'products';
}
$p = new Producto();
$p->find(15); // ?array
$p->where(['active' => 1], 'name'); // array
$p->create([...]); $p->update(15, [...]); $p->paginate($page, 20);
// SQL a medida SIEMPRE parametrizado:
$this->fetchAll('SELECT * FROM x WHERE name LIKE :q1', ['q1' => "%$q%"]);
$this->transaction(fn($pdo) => ...); // transacciones
Ojo: PDO nombrado no permite repetir el mismo placeholder — usa :q1, :q2 con el mismo valor.
Seguridad
- Escapa todo output con
e($var). - CSRF automático en POST/PUT/PATCH/DELETE:
<?= csrf_field() ?>en forms, headerX-CSRF-TOKENen AJAX. El áreaapiqueda exenta. - Validación fluida:
$this->validator($data)->required('name')->email('email')->fails(). - Páginas de error 404/405/419/500 con diseño propio y diagnóstico cuando
APP_DEBUG=true.
Helpers en español (Support)
Se cargan solos en el bootstrap (namespace App\Core):
fecha_es('2026-07-12') // "12 de Julio de 2026"
fecha_es($f, true) // "Domingo, 12 de Julio de 2026"
fecha_relativa($f) // "Hace 3 días"
dinero(1234.5) // "S/ 1,234.50"
num_corto(3400000) // "3.4 M"
num_letras(1234.50) // "MIL DOSCIENTOS ... CON 50/100 SOLES"
tamano_archivo($bytes) // "2.5 MB"
slug('Título Ñoño') // "titulo-nono"
sin_tildes($s) · texto_limpio($s) · token_hex(40)
paginar_array($items, $page, 20)
paginacion_links($page, $totalPages, url('/admin/x')) // HTML Bootstrap 5
Imagen::optimizar($_FILES['foto'], 'productos') // resize + WebP + marca de agua opcional
Crypto::encriptar('secreto') / Crypto::desencriptar($token) // AES-256 (APP_KEY)
Drivers Perú
Credenciales en el .env → mapeadas en config/services.php. Carga perezosa: si no llamas a un driver, su archivo jamás se lee.
use App\Drivers\Documento;
Documento::dni('45678912'); // ['ok'=>true,'nombre'=>...,'direccion'=>...] (Factiliza o apiperu.dev)
Documento::ruc('20601234567');
use App\Drivers\Whatsapp;
Whatsapp::link('987654321', 'Hola'); // https://wa.me/51987654321?text=Hola
Whatsapp::enviar('987654321', 'Su pedido está listo'); // Texvoo o wa-server
use App\Drivers\Correo;
Correo::enviar('cliente@mail.com', 'Asunto', '<h1>Hola</h1>'); // SMTP nativo o mail()
use App\Drivers\TipoCambio;
TipoCambio::hoy(); // ['ok'=>true,'compra'=>3.71,'venta'=>3.75] con caché diario
Driver SUNAT (facturación electrónica)
Un solo formato de documento, tres proveedores intercambiables con SUNAT_PROVIDER:
| Proveedor | Cómo funciona | Requiere |
|---|---|---|
apisperu | API REST gratuita (basada en Greenter); firma y envía por ti | Token JWT |
nubefact | API de pago; devuelve enlaces PDF/XML/CDR | Ruta + token |
greenter | Local: XML UBL 2.1 + firma con tu certificado + SOAP directo a SUNAT (beta/producción); guarda XML y CDR en storage/sunat/ | composer require greenter/lite + certificado .pem |
use App\Drivers\Sunat\Sunat;
$r = Sunat::enviar([
'tipo' => 'BOL', 'serie' => 'B001', 'numero' => 123, 'fecha' => '2026-07-12',
'cliente' => ['tipo_doc' => 'DNI', 'numero' => '45678912', 'nombre' => 'JUAN PEREZ'],
'items' => [
['descripcion' => 'PARACETAMOL 500MG', 'cantidad' => 2, 'precio_unitario' => 0.50],
],
]);
// ['ok'=>true,'estado'=>'aceptado','codigo'=>'0','mensaje'=>'...','pdf'=>...,'xml'=>...,'cdr'=>...]
Totales (gravada/IGV) e importe en letras se calculan solos. Sunat::probar() valida credenciales; Sunat::consultar($ticket) revisa envíos pendientes.
CLI oxi
php oxi check # diagnóstico: extensiones, permisos, .env, rutas manuales
php oxi fresh --confirmar # limpia el esqueleto demo (seguro: no toca lo tuyo)
php oxi migrate | seed # base de datos
php oxi make:controller Admin/X # scaffolding
php oxi make:model X | make:view admin/x/index | make:migration | make:seeder
php oxi route:list | route:guess Admin/Settings/Currencies
Despliegue a producción
- Usa
.env.production.examplecomo base:APP_DEBUG=false,SESSION_SECURE=true,APP_KEYdefinida. - Ejecuta
php oxi fresh --confirmarantes del primer deploy yphp oxi checkdespués de subir. - En cPanel apunta el dominio a
public/(o deja los .htaccess de raíz que redirigen solos). - Con
APP_DEBUG=falselos errores muestran páginas amigables y el detalle va astorage/logs/app.log.