Model   B
last analyzed

Complexity

Total Complexity 52

Size/Duplication

Total Lines 474
Duplicated Lines 0 %

Test Coverage

Coverage 86.92%

Importance

Changes 19
Bugs 0 Features 1
Metric Value
wmc 52
eloc 93
c 19
b 0
f 1
dl 0
loc 474
ccs 93
cts 107
cp 0.8692
rs 7.44

29 Methods

Rating   Name   Duplication   Size   Complexity  
A setAttribute() 0 13 3
A offsetGet() 0 3 1
A __construct() 0 4 1
A __unset() 0 3 1
B castTo() 0 47 9
A getterMethodName() 0 3 1
A __toString() 0 3 1
A hasCast() 0 5 2
A __call() 0 20 6
A __isset() 0 3 1
A hasGetter() 0 3 1
A toJson() 0 3 1
A serialize() 0 3 1
A __set() 0 3 1
A count() 0 3 1
A getCasts() 0 7 2
A __debugInfo() 0 3 1
A getIterator() 0 3 1
A setterMethodName() 0 3 1
A getAttribute() 0 25 6
A toArray() 0 4 1
A offsetUnset() 0 3 1
A fill() 0 7 2
A offsetSet() 0 3 1
A jsonSerialize() 0 3 1
A offsetExists() 0 3 1
A hasSetter() 0 3 1
A __get() 0 3 1
A unserialize() 0 3 1

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
namespace Spinen\ConnectWise\Support;
4
5
use ArrayAccess;
6
use ArrayIterator;
7
use Carbon\Carbon;
8
use Countable;
9
use Illuminate\Contracts\Support\Arrayable;
10
use Illuminate\Contracts\Support\Jsonable;
11
use Illuminate\Support\Str;
12
use InvalidArgumentException;
13
use IteratorAggregate;
14
use JsonSerializable;
15
use Serializable;
16
use Spinen\ConnectWise\Api\Client;
17
18
/**
19
 * Class Model
20
 *
21
 * This class is heavily modeled after Laravel's Eloquent model.  We are wanting the API to be familiar as we use
22
 * Laravel for most of our projects & want it to be very easy for our developers to use it.  Additionally, it is
23
 * just so well done that there is no reason not to copy/reuse some of the code.
24
 *
25
 * @package Spinen\ConnectWise\Support
26
 */
27
abstract class Model implements
28
    ArrayAccess,
29
    Arrayable,
30
    Countable,
31
    IteratorAggregate,
32
    Jsonable,
33
    JsonSerializable,
34
    Serializable
35
{
36
    /**
37
     * _info property
38
     */
39
    protected $_info = null;
40
41
    /**
42
     * The collection of attributes for the model
43
     *
44
     * @var array
45
     */
46
    protected $attributes = [];
47
48
    /**
49
     * Properties that need to be casts to a specific object or type
50
     *
51
     * @var array
52
     */
53
    protected $casts = [];
54
55
    /**
56
     * Client instance to go get related properties
57
     *
58
     * @var Client|null
59
     */
60
    protected $client;
61
62
    /**
63
     * Model constructor
64
     *
65
     * @param array $attributes
66
     * @param Client|null $client
67
     */
68 13
    public function __construct(array $attributes, Client $client = null)
69
    {
70 13
        $this->client = $client;
71 13
        $this->fill($attributes);
72 13
    }
73
74
    /**
75
     * Magic method to allow getting related items
76
     *
77
     * @param string $method
78
     * @param mixed $arguments
79
     *
80
     * @return mixed
81
     */
82
    public function __call($method, $arguments)
83
    {
84
        // Call existing method
85
        if (method_exists($this, $method)) {
86
            return call_user_func_array([$this, $method], $arguments);
87
        }
88
89
        // Look to see if the property has a relationship to call
90
        if ($this->client && ($this->{$method}->_info ?? null)) {
91
            foreach ($this->{$method}['_info'] as $k => $v) {
92
                if (Str::startsWith($v, $this->client->getUrl())) {
93
                    // Cache so that other request will not trigger additional calls
94
                    $this->setAttribute($method, $this->client->get($v));
95
96
                    return $this->{$method};
97
                }
98
            }
99
        }
100
101
        trigger_error('Call to undefined method ' . __CLASS__ . '::' . $method . '()', E_USER_ERROR);
102
    }
103
104
    /**
105
     * Only return the attributes for a var_dump
106
     *
107
     * This object proxies the properties to the keys in the attributes array, so only
108
     * expose it when doing a var_dump as the other properties are not needed in debugging.
109
     *
110
     * @return array
111
     */
112 1
    public function __debugInfo()
113
    {
114 1
        return $this->attributes;
115
    }
116
117
    /**
118
     * Allow the attributes of the model to be accessed like a public property
119
     *
120
     * @param string $attribute
121
     *
122
     * @return mixed
123
     */
124 4
    public function __get($attribute)
125
    {
126 4
        return $this->getAttribute($attribute);
127
    }
128
129
    /**
130
     * Allow checking to see if the model has an attribute set
131
     *
132
     * @param string $attribute
133
     *
134
     * @return bool
135
     */
136 4
    public function __isset($attribute)
137
    {
138 4
        return array_key_exists($attribute, $this->attributes);
139
    }
140
141
    /**
142
     * Set a property on the model in the attributes
143
     *
144
     * @param string $attribute
145
     * @param mixed $value
146
     */
147 1
    public function __set($attribute, $value)
148
    {
149 1
        $this->setAttribute($attribute, $value);
150 1
    }
151
152
    /**
153
     * Convert the model to its string representation
154
     *
155
     * @return string
156
     */
157 1
    public function __toString()
158
    {
159 1
        return $this->toJson();
160
    }
161
162
    /**
163
     * Unset a property on the model in the attributes
164
     *
165
     * @param string $attribute
166
     */
167 1
    public function __unset($attribute)
168
    {
169 1
        unset($this->attributes[$attribute]);
170 1
    }
171
172
    /**
173
     * Cast a item to a specific object or type
174
     *
175
     * @param mixed $value
176
     * @param string $cast
177
     *
178
     * @return mixed
179
     */
180 13
    public function castTo($value, $cast)
181
    {
182 13
        if (is_null($value) || is_object($value)) {
183 11
            return $value;
184
        }
185
186 13
        if (Carbon::class === $cast) {
187 11
            return Carbon::parse($value);
188
        }
189
190 13
        if (class_exists($cast)) {
191 11
            return new $cast((array)$value);
192
        }
193
194 13
        if (strcasecmp('json', $cast) == 0) {
195 11
            return json_encode((array)$value);
196
        }
197
198 13
        if (strcasecmp('collection', $cast) == 0) {
199 11
            return new Collection((array)$value);
200
        }
201
202 13
        if (in_array($cast, ['bool', 'boolean'])) {
203 13
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
204
        }
205
206
        $cast_types = [
207 13
            'array',
208
            'bool',
209
            'boolean',
210
            'double',
211
            'float',
212
            'int',
213
            'integer',
214
            'null',
215
            'object',
216
            'string',
217
        ];
218
219 13
        if (!in_array($cast, $cast_types)) {
220 1
            throw new InvalidArgumentException(sprintf("Attributes cannot be casted to [%s] type.", $cast));
221
        }
222
223 13
        settype($value, $cast);
224
225
        // settype returns true/false for pass/fail, not the value
226 13
        return $value;
227
    }
228
229
    /**
230
     * Count the number of properties
231
     *
232
     * @return int
233
     */
234 1
    public function count()
235
    {
236 1
        return count($this->attributes);
237
    }
238
239
    /**
240
     * Store the collection of attributes on the model
241
     *
242
     * @param array $attributes
243
     *
244
     * @return $this
245
     */
246 13
    public function fill(array $attributes)
247
    {
248 13
        foreach ($attributes as $attribute => $value) {
249 13
            $this->setAttribute($attribute, $value);
250
        }
251
252 13
        return $this;
253
    }
254
255
    /**
256
     * Check to see if there is a getter for the attribute
257
     *
258
     * @param string $attribute
259
     *
260
     * @return bool
261
     */
262 4
    public function hasGetter($attribute)
263
    {
264 4
        return method_exists($this, $this->getterMethodName($attribute));
265
    }
266
267
    /**
268
     * Check to see if there is a setter for the attribute
269
     *
270
     * @param string $attribute
271
     *
272
     * @return bool
273
     */
274 13
    public function hasSetter($attribute)
275
    {
276 13
        return method_exists($this, $this->setterMethodName($attribute));
277
    }
278
279
    /**
280
     * Is the attribute supposed to be cast
281
     *
282
     * @param string $attribute
283
     *
284
     * @return bool
285
     */
286 13
    public function hasCast($attribute)
287
    {
288 13
        $cast = $this->getCasts($attribute);
289
290 13
        return !empty($cast) && is_string($cast);
291
    }
292
293
    /**
294
     * Get the attribute from the model
295
     *
296
     * @param string $attribute
297
     *
298
     * @return mixed
299
     */
300 4
    public function getAttribute($attribute)
301
    {
302
        // Guard against no attribute
303 4
        if (!$attribute) {
304
            return;
305
        }
306
307
        // Use getter on model if there is one
308 4
        if ($this->hasGetter($attribute)) {
309 1
            return $this->{$this->getterMethodName($attribute)}();
310
        }
311
312
        // Allow for making related calls for "extra" properties in the "_info" property.
313
        // Cache the results so only 1 call is made
314 4
        if (!isset($this->{$attribute}) && isset($this->_info->{$attribute . '_href'})) {
315
            $this->setAttribute($attribute, $this->client->getAll($this->_info->{$attribute . '_href'}));
0 ignored issues
show
Bug introduced by
The method getAll() does not exist on null. ( Ignorable by Annotation )

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

315
            $this->setAttribute($attribute, $this->client->/** @scrutinizer ignore-call */ getAll($this->_info->{$attribute . '_href'}));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
316
        }
317
318
        // Pull the value from the attributes
319 4
        if (isset($this->{$attribute})) {
320 4
            return $this->attributes[$attribute];
321
        };
322
323
        // Attribute does not exist on the model
324
        trigger_error('Undefined property:' . __CLASS__ . '::$' . $attribute);
325
    }
326
327
    /**
328
     * Get the array of cast or a specific cast for an attribute
329
     *
330
     * @param mixed|null $attribute
331
     *
332
     * @return mixed
333
     */
334 13
    public function getCasts($attribute = null)
335
    {
336 13
        if (array_key_exists($attribute, $this->casts)) {
337 13
            return $this->casts[$attribute];
338
        }
339
340 2
        return $this->casts;
341
    }
342
343
    /**
344
     * Get an iterator for the attributes
345
     *
346
     * @return ArrayIterator
347
     */
348 1
    public function getIterator()
349
    {
350 1
        return new ArrayIterator($this->attributes);
351
    }
352
353
    /**
354
     * Build the name of the getter for an attribute
355
     *
356
     * @param string $attribute
357
     *
358
     * @return string
359
     */
360 4
    protected function getterMethodName($attribute)
361
    {
362 4
        return 'get' . Str::studly($attribute) . 'Attribute';
363
    }
364
365
    /**
366
     * Serialize Json (convert it to an array)
367
     *
368
     * @return array
369
     */
370 2
    public function jsonSerialize()
371
    {
372 2
        return $this->toArray();
373
    }
374
375
    /**
376
     * Allow the model to behave like an associative array, so see if the attribute is set
377
     *
378
     * @param string $attribute
379
     *
380
     * @return boolean
381
     */
382 2
    public function offsetExists($attribute)
383
    {
384 2
        return isset($this->{$attribute});
385
    }
386
387
    /**
388
     * Allow the model to behave like an associative array, so get attribute
389
     *
390
     * @param string $attribute
391
     *
392
     * @return mixed
393
     */
394 4
    public function offsetGet($attribute)
395
    {
396 4
        return $this->{$attribute};
397
    }
398
399
    /**
400
     * Allow the model to behave like an associative array, so set attribute
401
     *
402
     * @param string $attribute
403
     * @param mixed $value
404
     */
405 1
    public function offsetSet($attribute, $value)
406
    {
407 1
        $this->{$attribute} = $value;
408 1
    }
409
410
    /**
411
     * Allow the model to behave like an associative array, so unset attribute
412
     *
413
     * @param mixed $attribute
414
     *
415
     * @return void
416
     */
417 1
    public function offsetUnset($attribute)
418
    {
419 1
        unset($this->{$attribute});
420 1
    }
421
422
    /**
423
     * Serialize the attributes
424
     *
425
     * @return string
426
     */
427 1
    public function serialize()
428
    {
429 1
        return serialize($this->attributes);
430
    }
431
432
    /**
433
     * Set value on an attribute
434
     *
435
     * Since there can be a setter for an attribute, look to see if there is one to delegate the setting.  Then see if
436
     * the attribute is supposed to be cast to a specific value before setting.  Finally, store the value on the model.
437
     *
438
     * @param string $attribute
439
     * @param mixed $value
440
     *
441
     * @return $this
442
     */
443 13
    public function setAttribute($attribute, $value)
444
    {
445 13
        if ($this->hasSetter($attribute)) {
446 1
            return $this->{$this->setterMethodName($attribute)}($value);
447
        }
448
449 13
        if ($this->hasCast($attribute)) {
450 13
            $value = $this->castTo($value, $this->getCasts($attribute));
0 ignored issues
show
Bug introduced by
It seems like $this->getCasts($attribute) can also be of type array; however, parameter $cast of Spinen\ConnectWise\Support\Model::castTo() does only seem to accept string, 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

450
            $value = $this->castTo($value, /** @scrutinizer ignore-type */ $this->getCasts($attribute));
Loading history...
451
        }
452
453 13
        $this->attributes[$attribute] = $value;
454
455 13
        return $this;
456
    }
457
458
    /**
459
     * Build the name of the setter for an attribute
460
     *
461
     * @param string $attribute
462
     *
463
     * @return string
464
     */
465 13
    protected function setterMethodName($attribute)
466
    {
467 13
        return 'set' . Str::studly($attribute) . 'Attribute';
468
    }
469
470
    /**
471
     * Return the model as an array
472
     *
473
     * @return array
474
     */
475 3
    public function toArray()
476
    {
477
        // TODO: Need to actually roll through the attributes & make sure that nested objects are converted
478 3
        return $this->attributes;
479
    }
480
481
    /**
482
     * Return the model as JSON
483
     *
484
     * @param int $options
485
     *
486
     * @return string
487
     */
488 2
    public function toJson($options = 0)
489
    {
490 2
        return json_encode($this->jsonSerialize(), $options);
491
    }
492
493
    /**
494
     * Unserialize the attributes
495
     *
496
     * @param string $serialized
497
     */
498 1
    public function unserialize($serialized)
499
    {
500 1
        $this->attributes = unserialize($serialized);
501 1
    }
502
}
503