Процеси

Вступ

Laravel надає виразний, мінімальний API навколо компонента Symfony Process, дозволяючи зручно викликати зовнішні процеси з вашого 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 режиму для вашого процесу. 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');

Pipelines

Іноді ви можете захотіти зробити вихідні дані одного процесу вхідними даними для іншого процесу. Це часто називають "передачею" виходу одного процесу в інший. Метод 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 може бути використаний для отримання ідентифікатора процесу, призначеного операційною системою для запущеного процесу:

$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();

Замість того, щоб чекати, поки процес завершиться, ви можете використовувати метод waitUntil, щоб припинити очікування на основі виводу процесу. Laravel припинить очікування завершення процесу, коли замикання, передане методу waitUntil, поверне true:

$process = Process::start('bash import.sh');
 
$process->waitUntil(function (string $type, string $output) {
    return $output === 'Ready...';
});

Асинхронні тайм-аути процесів

Поки асинхронний процес виконується, ви можете перевірити, що процес не перевищив час очікування, використовуючи метод ensureNotTimedOut. Цей метод викличе виключення перевищення часу очікування, якщо процес перевищив час очікування:

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

Конкурентні процеси

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();

Ідентифікатори процесів пулу та сигнали

Оскільки метод running пулу процесів надає колекцію всіх викликаних процесів у пулі, ви можете легко отримати доступ до ідентифікаторів процесів у пулі:

$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 'Імпорт завершено!';
});

Коли тестуємо цей маршрут, ми можемо вказати Laravel повертати фальшивий, успішний результат процесу для кожного викликаного процесу, викликавши метод fake на фасаді Process без аргументів. Крім того, ми можемо навіть перевірити, що даний процес був "запущений":

<?php
 
use Illuminate\Process\PendingProcess;
use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Support\Facades\Process;
 
test('process is invoked', function () {
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;
});
});
<?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');
 
// Simple process assertion...
Process::assertRan('bash import.sh');
 
// Or, inspecting the process configuration...
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');