@@ -36,6 +36,10 @@ class Client
36
36
protected $ path ;
37
37
/** @var array */
38
38
protected $ curlOptions ;
39
+ /** @var bool $isConcurrentRequest */
40
+ protected $ isConcurrentRequest ;
41
+ /** @var array $savedRequests */
42
+ protected $ savedRequests ;
39
43
/** @var bool */
40
44
protected $ retryOnLimit ;
41
45
@@ -49,10 +53,10 @@ class Client
49
53
/**
50
54
* Initialize the client
51
55
*
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
56
60
*/
57
61
public function __construct ($ host , $ headers = [], $ version = '/v3 ' , $ path = [])
58
62
{
@@ -63,6 +67,8 @@ public function __construct($host, $headers = [], $version = '/v3', $path = [])
63
67
64
68
$ this ->curlOptions = [];
65
69
$ this ->retryOnLimit = false ;
70
+ $ this ->isConcurrentRequest = false ;
71
+ $ this ->savedRequests = [];
66
72
}
67
73
68
74
/**
@@ -125,6 +131,20 @@ public function setRetryOnLimit($retry)
125
131
return $ this ;
126
132
}
127
133
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
+
128
148
/**
129
149
* @return array
130
150
*/
@@ -150,43 +170,93 @@ private function buildUrl($queryParams = null)
150
170
}
151
171
152
172
/**
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 )
165
182
{
166
- $ curl = curl_init ($ url );
167
-
168
183
$ options = array_merge (
169
184
[
170
185
CURLOPT_RETURNTRANSFER => true ,
171
186
CURLOPT_HEADER => 1 ,
172
187
CURLOPT_CUSTOMREQUEST => strtoupper ($ method ),
173
188
CURLOPT_SSL_VERIFYPEER => false ,
174
- CURLOPT_FAILONERROR => false ,
189
+ CURLOPT_FAILONERROR => false
175
190
],
176
191
$ this ->curlOptions
177
192
);
178
193
179
- curl_setopt_array ($ curl , $ options );
180
-
181
194
if (isset ($ headers )) {
182
- $ this ->headers = array_merge ($ this ->headers , $ headers );
195
+ $ headers = array_merge ($ this ->headers , $ headers );
196
+ } else {
197
+ $ headers = [];
183
198
}
199
+
184
200
if (isset ($ body )) {
185
201
$ 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 ' ]);
188
204
}
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 );
190
260
191
261
$ response = curl_exec ($ curl );
192
262
$ headerSize = curl_getinfo ($ curl , CURLINFO_HEADER_SIZE );
@@ -212,6 +282,66 @@ public function makeRequest($method, $url, $body = null, $headers = null, $retry
212
282
return $ response ;
213
283
}
214
284
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
+
215
345
/**
216
346
* Add variable values to the url.
217
347
* (e.g. /your/api/{variable_value}/call)
@@ -242,7 +372,7 @@ public function _($name = null)
242
372
* @param string $name name of the dynamic method call or HTTP verb
243
373
* @param array $args parameters passed with the method call
244
374
*
245
- * @return Client|Response object
375
+ * @return Client|Response|Response[]|null object
246
376
*/
247
377
public function __call ($ name , $ args )
248
378
{
@@ -253,12 +383,27 @@ public function __call($name, $args)
253
383
return $ this ->_ ();
254
384
}
255
385
386
+ // send all saved requests
387
+ if (($ name === 'send ' ) && $ this ->isConcurrentRequest ) {
388
+ return $ this ->makeAllRequests ();
389
+ }
390
+
256
391
if (in_array ($ name , $ this ->methods , true )) {
257
392
$ body = isset ($ args [0 ]) ? $ args [0 ] : null ;
258
393
$ queryParams = isset ($ args [1 ]) ? $ args [1 ] : null ;
259
394
$ url = $ this ->buildUrl ($ queryParams );
260
395
$ headers = isset ($ args [2 ]) ? $ args [2 ] : null ;
261
396
$ 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
+
262
407
return $ this ->makeRequest ($ name , $ url , $ body , $ headers , $ retryOnLimit );
263
408
}
264
409
0 commit comments