Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти
Інше
Події Про нас

Процеси

Вступ

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->command();
$result->successful();
$result->failed();
$result->output();
$result->errorOutput();
$result->exitCode();

Викидання винятків

Якщо ви маєте результат процесу й хочете викинути екземпляр Illuminate\Process\Exceptions\ProcessFailedException, коли код виходу більший за нуль (що означає невдачу), скористайтеся методами throw та throwIf. Якщо процес не провалився, буде повернуто екземпляр ProcessResult:

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

Методи timeout та idleTimeout також приймають екземпляри CarbonInterval:

use function Illuminate\Support\minutes;

$result = Process::timeout(minutes(2))->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');

Режим TTY не підтримується у Windows.

Вивід процесу

Як уже згадувалося, до виводу процесу можна звернутися методами 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"');
}, 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, щоб дочекатися завершення процесу й отримати екземпляр ProcessResult:

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

// ...

$result = $process->wait();

ID процесів і сигнали

Метод 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();

Замість чекати на завершення процесу, ви можете скористатися методом 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 повертає об'єкт із доступом як до масиву, що дозволяє звертатися до екземпляра ProcessResult кожного процесу пулу за його ключем:

$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

use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;

test('process is invoked', function () {
    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;
    });
});
<?php

namespace Tests\Feature;

use Illuminate\Contracts\Process\ProcessResult;
use Illuminate\Process\PendingProcess;
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 також приймає замикання, яке отримає екземпляри PendingProcess та ProcessResult, дозволяючи оглянути налаштовані опції процесу. Якщо це замикання поверне 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...',
]);

// Fake response is returned...
Process::run('ls -la');

// An exception is thrown...
Process::run('bash import.sh');