Connection   F
last analyzed

Complexity

Total Complexity 84

Size/Duplication

Total Lines 751
Duplicated Lines 0 %

Importance

Changes 33
Bugs 4 Features 4
Metric Value
wmc 84
eloc 209
c 33
b 4
f 4
dl 0
loc 751
rs 2

47 Methods

Rating   Name   Duplication   Size   Complexity  
A setBaseUrl() 0 3 1
A getMinutelyLimitRemaining() 0 3 1
A getAuthUrl() 0 6 1
A getDailyLimitReset() 0 3 1
A post() 0 11 2
A getTokenUrl() 0 3 1
A setExactClientSecret() 0 3 1
A getApiUrl() 0 3 1
A insertMiddleWare() 0 3 1
A getDivision() 0 3 1
A getCurrentDivisionNumber() 0 8 2
A extractRateLimits() 0 8 1
A setExactClientId() 0 3 1
A redirectForAuthorization() 0 5 1
A parseExceptionForErrorMessages() 0 21 4
A getTimestampFromExpiresIn() 0 7 2
A setRefreshToken() 0 3 1
A setApiUrl() 0 3 1
A setTokenUpdateCallback() 0 3 1
A setRedirectUrl() 0 3 1
A setClient() 0 3 1
A getDailyLimitRemaining() 0 3 1
A setDivision() 0 3 1
A setAcquireAccessTokenLockCallback() 0 3 1
A setAuthUrl() 0 3 1
A setTokenUrl() 0 3 1
A getBaseUrl() 0 3 1
A getMinutelyLimit() 0 3 1
A setAuthorizationCode() 0 3 1
A getDailyLimit() 0 3 1
A setAcquireAccessTokenUnlockCallback() 0 3 1
A setAccessToken() 0 3 1
A getAccessToken() 0 3 1
A getRefreshToken() 0 3 1
B parseResponse() 0 35 9
A put() 0 11 2
A get() 0 11 2
A setTokenExpires() 0 3 1
A createRequest() 0 28 6
A connect() 0 15 4
A tokenHasExpired() 0 7 2
A getTokenExpires() 0 3 1
A formatUrl() 0 17 3
A needsAuthentication() 0 3 2
B acquireAccessToken() 0 50 7
A delete() 0 11 2
A client() 0 18 3

How to fix   Complexity   

Complex Class

Complex classes like Connection 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 Connection, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Picqer\Financials\Exact;
4
5
use Exception;
6
use GuzzleHttp\Client;
7
use GuzzleHttp\Exception\BadResponseException;
8
use GuzzleHttp\HandlerStack;
9
use GuzzleHttp\Psr7;
10
use GuzzleHttp\Psr7\Request;
11
use GuzzleHttp\Psr7\Response;
12
13
/**
14
 * Class Connection.
15
 */
16
class Connection
17
{
18
    /**
19
     * @var string
20
     */
21
    private $baseUrl = 'https://start.exactonline.nl';
22
23
    /**
24
     * @var string
25
     */
26
    private $apiUrl = '/api/v1';
27
28
    /**
29
     * @var string
30
     */
31
    private $authUrl = '/api/oauth2/auth';
32
33
    /**
34
     * @var string
35
     */
36
    private $tokenUrl = '/api/oauth2/token';
37
38
    /**
39
     * @var mixed
40
     */
41
    private $exactClientId;
42
43
    /**
44
     * @var mixed
45
     */
46
    private $exactClientSecret;
47
48
    /**
49
     * @var mixed
50
     */
51
    private $authorizationCode;
52
53
    /**
54
     * @var mixed
55
     */
56
    private $accessToken;
57
58
    /**
59
     * @var int the Unix timestamp at which the access token expires
60
     */
61
    private $tokenExpires;
62
63
    /**
64
     * @var mixed
65
     */
66
    private $refreshToken;
67
68
    /**
69
     * @var mixed
70
     */
71
    private $redirectUrl;
72
73
    /**
74
     * @var mixed
75
     */
76
    private $division;
77
78
    /**
79
     * @var Client|null
80
     */
81
    private $client;
82
83
    /**
84
     * @var callable(Connection)
85
     */
86
    private $tokenUpdateCallback;
87
88
    /**
89
     * @var callable(Connection)
90
     */
91
    private $acquireAccessTokenLockCallback;
92
93
    /**
94
     * @var callable(Connection)
95
     */
96
    private $acquireAccessTokenUnlockCallback;
97
98
    /**
99
     * @var callable[]
100
     */
101
    protected $middleWares = [];
102
103
    /**
104
     * @var string|null
105
     */
106
    public $nextUrl = null;
107
108
    /**
109
     * @var int|null
110
     */
111
    protected $dailyLimit;
112
113
    /**
114
     * @var int|null
115
     */
116
    protected $dailyLimitRemaining;
117
118
    /**
119
     * @var int|null
120
     */
121
    protected $dailyLimitReset;
122
123
    /**
124
     * @var int|null
125
     */
126
    protected $minutelyLimit;
127
128
    /**
129
     * @var int|null
130
     */
131
    protected $minutelyLimitRemaining;
132
133
    /**
134
     * @return Client
135
     */
136
    private function client()
137
    {
138
        if ($this->client) {
139
            return $this->client;
140
        }
141
142
        $handlerStack = HandlerStack::create();
143
        foreach ($this->middleWares as $middleWare) {
144
            $handlerStack->push($middleWare);
145
        }
146
147
        $this->client = new Client([
148
            'http_errors' => true,
149
            'handler'     => $handlerStack,
150
            'expect'      => false,
151
        ]);
152
153
        return $this->client;
154
    }
155
156
    /**
157
     * Insert a custom Guzzle client.
158
     *
159
     * @param Client $client
160
     */
161
    public function setClient($client)
162
    {
163
        $this->client = $client;
164
    }
165
166
    /**
167
     * Insert a Middleware for the Guzzle Client.
168
     *
169
     * @param $middleWare
170
     */
171
    public function insertMiddleWare($middleWare)
172
    {
173
        $this->middleWares[] = $middleWare;
174
    }
175
176
    /**
177
     * @throws ApiException
178
     *
179
     * @return Client
180
     */
181
    public function connect()
182
    {
183
        // Redirect for authorization if needed (no access token or refresh token given)
184
        if ($this->needsAuthentication()) {
185
            $this->redirectForAuthorization();
186
        }
187
188
        // If access token is not set or token has expired, acquire new token
189
        if (empty($this->accessToken) || $this->tokenHasExpired()) {
190
            $this->acquireAccessToken();
191
        }
192
193
        $client = $this->client();
194
195
        return $client;
196
    }
197
198
    /**
199
     * @param string $method
200
     * @param string $endpoint
201
     * @param mixed  $body
202
     * @param array  $params
203
     * @param array  $headers
204
     *
205
     * @return Request
206
     */
207
    private function createRequest($method, $endpoint, $body = null, array $params = [], array $headers = [])
208
    {
209
        // Add default json headers to the request
210
        $headers = array_merge($headers, [
211
            'Accept'       => 'application/json',
212
            'Content-Type' => 'application/json',
213
            'Prefer'       => 'return=representation',
214
        ]);
215
216
        // If access token is not set or token has expired, acquire new token
217
        if (empty($this->accessToken) || $this->tokenHasExpired()) {
218
            $this->acquireAccessToken();
219
        }
220
221
        // If we have a token, sign the request
222
        if (! $this->needsAuthentication() && ! empty($this->accessToken)) {
223
            $headers['Authorization'] = 'Bearer ' . $this->accessToken;
224
        }
225
226
        // Create param string
227
        if (! empty($params)) {
228
            $endpoint .= '?' . http_build_query($params);
229
        }
230
231
        // Create the request
232
        $request = new Request($method, $endpoint, $headers, $body);
233
234
        return $request;
235
    }
236
237
    /**
238
     * @param string $url
239
     * @param array  $params
240
     * @param array  $headers
241
     *
242
     * @throws ApiException
243
     *
244
     * @return mixed
245
     */
246
    public function get($url, array $params = [], array $headers = [])
247
    {
248
        $url = $this->formatUrl($url, $url !== 'current/Me', $url == $this->nextUrl);
249
250
        try {
251
            $request = $this->createRequest('GET', $url, null, $params, $headers);
252
            $response = $this->client()->send($request);
253
254
            return $this->parseResponse($response, $url != $this->nextUrl);
255
        } catch (Exception $e) {
256
            $this->parseExceptionForErrorMessages($e);
257
        }
258
    }
259
260
    /**
261
     * @param string $url
262
     * @param mixed  $body
263
     *
264
     * @throws ApiException
265
     *
266
     * @return mixed
267
     */
268
    public function post($url, $body)
269
    {
270
        $url = $this->formatUrl($url);
271
272
        try {
273
            $request = $this->createRequest('POST', $url, $body);
274
            $response = $this->client()->send($request);
275
276
            return $this->parseResponse($response);
277
        } catch (Exception $e) {
278
            $this->parseExceptionForErrorMessages($e);
279
        }
280
    }
281
282
    /**
283
     * @param string $url
284
     * @param mixed  $body
285
     *
286
     * @throws ApiException
287
     *
288
     * @return mixed
289
     */
290
    public function put($url, $body)
291
    {
292
        $url = $this->formatUrl($url);
293
294
        try {
295
            $request = $this->createRequest('PUT', $url, $body);
296
            $response = $this->client()->send($request);
297
298
            return $this->parseResponse($response);
299
        } catch (Exception $e) {
300
            $this->parseExceptionForErrorMessages($e);
301
        }
302
    }
303
304
    /**
305
     * @param string $url
306
     *
307
     * @throws ApiException
308
     *
309
     * @return mixed
310
     */
311
    public function delete($url)
312
    {
313
        $url = $this->formatUrl($url);
314
315
        try {
316
            $request = $this->createRequest('DELETE', $url);
317
            $response = $this->client()->send($request);
318
319
            return $this->parseResponse($response);
320
        } catch (Exception $e) {
321
            $this->parseExceptionForErrorMessages($e);
322
        }
323
    }
324
325
    /**
326
     * @return string
327
     */
328
    public function getAuthUrl()
329
    {
330
        return $this->baseUrl . $this->authUrl . '?' . http_build_query([
331
            'client_id'     => $this->exactClientId,
332
            'redirect_uri'  => $this->redirectUrl,
333
            'response_type' => 'code',
334
        ]);
335
    }
336
337
    /**
338
     * @param mixed $exactClientId
339
     */
340
    public function setExactClientId($exactClientId)
341
    {
342
        $this->exactClientId = $exactClientId;
343
    }
344
345
    /**
346
     * @param mixed $exactClientSecret
347
     */
348
    public function setExactClientSecret($exactClientSecret)
349
    {
350
        $this->exactClientSecret = $exactClientSecret;
351
    }
352
353
    /**
354
     * @param mixed $authorizationCode
355
     */
356
    public function setAuthorizationCode($authorizationCode)
357
    {
358
        $this->authorizationCode = $authorizationCode;
359
    }
360
361
    /**
362
     * @param mixed $accessToken
363
     */
364
    public function setAccessToken($accessToken)
365
    {
366
        $this->accessToken = $accessToken;
367
    }
368
369
    /**
370
     * @param mixed $refreshToken
371
     */
372
    public function setRefreshToken($refreshToken)
373
    {
374
        $this->refreshToken = $refreshToken;
375
    }
376
377
    public function redirectForAuthorization()
378
    {
379
        $authUrl = $this->getAuthUrl();
380
        header('Location: ' . $authUrl);
381
        exit;
382
    }
383
384
    /**
385
     * @param mixed $redirectUrl
386
     */
387
    public function setRedirectUrl($redirectUrl)
388
    {
389
        $this->redirectUrl = $redirectUrl;
390
    }
391
392
    /**
393
     * @return bool
394
     */
395
    public function needsAuthentication()
396
    {
397
        return empty($this->refreshToken) && empty($this->authorizationCode);
398
    }
399
400
    /**
401
     * @param Response $response
402
     * @param bool     $returnSingleIfPossible
403
     *
404
     * @throws ApiException
405
     *
406
     * @return mixed
407
     */
408
    private function parseResponse(Response $response, $returnSingleIfPossible = true)
409
    {
410
        try {
411
            if ($response->getStatusCode() === 204) {
412
                return [];
413
            }
414
415
            $this->extractRateLimits($response);
416
417
            Psr7\rewind_body($response);
0 ignored issues
show
Bug introduced by
The function rewind_body 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

417
            /** @scrutinizer ignore-call */ 
418
            Psr7\rewind_body($response);
Loading history...
418
            $json = json_decode($response->getBody()->getContents(), true);
419
            if (false === is_array($json)) {
420
                throw new ApiException('Json decode failed. Got response: ' . $response->getBody()->getContents());
421
            }
422
            if (array_key_exists('d', $json)) {
423
                if (array_key_exists('__next', $json['d'])) {
424
                    $this->nextUrl = $json['d']['__next'];
425
                } else {
426
                    $this->nextUrl = null;
427
                }
428
429
                if (array_key_exists('results', $json['d'])) {
430
                    if ($returnSingleIfPossible && count($json['d']['results']) == 1) {
431
                        return $json['d']['results'][0];
432
                    }
433
434
                    return $json['d']['results'];
435
                }
436
437
                return $json['d'];
438
            }
439
440
            return $json;
441
        } catch (\RuntimeException $e) {
442
            throw new ApiException($e->getMessage());
443
        }
444
    }
445
446
    /**
447
     * @return mixed
448
     */
449
    private function getCurrentDivisionNumber()
450
    {
451
        if (empty($this->division)) {
452
            $me = new Me($this);
453
            $this->division = $me->find()->CurrentDivision;
454
        }
455
456
        return $this->division;
457
    }
458
459
    /**
460
     * @return mixed
461
     */
462
    public function getRefreshToken()
463
    {
464
        return $this->refreshToken;
465
    }
466
467
    /**
468
     * @return mixed
469
     */
470
    public function getAccessToken()
471
    {
472
        return $this->accessToken;
473
    }
474
475
    private function acquireAccessToken()
476
    {
477
        try {
478
            if (is_callable($this->acquireAccessTokenLockCallback)) {
479
                call_user_func($this->acquireAccessTokenLockCallback, $this);
480
            }
481
482
            // If refresh token not yet acquired, do token request
483
            if (empty($this->refreshToken)) {
484
                $body = [
485
                    'form_params' => [
486
                        'redirect_uri'  => $this->redirectUrl,
487
                        'grant_type'    => 'authorization_code',
488
                        'client_id'     => $this->exactClientId,
489
                        'client_secret' => $this->exactClientSecret,
490
                        'code'          => $this->authorizationCode,
491
                    ],
492
                ];
493
            } else { // else do refresh token request
494
                $body = [
495
                    'form_params' => [
496
                        'refresh_token' => $this->refreshToken,
497
                        'grant_type'    => 'refresh_token',
498
                        'client_id'     => $this->exactClientId,
499
                        'client_secret' => $this->exactClientSecret,
500
                    ],
501
                ];
502
            }
503
504
            $response = $this->client()->post($this->getTokenUrl(), $body);
505
506
            Psr7\rewind_body($response);
0 ignored issues
show
Bug introduced by
The function rewind_body 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

506
            /** @scrutinizer ignore-call */ 
507
            Psr7\rewind_body($response);
Loading history...
507
            $body = json_decode($response->getBody()->getContents(), true);
508
509
            if (json_last_error() === JSON_ERROR_NONE) {
510
                $this->accessToken = $body['access_token'];
511
                $this->refreshToken = $body['refresh_token'];
512
                $this->tokenExpires = $this->getTimestampFromExpiresIn($body['expires_in']);
513
514
                if (is_callable($this->tokenUpdateCallback)) {
515
                    call_user_func($this->tokenUpdateCallback, $this);
516
                }
517
            } else {
518
                throw new ApiException('Could not acquire tokens, json decode failed. Got response: ' . $response->getBody()->getContents());
519
            }
520
        } catch (BadResponseException $ex) {
521
            throw new ApiException('Could not acquire or refresh tokens [http ' . $ex->getResponse()->getStatusCode() . ']', 0, $ex);
522
        } finally {
523
            if (is_callable($this->acquireAccessTokenUnlockCallback)) {
524
                call_user_func($this->acquireAccessTokenUnlockCallback, $this);
525
            }
526
        }
527
    }
528
529
    /**
530
     * Translates expires_in to a Unix timestamp.
531
     *
532
     * @param string $expiresIn number of seconds until the token expires
533
     *
534
     * @return int
535
     */
536
    private function getTimestampFromExpiresIn($expiresIn)
537
    {
538
        if (! ctype_digit($expiresIn)) {
539
            throw new \InvalidArgumentException('Function requires a numeric expires value');
540
        }
541
542
        return time() + $expiresIn;
543
    }
544
545
    /**
546
     * @return int the Unix timestamp at which the access token expires
547
     */
548
    public function getTokenExpires()
549
    {
550
        return $this->tokenExpires;
551
    }
552
553
    /**
554
     * @param int $tokenExpires the Unix timestamp at which the access token expires
555
     */
556
    public function setTokenExpires($tokenExpires)
557
    {
558
        $this->tokenExpires = $tokenExpires;
559
    }
560
561
    private function tokenHasExpired()
562
    {
563
        if (empty($this->tokenExpires)) {
564
            return true;
565
        }
566
567
        return ($this->tokenExpires - 60) < time();
568
    }
569
570
    private function formatUrl($endPoint, $includeDivision = true, $formatNextUrl = false)
571
    {
572
        if ($formatNextUrl) {
573
            return $endPoint;
574
        }
575
576
        if ($includeDivision) {
577
            return implode('/', [
578
                $this->getApiUrl(),
579
                $this->getCurrentDivisionNumber(),
580
                $endPoint,
581
            ]);
582
        }
583
584
        return implode('/', [
585
            $this->getApiUrl(),
586
            $endPoint,
587
        ]);
588
    }
589
590
    /**
591
     * @return mixed
592
     */
593
    public function getDivision()
594
    {
595
        return $this->division;
596
    }
597
598
    /**
599
     * @param mixed $division
600
     */
601
    public function setDivision($division)
602
    {
603
        $this->division = $division;
604
    }
605
606
    /**
607
     * @param callable $callback
608
     */
609
    public function setAcquireAccessTokenLockCallback($callback)
610
    {
611
        $this->acquireAccessTokenLockCallback = $callback;
612
    }
613
614
    /**
615
     * @param callable $callback
616
     */
617
    public function setAcquireAccessTokenUnlockCallback($callback)
618
    {
619
        $this->acquireAccessTokenUnlockCallback = $callback;
620
    }
621
622
    /**
623
     * @param callable $callback
624
     */
625
    public function setTokenUpdateCallback($callback)
626
    {
627
        $this->tokenUpdateCallback = $callback;
628
    }
629
630
    /**
631
     * Parse the reponse in the Exception to return the Exact error messages.
632
     *
633
     * @param Exception $e
634
     *
635
     * @throws ApiException
636
     */
637
    private function parseExceptionForErrorMessages(Exception $e)
638
    {
639
        if (! $e instanceof BadResponseException) {
640
            throw new ApiException($e->getMessage(), 0, $e);
641
        }
642
643
        $response = $e->getResponse();
644
645
        $this->extractRateLimits($response);
646
647
        Psr7\rewind_body($response);
0 ignored issues
show
Bug introduced by
The function rewind_body 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

647
        /** @scrutinizer ignore-call */ 
648
        Psr7\rewind_body($response);
Loading history...
648
        $responseBody = $response->getBody()->getContents();
649
        $decodedResponseBody = json_decode($responseBody, true);
650
651
        if (! is_null($decodedResponseBody) && isset($decodedResponseBody['error']['message']['value'])) {
652
            $errorMessage = $decodedResponseBody['error']['message']['value'];
653
        } else {
654
            $errorMessage = $responseBody;
655
        }
656
657
        throw new ApiException('Error ' . $response->getStatusCode() . ': ' . $errorMessage, $response->getStatusCode(), $e);
658
    }
659
660
    /**
661
     * @return int|null The maximum number of API calls that your app is permitted to make per company, per day
662
     */
663
    public function getDailyLimit()
664
    {
665
        return $this->dailyLimit;
666
    }
667
668
    /**
669
     * @return int|null The remaining number of API calls that your app is permitted to make for a company, per day
670
     */
671
    public function getDailyLimitRemaining()
672
    {
673
        return $this->dailyLimitRemaining;
674
    }
675
676
    /**
677
     * @return int|null The time at which the rate limit window resets in UTC epoch milliseconds
678
     */
679
    public function getDailyLimitReset()
680
    {
681
        return $this->dailyLimitReset;
682
    }
683
684
    /**
685
     * @return int|null The maximum number of API calls that your app is permitted to make per company, per minute
686
     */
687
    public function getMinutelyLimit()
688
    {
689
        return $this->minutelyLimit;
690
    }
691
692
    /**
693
     * @return int|null The remaining number of API calls that your app is permitted to make for a company, per minute
694
     */
695
    public function getMinutelyLimitRemaining()
696
    {
697
        return $this->minutelyLimitRemaining;
698
    }
699
700
    /**
701
     * @return string
702
     */
703
    protected function getBaseUrl()
704
    {
705
        return $this->baseUrl;
706
    }
707
708
    /**
709
     * @return string
710
     */
711
    private function getApiUrl()
712
    {
713
        return $this->baseUrl . $this->apiUrl;
714
    }
715
716
    /**
717
     * @return string
718
     */
719
    private function getTokenUrl()
720
    {
721
        return $this->baseUrl . $this->tokenUrl;
722
    }
723
724
    /**
725
     * Set base URL for different countries according to
726
     * https://developers.exactonline.com/#Exact%20Online%20sites.html.
727
     *
728
     * @param string $baseUrl
729
     */
730
    public function setBaseUrl($baseUrl)
731
    {
732
        $this->baseUrl = $baseUrl;
733
    }
734
735
    /**
736
     * @param string $apiUrl
737
     */
738
    public function setApiUrl($apiUrl)
739
    {
740
        $this->apiUrl = $apiUrl;
741
    }
742
743
    /**
744
     * @param string $authUrl
745
     */
746
    public function setAuthUrl($authUrl)
747
    {
748
        $this->authUrl = $authUrl;
749
    }
750
751
    /**
752
     * @param string $tokenUrl
753
     */
754
    public function setTokenUrl($tokenUrl)
755
    {
756
        $this->tokenUrl = $tokenUrl;
757
    }
758
759
    private function extractRateLimits(Response $response)
760
    {
761
        $this->dailyLimit = (int) $response->getHeaderLine('X-RateLimit-Limit');
762
        $this->dailyLimitRemaining = (int) $response->getHeaderLine('X-RateLimit-Remaining');
763
        $this->dailyLimitReset = (int) $response->getHeaderLine('X-RateLimit-Reset');
764
765
        $this->minutelyLimit = (int) $response->getHeaderLine('X-RateLimit-Minutely-Limit');
766
        $this->minutelyLimitRemaining = (int) $response->getHeaderLine('X-RateLimit-Minutely-Remaining');
767
    }
768
}
769