Modelo de Identidad & Perfil EAV
La administración de identidades en Mañuti descarta la estructura genérica users provista por defecto en Laravel para adoptar una arquitectura empresarial adaptada a requerimientos corporativos, anonimización ISO 27701 y extensibilidad vertical mediante el patrón EAV (Entity-Attribute-Value).
1. Esquema Relacional de la Tabla usuarios
La tabla almacena las credenciales nucleares y los datos de contacto indispensables:
CREATE TABLE `usuarios` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`usuario` VARCHAR(255) NOT NULL UNIQUE,
`nombres` VARCHAR(255) NOT NULL,
`apellido_paterno` VARCHAR(255) NOT NULL,
`apellido_materno` VARCHAR(255) NULL,
`correo` VARCHAR(255) NOT NULL UNIQUE,
`email_verified_at` TIMESTAMP NULL,
`password` VARCHAR(255) NOT NULL,
`remember_token` VARCHAR(100) NULL,
`cat_status` BIGINT UNSIGNED NOT NULL,
`password_changed_at` TIMESTAMP NULL,
`must_change_password` TINYINT(1) DEFAULT 0,
`two_factor_secret` TEXT NULL,
`two_factor_recovery_codes` TEXT NULL,
`two_factor_confirmed_at` TIMESTAMP NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
`deleted_at` TIMESTAMP NULL,
FOREIGN KEY (`cat_status`) REFERENCES `catalogos`(`id`)
);
Configuración del Proveedor en config/auth.php
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\Usuario::class,
],
],
2. Patrón EAV con Control de Versiones (configuracion_usuarios)
Para almacenar atributos personales (teléfono móvil, fecha de nacimiento, avatares, zona horaria, tema de UI) sin alterar el esquema relacional con migraciones constantes, Mañuti implementa la tabla configuracion_usuarios.
flowchart LR
U["[usuarios]"]
CONF["[configuracion_usuarios]"]
CAT["[catalogos]<br/>(CFG_ACTIVO / CFG_HISTORICO)"]
U <-->|"1 : N"| CONF
CONF <-->|"N : 1"| CAT
CREATE TABLE `configuracion_usuarios` (
`id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`usuario_id` BIGINT UNSIGNED NOT NULL,
`clave` VARCHAR(255) NOT NULL,
`valor` LONGTEXT NULL,
`cat_status` BIGINT UNSIGNED NOT NULL,
`created_at` TIMESTAMP NULL,
`updated_at` TIMESTAMP NULL,
`deleted_at` TIMESTAMP NULL,
FOREIGN KEY (`usuario_id`) REFERENCES `usuarios`(`id`) ON DELETE CASCADE,
FOREIGN KEY (`cat_status`) REFERENCES `catalogos`(`id`)
);
CREATE INDEX `config_usuario_clave_idx` ON `configuracion_usuarios`(`usuario_id`, `clave`);
3. Versionado de Historial e Inmutabilidad de Cambios
Al invocar $user->setConfig('clave', $valor), el sistema no ejecuta un UPDATE destructivo. Mantiene una auditoría histórica de cómo evoluciona el perfil:
flowchart TD
INPUT["[Nuevo Valor: '55-9999-8888']"]
CHECK{"¿Existe registro previo con<br/>cat_status = CFG_ACTIVO?"}
UPDATE["1. Actualiza registro previo a<br/>cat_status = CFG_HISTORICO"]
INSERT["2. Inserta nuevo registro con<br/>cat_status = CFG_ACTIVO"]
INPUT --> CHECK
CHECK -->|"Sí"| UPDATE
UPDATE --> INSERT
CHECK -->|"No"| INSERT
// app/Models/Usuario.php
public function setConfig(string $clave, mixed $valor): void
{
$idActivo = Catalogo::where('catalogo', 'CFG_ACTIVO')->value('id');
$idHistorico = Catalogo::where('catalogo', 'CFG_HISTORICO')->value('id');
$valorFinal = $this->isEncryptedKey($clave) && !empty($valor)
? Crypt::encryptString($valor)
: $valor;
$actual = $this->configuraciones()
->where('clave', $clave)
->where('cat_status', $idActivo)
->first();
if ($actual) {
if ($actual->valor === $valorFinal) return;
$actual->update(['cat_status' => $idHistorico]);
}
$this->configuraciones()->create([
'clave' => $clave,
'valor' => $valorFinal,
'cat_status' => $idActivo
]);
}
4. Desencriptación Selectiva al Vuelo
El método $user->getConfig($clave) detecta dinámicamente si el atributo solicitado forma parte del arreglo de datos personales protegidos (security_pii_fields) y ejecuta el descifrado simétrico AES-256 en memoria RAM de manera transparente.