Ejecutar scripts externos de forma segura en PHP
Automatizar tareas en PHP puede agilizar mucho tu flujo de trabajo, reducir el esfuerzo manual y ampliar las capacidades de tu aplicación. Una forma potente de lograrlo es ejecutar scripts externos personalizados directamente desde tu código PHP. En esta guía veremos cómo puedes ejecutar scripts externos con eficacia, automatizar tareas y mejorar tu automatización en PHP con el componente Symfony Process.
Introducción
Ejecutar scripts externos desde PHP permite a los desarrolladores aprovechar herramientas de línea de comandos existentes, ejecutar scripts escritos en otros lenguajes o realizar operaciones complejas en procesos aislados. Esto resulta útil para automatizar tareas repetitivas, ampliar funciones y simplificar flujos de trabajo complejos dentro de tus aplicaciones PHP.
Requisitos previos
Antes de empezar, asegúrate de tener:
- PHP 8.2 o posterior instalado (Symfony Process 7.x requiere PHP 8.2+)
- Composer instalado para gestionar dependencias
- Familiaridad básica con la escritura de scripts en PHP y con la línea de comandos
- La función
proc_opende PHP habilitada; Symfony Process no requiere las extensiones PCNTL ni POSIX
Configurar el entorno
Primero, crea un nuevo directorio de proyecto e inicializa Composer:
mkdir php-external-scripts
cd php-external-scripts
composer init --name=example/php-external-scripts --description="PHP External Scripts Example" --require="symfony/process:^7.0" --no-interaction
composer install
Usaremos el componente Process de Symfony para ejecutar scripts externos de forma segura y eficiente. Este componente ofrece un envoltorio robusto y orientado a objetos para la gestión de procesos.
Escribir un script personalizado para ejecutarlo
Creemos un sencillo script de PHP llamado hello.php que ejecutaremos desde otro script de PHP:
<?php
// hello.php
$name = $argv[1] ?? 'World';
echo "Hello, {$name}!\n";
Este script acepta un argumento opcional de línea de comandos e imprime un saludo.
Ejecutar el script desde PHP
Crea un nuevo archivo PHP llamado run-script.php para ejecutar tu script personalizado con el componente
Symfony Process:
<?php
// run-script.php
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
$scriptPath = __DIR__ . '/hello.php';
$nameArgument = 'Developer'; // Example argument
// Basic validation: Check if the script exists and is readable
if (!file_exists($scriptPath) || !is_readable($scriptPath)) {
throw new RuntimeException('Script file not found or not readable: ' . $scriptPath);
}
// Create the process instance
// Pass arguments safely as separate array elements
$process = new Process(['php', $scriptPath, $nameArgument]);
// Set timeouts for security and resource management
$process->setTimeout(60); // Maximum execution time (seconds)
$process->setIdleTimeout(30); // Maximum time allowed without any output (seconds)
try {
// Execute the process
$process->mustRun();
// Get the output
echo $process->getOutput();
} catch (ProcessFailedException $exception) {
error_log('Greeting process failed with exit code ' . $process->getExitCode());
throw new RuntimeException('The script execution failed.', 0, $exception);
}
El comando php debe resolverse a un ejecutable de la CLI de PHP que sea de confianza. Usa su
ruta absoluta si tu despliegue tiene un PATH restringido, sobre todo cuando lo invoques desde
PHP-FPM.
Ejecuta este script desde tu terminal:
php run-script.php
Deberías ver la salida de hello.php:
Hello, Developer!
Ejemplos prácticos
Automatizar el procesamiento de archivos
Supongamos que quieres automatizar el redimensionado de imágenes con la herramienta de línea de
comandos ImageMagick. Podrías crear un script de PHP resize-image.php para orquestarlo:
<?php
// resize-image.php
require __DIR__ . '/vendor/autoload.php';
use Symfony\Component\Process\Process;
use Symfony\Component\Process\Exception\ProcessFailedException;
$inputFile = 'input.jpg'; // Potentially user-provided
$outputFile = 'output-' . bin2hex(random_bytes(16)) . '.jpg'; // New name for each run
$targetWidth = 200;
$targetHeight = 200;
// These roots are fixed by the application, never supplied by the caller.
$inputRoot = realpath(__DIR__ . '/private/input');
$outputRoot = realpath(__DIR__ . '/private/output');
if ($inputRoot === false || $outputRoot === false || !is_writable($outputRoot)) {
throw new RuntimeException('Private processing directories are not configured.');
}
// Validate the whole name. Reject paths, leading options, and ImageMagick pseudo-filenames.
$filenamePattern = '/\A[a-zA-Z0-9][a-zA-Z0-9_-]*\.(?:jpg|jpeg|png)\z/i';
if (!preg_match($filenamePattern, $inputFile) || !preg_match($filenamePattern, $outputFile)) {
throw new InvalidArgumentException('Invalid filename provided.');
}
$inputPath = realpath($inputRoot . DIRECTORY_SEPARATOR . $inputFile);
if ($inputPath === false || !is_file($inputPath) || !is_readable($inputPath) ||
!str_starts_with($inputPath, $inputRoot . DIRECTORY_SEPARATOR)) {
throw new RuntimeException('Input file is not available in the trusted directory.');
}
$outputPath = $outputRoot . DIRECTORY_SEPARATOR . $outputFile;
if (file_exists($outputPath) || is_link($outputPath)) {
throw new RuntimeException('Output already exists. Choose a new output name.');
}
// Use modern ImageMagick `magick` command
// **Security**: Pass arguments as separate array elements to Process constructor
// This avoids shell injection vulnerabilities; escapeshellarg() is not needed here.
$command = [
'magick',
$inputPath,
'-resize',
"{$targetWidth}x{$targetHeight}",
$outputPath
];
$process = new Process($command);
$process->setTimeout(120); // Longer timeout for potentially slow image processing
// ImageMagick can be quiet while working; rely on the total timeout here.
try {
echo "Attempting to resize '{$inputFile}' to '{$outputFile}'...\n";
$process->mustRun();
echo "Image resized successfully!\n";
} catch (ProcessFailedException $exception) {
error_log('Image resizing failed with exit code ' . $process->getExitCode());
throw new RuntimeException('The image resizing command failed.', 0, $exception);
}
Asegúrate de tener ImageMagick instalado y de que el comando magick esté disponible en el
PATH de confianza de tu sistema. Crea private/input y private/output fuera del webroot y coloca un
archivo JPEG real en private/input/input.jpg. Mantén ambos directorios bajo el control de la aplicación para
que usuarios no confiables no puedan reemplazar archivos ni enlaces simbólicos entre la validación
y la ejecución. La salida no debe existir ya. Cada ejecución genera un nuevo nombre de archivo de
salida, por lo que las ejecuciones programadas no sobrescriben los resultados anteriores. Aplica
una política de retención a estas salidas, y las políticas de recursos y de seguridad de
ImageMagick para imágenes no confiables.
Tareas programadas
Puedes usar cron jobs (en Linux/macOS) o el Programador de tareas (en Windows) para programar la ejecución de tus scripts de PHP que lanzan otros procesos.
Ejemplo de entrada de cron job (edítala con crontab -e):
# Run the image resizing script every hour
0 * * * * /usr/bin/php /full/path/to/your/project/resize-image.php >> /full/path/to/your/logs/resize.log 2>&1
Sustituye /usr/bin/php por la ruta correcta a tu ejecutable de PHP y usa rutas absolutas para tu
script y tu archivo de registro. La parte >> ... 2>&1 añade tanto la salida estándar como el error
estándar al archivo de registro.
Manejo de errores y buenas prácticas
Consideraciones de seguridad
- Validación de entradas: nunca confíes directamente en entradas externas (como nombres de archivo o argumentos pasados a los scripts). Valida y sanea siempre las entradas con rigor. Usa expresiones regulares o listas de permitidos específicas para los nombres de archivo. Comprueba la existencia del archivo y sus permisos.
- Evita la ejecución mediante shell: siempre que sea posible, usa
Processcon un array de partes del comando (por ejemplo,new Process(['command', 'arg1', 'arg2'])) en lugar de pasar una sola cadena de comando. Esto evita el shell y previene la inyección de shell, de modo que funciones comoescapeshellarg()oescapeshellcmd()resultan innecesarias para los propios argumentos. Si tienes que ejecutar un comando a través del shell, ten muchísimo cuidado con el escapado. Los arrays de argumentos no previenen la inyección de opciones ni la sintaxis de nombres de archivo propia de una herramienta; valida esos valores por separado. - Establece tiempos de espera: define un tiempo de espera total de ejecución (
setTimeout). UsasetIdleTimeoutsolo para herramientas de las que esperas una salida regular; de lo contrario, un proceso sano pero silencioso puede terminarse. - Limita los permisos: ejecuta tus scripts de PHP y los procesos que invocan con los mínimos privilegios necesarios. Evita ejecutarlos como root.
Manejo integral de errores
Registra la operación y el código de salida, y mantén fuera de los registros los argumentos del comando y la salida sin filtrar cuando puedan contener secretos o contenido no confiable. En una aplicación web, devuelve un error fijo para el usuario y conserva las excepciones y los rastros de pila en el servidor. Este extracto reutiliza los imports anteriores:
<?php
try {
$process->mustRun();
// Process finished successfully
$output = $process->getOutput();
// Log success or process output
} catch (ProcessFailedException $exception) {
error_log('Exit Code: ' . $exception->getProcess()->getExitCode());
// Implement alerting or recovery logic here
throw $exception; // Re-throw if needed
} catch (Exception $e) {
// Catch other potential exceptions (e.g., LogicException from Process)
error_log('Process error: ' . get_class($e));
throw $e;
}
Gestión de recursos
Para scripts de larga duración o que consumen muchos recursos:
-
Límites de memoria: pasa la directiva
memory_limitde PHP con-dal crear un proceso hijo de PHP. Esto limita la memoria gestionada por PHP; no restringe a ImageMagick ni a otros programas.<?php $process = new Process(['php', '-d', 'memory_limit=256M', $scriptPath, $nameArgument]); -
Procesos en segundo plano: para tareas que no necesitan resultados inmediatos, ejecútalas de forma asíncrona.
<?php $process->start(); // Do independent work here, then wait before inspecting the result or exiting. $process->wait(); if (!$process->isSuccessful()) { throw new ProcessFailedException($process); }start()es asíncrono dentro del tiempo de vida de este proceso de PHP. Para trabajo que deba sobrevivir a la salida del proceso padre, usa un worker de cola o un supervisor de servicios; el destructor de Process detiene un proceso hijo en ejecución.
Conclusión
Ejecutar scripts externos desde PHP con el componente Symfony Process es una técnica potente para automatizar tareas, integrarte con otras herramientas y ampliar las capacidades de tus aplicaciones PHP. Si sigues las buenas prácticas de seguridad (validación de entradas, evitar la ejecución mediante shell, establecer tiempos de espera) e implementas un manejo de errores robusto, podrás incorporar de forma fiable la ejecución de scripts externos en tus flujos de trabajo.
Estas técnicas te permiten construir sistemas de automatización sofisticados, agilizar el desarrollo y reducir el esfuerzo manual en tus proyectos de PHP.
Para flujos de trabajo complejos de procesamiento de medios y automatización, también puedes explorar servicios como Transloadit, que ofrecen infraestructura gestionada para tareas como el encoding de video y la manipulación de imágenes mediante API.
