Completed
Pull Request — develop (#34)
by Jimmy
03:23
created

Client   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 553
Duplicated Lines 0 %

Test Coverage

Coverage 83.19%

Importance

Changes 0
Metric Value
wmc 43
eloc 121
dl 0
loc 553
ccs 99
cts 119
cp 0.8319
rs 8.96
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A getUrl() 0 3 1
A getIntegrator() 0 3 1
A setClientId() 0 5 1
A setHeaders() 0 5 1
A getPassword() 0 3 1
A buildOptions() 0 6 1
A processResponse() 0 22 3
A getClientId() 0 3 1
A setIntegrator() 0 5 1
A getVersion() 0 3 1
A emptyHeaders() 0 5 1
A setPassword() 0 5 1
A __construct() 0 6 1
A setVersion() 0 18 2
A __call() 0 13 5
A buildUri() 0 17 4
A addHeader() 0 7 2
A request() 0 29 4
A buildAuth() 0 7 2
A getHeaders() 0 23 2
A isLastPage() 0 3 1
A setPageSize() 0 5 1
A processError() 0 7 2
A isCollection() 0 8 1
A setUrl() 0 9 2

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
namespace Spinen\ConnectWise\Api;
4
5
use GuzzleHttp\Client as Guzzle;
6
use GuzzleHttp\Exception\GuzzleException;
7
use GuzzleHttp\Exception\RequestException;
8
use GuzzleHttp\Psr7;
9
use GuzzleHttp\Psr7\Response;
10
use InvalidArgumentException;
11
use Spinen\ConnectWise\Exceptions\MalformedRequest;
12
use Spinen\ConnectWise\Support\Collection;
13
use Spinen\ConnectWise\Support\Model;
14
use Spinen\ConnectWise\Support\ModelResolver;
15
16
/**
17
 * Class Client
18
 *
19
 * @package Spinen\ConnectWise\Api
20
 *
21
 * @method Collection|Model delete(string $resource, array $options = [])
22
 * @method Collection|Model get(string $resource, array $options = [])
23
 * @method Collection|Model getAll(string $resource, array $options = [])
24
 * @method Collection|Model head(string $resource, array $options = [])
25
 * @method Collection|Model patch(string $resource, array $options = [])
26
 * @method Collection|Model post(string $resource, array $options = [])
27
 * @method Collection|Model put(string $resource, array $options = [])
28
 */
29
class Client
30
{
31
    /**
32
     * The Client Id
33
     *
34
     * @var string
35
     * @see https://developer.connectwise.com/ClientID
36
     */
37
    protected $clientId;
38
39
    /**
40
     * @var Guzzle
41
     */
42
    protected $guzzle;
43
44
    /**
45
     * Headers that needs to be used with token
46
     *
47
     * @var array
48
     */
49
    protected $headers = [];
50
51
    /**
52
     * Integrator username to make global calls
53
     *
54
     * @var
55
     */
56
    protected $integrator;
57
58
    /**
59
     * Current page
60
     *
61
     * @var integer
62
     */
63
    protected $page;
64
65
    /**
66
     * Number or records to retrieve
67
     *
68
     * @var int
69
     */
70
    protected $page_size = 100;
71
72
    /**
73
     * Integration password for global calls
74
     *
75
     * @var string
76
     */
77
    protected $password;
78
79
    /**
80
     * Resolves a model for the uri
81
     *
82
     * @var ModelResolver
83
     */
84
    protected $resolver;
85
86
    /**
87
     * Public & private keys to log into CW
88
     *
89
     * @var Token
90
     */
91
    protected $token;
92
93
    /**
94
     * URL to the ConnectWise installation
95
     *
96
     * @var string
97
     */
98
    protected $url;
99
100
    /**
101
     * Supported verbs
102
     *
103
     * @var array
104
     */
105
    protected $verbs = [
106
        'delete',
107
        'get',
108
        'head',
109
        'put',
110
        'post',
111
        'patch',
112
    ];
113
114
    /**
115
     * Version of the API being requested
116
     *
117
     * @var string
118
     */
119
    protected $version;
120
121
    /**
122
     * Client constructor.
123
     *
124
     * @param Token $token
125
     * @param Guzzle $guzzle
126
     * @param ModelResolver $resolver
127
     * @param string $version Version of the models to use with the API responses
128
     */
129 23
    public function __construct(Token $token, Guzzle $guzzle, ModelResolver $resolver, $version = null)
130
    {
131 23
        $this->token = $token;
132 23
        $this->guzzle = $guzzle;
133 23
        $this->resolver = $resolver;
134 23
        $this->setVersion($version ?? '2019.3');
135
    }
136
137
    /**
138
     * Magic method to allow short cut to the request types
139
     *
140
     * @param string $verb
141
     * @param array $args
142
     *
143
     * @return Collection|Model|Response
144
     * @throws GuzzleException
145
     * @throws MalformedRequest
146
     */
147 11
    public function __call($verb, $args)
148
    {
149 11
        if (count($args) < 1) {
150 1
            throw new InvalidArgumentException('Magic request methods require a resource and optional options array');
151
        }
152
153 10
        ($verb === 'getAll') ? $this->page = 1 && $verb = 'get' : $this->page = 0;
0 ignored issues
show
Documentation Bug introduced by
The property $page was declared of type integer, but 1 && $verb = 'get' is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
154
155 10
        if (!in_array($verb, $this->verbs)) {
156 1
            throw new InvalidArgumentException(sprintf("Unsupported verb [%s] was requested.", $verb));
157
        }
158
159 9
        return $this->request($verb, $args[0], $args[1] ?? []);
160
    }
161
162
    /**
163
     * Adds key/value pair to the header to be sent
164
     *
165
     * @param array $header
166
     *
167
     * @return $this
168
     */
169 3
    public function addHeader(array $header)
170
    {
171 3
        foreach ($header as $key => $value) {
172 3
            $this->headers[$key] = $value;
173
        }
174
175 3
        return $this;
176
    }
177
178
    /**
179
     * Build authorization headers to send CW API
180
     *
181
     * @return string
182
     */
183 14
    public function buildAuth()
184
    {
185 14
        if ($this->token->needsRefreshing()) {
186 1
            $this->token->refresh($this);
187
        }
188
189 14
        return 'Basic ' . base64_encode($this->token->getUsername() . ':' . $this->token->getPassword());
190
    }
191
192
    /**
193
     * Build the options to send to API
194
     *
195
     * We allays need to login with Basic Auth, so add the "auth" option for Guzzle to use when logging in.
196
     * Additionally, pass any headers that have been set.
197
     *
198
     * @param array $options
199
     *
200
     * @return array
201
     */
202 10
    public function buildOptions(array $options = [])
203
    {
204 10
        return array_merge_recursive(
205 10
            $options,
206
            [
207 10
                'headers' => $this->getHeaders(),
208
            ]
209
        );
210
    }
211
212
    /**
213
     * Build the full path to the CW resource
214
     *
215
     * @param string $resource
216
     *
217
     * @return string
218
     * @throws MalformedRequest
219
     */
220 11
    public function buildUri($resource)
221
    {
222
        // TODO: Check to see if resource already starts with Url to not have to trim it
223 11
        $uri = $this->getUrl() . ltrim($resource, '/');
224
225
        // For getAll calls, make sure to add pageSize & page to request
226 11
        if ($this->page) {
227
            $uri .= (preg_match('/\\?/u', $uri) ? '&' : '?') . 'pageSize=' . $this->page_size . '&page=' . $this->page;
228
        }
229
230 11
        if (strlen($uri) > 2000) {
231 1
            throw new MalformedRequest(
232 1
                sprintf("The uri is too long. It is %s character(s) over the 2000 limit.", strlen($uri) - 2000)
233
            );
234
        }
235
236 11
        return $uri;
237
    }
238
239
    /**
240
     * Remove all set headers
241
     *
242
     * @return $this
243
     */
244 1
    public function emptyHeaders()
245
    {
246 1
        $this->setHeaders([]);
247
248 1
        return $this;
249
    }
250
251
    /**
252
     * Expose the client id
253
     *
254
     * @return string
255
     */
256 12
    public function getClientId()
257
    {
258 12
        return $this->clientId;
259
    }
260
261
    /**
262
     * The headers to send
263
     *
264
     * When making an integrator call (expired token), then you have to only send the "x-cw-usertype" header.
265
     *
266
     * @return array
267
     */
268 12
    public function getHeaders()
269
    {
270
        $authorization_headers = [
271 12
            'clientId'      => $this->getClientId(),
272 12
            'Authorization' => $this->buildAuth(),
273
        ];
274
275 12
        if ($this->token->isForUser($this->getIntegrator())) {
276 1
            return array_merge(
277
                [
278 1
                    'x-cw-usertype' => 'integrator',
279
                ],
280 1
                $authorization_headers
281
            );
282
        }
283
284 11
        return array_merge(
285
            [
286 11
                'x-cw-usertype' => 'member',
287 11
                'Accept'        => 'application/vnd.connectwise.com+json; version=' . $this->getVersion(),
288
            ],
289 11
            $authorization_headers,
290 11
            $this->headers
291
        );
292
    }
293
294
    /**
295
     * Expose the integrator username
296
     *
297
     * @return string
298
     */
299 13
    public function getIntegrator()
300
    {
301 13
        return $this->integrator;
302
    }
303
304
    /**
305
     * Expose the integrator password
306
     *
307
     * @return string
308
     */
309 1
    public function getPassword()
310
    {
311 1
        return $this->password;
312
    }
313
314
    /**
315
     * Expose the url
316
     *
317
     * @return string
318
     */
319 11
    public function getUrl()
320
    {
321 11
        return $this->url . '/v4_6_release/apis/3.0/';
322
    }
323
324
    /**
325
     * Expose the version
326
     *
327
     * @return string
328
     */
329 12
    public function getVersion()
330
    {
331 12
        return $this->version;
332
    }
333
334
    /**
335
     * Check to see if the array is a collection
336
     *
337
     * @param array $array
338
     *
339
     * @return bool
340
     */
341 8
    protected function isCollection(array $array)
342
    {
343
        // Keys of the array
344 8
        $keys = array_keys($array);
345
346
        // If the array keys of the keys match the keys, then the array must
347
        // not be associative (e.g. the keys array looked like {0:0, 1:1...}).
348 8
        return array_keys($keys) === $keys;
349
    }
350
351
    /**
352
     * Check to see it there are more pages in a paginated call
353
     *
354
     * For paginated calls, ConnectWise returns a "Link" property in teh header with a single string.
355
     *
356
     * There appears to be 3 potential responses...
357
     *  1) null -- The number of items in the collection is < the pageSize
358
     *  2) string ends with rel="last" -- More pages to go
359
     *  3) string ends with rel="first" -- On the last page
360
     *
361
     * Here are some examples...
362
     *  <https://some.host/v4_6_release/apis/3.0/finance/agreements&pageSize=10&page=2>; rel="next", \
363
     *      <https://some.host/v4_6_release/apis/3.0/finance/agreements&pageSize=10&page=3>; rel="last”
364
     *
365
     *  <https://some.host/v4_6_release/apis/3.0/finance/agreements&pageSize=10&page=3>; rel="next", \
366
     *      <https://some.host/v4_6_release/apis/3.0/finance/agreements&pageSize=10&page=1>; rel="first”
367
     *
368
     * @param Response $response
369
     *
370
     * @return boolean
371
     */
372
    protected function isLastPage(Response $response)
373
    {
374
        return !(bool)preg_match('/rel="last"$/u', $response->getHeader('Link')[0] ?? null);
375
    }
376
377
    /**
378
     * Process the error received from ConnectWise
379
     *
380
     * @param RequestException $exception
381
     */
382
    protected function processError(RequestException $exception)
383
    {
384
        echo Psr7\str($exception->getRequest());
0 ignored issues
show
Bug introduced by
The function str was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

384
        echo /** @scrutinizer ignore-call */ Psr7\str($exception->getRequest());
Loading history...
385
386
        // TODO: Figure out what to really do with an error...
387
        if ($exception->hasResponse()) {
388
            echo Psr7\str($exception->getResponse());
389
        }
390
    }
391
392
    /**
393
     * Parse the response for the given resource
394
     *
395
     * @param string $resource
396
     * @param Response $response
397
     *
398
     * @return Collection|Model|Response
399
     */
400 9
    protected function processResponse($resource, Response $response)
401
    {
402 9
        $response = (array)json_decode($response->getBody(), true);
403
404 9
        if ($model = $this->resolver->find($resource, $this->getVersion())) {
405 8
            if ($this->isCollection($response)) {
406 7
                $response = array_map(
407
                    function ($item) use ($model) {
408 1
                        $item = new $model($item, $this);
409
410 1
                        return $item;
411 7
                    },
412 7
                    $response
413
                );
414
415 7
                return new Collection($response);
416
            }
417
418 1
            return new $model($response, $this);
419
        }
420
421 1
        return $response;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response returns the type array which is incompatible with the documented return type GuzzleHttp\Psr7\Response...nnectWise\Support\Model.
Loading history...
422
    }
423
424
    /**
425
     * Make call to the resource
426
     *
427
     * @param string $method
428
     * @param string $resource
429
     * @param array|null $options
430
     *
431
     * @return Collection|Model|Response
432
     * @throws GuzzleException
433
     * @throws MalformedRequest
434
     */
435 9
    protected function request($method, $resource, array $options = [])
436
    {
437
        try {
438 9
            $response = $this->guzzle->request($method, $this->buildUri($resource), $this->buildOptions($options));
439
440 9
            $processed = $this->processResponse($resource, $response);
441
442
            // Are we doing a getAll?
443 9
            if ($this->page) {
444
                while (!$this->isLastPage($response)) {
445
                    $this->page = $this->page + 1;
446
447
                    // Make next call
448
                    $response = $this->guzzle->request(
449
                        $method,
450
                        $this->buildUri($resource),
451
                        $this->buildOptions($options)
452
                    );
453
454
                    $processed = $processed->merge($this->processResponse($resource, $response));
455
                }
456
457
                // Reset getAll in case using a singleton
458
                $this->page = null;
459
            }
460
461 9
            return $processed;
462
        } catch (RequestException $e) {
463
            $this->processError($e);
464
        }
465
    }
466
467
    /**
468
     * Set the Client Id
469
     *
470
     * @param string $clientId
471
     *
472
     * @return $this
473
     */
474 23
    public function setClientId($clientId)
475
    {
476 23
        $this->clientId = $clientId;
477
478 23
        return $this;
479
    }
480
481
    /**
482
     * Set the headers
483
     *
484
     * There is an "addHeader" method to push a single header onto the stack.  Otherwise,this replaces the headers.
485
     *
486
     * @param array $headers
487
     *
488
     * @return $this
489
     */
490 1
    public function setHeaders(array $headers)
491
    {
492 1
        $this->headers = $headers;
493
494 1
        return $this;
495
    }
496
497
    /**
498
     * Set the integrator username
499
     *
500
     * @param string $integrator
501
     *
502
     * @return $this
503
     */
504 5
    public function setIntegrator($integrator)
505
    {
506 5
        $this->integrator = $integrator;
507
508 5
        return $this;
509
    }
510
511
    /**
512
     * Set the integrator password
513
     *
514
     * @param string $password
515
     *
516
     * @return $this
517
     */
518 2
    public function setPassword($password)
519
    {
520 2
        $this->password = $password;
521
522 2
        return $this;
523
    }
524
525
    /**
526
     * Set the page size
527
     *
528
     * @param integer $page_size
529
     *
530
     * @return $this
531
     */
532
    public function setPageSize($page_size)
533
    {
534
        $this->page_size = $page_size;
535
536
        return $this;
537
    }
538
539
    /**
540
     * Set the URL to ConnectWise
541
     *
542
     * @param string $url
543
     *
544
     * @return $this
545
     */
546 3
    public function setUrl($url)
547
    {
548 3
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
549 1
            throw new InvalidArgumentException(sprintf("The URL provided[%] is not a valid format.", $url));
550
        }
551
552 2
        $this->url = rtrim($url, '/');
553
554 2
        return $this;
555
    }
556
557
    /**
558
     * Set the version of the API response models
559
     *
560
     * @param string $version
561
     *
562
     * @return $this
563
     */
564 23
    public function setVersion($version)
565
    {
566
        $supported = [
567 23
            '2018.4',
568
            '2018.5',
569
            '2018.6',
570
            '2019.1',
571
            '2019.2',
572
            '2019.3',
573
        ];
574
575 23
        if (!in_array($version, $supported)) {
576 1
            throw new InvalidArgumentException(sprintf("The Version provided[%s] is not supported.", $version));
577
        }
578
579 23
        $this->version = $version;
580
581 23
        return $this;
582
    }
583
}
584