Search code examples
phppromiseslack-apireactphp

How do I "extract" the data from a ReactPHP\Promise\Promise?


Disclaimer: This is my first time working with ReactPHP and "php promises", so the solution might just be staring me in the face 🙄

I'm currently working on a little project where I need to create a Slack bot. I decided to use the Botman package and utilize it's Slack RTM driver. This driver utilizes ReactPHP's promises to communicate with the RTM API.

My problem:

When I make the bot reply on a command, I want to get the get retrieve the response from RTM API, so I can cache the ID of the posted message.

Problem is that, the response is being returned inside one of these ReactPHP\Promise\Promise but I simply can't figure out how to retrieve the data.

What I'm doing:

So when a command is triggered, the bot sends a reply Slack:

$response = $bot->reply($response)->then(function (Payload $item) {
    return $this->reply = $item;
});

But then $response consists of an (empty?) ReactPHP\Promise\Promise:

React\Promise\Promise^ {#887
  -canceller: null
  -result: null
  -handlers: []
  -progressHandlers: & []
  -requiredCancelRequests: 0
  -cancelRequests: 0
}

I've also tried using done() instead of then(), which is what (as far as I can understand) the official ReactPHP docs suggest you should use to retrieve data from a promise:

$response = $bot->reply($response)->done(function (Payload $item) {
    return $this->reply = $item;
});

But then $response returns as null.

The funny thing is, during debugging, I tried to do a var_dump($item) inside the then() but had forgot to remove a non-existing method on the promise. But then the var_dump actually returned the data 🤯

$response = $bot->reply($response)->then(function (Payload $item) {
    var_dump($item);
    return $this->reply = $item;
})->resolve();

So from what I can fathom, it's like I somehow need to "execute" the promise again, even though it has been resolved before being returned.

Inside the Bot's reply method, this is what's going on and how the ReactPHP promise is being generated:

public function apiCall($method, array $args = [], $multipart = false, $callDeferred = true)
{
    // create the request url
    $requestUrl = self::BASE_URL . $method;

    // set the api token
    $args['token'] = $this->token;

    // send a post request with all arguments
    $requestType = $multipart ? 'multipart' : 'form_params';
    $requestData = $multipart ? $this->convertToMultipartArray($args) : $args;

    $promise = $this->httpClient->postAsync($requestUrl, [
        $requestType => $requestData,
    ]);

    // Add requests to the event loop to be handled at a later date.
    if ($callDeferred) {
        $this->loop->futureTick(function () use ($promise) {
            $promise->wait();
        });
    } else {
        $promise->wait();
    }

    // When the response has arrived, parse it and resolve. Note that our
    // promises aren't pretty; Guzzle promises are not compatible with React
    // promises, so the only Guzzle promises ever used die in here and it is
    // React from here on out.
    $deferred = new Deferred();
    $promise->then(function (ResponseInterface $response) use ($deferred) {
        // get the response as a json object
        $payload = Payload::fromJson((string) $response->getBody());

        // check if there was an error
        if (isset($payload['ok']) && $payload['ok'] === true) {
            $deferred->resolve($payload);
        } else {
            // make a nice-looking error message and throw an exception
            $niceMessage = ucfirst(str_replace('_', ' ', $payload['error']));
            $deferred->reject(new ApiException($niceMessage));
        }
    });

    return $deferred->promise();
}

You can see the full source of it here.

Please just point me in some kind of direction. I feel like I tried everything, but obviously I'm missing something or doing something wrong.


Solution

  • ReactPHP core team member here. There are a few options and things going on here.

    First off then will never return the value from a promise, it will return a new promise so you can create a promise chain. As a result of that you do a new async operation in each then that takes in the result from the previous one.

    Secondly done never returns result value and works pretty much like then but will throw any uncaught exceptions from the previous promise in the chain.

    The thing with both then and done is that they are your resolution methods. A promise a merely represents the result of an operation that isn't done yet. It will call the callable you hand to then/done once the operation is ready and resolves the promise. So ultimately all your operations happen inside a callable one way or the other and in the broadest sense. (Which can also be a __invoke method on a class depending on how you set it up. And also why I'm so excited about short closures coming in PHP 7.4.)

    You have two options here:

    • Run all your operations inside callable's
    • Use RecoilPHP

    The former means a lot more mind mapping and learning how async works and how to wrap your mind around that. The latter makes it easier but requires you to run each path in a coroutine (callable with some cool magic).