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.
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).
Related
I am using the below sample code to return data to the browser
$response->write($image);
return $response->withHeader('Content-Type', 'image/jpeg');
which works fine but how would i go about it if i wanted to also return Content-Length ?
the only option i found so far is to copy the response into a new object like below but i don't think that's efficient if i have a $image in the response.
$response = $response->withAddedHeader('Content-Length', strlen($image));
I tried it as array but that doesn't work..
Quoting from Slim 3 docs
Reminder
Unlike the withHeader() method, this method appends the new value to the set of values that already exist for the same header name. The Response object is immutable. This method returns a copy of the Response object that has the appended header value.
Both withHeader() and with appendedHeader() methods return a copy of the response object. So even if your do not assign the return value of $response->withHeader() to a variable and return the result directly, you are still working with a copy of the response object.
Regarding your concern about efficiency, you should use streams instead of string as response body. The following is an example of how to use streams for returning an image as the response:
<?php
use Slim\App;
use Slim\Http\Request;
use Slim\Http\Response;
use Slim\Http\Body;
return function (App $app) {
$container = $app->getContainer();
$app->get('/', function(Request $request, Response $response) {
$image = fopen('sample.jpg', 'r');
return $response->withBody(new Body($image))
->withHeader('Content-Type', 'image/jpeg')
->withAddedHeader('Content-Length', fstat($image)['size']);
});
};
So I'm trying to use guzzle for a couple of concurrent requests. I've seen several examples online and this is what I came up with, but can't seem to get it to work. No errors, no warnings, nothing. I've tried logging inside each promise but nothing happens.
And I know for sure that nothing is happening because nothing is getting inserted in the DB. Any ideas what I'm missing? (I'm yielding each request with its respective then because at the end of each promise, the DB operations are specific to that user)
use GuzzleHttp\Promise\EachPromise;
use Psr\Http\Message\ResponseInterface;
$promises = (function () use($userUrls){
$userUrls->each(function($user) {
yield $this->client->requestAsync('GET', $user->pivot->url)
->then(function (ResponseInterface $response) use ($user) {
$this->dom->load((string)$response->getBody());
// ... some db stuff that inserts row in table for this
// $user with stuff from this request
});
});
});
$all = new EachPromise($promises, [
'concurrency' => 4,
'fulfilled' => function () {
},
]);
$all->promise()->wait();
Not sure wht you don't get an error, but your generator is definitely wrong.
use Psr\Http\Message\ResponseInterface;
use function GuzzleHttp\Promise\each_limit_all;
$promises = function () use ($userUrls) {
foreach ($userUrls as $user) {
yield $this->client->getAsync($user->pivot->url)
->then(function (ResponseInterface $response) use ($user) {
$this->dom->load((string)$response->getBody());
// ... some db stuff that inserts row in table for this
// $user with stuff from this request
});
};
};
$all = each_limit_all($promises(), 4);
$all->promise()->wait();
Note foreach instead of $userUrls->each(), it's important because in your version generator function is the function that is passes to ->each() call, not the one you assign to $promise.
Also note that you must activate the generator (call $promises() as pass the result, not pass the function itself to Guzzle).
Otherwise all looks good, try the code with my changes.
I have written some functions using Authorize.Net's PHP SDK's that look like the following:
public function getCustomerProfiles() {
$customerProfiles = array();
// Before we can get customer profiles, we need to get a list of all customer id's.
$customerIdListRequest = new AnetAPI\GetCustomerProfileIdsRequest();
$customerIdListRequest->setMerchantAuthentication(self::getMerchantAuth(Config::LOGIN_ID, Config::TRANSACTION_KEY));
$customerIdListController = new AnetController\GetCustomerProfileIdsController($customerIdListRequest);
$customerIdListResponse = $customerIdListController->executeWithApiResponse(\net\authorize\api\constants\ANetEnvironment::SANDBOX);
if(($customerIdListResponse != null) && ($customerIdListResponse->getMessages()->getResultCode() == "Ok")) {
// TODO: Investigate warning about no method named getIds().
foreach( $customerIdListResponse->getIds() as $id ) {
// Now we can get each customer profile.
$request = new AnetAPI\GetCustomerProfileRequest();
$request->setMerchantAuthentication(self::getMerchantAuth(Config::LOGIN_ID, Config::TRANSACTION_KEY));
$request->setCustomerProfileId($id);
$controller = new AnetController\GetCustomerProfileController($request);
$response = $controller->executeWithApiResponse(\net\authorize\api\constants\ANetEnvironment::SANDBOX);
if(($response != null) && ($response->getMessages()->getResultCode() == "Ok")) {
// TODO: Investigate warning about no method named getProfile()
// Add it to the array.
array_push($customerProfiles, $response->getProfile()->xml);
} else {
throw new \Exception($response->getMessages()->getMessage());
}
}
} else {
throw new \Exception($customerIdListResponse->getMessages()->getMessage());
}
return $customerProfiles;
}
Currently, I'm just returning an array of objects. I'd prefer to get the raw XML response. Is this functionality available via Authorize.Net's PHP SDK? Or am I better of using something like Guzzle and making the request manually?
Looking at the source code I think it would be simple enough.
Look the execute method that is invoked by executeWithApiResponse there. See xmlResponse? Just need to store that as a class property (and add a public getter), or maybe tweak the function to take an extra argument telling it to return the raw response. Could hack it, or better yet, extend that ApiOperationBase class (note the interface IApiOperation gives you a outline to follow).
Seeing that serializer also...
$this->apiResponse = $this->serializer->deserialize( $xmlResponse, $this->apiResponseType , 'xml');
Could maybe do something more elegant with that. But not as clear as path I first described.
I need to return from a function call once a React/Promise has been resolved. The basic idea is to fake a synchronous call from an ansynchronous one. This means that the outer function must return a value once a promise has been resolved or rejected.
This is to create a driver for RedBeanPHP using React/Mysql. I am aware that this will likely lead to CPU starvation in the React event loop.
My initial idea was to use a generator then call yield inside a \React\Promise\Deferred::then callback.
function synchronous()
{
$result = asynchronous();
}
function asynchronous()
{
$deferred = new \React\Promise\Deferred;
$sleep = function() use ($deferred)
{
sleep(5);
$deferred->resolve(true);
};
$deferred->then(function($ret) {
yield $ret;
});
$sleep();
}
The PHP generator class, AFAICT, is only directly constructable by the PHP engine itself. The then callback would need to directly invoke send on the generator of the asynchronous function for this to work.
PHP lacks both continuations as well as generator delegation, which would make it possible to call yield from inside a nested callback, making this entirely impossible to achieve for the moment.
ReactPhp offers the async tools package which has an await function.
Code can then become:
function synchronous()
{
$result = \React\Async\await(asynchronous());
}
function asynchronous()
{
$deferred = new \React\Promise\Deferred;
$sleep = function() use ($deferred)
{
sleep(5);
$deferred->resolve(true);
};
$sleep();
return $deferred->promise();
}
Can I get a HttpRequest automatically created from the environment? In other words, right now it seems like you have to...
$request = new HttpRequest;
$request->setCookies($_COOKIE);
$request->setHeaders(apache_request_headers());
$request->setPostFields($_POST);
$request->setQueryData($_GET);
$request->setRawPostData(file_get_contents('php://input'));
$request->setUrl($_SERVER['REQUEST_URI'']);
We also need to set the method -- a ridiculous chore, since $_SERVER['REQUEST_METHOD'] is a string and HttpRequest::setMethod takes an int in the HTTP_METH_* series of contants. So you have to set up your own mapping.
I want to like HttpRequest, but it seems cumbersome to use at the moment. I hope I'm missing something.
Edit:
The idea is to make testing cleaner. $_COOKIE and friends are superglobals. How do you test that?
function receiveRequest() {
$code = 'that touches superglobals like' . $_COOKIE['example'];
$response = new HttpResponse;
$response->setStatus(200);
return $response;
}
function testServer() {
$oldCookie = $_COOKIE;
$oldPost = $_POST;
// etc...
$_COOKIE = array('example' => 'stuff');
$_POST = array();
// etc...
$response = receiveRequest();
$_COOKIE = $oldCookie;
$_POST = $oldPost;
// etc...
assert($response->getStatus() === 200);
}
You need to control the state of not just what you use -- $_COOKIE in this example -- but every superglobal. There are about a dozen. It would be a lot cleaner to wrap up all that stuff in HttpRequest.
function receiveRequest(HttpRequest $request) {
$code = 'is purely a function of arguments like' . $request->getCookie('example');
$response = new HttpResponse;
$response->setStatus(200);
return $response;
}
function testServer() {
$request = new HttpRequest;
$request->setCookie('example' => 'stuff');
$response = receiveRequest($request);
assert($response->getStatus() === 200);
}
Then my actual server.php would use the hypothetical static method that I'm looking for.
$request = HttpRequest::generateRequestFromEnvironment($_COOKIE, $_POST, ...);
unset($_COOKIE, $_POST, ...);
$response = receiveRequest($request);
$response->send();
This seems to me like an odd use case - you want to create an HttpRequest to...yourself? Using exactly the parameters you were passed? Why?
The normal case is for requesting another resource, from another host, for which the kind of "automatic setup" you desire is pretty useless. If you really need this situation, it seems trivial to wrap this in a function that's easily reused?
From what you've posted, I think you're using HttpRequest as a container for all the information about the request which triggered your script.
This isn't what it is for - it is for making HTTP requests to other services from within your script. For example, you might request data from the Flickr API