<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>bLivecode</title><description> En bLivecode, llevamos la programación al corazón de la vida diaria. Descubre tutoriales, guías prácticas y proyectos reales que integran desarrollo web, automatización, productividad y soluciones creativas con código. Ya seas principiante o desarrollador con experiencia, aquí encontrarás inspiración para transformar tus ideas en software útil.</description><link>https://blivecode.com/</link><item><title>Automatizando la Creación de Virtual Hosts en Nginx con un Script Bash</title><link>https://blivecode.com/posts/script-bash-para-automatizar-vhost-en-ubuntu</link><guid isPermaLink="true">https://blivecode.com/posts/script-bash-para-automatizar-vhost-en-ubuntu</guid><description>Vamos a crear un script creado en BASH para automatizar la creación del v-Host en Ubuntu usando el stack LEMP (Linux, Nginx, MySQL y PHP). El script recibe 2 parámetros obligatorios, el nombre del dominio local y la carpeta del proyecto.</description><content:encoded>Si trabajas con servidores Nginx y necesitas configurar múltiples dominios locales para desarrollo, escribir las configuraciones manualmente puede ser tedioso. En este post, te mostraré cómo crear un script Bash que automatiza la creación y despliegue de virtual hosts en Nginx con PHP 8.3-FPM.

## El Script

Este script tiene varias funciones para hacer que un solo comando genere lo necesario para poner un dominio local en funcionamiento y cree el host a donde va a apuntar el dominio local. A continuación describo lo que hará el script.

- Crea un archivo de configuración en /etc/nginx/sites-available/
- Lo enlaza en /etc/nginx/sites-enabled/.
- Agrega el dominio al archivo /etc/hosts.
- Verifica la configuración de Nginx y recarga el servicio.

Para empezar vamos a necesitar algunas rutas y validación de datos requeridos por el script, empezamos con la validación

```bash
#!/bin/bash

# Verifica si se pasaron los parámetros correctos
# Nombre del Host local
# Ruta donde esta el proyecto
if [ &quot;$#&quot; -ne 2 ]; then
    echo &quot;El nombre de domino y la ubicación del proyecto son requeridos:&quot;
    echo &quot;$0 dominio.local ruta_proyecto_relativa&quot;
    exit 1
fi
# Creamos las variables a ocupar, Los nombres los puedes personalizar.
DOMINIO=&quot;$1&quot;
RUTA_RELATIVA=&quot;$2&quot;
USUARIO=$(whoami)
HOME_DIR=&quot;/home/$USUARIO&quot;
RUTA_COMPLETA=&quot;$HOME_DIR/$RUTA_RELATIVA&quot;
NGINX_SITES_AVAILABLE=&quot;/etc/nginx/sites-available&quot;
NGINX_SITES_ENABLED=&quot;/etc/nginx/sites-enabled&quot;
VHOST_CONF=&quot;$NGINX_SITES_AVAILABLE/$DOMINIO&quot;

# Verifica si la carpeta del proyecto existe
if [ ! -d &quot;$RUTA_COMPLETA&quot; ]; then
    echo &quot;Error: La carpeta del proyecto no existe en $RUTA_COMPLETA&quot;
    exit 1
fi
```

Bien, si las validaciones pasan correctamente vamos a proceder a la creación del archivo de configuración del v-Host y a realizar la configuración en Nginx para que este lo reconozca como un archivo de configuración

```bash

# Crea el archivo de configuración del vhost
sudo tee &quot;$VHOST_CONF&quot; &gt; /dev/null &lt;&lt;EOL
server {
    listen 80;
    server_name $DOMINIO;
    root $RUTA_COMPLETA/public;
    index index.php index.html index.htm;
    
    location / {
        try_files \$uri \$uri/ /index.php?\$query_string;
    }

    location ~ \\.php\$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
        fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
}
EOL

# Enlaza el vhost en sites-enabled
sudo ln -s &quot;$VHOST_CONF&quot; &quot;$NGINX_SITES_ENABLED/&quot;
```

Con esto creamos el archivo del v-Host y lo enlazamos a producción del servidor Nginx, ahora solo nos queda agregar el dominio al archivo host del sistema para que apunte a mi misma máquina y reiniciar el servicio de Nginx para que tome la nueva configuración, bien vamos a ver cómo quedaría.

```bash
# Agrega el dominio al archivo /etc/hosts si no está presente
if ! grep -q &quot;$DOMINIO&quot; /etc/hosts; then
    echo &quot;127.0.0.1 $DOMINIO&quot; | sudo tee -a /etc/hosts &gt; /dev/null
    echo &quot;Dominio $DOMINIO agregado a /etc/hosts&quot;
fi

# Verifica la configuración y recarga Nginx
sudo nginx -t &amp;&amp; sudo systemctl reload nginx

echo &quot;Vhost para $DOMINIO creado y habilitado con éxito.&quot;
```

Listo con esto ya tenemos el script para generar toda la configuración de un nuevo host que usemos de manera local con nginx y php.

Ahora solo quedaría configurarlo para poder ejecutarlo en la línea de comandos y pasar los parámetros, esto quedaría de la siguiente manera.

## 🛠️ Instalación del Script como Comando Global
Para que este script se pueda ejecutar desde cualquier parte del sistema como un comando, sigue estos pasos:

1. Mueve el script a `/usr/local/bin/` y dale permisos de ejecución:
   ```bash
   sudo mv crear_vhost.sh /usr/local/bin/crear_vhost
   sudo chmod +x /usr/local/bin/crear_vhost
   ```

2. Ahora puedes ejecutarlo desde cualquier ubicación con:
   ```bash
   crear_vhost midominio.local Proyectos/midominio
   ```


## 🔍 Uso del Script

Para crear un nuevo virtual host, simplemente usa:
```bash
crear_vhost midominio.local Proyectos/midominio
```
Esto hará que el dominio `midominio.local` apunte a `~/Proyectos/midominio/public`, permitiéndote acceder a él desde tu navegador.

Espero que esto les funcione para automatizar algo de sus tareas diarias.</content:encoded></item><item><title>📐 Convertir Pixeles a Centímetros en PHP</title><link>https://blivecode.com/posts/convertir-pixeles-a-centimetros</link><guid isPermaLink="true">https://blivecode.com/posts/convertir-pixeles-a-centimetros</guid><description>Algunas veces quieres saber a cuantos centimetros equivale la medida de pixeles en una imagen y esto puede requerirte si eres impresor de serigrafia o prendas en general</description><content:encoded>En mi trabajo diario con imágenes, muchas veces necesito convertir medidas de pixeles a centímetros, especialmente cuando trabajo con generación de PDFs, diseño para impresión o manipulación de imágenes. Para facilitarme la vida, tengo una función en PHP que utilizo constantemente

### 🧠 ¿Qué hace esta función?

La función recibe como parámetros un Array asociativo con las claves **width** y **height** de la imagen de la cual necesitas saber los centimetros, tambien recibe los **DPI** (Dots Per Inch) ó (puntos por pulgada), con esta información se realizará el calculo y regresará un Array con los datos.

```php
public function convertPixelToCentimeter($dataMeasures=[], $dpi=&quot;&quot;)
{
    $arrayCentimeter = [];
    if (empty($dataMeasures)) {
        return false;
    }

    if(!isset($dataMeasures[&apos;width&apos;]) &amp;&amp; !isset($dataMeasures[&apos;height&apos;]))
    {
        return false;
    }

    $dpiImage = ($dpi==&quot;&quot;) ? 300 : $dpi;
    $centimeterPerInch = 2.54; // 1 pulgada = 2.54 cm

    $arrayCentimeter[&apos;width&apos;] = round(($dataMeasures[&apos;width&apos;] * $centimeterPerInch) / $dpiImage, 2);
    $arrayCentimeter[&apos;height&apos;] = round(($dataMeasures[&apos;height&apos;] * $centimeterPerInch) / $dpiImage, 2);

    return $arrayCentimeter;
}

```

### 🧮 ¿Cómo funciona?

La funcion recibe como parametro el array **$dataMeasures** donde contendra los datos **width** y **height** de la imagen, en dado caso que el array se encuentre vacio o que alguno de los datos no venga definido, la funcion regresara un **false**.

El parametro de **$dpi** puede estar vacio  y se asignara un valor por defecto de **300** puntos por pulgada. 

La variable **$centimeterPerInch** es una constante, el valor asignado es por que **1 pulgada** equivale a **2.54 cm**.

### 🧪 Ejemplo de uso

El uso de la funcion puede darle al agregarla a un **Helper** o cualquier **clase** en PHP, a continuación dejo un ejemplo de como se puede usar en un ejercicio real.

```php

&lt;?php

$pixeles = [&apos;width&apos; =&gt; 1200, &apos;height&apos; =&gt; 1800];
$utils = new Utils();
$cm = $utils-&gt;convertPixelToCentimeter($pixeles);

print_r($cm);
// Resultado: [&apos;width&apos; =&gt; 10.16, &apos;height&apos; =&gt; 15.24]

```</content:encoded></item><item><title>Obtener ultimo día del mes</title><link>https://blivecode.com/posts/obtener-ultimo-dia-del-mes</link><guid isPermaLink="true">https://blivecode.com/posts/obtener-ultimo-dia-del-mes</guid><description>En el desarrollo web, a menudo es necesario manipular fechas y tiempos. Una tarea común es determinar el último día de un mes dado. Aquí te presento una función en PHP que realiza esta tarea de manera eficiente.</description><content:encoded>En el desarrollo web, a menudo es necesario manipular fechas y tiempos. Una tarea común es determinar el último día de un mes dado. Aquí te presento una función en PHP que realiza esta tarea de manera eficiente.

## Descripción de la Función

La función **getLastDayMonth** toma una fecha como entrada y devuelve el último día del mes de esa fecha. Esta función es útil, por ejemplo, para calcular fechas límites, programar eventos o validar rangos de fechas.

## Definición de la Función

Aquí está la función que vamos a revisar:

```php
/**
 *    Función para Regresar el Último Día del Mes
 * @date $date - Fecha de la cual se va a obtener el último día
 */
public static function getLastDayMonth($date)
{
    list($year, $mon, $day) = explode(&apos;-&apos;, $date);
    return date(&quot;Y-m-d&quot;, (mktime(0, 0, 0, $mon + 1, 1, $year) - 1));
}
```

- **Parámetro de Entrada**: La función acepta un único parámetro, $date, que es una cadena de texto en formato YYYY-MM-DD.
- **Descomposición de la Fecha**: Utiliza explode para dividir la fecha en año ($year), mes ($mon) y día ($day).

```php
list($year, $mon, $day) = explode(&apos;-&apos;, $date);
```

- **Calcular el Último Día**: La función mktime se usa para generar una marca de tiempo Unix para el primer día del mes siguiente ($mon + 1) a las 00:00 horas.

```php
mktime(0, 0, 0, $mon + 1, 1, $year)
```

- **Restar un Segundo**: Al restar un segundo (-1) de esta marca de tiempo, se obtiene el último día del mes anterior, que es el mes de la fecha original.
- **Formato de la Fecha**: Finalmente, la función date se usa para formatear esta marca de tiempo como una cadena de texto en formato YYYY-MM-DD.
  
```php
return date(&quot;Y-m-d&quot;, (mktime(0, 0, 0, $mon + 1, 1, $year) - 1));
```

### Ejemplo de Uso

Imaginemos que queremos obtener el último día del mes para diferentes fechas:

```php
echo getLastDayMonth(&apos;2024-06-14&apos;); // Salida: 2024-06-30
echo getLastDayMonth(&apos;2024-02-10&apos;); // Salida: 2024-02-29 (Año bisiesto)
echo getLastDayMonth(&apos;2023-12-01&apos;); // Salida: 2023-12-31

```

## Ventajas de la Función

- **Simplicidad**: La función es fácil de entender y de implementar.
- **Flexibilidad**: Funciona para cualquier fecha en formato YYYY-MM-DD.
- **Compatibilidad**: Utiliza funciones nativas de PHP, garantizando un amplio soporte en diferentes entornos.

## Conclusión

Con esta función getLastDayMonth, calcular el último día de cualquier mes es una tarea sencilla en PHP. Su implementación eficiente y clara facilita su integración en cualquier proyecto que maneje fechas y calendarios.</content:encoded></item><item><title>Días transcurridos entre dos Fechas</title><link>https://blivecode.com/posts/dias-transcurridos-entre-dos-fechas</link><guid isPermaLink="true">https://blivecode.com/posts/dias-transcurridos-entre-dos-fechas</guid><description>En el mundo de la programación, calcular la diferencia entre dos fechas es una tarea común que puede ser crucial en aplicaciones que necesitan rastrear eventos, calcular vencimientos, o analizar datos históricos</description><content:encoded>En el mundo de la programación, calcular la diferencia entre dos fechas es una tarea común que puede ser crucial en aplicaciones que necesitan rastrear eventos, calcular vencimientos, o analizar datos históricos. En esta entrada del blog, vamos a analizar una función en PHP que calcula la cantidad de días transcurridos entre dos fechas. Revisaremos su código, explicaremos su funcionamiento paso a paso, y discutiremos cómo podemos utilizarla en un entorno de producción.

## Código de la Función

Aquí está la función que vamos a revisar:

```php
public static function getDaysPassed($dateStart, $dateEnd)
{
    $dateInit = strtotime($dateStart);
    $dateEnd = strtotime($dateEnd);
    if ($dateEnd &gt; $dateInit) {
        $dias = ($dateEnd - $dateInit) / 86400;
        $dias = abs($dias);
        $dias = floor($dias);
        return intval($dias);
    } else {
        throw new \Exception(&quot;La fecha final no puede ser menor que la fecha inicial&quot;, 1);
    }
}
```

## Desglosando la Función

### Declaración de la Función

```php
public static function getDaysPassed($dateStart, $dateEnd)
```

- public: La función es accesible desde fuera de la clase.
- static: La función pertenece a la clase y no a una instancia específica.
- function getDaysPassed: Define el nombre de la función y sus parámetros.
- $dateStart, $dateEnd: Son las dos fechas de entrada que deben proporcionarse en formato de cadena (por ejemplo, &quot;2024-01-01&quot;).

### Conversión de Fechas a Timestamps

```php
$dateInit = strtotime($dateStart);
$dateEnd = strtotime($dateEnd);
```

- strtotime: Convierte una cadena de fecha en un timestamp Unix (el número de segundos desde la &quot;Época Unix&quot;, es decir, el 1 de enero de 1970).
- $dateInit y $dateEnd: Ahora contienen los valores de timestamp correspondientes a las fechas proporcionadas.

### Comparación de Fechas

```php
if ($dateEnd &gt; $dateInit)
```

Compara los timestamps para asegurarse de que la fecha final no sea anterior a la fecha de inicio. Si $dateEnd es mayor, significa que $dateEnd es una fecha posterior a $dateStart.

### Cálculo de la Diferencia en Días

```php
$dias = ($dateEnd - $dateInit) / 86400;
```

- **($dateEnd - $dateInit)**: Calcula la diferencia en segundos entre las dos fechas.
- **86400**: Es el número de segundos en un día **(60 segundos * 60 minutos * 24 horas).**
- **$dias**: Contiene la diferencia en días entre las dos fechas.

### Asegurando que el resultado no sea negativo

```php
$dias = abs($dias);
$dias = floor($dias);
return intval($dias);
```

- **abs($dias)**: Toma el valor absoluto de la diferencia para asegurarse de que el número de días no sea negativo.
- **floor($dias)**: Redondea hacia abajo para obtener un número entero de días.
- **intval($dias)**: Convierte el resultado final a un entero antes de devolverlo.

### Manejo de Excepciones

```php
else {
    throw new \Exception(&quot;La fecha final no puede ser menor que la fecha inicial&quot;, 1);
}
```

- Si la fecha final es menor que la fecha inicial, lanza una excepción con un mensaje descriptivo.

La función getDaysPassed es una solución simple y efectiva para calcular la cantidad de días transcurridos entre dos fechas en PHP. Siguiendo las mejores prácticas de validación y manejo de excepciones, puedes integrar esta función de manera segura en tus aplicaciones de producción.</content:encoded></item><item><title>Script Bash para backups automáticos de archivos y bases de datos</title><link>https://blivecode.com/posts/script-bash-para-backups-automaticos-de-archivos-y-bases-de-datos</link><guid isPermaLink="true">https://blivecode.com/posts/script-bash-para-backups-automaticos-de-archivos-y-bases-de-datos</guid><description>Cómo crear un script en Bash que comprime directorios, exporta bases de datos MySQL con mysqldump y aplica rotación semanal para no llenar el disco del servidor.</description><content:encoded>Si tenés un servidor en producción, tarde o temprano algo se va a romper. Un disco que se llena, una tabla que se borra por error, un `rm -rf` mal puesto, un update de sistema operativo que sale mal. En todos esos casos, la diferencia entre un &quot;uh, qué mal&quot; y un &quot;se perdió todo&quot; es si tenés un backup actualizado y, más importante, si sabés cómo restaurarlo.

En este post vamos a armar un script en Bash que se encargue de las dos cosas que más me pidieron a lo largo de los años: respaldar directorios de archivos (el código de la app, configs de Nginx, archivos subidos por usuarios) y exportar bases de datos MySQL. El script va a ser ejecutable a mano, pero pensado para que lo metas en un cron y te olvides de que existe.

## Qué tiene que hacer el script

Antes de escribir una línea, listemos los requisitos. Esto es lo que yo necesito en cualquier servidor de producción:

1. **Respaldar archivos críticos** comprimidos con `tar.gz`. En mi caso suele ser `/var/www/miapp` (el código), `/etc/nginx` (las configs del servidor web) y a veces `/etc/letsencrypt` (los certificados).
2. **Exportar todas las bases de datos MySQL** con `mysqldump`, en formato `.sql` comprimido con `gzip`.
3. **Nombrar los archivos con la fecha** para que sea fácil identificar cuándo se hizo cada backup.
4. **Borrar los backups viejos** según una cantidad de días configurable, así no se llena el disco.
5. **Registrar todo en un log** para poder revisar después qué pasó en cada ejecución.
6. **Fallar rápido** si algo sale mal. Un script de backup que termina con código 0 cuando en realidad no pudo respaldar algo es peor que no tener backup, porque te da una falsa sensación de seguridad.

Con esos seis puntos claros, vamos a escribir el script.

## El script completo

Acá está la versión final, comentada por secciones. Después de la versión completa explico cada bloque en detalle.

```bash
#!/bin/bash
#
# backup.sh - Script de backup automático para archivos y MySQL
# Uso: sudo ./backup.sh
# Crontab sugerido: 0 3 * * * /opt/scripts/backup.sh
#

set -euo pipefail

# ============================================
# CONFIGURACIÓN - EDITAR ESTOS VALORES
# ============================================
BACKUP_DIR=&quot;/var/backups/miapp&quot;
LOG_FILE=&quot;/var/log/backup.log&quot;
RETENTION_DAYS=7

# Directorios a respaldar (separados por espacio)
DIRS_TO_BACKUP=(
    &quot;/var/www/miapp&quot;
    &quot;/etc/nginx&quot;
    &quot;/etc/letsencrypt&quot;
)

# Credenciales de MySQL (idealmente leer de un archivo seguro)
MYSQL_USER=&quot;backup_user&quot;
MYSQL_PASS=&quot;tu_password_seguro&quot;
MYSQL_HOST=&quot;localhost&quot;

# ============================================
# FIN DE LA CONFIGURACIÓN
# ============================================

FECHA=$(date +&quot;%Y-%m-%d&quot;)
HORA=$(date +&quot;%H%M%S&quot;)
DEST_DIR=&quot;$BACKUP_DIR/$FECHA&quot;

# Función de logging
log() {
    echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] $1&quot; | tee -a &quot;$LOG_FILE&quot;
}

# Validaciones iniciales
if [[ $EUID -ne 0 ]]; then
    echo &quot;Este script debe ejecutarse como root (sudo)&quot;
    exit 1
fi

if ! command -v mysqldump &amp;&gt; /dev/null; then
    log &quot;ERROR: mysqldump no está instalado&quot;
    exit 1
fi

# Crear directorio del día
mkdir -p &quot;$DEST_DIR&quot;
log &quot;Iniciando backup en $DEST_DIR&quot;

# Backup de archivos
log &quot;Respaldando directorios de archivos...&quot;
for DIR in &quot;${DIRS_TO_BACKUP[@]}&quot;; do
    if [[ -d &quot;$DIR&quot; ]]; then
        NOMBRE=$(basename &quot;$DIR&quot;)
        ARCHIVO=&quot;$DEST_DIR/files-${NOMBRE}-${FECHA}.tar.gz&quot;
        tar -czf &quot;$ARCHIVO&quot; -C &quot;$(dirname &quot;$DIR&quot;)&quot; &quot;$NOMBRE&quot; 2&gt;&gt; &quot;$LOG_FILE&quot;
        log &quot;  ✓ $DIR -&gt; $ARCHIVO&quot;
    else
        log &quot;  ⚠ Directorio no existe, se omite: $DIR&quot;
    fi
done

# Backup de MySQL
log &quot;Exportando bases de datos MySQL...&quot;
DUMP_FILE=&quot;$DEST_DIR/mysql-${FECHA}-${HORA}.sql.gz&quot;
mysqldump \
    --user=&quot;$MYSQL_USER&quot; \
    --password=&quot;$MYSQL_PASS&quot; \
    --host=&quot;$MYSQL_HOST&quot; \
    --single-transaction \
    --quick \
    --routines \
    --triggers \
    --events \
    --all-databases \
    2&gt;&gt; &quot;$LOG_FILE&quot; | gzip &gt; &quot;$DUMP_FILE&quot;

if [[ -s &quot;$DUMP_FILE&quot; ]]; then
    log &quot;  ✓ MySQL dump -&gt; $DUMP_FILE&quot;
else
    log &quot;  ✗ El dump de MySQL está vacío o falló&quot;
    exit 1
fi

# Calcular tamaños
log &quot;Resumen de tamaños:&quot;
du -sh &quot;$DEST_DIR&quot;/* 2&gt;&gt; &quot;$LOG_FILE&quot; | tee -a &quot;$LOG_FILE&quot;

# Aplicar rotación: borrar backups más viejos que RETENTION_DAYS
log &quot;Aplicando rotación: borrando backups con más de $RETENTION_DAYS días...&quot;
find &quot;$BACKUP_DIR&quot; -maxdepth 1 -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \; 2&gt;&gt; &quot;$LOG_FILE&quot;
log &quot;Rotación completada&quot;

log &quot;Backup finalizado correctamente&quot;
exit 0
```

Copiá este script a `/opt/scripts/backup.sh` (o donde te quede cómodo), dale permisos de ejecución con `chmod 700` y ya lo podés correr. Ahora vamos bloque por bloque.

## Bloque 1: Configuración

Las primeras líneas después del shebang son las variables que vas a tocar cuando migres de servidor o cambies de proyecto. Por convención las pongo todas juntas al inicio para que sea fácil encontrarlas.

```bash
BACKUP_DIR=&quot;/var/backups/miapp&quot;
LOG_FILE=&quot;/var/log/backup.log&quot;
RETENTION_DAYS=7
```

`BACKUP_DIR` es donde van a quedar todos los backups organizados por fecha. `LOG_FILE` es el archivo donde se registra cada ejecución, ideal para debuggear cuando algo falla a las 3 AM. `RETENTION_DAYS` es cuántos días de historial querés conservar. Para un backup diario con retención semanal, 7 está bien. Si querés un mes de historial, 30.

El array `DIRS_TO_BACKUP` acepta varias rutas. Cada elemento se procesa por separado en el loop, así un directorio que no existe no rompe todo el backup.

```bash
DIRS_TO_BACKUP=(
    &quot;/var/www/miapp&quot;
    &quot;/etc/nginx&quot;
    &quot;/etc/letsencrypt&quot;
)
```

Para las credenciales de MySQL, hay una decisión importante que tomar. La opción más simple es hardcodear la contraseña en el script, pero eso es un riesgo de seguridad: si alguien ve el archivo, ve la contraseña de la base de datos. La opción más segura es leerla de un archivo separado con permisos 600:

```bash
# Archivo /root/.mybackup.cnf
[client]
user=backup_user
password=tu_password_seguro
host=localhost
```

Y en el script:

```bash
chmod 600 /root/.mybackup.cnf
mysqldump --defaults-file=/root/.mybackup.cnf ...
```

Si en tu proyecto tenés un usuario de MySQL dedicado solo para backups (lo cual es una buena práctica), esto te queda limpio. Si no querés complicarte, dejá la contraseña en el script pero asegurate de que el archivo tenga permisos 600 y sea solo accesible por root.

## Bloque 2: set -euo pipefail y validaciones

Esta línea es la más importante del script y mucha gente la saltea:

```bash
set -euo pipefail
```

Cada flag hace algo:
- `-e`: si cualquier comando falla, el script termina inmediatamente con código de error.
- `-u`: si usás una variable que no está definida, el script falla (en lugar de tratarla como string vacío).
- `-o pipefail`: si en un pipe como `comando1 | comando2` falla `comando1`, el pipe entero se considera fallido. Sin esta flag, solo importa el exit code del último comando.

Juntas, estas tres flags hacen que el script falle rápido en lugar de seguir ejecutándose cuando algo salió mal. Es exactamente lo que querés en un script de backup.

Después vienen las validaciones previas a empezar a respaldar:

```bash
if [[ $EUID -ne 0 ]]; then
    echo &quot;Este script debe ejecutarse como root (sudo)&quot;
    exit 1
fi

if ! command -v mysqldump &amp;&gt; /dev/null; then
    log &quot;ERROR: mysqldump no está instalado&quot;
    exit 1
fi
```

La primera chequea que estés corriendo como root (necesario para leer `/etc/letsencrypt` y `/etc/nginx`). La segunda verifica que `mysqldump` esté instalado antes de seguir, así no llegás a la mitad del backup y te explotás.

## Bloque 3: Backup de archivos

Acá es donde se comprimen los directorios. El comando `tar` con `-czf` crea un archivo `.tar.gz` y `-C` cambia al directorio padre para que el path dentro del archivo sea relativo.

```bash
for DIR in &quot;${DIRS_TO_BACKUP[@]}&quot;; do
    if [[ -d &quot;$DIR&quot; ]]; then
        NOMBRE=$(basename &quot;$DIR&quot;)
        ARCHIVO=&quot;$DEST_DIR/files-${NOMBRE}-${FECHA}.tar.gz&quot;
        tar -czf &quot;$ARCHIVO&quot; -C &quot;$(dirname &quot;$DIR&quot;)&quot; &quot;$NOMBRE&quot; 2&gt;&gt; &quot;$LOG_FILE&quot;
        log &quot;  ✓ $DIR -&gt; $ARCHIVO&quot;
    else
        log &quot;  ⚠ Directorio no existe, se omite: $DIR&quot;
    fi
done
```

La parte clave es el `-C &quot;$(dirname &quot;$DIR&quot;)&quot;`. Si haces `tar -czf backup.tar.gz /var/www/miapp`, cuando descomprimís te queda un directorio `/var/www/miapp` adentro, lo cual es un quilombo para restaurar. Con `-C /var/www miapp` te queda directamente `miapp/` en la raíz del archivo, que es mucho más limpio.

Notá también que cada directorio se guarda en un archivo separado (`files-www-2026-07-03.tar.gz`, `files-nginx-2026-07-03.tar.gz`, etc). Esto es útil cuando necesitás restaurar un solo directorio sin tener que tocar los demás.

## Bloque 4: Dump de MySQL

El comando `mysqldump` tiene un montón de flags, pero para un backup de producción estándar los que importan son estos:

```bash
mysqldump \
    --user=&quot;$MYSQL_USER&quot; \
    --password=&quot;$MYSQL_PASS&quot; \
    --host=&quot;$MYSQL_HOST&quot; \
    --single-transaction \
    --quick \
    --routines \
    --triggers \
    --events \
    --all-databases
```

- `--single-transaction`: hace el dump dentro de una transacción, así las tablas InnoDB no se lockean durante el proceso. Imprescindible si tu base está en producción.
- `--quick`: hace que `mysqldump` vuelque las filas de a una en lugar de cargar todo en memoria. Es la diferencia entre que el script funcione con tablas grandes o que te tire un &quot;out of memory&quot;.
- `--routines`, `--triggers`, `--events`: incluye stored procedures, triggers y eventos programados. Si no los ponés, el día que necesites restaurar te van a faltar cosas sin saber por qué.
- `--all-databases`: exporta todas las bases. Si querés ser más selectivo, podés usar `--databases db1 db2 db3` o incluso `--ignore-table=db.tabla_grande_que_no_necesitas`.

Después, el output se comprime con gzip en el mismo pipe:

```bash
| gzip &gt; &quot;$DUMP_FILE&quot;
```

El chequeo posterior es importante:

```bash
if [[ -s &quot;$DUMP_FILE&quot; ]]; then
    log &quot;  ✓ MySQL dump -&gt; $DUMP_FILE&quot;
else
    log &quot;  ✗ El dump de MySQL está vacío o falló&quot;
    exit 1
fi
```

`-s` chequea que el archivo exista y no esté vacío. Si el dump falló silenciosamente, este chequeo lo detecta. Hay un caso clásico donde esto salva: las credenciales están mal y `mysqldump` no puede conectarse, pero como el comando igual crea el archivo de salida (vacío), sin este chequeo creerías que tenés un backup válido.

## Bloque 5: Rotación

Esta es la parte que evita que el disco se llene:

```bash
find &quot;$BACKUP_DIR&quot; -maxdepth 1 -type d -mtime +$RETENTION_DAYS -exec rm -rf {} \;
```

El comando `find` busca directorios dentro de `$BACKUP_DIR` (sin recursionar, por eso `-maxdepth 1`) que tengan más de `RETENTION_DAYS` días de antigüedad y los borra con `rm -rf`. Los directorios con nombre `2026-07-03`, `2026-07-04`, etc. entran en esta categoría cuando son más viejos que el cutoff.

El `-mtime +7` significa &quot;más de 7 días&quot; (estrictamente, son 8 o más). Si querés ser más preciso y conservar exactamente 7, usá `-mtime +6` que cuenta días completos desde la última modificación.

## Programación con cron

Una vez que el script está probado y funcionando, lo metés en un cron para que corra solo:

```bash
sudo crontab -e
```

Y agregás una línea como esta:

```
0 3 * * * /opt/scripts/backup.sh &gt;&gt; /var/log/backup-cron.log 2&gt;&amp;1
```

Esto corre todos los días a las 3:00 AM. La hora es importante: elegí un momento de poca actividad en tu sitio. Las 3 AM es la convención porque es cuando casi nadie usa internet. La parte `&gt;&gt; /var/log/backup-cron.log 2&gt;&amp;1` redirige la salida estándar y los errores a un archivo separado, así no perdés información si algo sale mal.

Para verificar que el cron está bien configurado, podés listar las tareas con `crontab -l`. Y para testear, cambiá la hora a un par de minutos en el futuro y esperá.

## Cómo restaurar (lo que casi nadie testea)

Hacer un backup es la mitad del trabajo. La otra mitad es poder restaurarlo. Te dejo los comandos básicos para que los pruebes antes de necesitarlos en una emergencia.

Para restaurar un directorio desde un `.tar.gz`:

```bash
# Restaurar en el directorio actual
tar -xzf files-www-2026-07-03.tar.gz

# Restaurar en una ruta específica
tar -xzf files-www-2026-07-03.tar.gz -C /
```

Para restaurar la base de datos MySQL:

```bash
# Descomprimir el dump
gunzip mysql-2026-07-03-030000.sql.gz

# Importar todas las bases
mysql -u root -p &lt; mysql-2026-07-03-030000.sql
```

Mi recomendación: hacé un restore de prueba en otro servidor (o en un contenedor Docker) cada tres meses, así te asegurás de que el backup se pueda restaurar y de que sabés los comandos de memoria cuando los necesitás. Un backup que no se restaura no es un backup.

## Mejoras opcionales que valen la pena

Una vez que el script base funcione, hay varias mejoras que podés sumarle según tu proyecto:

**Subir a un servidor remoto con rsync:**

```bash
rsync -avz --delete &quot;$BACKUP_DIR/&quot; backup@backup-server:/backups/miapp/
```

**Notificación por email cuando termina:**

```bash
echo &quot;Backup completado en $(date)&quot; | mail -s &quot;Backup OK&quot; tu@email.com
```

**Encriptar el backup con GPG antes de subirlo:**

```bash
gpg --symmetric --cipher-algo AES256 &quot;$DUMP_FILE&quot;
```

**Verificar la integridad con un hash:**

```bash
md5sum &quot;$DUMP_FILE&quot; &gt; &quot;$DUMP_FILE.md5&quot;

# Al restaurar, verificar
md5sum -c mysql-2026-07-03-030000.sql.gz.md5
```

**Lock file para evitar ejecuciones simultáneas:**

```bash
LOCKFILE=&quot;/var/run/backup.lock&quot;
if [ -e &quot;$LOCKFILE&quot; ]; then
    log &quot;ERROR: ya hay un backup en ejecución&quot;
    exit 1
fi
touch &quot;$LOCKFILE&quot;
trap &quot;rm -f $LOCKFILE&quot; EXIT
```

Esta última es clave si tu backup puede llegar a demorar más de un día por algún motivo, o si el cron se superpone con una corrida anterior.

## Errores comunes que vale la pena evitar

- **No probar el restore.** El error más caro de todos. Un backup no es válido hasta que lo restaurás al menos una vez.
- **Dejar la contraseña de MySQL hardcodeada y el script con permisos 644.** Cualquier usuario del sistema puede leer tu base de datos. Usá `chmod 600` siempre.
- **No poner `set -e` y tener un script que &quot;termina bien&quot; cuando falló a mitad de camino.** El exit code 0 te miente, el cron te avisa con un mail, pero si no configurás el mail te quedás sin saber.
- **Olvidar la regla 3-2-1 de backups.** Tres copias, en dos medios distintos, una fuera del servidor. Si el servidor se incendia y el backup estaba en el mismo disco, perdiste todo.
- **Hacer el backup en el mismo disco que la base de datos.** Si el disco muere, te llevás puesta la base y el backup juntos.

## Conclusión

Con este script ya tenés una base sólida para respaldar tu servidor. Es la versión que uso en mis proyectos personales y la he ido refinando con el tiempo. La parte más importante no es el script en sí, sino el hábito de probarlo periódicamente y de mantener la rotación para no quedarte sin espacio.

En el próximo post vamos a ver otra cosa clave: monitoreo de recursos del servidor con Bash, así podés anticiparte a problemas antes de que se conviertan en caídas. Si tenés alguna mejora al script o un caso de uso particular, me encantaría leerlo en los comentarios.</content:encoded></item><item><title>Monitoreo de recursos del servidor con Bash: CPU, RAM y disco</title><link>https://blivecode.com/posts/monitoreo-de-recursos-del-servidor-con-bash</link><guid isPermaLink="true">https://blivecode.com/posts/monitoreo-de-recursos-del-servidor-con-bash</guid><description>Script en Bash que genera reportes de uso de CPU, memoria RAM, espacio en disco y procesos activos con salida formateada, alertas por umbrales y programación con cron.</description><content:encoded>Un servidor en producción sin monitoreo es como manejar un auto sin tablero: puedes ir a 200 km/h sin saberlo, quedarte sin combustible en medio de la ruta o reventar el motor sin que te avise nada. Las herramientas de monitoreo como Grafana, Prometheus o Datadog son excelentes, pero para un VPS chico o un proyecto personal, armar tu propio script de monitoreo en Bash es más que suficiente y te da un control total sobre qué métricas ver y cómo verlas.

En este post vamos a construir un script que te muestra el estado completo de tu servidor en una sola ejecución: CPU, RAM, disco, los procesos que más consumen y alertas automáticas cuando algo está por explotar. Todo en texto plano, sin dependencias raras, listo para correr a mano o programar con cron.

## Qué tiene que reportar el script

Antes de escribir código, definamos qué información necesitamos. Esto es lo que yo reviso cada vez que entro a un servidor:

1. **Información general del sistema**: hostname, kernel, uptime y cantidad de usuarios conectados.
2. **Uso de CPU**: load average, porcentaje de uso por núcleo y los 5 procesos que más CPU consumen.
3. **Uso de memoria RAM**: total, usada, libre, disponible y los 5 procesos que más RAM consumen.
4. **Uso de disco**: espacio total, usado y disponible por partición, más los directorios que más ocupan.
5. **Alertas automáticas**: si algún recurso supera un umbral configurable, el script lo marca claramente.
6. **Todo en un formato legible**, con barras de progreso en ASCII y colores cuando la terminal lo soporta.

Con eso definido, vamos al script.

## El script completo

Acá está la versión final. Después de la versión completa explico cada bloque en detalle.

```bash
#!/bin/bash
#
# monitor.sh - Monitoreo de recursos del servidor
# Uso: ./monitor.sh
# Crontab sugerido: */5 * * * * /opt/scripts/monitor.sh --alert-only &gt;&gt; /var/log/monitor.log
#

set -euo pipefail

# ============================================
# CONFIGURACIÓN - UMBRALES DE ALERTA
# ============================================
CPU_ALERT_THRESHOLD=80
RAM_ALERT_THRESHOLD=80
DISK_ALERT_THRESHOLD=85
TOP_N=5

# ============================================
# COLORES (se desactivan si no hay terminal)
# ============================================
if [[ -t 1 ]]; then
    RED=&apos;\033[0;31m&apos;
    YELLOW=&apos;\033[1;33m&apos;
    GREEN=&apos;\033[0;32m&apos;
    CYAN=&apos;\033[0;36m&apos;
    BOLD=&apos;\033[1m&apos;
    RESET=&apos;\033[0m&apos;
else
    RED=&apos;&apos;
    YELLOW=&apos;&apos;
    GREEN=&apos;&apos;
    CYAN=&apos;&apos;
    BOLD=&apos;&apos;
    RESET=&apos;&apos;
fi

# ============================================
# FUNCIONES AUXILIARES
# ============================================
separator() {
    echo -e &quot;${CYAN}$(printf &apos;=%.0f&apos; {1..70})${RESET}&quot;
}

header() {
    echo &quot;&quot;
    separator
    echo -e &quot;${BOLD}  $1${RESET}&quot;
    separator
}

alert_check() {
    local value=$1
    local threshold=$2
    local label=$3

    if (( value &gt;= threshold )); then
        echo -e &quot;  ${RED}⚠ ALERTA: $label al ${value}% (umbral: ${threshold}%)${RESET}&quot;
    else
        echo -e &quot;  ${GREEN}✓ $label: ${value}% (OK)${RESET}&quot;
    fi
}

progress_bar() {
    local percent=$1
    local width=40
    local filled=$(( percent * width / 100 ))
    local empty=$(( width - filled ))
    local color=&quot;$GREEN&quot;

    if (( percent &gt;= 80 )); then
        color=&quot;$RED&quot;
    elif (( percent &gt;= 60 )); then
        color=&quot;$YELLOW&quot;
    fi

    printf &quot;  ${color}[&quot;
    printf &apos;#%.0f&apos; $(seq 1 $filled 2&gt;/dev/null) || true
    printf &apos;-%.0f&apos; $(seq 1 $empty 2&gt;/dev/null) || true
    printf &quot;] %3d%%${RESET}\n&quot; &quot;$percent&quot;
}

# ============================================
# MODO SOLO ALERTAS (para cron)
# ============================================
if [[ &quot;${1:-}&quot; == &quot;--alert-only&quot; ]]; then
    ALERTS=0

    CPU_USAGE=$(top -bn1 | grep &quot;Cpu(s)&quot; | awk &apos;{printf &quot;%d&quot;, $2 + $4}&apos;)
    RAM_PERCENT=$(free | awk &apos;/^Mem:/ {printf &quot;%d&quot;, $3/$2 * 100}&apos;)
    
    while IFS= read -r line; do
        DISK_PERCENT=$(echo &quot;$line&quot; | awk &apos;{gsub(/%/,&quot;&quot;); print $5}&apos;)
        DISK_MOUNT=$(echo &quot;$line&quot; | awk &apos;{print $6}&apos;)
        if (( DISK_PERCENT &gt;= DISK_ALERT_THRESHOLD )); then
            echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] ALERTA DISCO: $DISK_MOUNT al ${DISK_PERCENT}%&quot;
            ALERTS=$((ALERTS + 1))
        fi
    done &lt; &lt;(df -h --output=pcent,target -x tmpfs -x devtmpfs -x squashfs 2&gt;/dev/null | tail -n +2)

    if (( CPU_USAGE &gt;= CPU_ALERT_THRESHOLD )); then
        echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] ALERTA CPU: ${CPU_USAGE}%&quot;
        ALERTS=$((ALERTS + 1))
    fi

    if (( RAM_PERCENT &gt;= RAM_ALERT_THRESHOLD )); then
        echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] ALERTA RAM: ${RAM_PERCENT}%&quot;
        ALERTS=$((ALERTS + 1))
    fi

    if (( ALERTS == 0 )); then
        exit 0
    fi
    exit 0
fi

# ============================================
# REPORTE COMPLETO
# ============================================
echo &quot;&quot;
echo -e &quot;${BOLD}╔══════════════════════════════════════════════════════════════════╗${RESET}&quot;
echo -e &quot;${BOLD}║          REPORTE DE RECURSOS DEL SERVIDOR                       ║${RESET}&quot;
echo -e &quot;${BOLD}║          $(date &apos;+%Y-%m-%d %H:%M:%S&apos;)                              ║${RESET}&quot;
echo -e &quot;${BOLD}╚══════════════════════════════════════════════════════════════════╝${RESET}&quot;

# ------------------------------------------
# INFORMACIÓN DEL SISTEMA
# ------------------------------------------
header &quot;INFORMACIÓN DEL SISTEMA&quot;
echo -e &quot;  ${BOLD}Hostname:${RESET}   $(hostname)&quot;
echo -e &quot;  ${BOLD}Kernel:${RESET}     $(uname -r)&quot;
echo -e &quot;  ${BOLD}SO:${RESET}         $(cat /etc/os-release 2&gt;/dev/null | grep PRETTY_NAME | cut -d&apos;&quot;&apos; -f2 || echo &apos;Desconocido&apos;)&quot;
echo -e &quot;  ${BOLD}Uptime:${RESET}     $(uptime -p 2&gt;/dev/null || uptime | sed &apos;s/.*up/up/&apos;)&quot;
echo -e &quot;  ${BOLD}Usuarios:${RESET}   $(who | wc -l) conectados&quot;

# ------------------------------------------
# USO DE CPU
# ------------------------------------------
header &quot;USO DE CPU&quot;

LOAD_AVG=$(cat /proc/loadavg)
LOAD_1=$(echo &quot;$LOAD_AVG&quot; | awk &apos;{print $1}&apos;)
LOAD_5=$(echo &quot;$LOAD_AVG&quot; | awk &apos;{print $2}&apos;)
LOAD_15=$(echo &quot;$LOAD_AVG&quot; | awk &apos;{print $3}&apos;)
CPU_CORES=$(nproc)

echo -e &quot;  ${BOLD}Núcleos:${RESET}    $CPU_CORES&quot;
echo -e &quot;  ${BOLD}Load Average:${RESET} $LOAD_1 (1min) | $LOAD_5 (5min) | $LOAD_15 (15min)&quot;

CPU_USAGE=$(top -bn1 | grep &quot;Cpu(s)&quot; | awk &apos;{printf &quot;%d&quot;, $2 + $4}&apos;)
echo -e &quot;  ${BOLD}Uso total:${RESET}  ${CPU_USAGE}%&quot;
progress_bar &quot;$CPU_USAGE&quot;

alert_check &quot;$CPU_USAGE&quot; &quot;$CPU_ALERT_THRESHOLD&quot; &quot;CPU&quot;

echo &quot;&quot;
echo -e &quot;  ${BOLD}Top $TOP_N procesos por CPU:${RESET}&quot;
echo -e &quot;  ${CYAN}$(printf &apos;%-8s %-6s %-6s %s&apos; &apos;PID&apos; &apos;%CPU&apos; &apos;%MEM&apos; &apos;COMANDO&apos;)${RESET}&quot;
ps aux --sort=-%cpu | head -n $((TOP_N + 1)) | tail -n $TOP_N | while read -r line; do
    PID=$(echo &quot;$line&quot; | awk &apos;{print $2}&apos;)
    CPU=$(echo &quot;$line&quot; | awk &apos;{printf &quot;%.1f&quot;, $3}&apos;)
    MEM=$(echo &quot;$line&quot; | awk &apos;{printf &quot;%.1f&quot;, $4}&apos;)
    CMD=$(echo &quot;$line&quot; | awk &apos;{for(i=11;i&lt;=NF;i++) printf &quot;%s &quot;, $i; print &quot;&quot;}&apos; | xargs)
    printf &quot;  %-8s %-6s %-6s %s\n&quot; &quot;$PID&quot; &quot;$CPU&quot; &quot;$MEM&quot; &quot;$CMD&quot;
done

# ------------------------------------------
# USO DE MEMORIA RAM
# ------------------------------------------
header &quot;USO DE MEMORIA RAM&quot;

RAM_INFO=$(free -h)
RAM_TOTAL=$(echo &quot;$RAM_INFO&quot; | awk &apos;/^Mem:/ {print $2}&apos;)
RAM_USED=$(echo &quot;$RAM_INFO&quot; | awk &apos;/^Mem:/ {print $3}&apos;)
RAM_FREE=$(echo &quot;$RAM_INFO&quot; | awk &apos;/^Mem:/ {print $4}&apos;)
RAM_AVAILABLE=$(echo &quot;$RAM_INFO&quot; | awk &apos;/^Mem:/ {print $7}&apos;)
RAM_PERCENT=$(free | awk &apos;/^Mem:/ {printf &quot;%d&quot;, $3/$2 * 100}&apos;)

SWAP_TOTAL=$(echo &quot;$RAM_INFO&quot; | awk &apos;/^Swap:/ {print $2}&apos;)
SWAP_USED=$(echo &quot;$RAM_INFO&quot; | awk &apos;/^Swap:/ {print $3}&apos;)

echo -e &quot;  ${BOLD}Total:${RESET}       $RAM_TOTAL&quot;
echo -e &quot;  ${BOLD}Usada:${RESET}       $RAM_USED&quot;
echo -e &quot;  ${BOLD}Libre:${RESET}       $RAM_FREE&quot;
echo -e &quot;  ${BOLD}Disponible:${RESET}  $RAM_AVAILABLE&quot;
echo -e &quot;  ${BOLD}Uso:${RESET}         ${RAM_PERCENT}%&quot;
progress_bar &quot;$RAM_PERCENT&quot;

alert_check &quot;$RAM_PERCENT&quot; &quot;$RAM_ALERT_THRESHOLD&quot; &quot;RAM&quot;

echo &quot;&quot;
echo -e &quot;  ${BOLD}Swap:${RESET} $SWAP_USED / $SWAP_TOTAL&quot;

echo &quot;&quot;
echo -e &quot;  ${BOLD}Top $TOP_N procesos por RAM:${RESET}&quot;
echo -e &quot;  ${CYAN}$(printf &apos;%-8s %-6s %-6s %s&apos; &apos;PID&apos; &apos;%MEM&apos; &apos;%CPU&apos; &apos;COMANDO&apos;)${RESET}&quot;
ps aux --sort=-%mem | head -n $((TOP_N + 1)) | tail -n $TOP_N | while read -r line; do
    PID=$(echo &quot;$line&quot; | awk &apos;{print $2}&apos;)
    CPU=$(echo &quot;$line&quot; | awk &apos;{printf &quot;%.1f&quot;, $3}&apos;)
    MEM=$(echo &quot;$line&quot; | awk &apos;{printf &quot;%.1f&quot;, $4}&apos;)
    CMD=$(echo &quot;$line&quot; | awk &apos;{for(i=11;i&lt;=NF;i++) printf &quot;%s &quot;, $i; print &quot;&quot;}&apos; | xargs)
    printf &quot;  %-8s %-6s %-6s %s\n&quot; &quot;$PID&quot; &quot;$MEM&quot; &quot;$CPU&quot; &quot;$CMD&quot;
done

# ------------------------------------------
# USO DE DISCO
# ------------------------------------------
header &quot;USO DE DISCO&quot;

echo -e &quot;  ${BOLD}Particiones:${RESET}&quot;
echo -e &quot;  ${CYAN}$(printf &apos;%-20s %-8s %-8s %-8s %-6s %s&apos; &apos;FILESYSTEM&apos; &apos;TOTAL&apos; &apos;USADO&apos; &apos;LIBRE&apos; &apos;%USO&apos; &apos;MOUNT&apos;)${RESET}&quot;

df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs -x squashfs 2&gt;/dev/null | tail -n +2 | while read -r line; do
    FS=$(echo &quot;$line&quot; | awk &apos;{print $1}&apos;)
    SIZE=$(echo &quot;$line&quot; | awk &apos;{print $2}&apos;)
    USED=$(echo &quot;$line&quot; | awk &apos;{print $3}&apos;)
    AVAIL=$(echo &quot;$line&quot; | awk &apos;{print $4}&apos;)
    PCT=$(echo &quot;$line&quot; | awk &apos;{gsub(/%/,&quot;&quot;); print $5}&apos;)
    MOUNT=$(echo &quot;$line&quot; | awk &apos;{print $6}&apos;)

    local_color=&quot;$GREEN&quot;
    if (( PCT &gt;= DISK_ALERT_THRESHOLD )); then
        local_color=&quot;$RED&quot;
    elif (( PCT &gt;= 60 )); then
        local_color=&quot;$YELLOW&quot;
    fi

    printf &quot;  %-20s %-8s %-8s %-8s ${local_color}%-5s%%${RESET} %s\n&quot; &quot;$FS&quot; &quot;$SIZE&quot; &quot;$USED&quot; &quot;$AVAIL&quot; &quot;$PCT&quot; &quot;$MOUNT&quot;
done

echo &quot;&quot;
echo -e &quot;  ${BOLD}Top 10 directorios más pesados en /:${RESET}&quot;
du -h --max-depth=2 / 2&gt;/dev/null | sort -rh | head -10 | while read -r size dir; do
    printf &quot;  %-10s %s\n&quot; &quot;$size&quot; &quot;$dir&quot;
done

echo &quot;&quot;
while IFS= read -r line; do
    DISK_PERCENT=$(echo &quot;$line&quot; | awk &apos;{gsub(/%/,&quot;&quot;); print $5}&apos;)
    DISK_MOUNT=$(echo &quot;$line&quot; | awk &apos;{print $6}&apos;)
    alert_check &quot;$DISK_PERCENT&quot; &quot;$DISK_ALERT_THRESHOLD&quot; &quot;Disco ($DISK_MOUNT)&quot;
done &lt; &lt;(df -h --output=pcent,target -x tmpfs -x devtmpfs -x squashfs 2&gt;/dev/null | tail -n +2)

# ------------------------------------------
# RESUMEN FINAL
# ------------------------------------------
header &quot;RESUMEN&quot;
alert_check &quot;$CPU_USAGE&quot; &quot;$CPU_ALERT_THRESHOLD&quot; &quot;CPU&quot;
alert_check &quot;$RAM_PERCENT&quot; &quot;$RAM_ALERT_THRESHOLD&quot; &quot;RAM&quot;

while IFS= read -r line; do
    DISK_PERCENT=$(echo &quot;$line&quot; | awk &apos;{gsub(/%/,&quot;&quot;); print $5}&apos;)
    DISK_MOUNT=$(echo &quot;$line&quot; | awk &apos;{print $6}&apos;)
    alert_check &quot;$DISK_PERCENT&quot; &quot;$DISK_ALERT_THRESHOLD&quot; &quot;Disco ($DISK_MOUNT)&quot;
done &lt; &lt;(df -h --output=pcent,target -x tmpfs -x devtmpfs -x squashfs 2&gt;/dev/null | tail -n +2)

echo &quot;&quot;
echo -e &quot;  ${BOLD}Generado:${RESET} $(date &apos;+%Y-%m-%d %H:%M:%S&apos;)&quot;
separator
echo &quot;&quot;
```

Copiá este script a `/opt/scripts/monitor.sh`, dale permisos con `chmod 700` y ya lo podés correr con `./monitor.sh`. Ahora vamos bloque por bloque.

## Bloque 1: Configuración de umbrales

Las primeras variables son los umbrales de alerta. Cuando un recurso supera el porcentaje configurado, el script lo marca en rojo y muestra una advertencia.

```bash
CPU_ALERT_THRESHOLD=80
RAM_ALERT_THRESHOLD=80
DISK_ALERT_THRESHOLD=85
TOP_N=5
```

El umbral de disco es más alto (85%) porque los discos suelen tener más margen que la RAM o la CPU. El `TOP_N` define cuántos procesos mostrar en el ranking de consumidores. Estos son los valores que uso habitualmente, pero ajustalos según tu servidor: si tenés una app que normalmente consume 70% de RAM, bajá el umbral a 70 para que te avise antes de que sea un problema.

## Bloque 2: Colores adaptativos

Los colores hacen que el reporte sea mucho más fácil de leer de un vistazo, pero si redirigís la salida a un archivo o a un pipe, los códigos de escape ANSI se convierten en basura ilegible. Por eso detectamos si hay una terminal interactiva:

```bash
if [[ -t 1 ]]; then
    RED=&apos;\033[0;31m&apos;
    YELLOW=&apos;\033[1;33m&apos;
    GREEN=&apos;\033[0;32m&apos;
    # ...
else
    RED=&apos;&apos;
    YELLOW=&apos;&apos;
    GREEN=&apos;&apos;
    # ...
fi
```

`-t 1` verifica que el file descriptor 1 (stdout) esté conectado a una terminal. Si estás corriendo el script desde cron o redirigiendo la salida, los colores se desactivan automáticamente. Es un detalle chico pero marca la diferencia cuando el output llega a un log.

## Bloque 3: Funciones auxiliares

Las funciones `separator`, `header`, `alert_check` y `progress_bar` son las que le dan formato al reporte. La más interesante es `progress_bar`:

```bash
progress_bar() {
    local percent=$1
    local width=40
    local filled=$(( percent * width / 100 ))
    local empty=$(( width - filled ))
    local color=&quot;$GREEN&quot;

    if (( percent &gt;= 80 )); then
        color=&quot;$RED&quot;
    elif (( percent &gt;= 60 )); then
        color=&quot;$YELLOW&quot;
    fi

    printf &quot;  ${color}[&quot;
    printf &apos;#%.0f&apos; $(seq 1 $filled 2&gt;/dev/null) || true
    printf &apos;-%.0f&apos; $(seq 1 $empty 2&gt;/dev/null) || true
    printf &quot;] %3d%%${RESET}\n&quot; &quot;$percent&quot;
}
```

Genera una barra como `[####################--------------------] 50%` que cambia de color según el porcentaje: verde hasta 59%, amarillo de 60% a 79%, rojo de 80% en adelante. El `|| true` en los `seq` evita que falle cuando `filled` o `empty` es 0.

La función `alert_check` es la que usa el resumen final y el bloque de disco para mostrar si un recurso está en estado crítico:

```bash
alert_check() {
    local value=$1
    local threshold=$2
    local label=$3

    if (( value &gt;= threshold )); then
        echo -e &quot;  ${RED}⚠ ALERTA: $label al ${value}% (umbral: ${threshold}%)${RESET}&quot;
    else
        echo -e &quot;  ${GREEN}✓ $label: ${value}% (OK)${RESET}&quot;
    fi
}
```

## Bloque 4: Modo solo alertas para cron

Esta es la parte que permite usar el script en un cron sin llenar un log con reportes completos cada 5 minutos:

```bash
if [[ &quot;${1:-}&quot; == &quot;--alert-only&quot; ]]; then
    ALERTS=0

    CPU_USAGE=$(top -bn1 | grep &quot;Cpu(s)&quot; | awk &apos;{printf &quot;%d&quot;, $2 + $4}&apos;)
    RAM_PERCENT=$(free | awk &apos;/^Mem:/ {printf &quot;%d&quot;, $3/$2 * 100}&apos;)

    while IFS= read -r line; do
        DISK_PERCENT=$(echo &quot;$line&quot; | awk &apos;{gsub(/%/,&quot;&quot;); print $5}&apos;)
        DISK_MOUNT=$(echo &quot;$line&quot; | awk &apos;{print $6}&apos;)
        if (( DISK_PERCENT &gt;= DISK_ALERT_THRESHOLD )); then
            echo &quot;[$(date &apos;+%Y-%m-%d %H:%M:%S&apos;)] ALERTA DISCO: $DISK_MOUNT al ${DISK_PERCENT}%&quot;
            ALERTS=$((ALERTS + 1))
        fi
    done &lt; &lt;(df -h --output=pcent,target -x tmpfs -x devtmpfs -x squashfs 2&gt;/dev/null | tail -n +2)
    # ...
fi
```

Cuando corrés el script con `--alert-only`, solo imprime líneas si hay algo que supera los umbrales. Si todo está bien, no genera output. Esto es ideal para cron, porque cron te manda un mail cuando un script produce output: si no hay alertas, no hay mail.

## Bloque 5: Información del sistema

El bloque más simple, pero útil para cuando tenés muchos servidores y no recordás cuál es cuál:

```bash
echo -e &quot;  ${BOLD}Hostname:${RESET}   $(hostname)&quot;
echo -e &quot;  ${BOLD}Kernel:${RESET}     $(uname -r)&quot;
echo -e &quot;  ${BOLD}SO:${RESET}         $(cat /etc/os-release 2&gt;/dev/null | grep PRETTY_NAME | cut -d&apos;&quot;&apos; -f2)&quot;
echo -e &quot;  ${BOLD}Uptime:${RESET}     $(uptime -p 2&gt;/dev/null || uptime | sed &apos;s/.*up/up/&apos;)&quot;
echo -e &quot;  ${BOLD}Usuarios:${RESET}   $(who | wc -l) conectados&quot;
```

`uptime -p` da el uptime en formato legible (&quot;up 3 weeks, 2 days, 4 hours&quot;), pero no está disponible en todos los Linux. El fallback con `sed` extrae la parte relevante del output clásico de `uptime`.

## Bloque 6: Uso de CPU

Acá medimos dos cosas: el load average y el porcentaje de uso real.

```bash
LOAD_AVG=$(cat /proc/loadavg)
CPU_USAGE=$(top -bn1 | grep &quot;Cpu(s)&quot; | awk &apos;{printf &quot;%d&quot;, $2 + $4}&apos;)
```

El load average (que leemos de `/proc/loadavg`) te dice cuántos procesos están esperando para usar la CPU. Un load average de 2.0 en un servidor de 4 núcleos está bien; en uno de 1 núcleo, significa que hay cola. Por eso siempre muestro la cantidad de núcleos junto al load:

```bash
CPU_CORES=$(nproc)
echo -e &quot;  ${BOLD}Núcleos:${RESET}    $CPU_CORES&quot;
echo -e &quot;  ${BOLD}Load Average:${RESET} $LOAD_1 (1min) | $LOAD_5 (5min) | $LOAD_15 (15min)&quot;
```

El uso de CPU se calcula con `top` en modo batch (`-bn1`), que corre una sola iteración y sale. Sumamos el `%us` (user) y `%sy` (system) para obtener el uso total.

Después listamos los procesos que más CPU consumen:

```bash
ps aux --sort=-%cpu | head -n $((TOP_N + 1)) | tail -n $TOP_N
```

El `head -n $((TOP_N + 1))` incluye la cabecera de `ps`, y el `tail -n $TOP_N` la descarta. Así nos quedan solo los 5 procesos (o los que definas en `TOP_N`).

## Bloque 7: Uso de memoria RAM

Similar al bloque de CPU, pero usando `free`:

```bash
RAM_INFO=$(free -h)
RAM_TOTAL=$(echo &quot;$RAM_INFO&quot; | awk &apos;/^Mem:/ {print $2}&apos;)
RAM_USED=$(echo &quot;$RAM_INFO&quot; | awk &apos;/^Mem:/ {print $3}&apos;)
RAM_PERCENT=$(free | awk &apos;/^Mem:/ {printf &quot;%d&quot;, $3/$2 * 100}&apos;)
```

La diferencia entre &quot;libre&quot; y &quot;disponible&quot; es importante: Linux usa la RAM libre para caché de disco, así que &quot;libre&quot; suele ser un número chico incluso cuando todo está bien. &quot;Disponible&quot; es la métrica que importa: te dice cuánta RAM puede usar realmente una aplicación nueva sin swapear.

El ranking de procesos por RAM usa el mismo patrón que el de CPU, pero ordenando por `%mem`:

```bash
ps aux --sort=-%mem | head -n $((TOP_N + 1)) | tail -n $TOP_N
```

También incluyo el swap porque cuando el swap empieza a usarse, es señal de que la RAM no alcanza y el servidor va a empezar a andar lento:

```bash
echo -e &quot;  ${BOLD}Swap:${RESET} $SWAP_USED / $SWAP_TOTAL&quot;
```

## Bloque 8: Uso de disco

El comando `df` con `--output` te deja elegir exactamente qué columnas mostrar, y con `-x` excluimos los filesystems virtuales que solo hacen ruido:

```bash
df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs -x squashfs
```

Después, el `du` de los directorios más pesados es útil para encontrar dónde se está llenando el disco:

```bash
du -h --max-depth=2 / 2&gt;/dev/null | sort -rh | head -10
```

`--max-depth=2` limita la recursión para que no tarde una eternidad, `sort -rh` ordena de mayor a menor en formato humano, y `head -10` nos da los 10 más pesados. El `2&gt;/dev/null` descarta los errores de permiso que aparecen al recorrer directorios como `/proc` o `/root`.

## Programación con cron

Para monitoreo automático, lo mejor es correr el modo `--alert-only` cada 5 minutos:

```bash
sudo crontab -e
```

Y agregar:

```
*/5 * * * * /opt/scripts/monitor.sh --alert-only &gt;&gt; /var/log/monitor-alerts.log 2&gt;&amp;1
```

Esto solo genera output cuando hay una alerta, así el log se mantiene chico y fácil de revisar. Si querés un reporte completo cada hora para revisar al final del día:

```
0 * * * * /opt/scripts/monitor.sh &gt;&gt; /var/log/monitor-hourly.log 2&gt;&amp;1
```

## Mejoras opcionales que valen la pena

Una vez que el script base funciona, hay varias cosas que podés sumarle:

**Enviar alertas por email:**

```bash
# Dentro del bloque --alert-only, al final:
if (( ALERTS &gt; 0 )); then
    echo &quot;Hay $ALERTS alertas en $(hostname)&quot; | mail -s &quot;Alerta servidor&quot; tu@email.com
fi
```

**Notificar por Slack o Discord con un webhook:**

```bash
WEBHOOK_URL=&quot;https://hooks.slack.com/services/T.../B.../xxx&quot;
if (( ALERTS &gt; 0 )); then
    curl -s -X POST -H &apos;Content-type: application/json&apos; \
        --data &quot;{\&quot;text\&quot;:\&quot;⚠ $ALERTS alertas en $(hostname)\&quot;}&quot; \
        &quot;$WEBHOOK_URL&quot;
fi
```

**Guardar un historial en CSV para graficar después:**

```bash
CSV_FILE=&quot;/var/log/monitor-history.csv&quot;
echo &quot;$(date &apos;+%Y-%m-%d %H:%M:%S&apos;),$CPU_USAGE,$RAM_PERCENT&quot; &gt;&gt; &quot;$CSV_FILE&quot;
```

Con ese CSV podés armar un gráfico simple con gnuplot o importarlo a una planilla para ver tendencias. Muy útil para anticipar cuándo se va a llenar el disco o cuándo necesitás más RAM.

**Verificar servicios críticos:**

```bash
for SERVICE in nginx mysql php-fpm; do
    if systemctl is-active --quiet &quot;$SERVICE&quot;; then
        echo -e &quot;  ${GREEN}✓ $SERVICE: activo${RESET}&quot;
    else
        echo -e &quot;  ${RED}✗ $SERVICE: inactivo${RESET}&quot;
    fi
done
```

Esto es clave si tenés un servidor web: no solo monitoreás recursos, sino que también verificás que los servicios estén corriendo.

**Monitorear temperatura (servidores físicos):**

```bash
if command -v sensors &amp;&gt; /dev/null; then
    echo -e &quot;  ${BOLD}Temperatura:${RESET}&quot;
    sensors | grep &quot;Core&quot; | while read -r line; do
        echo &quot;  $line&quot;
    done
fi
```

## Errores comunes que vale la pena evitar

- **Usar `top` interactivo en un script.** Siempre usá `top -bn1` para que corra una sola iteración en modo batch. Sin el `-b`, `top` se queda esperando input y el script nunca termina.
- **Confiar en el load average sin saber cuántos núcleos tiene el servidor.** Un load de 4.0 puede ser normal en 8 núcleos o catastrófico en 1 núcleo. Siempre mostrá o verificá `nproc`.
- **No excluir filesystems virtuales en `df`.** Sin `-x tmpfs -x devtmpfs`, la salida de `df` se llena de filesystems que no importan y dificulta encontrar el problema real.
- **Correr `du` desde `/` sin `--max-depth`.** En un servidor con millones de archivos, esto puede tardar minutos y consumir más recursos de los que estás intentando monitorear.
- **No tener en cuenta la diferencia entre RAM &quot;libre&quot; y &quot;disponible&quot;.** Si mirás solo la RAM libre, vas a creer que tu servidor está al límite cuando en realidad Linux está usando esa RAM para caché, que se libera instantáneamente cuando una app la necesita.

## Conclusión

Con este script ya tenés un sistema de monitoreo funcional, liviano y sin dependencias externas. Lo corrés a mano cuando entrás a revisar un servidor, lo programás con cron para alertas automáticas y le sumás las mejoras que necesites con el tiempo.

La clave del monitoreo no es tener la herramienta más sofisticada, sino tener visibilidad constante sobre lo que importa: CPU, RAM y disco. Si esos tres están bien, tu servidor probablemente esté bien. Si alguno se dispara, lo querés saber antes de que los usuarios lo noten.

En el próximo post vamos a ver automatización de certificados SSL con Let&apos;s Encrypt y Bash, para que tus sitios siempre tengan HTTPS válido sin tener que renovarlos a mano.</content:encoded></item><item><title>Cálculos avanzados de fecha y hora en PHP con DateTime</title><link>https://blivecode.com/posts/calculos-avanzados-de-fecha-y-hora-en-php-con-datetime</link><guid isPermaLink="true">https://blivecode.com/posts/calculos-avanzados-de-fecha-y-hora-en-php-con-datetime</guid><description>Cómo dominar fechas en PHP usando la clase DateTime, DateTimeZone y una clase helper Fechas con funciones reutilizables para formatear, sumar, restar y convertir zonas horarias.</description><content:encoded>Trabajar con fechas en PHP es una de esas tareas que parecen simples hasta que te toca calcular el último día del mes, sumar días hábiles a una fecha, convertir una zona horaria o generar el rango de los últimos seis meses para un reporte. Las funciones nativas tipo `date()` resuelven lo básico, pero cuando el proyecto crece necesitas algo más robusto.

En este post te voy a mostrar dos approaches: por un lado la API moderna de `DateTime` y `DateTimeZone` que trae PHP, y por el otro una clase helper que tengo en mi repositorio personal desde hace años y que uso en prácticamente todos los proyectos. La llamo `Fechas` y la armé justamente para no estar reinventando la rueda cada vez que tengo que formatear una fecha en español, sumar días hábiles o convertir entre zonas horarias.

## El problema con las funciones nativas sueltas

Si alguna vez tuviste que hacer algo más allá de un `date(&apos;Y-m-d&apos;)`, seguro te topaste con esta clase de código:

```php
$fecha = &quot;2026-06-27&quot;;
$partes = explode(&apos;-&apos;, $fecha);
$mesEnTexto = array(&apos;Enero&apos;, &apos;Febrero&apos;, ...)[(int)$partes[1] - 1];
echo $partes[2] . &quot; de &quot; . $mesEnTexto . &quot; del &quot; . $partes[0];
```

Funciona, pero es feo, repetitivo y está lleno de números mágicos. Si lo copias en cinco archivos distintos, el día que cambies el formato del mes, vas a tener que cazar cada copia. Y ni hablar de los bugs clásicos: usar `strtotime` con una fecha ambigua, olvidar configurar la zona horaria y obtener horas fantasma, o calcular mal el último día de febrero en año bisiesto.

La API de `DateTime` te da objetos con métodos claros y zona horaria explícita, lo cual ya es una mejora enorme. Pero aún así, en proyectos reales uno termina creando funciones de envoltura porque la sintaxis se vuelve verbosa para cosas que haces todo el tiempo. Por eso armé la clase `Fechas`.

## La clase Fechas: un helper reutilizable

La idea es tener un lugar centralizado con todas las operaciones de fecha que repito en mis proyectos. La clase vive en el namespace `App\Helpers` y todos sus métodos son `static`, así la uso sin necesidad de instanciar nada.

```php
&lt;?php
namespace App\Helpers;

use DateTime;
use DateTimeZone;

class Fechas
{
    private static $dayOfWeek = array(&apos;Domingo&apos;, &apos;Lunes&apos;, &apos;Martes&apos;, &apos;Miercoles&apos;, &apos;Jueves&apos;, &apos;Viernes&apos;, &apos;Sabado&apos;);
    private static $monthOfYear = array(&apos;Enero&apos;, &apos;Febrero&apos;, &apos;Marzo&apos;, &apos;Abril&apos;, &apos;Mayo&apos;, &apos;Junio&apos;, &apos;Julio&apos;, &apos;Agosto&apos;, &apos;Septiembre&apos;, &apos;Octubre&apos;, &apos;Noviembre&apos;, &apos;Diciembre&apos;);
}
```

Los dos arrays estáticos al inicio guardan los nombres en español, que es lo que más uso. En lugar de pelearme con `IntlDateFormatter` o `setlocale` cada vez, los tengo ahí disponibles y puedo abreviarlos cuando los necesito para una interfaz compacta (por ejemplo, &quot;Lun 27 de Jun&quot;).

## Obtener fecha y hora actual

Empiezo por lo más básico: cómo pedirle al sistema la fecha y la hora en distintos formatos. Estos métodos son pequeños wrappers sobre `date()`, pero el valor real es que estandarizan el formato en todo el proyecto.

```php
echo Fechas::getDate();
// &quot;2026-06-27&quot; (formato ISO, ideal para guardar en BD)

echo Fechas::getDate(&quot;es&quot;);
// &quot;27-06-2026&quot; (formato latino)

echo Fechas::getHour();
// &quot;14:32:08&quot;

echo Fechas::getDateTime(&quot;es&quot;);
// &quot;27-06-2026 14:32:08&quot;
```

El método `getDate` acepta un parámetro `type` con valor `&quot;es&quot;` para invertir día y mes. Es un parámetro muy pequeño pero útil: en formularios de países hispanohablantes solemos esperar `dd-mm-aaaa` y en bases de datos siempre guardamos `aaaa-mm-dd`. Tener una sola función que cubre ambos casos evita que el equipo invente sus propias convenciones.

## Nombres de días y meses en español

Esta es la parte que más uso. En lugar de tener un array perdido en cada vista, lo centralizo aquí.

```php
Fechas::getNameDay(1);      // &quot;Lunes&quot;
Fechas::getNameDay(1, 3);   // &quot;Lun&quot;
Fechas::getNameDay(1, 2);   // &quot;Lu&quot;

Fechas::getNameMonth(5);    // &quot;Junio&quot;
Fechas::getNameMonth(5, 3); // &quot;Jun&quot;
Fechas::getNameMonth(5, 2); // &quot;Ju&quot;
```

El parámetro `avr` controla la abreviatura: 3 caracteres para algo legible en una columna de tabla, 2 caracteres para interfaces muy compactas como badges o chips. `null` (el valor por defecto) devuelve el nombre completo.

Hay un detalle interesante en la implementación. Para abreviar usa `substr` con un cálculo de longitud:

```php
$long = strlen(self::$dayOfWeek[$num]);
$day = substr(self::$dayOfWeek[$num], 0, -($long - 3));
```

Es un truco para no hardcodear la longitud de cada palabra. Si mañana cambio `&quot;Miercoles&quot;` por `&quot;Miércoles&quot;` (con tilde), la abreviatura sigue funcionando porque recalcula el corte. Funciona, aunque si alguna vez agregas palabras de menos de 3 caracteres te va a explotar (cosa rara en días y meses, pero ojo).

## Formatear fechas en texto legible

Una necesidad muy común en backoffice y reportes es mostrar la fecha en formato humano, no `2026-06-27` sino &quot;27 de Junio del 2026&quot;. Eso lo resuelve `getDateText`.

```php
echo Fechas::getDateText();
// &quot;27 de Junio del 2026&quot;

echo Fechas::getDateText(&apos;/&apos;, &apos;2026-06-27&apos;, 3);
// &quot;27/Jun/2026&quot;

echo Fechas::getDateText(&apos;txt&apos;, &apos;2026-06-27&apos;, 2);
// &quot;27 de Ju del 2026&quot;
```

El primer parámetro define el separador (`&apos;txt&apos;` para texto natural con &quot;de&quot; y &quot;del&quot;, o cualquier otro string como `&apos;/&apos;` o `&apos;-&apos;`). El segundo es la fecha (si lo dejas en null toma la actual) y el tercero es la abreviatura del mes.

También hay una variante que incluye la hora, útil para logs o timestamps de actividades:

```php
echo Fechas::getDateHourText(&apos;2026-06-27 14:32:08&apos;);
// &quot;27-Junio-2026 a 02:32&quot;
```

Notá que la hora se convierte de 24 a 12 horas, lo cual es típico para mostrar al usuario final. Si necesitás la hora en formato 24, podés modificarla cambiando la línea `$dateText .= Fechas::getHours($hora[0]) . &apos;:&apos; . $hora[1];` por `$dateText .= $hora[0] . &apos;:&apos; . $hora[1];`.

## Sumar y restar días, meses y años

Otra operación del día a día. Calcular vencimientos, fechas de corte, plazos de gracia. Acá los métodos están espejados: `addDay` y `downDay`, `addMonth` y `downMonth`, `addYear` y `downYear`.

```php
$hoy = &quot;2026-06-27&quot;;

echo Fechas::addDay($hoy, 15);
// &quot;2026-07-12&quot;

echo Fechas::downDay($hoy, 7);
// &quot;2026-06-20&quot;

echo Fechas::addMonth($hoy, 3);
// &quot;2026-09-27&quot;

echo Fechas::addYear($hoy, 1);
// &quot;2027-06-27&quot;
```

La implementación usa `mktime` y `list` para descomponer la fecha. Hay que tener presente un detalle: si hacés `addDay(&quot;2026-01-31&quot;, 1)`, el resultado es `&quot;2026-02-01&quot;` porque PHP normaliza automáticamente la fecha. Esto suele ser lo que querés, pero si necesitás mantener el día del mes original, deberías trabajar con objetos `DateTime` que tengan métodos como `modify(&apos;+1 day&apos;)` con comportamiento más predecible.

Acá es donde entra la API moderna de PHP, que es más explícita sobre ese tipo de cosas:

```php
$fecha = new DateTime(&quot;2026-01-31&quot;);
$fecha-&gt;modify(&apos;+1 day&apos;);
echo $fecha-&gt;format(&apos;Y-m-d&apos;); // &quot;2026-02-01&quot;
```

Mismo resultado, pero queda más claro qué estás haciendo.

## Calcular días entre dos fechas

Para calcular la diferencia entre dos fechas hay un método específico. Lo armé para reportes de antigüedad, tiempo transcurrido desde una compra, o cualquier validación tipo &quot;esto no puede tener más de N días&quot;.

```php
echo Fechas::getDaysPassed(&quot;2026-01-15&quot;, &quot;2026-06-27&quot;);
// 163
```

La implementación toma ambas fechas, las convierte a timestamp Unix, las resta en segundos, divide por 86400 (segundos en un día) y devuelve el valor absoluto como entero. Si la fecha final es menor que la inicial, lanza una excepción.

Un detalle interesante: el cálculo usa `floor`, lo que significa que devuelve días completos. Si querés incluir fracciones de día (por ejemplo para calcular edades o tiempos de servicio), deberías quitar el `floor` y devolver un float. También es importante notar que ignora las horas, así que si pasás `&quot;2026-01-15 23:00&quot;` y `&quot;2026-01-16 01:00&quot;` te va a decir que pasó 1 día, no 2 horas. Para casos donde la hora importa, conviene normalizar ambas fechas a `Y-m-d` antes de pasarlas.

## Último día del mes

Esta es una operación que parece trivial pero esconde varios casos especiales. Febrero puede tener 28 o 29 días según si el año es bisiesto. Los meses de 30 y 31 días hay que recordarlos a mano. Por eso vale la pena centralizarla.

```php
echo Fechas::getLastDayMonth(&quot;2026-02-15&quot;);
// &quot;2026-02-28&quot;

echo Fechas::getLastDayMonth(&quot;2024-02-15&quot;);
// &quot;2024-02-29&quot; (2024 es año bisiesto)

echo Fechas::getLastDayMonth(&quot;2026-04-15&quot;);
// &quot;2026-04-30&quot;
```

El truco de la implementación es saltar al primer día del mes siguiente y restar un segundo. No es la forma más elegante, pero funciona y no necesita de una tabla de días por mes. Como bonus, devuelve la fecha en formato `Y-m-d`, lista para insertar en una base de datos o comparar con otras fechas.

## Convertir entre zonas horarias

Acá es donde entra la API moderna de PHP. Para trabajar con zonas horarias necesitás dos clases: `DateTime` para la fecha y `DateTimeZone` para la zona destino.

```php
echo Fechas::convertDateIsoToTimeZone(&quot;2026-06-27 14:00:00&quot;, &quot;America/Mexico_City&quot;);
// &quot;2026-06-27 08:00:00&quot;
```

El método toma la fecha (asume que está en la zona horaria por defecto del servidor) y la convierte a la zona horaria destino usando `DateTime::setTimezone`. La lista de zonas válidas la podés sacar de la [documentación oficial de PHP](https://www.php.net/manual/es/timezones.php), que incluye todas las zonas IANA.

Un caso de uso típico: tu servidor está en UTC y guardás todo en UTC, pero necesitás mostrarle al usuario la hora en su zona local. Si tenés la zona del usuario guardada en su perfil, esto es directo:

```php
$zonaUsuario = &quot;America/Buenos_Aires&quot;;
echo Fechas::convertDateIsoToTimeZone($evento-&gt;created_at, $zonaUsuario);
```

Importante: para que esto funcione correctamente, la zona horaria del servidor tiene que estar bien configurada con `date_default_timezone_set` en el bootstrap de la aplicación, o seteada en el `php.ini`. Si la fecha de entrada ya tiene la zona incluida (formato ISO 8601 con offset como `2026-06-27T14:00:00+00:00`), PHP la respeta automáticamente.

## Sumar días hábiles

Esta función la armé para sistemas de gestión donde los plazos se cuentan solo en días laborables. Por ejemplo, &quot;entrega en 5 días hábiles&quot; o &quot;vencimiento en 10 días hábiles a partir de hoy&quot;.

```php
echo Fechas::addWeekDay(5, &quot;2026-06-26&quot;);
// &quot;Mie 01 de Julio&quot;
```

El primer parámetro es la cantidad de días hábiles a sumar y el segundo es la fecha de inicio. Si no pasás fecha, toma la actual. El formato de salida incluye el nombre abreviado del día y el mes completo, ideal para mostrar en una pantalla de &quot;fecha estimada de entrega&quot;.

La lógica de cálculo es interesante: toma el día de la semana de la fecha de inicio (1 para lunes, 7 para domingo en formato ISO), suma los días solicitados, calcula cuántos fines de semana completos hay en el medio y los suma al total. Después agrega esa cantidad de segundos a la fecha original. El truco está en los casos especiales de los bordes (cuando caés justo en sábado o domingo) que están manejados con los `if` que ajustan `$findesemana`.

Hay una versión &quot;raw&quot; que devuelve la fecha sin formatear, útil cuando solo necesitás el cálculo:

```php
echo Fechas::getNexBusinessDay(5, &quot;2026-06-26&quot;);
// &quot;2026-07-01&quot;
```

## Listar los últimos N meses

Para reportes con un selector de período, muchas veces necesitás generar un array con los últimos 6 o 12 meses y sus nombres, así el usuario puede elegir. Eso es lo que hace `getListMonthPast`:

```php
$meses = Fechas::getListMonthPast();
foreach ($meses as $mes) {
    echo $mes[&apos;name&apos;] . &quot; - &quot; . $mes[&apos;date&apos;] . &quot;\n&quot;;
}
// Junio - 2026-06
// Mayo - 2026-05
// Abril - 2026-04
// Marzo - 2026-03
// Febrero - 2026-02
// Enero - 2026-01
```

Itera desde el mes actual hacia atrás, generando el par `nombre + periodo`. Es ideal para llenar un select en un formulario de filtros, y la primera entrada siempre es el mes en curso. Si necesitás solo 3 meses, podés cortar con `array_slice($meses, 0, 3)`.

## Cuándo conviene usar DateTime puro

La clase `Fechas` resuelve el 90% de los casos que me encuentro, pero hay situaciones donde conviene ir directo a `DateTime`:

- **Cuando necesitás hacer cálculos con locales específicos** (por ejemplo, fechas en japonés con `IntlDateFormatter`).
- **Cuando operás con intervalos** (`DateInterval` permite hacer diff entre fechas y obtener años, meses y días por separado, no solo total de días).
- **Cuando el formato de entrada es ambiguo** y querés ser estricto con `DateTime::createFromFormat` y manejar errores de parseo.
- **Cuando hacés muchas operaciones encadenadas** sobre la misma fecha (sumar, restar, formatear varias veces), porque mantener una instancia es más eficiente que parsear strings una y otra vez.

Un ejemplo concreto con `DateTime` y `DateInterval` que la clase `Fechas` no cubre:

```php
$inicio = new DateTime(&quot;2026-01-15&quot;);
$fin = new DateTime(&quot;2026-06-27&quot;);
$diff = $inicio-&gt;diff($fin);

echo $diff-&gt;y . &quot; años, &quot; . $diff-&gt;m . &quot; meses, &quot; . $diff-&gt;d . &quot; días&quot;;
// &quot;0 años, 5 meses, 12 días&quot;
```

Esto es útil para calcular edades o antigüedad en formato &quot;X años, Y meses&quot;, algo común en sistemas de recursos humanos o contratos.

## Conclusión

La regla que sigo es: para el 90% de operaciones repetidas en mis proyectos, tener una clase helper como `Fechas` me ahorra tiempo y mantiene consistencia. Para cálculos más complejos, cálculos con locales específicos, o cuando necesito el poder completo de la API moderna, voy directo a `DateTime` y `DateInterval`.

Lo que recomiendo es no empezar con la clase completa el día uno. Empezá con dos o tres métodos que más uses, sumá los que vayas necesitando, y refactorizá a `DateTime` cuando te topes con un caso que el helper no cubra bien. La idea es tener un toolkit propio que crezca con tu experiencia, no un mostruo de 30 métodos que no usás.

En el próximo post vamos a meternos con arrays, y después con el manejo de sesiones y cookies. Si tenés tu propia clase helper o alguna función de fecha que uses seguido, me encantaría leerla en los comentarios.</content:encoded></item><item><title>Domina los arrays en PHP: map, filter, reduce y más</title><link>https://blivecode.com/posts/domina-los-arrays-en-php</link><guid isPermaLink="true">https://blivecode.com/posts/domina-los-arrays-en-php</guid><description>Uso práctico de array_map, array_filter, array_reduce, array_merge, array_unique y ordenamiento con sort, usort. Aprende a procesar colecciones de datos como un profesional.</description><content:encoded>Los arrays son quizá la estructura de datos más usada en PHP. Desde listar productos de una tienda, filtrar resultados de una base de datos, ordenar usuarios o agrupar estadísticas, casi todo pasa por un array en algún momento.

En este post te voy a enseñar las funciones que más uso para transformar, filtrar, combinar y ordenar arrays, con ejemplos reales que puedes copiar y adaptar directamente a tus proyectos.

## Transformar datos con array_map()

`array_map()` aplica una función a cada elemento de un array y devuelve un nuevo array con los resultados. Es ideal cuando necesitas transformar una colección sin modificar la original.

```php
$precios = [100, 250, 80, 430];

$conIva = array_map(function ($precio) {
    return $precio * 1.16;
}, $precios);

print_r($conIva);
// [116, 290, 92.8, 498.8]
```

También puedes usar funciones con nombre o arrow functions de PHP 7.4+:

```php
$nombres = [&quot;juan&quot;, &quot;maria&quot;, &quot;pedro&quot;];
$capitalizados = array_map(&apos;ucfirst&apos;, $nombres);

print_r($capitalizados);
// [&quot;Juan&quot;, &quot;Maria&quot;, &quot;Pedro&quot;]
```

**Caso de uso real:** Formatear una lista de precios para mostrar en una tabla.

```php
$productos = [
    [&apos;nombre&apos; =&gt; &apos;Mouse&apos;, &apos;precio&apos; =&gt; 350],
    [&apos;nombre&apos; =&gt; &apos;Teclado&apos;, &apos;precio&apos; =&gt; 750],
    [&apos;nombre&apos; =&gt; &apos;Monitor&apos;, &apos;precio&apos; =&gt; 3200],
];

$preciosFormateados = array_map(function ($producto) {
    return [
        &apos;nombre&apos; =&gt; $producto[&apos;nombre&apos;],
        &apos;precio&apos; =&gt; &apos;$&apos; . number_format($producto[&apos;precio&apos;], 2),
    ];
}, $productos);
```

## Filtrar elementos con array_filter()

`array_filter()` devuelve un nuevo array con solo los elementos que cumplen una condición. La clave original se conserva, así que si necesitas índices consecutivos, recuerda usar `array_values()`.

```php
$numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

$pares = array_filter($numeros, function ($numero) {
    return $numero % 2 === 0;
});

print_r($pares);
// [1 =&gt; 2, 3 =&gt; 4, 5 =&gt; 6, 7 =&gt; 8, 9 =&gt; 10]

$pares = array_values($pares);
// [2, 4, 6, 8, 10]
```

**Ejemplo práctico:** Filtrar productos que tienen stock disponible.

```php
$productos = [
    [&apos;nombre&apos; =&gt; &apos;Mouse&apos;, &apos;stock&apos; =&gt; 12],
    [&apos;nombre&apos; =&gt; &apos;Teclado&apos;, &apos;stock&apos; =&gt; 0],
    [&apos;nombre&apos; =&gt; &apos;Monitor&apos;, &apos;stock&apos; =&gt; 3],
];

$disponibles = array_filter($productos, function ($producto) {
    return $producto[&apos;stock&apos;] &gt; 0;
});
```

Si omites la función de callback, `array_filter()` elimina los valores que son &quot;falsos&quot; (`false`, `0`, `&quot;&quot;`, `null`, `[]`):

```php
$valores = [0, 1, &quot;&quot;, &quot;hola&quot;, null, [], true];
$limpio = array_filter($valores);

print_r($limpio);
// [1 =&gt; 1, 3 =&gt; &quot;hola&quot;, 6 =&gt; true]
```

## Reducir arrays con array_reduce()

`array_reduce()` convierte un array en un único valor acumulado. Es perfecto para sumar totales, construir strings, contar elementos o transformar una lista en una estructura diferente.

```php
$ventas = [120, 340, 560, 90];

$total = array_reduce($ventas, function ($acumulador, $venta) {
    return $acumulador + $venta;
}, 0);

echo $total; // 1110
```

**Caso de uso real:** Calcular el total de un carrito de compras con productos y cantidades.

```php
$carrito = [
    [&apos;producto&apos; =&gt; &apos;Mouse&apos;, &apos;precio&apos; =&gt; 350, &apos;cantidad&apos; =&gt; 2],
    [&apos;producto&apos; =&gt; &apos;Teclado&apos;, &apos;precio&apos; =&gt; 750, &apos;cantidad&apos; =&gt; 1],
];

$total = array_reduce($carrito, function ($acumulador, $item) {
    return $acumulador + ($item[&apos;precio&apos;] * $item[&apos;cantidad&apos;]);
}, 0);

echo $total; // 1450
```

También puedes usar `array_reduce()` para construir estructuras más complejas:

```php
$usuarios = [
    [&apos;nombre&apos; =&gt; &apos;Ana&apos;, &apos;rol&apos; =&gt; &apos;admin&apos;],
    [&apos;nombre&apos; =&gt; &apos;Luis&apos;, &apos;rol&apos; =&gt; &apos;editor&apos;],
    [&apos;nombre&apos; =&gt; &apos;Sofía&apos;, &apos;rol&apos; =&gt; &apos;admin&apos;],
];

$porRol = array_reduce($usuarios, function ($acumulador, $usuario) {
    $acumulador[$usuario[&apos;rol&apos;]][] = $usuario[&apos;nombre&apos;];
    return $acumulador;
}, []);

print_r($porRol);
// [
//   &quot;admin&quot; =&gt; [&quot;Ana&quot;, &quot;Sofía&quot;],
//   &quot;editor&quot; =&gt; [&quot;Luis&quot;]
// ]
```

## Combinar arrays con array_merge()

`array_merge()` une dos o más arrays en uno solo. Si las claves son numéricas, se reindexan automáticamente. Si son strings, los valores posteriores sobrescriben a los anteriores.

```php
$frutas = [&quot;manzana&quot;, &quot;pera&quot;];
$verduras = [&quot;zanahoria&quot;, &quot;espinaca&quot;];

$alimentos = array_merge($frutas, $verduras);

print_r($alimentos);
// [&quot;manzana&quot;, &quot;pera&quot;, &quot;zanahoria&quot;, &quot;espinaca&quot;]
```

**Ejemplo práctico:** Unir configuraciones por defecto con configuraciones personalizadas del usuario.

```php
$defaultConfig = [
    &apos;theme&apos; =&gt; &apos;light&apos;,
    &apos;lang&apos; =&gt; &apos;es&apos;,
    &apos;notifications&apos; =&gt; true,
];

$userConfig = [
    &apos;theme&apos; =&gt; &apos;dark&apos;,
    &apos;lang&apos; =&gt; &apos;en&apos;,
];

$config = array_merge($defaultConfig, $userConfig);

print_r($config);
// [&apos;theme&apos; =&gt; &apos;dark&apos;, &apos;lang&apos; =&gt; &apos;en&apos;, &apos;notifications&apos; =&gt; true]
```

## Eliminar duplicados con array_unique()

`array_unique()` elimina los valores duplicados de un array, conservando la primera aparición de cada valor.

```php
$tags = [&quot;php&quot;, &quot;mysql&quot;, &quot;php&quot;, &quot;linux&quot;, &quot;mysql&quot;, &quot;bash&quot;];

$tagsUnicos = array_unique($tags);

print_r($tagsUnicos);
// [&quot;php&quot;, &quot;mysql&quot;, &quot;linux&quot;, &quot;bash&quot;]
```

**Caso real:** Limpiar una lista de correos antes de enviar una newsletter.

```php
$emails = [
    &quot;juan@email.com&quot;,
    &quot;maria@email.com&quot;,
    &quot;juan@email.com&quot;,
    &quot;pedro@email.com&quot;,
];

$emailsUnicos = array_values(array_unique($emails));
// [&quot;juan@email.com&quot;, &quot;maria@email.com&quot;, &quot;pedro@email.com&quot;]
```

## Ordenar arrays con sort() y usort()

PHP ofrece muchas funciones para ordenar arrays. Las más comunes son:

- `sort()` - Ordena un array indexado de menor a mayor.
- `rsort()` - Ordena un array indexado de mayor a menor.
- `asort()` / `arsort()` - Ordenan arrays asociativos preservando las claves.
- `ksort()` / `krsort()` - Ordenan arrays asociativos por sus claves.
- `usort()` - Ordena con una función de comparación personalizada.

```php
$numeros = [5, 2, 8, 1, 9];
sort($numeros);

print_r($numeros);
// [1, 2, 5, 8, 9]
```

**Ejemplo con array asociativo:**

```php
$usuarios = [
    [&apos;nombre&apos; =&gt; &apos;Carlos&apos;, &apos;edad&apos; =&gt; 30],
    [&apos;nombre&apos; =&gt; &apos;Ana&apos;, &apos;edad&apos; =&gt; 25],
    [&apos;nombre&apos; =&gt; &apos;Luis&apos;, &apos;edad&apos; =&gt; 35],
];

usort($usuarios, function ($a, $b) {
    return $a[&apos;edad&apos;] &lt;=&gt; $b[&apos;edad&apos;];
});

print_r($usuarios);
// Ana (25), Carlos (30), Luis (35)
```

El operador spaceship `&lt;=&gt;` compara dos valores y devuelve `-1`, `0` o `1`, lo que hace que `usort()` sea muy limpio de leer.

## Combinando funciones: el poder real

La verdadera potencia de estas funciones aparece cuando las combinas. Imagina que tienes una lista de productos y necesitas filtrar los disponibles, calcular el total con IVA y formatear el resultado.

```php
$productos = [
    [&apos;nombre&apos; =&gt; &apos;Mouse&apos;, &apos;precio&apos; =&gt; 350, &apos;stock&apos; =&gt; 12],
    [&apos;nombre&apos; =&gt; &apos;Teclado&apos;, &apos;precio&apos; =&gt; 750, &apos;stock&apos; =&gt; 0],
    [&apos;nombre&apos; =&gt; &apos;Monitor&apos;, &apos;precio&apos; =&gt; 3200, &apos;stock&apos; =&gt; 3],
    [&apos;nombre&apos; =&gt; &apos;Webcam&apos;, &apos;precio&apos; =&gt; 1200, &apos;stock&apos; =&gt; 8],
];

$totalConIva = array_reduce(
    array_filter($productos, fn($p) =&gt; $p[&apos;stock&apos;] &gt; 0),
    fn($total, $p) =&gt; $total + ($p[&apos;precio&apos;] * 1.16),
    0
);

echo number_format($totalConIva, 2);
// 5559.00
```

Otro patrón común: obtener una lista limpia de valores únicos a partir de datos crudos.

```php
$registros = [
    [&apos;ciudad&apos; =&gt; &apos;CDMX&apos;, &apos;venta&apos; =&gt; 100],
    [&apos;ciudad&apos; =&gt; &apos;Guadalajara&apos;, &apos;venta&apos; =&gt; 200],
    [&apos;ciudad&apos; =&gt; &apos;CDMX&apos;, &apos;venta&apos; =&gt; 150],
];

$ciudades = array_values(array_unique(array_map(
    fn($r) =&gt; $r[&apos;ciudad&apos;],
    $registros
)));

print_r($ciudades);
// [&quot;CDMX&quot;, &quot;Guadalajara&quot;]
```

## Funciones adicionales útiles

Estas no las cubrimos en detalle pero vale la pena conocerlas:

- `array_column()` - Extrae una sola columna de un array multidimensional.
- `array_search()` - Busca un valor y devuelve su clave.
- `in_array()` - Verifica si un valor existe en un array.
- `array_key_exists()` - Verifica si una clave existe.
- `array_slice()` - Extrae una porción de un array.
- `array_splice()` - Elimina o reemplaza una porción de un array.
- `array_chunk()` - Divide un array en fragmentos más pequeños.

```php
$usuarios = [
    [&apos;id&apos; =&gt; 1, &apos;nombre&apos; =&gt; &apos;Ana&apos;],
    [&apos;id&apos; =&gt; 2, &apos;nombre&apos; =&gt; &apos;Luis&apos;],
];

$nombres = array_column($usuarios, &apos;nombre&apos;);
// [&quot;Ana&quot;, &quot;Luis&quot;]

$ids = array_column($usuarios, &apos;id&apos;, &apos;nombre&apos;);
// [&quot;Ana&quot; =&gt; 1, &quot;Luis&quot; =&gt; 2]
```

## Conclusión

Dominar estas funciones te permite escribir código PHP más limpio, declarativo y fácil de mantener. En lugar de llenar tu código con bucles `foreach` anidados, puedes expresar la intención de tu lógica de forma clara con `map`, `filter` y `reduce`.

Mi recomendación: identifica en tu proyecto actual dónde estás transformando, filtrando o reduciendo listas y reemplaza esos bloques con las funciones adecuadas. Con la práctica, empezarás a ver patrones de procesamiento de datos de forma natural.

¡Espero que estos ejemplos te sean útiles para tu próximo proyecto PHP!</content:encoded></item><item><title>Validación de datos en PHP: sanitizar y asegurar entradas de usuario</title><link>https://blivecode.com/posts/validacion-de-datos-en-php-sanitizar-entradas-de-usuario</link><guid isPermaLink="true">https://blivecode.com/posts/validacion-de-datos-en-php-sanitizar-entradas-de-usuario</guid><description>Cómo usar filter_var para validar emails, URLs, enteros y floats en PHP. Aprende a crear funciones reutilizables de validación y sanitización para proteger tus formularios.</description><content:encoded>Cada formulario de tu aplicación es una puerta de entrada a tu sistema. Y cada campo de ese formulario es una oportunidad para que alguien malintencionado te mande código SQL, JavaScript, comandos de shell, o simplemente un email con un formato tan raro que revienta tu función de envío. Si no validás y sanitizás lo que te llega, no estás haciendo desarrollo web, estás jugando a la ruleta rusa con tu base de datos.

En este post vamos a recorrer `filter_var`, que es la función nativa de PHP dedicada a esto, y vamos a armar un set de funciones reutilizables que podés copiar a tus proyectos para tener una capa de validación consistente. No es la cobertura más exhaustiva del tema (hay libros enteros sobre seguridad web), pero es lo mínimo que necesitás para dormir tranquilo.

## Validar vs sanitizar: no es lo mismo

Antes de meternos con código, hay una diferencia conceptual que confunde a mucha gente:

- **Validar** es comprobar que un dato cumple con un formato esperado. ¿Es un email válido? ¿Es un número entero positivo? ¿Está la cadena dentro de cierto rango? La validación responde con &quot;sí&quot; o &quot;no&quot;.
- **Sanitizar** es limpiar un dato para hacerlo seguro, removiendo o escapando los caracteres problemáticos. La sanitización transforma el dato.

Por ejemplo: si el usuario escribe `&quot;   juan@email.com   &quot;`, sanitizar te devuelve `&quot;juan@email.com&quot;` (sin espacios) y validar te dice que el formato es correcto. Si escribe `&quot;&lt;script&gt;alert(&apos;xss&apos;)&lt;/script&gt;&quot;`, sanitizar te devuelve un string inofensivo (con los tags escapados o removidos) y validar te dice que no es un formato aceptable para un campo de email.

El orden correcto siempre es: **primero sanitizar, después validar**. Si sanitizás bien, la validación va a pasar en la mayoría de los casos legítimos. Si no sanitizás, terminás rechazando emails válidos con espacios o rechazando entradas que eran perfectamente seguras.

## filter_var: el caballito de batalla

PHP trae una función专门 pensada para esto: `filter_var()`. Recibe el valor a procesar, un ID de filtro, y opciones opcionales. La misma función sirve para validar y para sanitizar, lo que cambia es el ID del filtro.

```php
filter_var($valor, FILTER_VALIDATE_EMAIL);
filter_var($valor, FILTER_SANITIZE_EMAIL);
```

Empecemos por la sanitización, que es donde menos se mete la gente.

## Sanitizar strings

El caso más común. Tenés un input de texto libre y querés sacarle todo lo que no debería estar ahí. El filtro `FILTER_SANITIZE_FULL_SPECIAL_CHARS` escapa HTML, comillas y otros caracteres especiales, y es ideal para datos que vas a mostrar después en una página.

```php
$nombre = &quot;&lt;script&gt;alert(&apos;hola&apos;)&lt;/script&gt;&quot;;
$limpio = filter_var($nombre, FILTER_SANITIZE_FULL_SPECIAL_CHARS);

echo $limpio;
// &quot;&amp;lt;script&amp;gt;alert(&apos;hola&apos;)&amp;lt;/script&amp;gt;&quot;
```

El string original sigue ahí, pero escapado. Cuando lo renderices en HTML, el navegador lo va a mostrar como texto literal en lugar de ejecutarlo como JavaScript. Esto por sí solo ya te protege de la mayoría de los ataques XSS reflejados.

Otros filtros de sanitización útiles:

```php
// Quitar tags HTML completos (más agresivo)
$limpio = filter_var($texto, FILTER_SANITIZE_STRING); // PHP 8.1+ deprecado

// Eliminar todo lo que no sea letras, números y espacios
$limpio = filter_var($texto, FILTER_SANITIZE_NUMBER_INT);

// Quitar caracteres no numéricos (deja el punto y el signo menos)
$precio = filter_var(&quot;$1,250.50&quot;, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION);
echo $precio; // &quot;1250.50&quot;

// Limpiar un email (quita caracteres que no son válidos en emails)
$email = filter_var(&quot;  juan@@email .com  &quot;, FILTER_SANITIZE_EMAIL);
echo $email; // &quot;juan@email.com&quot;

// Limpiar una URL (elimina caracteres no permitidos)
$url = filter_var(&quot;https://ejemplo.com/path&lt;script&gt;&quot;, FILTER_SANITIZE_URL);
echo $url; // &quot;https://ejemplo.com/pathscript&quot;
```

Un detalle: `FILTER_SANITIZE_STRING` está deprecado desde PHP 8.1. Si todavía lo usás, conviene migrar a `htmlspecialchars()` o `strip_tags()` según el caso. El primero escapa, el segundo elimina. Acá una comparación rápida:

```php
$texto = &quot;&lt;p&gt;Hola &lt;b&gt;mundo&lt;/b&gt;&lt;/p&gt;&quot;;

echo htmlspecialchars($texto);
// &quot;&amp;lt;p&amp;gt;Hola &amp;lt;b&amp;gt;mundo&amp;lt;/b&amp;gt;&amp;lt;/p&amp;gt;&quot;

echo strip_tags($texto);
// &quot;Hola mundo&quot;
```

`strip_tags` es más agresivo y borra todo el HTML, útil para campos tipo &quot;nombre&quot; donde no querés ningún tag. `htmlspecialchars` es más conservador, útil para campos como &quot;comentario&quot; donde querés permitir algo de formato pero sin que se ejecute.

## Validar emails

Este es probablemente el filtro que más vas a usar. Validar emails con `filter_var` es de una línea:

```php
$email = &quot;juan@ejemplo.com&quot;;

if (filter_var($email, FILTER_VALIDATE_EMAIL)) {
    echo &quot;Email válido&quot;;
} else {
    echo &quot;Email inválido&quot;;
}
```

El filtro `FILTER_VALIDATE_EMAIL` chequea que el formato sea correcto (usuario + @ + dominio + . + TLD) y que no haya caracteres inválidos. Es bastante estricto y rechaza emails malformados, pero ojo: **no verifica que el dominio exista ni que la casilla esté activa**. Para eso necesitás un servicio externo o mandar un email de confirmación con un link.

Casos reales que este filtro rechaza:

```php
filter_var(&quot;juan@&quot;, FILTER_VALIDATE_EMAIL);              // false (sin dominio)
filter_var(&quot;@ejemplo.com&quot;, FILTER_VALIDATE_EMAIL);       // false (sin usuario)
filter_var(&quot;juan@@ejemplo.com&quot;, FILTER_VALIDATE_EMAIL);  // false (doble @)
filter_var(&quot;juan espacio@ejemplo.com&quot;, FILTER_VALIDATE_EMAIL); // false (espacio)
filter_var(&quot;ju|an@ejemplo.com&quot;, FILTER_VALIDATE_EMAIL);  // false (carácter inválido)
```

Casos que sí pasa y que pueden ser válidos:

```php
filter_var(&quot;juan+etiqueta@gmail.com&quot;, FILTER_VALIDATE_EMAIL);     // válido
filter_var(&quot;juan.perez@sub.ejemplo.com&quot;, FILTER_VALIDATE_EMAIL);  // válido
filter_var(&quot;a@b.io&quot;, FILTER_VALIDATE_EMAIL);                       // válido
```

## Validar URLs

Para URLs el filtro es `FILTER_VALIDATE_URL`. Valida que el formato sea correcto, pero no chequea que el dominio exista ni que la URL responda. Sirve para descartar basura obvia antes de hacer una request o de guardar el dato.

```php
$url = &quot;https://blivecode.com/blog/php&quot;;

if (filter_var($url, FILTER_VALIDATE_URL)) {
    echo &quot;URL con formato válido&quot;;
}
```

Por defecto acepta varios esquemas (`http`, `https`, `ftp`, etc). Si querés restringir a `https` solamente (recomendado para sitios modernos), usá la bandera `FILTER_FLAG_PATH_REQUIRED`:

```php
$opciones = FILTER_FLAG_PATH_REQUIRED;
filter_var(&quot;https://ejemplo.com&quot;, FILTER_VALIDATE_URL, $opciones); // false (sin path)
filter_var(&quot;https://ejemplo.com/&quot;, FILTER_VALIDATE_URL, $opciones); // válido
```

Y si querés ser más estricto con el query string:

```php
filter_var(&quot;https://ejemplo.com/path?q=1&quot;, FILTER_VALIDATE_URL, FILTER_FLAG_QUERY_REQUIRED);
// válido, porque tiene query string
```

## Validar números

Acá hay tres filtros: `FILTER_VALIDATE_INT`, `FILTER_VALIDATE_FLOAT` y `FILTER_VALIDATE_REGEXP`. Los dos primeros aceptan banderas para forzar rangos.

```php
filter_var(&quot;42&quot;, FILTER_VALIDATE_INT);          // 42
filter_var(&quot;42.5&quot;, FILTER_VALIDATE_INT);        // false (es float)
filter_var(&quot;42abc&quot;, FILTER_VALIDATE_INT);       // false (no es puro número)
filter_var(&quot;-10&quot;, FILTER_VALIDATE_INT);         // -10 (acepta negativos)

// Forzar que sea positivo
filter_var(&quot;-10&quot;, FILTER_VALIDATE_INT, [&quot;options&quot; =&gt; [&quot;min_range&quot; =&gt; 0]]); // false
filter_var(&quot;10&quot;, FILTER_VALIDATE_INT, [&quot;options&quot; =&gt; [&quot;min_range&quot; =&gt; 0, &quot;max_range&quot; =&gt; 100]]); // 10
```

Para floats es igual pero con `FILTER_VALIDATE_FLOAT` y la bandera `FILTER_FLAG_ALLOW_THOUSAND` para aceptar separadores de miles:

```php
filter_var(&quot;1,250.50&quot;, FILTER_VALIDATE_FLOAT, FILTER_FLAG_ALLOW_THOUSAND); // 1250.5
filter_var(&quot;1250.50&quot;, FILTER_VALIDATE_FLOAT); // 1250.5
```

El problema clásico con `$_GET` es que todo llega como string. Si esperás un entero de la query string y el usuario te manda `&quot;abc&quot;`, `$_GET[&apos;id&apos;]` te va a llegar como string, no como false. Por eso es importante siempre castear y validar:

```php
$id = filter_input(INPUT_GET, &apos;id&apos;, FILTER_VALIDATE_INT, [&quot;options&quot; =&gt; [&quot;min_range&quot; =&gt; 1]]);
if ($id === false || $id === null) {
    http_response_code(400);
    echo &quot;ID inválido&quot;;
    exit;
}
```

`filter_input` es hermano de `filter_var` pero toma el valor directamente de `$_GET`, `$_POST` o `$_COOKIE`. Si la variable no existe, devuelve `null` en lugar de `false`, así que chequeá ambos.

## Validar IPs y dominios

Si en algún momento necesitás validar que una IP tenga formato correcto (por ejemplo, para un sistema de logs o un panel de admin que filtra por IP), hay dos filtros专门 para eso:

```php
filter_var(&quot;192.168.1.1&quot;, FILTER_VALIDATE_IP);        // &quot;192.168.1.1&quot;
filter_var(&quot;256.1.1.1&quot;, FILTER_VALIDATE_IP);          // false (fuera de rango)
filter_var(&quot;::1&quot;, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); // &quot;::1&quot;
```

Para dominios (`ejemplo.com` sin path ni protocolo), usá `FILTER_VALIDATE_DOMAIN`. Sirve para casos tipo &quot;el usuario configura su propio subdominio y querés validar el formato antes de guardarlo&quot;.

```php
filter_var(&quot;ejemplo.com&quot;, FILTER_VALIDATE_DOMAIN);        // &quot;ejemplo.com&quot;
filter_var(&quot;sub.ejemplo.com&quot;, FILTER_VALIDATE_DOMAIN);    // válido
filter_var(&quot;ejemplo.com/path&quot;, FILTER_VALIDATE_DOMAIN);   // false (tiene path)
```

## Funciones reutilizables para tus proyectos

Tener `filter_var` en cada lugar está bien, pero si lo repetís 20 veces en un mismo proyecto, vale la pena envolverlo en funciones专门 para tu dominio. Acá te dejo un set que uso de base y voy extendiendo según cada proyecto.

```php
class Validador
{
    public static function email(string $valor, bool $obligatorio = true): bool|string
    {
        $valor = trim($valor);
        if (empty($valor) &amp;&amp; !$obligatorio) {
            return &apos;&apos;;
        }
        if (empty($valor) &amp;&amp; $obligatorio) {
            return &apos;El email es obligatorio&apos;;
        }
        $valor = filter_var($valor, FILTER_SANITIZE_EMAIL);
        if (!filter_var($valor, FILTER_VALIDATE_EMAIL)) {
            return &apos;El email no tiene un formato válido&apos;;
        }
        return true; // válido, sin error
    }

    public static function texto(string $valor, int $min = 1, int $max = 255, bool $obligatorio = true): bool|string
    {
        $valor = trim($valor);
        $longitud = mb_strlen($valor);
        if ($longitud === 0 &amp;&amp; !$obligatorio) {
            return &apos;&apos;;
        }
        if ($longitud === 0 &amp;&amp; $obligatorio) {
            return &apos;Este campo es obligatorio&apos;;
        }
        if ($longitud &lt; $min) {
            return &quot;Mínimo $min caracteres&quot;;
        }
        if ($longitud &gt; $max) {
            return &quot;Máximo $max caracteres&quot;;
        }
        return true;
    }

    public static function entero(mixed $valor, int $min = PHP_INT_MIN, int $max = PHP_INT_MAX, bool $obligatorio = true): bool|string
    {
        if (($valor === null || $valor === &apos;&apos; || $valor === false) &amp;&amp; !$obligatorio) {
            return &apos;&apos;;
        }
        if (($valor === null || $valor === &apos;&apos; || $valor === false) &amp;&amp; $obligatorio) {
            return &apos;Este campo es obligatorio&apos;;
        }
        $valor = filter_var($valor, FILTER_VALIDATE_INT);
        if ($valor === false) {
            return &apos;Debe ser un número entero&apos;;
        }
        if ($valor &lt; $min) {
            return &quot;El valor mínimo es $min&quot;;
        }
        if ($valor &gt; $max) {
            return &quot;El valor máximo es $max&quot;;
        }
        return true;
    }

    public static function url(string $valor, bool $obligatorio = true): bool|string
    {
        $valor = trim($valor);
        if (empty($valor) &amp;&amp; !$obligatorio) {
            return &apos;&apos;;
        }
        if (empty($valor) &amp;&amp; $obligatorio) {
            return &apos;La URL es obligatoria&apos;;
        }
        $valor = filter_var($valor, FILTER_SANITIZE_URL);
        if (!filter_var($valor, FILTER_VALIDATE_URL)) {
            return &apos;La URL no tiene un formato válido&apos;;
        }
        return true;
    }

    public static function password(string $valor, int $min = 8, bool $obligatorio = true): bool|string
    {
        if (empty($valor) &amp;&amp; !$obligatorio) {
            return &apos;&apos;;
        }
        if (empty($valor) &amp;&amp; $obligatorio) {
            return &apos;La contraseña es obligatoria&apos;;
        }
        if (strlen($valor) &lt; $min) {
            return &quot;Mínimo $min caracteres&quot;;
        }
        return true;
    }

    public static function fecha(string $valor, string $formato = &apos;Y-m-d&apos;, bool $obligatorio = true): bool|string
    {
        $valor = trim($valor);
        if (empty($valor) &amp;&amp; !$obligatorio) {
            return &apos;&apos;;
        }
        if (empty($valor) &amp;&amp; $obligatorio) {
            return &apos;La fecha es obligatoria&apos;;
        }
        $objeto = \DateTime::createFromFormat($formato, $valor);
        if (!$objeto || $objeto-&gt;format($formato) !== $valor) {
            return &quot;La fecha no tiene el formato esperado ($formato)&quot;;
        }
        return true;
    }
}
```

La convención que uso es: si la validación pasa, la función devuelve `true`. Si falla, devuelve un string con el mensaje de error. Eso permite usarla directamente en formularios HTML o en APIs JSON. La validación de fecha es más estricta porque usa `DateTime::createFromFormat` y compara el string original con el formato, evitando el bug clásico de que `2026-13-45` sea aceptado por un parser laxo.

### Uso típico en un formulario

```php
$errores = [];

$resultado = Validador::email($_POST[&apos;email&apos;] ?? &apos;&apos;);
if ($resultado !== true) {
    $errores[&apos;email&apos;] = $resultado;
}

$resultado = Validador::texto($_POST[&apos;nombre&apos;] ?? &apos;&apos;, 2, 50);
if ($resultado !== true) {
    $errores[&apos;nombre&apos;] = $resultado;
}

$resultado = Validador::entero($_POST[&apos;edad&apos;] ?? null, 18, 120);
if ($resultado !== true) {
    $errores[&apos;edad&apos;] = $resultado;
}

if (empty($errores)) {
    // Todo OK, procesar el formulario
    $email = filter_var($_POST[&apos;email&apos;], FILTER_SANITIZE_EMAIL);
    $nombre = htmlspecialchars(trim($_POST[&apos;nombre&apos;]), ENT_QUOTES, &apos;UTF-8&apos;);
    $edad = (int) $_POST[&apos;edad&apos;];

    // ... guardar en BD, mandar email, etc.
} else {
    // Mostrar errores al usuario
    foreach ($errores as $campo =&gt; $mensaje) {
        echo &quot;&lt;p&gt;&lt;strong&gt;$campo:&lt;/strong&gt; $mensaje&lt;/p&gt;&quot;;
    }
}
```

Notá que después de validar, vuelvo a sanitizar los valores que voy a usar. La validación me dijo &quot;el formato es OK&quot;, la sanitización me deja el valor listo para usar. Esa doble pasada es la que evita sorpresas.

## Sanitización final antes de guardar o mostrar

Aunque hayas validado, siempre sanitizá el output. El mantra clásico de seguridad es &quot;no confíes en el input, no confíes en el output&quot; y la idea es escapar todo lo que llega del usuario antes de mostrarlo en HTML, antes de mandarlo a la base de datos, antes de escribirlo en un log. Cada destino tiene su propio escape.

```php
// Antes de guardar en BD (usá prepared statements, esto es solo defensa adicional)
$valorParaBD = htmlspecialchars(strip_tags($input), ENT_QUOTES, &apos;UTF-8&apos;);

// Antes de mostrar en HTML
$valorParaHTML = htmlspecialchars($input, ENT_QUOTES, &apos;UTF-8&apos;);

// Antes de escribir en un log
$valorParaLog = strip_tags(str_replace([&quot;\n&quot;, &quot;\r&quot;], &apos; &apos;, $input));

// Antes de usar en una query de SQL (lo correcto es prepared statement)
$valorParaSQL = addslashes($input); // NO recomendado, usar PDO con prepare
```

`htmlspecialchars` con `ENT_QUOTES` y `UTF-8` es la combinación que uso siempre que voy a mostrar texto en HTML. La flag `ENT_QUOTES` escapa comillas simples y dobles, lo que cierra el agujero típico de atributos HTML mal escapados.

## Errores comunes que vale la pena evitar

- **Validar en cliente y confiar en que está validado en servidor.** JavaScript se desactiva. La validación en cliente es solo para UX, nunca para seguridad.
- **Usar `empty()` para validar.** `empty(&quot;0&quot;)` devuelve true, así que el string `&quot;0&quot;` lo toma como vacío. Esto es fuente de bugs clásicos en formularios con selects o radios.
- **Olvidar la zona horaria al validar fechas.** Si recibís `&quot;2026-06-27&quot;` y tu servidor está en otra zona, podés tener problemas al guardarlo. Mejor trabajar en UTC.
- **Confiar en `FILTER_VALIDATE_EMAIL` para saber si un email existe.** Solo chequea formato, no existencia. Para eso mandá un email de confirmación.
- **Hacer sanitización pero no validación, o viceversa.** Son complementarios, no excluyentes.

## Conclusión

`filter_var` no es la herramienta más sexy de PHP, pero es de las más útiles. Tener un set de funciones de validación reutilizables te ahorra horas de debug y te protege de los ataques más comunes sin tener que instalar librerías externas pesadas.

La recomendación es empezar con una clase simple como la que te mostré, copiarla a tus proyectos, y extenderla cuando necesites reglas de negocio más específicas (por ejemplo, validar que un CUIT sea válido, que un código postal exista en una base de datos, o que un nombre de usuario no esté en una lista negra). El 80% de los formularios del mundo real se cubren con `email`, `texto`, `entero`, `url` y `fecha`.

En el próximo post vamos a hablar de arrays y las funciones que más uso para procesarlos. Si tenés alguna función de validación que uses seguido y no esté acá, me encantaría leerla en los comentarios.</content:encoded></item><item><title>Manipulación de strings en PHP: funciones que todo desarrollador debe conocer</title><link>https://blivecode.com/posts/manipulacion-de-strings-en-php</link><guid isPermaLink="true">https://blivecode.com/posts/manipulacion-de-strings-en-php</guid><description>Recorrido práctico por strlen, substr, str_replace, strpos, trim, explode/implode y expresiones regulares básicas con preg_match. Aprende a manipular textos como un profesional.</description><content:encoded>Trabajar con texto es una de las cosas que más hacemos en PHP. Ya sea para validar formularios, procesar datos de usuario, construir URLs amigables o simplemente formatear información para mostrar, dominar las funciones de strings te va a ahorrar mucho tiempo y código spaghetti.

En este post te voy a mostrar las funciones que uso constantemente en mi día a día, con ejemplos reales que puedes copiar y adaptar directamente a tus proyectos.

## Longitud de un string con strlen()

Esta es de las más básicas pero fundamentales. `strlen()` nos devuelve la cantidad de caracteres que tiene un string, incluyendo espacios y símbolos.

```php
$nombre = &quot;blivecode&quot;;
$longitud = strlen($nombre);

echo $longitud; // 9
```

**¿Cuándo la uso?** Por ejemplo, cuando necesito validar que una contraseña tenga un mínimo de caracteres o para truncatear texto mostrando &quot;leer más&quot; si el contenido excede cierto límite.

```php
$descripcion = &quot;Este es un texto muy largo que necesito mostrar resumido...&quot;;
$limite = 30;

if (strlen($descripcion) &gt; $limite) {
    echo substr($descripcion, 0, $limite) . &quot;...&quot;;
} else {
    echo $descripcion;
}
```

## Extraer fragmentos con substr()

`substr()` nos permite obtener una porción de un string. Le pasas el string, desde dónde empezar y cuántos caracteres quieres (opcional).

```php
$texto = &quot;Hola mundo&quot;;
echo substr($texto, 0, 4);  // &quot;Hola&quot;
echo substr($texto, 5);      // &quot;mundo&quot;
echo substr($texto, -5);     // &quot;mundo&quot;
```

**Caso de uso real:** Cuando trabajo con URLs amigables o slugs. Por ejemplo, necesito obtener los primeros 3 caracteres de un código de país para hacer una validación rápida.

```php
$codigoPais = &quot;MX&quot;;
$prefijo = substr($codigoPais, 0, 2);

if (strtoupper($prefijo) === &quot;MX&quot;) {
    echo &quot;El producto está disponible para México&quot;;
}
```

## Reemplazar texto con str_replace()

Esta es de mis favoritas. Te permite reemplazar todas las ocurrencias de un texto dentro de un string.

```php
$oracion = &quot;Me encanta programar en PHP&quot;;
$modificada = str_replace(&quot;PHP&quot;, &quot;Laravel&quot;, $oracion);

echo $modificada; // &quot;Me encanta programar en Laravel&quot;
```

Puedes reemplazar varias palabras a la vez pasando arrays:

```php
$texto = &quot;Rojo y azul son colores&quot;;
$buscar = [&quot;Rojo&quot;, &quot;azul&quot;];
$reemplazar = [&quot;Verde&quot;, &quot;amarillo&quot;];

echo str_replace($buscar, $reemplazar, $texto);
// &quot;Verde y amarillo son colores&quot;
```

**Ejemplo práctico:** Normalizar datos importados de Excel donde hay caracteres extraños o espaciado inconsistente.

```php
$importedData = &quot;  Juan    Pérez  &quot;;
$limpio = str_replace([&quot;  &quot;, &quot;  &quot;], &quot; &quot;, trim($importedData));

echo $limpio; // &quot;Juan Pérez&quot;
```

## Buscar posición con strpos()

`strpos()` busca un string dentro de otro y te devuelve la posición donde lo encuentra. Si no lo encuentra, devuelve `false`.

```php
$email = &quot;usuario@ejemplo.com&quot;;
$arroba = strpos($email, &quot;@&quot;);

echo $arroba; // 7
```

**Importante:** Hay que usar `===` para comparar porque `strpos()` puede devolver `0` (que es una posición válida) y `0 == false` sería `true` pero `0 === false` sería `false`.

```php
if (strpos($email, &quot;@&quot;) === false) {
    echo &quot;Email inválido: falta el @&quot;;
} else {
    echo &quot;Email válido&quot;;
}
```

**Caso real:** Detectar el dominio de un email para separar el usuario del servidor.

```php
$email = &quot;contacto@blivecode.com&quot;;
$arrobaPos = strpos($email, &quot;@&quot;);
$usuario = substr($email, 0, $arrobaPos);
$dominio = substr($email, $arrobaPos + 1);

echo &quot;Usuario: $usuario\n&quot;;   // &quot;contacto&quot;
echo &quot;Dominio: $dominio\n&quot;;   // &quot;blivecode.com&quot;
```

## Limpiar espacios con trim(), ltrim() y rtrim()

Estas funciones eliminan espacios en blanco (u otros caracteres) del inicio y/o final de un string. Super útiles para sanitizar inputs de usuario.

```php
$nombre = &quot;   Juan Pérez   &quot;;
echo trim($nombre);   // &quot;Juan Pérez&quot; (ambos lados)
echo ltrim($nombre);  // &quot;Juan Pérez   &quot; (solo izquierda)
echo rtrim($nombre);  // &quot;   Juan Pérez&quot; (solo derecha)
```

También puedes especificar qué caracteres quieres eliminar:

```php
$precio = &quot;$150.00&quot;;
echo trim($precio, &quot;$&quot;); // &quot;150.00&quot;
```

**Aplicación común:** Limpiar datos de formularios antes de guardarlos en la base de datos.

```php
$input = $_POST[&apos;nombre&apos;] ?? &apos;&apos;;
$nombreLimpio = trim($input);

if (empty($nombreLimpio)) {
    echo &quot;El nombre es requerido&quot;;
}
```

## Dividir y unir con explode() e implode()

`explode()` divide un string en un array usando un separador. `implode()` hace lo contrario, une un array en un string.

```php
$frutas = &quot;manzana,pera,uva,mango&quot;;
$arrayFrutas = explode(&quot;,&quot;, $frutas);

print_r($arrayFrutas);
// Array ( [0] =&gt; manzana, [1] =&gt; pera, [2] =&gt; uva, [3] =&gt; mango )
```

```php
$arrayFrutas = [&quot;manzana&quot;, &quot;pera&quot;, &quot;uva&quot;];
$texto = implode(&quot; - &quot;, $arrayFrutas);

echo $texto; // &quot;manzana - pera - uva&quot;
```

**Ejemplo práctico:** Procesar tags separados por comas en un formulario.

```php
$inputTags = &quot;php, mysql, bash, linux&quot;;
$tags = array_map(&apos;trim&apos;, explode(&quot;,&quot;, $inputTags));

print_r($tags);
// Array ( [0] =&gt; php, [1] =&gt; mysql, [2] =&gt; bash, [3] =&gt; linux )
```

## Expresiones regulares con preg_match()

Para búsquedas más complejas, `preg_match()` usa expresiones regulares. Devuelve `1` si encontró coincidencia, `0` si no, y `false` si hubo error.

```php
$texto = &quot;Mi email es test@blivecode.com&quot;;
$patron = &quot;/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/&quot;;

if (preg_match($patron, $texto, $coincidencia)) {
    echo &quot;Email encontrado: &quot; . $coincidencia[0];
}
```

El tercer parámetro `$coincidencia` captura lo que encontró y lo mete en un array.

**Ejemplo real:** Validar que un número de teléfono tenga el formato correcto.

```php
$telefono = &quot;+52 55 1234 5678&quot;;
$patron = &quot;/^\+?[0-9]{1,3}\s?[0-9]{2}\s?[0-9]{4}\s?[0-9]{4}$/&quot;;

if (preg_match($patron, $telefono)) {
    echo &quot;Teléfono válido&quot;;
} else {
    echo &quot;Teléfono con formato inválido&quot;;
}
```

**Otro uso común:** Extraer solo números de un string (útil para limpiar datos).

```php
$precio = &quot;Precio: $150.50 USD&quot;;
preg_match_all(&apos;/[0-9.]+/&apos;, $precio, $coincidencias);

echo $coincidencias[0][0]; // &quot;150.50&quot;
```

## Combinando funciones

La verdadera magia ocurre cuando combinas estas funciones. Mira este ejemplo donde procesamos un CSV crudo:

```php
$csvLine = &quot;  Juan ,  juan@email.com , 35 &quot;;

// Limpiar y dividir
$campos = explode(&quot;,&quot;, $csvLine);
$campos = array_map(&apos;trim&apos;, $campos);

// Validar email
$email = $campos[1];
if (strpos($email, &quot;@&quot;) === false) {
    throw new Exception(&quot;Email inválido&quot;);
}

// Extraer nombre de usuario del email
$usuario = substr($email, 0, strpos($email, &quot;@&quot;));

echo &quot;Nombre: {$campos[0]}, Usuario: $usuario, Edad: {$campos[2]}&quot;;
// &quot;Nombre: Juan, Usuario: juan, Edad: 35&quot;
```

## Funciones adicionales útiles

No cubrire estas pero vale la pena mencionar:

- `strtoupper()` / `strtolower()` - Cambiar mayúsculas/minúsculas
- `ucwords()` - Primera letra de cada palabra en mayúscula
- `str_contains()` (PHP 8+) - Verificar si un string contiene otro
- `str_starts_with()` / `str_ends_with()` (PHP 8+) - Verificar inicio/fin

```php
$titulo = &quot;como crear un api rest&quot;;
echo ucwords($titulo); // &quot;Como Crear Un Api Rest&quot;

// PHP 8+
$url = &quot;https://blivecode.com/blog&quot;;
if (str_starts_with($url, &quot;https://&quot;)) {
    echo &quot;URL segura&quot;;
}
```

## Conclusión

Estas funciones son la base para manipular texto en PHP. No necesitas aprender todas de memoria, pero sí saber que existen y cuándo pueden servirte. Con la práctica vas a empezar a ver patrones donde aplicarlas naturalmente.

Mi recomendación: empieza usando las que más necesites en tu proyecto actual y ve ampliando tu repertorio conforme surjan nuevos casos. La manipulación de strings es de esas habilidades que mejora drasticamente cuanto más la practicas.

¡Espero que te sea útil para tus próximos proyectos!</content:encoded></item></channel></rss>