@@ -36,6 +36,10 @@ class Client
3636 protected $ path ;
3737 /** @var array */
3838 protected $ curlOptions ;
39+ /** @var bool $isConcurrentRequest */
40+ protected $ isConcurrentRequest ;
41+ /** @var array $savedRequests */
42+ protected $ savedRequests ;
3943 /** @var bool */
4044 protected $ retryOnLimit ;
4145
@@ -49,10 +53,10 @@ class Client
4953 /**
5054 * Initialize the client
5155 *
52- * @param string $host the base url (e.g. https://api.sendgrid.com)
53- * @param array $headers global request headers
54- * @param string $version api version (configurable)
55- * @param array $path holds the segments of the url path
56+ * @param string $host the base url (e.g. https://api.sendgrid.com)
57+ * @param array $headers global request headers
58+ * @param string $version api version (configurable)
59+ * @param array $path holds the segments of the url path
5660 */
5761 public function __construct ($ host , $ headers = [], $ version = '/v3 ' , $ path = [])
5862 {
@@ -63,6 +67,8 @@ public function __construct($host, $headers = [], $version = '/v3', $path = [])
6367
6468 $ this ->curlOptions = [];
6569 $ this ->retryOnLimit = false ;
70+ $ this ->isConcurrentRequest = false ;
71+ $ this ->savedRequests = [];
6672 }
6773
6874 /**
@@ -125,6 +131,20 @@ public function setRetryOnLimit($retry)
125131 return $ this ;
126132 }
127133
134+ /**
135+ * set concurrent request flag
136+ *
137+ * @param bool $isConcurrent
138+ *
139+ * @return Client
140+ */
141+ public function setIsConcurrentRequest ($ isConcurrent )
142+ {
143+ $ this ->isConcurrentRequest = $ isConcurrent ;
144+
145+ return $ this ;
146+ }
147+
128148 /**
129149 * @return array
130150 */
@@ -150,43 +170,93 @@ private function buildUrl($queryParams = null)
150170 }
151171
152172 /**
153- * Make the API call and return the response. This is separated into
154- * it's own function, so we can mock it easily for testing.
155- *
156- * @param string $method the HTTP verb
157- * @param string $url the final url to call
158- * @param array $body request body
159- * @param array $headers any additional request headers
160- * @param bool $retryOnLimit should retry if rate limit is reach?
161- *
162- * @return Response object
163- */
164- public function makeRequest ($ method , $ url , $ body = null , $ headers = null , $ retryOnLimit = false )
173+ * Creates curl options for a request
174+ * this function does not mutate any private variables
175+ *
176+ * @param string $method
177+ * @param array $body
178+ * @param array $headers
179+ * @return array
180+ */
181+ private function createCurlOptions ($ method , $ body = null , $ headers = null )
165182 {
166- $ curl = curl_init ($ url );
167-
168183 $ options = array_merge (
169184 [
170185 CURLOPT_RETURNTRANSFER => true ,
171186 CURLOPT_HEADER => 1 ,
172187 CURLOPT_CUSTOMREQUEST => strtoupper ($ method ),
173188 CURLOPT_SSL_VERIFYPEER => false ,
174- CURLOPT_FAILONERROR => false ,
189+ CURLOPT_FAILONERROR => false
175190 ],
176191 $ this ->curlOptions
177192 );
178193
179- curl_setopt_array ($ curl , $ options );
180-
181194 if (isset ($ headers )) {
182- $ this ->headers = array_merge ($ this ->headers , $ headers );
195+ $ headers = array_merge ($ this ->headers , $ headers );
196+ } else {
197+ $ headers = [];
183198 }
199+
184200 if (isset ($ body )) {
185201 $ encodedBody = json_encode ($ body );
186- curl_setopt ( $ curl , CURLOPT_POSTFIELDS , $ encodedBody) ;
187- $ this -> headers = array_merge ($ this -> headers , ['Content-Type: application/json ' ]);
202+ $ options [ CURLOPT_POSTFIELDS ] = $ encodedBody ;
203+ $ headers = array_merge ($ headers , ['Content-Type: application/json ' ]);
188204 }
189- curl_setopt ($ curl , CURLOPT_HTTPHEADER , $ this ->headers );
205+ $ options [CURLOPT_HTTPHEADER ] = $ headers ;
206+
207+ return $ options ;
208+ }
209+
210+ /**
211+ * @param array $requestData
212+ * e.g. ['method' => 'POST', 'url' => 'www.example.com', 'body' => 'test body', 'headers' => []]
213+ * @param bool $retryOnLimit
214+ *
215+ * @return array
216+ */
217+ private function createSavedRequest ($ requestData , $ retryOnLimit = false )
218+ {
219+ return array_merge ($ requestData , ['retryOnLimit ' => $ retryOnLimit ]);
220+ }
221+
222+ /**
223+ * @param array $requests
224+ *
225+ * @return array
226+ */
227+ private function createCurlMultiHandle ($ requests )
228+ {
229+ $ channels = [];
230+ $ multiHandle = curl_multi_init ();
231+
232+ foreach ($ requests as $ id => $ data ) {
233+ $ channels [$ id ] = curl_init ($ data ['url ' ]);
234+ $ curlOpts = $ this ->createCurlOptions ($ data ['method ' ], $ data ['body ' ], $ data ['headers ' ]);
235+ curl_setopt_array ($ channels [$ id ], $ curlOpts );
236+ curl_multi_add_handle ($ multiHandle , $ channels [$ id ]);
237+ }
238+
239+ return [$ channels , $ multiHandle ];
240+ }
241+
242+ /**
243+ * Make the API call and return the response. This is separated into
244+ * it's own function, so we can mock it easily for testing.
245+ *
246+ * @param string $method the HTTP verb
247+ * @param string $url the final url to call
248+ * @param array $body request body
249+ * @param array $headers any additional request headers
250+ * @param bool $retryOnLimit should retry if rate limit is reach?
251+ *
252+ * @return Response object
253+ */
254+ public function makeRequest ($ method , $ url , $ body = null , $ headers = null , $ retryOnLimit = false )
255+ {
256+ $ curl = curl_init ($ url );
257+
258+ $ curlOpts = $ this ->createCurlOptions ($ method , $ body , $ headers );
259+ curl_setopt_array ($ curl , $ curlOpts );
190260
191261 $ response = curl_exec ($ curl );
192262 $ headerSize = curl_getinfo ($ curl , CURLINFO_HEADER_SIZE );
@@ -212,6 +282,66 @@ public function makeRequest($method, $url, $body = null, $headers = null, $retry
212282 return $ response ;
213283 }
214284
285+ /**
286+ * Send all saved requests at once
287+ *
288+ * @param array $requests
289+ * @return Response[]
290+ */
291+ public function makeAllRequests ($ requests = [])
292+ {
293+ if (empty ($ requests )) {
294+ $ requests = $ this ->savedRequests ;
295+ }
296+ list ($ channels , $ multiHandle ) = $ this ->createCurlMultiHandle ($ requests );
297+
298+ // running all requests
299+ $ isRunning = null ;
300+ do {
301+ curl_multi_exec ($ multiHandle , $ isRunning );
302+ } while ($ isRunning );
303+
304+ // get response and close all handles
305+ $ retryRequests = [];
306+ $ responses = [];
307+ $ sleepDurations = 0 ;
308+ foreach ($ channels as $ id => $ ch ) {
309+ $ response = curl_multi_getcontent ($ ch );
310+ $ headerSize = curl_getinfo ($ ch , CURLINFO_HEADER_SIZE );
311+ $ statusCode = curl_getinfo ($ ch , CURLINFO_HTTP_CODE );
312+ $ responseBody = substr ($ response , $ headerSize );
313+
314+ $ responseHeaders = substr ($ response , 0 , $ headerSize );
315+ $ responseHeaders = explode ("\n" , $ responseHeaders );
316+ $ responseHeaders = array_map ('trim ' , $ responseHeaders );
317+
318+ $ response = new Response ($ statusCode , $ responseBody , $ responseHeaders );
319+ if (($ statusCode === 429 ) && $ requests [$ id ]['retryOnLimit ' ]) {
320+ $ headers = $ response ->headers (true );
321+ $ sleepDurations = max ($ sleepDurations , $ headers ['X-Ratelimit-Reset ' ] - time ());
322+ $ requestData = [
323+ 'method ' => $ requests [$ id ]['method ' ],
324+ 'url ' => $ requests [$ id ]['url ' ],
325+ 'body ' => $ requests [$ id ]['body ' ],
326+ 'headers ' =>$ headers ,
327+ ];
328+ $ retryRequests [] = $ this ->createSavedRequest ($ requestData , false );
329+ } else {
330+ $ responses [] = $ response ;
331+ }
332+
333+ curl_multi_remove_handle ($ multiHandle , $ ch );
334+ }
335+ curl_multi_close ($ multiHandle );
336+
337+ // retry requests
338+ if (!empty ($ retryRequests )) {
339+ sleep ($ sleepDurations > 0 ? $ sleepDurations : 0 );
340+ $ responses = array_merge ($ responses , $ this ->makeAllRequests ($ retryRequests ));
341+ }
342+ return $ responses ;
343+ }
344+
215345 /**
216346 * Add variable values to the url.
217347 * (e.g. /your/api/{variable_value}/call)
@@ -242,7 +372,7 @@ public function _($name = null)
242372 * @param string $name name of the dynamic method call or HTTP verb
243373 * @param array $args parameters passed with the method call
244374 *
245- * @return Client|Response object
375+ * @return Client|Response|Response[]|null object
246376 */
247377 public function __call ($ name , $ args )
248378 {
@@ -253,12 +383,27 @@ public function __call($name, $args)
253383 return $ this ->_ ();
254384 }
255385
386+ // send all saved requests
387+ if (($ name === 'send ' ) && $ this ->isConcurrentRequest ) {
388+ return $ this ->makeAllRequests ();
389+ }
390+
256391 if (in_array ($ name , $ this ->methods , true )) {
257392 $ body = isset ($ args [0 ]) ? $ args [0 ] : null ;
258393 $ queryParams = isset ($ args [1 ]) ? $ args [1 ] : null ;
259394 $ url = $ this ->buildUrl ($ queryParams );
260395 $ headers = isset ($ args [2 ]) ? $ args [2 ] : null ;
261396 $ retryOnLimit = isset ($ args [3 ]) ? $ args [3 ] : $ this ->retryOnLimit ;
397+
398+ if ($ this ->isConcurrentRequest ) {
399+ // save request to be sent later
400+ $ this ->savedRequests [] = $ this ->createSavedRequest (
401+ ['method ' => $ name , 'url ' => $ url , 'body ' => $ body , 'headers ' => $ headers ],
402+ $ retryOnLimit
403+ );
404+ return null ;
405+ }
406+
262407 return $ this ->makeRequest ($ name , $ url , $ body , $ headers , $ retryOnLimit );
263408 }
264409
0 commit comments