Instalar la edición PHP
La edición PHP se deposita tal cual en un alojamiento compartido: sin Composer, sin gestor de paquetes, sin servicio que mantener en marcha. Una carpeta, un archivo de configuración, una tarea programada. Este tutorial va de la carpeta vacía a la instancia en producción, incluido lo que la mayoría de las guías deja de lado: los permisos, el TLS, la tarea programada, las copias de seguridad y la actualización.
Lo que hace falta#
| Elemento | Versión | Por qué |
|---|---|---|
| PHP | 8.1 o más reciente | Tipos de unión, enum, readonly, never: el código los usa. |
pdo_sqlite | — | La base entera. Sin ella no funciona nada. |
mbstring | — | Tratamiento correcto de las cadenas en dieciséis idiomas. |
openssl | recomendada | Cifrado de los secretos en la base (contraseñas SMTP, tokens de externalización). |
curl | recomendada | Webhooks salientes y envío de copias a un tercero. |
intl | recomendada | Fechas escritas como en la edición Node. Sin ella, los nombres de meses vienen de los diccionarios. |
El asistente de instalación comprueba todo esto y lo muestra en pantalla antes de dejarle continuar: lo que es bloqueante detiene la instalación, lo que es opcional se señala sin bloquear.
No hay nada que instalar con composer: el lector IMAP, el archivo
tar.gz, el PDF, el CSV, el código QR y el cliente HTTP están escritos dentro
del producto. Es lo que permite depositarlo en un alojamiento donde no tiene línea de
comandos.
1. Subir los archivos#
Obtenga el archivo comprimido de la edición PHP y súbalo. La carpeta es así:
toutadmin/
├── app/ el código (nunca servido por el servidor web)
│ ├── Core/ núcleo: enrutamiento, base, sesiones, seguridad
│ ├── Modules/ negocio: RR. HH., gestión, proyectos, calidad…
│ ├── Controllers/
│ ├── views/ plantillas
│ ├── locales/ los dieciséis diccionarios
│ └── schema.sql el esquema, idéntico al de la edición Node
├── public/ ← la raíz web apunta aquí, y a ningún otro sitio
│ ├── index.php el controlador frontal
│ ├── css/ js/
├── data/ base, caja fuerte, portafirmas, documentos recibidos, copias
├── tools/ cron.php, check-keys.php
├── tests/
├── config.sample.php
└── VERSION
public/, no a la carpeta
Es el punto más importante de esta página. Si el servidor sirve la carpeta entera,
data/app.sqlite queda descargable por cualquiera: toda la base, huellas de
contraseñas y caja fuerte incluidas. La carpeta data/ está por encima de
public/ a propósito.
En un alojamiento compartido#
Se dan dos casos. Si su proveedor permite elegir la raíz de un dominio (cPanel, Plesk, y la
mayoría de los alojamientos en modo «carpeta raíz»), suba la carpeta fuera de
www/ y haga que el dominio apunte a toutadmin/public.
Si la raíz viene impuesta —a menudo www/ o public_html/— suba
dentro el contenido de public/ y el resto por encima, y
corrija luego la ruta al principio de public/index.php:
www/ ← raíz impuesta
├── index.php
├── css/ js/
toutadmin/ ← el resto, fuera del alcance de la web
├── app/
├── data/
└── config.php
// www/index.php — la línea que hay que ajustar
require_once __DIR__ . '/../toutadmin/app/bootstrap.php';
Como último recurso, si no puede poner nada por encima de la raíz, el archivo
.htaccess que va dentro de data/ ya deniega el acceso. Es un
cinturón, no una solución: prefiera siempre la carpeta fuera de la raíz.
2. Escribir la configuración#
cp config.sample.php config.php
Luego abra config.php:
<?php
return [
// Base SQLite y carpeta de datos: fuera de la raíz web.
'db_path' => __DIR__ . '/data/app.sqlite',
'data_dir' => __DIR__ . '/data',
// Clave propia de la instancia. Se genera una vez:
// php -r "echo bin2hex(random_bytes(32));"
'session_secret' => '…',
// Token de instalación: véase el paso siguiente.
'install_token' => '',
// Dirección pública, para los enlaces enviados por correo.
'base_url' => 'https://intranet.ejemplo.com',
// Actívelo solo detrás de un proxy de confianza.
'trust_proxy' => false,
// Topes: intentos de acceso por cuarto de hora y por dirección,
// peticiones por minuto y por dirección.
'login_rate_limit' => 10,
'global_rate_limit' => 300,
// Caducidad: inactividad, y luego duración absoluta que nada prolonga.
'session_idle_minutes' => 60,
'session_max_hours' => 12,
];
Sella las firmas del portafirmas y cifra los secretos en la base. Perderla invalida
firmas. Si deja el valor de ejemplo, el producto genera una por su cuenta en
data/.instance-key (permisos 0600): recuerde entonces incluir
ese archivo en las copias de seguridad.
También puede mantener la configuración en otro sitio: la variable de entorno
TOUTADMIN_CONFIG señala otro archivo.
3. Poner un token de instalación#
Entre la subida de los archivos y su paso por el asistente, la instancia es de quien la encuentre: el primero en llegar crea la cuenta de administración. La ventana es corta, y basta. Ponga, pues, un valor al azar antes de publicarla:
php -r "echo bin2hex(random_bytes(16));"
# luego en config.php:
'install_token' => 'c3f1…',
El asistente lo pide, lo compara en tiempo constante —una comparación ordinaria dejaría adivinar el token carácter a carácter— y registra cada rechazo. Sin el token no se crea nada. Una vez instalada la instancia, el asistente se cierra solo: el token puede quedarse, ya no sirve.
4. Permisos#
| Ruta | Permisos | Quién escribe |
|---|---|---|
data/ | 0770 | El servidor web, y solo él |
data/app.sqlite | 0660 | Creado por la instalación |
config.php | 0640 | Nadie: solo lectura para el servidor |
app/, public/ | 0755 | Nadie: solo lectura |
# ejemplo en un servidor donde PHP corre como www-data
sudo chown -R usted:www-data /var/www/toutadmin
sudo find /var/www/toutadmin -type d -exec chmod 750 {} \;
sudo find /var/www/toutadmin -type f -exec chmod 640 {} \;
sudo chmod 770 /var/www/toutadmin/data
sudo chmod 750 /var/www/toutadmin/public
SQLite escribe además archivos vecinos (-wal, -shm): es la
carpeta la que debe ser escribible, no solo la base.
5. El servidor web#
Nginx#
server {
listen 443 ssl http2;
server_name intranet.ejemplo.com;
root /var/www/toutadmin/public; # y nada más
index index.php;
ssl_certificate /etc/letsencrypt/live/intranet.ejemplo.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/intranet.ejemplo.com/privkey.pem;
client_max_body_size 20M; # depósitos en la caja fuerte, documentos recibidos
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Los archivos depositados no se sirven nunca directamente: pasan
# por una ruta que verifica la sesión y la huella.
location ~ ^/(data|app|tools|tests)/ { deny all; }
}
server {
listen 80;
server_name intranet.ejemplo.com;
return 301 https://$host$request_uri;
}
Apache#
<VirtualHost *:443>
ServerName intranet.ejemplo.com
DocumentRoot /var/www/toutadmin/public
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/intranet.ejemplo.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/intranet.ejemplo.com/privkey.pem
<Directory /var/www/toutadmin/public>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
El archivo public/.htaccess que se entrega ya reenvía todas las direcciones a
index.php. En un alojamiento compartido con Apache, es todo lo que hace falta.
TLS#
El producto pone la cabecera Strict-Transport-Security en cuanto la conexión
está cifrada, y nunca en claro: anunciada desde una página sin cifrar no se leería, y
encerraría una prueba local en https durante seis meses. En un servidor que
usted administre:
sudo certbot --nginx -d intranet.ejemplo.com
Detrás de un proxy o un balanceador que termina el TLS, ponga
'trust_proxy' => true —y solo ahí—: de lo contrario cualquiera podría anunciar
la dirección IP y el protocolo que quisiera en una cabecera.
6. La tarea programada#
Un sitio PHP solo funciona en el momento de una petición. Lo que la edición Node hace en su barrido horario —ocho operaciones— se hace aquí desde el cron del alojamiento:
* * * * * /usr/bin/php /var/www/toutadmin/tools/cron.php >> /var/log/toutadmin-cron.log 2>&1
| Qué hace la tarea | Con qué ritmo |
|---|---|
| Cierra las cuentas cuyo contrato ha vencido | en cada pasada |
| Emite las facturas de las suscripciones vencidas | en cada pasada, sin facturar nunca dos veces |
| Convierte los vencimientos en notificaciones | en cada pasada, sin duplicados |
| Purga las notificaciones leídas y el registro de auditoría | según la conservación elegida |
| Recoge el buzón contable (IMAP) | si la recogida está configurada |
| Vacía la cola de webhooks, con sus reintentos | en cada pasada |
| Crea la copia automática y la externaliza | cuando ha transcurrido el intervalo |
Llamar al script más a menudo que el intervalo configurado no hace copias más a menudo: nada
se dispara antes de tiempo. En un alojamiento que limita el cron a una pasada cada quince
minutos, sustituya * * * * * por */15 * * * *: los webhooks
saldrán simplemente con ese retraso.
Algunos alojamientos de gama baja no lo ofrecen. Una llamada HTTP desde un servicio de
terceros (un vigilante de disponibilidad y similares) no basta: cron.php se
niega a ejecutarse fuera de la línea de comandos, precisamente para que una dirección
pública no pueda disparar una copia de seguridad. En ese caso, la copia se lanza a mano
desde la pantalla Copias de seguridad.
7. El asistente#
Abra su dominio. Mientras no exista ninguna cuenta, toda dirección redirige a
/installation. El asistente cabe en una página:
- Los requisitos, comprobados y mostrados: lo que bloquea se marca como tal.
- El token de instalación, si está configurado.
- La organización: el nombre que se muestra en todas partes, y el número de días de vacaciones anuales atribuido a cada nuevo empleado no autónomo.
- El idioma por defecto de la instancia, entre los 16.
- La cuenta de administración: dirección y contraseña de al menos doce caracteres.
Todo se escribe en una sola transacción. En cuanto existe una cuenta,
/installation reenvía a la página de acceso: el asistente se ha cerrado solo, no
hay ningún archivo que borrar a mano.
8. Comprobar que todo funciona#
# las pruebas, en la máquina (no escriben en su base)
php tests/run.php
# las traducciones: ninguna pantalla usa una clave ausente de los diccionarios
php tools/check-keys.php
Luego, en la interfaz: cree un miembro, entre con él, deposite un adjunto, lance una copia manual y verifíquela desde la pantalla Copias de seguridad. Esos cuatro gestos tocan la base, los archivos, los permisos y el archivo comprimido; es decir, todo lo que el alojamiento debe saber hacer.
9. Actualizar#
# 1. una copia de seguridad primero, siempre
php tools/cron.php # o la pantalla Copias de seguridad
# 2. sustituir el código, no los datos
# app/ public/ tools/ tests/ VERSION ← sustituidos
# data/ config.php ← conservados tal cual
# 3. la base se actualiza sola en la primera petición
El esquema evoluciona por migraciones idempotentes: repetir la actualización no rompe nada. Una instancia de la edición Node y una de la edición PHP comparten el mismo esquema —142 tablas, 1311 columnas— y las mismas contraseñas: una base pasa de una a otra sin conversión.
10. Hacer copias de verdad#
El archivo tar.gz lleva la base (copiada mediante VACUUM INTO, por
tanto coherente incluso durante una escritura) y las cinco carpetas de archivos: fotos de
perfil, CV recibidos, caja fuerte, portafirmas, documentos recibidos. Cada archivo lleva su
huella SHA-256, verificada al restaurar.
Un archivo que se queda en el servidor que protege no protege nada: configure un destino remoto (FTPS o Google Drive) desde la pantalla Copias de seguridad. El detalle está en la página Copias de seguridad y restauración.
Probarlo primero en local#
cp config.sample.php config.php
php -S localhost:8000 -t public public/index.php
El servidor integrado de PHP basta para descubrir el producto. No sirve para producción: un solo proceso, sin TLS, sin límite de carga.
Documentación de Toutadmin, construida el 2026-09-13. Sitio autónomo, independiente del software.