Completed
Branch v2-beta (bab4d8)
by Karl
06:34
created

AbstractProvider   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 493
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 61.36%

Importance

Changes 8
Bugs 0 Features 1
Metric Value
wmc 47
c 8
b 0
f 1
lcom 1
cbo 5
dl 0
loc 493
ccs 81
cts 132
cp 0.6136
rs 8.439

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A __get() 0 12 2
A __set() 0 13 2
createJobObject() 0 1 ?
defaultParameters() 0 1 ?
defaultResponseFields() 0 1 ?
getListingsPath() 0 1 ?
requiredParameters() 0 1 ?
validParameters() 0 1 ?
A getFormat() 0 4 1
A getHttpClientOptions() 0 9 2
A getJobs() 0 21 3
A getSource() 0 4 1
A getQueryString() 0 4 1
A getUrl() 0 7 2
A getVerb() 0 4 1
A isValidParameter() 0 4 1
A parseAttributeDefaults() 0 9 2
A parseLocation() 0 4 1
A requiredParamsIncluded() 0 9 3
A setClient() 0 6 1
A getJobsCollectionFromListings() 0 14 1
A getRawListings() 0 12 2
A getValue() 0 17 4
A parseAsFormat() 0 10 2
A updateQuery() 0 7 2
A getValueCurrentIndex() 0 4 3
A isArrayNotEmpty() 0 4 2
A parseAsJson() 0 16 3
A parseAsXml() 0 19 2
A getShortName() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractProvider 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 AbstractProvider, and based on these observations, apply Extract Interface, too.

1
<?php namespace JobApis\Jobs\Client\Providers;
2
3
use GuzzleHttp\Client as HttpClient;
4
use JobApis\Jobs\Client\AttributeTrait;
5
use JobApis\Jobs\Client\Collection;
6
use JobApis\Jobs\Client\Exceptions\MissingParameterException;
7
8
abstract class AbstractProvider
9
{
10
    use AttributeTrait;
11
12
    /**
13
     * Base API Url
14
     *
15
     * @var string
16
     */
17
    protected $baseUrl;
18
19
    /**
20
     * HTTP Client
21
     *
22
     * @var HttpClient
23
     */
24
    protected $client;
25
26
    /**
27
     * Query params
28
     *
29
     * @var array
30
     */
31
    protected $queryParams = [];
32
33
    /**
34
     * Create new client
35
     *
36
     * @param array $parameters
37
     */
38 14
    public function __construct($parameters = [])
39
    {
40 14
        $parameters = array_merge($this->defaultParameters(), $parameters);
41
42 14
        array_walk($parameters, [$this, 'updateQuery']);
43
44 14
        $this->setClient(new HttpClient);
45 14
    }
46
47
    /**
48
     * Get query param as properties, if exists
49
     *
50
     * @param  string $name
0 ignored issues
show
Bug introduced by
There is no parameter named $name. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
51
     *
52
     * @return mixed
53
     * @throws \OutOfRangeException
54
     */
55 12
    public function __get($key)
56
    {
57
        // Then check to see if there's a query parameter
58 12
        if (!isset($this->queryParams[$key])) {
59
            throw new \OutOfRangeException(sprintf(
60
                '%s does not contain a property by the name of "%s"',
61
                __CLASS__,
62
                $key
63
            ));
64
        }
65 12
        return $this->queryParams[$key];
66
    }
67
68
    /**
69
     * Set query param as properties, if exists
70
     *
71
     * @param  string $name
0 ignored issues
show
Bug introduced by
There is no parameter named $name. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
72
     *
73
     * @return mixed
74
     * @throws \OutOfRangeException
75
     */
76 10
    public function __set($key, $value)
77
    {
78
        // Then check to see if there's a query parameter
79 10
        if (!$this->isValidParameter($key)) {
80 2
            throw new \OutOfRangeException(sprintf(
81 2
                '%s does not contain a property by the name of "%s"',
82 2
                __CLASS__,
83
                $key
84 2
            ));
85
        }
86 8
        $this->queryParams[$key] = $value;
87 8
        return $this;
88
    }
89
90
    /**
91
     * Returns the standardized job object
92
     *
93
     * @param array|object $payload
94
     *
95
     * @return \JobApis\Jobs\Client\Job
96
     */
97
    abstract public function createJobObject($payload);
98
99
    /**
100
     * Get default parameters and values
101
     *
102
     * @return  string
103
     */
104
    abstract public function defaultParameters();
105
106
    /**
107
     * Job object default keys that must be set.
108
     *
109
     * @return  string
110
     */
111
    abstract public function defaultResponseFields();
112
113
    /**
114
     * Get listings path
115
     *
116
     * @return  string
117
     */
118
    abstract public function getListingsPath();
119
120
    /**
121
     * Get parameters that MUST be set in order to satisfy the APIs requirements
122
     *
123
     * @return  string
124
     */
125
    abstract public function requiredParameters();
126
127
    /**
128
     * Get parameters that CAN be set
129
     *
130
     * @return  string
131
     */
132
    abstract public function validParameters();
133
134
    // Public methods
135
136
    /**
137
     * Get format
138
     *
139
     * @return  string Currently only 'json' and 'xml' supported
140
     */
141 2
    public function getFormat()
142
    {
143 2
        return 'json';
144
    }
145
146
    /**
147
     * Get http client options based on current client
148
     *
149
     * @return array
150
     */
151
    public function getHttpClientOptions()
152
    {
153
        $options = [];
154
        if (strtolower($this->getVerb()) != 'get') {
155
            $options['body'] = $this->queryParams;
156
        }
157
158
        return $options;
159
    }
160
161
    /**
162
     * Makes the api call and returns a collection of job objects
163
     *
164
     * @return  JobApis\Jobs\Client\Collection
165
     * @throws MissingParameterException
166
     */
167
    public function getJobs()
168
    {
169
        if ($this->requiredParamsIncluded()) {
170
            $client = $this->client;
171
            $verb = strtolower($this->getVerb());
172
            $url = $this->getUrl();
173
            $options = $this->getHttpClientOptions();
174
175
            $response = $client->{$verb}($url, $options);
176
177
            $body = (string) $response->getBody();
178
179
            $payload = $this->parseAsFormat($body, $this->getFormat());
180
181
            $listings = is_array($payload) ? $this->getRawListings($payload) : [];
182
183
            return $this->getJobsCollectionFromListings($listings);
184
        } else {
185
            throw new MissingParameterException("All Required parameters for this provider must be set");
186
        }
187
    }
188
189
    /**
190
     * Get source attribution
191
     *
192
     * @return string
193
     */
194 2
    public function getSource()
195
    {
196 2
        return $this->getShortName();
197
    }
198
199
    /**
200
     * Get query string for client based on properties
201
     *
202
     * @return string
203
     */
204
    public function getQueryString()
205
    {
206
        return '?'.http_build_query($this->queryParams);
207
    }
208
209
    /**
210
     * Get url
211
     *
212
     * @return  string
213
     */
214
    public function getUrl()
215
    {
216
        if (!$this->baseUrl) {
217
            throw new MissingParameterException("Base URL parameter not set in provider.");
218
        }
219
        return $this->baseUrl.$this->getQueryString();
220
    }
221
222
    /**
223
     * Get http verb to use when making request
224
     *
225
     * @return  string
226
     */
227
    public function getVerb()
228
    {
229
        return 'GET';
230
    }
231
232
    /**
233
     * Check whether a key is valid for this client
234
     *
235
     * @return  string
236
     */
237 10
    public function isValidParameter($key = null)
238
    {
239 10
        return in_array($key, $this->validParameters());
240
    }
241
242
    /**
243
     * Parse job attributes against defaults
244
     *
245
     * @param  array $attributes
246
     * @param  array $defaults
247
     *
248
     * @return array
249
     */
250 2
    public static function parseAttributeDefaults(array $attributes, array $defaults = array())
251
    {
252
        array_map(function ($attribute) use (&$attributes) {
253 2
            if (!isset($attributes[$attribute])) {
254 2
                $attributes[$attribute] = null;
255 2
            }
256 2
        }, $defaults);
257 2
        return $attributes;
258
    }
259
260
    /**
261
     * Parse location string into components.
262
     *
263
     * @param string $location
264
     *
265
     * @return  array
266
     **/
267
    public static function parseLocation($location, $separator = ', ')
268
    {
269
        return explode($separator, $location);
270
    }
271
272
    /**
273
     * Determines if all required parameters have been set
274
     *
275
     * @return  bool
276
     */
277 2
    public function requiredParamsIncluded()
278
    {
279 2
        foreach ($this->requiredParameters() as $key) {
0 ignored issues
show
Bug introduced by
The expression $this->requiredParameters() of type string is not traversable.
Loading history...
280 2
            if (!isset($this->queryParams[$key])) {
281
                return false;
282
            }
283 2
        }
284 2
        return true;
285
    }
286
287
    /**
288
     * Sets http client
289
     *
290
     * @param HttpClient $client
291
     *
292
     * @return  AbstractProvider
293
     */
294 14
    public function setClient(HttpClient $client)
295
    {
296 14
        $this->client = $client;
297
298 14
        return $this;
299
    }
300
301
    // Protected methods
302
303
    /**
304
     * Create and get collection of jobs from given listings
305
     *
306
     * @param  array $listings
307
     *
308
     * @return Collection
309
     */
310 2
    protected function getJobsCollectionFromListings(array $listings = [])
311
    {
312 2
        $collection = new Collection;
313
314 2
        array_map(function ($item) use ($collection) {
315 2
            $item = static::parseAttributeDefaults($item, $this->defaultResponseFields());
0 ignored issues
show
Documentation introduced by
$this->defaultResponseFields() is of type string, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
316 2
            $job = $this->createJobObject($item);
317 2
            $job->setQuery($this->getKeyword())
0 ignored issues
show
Documentation Bug introduced by
The method getKeyword does not exist on object<JobApis\Jobs\Clie...iders\AbstractProvider>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
318 2
                ->setSource($this->getSource());
319 2
            $collection->add($job);
320 2
        }, $listings);
321
322 2
        return $collection;
323
    }
324
325
    /**
326
     * Get raw listings from payload
327
     *
328
     * @param  array $payload
329
     *
330
     * @return array
331
     */
332 2
    protected function getRawListings(array $payload = array())
333
    {
334 2
        $path = $this->getListingsPath();
335
336 2
        if (!empty($path)) {
337 2
            $index = explode('.', $path);
338
339 2
            return (array) self::getValue($index, $payload);
340
        }
341
342
        return (array) $payload;
343
    }
344
345
    /**
346
     * Navigate through a payload array looking for a particular index
347
     *
348
     * @param array $index The index sequence we are navigating down
349
     * @param array $value The portion of the config array to process
350
     *
351
     * @return mixed
352
     */
353 2
    protected static function getValue($index, $value)
354
    {
355 2
        $current_index = self::getValueCurrentIndex($index);
356
357 2
        if (isset($value[$current_index])) {
358 2
            $index_array = self::isArrayNotEmpty($index);
359 2
            $value_array = self::isArrayNotEmpty($value[$current_index]);
360
361 2
            if ($index_array && $value_array) {
362
                return self::getValue($index, $value[$current_index]);
363
            } else {
364 2
                return $value[$current_index];
365
            }
366
        } else {
367
            throw new \OutOfRangeException("Attempt to access missing variable: $current_index");
368
        }
369
    }
370
371
    /**
372
     * Attempt to parse string as given format
373
     *
374
     * @param  string  $string
375
     * @param  string  $format
376
     *
377
     * @return array
378
     */
379 2
    protected function parseAsFormat($string, $format)
380
    {
381 2
        $method = 'parseAs'.ucfirst(strtolower($format));
382
383 2
        if (method_exists($this, $method)) {
384 2
            return $this->$method($string);
385
        }
386
387
        return [];
388
    }
389
390
    /**
391
     * Attempts to update current query parameters.
392
     *
393
     * @param  string  $value
394
     * @param  string  $key
395
     *
396
     * @return AbstractProvider
397
     */
398 14
    protected function updateQuery($value, $key)
399
    {
400 14
        if (in_array($key, $this->validParameters())) {
401 14
            $this->queryParams[$key] = $value;
402 14
        }
403 14
        return $this;
404
    }
405
406
    // Private methods
407
408
    /**
409
     * Get value current index
410
     *
411
     * @param  mixed $index
412
     *
413
     * @return array|null
414
     */
415 2
    private static function getValueCurrentIndex(&$index)
416
    {
417 2
        return is_array($index) && count($index) ? array_shift($index) : null;
418
    }
419
420
    /**
421
     * Checks if given value is an array and that it has contents
422
     *
423
     * @param  mixed $array
424
     *
425
     * @return boolean
426
     */
427 2
    private static function isArrayNotEmpty($array)
428
    {
429 2
        return is_array($array) && count($array);
430
    }
431
432
    /**
433
     * Attempt to parse as Json
434
     *
435
     * @param  string $string
436
     *
437
     * @return array
438
     */
439 2
    private function parseAsJson($string)
440
    {
441
        try {
442 2
            $json = json_decode($string, true);
443
444 2
            if (json_last_error() != JSON_ERROR_NONE) {
445
                throw new \Exception;
446
            }
447
448 2
            return $json;
449
        } catch (\Exception $e) {
450
            // Ignore malformed json.
451
        }
452
453
        return [];
454
    }
455
456
    /**
457
     * Attempt to parse as XML
458
     *
459
     * @param  string $string
460
     *
461
     * @return array
462
     */
463
    private function parseAsXml($string)
464
    {
465
        try {
466
            return json_decode(
467
                json_encode(
468
                    simplexml_load_string(
469
                        $string,
470
                        null,
471
                        LIBXML_NOCDATA
472
                    )
473
                ),
474
                true
475
            );
476
        } catch (\Exception $e) {
477
            // Ignore malformed xml.
478
        }
479
480
        return [];
481
    }
482
483
    /**
484
     * Get short name of a given or current class
485
     *
486
     * @param  object $object Optional object
487
     *
488
     * @return string
489
     */
490 2
    private function getShortName($object = null)
491
    {
492 2
        if (is_null($object)) {
493 2
            $object = $this;
494 2
        }
495
496 2
        $ref = new \ReflectionClass(get_class($object));
497
498 2
        return $ref->getShortName();
499
    }
500
}
501