Completed
Push — develop ( ddd9ad...807fde )
by Jimmy
25s queued 11s
created

Client::setPageSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
ccs 0
cts 3
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
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 Illuminate\Support\Collection as LaravelCollection;
11
use Illuminate\Support\Str;
12
use InvalidArgumentException;
13
use Psr\Http\Message\ResponseInterface;
14
use Spinen\ConnectWise\Exceptions\MalformedRequest;
15
use Spinen\ConnectWise\Support\Collection;
16
use Spinen\ConnectWise\Support\Model;
17
use Spinen\ConnectWise\Support\ModelResolver;
18
19
/**
20
 * Class Client
21
 *
22
 * @package Spinen\ConnectWise\Api
23
 *
24
 * @method LaravelCollection|Model delete(string $resource, array $options = [])
25
 * @method LaravelCollection|Model get(string $resource, array $options = [])
26
 * @method LaravelCollection|Model getAll(string $resource, array $options = [])
27
 * @method LaravelCollection|Model head(string $resource, array $options = [])
28
 * @method LaravelCollection|Model patch(string $resource, array $options = [])
29
 * @method LaravelCollection|Model post(string $resource, array $options = [])
30
 * @method LaravelCollection|Model put(string $resource, array $options = [])
31
 */
32
class Client
33
{
34
    /**
35
     * The Client Id
36
     *
37
     * @var string
38
     * @see https://developer.connectwise.com/ClientID
39
     */
40
    protected $clientId;
41
42
    /**
43
     * @var Guzzle
44
     */
45
    protected $guzzle;
46
47
    /**
48
     * Headers that needs to be used with token
49
     *
50
     * @var array
51
     */
52
    protected $headers = [];
53
54
    /**
55
     * Integrator username to make global calls
56
     *
57
     * @var
58
     */
59
    protected $integrator;
60
61
    /**
62
     * Current page
63
     *
64
     * @var integer
65
     */
66
    protected $page;
67
68
    /**
69
     * Number or records to retrieve
70
     *
71
     * @var int
72
     */
73
    protected $page_size = 100;
74
75
    /**
76
     * Integration password for global calls
77
     *
78
     * @var string
79
     */
80
    protected $password;
81
82
    /**
83
     * Resolves a model for the uri
84
     *
85
     * @var ModelResolver
86
     */
87
    protected $resolver;
88
89
    /**
90
     * Public & private keys to log into CW
91
     *
92
     * @var Token
93
     */
94
    protected $token;
95
96
    /**
97
     * URL to the ConnectWise installation
98
     *
99
     * @var string
100
     */
101
    protected $url;
102
103
    /**
104
     * Supported verbs
105
     *
106
     * @var array
107
     */
108
    protected $verbs = [
109
        'delete',
110
        'get',
111
        'head',
112
        'put',
113
        'post',
114
        'patch',
115
    ];
116
117
    /**
118
     * Version of the API being requested
119
     *
120
     * @var string
121
     */
122
    protected $version;
123
124
    /**
125
     * Client constructor.
126
     *
127
     * @param Token $token
128
     * @param Guzzle $guzzle
129
     * @param ModelResolver $resolver
130
     * @param string $version Version of the models to use with the API responses
131
     */
132 23
    public function __construct(Token $token, Guzzle $guzzle, ModelResolver $resolver, $version = null)
133
    {
134 23
        $this->token = $token;
135 23
        $this->guzzle = $guzzle;
136 23
        $this->resolver = $resolver;
137 23
        $this->setVersion($version ?? '2019.3');
138
    }
139
140
    /**
141
     * Magic method to allow short cut to the request types
142
     *
143
     * @param string $verb
144
     * @param array $args
145
     *
146
     * @return Collection|Model|Response
147
     * @throws GuzzleException
148
     * @throws MalformedRequest
149
     */
150 11
    public function __call($verb, $args)
151
    {
152 11
        if (count($args) < 1) {
153 1
            throw new InvalidArgumentException('Magic request methods require a resource and optional options array');
154
        }
155
156
        // For "getAll", set page to 1 & change verb to "get", otherwise, no page
157 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...
158
159 10
        if (!in_array($verb, $this->verbs)) {
160 1
            throw new InvalidArgumentException(sprintf("Unsupported verb [%s] was requested.", $verb));
161
        }
162
163 9
        return $this->request($verb, $this->trimResourceAsNeeded($args[0]), $args[1] ?? []);
164
    }
165
166
    /**
167
     * Adds key/value pair to the header to be sent
168
     *
169
     * @param array $header
170
     *
171
     * @return $this
172
     */
173 3
    public function addHeader(array $header)
174
    {
175 3
        foreach ($header as $key => $value) {
176 3
            $this->headers[$key] = $value;
177
        }
178
179 3
        return $this;
180
    }
181
182
    /**
183
     * Build authorization headers to send CW API
184
     *
185
     * @return string
186
     */
187 14
    public function buildAuth()
188
    {
189 14
        if ($this->token->needsRefreshing()) {
190 1
            $this->token->refresh($this);
191
        }
192
193 14
        return 'Basic ' . base64_encode($this->token->getUsername() . ':' . $this->token->getPassword());
194
    }
195
196
    /**
197
     * Build the options to send to API
198
     *
199
     * We allays need to login with Basic Auth, so add the "auth" option for Guzzle to use when logging in.
200
     * Additionally, pass any headers that have been set.
201
     *
202
     * @param array $options
203
     *
204
     * @return array
205
     */
206 10
    public function buildOptions(array $options = [])
207
    {
208 10
        return array_merge_recursive(
209 10
            $options,
210
            [
211 10
                'headers' => $this->getHeaders(),
212
            ]
213
        );
214
    }
215
216
    /**
217
     * Build the full path to the CW resource
218
     *
219
     * @param string $resource
220
     *
221
     * @return string
222
     * @throws MalformedRequest
223
     */
224 11
    public function buildUri($resource)
225
    {
226 11
        $uri = $this->getUrl($resource);
227
228
        // For getAll calls, make sure to add pageSize & page to request
229 11
        if ($this->page) {
230
            $uri .= (preg_match('/\\?/u', $uri) ? '&' : '?') . 'pageSize=' . $this->page_size . '&page=' . $this->page;
231
        }
232
233 11
        if (strlen($uri) > 2000) {
234 1
            throw new MalformedRequest(
235 1
                sprintf("The uri is too long. It is %s character(s) over the 2000 limit.", strlen($uri) - 2000)
236
            );
237
        }
238
239 11
        return $uri;
240
    }
241
242
    /**
243
     * Remove all set headers
244
     *
245
     * @return $this
246
     */
247 1
    public function emptyHeaders()
248
    {
249 1
        $this->setHeaders([]);
250
251 1
        return $this;
252
    }
253
254
    /**
255
     * Expose the client id
256
     *
257
     * @return string
258
     */
259 12
    public function getClientId()
260
    {
261 12
        return $this->clientId;
262
    }
263
264
    /**
265
     * The headers to send
266
     *
267
     * When making an integrator call (expired token), then you have to only send the "x-cw-usertype" header.
268
     *
269
     * @return array
270
     */
271 12
    public function getHeaders()
272
    {
273
        $authorization_headers = [
274 12
            'clientId'      => $this->getClientId(),
275 12
            'Authorization' => $this->buildAuth(),
276
        ];
277
278 12
        if ($this->token->isForUser($this->getIntegrator())) {
279 1
            return array_merge(
280
                [
281 1
                    'x-cw-usertype' => 'integrator',
282
                ],
283 1
                $authorization_headers
284
            );
285
        }
286
287 11
        return array_merge(
288
            [
289 11
                'x-cw-usertype' => 'member',
290 11
                'Accept'        => 'application/vnd.connectwise.com+json; version=' . $this->getVersion(),
291
            ],
292 11
            $authorization_headers,
293 11
            $this->headers
294
        );
295
    }
296
297
    /**
298
     * Expose the integrator username
299
     *
300
     * @return string
301
     */
302 13
    public function getIntegrator()
303
    {
304 13
        return $this->integrator;
305
    }
306
307
    /**
308
     * Expose the integrator password
309
     *
310
     * @return string
311
     */
312 1
    public function getPassword()
313
    {
314 1
        return $this->password;
315
    }
316
317
    /**
318
     * Expose the url
319
     *
320
     * @param string|null $path
321
     *
322
     * @return string
323
     */
324 11
    public function getUrl($path = null)
325
    {
326 11
        return $this->url . '/v4_6_release/apis/3.0/' . ltrim($path, '/');
327
    }
328
329
    /**
330
     * Expose the version
331
     *
332
     * @return string
333
     */
334 12
    public function getVersion()
335
    {
336 12
        return $this->version;
337
    }
338
339
    /**
340
     * Check to see if the array is a collection
341
     *
342
     * @param array $array
343
     *
344
     * @return bool
345
     */
346 8
    protected function isCollection(array $array)
347
    {
348
        // Keys of the array
349 8
        $keys = array_keys($array);
350
351
        // If the array keys of the keys match the keys, then the array must
352
        // not be associative (e.g. the keys array looked like {0:0, 1:1...}).
353 8
        return array_keys($keys) === $keys;
354
    }
355
356
    /**
357
     * Check to see it there are more pages in a paginated call
358
     *
359
     * For paginated calls, ConnectWise returns a "Link" property in teh header with a single string.
360
     *
361
     * There appears to be 3 potential responses...
362
     *  1) null -- The number of items in the collection is < the pageSize
363
     *  2) string ends with rel="last" -- More pages to go
364
     *  3) string ends with rel="first" -- On the last page
365
     *
366
     * Here are some examples...
367
     *  <https://some.host/v4_6_release/apis/3.0/finance/agreements&pageSize=10&page=2>; rel="next", \
368
     *      <https://some.host/v4_6_release/apis/3.0/finance/agreements&pageSize=10&page=3>; rel="last”
369
     *
370
     *  <https://some.host/v4_6_release/apis/3.0/finance/agreements&pageSize=10&page=3>; rel="next", \
371
     *      <https://some.host/v4_6_release/apis/3.0/finance/agreements&pageSize=10&page=1>; rel="first”
372
     *
373
     * @param ResponseInterface $response
374
     *
375
     * @return boolean
376
     */
377
    protected function isLastPage(ResponseInterface $response)
378
    {
379
        return !(bool)preg_match('/rel="last"$/u', $response->getHeader('Link')[0] ?? null);
380
    }
381
382
    /**
383
     * Process the error received from ConnectWise
384
     *
385
     * @param RequestException $exception
386
     */
387
    protected function processError(RequestException $exception)
388
    {
389
        // TODO: Figure out what to really do with an error...
390
        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

390
        echo /** @scrutinizer ignore-call */ Psr7\str($exception->getRequest());
Loading history...
391
392
        if ($exception->hasResponse()) {
393
            echo Psr7\str($exception->getResponse());
394
        }
395
    }
396
397
    /**
398
     * Parse the response for the given resource
399
     *
400
     * @param string $resource
401
     * @param ResponseInterface $response
402
     *
403
     * @return Collection|Model|ResponseInterface
404
     */
405 9
    protected function processResponse($resource, ResponseInterface $response)
406
    {
407 9
        $response = (array)json_decode($response->getBody(), true);
408
409
        // Nothing to map response, so just return it as is
410 9
        if (!$model = $this->resolver->find($resource, $this->getVersion())) {
411 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 Psr\Http\Message\Respons...nnectWise\Support\Model.
Loading history...
412
        }
413
414
        // Not a collection of records, so cast to model
415 8
        if (!$this->isCollection($response)) {
416 1
            return new $model($response, $this);
417
        }
418
419
        // Have a collection of records, so cast them all as a collection
420 7
        return new Collection(
421 7
            array_map(
422
                function ($item) use ($model) {
423 1
                    $item = new $model($item, $this);
424
425 1
                    return $item;
426 7
                },
427 7
                $response
428
            )
429
        );
430
    }
431
432
    /**
433
     * Make call to the resource
434
     *
435
     * @param string $method
436
     * @param string $resource
437
     * @param array|null $options
438
     *
439
     * @return LaravelCollection|Model|Response
440
     * @throws GuzzleException
441
     * @throws MalformedRequest
442
     */
443 9
    protected function request($method, $resource, array $options = [])
444
    {
445
        try {
446 9
            $response = $this->guzzle->request($method, $this->buildUri($resource), $this->buildOptions($options));
447
448 9
            $processed = $this->processResponse($resource, $response);
449
450
            // If, not a "gatAll" call, then return the response
451 9
            if (!$this->page) {
452 9
                return $processed;
453
            }
454
455
            // Get all of the other records
456
            while (!$this->isLastPage($response)) {
457
                $this->page = $this->page + 1;
458
459
                // Make next call
460
                $response = $this->guzzle->request($method, $this->buildUri($resource), $this->buildOptions($options));
461
462
                $processed = $processed->merge($this->processResponse($resource, $response));
463
            }
464
465
            return $processed;
466
        } catch (RequestException $e) {
467
            $this->processError($e);
468
        }
469
    }
470
471
    /**
472
     * Set the Client Id
473
     *
474
     * @param string $clientId
475
     *
476
     * @return $this
477
     */
478 23
    public function setClientId($clientId)
479
    {
480 23
        $this->clientId = $clientId;
481
482 23
        return $this;
483
    }
484
485
    /**
486
     * Set the headers
487
     *
488
     * There is an "addHeader" method to push a single header onto the stack.  Otherwise,this replaces the headers.
489
     *
490
     * @param array $headers
491
     *
492
     * @return $this
493
     */
494 1
    public function setHeaders(array $headers)
495
    {
496 1
        $this->headers = $headers;
497
498 1
        return $this;
499
    }
500
501
    /**
502
     * Set the integrator username
503
     *
504
     * @param string $integrator
505
     *
506
     * @return $this
507
     */
508 5
    public function setIntegrator($integrator)
509
    {
510 5
        $this->integrator = $integrator;
511
512 5
        return $this;
513
    }
514
515
    /**
516
     * Set the integrator password
517
     *
518
     * @param string $password
519
     *
520
     * @return $this
521
     */
522 2
    public function setPassword($password)
523
    {
524 2
        $this->password = $password;
525
526 2
        return $this;
527
    }
528
529
    /**
530
     * Set the page size
531
     *
532
     * @param integer $page_size
533
     *
534
     * @return $this
535
     */
536
    public function setPageSize($page_size)
537
    {
538
        $this->page_size = $page_size;
539
540
        return $this;
541
    }
542
543
    /**
544
     * Set the URL to ConnectWise
545
     *
546
     * @param string $url
547
     *
548
     * @return $this
549
     */
550 3
    public function setUrl($url)
551
    {
552 3
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
553 1
            throw new InvalidArgumentException(sprintf("The URL provided[%] is not a valid format.", $url));
554
        }
555
556 2
        $this->url = rtrim($url, '/');
557
558 2
        return $this;
559
    }
560
561
    /**
562
     * Set the version of the API response models
563
     *
564
     * @param string $version
565
     *
566
     * @return $this
567
     */
568 23
    public function setVersion($version)
569
    {
570
        $supported = [
571 23
            '2018.4',
572
            '2018.5',
573
            '2018.6',
574
            '2019.1',
575
            '2019.2',
576
            '2019.3',
577
        ];
578
579 23
        if (!in_array($version, $supported)) {
580 1
            throw new InvalidArgumentException(sprintf("The Version provided[%s] is not supported.", $version));
581
        }
582
583 23
        $this->version = $version;
584
585 23
        return $this;
586
    }
587
588
    /**
589
     * Make the resource being request be relative without the leading slash
590
     *
591
     * @param string $resource
592
     *
593
     * @return string
594
     */
595 9
    protected function trimResourceAsNeeded($resource)
596
    {
597 9
        return ltrim(Str::replaceFirst($this->getUrl(), '', $resource), '/');
598
    }
599
}
600