Search code examples
phpsymfonyasynchronousparallel-processingreactphp

Multiple function calls asynchronously in PHP


I'm using Symfony 3 and ReactPHP library for control all my functionality and I need to execute multiple calls to same function (subFunction() in code) asynchronously. I have 2 projects (project1 and project2):

Project 1 code:

/**
* Loop an array of urls and call sub function.
**/
public function startFunction() {
  $finalResponse = [];
  $urls = ['www.google.es', 'www.github.com', 'www.bitbucket.org'];

  foreach ($urls as $url) {
    $res = $this->subFunction($url);    // subfunction call ( **IT MAY TAKE A LONG TIME !!** )
    $finalResponse[] = $res;
 }

 return $finalResponse;
}

/**
* Uses Factory loop to get the Promise returned by finalRequest function.
**/
private function subFunction($url) {
  $loop = \React\EventLoop\Factory::create();
  $classA = new Project2\ClassA();
  $finalResponse = null;

  // project 2 function call
  $classA->finalRequest($url)->then(function($response) use(   
     &$finalResponse
  ) {
     $finalResponse = $response;
  })

  return $finalResponse;
}

Project 2 code:

classA {

/**
* Makes an React\HttpClient request (GET) to sent url and return his value inside a Promise.
**/
public function finalRequest($url) {
   $generalDeferred = new Deferred();
   $generalPromise = $generalDeferred->promise();

   // make React\HttpClient request
   $request = $client->request('GET', $url);
   $request->on('response', function ($response) use($generalDeferred) {
   $response->on('data', function ($response) {
        $generalDeferred->resolve($response);
      });
   });
  $request->end();

  return $generalPromise;
}

}

Problem is that on every subFunction($url) call, the program stops until the sub Function gets the response, but I need to do this asynchronously because this subFunction could take many seconds. So I would like to launch all subFunction($url) calls at the same time, and get all responses asynchronously.

It's possible solve this problem? Thanks.


Solution

  • First of all you can only have one loop running in an app. Second you have to make the loop run: https://reactphp.org/event-loop/

    You should create the app and then register all the services and events, start the loop and leave it running as a server.

    $loop = React\EventLoop\Factory::create();
    
    $server = stream_socket_server('tcp://127.0.0.1:8080');
    stream_set_blocking($server, 0);
    
    $loop->addReadStream($server, function ($server) use ($loop) {
    [...]
    });
    
    $loop->addPeriodicTimer(5, function () {
    [...]
    });
    
    $loop->run(); <---- you will not execute anything behind this point.
    

    Why? https://github.com/reactphp/event-loop/blob/master/src/ExtLibeventLoop.php#L196

    public function run()
    {
        $this->running = true;
        while ($this->running) { <------------------------------
            $this->futureTickQueue->tick();
            $flags = EVLOOP_ONCE;
            if (!$this->running || !$this->futureTickQueue->isEmpty()) {
                $flags |= EVLOOP_NONBLOCK;
            } elseif (!$this->streamEvents && !$this->timerEvents->count()) {
                break;
            }
            event_base_loop($this->eventBase, $flags);
        }
    }
    

    For the use you do of the loop I would recommend to use Guzzle Async: http://docs.guzzlephp.org/en/stable/faq.html

    $finalResponse = [];
    $promises = [];
    $urls = ['www.google.es', 'www.github.com', 'www.bitbucket.org'];
    foreach ($urls as $index => $url) {
        $promise = $client->requestAsync('GET', $url);
        $promise->then(function ($response) use ($index, &$finalResponse) {
            $finalResponse[$index]=$response;
        });
        $promises[$index]=$promise;
    }
    foreach ($promises as $promise) {
        $promise->wait();
    }
    return $finalResponse;