How can i call methods asynchronously in symfony2.7 Like.
I have to retrieve data making 4 different API connections. The problem is slow response from my application since PHP is synchronous so it has to wait for the response from all the API and then render the data.
class MainController{
public function IndexAction(){
// make Asynchronous Calls to GetFirstAPIData(), GetSecondAPIData(), GetThridAPIData()
}
public function GetFirstAPIData(){
// Get data
}
public function GetSecondAPIData(){
// Get data
}
public function GetThridAPIData(){
// Get data
}
}
You can use guzzle for that, especially when we're are talking about http based apis. Guzzle is a web-client which has async calls built in.
The code would look somewhat like this: (taken from the docs)
$client = new Client(['base_uri' => 'http://httpbin.org/']);
// Initiate each request but do not block
$promises = [
'image' => $client->getAsync('/image'),
'png' => $client->getAsync('/image/png'),
'jpeg' => $client->getAsync('/image/jpeg'),
'webp' => $client->getAsync('/image/webp')
];
// Wait on all of the requests to complete. Throws a ConnectException
// if any of the requests fail
$results = Promise\unwrap($promises);
// Wait for the requests to complete, even if some of them fail
$results = Promise\settle($promises)->wait();
// You can access each result using the key provided to the unwrap
// function.
echo $results['image']->getHeader('Content-Length');
echo $results['png']->getHeader('Content-Length');
In this example all 4 requests are executed in parallel. Note: Only IO is async not the handling of the results. But that's probably what you want.
Related
I created a simple API in Lumen (application A) which:
receives PSR-7 request interface
replaces URI of the request to the application B
and sends the request through Guzzle.
public function apiMethod(ServerRequestInterface $psrRequest)
{
$url = $this->getUri();
$psrRequest = $psrRequest->withUri($url);
$response = $this->httpClient->send($psrRequest);
return response($response->getBody(), $response->getStatusCode(), $response->getHeaders());
}
The above code passes data to the application B for the query params, x-www-form-urlencoded, or JSON content type. However, it fails to pass the multipart/form-data. (The file is available in the application A: $psrRequest->getUploadedFiles()).
Edit 1
I tried replacing the Guzzle invocation with the Buzz
$psr18Client = new Browser(new Curl(new Psr17Factory()), new Psr17Factory());
$response = $psr18Client->sendRequest($psrRequest);
but still, it does not make a difference.
Edit 2
Instances of ServerRequestInterface represent a request on the server-side. Guzzle and Buzz are using an instance of RequestInterface to send data. The RequestInterface is missing abstraction over uploaded files. So files should be added manually http://docs.guzzlephp.org/en/stable/request-options.html#multipart
$options = [];
/** #var UploadedFileInterface $uploadedFile */
foreach ($psrRequest->getUploadedFiles() as $uploadedFile) {
$options['multipart'][] = [
'name' => 'file',
'fileName' => $uploadedFile->getClientFilename(),
'contents' => $uploadedFile->getStream()->getContents()
];
}
$response = $this->httpClient->send($psrRequest, $options);
But still no luck with that.
What I am missing? How to change the request so files will be sent properly?
It seems that $options['multipart'] is taken into account when using post method from guzzle. So changing the code to the $response = $this->httpClient->post($psrRequest->getUri(), $options); solves the problem.
Also, it is important not to attach 'content-type header.
I'm trying to send a request to an endpoint, but I don't want to wait for them to respond, as I don't need the response. So I'm using Guzzle, here's how:
$url = 'http://example.com';
$client = new \Guzzelhttp\Client();
$promise = $client->postAsync($url, [
'headers' => ['Some headers and authorization'],
'query' => [
'params' => 'params',
]
])->then(function ($result) {
// I don't need the result. So I just leave it here.
});
$promise->wait();
A I understood, I have to call the wait method on the client in order to actually send the request. But it's totally negates the request being "async" because if the url was not accessible or the server was down, the application would wait for a timeout or any other errors.
So, the question here is, what does Guzzle mean by "async" when you have to wait for the response anyway? And how can I call a truly async request with PHP?
Thanks
What you can do is:
$url = 'http://example.com';
$client = new \Guzzelhttp\Client();
$promise = $client->postAsync($url, [
'headers' => ['Some headers and authorization'],
'query' => [
'params' => 'params',
]
])->then(function ($result) {
return $result->getStatusCode();
})
->wait();
echo $promise;
You need the wait() to be called as the last line so you get the result which will come from your promise.
In this case it will return just the status code.
Just as mentioned in Github is not able to "fire and forget"so i think what you are trying to achieve, like a complete promise like in Vue or React won't work for you here the way you want it to work.
Another approach and what i do personally is to use a try-catch on guzzle requests, so if there is a guzzle error then you catch it and throw an exception.
Call then() method if you don't want to wait for the result:
$client = new GuzzleClient();
$promise = $client->getAsync($url)
$promise->then();
Empty then() call will make an HTTP request without waiting for result, Very similar to
curl_setopt(CURLOPT_RETURNTRANSFER,false)
use Illuminate\Support\Facades\Http;
...Some Code here
$prom = Http::timeout(1)->async()->post($URL_STRING, $ARRAY_DATA)->wait();
... Some more important code here
return "Request sent"; //OR whatever you need to return
This works for me as I don't need to know the response always.
It still uses wait() but because of the small timeout value it doesn't truly wait for the response.
Hope this helps others.
I am using Guzzle to consume a SOAP API. I have to make 6 requests, but in the future this might be even an indeterminate amount of requests.
Problem is that the requests are being send sync, instead of async. Every request on it's own takes +-2.5s. When I send all 6 requests paralell (at least thats what I am trying) it takes +- 15s..
I tried all the examples on Guzzle, the one with a fixed array with $promises, and even the pool (which I need eventually). When I put everything in 1 file (functional) I manage to get the total timing back to 5-6s (which is OK right?). But when I put everything in Objects and functions somehow I do something that makes Guzzle decide to do them sync again.
Checks.php:
public function request()
{
$promises = [];
$promises['requestOne'] = $this->requestOne();
$promises['requestTwo'] = $this->requestTwo();
$promises['requestThree'] = $this->requestThree();
// etc
// wait for all requests to complete
$results = \GuzzleHttp\Promise\settle($promises)->wait();
// Return results
return $results;
}
public function requestOne()
{
$promise = (new API\GetProposition())
->requestAsync();
return $promise;
}
// requestTwo, requestThree, etc
API\GetProposition.php
public function requestAsync()
{
$webservice = new Webservice();
$xmlBody = '<some-xml></some-xml>';
return $webservice->requestAsync($xmlBody, 'GetProposition');
}
Webservice.php
public function requestAsync($xmlBody, $soapAction)
{
$client = new Client([
'base_uri' => 'some_url',
'timeout' => 5.0
]);
$xml = '<soapenv:Envelope>
<soapenv:Body>
'.$xmlBody.'
</soapenv:Body>
</soapenv:Envelope>';
$promise = $client->requestAsync('POST', 'NameOfService', [
'body' => $xml,
'headers' => [
'Content-Type' => 'text/xml',
'SOAPAction' => $soapAction, // SOAP Method to post to
],
]);
return $promise;
}
I changed the XML and some of the parameters for abbreviation. The structure is like this, because I eventually have to talk against multiple API's, to thats why I have a webservice class in between that does all the preparation needed for that API. Most API's have multiple methods/actions that you can call, so that why I have something like. API\GetProposition.
Before the ->wait() statement I can see all $promises pending. So it looks like there are being send async. After ->wait() they have all been fulfilled.
It's all working, minus the performance. All 6 requests don't take more then 2.5 to max 3s.
Hope someone can help.
Nick
The problem was that the $client object was created with every request. Causing the curl multi curl not to be able to know which handler to use.
Found the answer via https://stackoverflow.com/a/46019201/7924519.
I don't know if it's the right terms to employ...
I made an API, in which the answer is sent by the die() function, to avoid some more useless calculations and/or functions calls.
example :
if (isset($authorize->refusalReason)) {
die ($this->api_return(true, [
'resultCode' => $authorize->resultCode,
'reason' => $authorize->refusalReason
]
));
}
// api_return method:
protected function api_return($error, $params = []) {
$time = (new DateTime())->format('Y-m-d H:i:s');
$params = (array) $params;
$params = ['error' => $error, 'date_time' => $time] + $params;
return (Response::json($params)->sendHeaders()->getContent());
}
But my website is based on this API, so I made a function to create a Request and return the contents of it, based on its URI, method, params, and headers:
protected function get_route_contents($uri, $type, $params = [], $headers = []) {
$request = Request::create($uri, $type, $params);
if (Auth::user()->check()) {
$request->headers->set('S-token', Auth::user()->get()->Key);
}
foreach ($headers as $key => $header) {
$request->headers->set($key, $header);
}
// things to merge the Inputs into the new request.
$originalInput = Request::input();
Request::replace($request->input());
$response = Route::dispatch($request);
Request::replace($originalInput);
$response = json_decode($response->getContent());
// This header cancels the one there is in api_return. sendHeaders() makes Content-Type: application/json
header('Content-Type: text/html');
return $response;
}
But now when I'm trying to call an API function, The request in the API dies but dies also my current Request.
public function postCard($token) {
$auth = $this->get_route_contents("/api/v2/booking/payment/card/authorize/$token", 'POST', Input::all());
// the code below is not executed since the API request uses die()
if ($auth->error === false) {
return Redirect::route('appts')->with(['success' => trans('messages.booked_ok')]);
}
return Redirect::back()->with(['error' => $auth->reason]);
}
Do you know if I can handle it better than this ? Any suggestion of how I should turn my code into ?
I know I could just use returns, but I was always wondering if there were any other solutions. I mean, I want to be better, so I wouldn't ask this question if I knew for sure that the only way of doing what I want is using returns.
So it seems that you are calling an API endpoint through your code as if it is coming from the browser(client) and I am assuming that your Route:dispatch is not making any external request(like curl etc)
Now There can be various approaches to handle this:
If you function get_route_contents is going to handle all the requests, then you need to remove the die from your endpoints and simply make them return the data(instead of echoing). Your this "handler" will take care of response.
Make your Endpoint function to have an optional parameter(or some property set in the $request variable), which will tell the function that this is an internal request and data should be returned, when the request comes directly from a browser(client) you can do echo
Make an external call your code using curl etc(only do this if there is no other option)
Good day,
Im trying to develop a web platform using Slim framework. I've done it in MVC way. some of my APIs are used to render the view and some is just built to get data from the db.
for example :
$app->get('/api/getListAdmin', function () use ($app) {
$data = ...//code to get admins' list
echo json_encode($data);
})->name("getListAdmin");
$app->get('/adminpage', function () use ($app) {
// **** METHOD 1 :// get the data using file_get_contents
$result = file_get_contents(APP_ROOT.'api/getListAdmin');
// or
// **** METHOD 2 :// get data using router
$route = $this->app->router->getNamedRoute('getListAdmin');
$result = $route->dispatch();
$result = json_decode($result);
$app->render('adminpage.php', array(
'data' => $result
));
});
I'm trying to call the db handling Api '/api/getListAdmin' within the view related apis '/adminpage'.
based on solutions i have found in the web i tried method 1 and 2 but:
method 1 (using file_get_contents) take a long time to get the data (few seconds on my local environment).
method 2 (router->getNamedRoute->dispatch) seems dosnt work becuz it will render the result in the view even if i use $result = $route->dispatch(); to store the result in a variable but seems dispatch method still render to result to the screen.
I tried to create a new slim app only for db related API but still calling one of them takes quite long time 2 to 3 seconds.
Really appreciate it if some one can help me on what i'm doing wrong or what is the right way to get data from another api.
Thanks
Method 1
This could be another method, creating a Service layer, where redundant code is deleted:
class Api {
function getListAdmin() {
$admins = array("admin1", "admin2", "admin3"); //Retrieve your magic data
return $admins;
}
}
$app->get('/api/getListAdmin', function () use ($app) {
$api = new Api();
$admins = $api->getListAdmin();
echo json_encode($admins);
})->name("getListAdmin");
$app->get('/adminpage', function () use ($app) {
$api = new Api();
$admins = $api->getListAdmin();
$app->render('adminpage.php', array(
'data' => $admins
));
});
Method 2
If you are ok with an overkill method, you could use Httpful:
$app->get('/adminpage', function () use ($app) {
$result = \Httpful\Request::get(APP_ROOT.'api/getListAdmin')->send();
//No need to decode if there is the JSON Content-Type in the response
$result = json_decode($result);
$app->render('adminpage.php', array(
'data' => $result
));
});