Completed
Pull Request — develop (#27)
by Jimmy
12:03
created

Client::setClientId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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->integrator)) {
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->version,
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
     * Process the error received from ConnectWise
303
     *
304
     * @param RequestException $exception
305
     */
306
    // TODO: Figure out what to really do with an error...
307
    /**
308
     * @param RequestException $exception
309
     */
310
    protected function processError(RequestException $exception)
311
    {
312
        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

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