Passed
Push — master ( 0d72bd...7b2873 )
by Jimmy
07:01 queued 03:26
created

Model   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 469
Duplicated Lines 0 %

Test Coverage

Coverage 88%

Importance

Changes 0
Metric Value
wmc 52
eloc 92
dl 0
loc 469
ccs 88
cts 100
cp 0.88
rs 7.44
c 0
b 0
f 0

29 Methods

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

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

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