1. Profundizando
  2. Procesos

Introducción

Laravel proporciona una API expresiva y minimalista alrededor del componente de proceso de Symfony, lo que te permite invocar convenientemente procesos externos desde tu aplicación Laravel. Las características de procesos de Laravel se centran en los casos de uso más comunes y ofrecen una experiencia de desarrollador maravillosa.

Invocación de Procesos

Para invocar un proceso, puedes usar los métodos run y start ofrecidos por la fachada Process. El método run invocará un proceso y esperará a que el proceso termine de ejecutarse, mientras que el método start se utiliza para la ejecución asíncrona de procesos. Examinaremos ambos enfoques en esta documentación. Primero, examinemos cómo invocar un proceso básico y síncrono e inspeccionar su resultado:

use Illuminate\Support\Facades\Process;
 
$result = Process::run('ls -la');
 
return $result->output();

Por supuesto, la instancia de Illuminate\Contracts\Process\ProcessResult devuelta por el método run ofrece una variedad de métodos útiles que se pueden usar para inspeccionar el resultado del proceso:

$result = Process::run('ls -la');
 
$result->successful();
$result->failed();
$result->exitCode();
$result->output();
$result->errorOutput();

Lanzar Excepciones

Si tienes un resultado de proceso y te gustaría lanzar una instancia de Illuminate\Process\Exceptions\ProcessFailedException si el código de salida es mayor que cero (indicando así un fallo), puedes usar los métodos throw y throwIf. Si el proceso no falló, se devolverá la instancia del resultado del proceso:

$result = Process::run('ls -la')->throw();
 
$result = Process::run('ls -la')->throwIf($condition);

Opciones de Proceso

Por supuesto, es posible que necesites personalizar el comportamiento de un proceso antes de invocarlo. Afortunadamente, Laravel te permite ajustar varias características de los procesos, como el directorio de trabajo, el tiempo de espera y las variables de entorno.

Ruta del Directorio de Trabajo

Puedes usar el método path para especificar el directorio de trabajo del proceso. Si no se invoca este método, el proceso heredará el directorio de trabajo del script PHP que se está ejecutando actualmente:

$result = Process::path(__DIR__)->run('ls -la');

Entrada

Puedes proporcionar entrada a través de la "entrada estándar" del proceso mediante el método input:

$result = Process::input('Hello World')->run('cat');

Tiempos de Espera

Por defecto, los procesos arrojarán una instancia de Illuminate\Process\Exceptions\ProcessTimedOutException después de ejecutarse durante más de 60 segundos. Sin embargo, puedes personalizar este comportamiento mediante el método timeout:

$result = Process::timeout(120)->run('bash import.sh');

O, si deseas desactivar por completo el tiempo de espera del proceso, puedes invocar el método forever:

$result = Process::forever()->run('bash import.sh');

El método idleTimeout se puede utilizar para especificar el número máximo de segundos que el proceso puede ejecutarse sin devolver ninguna salida:

$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');

Variables de Entorno

Las variables de entorno se pueden proporcionar al proceso mediante el método env. El proceso invocado también heredará todas las variables de entorno definidas por su sistema:

$result = Process::forever()
->env(['IMPORT_PATH' => __DIR__])
->run('bash import.sh');

Si desea eliminar una variable de entorno heredada del proceso invocado, puede proporcionar esa variable de entorno con un valor de false:

$result = Process::forever()
->env(['LOAD_PATH' => false])
->run('bash import.sh');

Modo TTY

El método tty se puede utilizar para habilitar el modo TTY para su proceso. El modo TTY conecta la entrada y salida del proceso a la entrada y salida de su programa, lo que permite que su proceso abra un editor como Vim o Nano como un proceso:

Process::forever()->tty()->run('vim');

Salida de Proceso

Como se discutió anteriormente, la salida del proceso se puede acceder utilizando los métodos output (stdout) y errorOutput (stderr) en un resultado del proceso:

use Illuminate\Support\Facades\Process;
 
$result = Process::run('ls -la');
 
echo $result->output();
echo $result->errorOutput();

Sin embargo, la salida también se puede recopilar en tiempo real pasando un cierre como segundo argumento al método run. El cierre recibirá dos argumentos: el "tipo" de salida (stdout o stderr) y la cadena de salida en sí:

$result = Process::run('ls -la', function (string $type, string $output) {
echo $output;
});

Laravel también ofrece los métodos seeInOutput y seeInErrorOutput, que proporcionan una forma conveniente de determinar si una cadena dada estaba contenida en la salida del proceso:

if (Process::run('ls -la')->seeInOutput('laravel')) {
// ...
}

Desactivación de la Salida de Proceso

A veces es posible que desee hacer que la salida de un proceso sea la entrada de otro proceso. Esto se conoce comúnmente como "piping" (tubería) la salida de un proceso en otro. El método pipe proporcionado por la fachada Process facilita esto. El método pipe ejecutará los procesos conectados de forma síncrona y devolverá el resultado del proceso para el último proceso en la tubería:

use Illuminate\Support\Facades\Process;
 
$result = Process::quietly()->run('bash import.sh');

Tuberías

A veces es posible que desee hacer que la salida de un proceso sea la entrada de otro proceso. Esto se conoce comúnmente como "piping" (tubería) la salida de un proceso en otro. El método pipe proporcionado por la fachada Process facilita esto. El método pipe ejecutará los procesos conectados de forma síncrona y devolverá el resultado del proceso para el último proceso en la tubería:

use Illuminate\Process\Pipe;
use Illuminate\Support\Facades\Process;
 
$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
});
 
if ($result->successful()) {
// ...
}

Si no necesita personalizar los procesos individuales que componen la tubería, simplemente puede pasar una matriz de cadenas de comandos al método pipe:

$result = Process::pipe([
'cat example.txt',
'grep -i "laravel"',
]);

La salida del proceso se puede recopilar en tiempo real pasando un cierre como segundo argumento al método pipe. El cierre recibirá dos argumentos: el "tipo" de salida (stdout o stderr) y la cadena de salida en sí:

$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
}, function (string $type, string $output) {
echo $output;
});

Laravel también le permite asignar claves de cadena a cada proceso dentro de una tubería mediante el método as. Esta clave también se pasará al cierre proporcionado al método pipe, lo que le permitirá determinar a qué proceso pertenece la salida:

$result = Process::pipe(function (Pipe $pipe) {
$pipe->as('first')->command('cat example.txt');
$pipe->as('second')->command('grep -i "laravel"');
})->start(function (string $type, string $output, string $key) {
// ...
});

Procesos Asíncronos

Mientras que el método run invoca procesos de forma síncrona, el método start se puede utilizar para invocar un proceso de forma asíncrona. Esto permite que su aplicación continúe realizando otras tareas mientras el proceso se ejecuta en segundo plano. Una vez que se ha invocado el proceso, puede utilizar el método running para determinar si el proceso aún se está ejecutando:

$process = Process::timeout(120)->start('bash import.sh');
 
while ($process->running()) {
// ...
}
 
$result = $process->wait();

Como habrás notado, puedes invocar el método wait para esperar hasta que el proceso haya terminado de ejecutarse y obtener la instancia del resultado del proceso:

$process = Process::timeout(120)->start('bash import.sh');
 
// ...
 
$result = $process->wait();

IDs y Señales de Proceso

El método id se puede utilizar para recuperar el ID de proceso asignado por el sistema operativo del proceso en ejecución:

$process = Process::start('bash import.sh');
 
return $process->id();

Puede usar el método signal para enviar una "señal" al proceso en ejecución. Una lista de constantes de señal predefinidas se puede encontrar en la documentación de PHP:

$process->signal(SIGUSR2);

Salida de Proceso Asíncrono

Mientras un proceso asíncrono está en ejecución, puede acceder a toda su salida actual utilizando los métodos output y errorOutput; sin embargo, puede utilizar latestOutput y latestErrorOutput para acceder a la salida del proceso que ha ocurrido desde que se recuperó la salida por última vez:

$process = Process::timeout(120)->start('bash import.sh');
 
while ($process->running()) {
echo $process->latestOutput();
echo $process->latestErrorOutput();
 
sleep(1);
}

Al igual que el método run, la salida también se puede recopilar en tiempo real a partir de procesos asíncronos pasando un cierre como segundo argumento al método start. El cierre recibirá dos argumentos: el "tipo" de salida (stdout o stderr) y la cadena de salida en sí:

$process = Process::start('bash import.sh', function (string $type, string $output) {
echo $output;
});
 
$result = $process->wait();

Procesos Concurrentes

Laravel también facilita la gestión de un grupo de procesos asíncronos concurrentes, lo que le permite ejecutar fácilmente muchas tareas simultáneamente. Para comenzar, invoque el método pool, que acepta un cierre que recibe una instancia de Illuminate\Process\Pool.

Dentro de este cierre, puede definir los procesos que pertenecen al grupo. Una vez que se inicia un grupo de procesos mediante el método start, puede acceder a la colección de procesos en ejecución mediante el método running:

use Illuminate\Process\Pool;
use Illuminate\Support\Facades\Process;
 
$pool = Process::pool(function (Pool $pool) {
$pool->path(__DIR__)->command('bash import-1.sh');
$pool->path(__DIR__)->command('bash import-2.sh');
$pool->path(__DIR__)->command('bash import-3.sh');
})->start(function (string $type, string $output, int $key) {
// ...
});
 
while ($pool->running()->isNotEmpty()) {
// ...
}
 
$results = $pool->wait();

Como puedes ver, puedes esperar a que todos los procesos del grupo terminen de ejecutarse y resuelvan sus resultados mediante el método wait. El método wait devuelve un objeto accesible por matriz que te permite acceder a la instancia del resultado del proceso de cada proceso en el grupo mediante su clave:

$results = $pool->wait();
 
echo $results[0]->output();

O, para mayor comodidad, se puede utilizar el método concurrently para iniciar un grupo de procesos asíncronos y esperar inmediatamente sus resultados. Esto puede proporcionar una sintaxis particularmente expresiva cuando se combina con las capacidades de desestructuración de matrices de PHP:

[$first, $second, $third] = Process::concurrently(function (Pool $pool) {
$pool->path(__DIR__)->command('ls -la');
$pool->path(app_path())->command('ls -la');
$pool->path(storage_path())->command('ls -la');
});
 
echo $first->output();

Nombres de Procesos en Pools

Acceder a los resultados del grupo de procesos mediante una clave numérica no es muy expresivo; por lo tanto, Laravel te permite asignar claves de cadena a cada proceso dentro de un grupo mediante el método as. Esta clave también se pasará al cierre proporcionado al método start, lo que te permitirá determinar a qué proceso pertenece la salida:

$pool = Process::pool(function (Pool $pool) {
$pool->as('first')->command('bash import-1.sh');
$pool->as('second')->command('bash import-2.sh');
$pool->as('third')->command('bash import-3.sh');
})->start(function (string $type, string $output, string $key) {
// ...
});
 
$results = $pool->wait();
 
return $results['first']->output();

IDs y Señales de Procesos en Pools

Dado que el método running del grupo de procesos proporciona una colección de todos los procesos invocados dentro del grupo, puedes acceder fácilmente a los ID de proceso del grupo:

$processIds = $pool->running()->each->id();

Y, para mayor comodidad, puedes invocar el método signal en un grupo de procesos para enviar una señal a cada proceso dentro del grupo:

$pool->signal(SIGUSR2);

Pruebas

Muchos servicios de Laravel proporcionan funcionalidades que te ayudan a escribir pruebas fácil y expresivamente, y el servicio de procesos de Laravel no es una excepción. El método fake de la fachada Process te permite indicarle a Laravel que devuelva resultados simulados / ficticios cuando se invocan procesos.

Falsificación de Procesos

Para explorar la capacidad de Laravel para simular procesos, imaginemos una ruta que invoca un proceso:

use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Route;
 
Route::get('/import', function () {
Process::run('bash import.sh');
 
return 'Import complete!';
});

Al probar esta ruta, podemos indicarle a Laravel que devuelva un resultado de proceso falso y exitoso para cada proceso invocado llamando al método fake en la fachada Process sin argumentos. Además, incluso podemos afirmar que se "ejecutó" un proceso específico:

<?php
 
namespace Tests\Feature;
 
use Illuminate\Process\PendingProcess;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Support\Facades\Process;
use Tests\TestCase;
 
class ExampleTest extends TestCase
{
public function test_process_is_invoked(): void
{
Process::fake();
 
$response = $this->get('/');
 
// Afirmación de proceso simple...
Process::assertRan('bash import.sh');
 
// O, inspeccionando la configuración del proceso...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
}
}

Como se discutió, invocar el método fake en la fachada Process indicará a Laravel que siempre devuelva un resultado exitoso del proceso sin salida. Sin embargo, puede especificar fácilmente la salida y el código de salida para los procesos falsificados utilizando el método result de la fachada Process:

Process::fake([
'*' => Process::result(
output: 'Test output',
errorOutput: 'Test error output',
exitCode: 1,
),
]);

Falsificación de Procesos Específicos

Como puedes haber notado en un ejemplo anterior, la fachada Process te permite especificar resultados falsos diferentes por proceso al pasar una matriz al método fake.

Las claves de la matriz deben representar patrones de comando que deseas falsificar y sus resultados asociados. El carácter * se puede utilizar como un comodín. Cualquier comando de proceso que no haya sido falsificado realmente se invocará. Puedes usar el método result de la fachada Process para construir resultados simulados / falsos para estos comandos:

Process::fake([
'cat *' => Process::result(
output: 'Test "cat" output',
),
'ls *' => Process::result(
output: 'Test "ls" output',
),
]);

Si no necesitas personalizar el código de salida o la salida de error de un proceso falsificado, puede resultar más conveniente especificar los resultados falsos del proceso como cadenas simples:

Process::fake([
'cat *' => 'Test "cat" output',
'ls *' => 'Test "ls" output',
]);

Falsificación de Secuencias de Procesos

Si el código que estás probando invoca múltiples procesos con el mismo comando, es posible que desees asignar un resultado falso de proceso diferente a cada invocación del proceso. Puedes lograr esto mediante el método sequence de la fachada Process:

Process::fake([
'ls *' => Process::sequence()
->push(Process::result('First invocation'))
->push(Process::result('Second invocation')),
]);

Falsificación de Ciclos de Vida de Procesos Asíncronos

Hasta ahora, hemos hablado principalmente de falsificar procesos que se invocan de forma síncrona mediante el método run. Sin embargo, si estás intentando probar código que interactúa con procesos asíncronos invocados mediante start, es posible que necesites un enfoque más sofisticado para describir tus procesos falsos.

Por ejemplo, imaginemos la siguiente ruta que interactúa con un proceso asíncrono:

use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
 
Route::get('/import', function () {
$process = Process::start('bash import.sh');
 
while ($process->running()) {
Log::info($process->latestOutput());
Log::info($process->latestErrorOutput());
}
 
return 'Done';
});

Para falsificar correctamente este proceso, necesitamos poder describir cuántas veces debería devolver true el método running. Además, es posible que deseemos especificar varias líneas de salida que se devolverán en secuencia. Para lograr esto, podemos usar el método describe de la fachada Process:

Process::fake([
'bash import.sh' => Process::describe()
->output('First line of standard output')
->errorOutput('First line of error output')
->output('Second line of standard output')
->exitCode(0)
->iterations(3),
]);

Profundicemos en el ejemplo anterior. Utilizando los métodos output y errorOutput, podemos especificar varias líneas de salida que se devolverán en secuencia. El método exitCode se puede utilizar para especificar el código de salida final del proceso falso. Finalmente, el método iterations se puede utilizar para especificar cuántas veces debería devolver true el método running.

Afirmaciones Disponibles

Como se discutió anteriormente, Laravel proporciona varias afirmaciones de procesos para tus pruebas de funciones. Discutiremos cada una de estas afirmaciones a continuación.

assertRan

Afirmar que se invocó un proceso específico:

use Illuminate\Support\Facades\Process;
 
Process::assertRan('ls -la');

El método assertRan también acepta un cierre, que recibirá una instancia de un proceso y un resultado del proceso, permitiéndote inspeccionar las opciones configuradas para el proceso. Si este cierre devuelve true, la afirmación "pasará":

Process::assertRan(fn ($process, $result) =>
$process->command === 'ls -la' &&
$process->path === __DIR__ &&
$process->timeout === 60
);

El $process pasado al cierre assertRan es una instancia de Illuminate\Process\PendingProcess, mientras que $result es una instancia de Illuminate\Contracts\Process\ProcessResult.

assertDidntRun

Afirmar que no se invocó un proceso específico:

use Illuminate\Support\Facades\Process;
 
Process::assertDidntRun('ls -la');

Al igual que el método assertRan, el método assertDidntRun también acepta un cierre, que recibirá una instancia de un proceso y un resultado del proceso, permitiéndote inspeccionar las opciones configuradas para el proceso. Si este cierre devuelve true, la afirmación "fallará":

Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
$process->command === 'ls -la'
);

assertRanTimes

Afirmar que se invocó un proceso específico un número específico de veces:

use Illuminate\Support\Facades\Process;
 
Process::assertRanTimes('ls -la', times: 3);

El método assertRanTimes también acepta un cierre, que recibirá una instancia de un proceso y un resultado del proceso, permitiéndote inspeccionar las opciones configuradas para el proceso. Si este cierre devuelve true y el proceso se invocó el número especificado de veces, la afirmación "pasará":

Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'ls -la';
}, times: 3);

Prevención de Procesos Descontrolados

Si deseas asegurarte de que todos los procesos invocados se hayan falsificado en tu prueba individual o suite de pruebas completa, puedes llamar al método preventStrayProcesses. Después de llamar a este método, cualquier proceso que no tenga un resultado falso correspondiente arrojará una excepción en lugar de iniciar un proceso real:

use Illuminate\Support\Facades\Process;
 
Process::preventStrayProcesses();
 
Process::fake([
'ls *' => 'Test output...',
]);
 
// Se devuelve una respuesta falsa...
Process::run('ls -la');
 
// Se lanza una excepción...
Process::run('bash import.sh');