Api   F
last analyzed

Complexity

Total Complexity 78

Size/Duplication

Total Lines 599
Duplicated Lines 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 192
c 1
b 1
f 0
dl 0
loc 599
rs 2.16
wmc 78

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 2
A getDefaultAccept() 0 3 1
A setUrl() 0 4 1
B createRequest() 0 28 8
A delete() 0 4 1
A getUrl() 0 3 1
A prefix() 0 13 3
A getDefaultParameters() 0 3 1
A post() 0 4 1
A errorMessage() 0 13 3
A setToken() 0 4 1
A withUrl() 0 5 1
C expandUriTemplate() 0 93 14
A expandColonParameters() 0 20 3
C decode() 0 43 16
A patch() 0 4 1
A setDefaultAccept() 0 4 1
A put() 0 4 1
A escape() 0 25 6
A request() 0 14 2
A getClient() 0 3 1
A getToken() 0 3 1
A walk() 0 8 3
A head() 0 4 1
A setDefaultParameters() 0 4 2
A paginator() 0 5 1
A get() 0 4 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace XoopsModules\Wggithub\Github;
4
5
6
/**
7
 * Github API client library. Read readme.md in repository {@link http://github.com/milo/github-api}
8
 *
9
 * @see https://developer.github.com/v3/
10
 *
11
 * @author  Miloslav Hůla (https://github.com/milo)
12
 */
13
class Api extends Sanity
14
{
15
    /** @var string */
16
    private $url = 'https://api.github.com';
17
18
    /** @var string */
19
    private $defaultAccept = 'application/vnd.github.v3+json';
20
21
    /** @var array|NULL */
22
    private $defaultParameters = [];
23
24
    /** @var Http\IClient */
25
    private $client;
26
27
    /** @var OAuth\Token|NULL */
28
    private $token;
29
30
31
    public function __construct(Http\IClient $client = NULL)
32
    {
33
        $this->client = $client ?: Helpers::createDefaultClient();
34
    }
35
36
37
    /**
38
     * @param OAuth\Token|null $token
39
     * @return self
40
     */
41
    public function setToken(OAuth\Token $token = NULL)
42
    {
43
        $this->token = $token;
44
        return $this;
45
    }
46
47
48
    /**
49
     * @return OAuth\Token|NULL
50
     */
51
    public function getToken()
52
    {
53
        return $this->token;
54
    }
55
56
57
    /**
58
     * @param  array
59
     * @return self
60
     */
61
    public function setDefaultParameters(array $defaults = NULL)
62
    {
63
        $this->defaultParameters = $defaults ?: [];
64
        return $this;
65
    }
66
67
68
    /**
69
     * @return array
70
     */
71
    public function getDefaultParameters()
72
    {
73
        return $this->defaultParameters;
74
    }
75
76
77
    /**
78
     * @param  string
79
     * @return self
80
     */
81
    public function setDefaultAccept($accept)
82
    {
83
        $this->defaultAccept = $accept;
84
        return $this;
85
    }
86
87
88
    /**
89
     * @return string
90
     */
91
    public function getDefaultAccept()
92
    {
93
        return $this->defaultAccept;
94
    }
95
96
97
    /**
98
     * @see createRequest()
99
     * @see request()
100
     *
101
     * @param  string
102
     * @param array $parameters
103
     * @param array $headers
104
     * @return Http\Response
105
     *
106
     */
107
    public function delete($urlPath, array $parameters = [], array $headers = [])
108
    {
109
        return $this->request(
110
            $this->createRequest(Http\Request::DELETE, $urlPath, $parameters, $headers)
111
        );
112
    }
113
114
115
    /**
116
     * @see createRequest()
117
     * @see request()
118
     *
119
     * @param  string
120
     * @param array $parameters
121
     * @param array $headers
122
     * @return Http\Response
123
     *
124
     */
125
    public function get($urlPath, array $parameters = [], array $headers = [])
126
    {
127
        return $this->request(
128
            $this->createRequest(Http\Request::GET, $urlPath, $parameters, $headers)
129
        );
130
    }
131
132
133
    /**
134
     * @see createRequest()
135
     * @see request()
136
     *
137
     * @param  string
138
     * @param array $parameters
139
     * @param array $headers
140
     * @return Http\Response
141
     *
142
     */
143
    public function head($urlPath, array $parameters = [], array $headers = [])
144
    {
145
        return $this->request(
146
            $this->createRequest(Http\Request::HEAD, $urlPath, $parameters, $headers)
147
        );
148
    }
149
150
151
    /**
152
     * @see createRequest()
153
     * @see request()
154
     *
155
     * @param $urlPath
156
     * @param $content
157
     * @param array $parameters
158
     * @param array $headers
159
     * @return Http\Response
160
     *
161
     */
162
    public function patch($urlPath, $content, array $parameters = [], array $headers = [])
163
    {
164
        return $this->request(
165
            $this->createRequest(Http\Request::PATCH, $urlPath, $parameters, $headers, $content)
166
        );
167
    }
168
169
170
    /**
171
     * @see createRequest()
172
     * @see request()
173
     *
174
     * @param $urlPath
175
     * @param $content
176
     * @param array $parameters
177
     * @param array $headers
178
     * @return Http\Response
179
     *
180
     */
181
    public function post($urlPath, $content, array $parameters = [], array $headers = [])
182
    {
183
        return $this->request(
184
            $this->createRequest(Http\Request::POST, $urlPath, $parameters, $headers, $content)
185
        );
186
    }
187
188
189
    /**
190
     * @see createRequest()
191
     * @see request()
192
     *
193
     * @param $urlPath
194
     * @param $content
195
     * @param array $parameters
196
     * @param array $headers
197
     * @return Http\Response
198
     *
199
     */
200
    public function put($urlPath, $content = NULL, array $parameters = [], array $headers = [])
201
    {
202
        return $this->request(
203
            $this->createRequest(Http\Request::PUT, $urlPath, $parameters, $headers, $content)
204
        );
205
    }
206
207
208
    /**
209
     * @param Http\Request $request
210
     * @return Http\Response
211
     *
212
     */
213
    public function request(Http\Request $request)
214
    {
215
        $request = clone $request;
216
217
        $request->addHeader('Accept', $this->defaultAccept);
218
        $request->addHeader('Time-Zone', date_default_timezone_get());
219
        $request->addHeader('User-Agent', 'milo/github-api');
220
221
        if ($this->token) {
222
            /** @todo Distinguish token type? */
223
            $request->addHeader('Authorization', "token {$this->token->getValue()}");
224
        }
225
226
        return $this->client->request($request);
227
    }
228
229
230
    /**
231
     * @param  string  Http\Request::GET|POST|...
232
     * @param  string  path like '/users/:user/repos' where ':user' is substitution
233
     * @param  array[name => value]  replaces substitutions in $urlPath, the rest is appended as query string to URL
0 ignored issues
show
Documentation Bug introduced by
The doc comment => at position 0 could not be parsed: Unknown type name '=' at position 0 in =>.
Loading history...
234
     * @param  array[name => value]  name is case-insensitive
235
     * @param  mixed|NULL  arrays and objects are encoded to JSON and Content-Type is set
236
     * @return Http\Request
237
     *
238
     * @throws MissingParameterException  when substitution is used in URL but parameter is missing
239
     * @throws JsonException  when encoding to JSON fails
240
     */
241
    public function createRequest($method, $urlPath, array $parameters = [], array $headers = [], $content = NULL)
242
    {
243
        if (stripos($urlPath, $this->url) === 0) {  # Allows non-HTTPS URLs
244
            $baseUrl = $this->url;
245
            $urlPath = \substr($urlPath, \strlen($this->url));
246
247
        } elseif (\preg_match('#^(https://[^/]+)(/.*)?$#', $urlPath, $m)) {
248
            $baseUrl = $m[1];
249
            $urlPath = isset($m[2]) ? $m[2] : '';
250
251
        } else {
252
            $baseUrl = $this->url;
253
        }
254
255
        if (\strpos($urlPath, '{') === FALSE) {
256
            $urlPath = $this->expandColonParameters($urlPath, $parameters, $this->defaultParameters);
0 ignored issues
show
Bug introduced by
It seems like $this->defaultParameters can also be of type null; however, parameter $defaultParameters of XoopsModules\Wggithub\Gi...expandColonParameters() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

256
            $urlPath = $this->expandColonParameters($urlPath, $parameters, /** @scrutinizer ignore-type */ $this->defaultParameters);
Loading history...
257
        } else {
258
            $urlPath = $this->expandUriTemplate($urlPath, $parameters, $this->defaultParameters);
0 ignored issues
show
Bug introduced by
It seems like $this->defaultParameters can also be of type null; however, parameter $defaultParameters of XoopsModules\Wggithub\Gi...pi::expandUriTemplate() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

258
            $urlPath = $this->expandUriTemplate($urlPath, $parameters, /** @scrutinizer ignore-type */ $this->defaultParameters);
Loading history...
259
        }
260
261
        $url = \rtrim($baseUrl, '/') . '/' . \ltrim($urlPath, '/');
262
263
        if ($content !== NULL && (\is_array($content) || \is_object($content))) {
264
            $headers['Content-Type'] = 'application/json; charset=utf-8';
265
            $content = Helpers::jsonEncode($content);
266
        }
267
268
        return new Http\Request($method, $url, $headers, $content);
269
    }
270
271
272
    /**
273
     * @param  Http\Response
274
     * @param  array|NULL  these codes are treated as success; code < 300 if NULL
0 ignored issues
show
Bug introduced by
The type XoopsModules\Wggithub\Github\these was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
275
     * @return mixed
276
     *
277
     * @throws ApiException
278
     */
279
    public function decode(Http\Response $response, array $okCodes = NULL)
280
    {
281
        $content = $response->getContent();
282
        if (\preg_match('~application/json~i', $response->getHeader('Content-Type', ''))) {
283
            try {
284
                $content = Helpers::jsonDecode($response->getContent(), true);
285
            } catch (JsonException $e) {
286
                throw new InvalidResponseException('JSON decoding failed.', 0, $e, $response);
287
            }
288
289
            if (!\is_array($content) && !\is_object($content)) {
0 ignored issues
show
introduced by
The condition is_array($content) is always false.
Loading history...
introduced by
The condition is_object($content) is always false.
Loading history...
290
                throw new InvalidResponseException('Decoded JSON is not an array or object.', 0, NULL, $response);
291
            }
292
        }
293
294
        $code = $response->getCode();
295
        if (($okCodes === NULL && $code >= 300) || (\is_array($okCodes) && !\in_array($code, $okCodes))) {
296
            /** @var $content \stdClass */
297
            switch ($code) {
298
                case Http\Response::S400_BAD_REQUEST:
299
                    throw new BadRequestException(self::errorMessage($content), $code, NULL, $response);
300
301
                case Http\Response::S401_UNAUTHORIZED:
302
                    throw new UnauthorizedException(self::errorMessage($content), $code, NULL, $response);
303
304
                case Http\Response::S403_FORBIDDEN:
305
                    if ($response->getHeader('X-RateLimit-Remaining') === '0') {
306
                        throw new RateLimitExceedException(self::errorMessage($content), $code, NULL, $response);
307
                    }
308
                    throw new ForbiddenException(self::errorMessage($content), $code, NULL, $response);
309
310
                case Http\Response::S404_NOT_FOUND:
311
                    throw new NotFoundException('Resource not found or not authorized to access.', $code, NULL, $response);
312
313
                case Http\Response::S422_UNPROCESSABLE_ENTITY:
314
                    throw new UnprocessableEntityException(self::errorMessage($content), $code, NULL, $response);
315
            }
316
317
            $message = $okCodes === NULL ? '< 300' : \implode(' or ', $okCodes);
318
            throw new UnexpectedResponseException("Expected response with code $message.", $code, NULL, $response);
319
        }
320
321
        return $content;
322
    }
323
324
325
    /**
326
     * Creates paginator for HTTP GET requests.
327
     *
328
     * @see get()
329
     *
330
     * @param  string
331
     * @param array $parameters
332
     * @param array $headers
333
     * @return Paginator
334
     *
335
     */
336
    public function paginator($urlPath, array $parameters = [], array $headers = [])
337
    {
338
        return new Paginator(
339
            $this,
340
            $this->createRequest(Http\Request::GET, $urlPath, $parameters, $headers)
341
        );
342
    }
343
344
345
    /**
346
     * @return Http\IClient
347
     */
348
    public function getClient()
349
    {
350
        return $this->client;
351
    }
352
353
354
    /**
355
     * @param  string
356
     * @return Api
357
     */
358
    public function withUrl($url)
359
    {
360
        $api = clone $this;
361
        $api->setUrl($url);
362
        return $api;
363
    }
364
365
366
    /**
367
     * @param  string
368
     * @return self
369
     */
370
    public function setUrl($url)
371
    {
372
        $this->url = $url;
373
        return $this;
374
    }
375
376
377
    /**
378
     * @return string
379
     */
380
    public function getUrl()
381
    {
382
        return $this->url;
383
    }
384
385
386
    /**
387
     * @param  string
388
     * @param array $parameters
389
     * @param array $defaultParameters
390
     * @return string
391
     *
392
     */
393
    protected function expandColonParameters($url, array $parameters, array $defaultParameters)
394
    {
395
        $parameters += $defaultParameters;
396
397
        $url = preg_replace_callback('#(^|/|\.):([^/.]+)#', function($m) use ($url, & $parameters) {
398
            if (!isset($parameters[$m[2]])) {
399
                throw new MissingParameterException("Missing parameter '$m[2]' for URL path '$url'.");
400
            }
401
            $parameter = $parameters[$m[2]];
402
            unset($parameters[$m[2]]);
403
            return $m[1] . rawurlencode($parameter);
404
        }, $url);
405
406
        $url = \rtrim($url, '/');
407
408
        if (\count($parameters)) {
409
            $url .= '?' . \http_build_query($parameters);
410
        }
411
412
        return $url;
413
    }
414
415
416
    /**
417
     * Expands URI template (RFC 6570).
418
     *
419
     * @see http://tools.ietf.org/html/rfc6570
420
     * @todo Inject remaining default parameters into query string?
421
     *
422
     * @param  string
423
     * @param array $parameters
424
     * @param array $defaultParameters
425
     * @return string
426
     */
427
    protected function expandUriTemplate($url, array $parameters, array $defaultParameters)
428
    {
429
        $parameters += $defaultParameters;
430
431
        static $operatorFlags = [
432
            ''  => ['prefix' => '',  'separator' => ',', 'named' => FALSE, 'ifEmpty' => '',  'reserved' => FALSE],
433
            '+' => ['prefix' => '',  'separator' => ',', 'named' => FALSE, 'ifEmpty' => '',  'reserved' => TRUE],
434
            '#' => ['prefix' => '#', 'separator' => ',', 'named' => FALSE, 'ifEmpty' => '',  'reserved' => TRUE],
435
            '.' => ['prefix' => '.', 'separator' => '.', 'named' => FALSE, 'ifEmpty' => '',  'reserved' => FALSE],
436
            '/' => ['prefix' => '/', 'separator' => '/', 'named' => FALSE, 'ifEmpty' => '',  'reserved' => FALSE],
437
            ';' => ['prefix' => ';', 'separator' => ';', 'named' => TRUE,  'ifEmpty' => '',  'reserved' => FALSE],
438
            '?' => ['prefix' => '?', 'separator' => '&', 'named' => TRUE,  'ifEmpty' => '=', 'reserved' => FALSE],
439
            '&' => ['prefix' => '&', 'separator' => '&', 'named' => TRUE,  'ifEmpty' => '=', 'reserved' => FALSE],
440
        ];
441
442
        return preg_replace_callback('~{([+#./;?&])?([^}]+?)}~', function($m) use ($url, & $parameters, $operatorFlags) {
0 ignored issues
show
Unused Code introduced by
The import $url is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
443
            $flags = $operatorFlags[$m[1]];
444
445
            $translated = [];
446
            foreach (\explode(',', $m[2]) as $name) {
447
                $explode = FALSE;
448
                $maxLength = NULL;
449
                if (\preg_match('~^(.+)(?:(\*)|:(\d+))$~', $name, $tmp)) { // TODO: Speed up?
450
                    $name = $tmp[1];
451
                    if (isset($tmp[3])) {
452
                        $maxLength = (int) $tmp[3];
453
                    } else {
454
                        $explode = TRUE;
455
                    }
456
                }
457
458
                if (!isset($parameters[$name])) {  // TODO: Throw exception?
459
                    continue;
460
                }
461
462
                $value = $parameters[$name];
463
                if (is_scalar($value)) {
464
                    $translated[] = $this->prefix($flags, $name, $this->escape($flags, $value, $maxLength));
465
466
                } else {
467
                    $value = (array) $value;
468
                    $isAssoc = key($value) !== 0;
469
470
                    // The '*' (explode) modifier
471
                    if ($explode) {
472
                        $parts = [];
473
                        if ($isAssoc) {
474
                            $this->walk($value, function ($v, $k) use (& $parts, $flags, $maxLength) {
475
                                $parts[] = $this->prefix(['named' => TRUE] + $flags, $k, $this->escape($flags, $v, $maxLength));
476
                            });
477
478
                        } elseif ($flags['named']) {
479
                            $this->walk($value, function ($v) use (& $parts, $flags, $name, $maxLength) {
480
                                $parts[] = $this->prefix($flags, $name, $this->escape($flags, $v, $maxLength));
481
                            });
482
483
                        } else {
484
                            $this->walk($value, function ($v) use (& $parts, $flags, $maxLength) {
485
                                $parts[] = $this->escape($flags, $v, $maxLength);
486
                            });
487
                        }
488
489
                        if (isset($parts[0])) {
490
                            if ($flags['named']) {
491
                                $translated[] = \implode($flags['separator'], $parts);
492
                            } else {
493
                                $translated[] = $this->prefix($flags, $name, \implode($flags['separator'], $parts));
494
                            }
495
                        }
496
497
                    } else {
498
                        $parts = [];
499
                        $this->walk($value, function($v, $k) use (& $parts, $isAssoc, $flags, $maxLength) {
500
                            if ($isAssoc) {
501
                                $parts[] = $this->escape($flags, $k);
502
                            }
503
504
                            $parts[] = $this->escape($flags, $v, $maxLength);
505
                        });
506
507
                        if (isset($parts[0])) {
508
                            $translated[] = $this->prefix($flags, $name, \implode(',', $parts));
509
                        }
510
                    }
511
                }
512
            }
513
514
            if (isset($translated[0])) {
515
                return $flags['prefix'] . \implode($flags['separator'], $translated);
516
            }
517
518
            return '';
519
        }, $url);
520
    }
521
522
523
    /**
524
     * @param  array
525
     * @param  string
526
     * @param  string  already escaped
0 ignored issues
show
Bug introduced by
The type XoopsModules\Wggithub\Github\already was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
527
     * @return string
528
     */
529
    private function prefix(array $flags, $name, $value)
530
    {
531
        $prefix = '';
532
        if ($flags['named']) {
533
            $prefix .= $this->escape($flags, $name);
534
            if (isset($value[0])) {
535
                $prefix .= '=';
536
            } else {
537
                $prefix .= $flags['ifEmpty'];
538
            }
539
        }
540
541
        return $prefix . $value;
542
    }
543
544
545
    /**
546
     * @param  array
547
     * @param  mixed
548
     * @param  int|NULL
549
     * @return string
550
     */
551
    private function escape(array $flags, $value, $maxLength = NULL)
552
    {
553
        $value = (string) $value;
554
555
        if ($maxLength !== NULL) {
556
            if (\preg_match('~^(.{' . $maxLength . '}).~u', $value, $m)) {
557
                $value = $m[1];
558
            } elseif (\strlen($value) > $maxLength) {  # when malformed UTF-8
559
                $value = \substr($value, 0, $maxLength);
560
            }
561
        }
562
563
        if ($flags['reserved']) {
564
            $parts = preg_split('~(%[0-9a-fA-F]{2}|[:/?#[\]@!$&\'()*+,;=])~', $value, -1, PREG_SPLIT_DELIM_CAPTURE);
565
            $parts[] = '';
566
567
            $escaped = '';
568
            for ($i = 0, $count = \count($parts); $i < $count; $i += 2) {
569
                $escaped .= rawurlencode($parts[$i]) . $parts[$i + 1];
570
            }
571
572
            return $escaped;
573
        }
574
575
        return rawurlencode($value);
576
    }
577
578
579
    /**
580
     * @param  array
581
     * @param  callable
582
     */
583
    private function walk(array $array, $cb)
584
    {
585
        foreach ($array as $k => $v) {
586
            if ($v === NULL) {
587
                continue;
588
            }
589
590
            $cb($v, $k);
591
        }
592
    }
593
594
595
    /**
596
     * @param  \stdClass
597
     * @return string
598
     */
599
    private static function errorMessage($content)
600
    {
601
        $message = isset($content->message)
602
            ? $content->message
603
            : 'Unknown error';
604
605
        if (isset($content->errors)) {
606
            $message .= \implode(', ', array_map(function($error) {
607
                return '[' . \implode(':', (array) $error) . ']';
608
            }, $content->errors));
609
        }
610
611
        return $message;
612
    }
613
614
}
615