Appearance
Sistema de Configuración - Documentación Técnica
Conceptual Foundation: Para entender los CONCEPTOS de multi-tenancy con configuración de niveles, consulte Database Architecture: Multi-Tenant. Este documento describe la implementación técnica del sistema de configuración.
Este documento describe la implementación técnica del sistema de configuración data_config con soporte para claves anidadas usando notación de punto.
Tabla de Contenidos
- Descripción General
- Estructura de Base de Datos
- Soporte Multi-Tenant
- API del Modelo Config
- Sistema de Claves Anidadas
- Convenciones de Nomenclatura
- Resolución de Prioridad Multi-Tenant
- Consideraciones de Implementación
- Guía de Migración
- Testing
- Solución de Problemas
Descripción General
La tabla data_config proporciona un sistema de almacenamiento de configuración clave-valor flexible para Sistema Bautista. Soporta:
- Pares clave-valor simples: Almacenamiento de configuración plana tradicional
- Claves anidadas con notación de punto: Organización jerárquica usando formato
modulo.entidad.propiedad - Aislamiento multi-tenant: Configuraciones a nivel EMPRESA y SUCURSAL
- Compatibilidad hacia atrás: Las claves legacy continúan funcionando sin modificación
Archivos Clave:
- Migration:
migrations/migrations/tenancy/20240823200714_new_table_data_config.php - Model:
models/general/Config.php - Seeds:
migrations/seeds/tenancy/DataConfig.php - Tests:
Tests/Unit/General/ConfigModelTest.php
Estructura de Base de Datos
Definición de Tabla
sql
CREATE TABLE data_config (
id SERIAL PRIMARY KEY,
clave VARCHAR(100),
valor VARCHAR(500),
help VARCHAR(500)
);Descripción de Columnas
| Columna | Tipo | Descripción |
|---|---|---|
id | SERIAL | Clave primaria auto-incremental |
clave | VARCHAR(100) | Clave de configuración (soporta notación de punto) |
valor | VARCHAR(500) | Valor de configuración (almacenado como string) |
help | VARCHAR(500) | Descripción/documentación opcional para la clave |
Índices
Considerar agregar un índice en clave para rendimiento:
sql
CREATE INDEX idx_data_config_clave ON data_config(clave);Soporte Multi-Tenant
La tabla data_config se crea en dos niveles de tenancy:
| Nivel | Patrón de Schema | Descripción |
|---|---|---|
LEVEL_EMPRESA (1) | public | Configuraciones por defecto de toda la empresa |
LEVEL_SUCURSAL (2) | suc{XXXX} | Configuraciones específicas de sucursal que sobrescriben las de empresa |
Esto se configura en la migration usando ConfigurableMigration:
php
protected function getDefaultLevels(): array
{
return [self::LEVEL_EMPRESA, self::LEVEL_SUCURSAL];
}Aislamiento por Schema
Cada schema de tenant tiene su propia tabla data_config, permitiendo:
- Valores por defecto de toda la empresa en
public.data_config - Sobrescrituras específicas por sucursal en
suc0001.data_config,suc0002.data_config, etc.
API del Modelo Config
Implementación Actual
Ubicada en models/general/Config.php:
php
namespace App\models\general;
use App\models\Model;
use PDO;
class Config extends Model
{
public function __construct(PDO $conn)
{
parent::__construct($conn, 'data_config');
}
/**
* Obtiene todas las configuraciones como array clave-valor
* @return array<string, string>
*/
public function getAll(): array;
/**
* Obtiene un valor de configuración individual por clave
* @return mixed Valor o false si no se encuentra
*/
public function getOneByKey(string $key): mixed;
}Métodos Propuestos para Claves Anidadas
php
/**
* Obtiene valor de configuración con resolución automática de claves anidadas
*
* @param string $key Clave en notación de punto (ej: 'membresia.facturacion.plazo_dias')
* @return mixed Valor o false si no se encuentra
*/
public function getMapped(string $key): mixed;
/**
* Obtiene todas las configuraciones que coincidan con un prefijo como array anidado
*
* @param string $prefix Prefijo de clave (ej: 'membresia.facturacion')
* @return array Array asociativo anidado de configuraciones coincidentes
*/
public function getByPrefix(string $prefix): array;
/**
* Transforma filas planas clave-valor en array asociativo anidado
*
* @param array $rows Filas de base de datos con columnas 'clave' y 'valor'
* @return array Estructura anidada
* @internal
*/
private function mapNestedKeys(array $rows): array;Sistema de Claves Anidadas
Formato de Notación de Punto
Las claves usan puntos (.) como separadores de nivel:
{modulo}.{entidad}.{propiedad}Ejemplos
| Clave | Módulo | Entidad | Propiedad |
|---|---|---|---|
membresia.facturacion.plazo_vencimiento_dias | membresia | facturacion | plazo_vencimiento_dias |
membresia.facturacion.genera_recibo | membresia | facturacion | genera_recibo |
ventas.impresion.copias_defecto | ventas | impresion | copias_defecto |
ctacte.notificaciones.email_habilitado | ctacte | notificaciones | email_habilitado |
Ejemplos de Uso
Obtener un Valor Anidado Individual
php
// Enfoque legacy (sigue funcionando)
$plazo = $config->getOneByKey('membresia.facturacion.plazo_vencimiento_dias');
// Retorna: "30"
// Nuevo enfoque mapeado (propuesto)
$plazo = $config->getMapped('membresia.facturacion.plazo_vencimiento_dias');
// Retorna: "30"Obtener un Grupo de Configuraciones
php
$facturacionConfig = $config->getByPrefix('membresia.facturacion');
// Retorna array anidado:
// [
// 'plazo_vencimiento_dias' => '30',
// 'genera_recibo' => '1',
// 'tipo_comprobante_defecto' => 'FC',
// ]Obtener Todas las Configuraciones de un Módulo
php
$membresiaConfig = $config->getByPrefix('membresia');
// Retorna array completamente anidado:
// [
// 'facturacion' => [
// 'plazo_vencimiento_dias' => '30',
// 'genera_recibo' => '1',
// ],
// 'notificaciones' => [
// 'email_aviso_vencimiento' => '1',
// 'dias_anticipacion' => '7',
// ],
// ]Compatibilidad Hacia Atrás
Las claves legacy sin puntos continúan funcionando exactamente como antes:
php
// Clave legacy (sin puntos)
$config->getOneByKey('PermisosEmpresa'); // Funciona sin cambios
$config->getOneByKey('fact-defecto'); // Funciona sin cambios
$config->getOneByKey('CantCajas'); // Funciona sin cambios
// Nueva clave anidada
$config->getOneByKey('membresia.facturacion.plazo_dias'); // También funcionaConvenciones de Nomenclatura
Estructura de Clave
{nivel_1}.{nivel_2}.{nivel_3}| Nivel | Contenido | Ejemplos |
|---|---|---|
| 1º | Módulo/Área | membresia, ventas, ctacte, tesoreria, stock, crm |
| 2º | Entidad/Funcionalidad | facturacion, reportes, notificaciones, impresion |
| 3º | Propiedad específica | plazo_vencimiento_dias, email_habilitado, copias_defecto |
Reglas de Formato
- Todo en minúsculas: Usar
membresianoMembresia - Snake_case: Usar
plazo_vencimiento_diasnoplazoVencimientoDias - Nombres descriptivos: Usar
genera_recibo_automaticonogra - Máximo 3 niveles: Evitar
a.b.c.d.e- reestructurar si es necesario
Claves de Primer Nivel Reservadas
| Prefijo de Clave | Módulo |
|---|---|
membresia | Módulo de membresías |
ventas | Módulo de ventas |
compras | Módulo de compras |
ctacte | Cuentas corrientes |
tesoreria | Tesorería |
stock | Inventario |
crm | CRM |
contabilidad | Contabilidad |
sistema | Configuraciones del sistema |
Resolución de Prioridad Multi-Tenant
Orden de Búsqueda
Al recuperar valores de configuración:
- Nivel SUCURSAL (más específico) - Verificar
suc{XXXX}.data_config - Nivel EMPRESA (fallback) - Verificar
public.data_config
Patrón de Implementación
php
public function getWithFallback(string $key): mixed
{
// 1. Intentar nivel SUCURSAL primero (schema actual)
$value = $this->getOneByKey($key);
if ($value !== false) {
return $value;
}
// 2. Fallback a nivel EMPRESA
// Requiere conexión secundaria a schema public
return $this->getFromEmpresaLevel($key);
}Ejemplo de Caso de Uso
Escenario: El período de facturación por defecto de la empresa es 30 días, pero la sucursal suc0001 necesita 45 días.
| Schema | Clave | Valor |
|---|---|---|
public | membresia.facturacion.plazo_dias | 30 |
suc0001 | membresia.facturacion.plazo_dias | 45 |
suc0002 | (no configurado) | - |
Resultado:
suc0001recupera45(sobrescritura de sucursal)suc0002recupera30(fallback de empresa)
Consideraciones de Implementación
Parseo de Claves
php
private function parseKey(string $key): array
{
return explode('.', $key);
}Construcción de Arrays Anidados (Iterativo)
php
private function mapNestedKeys(array $rows): array
{
$result = [];
foreach ($rows as $row) {
$keys = explode('.', $row['clave']);
$current = &$result;
foreach ($keys as $i => $key) {
if ($i === count($keys) - 1) {
// Última clave - asignar valor
$current[$key] = $row['valor'];
} else {
// Clave intermedia - asegurar que existe array
if (!isset($current[$key])) {
$current[$key] = [];
}
$current = &$current[$key];
}
}
}
return $result;
}Construcción de Arrays Anidados (Recursivo)
php
private function setNestedValue(array &$array, array $keys, string $value): void
{
$key = array_shift($keys);
if (empty($keys)) {
$array[$key] = $value;
} else {
if (!isset($array[$key]) || !is_array($array[$key])) {
$array[$key] = [];
}
$this->setNestedValue($array[$key], $keys, $value);
}
}Query por Prefijo
php
public function getByPrefix(string $prefix): array
{
$sql = "SELECT clave, valor FROM {$this->table}
WHERE clave LIKE :prefix";
$stmt = $this->conn->prepare($sql);
$stmt->execute([':prefix' => $prefix . '.%']);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Eliminar prefijo de claves antes de mapear
$stripped = array_map(function($row) use ($prefix) {
$row['clave'] = substr($row['clave'], strlen($prefix) + 1);
return $row;
}, $rows);
return $this->mapNestedKeys($stripped);
}Consideraciones de Rendimiento
- Índice en
clave: Esencial para queriesLIKEcon prefijo - Estrategia de Cache: Considerar cachear todas las configs al inicio del request
- Carga Perezosa: Cargar por prefijo solo cuando se necesite
- Operaciones por Lote: Usar
getByPrefix()en lugar de múltiples llamadas agetOneByKey()
Invalidación de Cache
Al implementar caching:
php
class Config extends Model
{
private static ?array $cache = null;
public function getOneByKey(string $key): mixed
{
if (self::$cache === null) {
self::$cache = $this->getAll();
}
return self::$cache[$key] ?? false;
}
public static function clearCache(): void
{
self::$cache = null;
}
}Triggers de invalidación:
- Después de INSERT/UPDATE/DELETE en
data_config - Al cambiar de schema (multi-tenant)
- Por request (para simplicidad)
Guía de Migración
Configuraciones Existentes
No se requiere migración. Las claves planas existentes continúan funcionando:
php
// Estas siguen funcionando exactamente como antes
$config->getOneByKey('fact-defecto');
$config->getOneByKey('CantCajas');
$config->getOneByKey('ModMovimientosAsociados');Agregando Nuevas Configuraciones Anidadas
- Elegir prefijo apropiado basado en el módulo
- Seguir convenciones de nomenclatura (snake_case, descriptivo)
- Documentar en columna
help - Agregar a seeds si es necesario
Ejemplo de seed para nueva configuración anidada:
php
$configurations = [
[
'clave' => 'membresia.facturacion.plazo_vencimiento_dias',
'valor' => '30',
'help' => 'Cantidad de dias para el vencimiento de facturas de membresia',
],
[
'clave' => 'membresia.facturacion.genera_recibo',
'valor' => '1',
'help' => 'Si se genera recibo automaticamente al facturar (0=No, 1=Si)',
],
];Reglas de Coexistencia
| Escenario | Comportamiento |
|---|---|
| Clave legacy existe | Funciona sin cambios |
| Nueva clave anidada agregada | Funciona con ambas APIs |
| Mismo prefijo, diferentes profundidades | Todas coexisten de forma segura |
Colisión de claves (ej: a.b y a.b.c) | Evitar - a.b sería hoja, no puede ser también rama |
Testing
Ejemplo de Test Unitario
php
public function testGetByPrefixReturnsNestedArray(): void
{
// Arrange
$rows = [
['clave' => 'membresia.facturacion.plazo_dias', 'valor' => '30'],
['clave' => 'membresia.facturacion.genera_recibo', 'valor' => '1'],
['clave' => 'membresia.notificaciones.email', 'valor' => '1'],
];
// Mock database para retornar estas filas para query de prefijo
// ...
// Act
$result = $this->model->getByPrefix('membresia');
// Assert
$this->assertIsArray($result);
$this->assertArrayHasKey('facturacion', $result);
$this->assertArrayHasKey('notificaciones', $result);
$this->assertEquals('30', $result['facturacion']['plazo_dias']);
$this->assertEquals('1', $result['facturacion']['genera_recibo']);
}
public function testMapNestedKeysBuildsCorrectStructure(): void
{
// Arrange
$rows = [
['clave' => 'a.b.c', 'valor' => '1'],
['clave' => 'a.b.d', 'valor' => '2'],
['clave' => 'a.e', 'valor' => '3'],
];
// Act
$result = $this->invokePrivateMethod('mapNestedKeys', [$rows]);
// Assert
$expected = [
'a' => [
'b' => [
'c' => '1',
'd' => '2',
],
'e' => '3',
],
];
$this->assertEquals($expected, $result);
}Casos de Test a Cubrir
- Camino Feliz: Recuperación válida de clave anidada
- Nivel Único: Clave sin puntos (legacy)
- Prefijo Vacío: Sin configuraciones coincidentes
- Anidamiento Profundo: 4+ niveles (caso extremo)
- Caracteres Especiales: Claves con unicode, números
- Inyección SQL: Patrones de prefijo maliciosos
- Comportamiento de Cache: Invalidación correcta
- Multi-Tenant: Resolución correcta de schema
Solución de Problemas
Problemas Comunes
Configuración No Encontrada
Síntoma: getOneByKey() retorna false
Checklist:
- Verificar ortografía de clave (sensible a mayúsculas/minúsculas)
- Verificar que el schema correcto está activo (multi-tenant)
- Confirmar que el registro existe:
SELECT * FROM data_config WHERE clave = 'tu.clave' - Verificar espacios al inicio/final en la clave
Estructura Anidada No se Construye
Síntoma: getByPrefix() retorna array plano en lugar de anidado
Checklist:
- Asegurar que las claves usan puntos (
.) no guiones (-) o guiones bajos (_) como separadores - Verificar que
mapNestedKeys()se está llamando en los resultados - Verificar colisiones de claves (misma clave como hoja y rama)
Problemas de Rendimiento
Síntoma: Cargas de página lentas con muchas llamadas a configuración
Soluciones:
- Agregar índice:
CREATE INDEX idx_data_config_clave ON data_config(clave) - Implementar caching (ver sección Cache)
- Usar
getByPrefix()para recuperación por lote en lugar de múltiples llamadas agetOneByKey()
Sobrescritura Multi-Tenant No Funciona
Síntoma: Sobrescritura de sucursal ignorada, siempre retorna el valor por defecto de empresa
Checklist:
- Verificar que la conexión es al schema correcto (
suc{XXXX}) - Verificar que el registro existe en el schema de la sucursal
- Verificar que
search_pathestá configurado correctamente en la conexión
Referencias
- Arquitectura Backend - Patrones generales del backend
- Sistema Multi-Tenant - Detalles de aislamiento por schema
- Sistema de Migrations - Uso de ConfigurableMigration
Última Actualización: 2026-01-28 Versión: 1.0.0 Estado: Propuesto (Funcionalidad de Claves Anidadas pendiente de implementación)