Completed
Push — develop ( c248db...298294 )
by Jimmy
32:32 queued 17:36
created

Client::getVersion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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 array delete(string $resource, array $options = [])
22
 * @method array get(string $resource, array $options = [])
23
 * @method array head(string $resource, array $options = [])
24
 * @method array patch(string $resource, array $options = [])
25
 * @method array post(string $resource, array $options = [])
26
 * @method array put(string $resource, array $options = [])
27
 */
28
class Client
29
{
30
    /**
31
     * The Client Id
32
     *
33
     * @var string
34
     * @see https://developer.connectwise.com/ClientID
35
     */
36
    protected $clientId;
37
38
    /**
39
     * @var Guzzle
40
     */
41
    protected $guzzle;
42
43
    /**
44
     * Headers that needs to be used with token
45
     *
46
     * @var array
47
     */
48
    protected $headers = [];
49
50
    /**
51
     * Integrator username to make global calls
52
     *
53
     * @var
54
     */
55
    protected $integrator;
56
57
    /**
58
     * Integration password for global calls
59
     *
60
     * @var
61
     */
62
    protected $password;
63
64
    /**
65
     * Resolves a model for the uri
66
     *
67
     * @var ModelResolver
68
     */
69
    protected $resolver;
70
71
    /**
72
     * Public & private keys to log into CW
73
     *
74
     * @var Token
75
     */
76
    protected $token;
77
78
    /**
79
     * URL to the ConnectWise installation
80
     *
81
     * @var string
82
     */
83
    protected $url;
84
85
    /**
86
     * Supported verbs
87
     *
88
     * @var array
89
     */
90
    protected $verbs = [
91
        'delete',
92
        'get',
93
        'head',
94
        'put',
95
        'post',
96
        'patch',
97
    ];
98
99
    /**
100
     * Version of the API being requested
101
     *
102
     * @var string
103
     */
104
    protected $version;
105
106
    /**
107
     * Client constructor.
108
     *
109
     * @param Token $token
110
     * @param Guzzle $guzzle
111
     * @param ModelResolver $resolver
112
     * @param string $version Version of the models to use with the API responses
113
     */
114
    public function __construct(Token $token, Guzzle $guzzle, ModelResolver $resolver, $version = null)
115
    {
116
        $this->token = $token;
117
        $this->guzzle = $guzzle;
118
        $this->resolver = $resolver;
119
        $this->setVersion($version ?? '2019.3');
120
    }
121
122
    /**
123
     * Magic method to allow short cut to the request types
124
     *
125
     * @param string $verb
126
     * @param array $args
127
     *
128
     * @return Collection|Model|Response
129
     * @throws GuzzleException
130
     * @throws MalformedRequest
131
     */
132
    public function __call($verb, $args)
133
    {
134
        if (count($args) < 1) {
135
            throw new InvalidArgumentException('Magic request methods require a resource and optional options array');
136
        }
137
138
        if (!in_array($verb, $this->verbs)) {
139
            throw new InvalidArgumentException(sprintf("Unsupported verb [%s] was requested.", $verb));
140
        }
141
142
        return $this->request($verb, $args[0], $args[1] ?? []);
143
    }
144
145
    /**
146
     * Adds key/value pair to the header to be sent
147
     *
148
     * @param array $header
149
     *
150
     * @return $this
151
     */
152
    public function addHeader(array $header)
153
    {
154
        foreach ($header as $key => $value) {
155
            $this->headers[$key] = $value;
156
        }
157
158
        return $this;
159
    }
160
161
    /**
162
     * Build authorization headers to send CW API
163
     *
164
     * @return string
165
     */
166
    public function buildAuth()
167
    {
168
        if ($this->token->needsRefreshing()) {
169
            $this->token->refresh($this);
170
        }
171
172
        return 'Basic ' . base64_encode($this->token->getUsername() . ':' . $this->token->getPassword());
173
    }
174
175
    /**
176
     * Build the options to send to API
177
     *
178
     * We allays need to login with Basic Auth, so add the "auth" option for Guzzle to use when logging in.
179
     * Additionally, pass any headers that have been set.
180
     *
181
     * @param array $options
182
     *
183
     * @return array
184
     */
185
    public function buildOptions(array $options = [])
186
    {
187
        return array_merge_recursive(
188
            $options,
189
            [
190
                'headers' => $this->getHeaders(),
191
            ]
192
        );
193
    }
194
195
    /**
196
     * Build the full path to the CW resource
197
     *
198
     * @param string $resource
199
     *
200
     * @return string
201
     * @throws MalformedRequest
202
     */
203
    public function buildUri($resource)
204
    {
205
        $uri = $this->getUrl() . ltrim($resource, '/');
206
207
        if (strlen($uri) > 2000) {
208
            throw new MalformedRequest(
209
                sprintf("The uri is too long. It is %s character(s) over the 2000 limit.", strlen($uri) - 2000)
210
            );
211
        }
212
213
        return $uri;
214
    }
215
216
    /**
217
     * Remove all set headers
218
     *
219
     * @return $this
220
     */
221
    public function emptyHeaders()
222
    {
223
        $this->setHeaders([]);
224
225
        return $this;
226
    }
227
228
    /**
229
     * Expose the client id
230
     *
231
     * @return string
232
     */
233
    public function getClientId()
234
    {
235
        return $this->clientId;
236
    }
237
238
    /**
239
     * The headers to send
240
     *
241
     * When making an integrator call (expired token), then you have to only send the "x-cw-usertype" header.
242
     *
243
     * @return array
244
     */
245
    public function getHeaders()
246
    {
247
        $authorization_headers = [
248
            'clientId'      => $this->getClientId(),
249
            'Authorization' => $this->buildAuth(),
250
        ];
251
252
        if ($this->token->isForUser($this->getIntegrator())) {
253
            return array_merge(
254
                [
255
                    'x-cw-usertype' => 'integrator',
256
                ],
257
                $authorization_headers
258
            );
259
        }
260
261
        return array_merge(
262
            [
263
                'x-cw-usertype' => 'member',
264
                'Accept'        => 'application/vnd.connectwise.com+json; version=' . $this->getVersion(),
265
            ],
266
            $authorization_headers,
267
            $this->headers
268
        );
269
    }
270
271
    /**
272
     * Expose the integrator username
273
     *
274
     * @return string
275
     */
276
    public function getIntegrator()
277
    {
278
        return $this->integrator;
279
    }
280
281
    /**
282
     * Expose the integrator password
283
     *
284
     * @return string
285
     */
286
    public function getPassword()
287
    {
288
        return $this->password;
289
    }
290
291
    /**
292
     * Expose the url
293
     *
294
     * @return string
295
     */
296
    public function getUrl()
297
    {
298
        return $this->url . '/v4_6_release/apis/3.0/';
299
    }
300
301
    /**
302
     * Expose the version
303
     *
304
     * @return string
305
     */
306
    public function getVersion()
307
    {
308
        return $this->version;
309
    }
310
311
    /**
312
     * Process the error received from ConnectWise
313
     *
314
     * @param RequestException $exception
315
     */
316
    // TODO: Figure out what to really do with an error...
317
    /**
318
     * @param RequestException $exception
319
     */
320
    protected function processError(RequestException $exception)
321
    {
322
        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

322
        echo /** @scrutinizer ignore-call */ Psr7\str($exception->getRequest());
Loading history...
323
324
        if ($exception->hasResponse()) {
325
            echo Psr7\str($exception->getResponse());
326
        }
327
    }
328
329
    /**
330
     * @param          $resource
331
     * @param Response $response
332
     *
333
     * @return Collection|Model|Response
334
     */
335
    protected function processResponse($resource, Response $response)
336
    {
337
        $response = (array)json_decode($response->getBody(), true);
338
339
        if ($model = $this->resolver->find($resource, $this->getVersion())) {
340
            $model = 'Spinen\ConnectWise\Models\\' . $model;
341
342
            if ($this->isCollection($response)) {
343
                $response = array_map(
344
                    function ($item) use ($model) {
345
                        $item = new $model($item, $this);
346
347
                        return $item;
348
                    },
349
                    $response
350
                );
351
352
                return new Collection($response);
353
            }
354
355
            return new $model($response, $this);
356
        }
357
358
        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...
359
    }
360
361
    /**
362
     * Make call to the resource
363
     *
364
     * @param string $method
365
     * @param string $resource
366
     * @param array|null $options
367
     *
368
     * @return Collection|Model|Response
369
     * @throws GuzzleException
370
     * @throws MalformedRequest
371
     */
372
    protected function request($method, $resource, array $options = [])
373
    {
374
        try {
375
            $response = $this->guzzle->request($method, $this->buildUri($resource), $this->buildOptions($options));
376
377
            return $this->processResponse($resource, $response);
378
        } catch (RequestException $e) {
379
            $this->processError($e);
380
        }
381
    }
382
383
    /**
384
     * Set the Client Id
385
     *
386
     * @param string $clientId
387
     *
388
     * @return $this
389
     */
390
    public function setClientId($clientId)
391
    {
392
        $this->clientId = $clientId;
393
394
        return $this;
395
    }
396
397
    /**
398
     * Set the headers
399
     *
400
     * There is an "addHeader" method to push a single header onto the stack.  Otherwise,this replaces the headers.
401
     *
402
     * @param array $headers
403
     *
404
     * @return $this
405
     */
406
    public function setHeaders(array $headers)
407
    {
408
        $this->headers = $headers;
409
410
        return $this;
411
    }
412
413
    /**
414
     * Set the integrator username
415
     *
416
     * @param string $integrator
417
     *
418
     * @return $this
419
     */
420
    public function setIntegrator($integrator)
421
    {
422
        $this->integrator = $integrator;
423
424
        return $this;
425
    }
426
427
    /**
428
     * Set the integrator password
429
     *
430
     * @param string $password
431
     *
432
     * @return $this
433
     */
434
    public function setPassword($password)
435
    {
436
        $this->password = $password;
437
438
        return $this;
439
    }
440
441
    /**
442
     * Set the URL to ConnectWise
443
     *
444
     * @param string $url
445
     *
446
     * @return $this
447
     */
448
    public function setUrl($url)
449
    {
450
        if (!filter_var($url, FILTER_VALIDATE_URL)) {
451
            throw new InvalidArgumentException(sprintf("The URL provided[%] is not a valid format.", $url));
452
        }
453
454
        $this->url = rtrim($url, '/');
455
456
        return $this;
457
    }
458
459
    /**
460
     * Set the version of the API response models
461
     *
462
     * @param string $version
463
     *
464
     * @return $this
465
     */
466
    public function setVersion($version)
467
    {
468
        $supported = [
469
            '2018.4',
470
            '2018.5',
471
            '2018.6',
472
            '2019.1',
473
            '2019.2',
474
            '2019.3',
475
        ];
476
477
        if (!in_array($version, $supported)) {
478
            throw new InvalidArgumentException(sprintf("The Version provided[%] is not supported.", $version));
479
        }
480
481
        $this->version = $version;
482
483
        return $this;
484
    }
485
486
    protected function isCollection(array $array)
487
    {
488
        // Keys of the array
489
        $keys = array_keys($array);
490
491
        // If the array keys of the keys match the keys, then the array must
492
        // not be associative (e.g. the keys array looked like {0:0, 1:1...}).
493
        return array_keys($keys) === $keys;
494
    }
495
}
496