#Введение
Laravel предоставляет выразительный, минималистичный API поверх Symfony Process component, позволяющий удобно вызывать внешние процессы из вашего Laravel-приложения. Функции работы с процессами в Laravel ориентированы на самые распространённые сценарии и обеспечивают отличный опыт разработчика.
#Вызов процессов
Для вызова процесса вы можете использовать методы run и start, предоставляемые фасадом Process. Метод run запускает процесс и ожидает его завершения, тогда как start используется для асинхронного выполнения процесса. В этой документации мы рассмотрим оба подхода. Сначала изучим, как вызвать простой синхронный процесс и проверить его результат:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
return $result->output();
Разумеется, экземпляр Illuminate\Contracts\Process\ProcessResult, возвращаемый методом run, предоставляет множество полезных методов для анализа результата процесса:
$result = Process::run('ls -la');
$result->successful();
$result->failed();
$result->exitCode();
$result->output();
$result->errorOutput();
#Генерация исключений
Если у вас есть результат процесса и вы хотите выбросить исключение Illuminate\Process\Exceptions\ProcessFailedException, если код выхода больше нуля (что означает ошибку), вы можете использовать методы throw и throwIf. Если процесс не завершился с ошибкой, будет возвращён экземпляр результата процесса:
$result = Process::run('ls -la')->throw();
$result = Process::run('ls -la')->throwIf($condition);
#Опции процесса
Конечно, перед вызовом процесса может потребоваться настроить его поведение. К счастью, Laravel позволяет изменять различные параметры процесса, такие как рабочая директория, таймаут и переменные окружения.
#Путь рабочей директории
Вы можете использовать метод path для указания рабочей директории процесса. Если этот метод не вызван, процесс унаследует рабочую директорию текущего PHP-скрипта:
$result = Process::path(__DIR__)->run('ls -la');
#Ввод
Вы можете передать ввод через «стандартный ввод» процесса с помощью метода input:
$result = Process::input('Hello World')->run('cat');
#Таймауты
По умолчанию процессы выбрасывают исключение Illuminate\Process\Exceptions\ProcessTimedOutException, если выполняются более 60 секунд. Однако вы можете настроить это поведение с помощью метода timeout:
$result = Process::timeout(120)->run('bash import.sh');
Или, если хотите полностью отключить таймаут процесса, вызовите метод forever:
$result = Process::forever()->run('bash import.sh');
Метод idleTimeout позволяет указать максимальное количество секунд, в течение которых процесс может работать без вывода данных:
$result = Process::timeout(60)->idleTimeout(30)->run('bash import.sh');
#Переменные окружения
Переменные окружения можно передать процессу через метод env. Вызываемый процесс также унаследует все переменные окружения вашей системы:
$result = Process::forever()
->env(['IMPORT_PATH' => __DIR__])
->run('bash import.sh');
Если вы хотите удалить унаследованную переменную окружения из вызываемого процесса, укажите для неё значение false:
$result = Process::forever()
->env(['LOAD_PATH' => false])
->run('bash import.sh');
#Режим TTY
Метод tty позволяет включить режим TTY для вашего процесса. В этом режиме ввод и вывод процесса подключаются к вводу и выводу вашей программы, что позволяет процессу открыть редактор, например Vim или Nano:
Process::forever()->tty()->run('vim');
#Вывод процесса
Как уже упоминалось, вывод процесса можно получить с помощью методов output (stdout) и errorOutput (stderr) у результата процесса:
use Illuminate\Support\Facades\Process;
$result = Process::run('ls -la');
echo $result->output();
echo $result->errorOutput();
Однако вывод также можно получать в реальном времени, передавая замыкание вторым аргументом методу run. Замыкание получит два аргумента: тип вывода (stdout или stderr) и саму строку вывода:
$result = Process::run('ls -la', function (string $type, string $output) {
echo $output;
});
Laravel также предоставляет методы seeInOutput и seeInErrorOutput, которые удобны для проверки наличия заданной строки в выводе процесса:
if (Process::run('ls -la')->seeInOutput('laravel')) {
// ...
}
#Отключение вывода процесса
Если ваш процесс генерирует большой объём вывода, который вам не нужен, вы можете сэкономить память, полностью отключив получение вывода. Для этого вызовите метод quietly при построении процесса:
use Illuminate\Support\Facades\Process;
$result = Process::quietly()->run('bash import.sh');
#Конвейеры
Иногда нужно сделать вывод одного процесса вводом для другого. Это часто называют «конвейером» вывода одного процесса в другой. Метод pipe, предоставляемый фасадом Process, упрощает эту задачу. Метод pipe выполнит процессы конвейера синхронно и вернёт результат последнего процесса в цепочке:
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()) {
// ...
}
Если вам не нужно настраивать отдельные процессы в конвейере, вы можете просто передать массив строк команд методу pipe:
$result = Process::pipe([
'cat example.txt',
'grep -i "laravel"',
]);
Вывод процесса можно получать в реальном времени, передавая замыкание вторым аргументом методу pipe. Замыкание получит два аргумента: тип вывода (stdout или stderr) и строку вывода:
$result = Process::pipe(function (Pipe $pipe) {
$pipe->command('cat example.txt');
$pipe->command('grep -i "laravel"');
}, function (string $type, string $output) {
echo $output;
});
Laravel также позволяет назначать строковые ключи каждому процессу в конвейере через метод as. Этот ключ будет передан в замыкание вывода, передаваемое методу pipe, что позволит определить, к какому процессу относится вывод:
$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) {
// ...
});
#Асинхронные процессы
В то время как метод run вызывает процессы синхронно, метод start позволяет запустить процесс асинхронно. Это даёт вашему приложению возможность выполнять другие задачи, пока процесс работает в фоне. После запуска процесса вы можете использовать метод running, чтобы проверить, выполняется ли процесс:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
// ...
}
$result = $process->wait();
Как вы могли заметить, метод wait позволяет дождаться завершения процесса и получить экземпляр результата процесса:
$process = Process::timeout(120)->start('bash import.sh');
// ...
$result = $process->wait();
#ID процессов и сигналы
Метод id позволяет получить идентификатор процесса, назначенный операционной системой, для запущенного процесса:
$process = Process::start('bash import.sh');
return $process->id();
Метод signal позволяет отправить «сигнал» запущенному процессу. Список предопределённых констант сигналов можно найти в документации PHP:
$process->signal(SIGUSR2);
#Вывод асинхронных процессов
Пока асинхронный процесс выполняется, вы можете получить весь текущий вывод с помощью методов output и errorOutput; однако методы latestOutput и latestErrorOutput позволяют получить вывод, появившийся с момента последнего запроса вывода:
$process = Process::timeout(120)->start('bash import.sh');
while ($process->running()) {
echo $process->latestOutput();
echo $process->latestErrorOutput();
sleep(1);
}
Как и метод run, вывод можно получать в реальном времени из асинхронных процессов, передавая замыкание вторым аргументом методу start. Замыкание получит два аргумента: тип вывода (stdout или stderr) и строку вывода:
$process = Process::start('bash import.sh', function (string $type, string $output) {
echo $output;
});
$result = $process->wait();
#Параллельные процессы
Laravel также упрощает управление пулом параллельных асинхронных процессов, позволяя легко выполнять множество задач одновременно. Для начала вызовите метод pool, который принимает замыкание с экземпляром Illuminate\Process\Pool.
Внутри этого замыкания вы можете определить процессы, входящие в пул. После запуска пула процессов методом start вы можете получить коллекцию запущенных процессов через метод 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();
Как видите, вы можете дождаться завершения всех процессов пула и получить их результаты через метод wait. Метод wait возвращает объект, доступный как массив, позволяющий получить результат каждого процесса пула по его ключу:
$results = $pool->wait();
echo $results[0]->output();
Для удобства метод concurrently позволяет запустить пул асинхронных процессов и сразу дождаться их результатов. Это особенно удобно в сочетании с возможностями деструктуризации массивов в 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();
#Именование процессов пула
Доступ к результатам пула процессов по числовому ключу не очень информативен; поэтому Laravel позволяет назначать строковые ключи каждому процессу в пуле через метод as. Этот ключ также передаётся в замыкание, передаваемое методу start, что позволяет определить, к какому процессу относится вывод:
$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();
#ID процессов пула и сигналы
Поскольку метод running пула процессов возвращает коллекцию всех запущенных процессов в пуле, вы можете легко получить ID этих процессов:
$processIds = $pool->running()->each->id();
Для удобства вы можете вызвать метод signal у пула процессов, чтобы отправить сигнал всем процессам в пуле:
$pool->signal(SIGUSR2);
#Тестирование
Многие сервисы Laravel предоставляют функциональность для удобного и выразительного написания тестов, и сервис процессов Laravel не исключение. Метод fake фасада Process позволяет указать Laravel возвращать заглушки / фиктивные результаты при вызове процессов.
#Фейковые процессы
Чтобы изучить возможность фейка процессов в Laravel, представим маршрут, который вызывает процесс:
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Route;
Route::get('/import', function () {
Process::run('bash import.sh');
return 'Import complete!';
});
При тестировании этого маршрута мы можем указать Laravel возвращать фиктивный успешный результат для каждого вызванного процесса, вызвав метод fake фасада Process без аргументов. Кроме того, мы можем даже проверить, что определённый процесс был «запущен»:
<?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('/import');
// Простая проверка вызова процесса...
Process::assertRan('bash import.sh');
// Или проверка конфигурации процесса...
Process::assertRan(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'bash import.sh' &&
$process->timeout === 60;
});
}
}
Как уже обсуждалось, вызов метода fake фасада Process заставит Laravel всегда возвращать успешный результат процесса без вывода. Однако вы можете легко указать вывод и код выхода для фейковых процессов с помощью метода result фасада Process:
Process::fake([
'*' => Process::result(
output: 'Test output',
errorOutput: 'Test error output',
exitCode: 1,
),
]);
#Фейковые конкретные процессы
Как вы могли заметить в предыдущем примере, фасад Process позволяет задавать разные фейковые результаты для каждого процесса, передавая массив в метод fake.
Ключи массива должны представлять шаблоны команд, которые вы хотите подделать, и соответствующие им результаты. Символ * может использоваться как подстановочный знак. Любые команды процессов, не подделанные, будут действительно вызваны. Вы можете использовать метод result фасада Process для создания заглушек / фейковых результатов для этих команд:
Process::fake([
'cat *' => Process::result(
output: 'Test "cat" output',
),
'ls *' => Process::result(
output: 'Test "ls" output',
),
]);
Если вам не нужно настраивать код выхода или вывод ошибок фейкового процесса, удобнее указать фейковые результаты в виде простых строк:
Process::fake([
'cat *' => 'Test "cat" output',
'ls *' => 'Test "ls" output',
]);
#Фейковые последовательности процессов
Если тестируемый код вызывает несколько процессов с одной и той же командой, вы можете захотеть назначить разные фейковые результаты для каждого вызова процесса. Это можно сделать с помощью метода sequence фасада Process:
Process::fake([
'ls *' => Process::sequence()
->push(Process::result('First invocation'))
->push(Process::result('Second invocation')),
]);
#Фейковые жизненные циклы асинхронных процессов
До сих пор мы в основном рассматривали фейковые процессы, вызываемые синхронно через метод run. Однако если вы тестируете код, взаимодействующий с асинхронными процессами, запущенными через start, может потребоваться более сложный подход к описанию фейков.
Например, представим следующий маршрут, который взаимодействует с асинхронным процессом:
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';
});
Чтобы корректно подделать этот процесс, нужно описать, сколько раз метод running должен возвращать true. Кроме того, можно указать несколько строк вывода, которые будут возвращаться последовательно. Для этого используется метод describe фасада 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),
]);
Рассмотрим пример выше. С помощью методов output и errorOutput можно указать несколько строк вывода, которые будут возвращаться последовательно. Метод exitCode задаёт конечный код выхода фейкового процесса. Наконец, метод iterations указывает, сколько раз метод running должен возвращать true.
#Доступные утверждения
Как обсуждалось ранее, Laravel предоставляет несколько утверждений для тестирования процессов. Ниже мы рассмотрим каждое из них.
#assertRan
Проверяет, что указанный процесс был вызван:
use Illuminate\Support\Facades\Process;
Process::assertRan('ls -la');
Метод assertRan также принимает замыкание, которое получает экземпляры процесса и результата процесса, позволяя проверить настройки процесса. Если замыкание возвращает true, утверждение считается успешным:
Process::assertRan(fn ($process, $result) =>
$process->command === 'ls -la' &&
$process->path === __DIR__ &&
$process->timeout === 60
);
Параметр $process, передаваемый в замыкание assertRan, является экземпляром Illuminate\Process\PendingProcess, а $result — экземпляром Illuminate\Contracts\Process\ProcessResult.
#assertDidntRun
Проверяет, что указанный процесс не был вызван:
use Illuminate\Support\Facades\Process;
Process::assertDidntRun('ls -la');
Как и метод assertRan, assertDidntRun принимает замыкание с экземплярами процесса и результата процесса для проверки настроек. Если замыкание возвращает true, утверждение считается проваленным:
Process::assertDidntRun(fn (PendingProcess $process, ProcessResult $result) =>
$process->command === 'ls -la'
);
#assertRanTimes
Проверяет, что указанный процесс был вызван заданное количество раз:
use Illuminate\Support\Facades\Process;
Process::assertRanTimes('ls -la', times: 3);
Метод assertRanTimes также принимает замыкание с экземплярами процесса и результата процесса для проверки настроек. Если замыкание возвращает true и процесс был вызван указанное число раз, утверждение считается успешным:
Process::assertRanTimes(function (PendingProcess $process, ProcessResult $result) {
return $process->command === 'ls -la';
}, times: 3);
#Предотвращение незапланированных процессов
Если вы хотите убедиться, что все вызванные процессы были подделаны в рамках отдельного теста или всего набора тестов, вызовите метод preventStrayProcesses. После этого любые процессы без соответствующего фейкового результата будут выбрасывать исключение вместо запуска реального процесса:
use Illuminate\Support\Facades\Process;
Process::preventStrayProcesses();
Process::fake([
'ls *' => 'Test output...',
]);
// Возвращается фейковый ответ...
Process::run('ls -la');
// Выбрасывается исключение...
Process::run('bash import.sh');