|
|
Crear Extensiones |
| Método | Para qué |
|---|---|
| create($type, $name, $settings) | Nueva instancia. Regresa el extension_id. |
| update($extensionId, $name, $settings) | Actualiza una instancia existente. |
| getById($extensionId) | Trae una instancia — usa esto para precargar el formulario al editar. |
| getByType($type) | Todas las instancias de un tipo — para listarlas. |
| delete($extensionId) | Elimina una instancia. |
Para la vista (theme/template/admin/edit.tpl), llama primero a renderTemplate('admin/common/header', [...]) y después utiliza include para cargar tu propio .tpl. El patrón es el mismo que utiliza cualquier pantalla de administración del núcleo; la diferencia es que tu .tpl reside dentro de la carpeta de la extensión en lugar de theme/default/template/admin/.
Paso 3: Crear render.php
Define una única función con el nombre exacto nx_extension_render_{nombre_de_tu_carpeta}($settings).
La función recibe el array de configuración ya decodificado desde JSON y debe devolver un string de HTML. No debe utilizar echo; debe utilizar return.
<?php
function nx_extension_render_mi_extension_nueva($settings) {
$titulo = $settings['titulo'] ?? 'Mi Widget';
$cuantos = max(1, (int)($settings['cuantos'] ?? 5));
ob_start();
?>
<h3 style="margin:0 0 14px 0;"><?php echo htmlspecialchars($titulo, ENT_QUOTES, 'UTF-8'); ?></h3>
<p>Aquí va tu HTML.</p>
<?php
return ob_get_clean();
}
Regla importante: no envolver el HTML en .card
nx_render_items() ya coloca la tarjeta externamente, siguiendo el mismo patrón que un Bloque de Contenido normal. Si render.php agrega su propio <div class="card">, se generará una tarjeta anidada dentro de otra. La función debe devolver únicamente el contenido interno.
Responsive: el widget no conoce la columna en la que se encuentra
nx_render_items() no le pasa a tu extensión ninguna pista de si está en la barra lateral (angosta) o en la columna central (ancha) — y no es necesario que lo sepa. Permite que el CSS se adapte al ancho real disponible, sin imponer anchos mínimos ni cuadrículas forzadas (nada de grid-template-columns: minmax(220px, ...) ni @container — eso rompe en columnas angostas). Permite que el texto se ajuste y envuelva de forma natural.
Si el widget utiliza display: flex en cualquier nivel, agrega min-width: 0 tanto al contenedor flex como a sus hijos — si no, un hijo con contenido largo puede forzar que toda la columna se ensanche y descuadre la página completa.
Sesión y usuario dentro de render.php
Si el widget necesita determinar si existe un cliente con una sesión iniciada (por ejemplo, un formulario que solo aplica a usuarios logueados), consulta directamente $_SESSION['user_id']. render.php se ejecuta dentro del mismo proceso que la página pública, con la sesión ya iniciada. Si no existe una sesión, muestra un mensaje alternativo en lugar del widget completo. Nunca asumas que siempre habrá un usuario autenticado.
Si necesitas datos de otros módulos
Utiliza require_once para cargar lo que necesites al inicio de render.php; se trata de un archivo PHP normal:
require_once(__DIR__ . '/../../system/layout_model.php');
// o el modelo que necesites: category_model.php, form_model.php, etc.
Constantes/funciones ya disponibles sin hacer nada
SITE_URL— la URL base del sitio, sin diagonal al final.htmlspecialchars($texto, ENT_QUOTES, 'UTF-8')— úsalo siempre en cualquier dato que venga de$settingso de la base de datos, antes de imprimirlo.
Variables de CSS para que el widget combine con el tema
var(--color-primary)
var(--color-primary-hover)
var(--color-text)
var(--color-text-muted) /* gris para texto secundario/fechas */
var(--color-border)
var(--radius) /* redondeado estándar */
Endpoint público propio (blog/controller/{tipo}.php)
Si el widget necesita un endpoint AJAX que sea invocado por el visitante del sitio (no por el administrador), debe ubicarse en blog/controller/ y seguir el mismo patrón de sesión y CSRF utilizado por los controladores públicos del núcleo. Esto es diferente de admin/controller/edit.php, descrito anteriormente, que únicamente es accesible para el administrador durante la configuración de una instancia.
Idioma propio de la extensión
Si el widget contiene textos fijos que deban traducirse (es-mx / en-gb), no deben agregarse al archivo de idioma del núcleo. Cada extensión mantiene sus propios archivos de idioma dentro de su carpeta:
extensions/{tipo}/language/es-mx/{tipo}.php
extensions/{tipo}/language/en-gb/{tipo}.php
Cárgalo con la función hermana de loadLanguage(), hecha justo para esto:
require_once(__DIR__ . '/../../system/language.php');
$lang = loadExtensionLanguage('mi_extension_nueva');
echo htmlspecialchars($lang['texto_bienvenida'] ?? '', ENT_QUOTES, 'UTF-8');
El formato es el mismo que el de cualquier archivo de idioma del núcleo: un array $_ con las claves utilizadas por la extensión.
<?php
// extensions/mi_extension_nueva/language/es-mx/mi_extension_nueva.php
$_['texto_bienvenida'] = 'Bienvenido';
loadExtensionLanguage() respeta el idioma configurado en el sitio. Si el archivo correspondiente no existe, utiliza es-mx como respaldo o devuelve un arreglo vacío; la página pública no se interrumpe.
El mismo mecanismo puede utilizarse dentro de manifest.php, si el nombre o la descripción de tu extensión deben traducirse: carga loadExtensionLanguage() arriba del return y usa $lang['manifest_name'] ?? 'texto por default'.
Paso 4: Probar la extensión
- Copia la carpeta de la extensión dentro de
extensions/. - Ingresa al administrador. En el menú, "Extensiones" debería mostrar automáticamente el nuevo tipo dentro del acordeón, ya que el sistema realiza el escaneo de forma automática.
- Crea una instancia de prueba y configura sus valores.
- Ve a "Crear Contenido". En el selector de columnas debería aparecer la instancia dentro de "Extensiones".
- Inserta la instancia, guarda los cambios y visita la página en modo público. Pruébala tanto en la barra lateral como en la columna central para verificar que funcione correctamente en ambas.
Patrones reales ya probados en el sistema
Ya existen extensiones reales funcionando en producción que cubren cada uno de estos casos. Puedes utilizarlas como referencia para implementar un nuevo tipo sin partir desde cero:
| Patrón | Qué ilustra |
|---|---|
| Caso simple | Solo manifest.php + render.php, editor genérico de texto/número. |
| Sin llamadas a servidor | Varios campos de configuración, todo corre en el navegador del visitante. |
| Sesión del visitante | Revisa si hay un usuario con sesión iniciada dentro de render.php antes de mostrar contenido. |
| Editor propio simple | admin_url con buscador en vivo, guarda una lista dentro de settings. |
| Editor propio + base externa | admin_url conectado a una fuente de datos externa vía ExternalDbConnector. |
| Wizard completo | Varios pasos, explora tablas/columnas en vivo, arma JOINs, pagina resultados — con varios endpoints AJAX propios (guardar, borrar, consultar esquema). |
Cómo conectar la extensión a una base de datos externa
Si la extensión necesita obtener datos de un sistema externo (una tienda, un ERP, cualquier base MySQL que no sea la de NexFlow), nunca solicites las credenciales directamente dentro de la pantalla de la extensión. El administrador del sitio las registra una sola vez en Settings > Conectores > Bases de Datos Externas, asignándoles un nombre amigable (ej. "tienda_principal"). La extensión únicamente solicita ese nombre; nunca debe acceder ni gestionar directamente el usuario o la contraseña reales.
Este mecanismo ya se utiliza en extensiones reales del sistema que requieren datos externos (gráficas configurables, vitrinas de productos). Cualquier extensión nueva que lo necesite usa la misma clase central: system/external_db_connector.php.
require_once(__DIR__ . '/../../../../system/external_db_connector.php');
try {
$conn = ExternalDbConnector::connect($connectorName); // el nombre amigable guardado en Conectores
$tablas = ExternalDbConnector::listTables($conn);
// ... tu lógica ...
mysqli_close($conn);
} catch (\Throwable $e) {
// Nunca regreses $e->getMessage() al navegador -- podría revelar
// detalles del servidor externo. Muestra un aviso generico.
}
Métodos disponibles en ExternalDbConnector:
| Método | Para qué |
|---|---|
| connect(string $friendlyName): \mysqli | Abre la conexión usando el conector guardado. Truena con RuntimeException si el nombre no existe o la conexión falla. |
| listTables($conn): array | Todas las tablas del servidor conectado. |
| listColumns($conn, string $table): array | Columnas de una tabla. |
| autoDetectJoins(array $tableColumns): array | Dado ['tabla1' => ['col1','col2'], 'tabla2' => [...]], propone JOINs donde encuentra un nombre de columna en común entre dos tablas — útil para armar un wizard que sugiera relaciones sin que el admin tenga que saber el esquema de memoria. |
| quoteIdentifier(string $name): string | Úsalo siempre que un nombre de tabla o columna venga de la elección del admin (no de tu código fijo) antes de meterlo en una consulta. Nombres de tabla/columna nunca se pueden mandar como parámetro preparado (?) — solo los valores aceptan eso — así que este método valida que el nombre sea únicamente letras, números y guion_bajo y lo envuelve en backticks; cualquier cosa rara se rechaza de plano en vez de intentar escaparla. |
| fetchGroupedData($conn, array $settings, ?int $page, ?int $perPage): array | Trae datos agrupados por una columna con una o más medidas (SUM/COUNT/AVG/MAX/MIN), con JOINs y WHERE opcionales — ya armado y probado, no reinventes tu propio SQL dinámico si lo que necesitas es "agrupar y sumar". Ver la firma completa de $settings en el código fuente si tu caso lo necesita. |
Siempre debe crearse una conexión nueva por cada request. La conexión no se almacena como singleton (a diferencia de DB:: para la base propia de NexFlow) porque cada instancia de una extensión puede apuntar a un servidor externo diferente y, por lo tanto, no corresponde reutilizar una conexión entre peticiones.
Cómo configura el administrador el conector
Antes de que la extensión pueda utilizar un nombre amigable, dicho conector debe haber sido registrado en Settings > Conectores > Bases de Datos Externas. Desde system/database_connector_model.php, estos son los métodos relevantes si la pantalla de administración necesita, por ejemplo, ofrecer un <select> con los conectores ya registrados:
require_once(__DIR__ . '/../../../../system/database_connector_model.php');
$nombres = DatabaseConnectorModel::getFriendlyNamesList(); // ['tienda_principal', 'erp_bodega', ...]
Cómo leer datos de formularios creados con NexFlow
Los Formularios (los que el administrador crea desde "Formularios" en el menú y que son distintos de las Extensiones) almacenan cada respuesta en blog_form_submission. Las respuestas del usuario se encuentran en una única columna data, en formato JSON, y las claves del JSON corresponden al field_name de cada campo del formulario, no a una columna independiente por campo.
Modelos a usar — system/form_submission_model.php y system/form_field_model.php:
| Método | Para qué |
|---|---|
| FormSubmissionModel::getByForm($formId) | Todas las respuestas de un formulario. |
| FormSubmissionModel::getByFormAndUser($formId, $userId) | Las respuestas de un usuario específico a ese formulario. |
| FormSubmissionModel::hasSubmitted($formId, $userId) / canSubmit($formId, $userId) | Para no dejar que alguien conteste dos veces, si el formulario lo exige. |
| FormSubmissionModel::create($formId, $userId, $data) | Guarda una respuesta nueva — $data es el array de respuestas, se guarda como JSON solo. |
| FormSubmissionModel::updateReviewStatus($submissionId, $status) | Cambia el estado de revisión: 0 pendiente, 1 aprobado, 2 rechazado. |
| FormSubmissionModel::getAllForReview($status, $limit, $offset) / getTotalCountForReview($status) | Listado paginado para una pantalla de revisión — pasa null como $status para traer las tres bandejas juntas. |
| FormFieldModel::getByForm($formId) | Los campos definidos del formulario (field_name, label, tipo, etc.) — necesario para saber qué significa cada clave dentro del JSON de data. |
El patrón real para convertir el JSON en pares etiqueta/valor legibles, ya utilizado en pantallas de revisión reales del sistema:
$fields = FormFieldModel::getByForm($formId);
$answers = json_decode($submission['data'], true) ?: [];
$readableAnswers = [];
foreach ($fields as $field) {
if (!array_key_exists($field['field_name'], $answers)) {
continue; // el campo no existía cuando se contesto, o se omitió
}
$value = $answers[$field['field_name']];
if (is_array($value)) {
$value = implode(', ', $value); // checkboxes multiples, etc.
}
$readableAnswers[] = ['label' => $field['label'], 'value' => $value];
}
Esto es necesario porque el JSON almacenado utiliza field_name (el identificador interno del campo) como clave, no la etiqueta legible que ve el usuario — y un campo puede haber sido agregado o eliminado del formulario después de que ya existan respuestas. Por ello, siempre debes validar mediante array_key_exists() antes de asumir que una clave existe.
Antes de subir la extensión
- Confirma que la carpeta contiene únicamente los archivos realmente necesarios. No incluyas archivos de prueba, archivos .zip antiguos ni carpetas correspondientes a herramientas de build.
- No incluyas credenciales, API keys ni contraseñas directamente en el código. Si la extensión necesita una clave externa, solicítala mediante un campo de configuración (fields en manifest.php o un campo dentro de la pantalla definida por admin_url), nunca escrita de forma permanente en un archivo.
- Realiza la prueba completa de la extensión (Paso 4) antes de subirla.
La extensión se sube como un archivo .zip que contiene la carpeta completa. Después de la subida, queda pendiente de revisión antes de poder utilizarse en un sitio real.
Errores comunes y diagnóstico
| Síntoma | Causa probable |
|---|---|
| Tu tipo no aparece en el acordeón | El nombre de la carpeta tiene algo mal escrito, falta manifest.php, o el manifest no regresa un array con name. |
| El widget no aparece en público, pero tampoco da error | Tu función nx_extension_render_{tipo} no se llama EXACTO como tu carpeta, o no existe. |
| Sale "Notice: undefined array key" en $settings | Siempre usa ?? 'valor por defecto' al leer cualquier clave de $settings — una instancia vieja podría no tener un campo que agregaste después. |
| Tu tarjeta sale doble/anidada | Le pusiste tu propio <div class="card"> por dentro — quítalo, ya lo pone nx_render_items(). |
| Las viñetas de una lista se salen del margen | Envuelve tu contenido en un <div style="width:100%; padding:0; margin:0; box-sizing:border-box;"> — la regla .card > * le quita el padding a listas que sean hijas directas. |
| Tu widget se ve bien en el centro pero rompe/ensancha la barra lateral | Revisa que no tengas white-space: nowrap en texto largo dentro de un elemento sin min-width: 0. Permite que el texto se ajuste y envuelva de forma natural. |
| Tu widget se ve chiquito/con letra distinta al resto del sitio | No le pongas font-size propio a tu texto — hereda el tamaño del tema a propósito. |
| Con admin_url, la pantalla de edición no guarda nada | Revisa que estés llamando a ExtensionModel::create()/update() — no hay otro camino soportado para escribir en blog_extension. |

Reseñas
Tu opinión nos ayuda a seguir creciendo. Si ya trabajaste con nosotras, cuéntanos tu experiencia y ayuda a otras creadoras a decidirse.
Reseñas de Clientes
“Excellent Software! I Simply love it!”
“Esta increíble su sistema Tíos! Ahora vuelo con mi ordenador Gracias!”
“Que bonito CMS!”

Comentarios
Todavia no hay comentarios. Se el primero en comentar.
Inicia sesion para dejar un comentario.