Cuando trabajas con múltiples paquetes en PHP (como módulos propios), llega un punto donde necesitas debuggear directamente el código del paquete y no una copia dentro de vendor.
Si alguna vez terminaste debuggeando en vendor/ en lugar de tu proyecto real… esto es para ti.
En el desarrollo moderno de aplicaciones PHP con CodeIgniter 4, uno de los mayores retos es mantener un flujo de trabajo eficiente entre desarrollo local, control de versiones y gestión de dependencias.
Este artículo explica una arquitectura híbrida usando Composer + Git + symlinks que permite trabajar módulos de forma profesional sin depender del directorio vendor.
🧠 El problema clásico
Cuando trabajamos con módulos reutilizables (inventarios, CFDI, pagos, etc.), normalmente terminamos instalando todo en vendor/, lo que genera problemas:
Después de varios intentos y errores típicos (puertos, certificados, Docker, etc.), finalmente logré dejar funcionando ONLYOFFICE Document Server en Docker con acceso HTTPS y listo para integrarse con Nextcloud instalado vía Snap. Aquí te dejo el proceso completo con datos genéricos 👇
🚧 Problemas comunes
Error de conexión (connection refused o connection reset)
Certificados SSL no detectados
Confusión entre Apache, Nginx y Docker
Intentar usar HTTPS dentro del contenedor (mala idea 😅)
🧠 Lo importante que debes entender
ONLYOFFICE usa Nginx interno, no Apache
Docker debe correr en HTTP interno
El SSL se maneja mejor fuera del contenedor
Nextcloud Snap ya trae su propio Apache (aislado)
👉 La solución correcta: usar Apache HTTP Server del sistema como proxy inverso
⚙️ Configuración final
🐳 1. Ejecutar ONLYOFFICE en HTTP
docker run -d -p 8080:80 onlyoffice/documentserver
Si ya tienes Nextcloud instalado con Snap, el siguiente paso lógico es convertirlo en una suite completa tipo Google Workspace. Para eso, puedes integrar OnlyOffice, una poderosa herramienta que permite editar documentos Word, Excel y PowerPoint directamente desde tu nube.
¿Qué es OnlyOffice?
OnlyOffice es una suite ofimática online que se integra perfectamente con Nextcloud, permitiendo editar documentos en tiempo real, colaborar con otros usuarios y mantener compatibilidad con formatos de Microsoft Office.
sudo snap set nextcloud ports.http=80
sudo snap set nextcloud ports.https=443
sudo ufw allow 8080
Recomendaciones
🔒 Usa HTTPS (Let’s Encrypt)
⚡ Usa mínimo 2GB de RAM
🌐 Configura dominio si lo usarás fuera de red local
📦 Usa Docker para mayor estabilidad
Conclusión
Integrar OnlyOffice con Nextcloud transforma tu servidor en una plataforma completa de productividad, similar a Google Drive o Microsoft 365, pero con control total sobre tus datos.
Apóyame
Si esta guía te fue útil y quieres apoyar más contenido como este:
Si quieres tener tu propia nube privada tipo Google Drive o Dropbox, Nextcloud es una de las mejores opciones. En esta guía te mostraré cómo instalarlo en Ubuntu Server 24.04 de forma sencilla, incluyendo un script automático que hace casi todo por ti.
¿Qué es Nextcloud?
Nextcloud es una plataforma de almacenamiento en la nube de código abierto que te permite guardar archivos, sincronizarlos entre dispositivos, compartirlos y mucho más, todo en tu propio servidor.
📁 Almacenamiento privado
🔄 Sincronización de archivos
🔐 Control total de tus datos
👥 Compartir archivos fácilmente
Requisitos
Servidor con Ubuntu Server 24.04
Acceso SSH con permisos sudo
Conexión a internet
Instalación automática (Script)
Para facilitar todo el proceso, puedes usar el siguiente script que instala Apache, PHP, MariaDB y Nextcloud automáticamente.
#!/bin/bash
# ==========================================
# Instalador automático de Nextcloud
# Ubuntu Server 24.04 (con soporte para PPA sury.org)
# Versión: 4.0 - Definitiva (resuelve conflictos de PHP)
# Adaptado por julio101290
# ==========================================
set -e
# -------------------- CONFIGURACIÓN --------------------
DEFAULT_PORT="${NEXTCLOUD_PORT:-80}"
NC_DIR="${NEXTCLOUD_DIR:-/var/www/nextcloud}"
DB_NAME="${NEXTCLOUD_DB_NAME:-nextcloud}"
DB_USER="${NEXTCLOUD_DB_USER:-ncuser}"
# Generar contraseña segura si no se proporciona
if [ -z "$NEXTCLOUD_DB_PASS" ]; then
DB_PASS=$(openssl rand -base64 24 | tr -d "=+/" | cut -c1-24)
else
DB_PASS="$NEXTCLOUD_DB_PASS"
fi
NONINTERACTIVE="${NONINTERACTIVE:-false}"
# Colores
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# -------------------- FUNCIONES AUXILIARES --------------------
error_exit() {
echo -e "${RED}ERROR: $1${NC}" >&2
exit 1
}
info() {
echo -e "${GREEN}➡️ $1${NC}"
}
warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
confirm() {
if [ "$NONINTERACTIVE" = "true" ]; then
return 0
fi
read -p "$1 [S/n]: " -n 1 -r
echo
[[ $REPLY =~ ^[Ss]$ ]] || [[ -z $REPLY ]]
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# -------------------- VERIFICACIONES INICIALES --------------------
if [ "$EUID" -ne 0 ]; then
error_exit "Ejecuta con sudo o como root."
fi
# -------------------- ACTUALIZACIÓN Y REPARACIÓN --------------------
info "Actualizando lista de paquetes..."
apt update -qq
info "Reparando paquetes rotos si los hay..."
apt --fix-broken install -y
# -------------------- APACHE Y PUERTO --------------------
info "Configurando Apache..."
if command_exists apache2; then
warn "Apache ya está instalado."
# Detectar puerto activo
if systemctl is-active --quiet apache2; then
CURRENT_PORT=$(ss -tlnp 2>/dev/null | grep apache2 | grep -oP ':\K\d+' | head -1)
fi
[ -z "$CURRENT_PORT" ] && CURRENT_PORT=$(grep -i "^Listen" /etc/apache2/ports.conf 2>/dev/null | head -1 | awk '{print $2}')
[ -z "$CURRENT_PORT" ] && CURRENT_PORT=80
if [ "$CURRENT_PORT" != "$DEFAULT_PORT" ] && [ "$DEFAULT_PORT" != "80" ]; then
warn "Apache ya escucha en el puerto $CURRENT_PORT. Se usará ese."
PORT=$CURRENT_PORT
else
PORT=$DEFAULT_PORT
fi
else
info "Instalando Apache..."
apt install -y apache2
PORT=$DEFAULT_PORT
fi
# Configurar puerto si es necesario
if [ "$PORT" != "80" ]; then
info "Ajustando Apache al puerto $PORT..."
sed -i "s/^Listen 80/Listen $PORT/g" /etc/apache2/ports.conf
grep -q "^Listen $PORT" /etc/apache2/ports.conf || echo "Listen $PORT" >> /etc/apache2/ports.conf
systemctl restart apache2
fi
# -------------------- PHP Y EXTENSIONES (RESOLUCIÓN DEFINITIVA) --------------------
info "Verificando PHP..."
PHP_INSTALLED=false
USE_SURY=false
if command_exists php; then
PHP_INSTALLED=true
PHP_VER=$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' 2>/dev/null)
warn "PHP $PHP_VER detectado."
# Determinar origen de PHP
if apt-cache policy php | grep -q "sury.org"; then
USE_SURY=true
warn "PHP instalado desde el repositorio 'deb.sury.org'."
fi
else
info "PHP no está instalado. Se instalará desde los repositorios oficiales de Ubuntu."
fi
# Si PHP está presente pero no todas las extensiones, las instalamos según el origen
if [ "$PHP_INSTALLED" = true ]; then
# Extensiones necesarias
NEEDED_EXTS="bcmath gmp imagick mysql zip gd mbstring curl xml intl"
MISSING_EXTS=""
# Verificar qué extensiones faltan
for ext in $NEEDED_EXTS; do
case $ext in
mysql)
if ! php -m | grep -qiE "mysqli|pdo_mysql"; then
MISSING_EXTS="$MISSING_EXTS mysql"
fi
;;
*)
if ! php -m | grep -qi "$ext"; then
MISSING_EXTS="$MISSING_EXTS $ext"
fi
;;
esac
done
if [ -n "$MISSING_EXTS" ]; then
info "Faltan extensiones: $MISSING_EXTS"
# Solución específica para el conflicto de php-imagick
if echo "$MISSING_EXTS" | grep -q "imagick"; then
warn "Se detectó conflicto con php-imagick. Eliminando versión incorrecta..."
apt remove -y php-imagick 2>/dev/null || true
fi
# Instalar según el origen
if [ "$USE_SURY" = true ]; then
info "Instalando extensiones desde el PPA sury.org..."
# Construir lista de paquetes específicos para sury
SURY_PKGS=""
for ext in $MISSING_EXTS; do
case $ext in
mysql) SURY_PKGS="$SURY_PKGS php${PHP_VER}-mysql" ;;
imagick) SURY_PKGS="$SURY_PKGS php${PHP_VER}-imagick" ;;
bcmath) SURY_PKGS="$SURY_PKGS php${PHP_VER}-bcmath" ;;
gmp) SURY_PKGS="$SURY_PKGS php${PHP_VER}-gmp" ;;
zip) SURY_PKGS="$SURY_PKGS php${PHP_VER}-zip" ;;
gd) SURY_PKGS="$SURY_PKGS php${PHP_VER}-gd" ;;
mbstring) SURY_PKGS="$SURY_PKGS php${PHP_VER}-mbstring" ;;
curl) SURY_PKGS="$SURY_PKGS php${PHP_VER}-curl" ;;
xml) SURY_PKGS="$SURY_PKGS php${PHP_VER}-xml" ;;
intl) SURY_PKGS="$SURY_PKGS php${PHP_VER}-intl" ;;
esac
done
apt install -y --allow-downgrades $SURY_PKGS
else
# Repositorios oficiales de Ubuntu
info "Instalando extensiones desde repositorios oficiales..."
OFFICIAL_PKGS=""
for ext in $MISSING_EXTS; do
case $ext in
mysql) OFFICIAL_PKGS="$OFFICIAL_PKGS php-mysql" ;;
imagick) OFFICIAL_PKGS="$OFFICIAL_PKGS php-imagick" ;;
bcmath) OFFICIAL_PKGS="$OFFICIAL_PKGS php-bcmath" ;;
gmp) OFFICIAL_PKGS="$OFFICIAL_PKGS php-gmp" ;;
zip) OFFICIAL_PKGS="$OFFICIAL_PKGS php-zip" ;;
gd) OFFICIAL_PKGS="$OFFICIAL_PKGS php-gd" ;;
mbstring) OFFICIAL_PKGS="$OFFICIAL_PKGS php-mbstring" ;;
curl) OFFICIAL_PKGS="$OFFICIAL_PKGS php-curl" ;;
xml) OFFICIAL_PKGS="$OFFICIAL_PKGS php-xml" ;;
intl) OFFICIAL_PKGS="$OFFICIAL_PKGS php-intl" ;;
esac
done
apt install -y $OFFICIAL_PKGS
fi
else
info "Todas las extensiones necesarias ya están presentes."
fi
else
# Instalación limpia de PHP (sin PPA previo)
info "Instalando PHP 8.3 y extensiones desde repositorios oficiales..."
apt install -y php8.3 php8.3-cli php8.3-common php8.3-mysql php8.3-zip \
php8.3-gd php8.3-mbstring php8.3-curl php8.3-xml php8.3-bcmath \
php8.3-intl php8.3-imagick php8.3-gmp libapache2-mod-php8.3
PHP_VER="8.3"
fi
# Habilitar módulos de Apache
a2enmod rewrite headers env dir mime 2>/dev/null || true
systemctl restart apache2
# -------------------- BASE DE DATOS --------------------
info "Configurando base de datos..."
if command_exists mysql; then
warn "MySQL/MariaDB ya está instalado."
systemctl start mariadb 2>/dev/null || systemctl start mysql 2>/dev/null || true
else
info "Instalando MariaDB..."
apt install -y mariadb-server
systemctl start mariadb
fi
# Crear base de datos y usuario (sin romper existentes)
create_database() {
local db="$1" user="$2" pass="$3"
if mysql -u root -e "exit" 2>/dev/null; then
MYSQL_CMD="mysql -u root"
else
echo -ne "${YELLOW}Contraseña de root de MySQL:${NC} "
read -s root_pass
echo
MYSQL_CMD="mysql -u root -p$root_pass"
fi
if $MYSQL_CMD -e "USE $db" 2>/dev/null; then
warn "Base de datos '$db' ya existe. Se usará la existente."
else
info "Creando base de datos '$db'..."
$MYSQL_CMD -e "CREATE DATABASE $db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
fi
if $MYSQL_CMD -e "SELECT 1 FROM mysql.user WHERE User='$user' AND Host='localhost'" | grep -q 1; then
warn "Usuario '$user' ya existe. Actualizando contraseña..."
$MYSQL_CMD -e "ALTER USER '$user'@'localhost' IDENTIFIED BY '$pass';"
else
info "Creando usuario '$user'..."
$MYSQL_CMD -e "CREATE USER '$user'@'localhost' IDENTIFIED BY '$pass';"
fi
$MYSQL_CMD -e "GRANT ALL PRIVILEGES ON $db.* TO '$user'@'localhost';"
$MYSQL_CMD -e "FLUSH PRIVILEGES;"
}
create_database "$DB_NAME" "$DB_USER" "$DB_PASS"
# -------------------- DESCARGA E INSTALACIÓN DE NEXTCLOUD --------------------
info "Preparando Nextcloud en $NC_DIR..."
mkdir -p "$(dirname "$NC_DIR")"
cd "$(dirname "$NC_DIR")"
if [ -d "$NC_DIR" ] && [ -f "$NC_DIR/config/config.php" ]; then
warn "Parece que Nextcloud ya está instalado."
if confirm "¿Deseas reinstalar (se perderán los datos)?"; then
info "Eliminando instalación anterior..."
rm -rf "$NC_DIR"
else
info "Manteniendo instalación existente. Saliendo."
exit 0
fi
fi
# Descargar Nextcloud
if [ ! -f latest.zip ]; then
info "Descargando Nextcloud..."
wget -q --show-progress https://download.nextcloud.com/server/releases/latest.zip
else
warn "El archivo latest.zip ya existe. Usándolo."
fi
info "Instalando unzip..."
apt install -y unzip
info "Descomprimiendo..."
unzip -q -o latest.zip
rm -f latest.zip
# Si se movió el directorio de instalación
if [ "$NC_DIR" != "/var/www/nextcloud" ] && [ -d "/var/www/nextcloud" ]; then
mv /var/www/nextcloud "$NC_DIR"
fi
# Configurar permisos
chown -R www-data:www-data "$NC_DIR"
chmod -R 755 "$NC_DIR"
# -------------------- CONFIGURACIÓN DE APACHE --------------------
info "Configurando VirtualHost de Apache..."
SERVER_IP=$(hostname -I | awk '{print $1}')
cat > /etc/apache2/sites-available/nextcloud.conf <<EOF
<VirtualHost *:$PORT>
ServerName $SERVER_IP
DocumentRoot $NC_DIR
<Directory $NC_DIR>
Require all granted
AllowOverride All
Options FollowSymLinks MultiViews
</Directory>
ErrorLog \${APACHE_LOG_DIR}/nextcloud_error.log
CustomLog \${APACHE_LOG_DIR}/nextcloud_access.log combined
</VirtualHost>
EOF
# Deshabilitar sitio por defecto y habilitar Nextcloud
a2dissite 000-default.conf 2>/dev/null || true
a2ensite nextcloud.conf
systemctl reload apache2
# -------------------- RESUMEN FINAL --------------------
echo ""
echo "========================================="
echo -e "${GREEN}✅ INSTALACIÓN COMPLETA${NC}"
echo "========================================="
echo -e "${BLUE}🌐 Accede a Nextcloud en tu navegador:${NC}"
echo -e " http://$SERVER_IP:$PORT"
echo ""
echo -e "${BLUE}📌 Datos de la base de datos:${NC}"
echo " Nombre: $DB_NAME"
echo " Usuario: $DB_USER"
echo " Contraseña: $DB_PASS"
echo " Host: localhost"
echo ""
echo -e "${YELLOW}⚠️ IMPORTANTE: Completa la instalación en el navegador${NC}"
echo " Selecciona 'MySQL/MariaDB' e introduce los datos de arriba."
echo "========================================="
# Guardar credenciales opcionalmente
if [ "$NONINTERACTIVE" != "true" ]; then
if confirm "¿Guardar las credenciales en /root/nextcloud_credentials.txt?"; then
cat > /root/nextcloud_credentials.txt <<EOF
# Credenciales de Nextcloud - $(date)
IP: $SERVER_IP
Puerto: $PORT
Base de datos: $DB_NAME
Usuario DB: $DB_USER
Contraseña DB: $DB_PASS
EOF
chmod 600 /root/nextcloud_credentials.txt
info "Credenciales guardadas en /root/nextcloud_credentials.txt"
fi
fi
echo -e "${GREEN}¡Disfruta de Nextcloud! 🚀${NC}"
Cómo ejecutar el script
Crear el archivo: nano install_nextcloud.sh
Pegar el script y guardar
Dar permisos: chmod +x install_nextcloud.sh
Ejecutar: ./install_nextcloud.sh
Acceso a Nextcloud
Una vez terminado, abre tu navegador y entra a:
http://TU_IP
Desde ahí podrás crear el usuario administrador y conectar la base de datos.
Recomendaciones de seguridad
🔐 Cambiar la contraseña de la base de datos
🔒 Activar HTTPS con Let’s Encrypt
🧱 Configurar firewall (UFW)
💾 Hacer backups regularmente
Desinstalación
#!/bin/bash
# ==========================================
# Desinstalador automático de Nextcloud
# Ubuntu Server 24.04
# Revierte todos los cambios del instalador
# ==========================================
set -e
# -------------------- CONFIGURACIÓN --------------------
NC_DIR="${NEXTCLOUD_DIR:-/var/www/nextcloud}"
DB_NAME="${NEXTCLOUD_DB_NAME:-nextcloud}"
DB_USER="${NEXTCLOUD_DB_USER:-ncuser}"
APACHE_SITE="nextcloud.conf"
# Colores
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# -------------------- FUNCIONES --------------------
error_exit() {
echo -e "${RED}ERROR: $1${NC}" >&2
exit 1
}
info() {
echo -e "${GREEN}➡️ $1${NC}"
}
warn() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
confirm() {
read -p "$1 [s/N]: " -n 1 -r
echo
[[ $REPLY =~ ^[Ss]$ ]]
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# -------------------- VERIFICACIÓN DE EJECUCIÓN --------------------
if [ "$EUID" -ne 0 ]; then
error_exit "Ejecuta con sudo o como root."
fi
echo ""
echo -e "${RED}=========================================${NC}"
echo -e "${RED} DESINSTALADOR DE NEXTCLOUD${NC}"
echo -e "${RED}=========================================${NC}"
warn "Este script eliminará Nextcloud y sus configuraciones."
warn "Los datos de la base de datos y archivos se perderán permanentemente."
echo ""
if ! confirm "¿Estás seguro de que deseas continuar?"; then
info "Desinstalación cancelada."
exit 0
fi
# -------------------- 1. DETENER SERVICIOS --------------------
info "Deteniendo servicios relacionados..."
systemctl stop apache2 2>/dev/null || true
systemctl stop mariadb 2>/dev/null || true
systemctl stop mysql 2>/dev/null || true
# -------------------- 2. ELIMINAR ARCHIVOS DE NEXTCLOUD --------------------
if [ -d "$NC_DIR" ]; then
info "Eliminando directorio de Nextcloud en $NC_DIR..."
rm -rf "$NC_DIR"
else
warn "El directorio $NC_DIR no existe."
fi
# Buscar y eliminar cualquier otro directorio de Nextcloud en /var/www
if [ -d "/var/www/nextcloud" ] && [ "/var/www/nextcloud" != "$NC_DIR" ]; then
info "Eliminando /var/www/nextcloud..."
rm -rf "/var/www/nextcloud"
fi
# -------------------- 3. ELIMINAR CONFIGURACIÓN DE APACHE --------------------
info "Eliminando configuración de Apache para Nextcloud..."
a2dissite "$APACHE_SITE" 2>/dev/null || true
rm -f "/etc/apache2/sites-available/$APACHE_SITE"
rm -f "/etc/apache2/sites-enabled/$APACHE_SITE"
# Opcionalmente restaurar el sitio por defecto si existe
if [ -f "/etc/apache2/sites-available/000-default.conf" ]; then
a2ensite 000-default.conf 2>/dev/null || true
fi
# Revertir cambios en el puerto si solo se usó para Nextcloud (opcional avanzado)
# Nota: no revertimos automáticamente porque podría haber otros sitios usando ese puerto.
systemctl reload apache2 2>/dev/null || true
# -------------------- 4. ELIMINAR BASE DE DATOS Y USUARIO (OPCIONAL) --------------------
echo ""
if confirm "¿Eliminar la base de datos '$DB_NAME' y el usuario '$DB_USER'?"; then
if command_exists mysql; then
info "Eliminando base de datos y usuario..."
# Determinar comando MySQL con o sin contraseña
if mysql -u root -e "exit" 2>/dev/null; then
MYSQL_CMD="mysql -u root"
else
echo -ne "${YELLOW}Ingresa la contraseña de root de MySQL:${NC} "
read -s root_pass
echo
MYSQL_CMD="mysql -u root -p$root_pass"
fi
$MYSQL_CMD -e "DROP DATABASE IF EXISTS $DB_NAME;" 2>/dev/null && info "Base de datos $DB_NAME eliminada."
$MYSQL_CMD -e "DROP USER IF EXISTS '$DB_USER'@'localhost';" 2>/dev/null && info "Usuario $DB_USER eliminado."
$MYSQL_CMD -e "FLUSH PRIVILEGES;" 2>/dev/null
else
warn "MySQL/MariaDB no está instalado. No se pudo eliminar la base de datos."
fi
else
info "Base de datos conservada."
fi
# -------------------- 5. ELIMINAR PAQUETES (OPCIONAL) --------------------
echo ""
if confirm "¿Eliminar paquetes de Apache, PHP y MariaDB instalados por el script?"; then
info "Eliminando paquetes..."
# Lista de paquetes comunes del instalador
PACKAGES_TO_REMOVE="apache2 mariadb-server mariadb-client"
PHP_PACKAGES="php8.3 php8.3-cli php8.3-common php8.3-mysql php8.3-zip php8.3-gd php8.3-mbstring php8.3-curl php8.3-xml php8.3-bcmath php8.3-intl php8.3-imagick php8.3-gmp libapache2-mod-php8.3"
# Añadir también los nombres genéricos por si acaso
PHP_GENERIC="php php-cli php-common php-mysql php-zip php-gd php-mbstring php-curl php-xml php-bcmath php-intl php-imagick php-gmp libapache2-mod-php"
# Preguntar si se quiere purgar (eliminar también configuraciones)
PURGE=""
if confirm "¿Eliminar también los archivos de configuración (purgar)?"; then
PURGE="--purge"
fi
apt remove $PURGE -y $PACKAGES_TO_REMOVE $PHP_PACKAGES $PHP_GENERIC 2>/dev/null || true
apt autoremove -y
info "Paquetes eliminados."
else
info "Paquetes conservados."
fi
# -------------------- 6. LIMPIEZA ADICIONAL --------------------
info "Limpiando archivos residuales..."
rm -f /var/www/latest.zip 2>/dev/null || true
rm -f /root/nextcloud_credentials.txt 2>/dev/null || true
# -------------------- 7. RESUMEN FINAL --------------------
echo ""
echo "========================================="
echo -e "${GREEN}✅ DESINSTALACIÓN COMPLETA${NC}"
echo "========================================="
echo -e "${YELLOW}Se han eliminado:${NC}"
echo " - Nextcloud (directorio $NC_DIR)"
echo " - Configuración de Apache (sitio nextcloud)"
if confirm "¿Eliminar base de datos?" 2>/dev/null; then
echo " - Base de datos $DB_NAME y usuario $DB_USER"
fi
if confirm "¿Eliminar paquetes?" 2>/dev/null; then
echo " - Paquetes de Apache, PHP y MariaDB (si se seleccionó)"
fi
echo ""
echo -e "${BLUE}Nota: Los datos personales de Nextcloud no son recuperables.${NC}"
echo "========================================="
Conclusión
Con este método puedes tener tu propia nube privada funcionando en pocos minutos. Es ideal para uso personal o incluso para pequeñas empresas que quieran tener control total sobre sus datos.
Apóyame
Si esta guía te fue útil y quieres apoyar más contenido como este, puedes hacerlo aquí:
Resumen: implementación limpia y los bloques de código bien acomodados para que en la vista de saldos solo se muestren los productos/lotes de los almacenes a los que el usuario tiene permiso.
🧾 1) Controller — Preparar empresas y almacenes del usuario
Este bloque es el handler principal (método index()). Se encarga de:
Recuperar empresas asociadas al usuario
Recuperar almacenes activos asignados al usuario
Construir el builder via mdlGetSaldos y preparar respuesta para DataTables (draw/records/paginación)
// Controller: index()
public function index() {
helper('auth');
// 1) Obtener usuario
$idUser = user()->id;
// 2) Empresas del usuario (fallback a [0] si no tiene)
$titulos["empresas"] = $this->empresa->mdlEmpresasPorUsuario($idUser);
$empresasID = count($titulos["empresas"]) === 0 ? [0] : array_column($titulos["empresas"], "id");
// 3) Almacenes (storages) asignados al usuario y activos (status = 'on')
$storagesUser = $this->storagesPerUser
->where("idUsuario", $idUser)
->where("status", "on")
->asArray()
->findAll();
$storagesUser = count($storagesUser) === 0 ? [0] : array_column($storagesUser, "idStorage");
// 4) Si es petición AJAX (DataTables) devolvemos JSON paginado
if ($this->request->isAJAX()) {
$request = service('request');
$draw = (int) $request->getGet('draw');
$start = (int) $request->getGet('start');
$length = (int) $request->getGet('length');
$searchValue = $request->getGet('search')['value'] ?? '';
$orderColumnIndex = (int) ($request->getGet('order')[0]['column'] ?? 0);
$orderDir = $request->getGet('order')[0]['dir'] ?? 'asc';
// Mapeo de columnas (orden)
$fields = [
'id' => 'a.id',
'nombreAlmacen' => 'c.name',
'lote' => 'a.lote',
'codigoProducto' => 'a.codigoProducto',
'descripcion' => 'a.descripcion',
'fullname' => 'e.fullname'
];
$orderField = $fields[$orderColumnIndex] ?? 'id';
// Builder desde el modelo con filtros de empresas y almacenes
$builder = $this->saldos->mdlGetSaldos($empresasID, $storagesUser);
// Conteo total (sin filtros de búsqueda)
$total = clone $builder;
$recordsTotal = $total->countAllResults(false);
// Filtro de búsqueda global
if (!empty($searchValue)) {
$builder->groupStart();
foreach ($fields as $field) {
$builder->orLike($field, $searchValue);
}
$builder->groupEnd();
}
// Conteo filtrado
$filteredBuilder = clone $builder;
$recordsFiltered = $filteredBuilder->countAllResults(false);
// Obtener página
$data = $builder->orderBy("a." . $orderField, $orderDir)
->get($length, $start)
->getResultArray();
// Respuesta JSON para DataTables
return $this->response->setJSON([
'draw' => $draw,
'recordsTotal' => $recordsTotal,
'recordsFiltered' => $recordsFiltered,
'data' => $data,
]);
}
// Vista normal (no-AJAX)
$titulos["title"] = "Info Productos";
$titulos["subtitle"] = "Extrae la información de los productos por el código de barras";
return view('julio101290\\boilerplateinventory\\Views\\saldos', $titulos);
}
🧱 2) Modelo — Builder con filtros por empresa y almacén
Este método devuelve un Query Builder ya filtrado por $idEmpresas y $storagesUser. Úsalo tal cual en el controller.
public function mdlGetSaldos($idEmpresas, $storagesUser) {
return $this->db->table('saldos a')
->select("
a.id,
a.idEmpresa,
a.idAlmacen,
a.idProducto,
a.codigoProducto,
a.lote,
a.descripcion,
a.cantidad,
a.created_at,
a.deleted_at,
a.updated_at,
b.nombre AS nombreEmpresa,
c.name AS nombreAlmacen,
COALESCE(e.fullname, 'Sin asignar') AS fullname
")
// JOINs para mostrar nombres legibles
->join('empresas b', 'a.idEmpresa = b.id')
->join('storages c', 'a.idAlmacen = c.id')
// LEFT JOINs para evitar romper si no hay relación
->join('productsemployes pe', 'pe.idProduct = a.id', 'left')
->join('employes e', 'e.id = pe.idEmploye', 'left')
// filtros de permisos: solo empresas/almacenes permitidos
->whereIn('a.idEmpresa', $idEmpresas)
->whereIn('a.idAlmacen', $storagesUser)
->orderBy('a.id', 'DESC');
}
🛠️ 3) Notas técnicas y buenas prácticas aplicadas
Back-end es la fuente de verdad: los arrays de IDs ($empresasID y $storagesUser) se construyen en el servidor a partir de user()->id. Nunca confíes en listas enviadas por cliente.
Fallback seguro: usar [0] cuando no hay empresas/almacenes evita que whereIn reciba un array vacío y genere errores SQL. En ese caso la consulta devuelve resultados vacíos.
LEFT JOINs: mantuvimos LEFT JOIN en relaciones opcionales para que la consulta no falle si no hay datos relacionados (por ejemplo, empleado no asignado).
Clonar builder: clonar el builder para conteo (countAllResults(false)) mantiene el flujo de DataTables sin re-ejecutar joins innecesarios.
🔍 4) Sugerencias de índices SQL (para rendimiento)
Recomiendo agregar índices (si no existen) para acelerar filtros y joins:
-- Índices recomendados
CREATE INDEX idx_saldos_idEmpresa ON saldos (idEmpresa);
CREATE INDEX idx_saldos_idAlmacen ON saldos (idAlmacen);
CREATE INDEX idx_saldos_codigoProducto ON saldos (codigoProducto);
CREATE INDEX idx_saldos_lote ON saldos (lote);
-- Índices en tablas relacionadas
CREATE INDEX idx_storages_id ON storages (id);
CREATE INDEX idx_empresas_id ON empresas (id);
Si usas PostgreSQL, considera índices compuestos o índices GIN si aplicarás búsquedas textuales complejas.
🧪 5) Pruebas recomendadas (QA) — pasos concretos
Usuario sin almacenes: Inicia sesión con un usuario sin almacenes asignados. La tabla debe venir vacía. Ver el mensaje UX (ver sección UX abajo).
Usuario con 1 almacén: Inicia sesión con acceso a un solo almacén; la vista debe mostrar únicamente los saldos de ese almacén.
Usuario con múltiples almacenes: Comprueba que aparecen filas de cualquiera de esos almacenes, y que no aparecen filas de almacenes no asignados.
Busqueda global: Ejecuta una búsqueda por codigoProducto, lote o descripcion y valida que los resultados respetan el filtro por almacén.
Paginación y orden: Revisa recordsTotal y recordsFiltered cuando aplicas orden y búsqueda; deben reflejar correctamente la cantidad total y la cantidad filtrada.
Seguridad: Intenta manipular parámetros GET/POST desde el cliente (por ejemplo, forzar otro idAlmacen) y verifica que no se muestran saldos si el usuario no tiene permiso.
💬 6) Mensajes UX sugeridos
Si el usuario no tiene almacenes asignados, muestra un mensaje amigable y accionable en la UI (evita pantalla en blanco):
<div class="alert alert-info">
<strong>Sin almacén asignado</strong><br>
No tienes almacenes asignados. Contacta al administrador para que te asigne los permisos necesarios.
</div>
🔐 7) Seguridad y consideraciones adicionales
Recalcula permisos siempre en servidor: no aceptes listas desde cliente.
Validar status: al desactivar un permiso (status != ‘on’), asegúrate que en la siguiente petición el usuario deje de ver los datos correspondientes.
Auditoría: opcionalmente loguea consultas sensibles (quién vio qué y cuándo) para trazabilidad.
📦 8) Release & commit (referencia)
Los cambios fueron incluidos en el release v1.2.3 y el commit con la implementación es este:
Generador Automático de CRUD para CodeIgniter 4 – julio101290/boilerplate
🚀 Generador Automático de CRUD para CodeIgniter 4: Crea Módulos Completos en 1 Minuto 🔥
¿Cansado de escribir el mismo código una y otra vez? ¿Tus proyectos se retrasan por la tediosa creación de modelos, controladores y vistas? ¡Tenemos la solución! Te presento el Generador Automático de CRUD para CodeIgniter 4, una herramienta integrada en mi boilerplate que transforma una tabla de base de datos en un módulo funcional, seguro y profesional en menos de 60 segundos. Ahorra cientos de horas y olvídate de los errores repetitivos.
Es un controlador inteligente que, a partir del nombre de una tabla existente en tu base de datos, genera de forma automática todos los archivos necesarios para un CRUD completo:
✅ Modelo con validaciones y soft delete
✅ Controlador con DataTables server-side
✅ Vistas (listado + modal) integradas con AdminLTE
✅ Archivos de idioma (inglés y español)
✅ Migración lista para ejecutar
✅ Rutas listas para copiar o integradas en tu paquete
✅ Permisos creados automáticamente (RBAC)
Todo esto con código limpio, indentado y siguiendo las mejores prácticas.
💡 Dato curioso: El generador lee la estructura de tu tabla y adapta los campos automáticamente. Si tu tabla tiene campos como created_at, updated_at, deleted_at, los maneja de forma especial para que el soft delete funcione perfectamente.
🏗️ El entorno perfecto: julio101290/boilerplate
Este generador vive dentro de mi fork del excelente boilerplate de agungsugiarto, adaptado y mejorado para proyectos reales. Incluye:
🔹 Lee el composer.json y extrae el namespace PSR-4 automáticamente.
🔹 Crea la estructura src/Models, src/Controllers, etc.
🔹 Actualiza el archivo src/Config/Routes.php del paquete con las nuevas rutas.
🔹 Tu paquete se vuelve autónomo y portable.
🔑 Permisos gestionados como profesionales
Antes: los permisos se creaban en caliente al generar el CRUD (poco ortodoxo). Ahora: cuando el destino es un paquete vendor, el generador actualiza el Seeder correspondiente (ej. BoilerplateCFDIDescargaMasiva.php), añadiendo la línea para crear el permiso y asignarlo al admin.
Así, la instalación de permisos se hace como Dios manda: con php spark db:seed.
⏱️ Ahorro de tiempo real
Tarea
Sin generador
Con generador
Ahorro
CRUD de 10 campos
45-60 min
1 min
~98%
20 tablas por proyecto
15-20 horas
20 minutos
¡Días!
Ese tiempo lo puedes reinvertir en lógica de negocio que realmente aporta valor. Además, todo el código generado sigue el mismo patrón, reduciendo la deuda técnica y facilitando el mantenimiento.
📝 Código completo del generador
Aquí tienes la clase AutoCrudControllerComposer en su versión final. Cópiala directamente en tu proyecto (julio101290/boilerplate/Controllers/).
db = \Config\Database::connect();
$this->authorize = Services::authorization();
$this->users = new UserModel();
helper('utilerias');
}
/**
* Método principal para generar el CRUD
*
* @param string $table Nombre de la tabla
* @param string|null $targetType 'app' o 'vendor' (por GET)
* @param string|null $vendorPackage Paquete vendor
* @param string|null $vendorNamespace Namespace (auto-detected)
*/
public function index($table, $targetType = null, $vendorPackage = null, $vendorNamespace = null)
{
// Leer de GET si no se pasaron como argumentos
if ($targetType === null) {
$targetType = $this->request->getGet('target') ?? 'app';
}
if ($vendorPackage === null && $targetType === 'vendor') {
$vendorPackage = $this->request->getGet('package');
}
if ($vendorNamespace === null && $targetType === 'vendor') {
$vendorNamespace = $this->request->getGet('namespace');
}
$this->targetType = $targetType;
if ($targetType === 'vendor' && $vendorPackage) {
$this->setupVendorPaths($vendorPackage, $vendorNamespace);
}
$this->generateModel($table);
$this->generateController($table);
$this->generateView($table);
$this->generateViewModal($table);
$this->generateLanguage($table);
$this->generateMigration($table);
$this->generateLanguageES($table);
if ($targetType === 'vendor') {
$this->generateVendorRoutesFile($table);
$this->updateSeederPermissions($table);
} else {
$this->generatePermissions($table);
}
$tableUpCase = ucfirst($table);
echo "";
echo "✅ CRUD generado exitosamente en: " . ($targetType === 'vendor' ? $this->vendorPackage : 'app') . "";
echo "";
}
// ... (el resto de métodos: setupVendorPaths, generateModel, generateController, etc.)
// Por brevedad, no repetimos todo el código aquí, pero en el artículo real debes incluir el código completo.
}
?>
⚠️ Nota: El código anterior es un resumen. Para obtener el código completo, visita el repositorio en GitHub o copia el bloque que aparece al final de este artículo.
🔌 Cómo usarlo
Agrega la ruta en app/Config/Routes.php: $routes->get('generateCRUDComposer/(:any)', 'julio101290\boilerplate\Controllers\AutoCrudControllerComposer::index/$1');
Genera un CRUD en app: http://tusitio.com/generateCRUDComposer/nombre_tabla
Genera en tu paquete vendor: http://tusitio.com/generateCRUDComposer/nombre_tabla?target=vendor&package=tu/paquete
¡Y listo! En segundos tendrás todo el código listo para usar.
🎯 Conclusión y llamado a la acción
El generador automático de CRUD ha evolucionado de un simple script a una herramienta profesional que:
Se ha implementado una mejora importante en el manejo de DataTables con procesamiento server-side para el módulo de Bitácora en el proyecto jcposUltimate, resolviendo problemas de ordenamiento, paginación y búsqueda que impedían un funcionamiento correcto con MariaDB / MySQL.
Esta guía detalla el despliegue del cliente de base de datos, componente esencial para conectar SAP B1, Crystal Reports, Excel y Power BI.
1 Ubicación del Kit de Instalación
En entornos de SAP Business One, el instalador reside dentro de los paquetes de datos del servidor. No busque instaladores genéricos; navegue a la siguiente ruta:
…\HANA 2.0 rev 56\DATA_UNITS\SAP HANA CLIENT 2.0 FOR B1
Dentro encontrará la arquitectura dual de SAP:
📂 NT_X64: Carpeta exclusiva para Windows (64 bits).
📂 LINX64SUSE: Carpeta para servidores Linux/SUSE.
2 Instalación Paso a Paso
Acceda a la carpeta NT_X64.
Localice el ejecutable hdbinst.exe.
Derechos de Administrador: Haga clic derecho y seleccione “Ejecutar como administrador”.
Consola: Presione Enter para aceptar la ruta por defecto C:\hdbclient.
⚠️ Verificación: La instalación finaliza correctamente cuando visualiza el mensaje: Installation done.
3 Configuración del PATH
Para ejecutar herramientas como hdbsql desde cualquier carpeta, configure la variable de entorno:
Busque “Variables de Entorno” en el menú Inicio.
En Variables del sistema, edite la variable Path.
Agregue una nueva línea con: C:\hdbclient.
4 Activación ODBC (64 bits)
Para habilitar la conexión en aplicaciones de terceros:
Abra el Administrador de orígenes de datos ODBC (64 bits).
Vaya a DSN de sistema > Agregar.
Seleccione el controlador HDBODBC.
Configure la IP del servidor y el puerto (ej: 30015).
// Prueba de fuego en CMDhdbsql -n 192.168.X.X:30015 -u SYSTEM -p Password123 "SELECT * FROM DUMMY"
🚀 Troubleshooting Rápido
¿No ve el driver? Instale “Visual C++ Redistributable 2013/2015”.
¿Error -10709? Verifique el Firewall del servidor en el puerto 30015.
Este manual técnico explica cómo enlazar sucursales locales con sucursales de SAP Business One (OBPL) utilizando ODBC (eODBC) sobre SAP HANA, evitando el uso del Service Layer para consultas de alto rendimiento.
¿Qué problema resuelve esta implementación?
En muchos proyectos con SAP Business One, el uso del Service Layer para catálogos o consultas simples introduce latencia innecesaria. Este módulo soluciona ese problema permitiendo:
Relacionar sucursales locales con sucursales SAP
Consultar la tabla OBPL directamente desde SAP HANA
Reducir tiempos de respuesta usando ODBC
Eliminar dependencias innecesarias del Service Layer
¿Por qué usar ODBC en SAP Business One?
El uso de ODBC en SAP HANA es ideal para consultas de solo lectura, catálogos y validaciones rápidas.
Característica
Service Layer
ODBC SAP HANA
Rendimiento
Medio
Alto
Latencia
Alta
Baja
Ideal para
CRUD complejo
Catálogos y consultas
Dependencia SAP
Alta
Media
Estructura del módulo de enlace SAP
La solución se divide en cuatro componentes principales:
Tabla de enlace entre sucursal local y SAP
Controlador con consultas ODBC
Vista con DataTables server-side
Formulario con Select2 dinámico
Tabla link_sap_branchoffice
Esta tabla permite mantener la relación entre el sistema local y SAP Business One sin duplicar información de SAP.
Campo
Descripción
id
ID interno
idEmpresa
Empresa local
idBranchOffice
Sucursal local
idBranchOfficeSAP
Sucursal SAP (OBPL.BPLId)
created_at
Fecha de creación
updated_at
Fecha de actualización
deleted_at
Borrado lógico
Conexión a SAP HANA usando ODBC
La conexión ODBC se realiza directamente contra SAP HANA, lo que permite consultas rápidas y estables.
Este paso es obligatorio para evitar errores como invalid table name en SAP HANA.
Consulta optimizada a la tabla OBPL
Para obtener una sucursal SAP se realiza una consulta directa a la tabla OBPL:
SELECT
"BPLId",
"BPLName"
FROM OBPL
WHERE "Disabled" <> 'Y'
AND "BPLId" = ?
Consulta directa
Sin bucles innecesarios
Ideal para formularios y edición
Listado con DataTables server-side
El módulo utiliza DataTables en modo server-side para manejar grandes volúmenes de datos:
Paginación eficiente
Búsqueda global
Ordenamiento dinámico
Filtrado por empresa del usuario
Select2 dinámico para sucursales
Sucursal local
Se cargan las sucursales locales vía AJAX usando Select2.
Sucursal SAP (OBPL)
Las sucursales SAP se consultan directamente desde SAP HANA vía ODBC, sin Service Layer.
Beneficios clave de esta arquitectura
Mayor rendimiento en SAP Business One
Menor latencia en consultas
Menos carga en el Service Layer
Arquitectura escalable y mantenible
Buenas prácticas recomendadas
Usar ODBC solo para consultas de lectura
Usar Service Layer para escrituras en SAP
Centralizar credenciales ODBC
Siempre definir el schema en SAP HANA
Conclusión
Este enfoque permite integrar SAP Business One con sistemas locales de forma eficiente, utilizando ODBC sobre SAP HANA como alternativa rápida y estable al Service Layer.
Ideal para catálogos, validaciones, reportes y sistemas híbridos SAP.
Usamos cookies en nuestro sitio web para brindarle la experiencia más relevante recordando sus preferencias y visitas repetidas. Al hacer clic en "Aceptar", acepta el uso de TODAS las cookies.
This website uses cookies to improve your experience while you navigate through the website. Out of these cookies, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may have an effect on your browsing experience.
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Any cookies that may not be particularly necessary for the website to function and is used specifically to collect user personal data via analytics, ads, other embedded contents are termed as non-necessary cookies. It is mandatory to procure user consent prior to running these cookies on your website.