Поддержите проект сделав пожертвование.
Ищете работу? Мы поможем!
Ищете работу? Мы поможем!

Процессы

Введение

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

Параметры процесса

Путь к рабочему каталогу

Вы можете использовать метод 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 and seeInErrorOutput, которые предоставляют удобный способ определить, содержится ли заданная строка в выводе процесса:

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

Отключение вывода процесса

Если ваш процесс записывает большое количество вывода, которое вам неинтересно, вы можете сэкономить память, полностью отключив получение вывода. Для этого вызовите метод quietly при создании процесса:

use Illuminate\Support\Facades\Process;

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

Pipelines

Иногда вам может потребоваться передать вывод одного процесса в качестве ввода для другого процесса. Это часто называется “перенаправлением” (piping) вывода одного процесса в другой. Метод pipe, предоставляемый фасадом Process упрощает это. Методpipe выполнит связанные процессы синхронно и вернет результат последнего процесса в pipeline:

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()) {
    // ...
}

Если вам не нужно настраивать отдельные процессы, составляющие pipeline, вы можете просто передать массив строк команд методу 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 также позволяет назначать строковые ключи каждому процессу, содержащемуся в pipeline, с помощью метода 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...';
});

Параллельные процессы

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 'Import complete!';
});

При тестировании этого маршрута мы можем указать 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');

    // 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\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';
});

To properly fake this process, we need to be able to describe how many times the running method should return true. In addition, we may want to specify multiple lines of output that should be returned in sequence. To accomplish this, we can use the Process facade’s describe method:

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),
]);

Чтобы корректно подделать этот процесс, нам нужно иметь возможность описать, сколько раз метод running должен возвращать true. Кроме того, по желанию, мы можем указать несколько строк вывода, которые должны быть возвращены последовательно. Для этого мы можем использовать метод describe фасада Process:

Доступные утверждения

Как уже обсуждалось ранее, 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...',
]);

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

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