Passed
Push — master ( 8247dd...ae3e36 )
by Joe Nilson
13:53
created

Client   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 491
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 55
eloc 150
c 1
b 1
f 0
dl 0
loc 491
rs 6

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getHost() 0 3 1
A setVerifySSLCerts() 0 5 1
A getHeaders() 0 3 1
A setIsConcurrentRequest() 0 5 1
A buildUrl() 0 9 3
A __construct() 0 18 4
B __call() 0 34 10
A parseResponse() 0 12 1
A retryRequest() 0 6 2
B createCurlOptions() 0 33 8
A createCurlMultiHandle() 0 13 2
A _() 0 12 2
A createSavedRequest() 0 3 1
A getVersion() 0 3 1
B makeAllRequests() 0 51 9
A setCurlOptions() 0 5 1
A getPath() 0 3 1
A makeRequest() 0 24 4
A setRetryOnLimit() 0 5 1
A getCurlOptions() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Client often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Client, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * HTTP Client library
5
 */
6
7
namespace SendGrid;
8
9
use SendGrid\Exception\InvalidRequest;
10
11
/**
12
 * Class Client
13
 * @version 3.9.5
14
 *
15
 * Quickly and easily access any REST or REST-like API.
16
 *
17
 * @method Response get($body = null, $query = null, $headers = null, $retryOnLimit = null)
18
 * @method Response post($body = null, $query = null, $headers = null, $retryOnLimit = null)
19
 * @method Response patch($body = null, $query = null, $headers = null, $retryOnLimit = null)
20
 * @method Response put($body = null, $query = null, $headers = null, $retryOnLimit = null)
21
 * @method Response delete($body = null, $query = null, $headers = null, $retryOnLimit = null)
22
 *
23
 * @method Client version($value)
24
 * @method Client|Response send()
25
 *
26
 * Adding all the endpoints as a method so code completion works
27
 *
28
 * General
29
 * @method Client stats()
30
 * @method Client search()
31
 * @method Client monthly()
32
 * @method Client sums()
33
 * @method Client monitor()
34
 * @method Client test()
35
 *
36
 * Access settings
37
 * @method Client access_settings()
38
 * @method Client activity()
39
 * @method Client whitelist()
40
 *
41
 * Alerts
42
 * @method Client alerts()
43
 *
44
 * Api keys
45
 * @method Client api_keys()
46
 *
47
 * ASM
48
 * @method Client asm()
49
 * @method Client groups()
50
 * @method Client suppressions()
51
 *
52
 * Browsers
53
 * @method Client browsers()
54
 *
55
 * Campaigns
56
 * @method Client campaigns()
57
 * @method Client schedules()
58
 * @method Client now()
59
 *
60
 * Categories
61
 * @method Client categories()
62
 *
63
 * Clients
64
 * @method Client clients()
65
 *
66
 * Marketing
67
 * @method Client marketing()
68
 * @method Client contacts()
69
 * @method Client count()
70
 * @method Client exports()
71
 * @method Client imports()
72
 * @method Client lists()
73
 * @method Client field_definitions()
74
 * @method Client segments()
75
 * @method Client singlesends()
76
 *
77
 * Devices
78
 * @method Client devices()
79
 *
80
 * Geo
81
 * @method Client geo()
82
 *
83
 * Ips
84
 * @method Client ips()
85
 * @method Client assigned()
86
 * @method Client pools()
87
 * @method Client warmup()
88
 *
89
 * Mail
90
 * @method Client mail()
91
 * @method Client batch()
92
 *
93
 * Mailbox Providers
94
 * @method Client mailbox_providers()
95
 *
96
 * Mail settings
97
 * @method Client mail_settings()
98
 * @method Client address_whitelist()
99
 * @method Client bcc()
100
 * @method Client bounce_purge()
101
 * @method Client footer()
102
 * @method Client forward_bounce()
103
 * @method Client forward_spam()
104
 * @method Client plain_content()
105
 * @method Client spam_check()
106
 * @method Client template()
107
 *
108
 * Partner settings
109
 * @method Client partner_settings()
110
 * @method Client new_relic()
111
 *
112
 * Scopes
113
 * @method Client scopes()
114
 *
115
 * Senders
116
 * @method Client senders()
117
 * @method Client resend_verification()
118
 *
119
 * Sub Users
120
 * @method Client subusers()
121
 * @method Client reputations()
122
 *
123
 * Suppressions
124
 * @method Client suppression()
125
 * @method Client global()
126
 * @method Client blocks()
127
 * @method Client bounces()
128
 * @method Client invalid_emails()
129
 * @method Client spam_reports()
130
 * @method Client unsubscribes()
131
 *
132
 * Templates
133
 * @method Client templates()
134
 * @method Client versions()
135
 * @method Client activate()
136
 *
137
 * Tracking settings
138
 * @method Client tracking_settings()
139
 * @method Client click()
140
 * @method Client google_analytics()
141
 * @method Client open()
142
 * @method Client subscription()
143
 *
144
 * User
145
 * @method Client user()
146
 * @method Client account()
147
 * @method Client credits()
148
 * @method Client email()
149
 * @method Client password()
150
 * @method Client profile()
151
 * @method Client scheduled_sends()
152
 * @method Client enforced_tls()
153
 * @method Client settings()
154
 * @method Client username()
155
 * @method Client webhooks()
156
 * @method Client event()
157
 * @method Client parse()
158
 *
159
 * Missed any? Simply add them by doing: @method Client method()
160
 */
161
class Client
162
{
163
    const TOO_MANY_REQUESTS_HTTP_CODE = 429;
164
165
    /**
166
     * @var string
167
     */
168
    protected $host;
169
170
    /**
171
     * @var array
172
     */
173
    protected $headers;
174
175
    /**
176
     * @var string
177
     */
178
    protected $version;
179
180
    /**
181
     * @var array
182
     */
183
    protected $path;
184
185
    /**
186
     * @var array
187
     */
188
    protected $curlOptions;
189
190
    /**
191
     * @var bool
192
     */
193
    protected $isConcurrentRequest;
194
195
    /**
196
     * @var array
197
     */
198
    protected $savedRequests;
199
200
    /**
201
     * @var bool
202
     */
203
    protected $verifySSLCerts;
204
    
205
    /**
206
     * @var bool
207
     */
208
    protected $retryOnLimit;
209
210
    /**
211
     * Supported HTTP verbs.
212
     *
213
     * @var array
214
     */
215
    private $methods = ['get', 'post', 'patch', 'put', 'delete'];
216
217
    /**
218
     * Initialize the client.
219
     *
220
     * @param string $host           the base url (e.g. https://api.sendgrid.com)
221
     * @param array  $headers        global request headers
222
     * @param string $version        api version (configurable) - this is specific to the SendGrid API
223
     * @param array  $path           holds the segments of the url path
224
     * @param array  $curlOptions    extra options to set during curl initialization
225
     * @param bool   $retryOnLimit   set default retry on limit flag
226
     * @param bool   $verifySSLCerts set default verify certificates flag
227
     */
228
    public function __construct(
229
        $host,
230
        $headers = null,
231
        $version = null,
232
        $path = null,
233
        $curlOptions = null,
234
        $retryOnLimit = false,
235
        $verifySSLCerts = true
236
    ) {
237
        $this->host = $host;
238
        $this->headers = $headers ?: [];
239
        $this->version = $version;
240
        $this->path = $path ?: [];
241
        $this->curlOptions = $curlOptions ?: [];
242
        $this->retryOnLimit = $retryOnLimit;
243
        $this->verifySSLCerts = $verifySSLCerts;
244
        $this->isConcurrentRequest = false;
245
        $this->savedRequests = [];
246
    }
247
248
    /**
249
     * @return string
250
     */
251
    public function getHost()
252
    {
253
        return $this->host;
254
    }
255
256
    /**
257
     * @return array
258
     */
259
    public function getHeaders()
260
    {
261
        return $this->headers;
262
    }
263
264
    /**
265
     * @return string|null
266
     */
267
    public function getVersion()
268
    {
269
        return $this->version;
270
    }
271
272
    /**
273
     * @return array
274
     */
275
    public function getPath()
276
    {
277
        return $this->path;
278
    }
279
280
    /**
281
     * @return array
282
     */
283
    public function getCurlOptions()
284
    {
285
        return $this->curlOptions;
286
    }
287
288
    /**
289
     * Set extra options to set during curl initialization.
290
     *
291
     * @param array $options
292
     *
293
     * @return Client
294
     */
295
    public function setCurlOptions(array $options)
296
    {
297
        $this->curlOptions = $options;
298
299
        return $this;
300
    }
301
302
    /**
303
     * Set default retry on limit flag.
304
     *
305
     * @param bool $retry
306
     *
307
     * @return Client
308
     */
309
    public function setRetryOnLimit($retry)
310
    {
311
        $this->retryOnLimit = $retry;
312
313
        return $this;
314
    }
315
316
    /**
317
     * Set default verify certificates flag
318
     *
319
     * @param bool $verifySSLCerts
320
     *
321
     * @return Client
322
     */
323
    public function setVerifySSLCerts($verifySSLCerts)
324
    {
325
        $this->verifySSLCerts = $verifySSLCerts;
326
327
        return $this;
328
    }
329
330
    /**
331
     * Set concurrent request flag
332
     *
333
     * @param bool $isConcurrent
334
     *
335
     * @return Client
336
     */
337
    public function setIsConcurrentRequest($isConcurrent)
338
    {
339
        $this->isConcurrentRequest = $isConcurrent;
340
341
        return $this;
342
    }
343
344
    /**
345
     * Build the final URL to be passed.
346
     *
347
     * @param array $queryParams an array of all the query parameters
348
     *
349
     * Nested arrays will resolve to multiple instances of the same parameter
350
     *
351
     * @return string
352
     */
353
    private function buildUrl($queryParams = null)
354
    {
355
        $path = '/' . implode('/', $this->path);
356
        if (isset($queryParams)) {
357
            // Regex replaces `[0]=`, `[1]=`, etc. with `=`.
358
            $path .= '?' . preg_replace('/%5B(?:\d|[1-9]\d+)%5D=/', '=', http_build_query($queryParams));
359
        }
360
361
        return sprintf('%s%s%s', $this->host, $this->version ?: '', $path);
362
    }
363
364
    /**
365
     * Creates curl options for a request
366
     * this function does not mutate any private variables.
367
     *
368
     * @param string $method
369
     * @param array  $body
370
     * @param array  $headers
371
     *
372
     * @return array
373
     */
374
    private function createCurlOptions($method, $body = null, $headers = null)
375
    {
376
        $options = [
377
                CURLOPT_RETURNTRANSFER => true,
378
                CURLOPT_HEADER => true,
379
                CURLOPT_CUSTOMREQUEST => strtoupper($method),
380
                CURLOPT_SSL_VERIFYPEER => $this->verifySSLCerts,
381
                CURLOPT_FAILONERROR => false,
382
            ] + $this->curlOptions;
383
384
        if (isset($headers)) {
385
            $headers = array_merge($this->headers, $headers);
386
        } else {
387
            $headers = $this->headers;
388
        }
389
390
        if (isset($body)) {
391
            $encodedBody = json_encode($body);
392
            $options[CURLOPT_POSTFIELDS] = $encodedBody;
393
            $headers = array_merge($headers, ['Content-Type: application/json']);
394
        }
395
        $options[CURLOPT_HTTPHEADER] = $headers;
396
397
        if (class_exists('\\Composer\\CaBundle\\CaBundle') && method_exists('\\Composer\\CaBundle\\CaBundle', 'getSystemCaRootBundlePath')) {
398
            $caPathOrFile = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
399
            if (is_dir($caPathOrFile) || (is_link($caPathOrFile) && is_dir(readlink($caPathOrFile)))) {
400
                $options[CURLOPT_CAPATH] = $caPathOrFile;
401
            } else {
402
                $options[CURLOPT_CAINFO] = $caPathOrFile;
403
            }
404
        }
405
406
        return $options;
407
    }
408
409
    /**
410
     * @param array $requestData  (method, url, body and headers)
411
     * @param bool  $retryOnLimit
412
     *
413
     * @return array
414
     */
415
    private function createSavedRequest(array $requestData, $retryOnLimit = false)
416
    {
417
        return array_merge($requestData, ['retryOnLimit' => $retryOnLimit]);
418
    }
419
420
    /**
421
     * @param array $requests
422
     *
423
     * @return array
424
     */
425
    private function createCurlMultiHandle(array $requests)
426
    {
427
        $channels = [];
428
        $multiHandle = curl_multi_init();
429
430
        foreach ($requests as $id => $data) {
431
            $channels[$id] = curl_init($data['url']);
432
            $curlOpts = $this->createCurlOptions($data['method'], $data['body'], $data['headers']);
433
            curl_setopt_array($channels[$id], $curlOpts);
434
            curl_multi_add_handle($multiHandle, $channels[$id]);
0 ignored issues
show
Bug introduced by
It seems like $multiHandle can also be of type true; however, parameter $multi_handle of curl_multi_add_handle() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

434
            curl_multi_add_handle(/** @scrutinizer ignore-type */ $multiHandle, $channels[$id]);
Loading history...
435
        }
436
437
        return [$channels, $multiHandle];
438
    }
439
440
    /**
441
     * Prepare response object.
442
     *
443
     * @param resource $channel the curl resource
444
     * @param string   $content
445
     *
446
     * @return Response object
447
     */
448
    private function parseResponse($channel, $content)
449
    {
450
        $headerSize = curl_getinfo($channel, CURLINFO_HEADER_SIZE);
451
        $statusCode = curl_getinfo($channel, CURLINFO_HTTP_CODE);
452
453
        $responseBody = mb_substr($content, $headerSize);
454
455
        $responseHeaders = mb_substr($content, 0, $headerSize);
456
        $responseHeaders = explode("\n", $responseHeaders);
457
        $responseHeaders = array_map('trim', $responseHeaders);
458
459
        return new Response($statusCode, $responseBody, $responseHeaders);
460
    }
461
462
    /**
463
     * Retry request.
464
     *
465
     * @param array  $responseHeaders headers from rate limited response
466
     * @param string $method          the HTTP verb
467
     * @param string $url             the final url to call
468
     * @param array  $body            request body
469
     * @param array  $headers         original headers
470
     *
471
     * @return Response response object
472
     *
473
     * @throws InvalidRequest
474
     */
475
    private function retryRequest(array $responseHeaders, $method, $url, $body, $headers)
476
    {
477
        $sleepDurations = $responseHeaders['X-Ratelimit-Reset'] - time();
478
        sleep($sleepDurations > 0 ? $sleepDurations : 0);
479
480
        return $this->makeRequest($method, $url, $body, $headers, false);
481
    }
482
483
    /**
484
     * Make the API call and return the response.
485
     * This is separated into it's own function, so we can mock it easily for testing.
486
     *
487
     * @param string $method       the HTTP verb
488
     * @param string $url          the final url to call
489
     * @param array  $body         request body
490
     * @param array  $headers      any additional request headers
491
     * @param bool   $retryOnLimit should retry if rate limit is reach?
492
     *
493
     * @return Response object
494
     *
495
     * @throws InvalidRequest
496
     */
497
    public function makeRequest($method, $url, $body = null, $headers = null, $retryOnLimit = false)
498
    {
499
        $channel = curl_init($url);
500
501
        $options = $this->createCurlOptions($method, $body, $headers);
502
503
        curl_setopt_array($channel, $options);
504
        $content = curl_exec($channel);
505
506
        if ($content === false) {
507
            throw new InvalidRequest(curl_error($channel), curl_errno($channel));
508
        }
509
510
        $response = $this->parseResponse($channel, $content);
0 ignored issues
show
Bug introduced by
It seems like $channel can also be of type CurlHandle; however, parameter $channel of SendGrid\Client::parseResponse() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

510
        $response = $this->parseResponse(/** @scrutinizer ignore-type */ $channel, $content);
Loading history...
Bug introduced by
It seems like $content can also be of type true; however, parameter $content of SendGrid\Client::parseResponse() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

510
        $response = $this->parseResponse($channel, /** @scrutinizer ignore-type */ $content);
Loading history...
511
512
        if ($retryOnLimit && $response->statusCode() === self::TOO_MANY_REQUESTS_HTTP_CODE) {
513
            $responseHeaders = $response->headers(true);
514
515
            return $this->retryRequest($responseHeaders, $method, $url, $body, $headers);
516
        }
517
518
        curl_close($channel);
519
520
        return $response;
521
    }
522
523
    /**
524
     * Send all saved requests at once.
525
     *
526
     * @param array $requests
527
     *
528
     * @return Response[]
529
     *
530
     * @throws InvalidRequest
531
     */
532
    public function makeAllRequests(array $requests = [])
533
    {
534
        if (empty($requests)) {
535
            $requests = $this->savedRequests;
536
        }
537
        list($channels, $multiHandle) = $this->createCurlMultiHandle($requests);
538
539
        // running all requests
540
        $isRunning = null;
541
        do {
542
            curl_multi_exec($multiHandle, $isRunning);
0 ignored issues
show
Bug introduced by
It seems like $multiHandle can also be of type true; however, parameter $multi_handle of curl_multi_exec() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

542
            curl_multi_exec(/** @scrutinizer ignore-type */ $multiHandle, $isRunning);
Loading history...
543
        } while ($isRunning);
544
545
        // get response and close all handles
546
        $retryRequests = [];
547
        $responses = [];
548
        $sleepDurations = 0;
549
        foreach ($channels as $id => $channel) {
550
            $content = curl_multi_getcontent($channel);
551
552
            if ($content === false) {
553
                throw new InvalidRequest(curl_error($channel), curl_errno($channel));
554
            }
555
556
            $response = $this->parseResponse($channel, $content);
557
558
            if ($requests[$id]['retryOnLimit'] && $response->statusCode() === self::TOO_MANY_REQUESTS_HTTP_CODE) {
559
                $headers = $response->headers(true);
560
                $sleepDurations = max($sleepDurations, $headers['X-Ratelimit-Reset'] - time());
561
                $requestData = [
562
                    'method' => $requests[$id]['method'],
563
                    'url' => $requests[$id]['url'],
564
                    'body' => $requests[$id]['body'],
565
                    'headers' => $headers,
566
                ];
567
                $retryRequests[] = $this->createSavedRequest($requestData, false);
568
            } else {
569
                $responses[] = $response;
570
            }
571
572
            curl_multi_remove_handle($multiHandle, $channel);
0 ignored issues
show
Bug introduced by
It seems like $multiHandle can also be of type true; however, parameter $multi_handle of curl_multi_remove_handle() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

572
            curl_multi_remove_handle(/** @scrutinizer ignore-type */ $multiHandle, $channel);
Loading history...
573
        }
574
        curl_multi_close($multiHandle);
0 ignored issues
show
Bug introduced by
It seems like $multiHandle can also be of type true; however, parameter $multi_handle of curl_multi_close() does only seem to accept CurlMultiHandle|resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

574
        curl_multi_close(/** @scrutinizer ignore-type */ $multiHandle);
Loading history...
575
576
        // retry requests
577
        if (!empty($retryRequests)) {
578
            sleep($sleepDurations > 0 ? $sleepDurations : 0);
579
            $responses = array_merge($responses, $this->makeAllRequests($retryRequests));
580
        }
581
582
        return $responses;
583
    }
584
585
    /**
586
     * Add variable values to the url. (e.g. /your/api/{variable_value}/call)
587
     * Another example: if you have a PHP reserved word, such as and, in your url, you must use this method.
588
     *
589
     * @param string $name name of the url segment
590
     *
591
     * @return Client object
592
     */
593
    public function _($name = null)
594
    {
595
        if (isset($name)) {
596
            $this->path[] = $name;
597
        }
598
        $client = new static($this->host, $this->headers, $this->version, $this->path);
599
        $client->setCurlOptions($this->curlOptions);
600
        $client->setVerifySSLCerts($this->verifySSLCerts);
601
        $client->setRetryOnLimit($this->retryOnLimit);
602
        $this->path = [];
603
604
        return $client;
605
    }
606
607
    /**
608
     * Dynamically add method calls to the url, then call a method.
609
     * (e.g. client.name.name.method()).
610
     *
611
     * @param string $name name of the dynamic method call or HTTP verb
612
     * @param array  $args parameters passed with the method call
613
     *
614
     * @return Client|Response|Response[]|null object
615
     *
616
     * @throws InvalidRequest
617
     */
618
    public function __call($name, $args)
619
    {
620
        $name = mb_strtolower($name);
621
622
        if ($name === 'version') {
623
            $this->version = $args[0];
624
625
            return $this->_();
626
        }
627
628
        // send all saved requests
629
        if (($name === 'send') && $this->isConcurrentRequest) {
630
            return $this->makeAllRequests();
631
        }
632
633
        if (\in_array($name, $this->methods, true)) {
634
            $body = isset($args[0]) ? $args[0] : null;
635
            $queryParams = isset($args[1]) ? $args[1] : null;
636
            $url = $this->buildUrl($queryParams);
637
            $headers = isset($args[2]) ? $args[2] : null;
638
            $retryOnLimit = isset($args[3]) ? $args[3] : $this->retryOnLimit;
639
640
            if ($this->isConcurrentRequest) {
641
                // save request to be sent later
642
                $requestData = ['method' => $name, 'url' => $url, 'body' => $body, 'headers' => $headers];
643
                $this->savedRequests[] = $this->createSavedRequest($requestData, $retryOnLimit);
644
645
                return null;
646
            }
647
648
            return $this->makeRequest($name, $url, $body, $headers, $retryOnLimit);
649
        }
650
651
        return $this->_($name);
652
    }
653
}
654