Inicio rápido

  1. Descarga y descomprime oxira-micromvc.zip en tu carpeta o hosting (cPanel: apunta el dominio a public/, o usa los .htaccess incluidos).
  2. Copia .env.example a .env y configura tu base de datos (opcional: el framework corre sin BD).
  3. 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, header X-CSRF-TOKEN en AJAX. El área api queda 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:

ProveedorCómo funcionaRequiere
apisperuAPI REST gratuita (basada en Greenter); firma y envía por tiToken JWT
nubefactAPI de pago; devuelve enlaces PDF/XML/CDRRuta + token
greenterLocal: 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

  1. Usa .env.production.example como base: APP_DEBUG=false, SESSION_SECURE=true, APP_KEY definida.
  2. Ejecuta php oxi fresh --confirmar antes del primer deploy y php oxi check después de subir.
  3. En cPanel apunta el dominio a public/ (o deja los .htaccess de raíz que redirigen solos).
  4. Con APP_DEBUG=false los errores muestran páginas amigables y el detalle va a storage/logs/app.log.