DevGang
Авторизоваться

Начало работы с асинхронным PHP с использованием ReactPHP

Эта статья была первоначально написана Верном Анчетой в блоге разработчиков Honeybadger.

Как веб-разработчики, мы все знакомы с моделью запрос-ответ. Обычно это происходит примерно так:

  • Пользователь заходит на страницу с помощью браузера.
  • Сервер запрашивает необходимые данные из базы данных.
  • База данных отвечает данными.
  • Сервер отправляет запрошенную страницу обратно пользователю.

По сути, сервер выполняет задачу, запрошенную клиентом, а затем завершает работу после получения ответа. Это просто: если сервер не обрабатывает запросы от сотен пользователей одновременно, это может вызвать проблемы. Задачи, требующие много времени, обычно включают операции ввода-вывода, такие как чтение из файла или запрос к базе данных, поэтому вы всегда ограничены скоростью оборудования. Вы можете масштабировать сервер вертикально или горизонтально; однако это не решит основную проблему, поскольку существует ограничение на количество одновременных запросов, которые сервер может одновременно обрабатывать. Как только вы начнете достигать этого предела, вам необходимо будет внедрить такие методы, как балансировка нагрузки, чтобы сервер мог продолжать обслуживать ваших пользователей.

Если это задача, требующая некоторого времени, например отправка электронного письма или обработка загруженного файла, мы обычно используем какую-либо систему очередей с использованием Redis или Beanstalkd. Эта система очередей затем позаботится о выполнении трудоемкой задачи в качестве фоновой задачи на сервере. Таким образом, пользователю не нужно ждать завершения операции, прежде чем он увидит ответ. В большинстве случаев это хорошее решение.

Однако что, если я скажу вам, что существует альтернативное решение, не использующее сервер очередей? Это метод, который позволяет использовать всю мощность вашего сервера без необходимости ожидания завершения операций ввода-вывода. В этой статье мы объясним, как использовать ReactPHP в асинхронном PHP.

Что такое ReactPHP?

ReactPHP — это библиотека, которая позволяет превратить PHP во что-то вроде Go или Node.js, чтобы задачи могли выполняться асинхронно. Обратите внимание, что ReactPHP — это просто библиотека, которую вы устанавливаете вместе с Composer. У него нет громоздких требований, таких как установка специального расширения PHP; он просто работает так, как задумано.

В асинхронной модели некоторые задачи, например обработка загруженного файла, выполняются постепенно. В качестве примера возьмем заказ еды в ресторане. Это определенно не соответствует модели «запрос → выполнение → ответ», потому что в противном случае было бы много недовольных клиентов. Таким образом, вместо того, чтобы принимать заказ от одного клиента, готовить для него еду и затем переходить к следующему клиенту только после того, как он будет готов, мы просто принимаем заказы от всех клиентов и поручаем другому сотруднику приготовить еду. Это означает, что не все заказы будут выполнены в том порядке, в котором они размещены, поскольку подготовка одного заказа может занять больше времени, чем других, а другой заказ может быть проще подготовить. Однако в конце концов все заказы в какой-то момент будут выполнены. Вот как работает асинхронная модель.

Событийная архитектура

По умолчанию PHP работает синхронно, поэтому ему приходится ждать завершения операций ввода-вывода, прежде чем он сможет начать выполнение другой задачи. В архитектуре, управляемой событиями, мы вместо этого передаем трудоемкую операцию ввода-вывода операционной системе и просим ее уведомить нас после завершения задачи. Пока операционная система выполняет свою работу, сценарий может заняться другой задачей. Как только он получит уведомление о завершении задачи, он сможет обработать ее дальше.

Установите ReactPHP

В корне вашего рабочего каталога установите ReactPHP с помощью следующей команды:

composer require react/react

ReactPHP имеет различные компоненты, такие как цикл событий, обещания и потоки. При установке ReactPHP устанавливаются эти и некоторые другие компоненты, поэтому вам не нужно устанавливать их отдельно.

Цикл событий

Цикл событий — это просто бесконечный цикл, который прослушивает события и вызывает для них обработчики. Например, вот общие шаги, выполняемые при чтении данных из базы данных:

  • Цикл событий запускается.
  • Основной поток получает запрос от клиента на запрос базы данных.
  • Задача запроса к базе данных передается операционной системе.
  • Другой пользователь запускает запрос к базе данных, поэтому шаги 2 и 3 выполняются снова.
  • Операционная система завершает один из запросов, поэтому она запускает событие с запрашиваемыми данными.
  • Основной поток получает это событие и передает результат клиенту.
  • Через некоторое время другая задача также будет завершена, поэтому для этой задачи выполняются шаги 5 и 6.

Обратите внимание, что существует только один поток, и каждый запрос принимается и обрабатывается этим потоком за наносекунды, поэтому это очень быстро. Цикл событий также использует очередь, поэтому он знает, какую из них обрабатывать первой. Основным решающим фактором является то, насколько быстро операционная система возвращает запрошенные данные. Таким образом, запросы, поступившие первыми, не обязательно выполняются первыми.

Чтобы продемонстрировать, как работает цикл событий, давайте рассмотрим простой пример с использованием таймеров.

Если вы какое-то время работали с JavaScript, то вы уже знакомы с функциями setTimeout() и setInterval():

setTimeout(() => {
    console.log('hello world after 1 second');
}, 1000);
setInterval(() => {
    console.log('I show up every 5 seconds');
}, 5000);

По своей природе JavaScript является асинхронным языком, поэтому в нем есть функции, подобные двум упомянутым ранее. В него уже встроен цикл событий, поэтому нам не нужно его явно создавать и запускать. Однако для PHP нам нужна библиотека типа ReactPHP, чтобы сделать его асинхронным.

Вот эквивалент функции setTimeout() в PHP с использованием ReactPHP:

<?php
// timeout.php

require 'vendor/autoload.php';

$loop = \React\EventLoop\Factory::create();

// add a timer
$loop->addTimer(1, function () {
    echo "After timer\n";
});

echo "Before timer\n";

$loop->run(); // start the event loop

Вы можете запустить его из терминала:

php timer.php

Вы увидите следующий результат:

Before timer <-- this shows up immediately when you run the script
After timer  <-- this shows up a second later

Вот эквивалент функции setInterval():

<?php
// interval.php

use React\EventLoop\TimerInterface;

require 'vendor/autoload.php';

$loop = \React\EventLoop\Factory::create();

$counter = 0;
$loop->addPeriodicTimer(1, function (TimerInterface $timer) use (&$counter, $loop) {

    $counter += 1;
    echo "{$counter} iterations\n";

    if ($counter === 10) {
        echo "Cancelled after 10 iterations\n";
        $loop->cancelTimer($timer);
    } 


});

$loop->run();

Запустить его:

php periodic-timer.php

Вы увидите следующий вывод, каждый из которых находится с интервалом в одну секунду:

1 iterations
2 iterations
3 iterations
4 iterations
5 iterations
6 iterations
7 iterations
8 iterations
9 iterations
10 iterations
Cancelled after 10 iterations

В обоих случаях выше мы сделали три вещи:

  • Создал цикл событий:
   $loop = \React\EventLoop\Factory::create();
  • Зарегистрировал обратный вызов для конкретного события:
 $loop->addTimer(1, function () {
      echo "after\n";
   });
  • Запустил цикл:
   $loop->run();

Вот и весь шаблон, поэтому вы можете зарегистрировать обратный вызов для нескольких событий, и каждое из них будет выполняться в цикле. Попробуйте объединить события таймера и периодического таймера, чтобы вы поняли, что я имею в виду:

<?php

// interval-and-timeout.php

use React\EventLoop\TimerInterface;

require 'vendor/autoload.php';

$loop = \React\EventLoop\Factory::create();

$loop->addTimer(2, function () {
    echo "At the second iteration\n";
});

$loop->addTimer(4, function () {
    echo "At the fourth iteration\n";
});

$counter = 0;
$loop->addPeriodicTimer(1, function (TimerInterface $timer) use (&$counter, $loop) {

    $counter += 1;
    echo "{$counter} iterations\n";

    if ($counter === 10) {
        echo "Cancelled after 10 iterations\n";
        $loop->cancelTimer($timer);
    } 

});

$loop->run();

Когда вы его запустите:

php interval-and-timeout.php

Вы увидите следующее:

1 iterations
At the second iteration <-- this gets outputted at the same time as the one below it
2 iterations <-- this gets outputted at the same time as the one above it
3 iterations
At the fourth iteration <-- this gets outputted at the same time as the one below it
4 iterations  <-- this gets outputted at the same time as the one above it
5 iterations
6 iterations
7 iterations
8 iterations
9 iterations
10 iterations
Cancelled after 10 iterations

Это доказывает, что код внутри обратного вызова вызывается, когда приходит время выполнения. В этом случае основой выполнения обратного вызова является время в секундах. Каждая итерация цикла называется тактом, и он будет продолжать выполняться до тех пор, пока не останется слушателей или пока он не будет остановлен вручную.

Обратите внимание, что тик не выполняется в секунду; мы настроили обратный вызов только на выполнение в секунду, чтобы мы могли легко его воспринимать. Вы также можете установить его в миллисекундах:

$loop->addTimer(0.3, function(){
    // do something
});

Приведенный выше код будет выполнен через 300 миллисекунд.

Еще одна важная вещь, на которую следует обратить внимание, это то, что таймеры не являются точными по времени. Это просто потому, что коды внутри обратного вызова не выполняются параллельно; это просто так выглядит, потому что выполнение кода, исполняемого в обратном вызове, не занимает много времени. Это значит, что если у вас в коде есть что-то вроде Sleep(), то это задержит все остальные события:

<?php
// interval-with-sleep.php

use React\EventLoop\TimerInterface;

require 'vendor/autoload.php';

$loop = \React\EventLoop\Factory::create();

$counter = 0;
$loop->addPeriodicTimer(1, function (TimerInterface $timer) use (&$counter, $loop) {

    $counter += 1;
    echo "{$counter} iterations\n";

    if ($counter === 10) {
        echo "Cancelled after 10 iterations\n";
        $loop->cancelTimer($timer);
    } 

    sleep(5);
});

$counter2 = 0;
$loop->addPeriodicTimer(1, function (TimerInterface $timer) use (&$counter2, $loop) {

    $counter2 += 1;
    echo "Counter 2: {$counter2} iterations\n";

    if ($counter2 === 10) {
        echo "Counter 2: Cancelled after 10 iterations\n";
        $loop->cancelTimer($timer);
    }     
});

$loop->run();

Когда вы запустите приведенный выше код, вот как будет выглядеть результат:

1 iterations
Counter 2: 1 iterations <-- shows up 5 seconds later
2 iterations <-- shows up 1 second after the one directly above it
Counter 2: 2 iterations <-- shows up 5 seconds after the one directly above it
3 iterations <-- shows up 1 second after the one directly above it

Ключевой вывод здесь заключается в том, что код внутри обратного вызова должен сохраняться быстро. В противном случае это задержит всех остальных. Это означает, что вы не можете просто писать код, как обычно, особенно если вы планируете использовать код, использующий ввод-вывод. В следующих нескольких разделах вы познакомитесь с некоторыми приемами, которые позволят вам работать с вводом-выводом, сохраняя при этом его простоту.

Потоки

Потоки используются для обработки больших объемов данных в фрагменты. Каждый раз, когда получен один из этих фрагментов, он вызывает уведомление, чтобы новый фрагмент данных мог быть использован тем, кто его запросил.

Вот пример использования потоков для чтения текстового файла:

<?php 
// streams.php

use React\Stream\ReadableResourceStream;

require 'vendor/autoload.php';

$loop = React\EventLoop\Factory::create();

$logFile = './logfile.txt';

$readChunkSize = 100;

$stream = new ReadableResourceStream(fopen($logFile, 'r'), $loop, $readChunkSize);
$stream->on(
    'data',
    function ($data) {
        echo "===" . $data . "\n";
    }
);

$stream->on('end', fn() => print "Finished reading the file."); 
$loop->run();

Здесь мы передаем содержимое файла logfile.txt в тот же каталог. Мы делаем это, создавая новый экземпляр ReadableResourceStream, который принимает два аргумента: ресурс и цикл событий. Помните галочку? Поток все еще придерживается этого. Таким образом, он считывает содержимое файла постепенно на каждом такте. Вы даже можете указать $readChunkSize в качестве третьего аргумента, который позволит вам указать байты для каждого фрагмента.

В последнем разделе вы увидите, как мы можем использовать потоки при обработке загрузки файлов. По сути, вы обращаетесь к потокам, когда работаете с файлами или телом HTTP-запроса, которые не могут быть обработаны мгновенно.

Обещания

Обещание — это представление некоторой будущей ценности. Это результат выполнения асинхронного кода. Это означает, что вы можете прикрепить к нему слушателя; поэтому, когда результат станет доступен, вы можете легко получить доступ к нему (или к ошибке, если операция не увенчалась успехом).

Обещание имеет три возможных состояния:

  • Невыполнено — состояние по умолчанию. Он остается в этом состоянии до завершения операции.
  • Выполнено — в этом состоянии обещание находится, если операция прошла успешно и результаты готовы к отправке клиенту.
  • Не удалось — это состояние, когда операция не удалась. Затем он вернет некоторую информацию о проблеме.

Обещание не может быть создано само по себе. Для этого вам понадобится отложенный объект. Это представляет собой код, который необходимо выполнить для получения результата. Отложенный объект имеет три метода:

  • promise() — для создания обещания.
  • solve() — если операция прошла успешно, это используется для изменения состояния обещания на выполненное.
  • ignore() — если операция не удалась, вы можете использовать это, чтобы установить обещание как неудавшееся.

Вот пример использования обещаний в ReactPHP. Давайте сделаем запрос к Pokemon API:

<?php
// promises.php

use React\Promise\Deferred;

require 'vendor/autoload.php';

function httpRequest($url) {
    $response = file_get_contents($url);
    $deferred = new Deferred();

    if($response) {
        $deferred->resolve($response);
    } else {
        $deferred->reject(new Exception('No response returned'));
    }

    return $deferred->promise();
}

httpRequest('https://pokeapi.co/api/v2/pokemon/ditto')
    ->then(function ($response) {
        $response_arr = json_decode($response, true);
            return [
                'name' => $response_arr['name'],
                'sprite' => $response_arr['sprites']['front_default'],
            ];
    })
    ->then(
        function ($response) {
            print_r($response);
        })
    ->otherwise(
        function (Exception $exception) {
            echo $exception->getMessage();
        });

Давайте разберем приведенный выше код. Сначала мы создаем функцию, которая создаст новый отложенный объект, который мы будем использовать для создания обещания. Обещание — это то, что мы возвращаем из этой функции. Для простоты мы делаем HTTP-запрос, используя функцию file_get_contents. Если ответ становится доступным, мы вызываем функцию resolve() для отложенного объекта и передаем ему ответ. В противном случае мы вызываем функцию ignore() и передаем исключение:

function httpRequest($url) {
    $response = file_get_contents($url);
    $deferred = new Deferred();

    if ($response) {
        $deferred->resolve($response);
    } else {
        $deferred->reject(new Exception('No response returned'));
    }

    return $deferred->promise();
}

Когда мы вызываем функцию httpRequest(), мы передаем ей URL-адрес, к которому мы хотим сделать запрос. Поскольку ранее мы вернули обещание, это позволяет нам прикрепить обратный вызов с помощью then() и передать функцию обратного вызова. Он содержит любое значение, которое мы ранее передали в функцию resolve() отложенного объекта. В данном случае это строка JSON. Отсюда мы можем немного обработать данные, чтобы в итоге получить ту часть данных и формат, которые нам нужны. В этом случае мы преобразуем его в массив и возвращаем только имя и спрайт. Далее мы можем продолжить передачу вызовов then() для дальнейшей обработки данных или возврата окончательного результата. Чтобы обрабатывать исключения, вы можете вызвать функцию else() и передать обратный вызов для обработки проблемы:

httpRequest('https://pokeapi.co/api/v2/pokemon/ditto')
    ->then(function ($response) {
        $response_arr = json_decode($response, true);
        return [
            'name' => $response_arr['name'],
            'sprite' => $response_arr['sprites']['front_default'],
        ];
    })
    ->then(
        function ($response) {
            print_r($response);
        })
    ->otherwise(
        function (Exception $exception) {
            echo $exception->getMessage();
        });

Обратите внимание, что упаковка вашего кода в промис не делает его автоматически асинхронным. В приведенном выше примере мы использовали функцию file_get_contents(). При этом используется ввод-вывод, поэтому по своей природе он «блокирующий».

Дочерние процессы

ReactPHP предоставляет служебный компонент Child Process как средство для выполнения неблокирующих вызовов ввода-вывода. Это интегрирует вызовы операционной системы, такие как Curl или Cat, в цикл событий, чтобы мы могли подключить функцию обратного вызова, когда в выполняемых нами вызовах операционной системы происходят определенные события.

Компонент Child Process по умолчанию не установлен, поэтому вам придется установить его вручную:

composer require react/child-process

Вот пример использования дочернего процесса вместе с циклом событий. Это неблокирующий эквивалент вызова file_get_contents(), который мы выполнили ранее:

<?php 
// child-process.php

use React\ChildProcess\Process;

require 'vendor/autoload.php';

$loop = React\EventLoop\Factory::create();

$process = new Process('curl https://pokeapi.co/api/v2/pokemon/ditto');
$process->start($loop);

$process->stdout->on('data', function ($data) {
    echo $data;
});

$loop->run();

Разбивая его, мы сначала создаем новый объект Process. Здесь мы передаем команду, которую хотим выполнить. В данном случае мы выполняем HTTP-запрос к API Pokemon с помощью команды curl:

$process = new Process('curl https://pokeapi.co/api/v2/pokemon/ditto');

Чтобы интегрировать его с циклом событий, когда вы запускаете процесс, мы просто передаем экземпляр созданного нами цикла событий:

$process->start($loop);

Затем прослушайте событие data в стандартном выводе (стандартный вывод). По сути, это то же самое, что и когда вы видите вывод консоли при выполнении той же команды в консоли:

$process->stdout->on('data', function ($data) {
    echo $data;
});

Дочерние процессы — это то, к чему вы обращаетесь всякий раз, когда вам нужно выполнить операции ввода-вывода, такие как создание и добавление к файлу или выполнение запроса на другой сервер.

Обработка загрузки файлов с использованием ReactPHP

Теперь, когда мы знакомы с основными концепциями ReactPHP, пришло время проверить наши знания, реализовав загрузку файлов. Мы будем использовать фреймворк Laravel вместе с ReactPHP, чтобы упростить реализацию.

Проект Laravel

Начните с установки Laravel:

composer create-project laravel/laravel reactphp-fileupload
cd reactphp-fileupload
npm install
php artisan serve

Далее создайте новый контроллер:

php artisan make:controller FileUploadController --invokable

Добавьте следующий код в сгенерированный файл:

<?php
// app/Http/Controllers/FileUploadController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FileUploadController extends Controller
{
    /**
      * Handle the incoming request.
    */
    public function __invoke(Request $request)
    {
        return view('upload');
    }
}

Вот код для представления загрузки. Как видите, мы отправляем форму в другое место. Это будет отдельный сервер, который мы создадим с помощью ReactPHP:

<!--resources/views/upload.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>File Upload</title>
    <link rel="stylesheet" href="/css/bootstrap.min.css" />
</head>
<body>
    <div class="p-5">
        <form 
            action="http://localhost:8080"
            method="POST"
            enctype="multipart/form-data">

            <div>
                <label for="file" class="form-label">Upload File</label>
                <input class="form-control form-control-lg" id="file" name="file" type="file">
            </div>

            <div class="mt-3">
                <button type="submit" class="btn btn-lg btn-primary">
                Submit
                </button>
            </div>

        </form>
    </div>
</body>
</html>

Откройте файл маршрутов и добавьте страницу загрузки:

<?php
// routes/web.php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\FileUploadController;

// ...

Route::get('/upload', FileUploadController::class);

Вот как это будет выглядеть:

Сервер ReactPHP

Теперь мы готовы приступить к созданию ReactPHP. Вы можете использовать тот же рабочий каталог, который использовался для тестов, которые мы проводили ранее.

Внутри вашего рабочего каталога создайте файл upload-server.php и добавьте следующее:

<?php 
// upload-server.php

use React\EventLoop\LoopInterface;

use React\Http\Server;
use React\Socket\SocketServer;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;

use React\Http\Middleware\StreamingRequestMiddleware;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Http\Middleware\RequestBodyParserMiddleware;

use React\ChildProcess\Process;

require 'vendor/autoload.php';

$loop = React\EventLoop\Factory::create();

$server = new Server(
    $loop,  

    new StreamingRequestMiddleware(),
    new RequestBodyBufferMiddleware(5 * 1024 * 1024), // 5 Mb
    new RequestBodyParserMiddleware(5 * 1024 * 1024, 1), // 5 Mb

    function (ServerRequestInterface $request) use ($loop) { 

        if ($request->getMethod() === 'POST') {

            $files = $request->getUploadedFiles();


            $file = $files['file'];

            $process = new Process(
                "cat > uploads/{$file->getClientFilename()}",
                __DIR__
            );

            $process->start($loop);
            $process->stdin->write($file->getStream()->getContents());

            return new Response(
                200, ['Content-Type' => 'text/plain'], "Uploaded!"
            ); 
     }
});

$socket = new SocketServer(8080); 
$server->listen($socket);

$loop->run();

Разбивая его, мы сначала импортируем то, что нам нужно. С некоторыми из них вы уже знакомы, поскольку мы использовали их в предыдущих разделах, поэтому мы рассмотрим только те, которые ранее не рассматривались:

use React\EventLoop\LoopInterface;

use React\Http\Server;
use React\Socket\SocketServer;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;

use React\Http\Middleware\StreamingRequestMiddleware;
use React\Http\Middleware\RequestBodyBufferMiddleware;
use React\Http\Middleware\RequestBodyParserMiddleware;

use React\ChildProcess\Process;
  • React\Http\Server — это серверный компонент ReactPHP. Он уже установлен при установке ReactPHP, поэтому нет необходимости устанавливать его отдельно. Он состоит из API-интерфейсов сервера и браузера. Сервер используется для создания серверов и обработки входящих HTTP-запросов. Браузер используется для выполнения исходящих запросов (например, при обращении к внешнему API).
  • React\Socket\SocketServer — HTTP-сервер не может быть создан самостоятельно. Он должен быть отправлен в определенный сокет на сервере, чтобы он был доступен внешнему миру. Для этого используется сервер сокетов.
  • React\Http\Message\Response — для указания ответа, который вы хотите вернуть.
  • Psr\Http\Message\ServerRequestInterface — это позволяет нам получить доступ к различным данным о запросе, полученном сервером. Так мы получаем доступ к данным формы.
  • React\Http\Middleware — содержит различное промежуточное программное обеспечение для анализа и обработки тела запроса.

Теперь давайте пройдемся по телу кода. Сначала мы создаем цикл событий:

$loop = React\EventLoop\Factory::create();

Затем мы создаем новый сервер и передаем ему цикл событий, а затем запрос промежуточного программного обеспечения:

  • StreamingRequestMiddleware — с помощью этого промежуточного программного обеспечения оно будет передавать тело запроса в потоковом режиме, поэтому оно передается на сервер в виде фрагментов.
  • RequestBodyBufferMiddleware — используется для буферизации всего тела входящего запроса в память. По сути, именно он отвечает за объединение потоковых фрагментов. Для этого необходимо передать общее количество байтов, разрешенное для каждого тела запроса (т. е. общий размер загруженного файла). То же самое относится и к промежуточному программному обеспечению ниже.
  • RequestBodyParserMiddleware — отвечает за получение полностью буферизованного тела запроса и анализ формы и загрузки файлов из тела входящего HTTP-запроса.

Возможно, вы заметили, что упомянутые выше промежуточные программы работают вместе, и каждое из них просто передает свои выходные данные следующему. Однако это не значит, что вы не можете перепутать их порядок. Можно, но разумнее расположить их так:

$server = new Server(
    $loop,  

    new StreamingRequestMiddleware(),
    new RequestBodyBufferMiddleware(5 * 1024 * 1024), // 5 Mb
    new RequestBodyParserMiddleware(5 * 1024 * 1024, 1), // 5 Mb
// ...

Далее у нас есть функция обратного вызова. Это вызывается всякий раз, когда отправляется форма загрузки файла. Сначала мы проверяем, соответствует ли метод запроса ожидаемому. В данном случае мы хотим, чтобы была опубликована форма. Только тогда мы можем предположить, что файл был загружен. Мы получаем к нему доступ, используя значение, присвоенное атрибуту name входа:

function (ServerRequestInterface $request) use ($loop) { 

    if ($request->getMethod() === 'POST') {

        $files = $request->getUploadedFiles();
        $file = $files['file'];

        // ...
    }

}

Далее мы запускаем новый дочерний процесс. Это создаст файл в каталоге uploads на сервере и будет использовать данное имя в качестве имени файла. Нам нужно передать __DIR__ в качестве второго аргумента, чтобы он работал в том же каталоге, что и файл upload-server.php:

$process = new Process(
    "cat > uploads/{$file->getClientFilename()}",
    __DIR__
);

Далее запускаем дочерний процесс. Не забудьте передать экземпляр цикла событий:

$process->start($loop);

Далее мы получаем содержимое из потока и затем записываем это содержимое в файл с помощью метода стандартного ввода write():

$file_contents = $file->getStream()->getContents();
$process->stdin->write($file_contents);

Далее мы возвращаем простой текстовый ответ:

return new Response(
    200, 
    ['Content-Type' => 'text/plain'], 
    "uploaded!"
);

Вне обратного вызова мы создаем новый сервер сокетов и заставляем HTTP-сервер прослушивать запросы на этом сокете:

$socket = new SocketServer(8080); 
$server->listen($socket);

$loop->run();

Запуск сервера

На этом этапе вы можете запустить сервер:

php upload-server.php

Затем откройте проект Laravel в своем браузере, выберите файл и нажмите кнопку «Отправить». Если все настроено правильно, выбранный вами файл должен находиться в каталоге загрузок. Если вы еще не создали каталог загрузок, сейчас самое время сделать это, поскольку дочерний процесс не создаст папку за вас.

Вы также можете использовать Postman, чтобы попробовать:

Заключение

Вот и все! В этой статье вы узнали основы ReactPHP и научились использовать его в своих проектах. Ключевой вывод заключается в том, что у ReactPHP есть свой собственный способ ведения дел таким образом, вы не можете просто установить его через Composer и интегрировать в существующий проект. Вместо этого вам придется создать новый сервер и выполнять на нем все асинхронные операции. Конечно, это также означает, что вы не можете просто использовать существующие библиотеки, например те, которые используются для взаимодействия с базой данных. Все должно быть асинхронно. Избегайте использования блокирующего кода, такого как file_get_contents(), или всего, что использует ввод-вывод. Вместо этого используйте дочерние процессы, потоки и обещания, чтобы обеспечить асинхронность и привязку событий к циклу событий.

Коды, используемые в этой статье, находятся в этом репозитории GitHub.

Источник:

#JavaScript #PHP #React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу