Completed
Push — develop ( 759f4f...a2304e )
by Stephen
18s queued 10s
created

Model   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 401
Duplicated Lines 0 %

Test Coverage

Coverage 76.4%

Importance

Changes 0
Metric Value
wmc 45
eloc 79
dl 0
loc 401
ccs 68
cts 89
cp 0.764
rs 8.8
c 0
b 0
f 0

24 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 49 8
A getterMethodName() 0 3 1
A __toString() 0 3 1
A hasCast() 0 5 2
A __isset() 0 3 1
B __call() 0 20 7
A hasGetter() 0 3 1
A __set() 0 3 1
A getCasts() 0 7 2
A setterMethodName() 0 3 1
A getAttribute() 0 15 4
A offsetUnset() 0 3 1
A fill() 0 7 2
A offsetSet() 0 3 1
A offsetExists() 0 3 1
A hasSetter() 0 3 1
A __get() 0 3 1
A toJson() 0 3 1
A toArray() 0 4 1
A jsonSerialize() 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 Carbon\Carbon;
7
use Illuminate\Contracts\Support\Arrayable;
8
use Illuminate\Contracts\Support\Jsonable;
9
use Illuminate\Support\Str;
10
use InvalidArgumentException;
11
use JsonSerializable;
12
use Spinen\ConnectWise\Api\Client;
13
14
/**
15
 * Class Model
16
 *
17
 * This class is heavily modeled after Laravel's Eloquent model.  We are wanting the API to be familiar as we use
18
 * Laravel for most of our projects & want it to be very easily to have our developers use it.  Additionally, it is
19
 * just so well done, that there there is not a reason to not copy/reuse some of the code.
20
 *
21
 * @package Spinen\ConnectWise\Support
22
 */
23
abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable
24
{
25
    /**
26
     * The collection of attributes for the model
27
     *
28
     * @var array
29
     */
30
    protected $attributes = [];
31
32
    /**
33
     * Properties that need to be casts to a specific object or type
34
     *
35
     * @var array
36
     */
37
    protected $casts = [];
38
39
    /**
40
     * Client instance to go get related properties
41
     *
42
     * @var Client|null
43
     */
44
    protected $client;
45
46
    /**
47
     * Model constructor.
48
     *
49
     * @param array $attributes
50
     * @param Client|null $client
51
     */
52 8
    public function __construct(array $attributes, Client $client = null)
53
    {
54 8
        $this->client = $client;
55 8
        $this->fill($attributes);
56
    }
57
58
    /**
59
     * Convert the model to its string representation.
60
     *
61
     * @return string
62
     */
63
    public function __toString()
64
    {
65
        return $this->toJson();
66
    }
67
68
    /**
69
     * Magic method to allow getting related items
70
     *
71
     * @param string $method
72
     * @param mixed $arguments
73
     *
74
     * @return mixed
75
     */
76
    public function __call($method, $arguments)
77
    {
78
        // Call existing method
79
        if (method_exists($this, $method)) {
80
            return call_user_func_array([$this, $method], $arguments);
81
        }
82
83
        // Look to see if the property has a relationship to call
84
        if ($this->client && is_array($this->{$method}) && array_key_exists('_info', $this->{$method})) {
85
            foreach ($this->{$method}['_info'] as $k => $v) {
86
                if (Str::startsWith($v, $this->client->getUrl())) {
87
                    // Cache so that other request will not trigger additional calls
88
                    $this->{$method} = $this->client->get(Str::replaceFirst($this->client->getUrl(), '', $v));
89
90
                    return $this->{$method};
91
                }
92
            }
93
        }
94
95
        trigger_error('Call to undefined method ' . __CLASS__ . '::' . $method . '()', E_USER_ERROR);
96
    }
97
98
    /**
99
     * Allow the attributes of the model to be accessed like a public property
100
     *
101
     * @param string $attribute
102
     *
103
     * @return mixed
104
     */
105 3
    public function __get($attribute)
106
    {
107 3
        return $this->getAttribute($attribute);
108
    }
109
110
    /**
111
     * Allow checking to see if the model has an attribute set
112
     *
113
     * @param string $attribute
114
     *
115
     * @return bool
116
     */
117 3
    public function __isset($attribute)
118
    {
119 3
        return array_key_exists($attribute, $this->attributes);
120
    }
121
122
    /**
123
     * Set a property on the model in the attributes
124
     *
125
     * @param string $attribute
126
     * @param mixed  $value
127
     */
128
    public function __set($attribute, $value)
129
    {
130
        $this->setAttribute($attribute, $value);
131
    }
132
133
    /**
134
     * Unset a property on the model in the attributes
135
     *
136
     * @param string $attribute
137
     */
138
    public function __unset($attribute)
139
    {
140
        unset($this->attributes[$attribute]);
141
    }
142
143
    /**
144
     * Cast a item to a specific object or type
145
     *
146
     * @param mixed  $value
147
     * @param string $cast
148
     *
149
     * @return mixed
150
     */
151 5
    public function castTo($value, $cast)
152
    {
153 5
        if (is_null($value)) {
154 2
            return $value;
155
        }
156
157 5
        $class = 'Spinen\ConnectWise\\' . $cast;
158
159 5
        if (class_exists($class)) {
160 2
            return new $class($value);
161
        }
162
163 5
        if (strcasecmp('carbon', $cast) == 0) {
164 2
            return Carbon::parse($value);
165
        }
166
167 5
        if (strcasecmp('json', $cast) == 0) {
168 2
            return json_encode((array)$value);
169
        }
170
171 5
        if (strcasecmp('collection', $cast) == 0) {
172 2
            return new Collection((array)$value);
173
        }
174
175 5
        if (in_array($cast, ['bool', 'boolean'])) {
176 4
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
177
        }
178
179
        $cast_types = [
180 3
            'array',
181
            'bool',
182
            'boolean',
183
            'double',
184
            'float',
185
            'int',
186
            'integer',
187
            'null',
188
            'object',
189
            'string',
190
        ];
191
192 3
        if (!in_array($cast, $cast_types)) {
193 1
            throw new InvalidArgumentException(sprintf("Attributes cannot be casted to [%s] type.", $cast));
194
        }
195
196 2
        settype($value, $cast);
197
198
        // settype returns true/false for pass/fail, not the value
199 2
        return $value;
200
    }
201
202
    /**
203
     * Store the collection of attributes on the model
204
     *
205
     * @param array $attributes
206
     *
207
     * @return $this
208
     */
209 8
    public function fill(array $attributes)
210
    {
211 8
        foreach ($attributes as $attribute => $value) {
212 7
            $this->setAttribute($attribute, $value);
213
        }
214
215 7
        return $this;
216
    }
217
218
    /**
219
     * Check to see if there is a getter for the attribute
220
     *
221
     * @param string $attribute
222
     *
223
     * @return bool
224
     */
225 3
    public function hasGetter($attribute)
226
    {
227 3
        return method_exists($this, $this->getterMethodName($attribute));
228
    }
229
230
    /**
231
     * Check to see if there is a setter for the attribute
232
     *
233
     * @param string $attribute
234
     *
235
     * @return bool
236
     */
237 7
    public function hasSetter($attribute)
238
    {
239 7
        return method_exists($this, $this->setterMethodName($attribute));
240
    }
241
242
    /**
243
     * Is the attribute supposed to be cast
244
     *
245
     * @param string $attribute
246
     *
247
     * @return bool
248
     */
249 7
    public function hasCast($attribute)
250
    {
251 7
        $cast = $this->getCasts($attribute);
252
253 7
        return !empty($cast) && is_string($cast);
254
    }
255
256
    /**
257
     * Get the attribute from the model
258
     *
259
     * @param string $attribute
260
     *
261
     * @return mixed
262
     */
263 3
    public function getAttribute($attribute)
264
    {
265 3
        if (!$attribute) {
266
            return;
267
        }
268
269 3
        if ($this->hasGetter($attribute)) {
270 1
            return $this->{$this->getterMethodName($attribute)}();
271
        }
272
273 3
        if (isset($this->$attribute)) {
274 3
            return $this->attributes[$attribute];
275
        };
276
277
        trigger_error('Undefined property:'. __CLASS__ . '::$' . $attribute);
278
    }
279
280
    /**
281
     * Get the array of cast or a specific cast for an attribute
282
     *
283
     * @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...
284
     *
285
     * @return mixed
286
     */
287 7
    public function getCasts($attribute = null)
288
    {
289 7
        if (array_key_exists($attribute, $this->casts)) {
290 5
            return $this->casts[$attribute];
291
        }
292
293 2
        return $this->casts;
294
    }
295
296
    /**
297
     * Build the name of the getter for an attribute
298
     *
299
     * @param string $attribute
300
     *
301
     * @return string
302
     */
303 3
    protected function getterMethodName($attribute)
304
    {
305 3
        return 'get' . Str::studly($attribute) . 'Attribute';
306
    }
307
308
    /**
309
     * Serialize Json (convert it to an array)
310
     *
311
     * @return array
312
     */
313 1
    public function jsonSerialize()
314
    {
315 1
        return $this->toArray();
316
    }
317
318
    /**
319
     * Allow the model to behave like an associate array, so see if attribute is set
320
     *
321
     * @param string $attribute
322
     *
323
     * @return boolean
324
     */
325 1
    public function offsetExists($attribute)
326
    {
327 1
        return isset($this->$attribute);
328
    }
329
330
    /**
331
     * Allow the model to behave like an associate array, so get attribute
332
     *
333
     * @param string $attribute
334
     *
335
     * @return mixed
336
     */
337 3
    public function offsetGet($attribute)
338
    {
339 3
        return $this->$attribute;
340
    }
341
342
    /**
343
     * Allow the model to behave like an associate array, so set attribute
344
     *
345
     * @param string $attribute
346
     * @param mixed  $value
347
     */
348
    public function offsetSet($attribute, $value)
349
    {
350
        $this->$attribute = $value;
351
    }
352
353
    /**
354
     * Allow the model to behave like an associate array, so unset attribute
355
     *
356
     * @param mixed $attribute
357
     *
358
     * @return void
359
     */
360
    public function offsetUnset($attribute)
361
    {
362
        unset($this->$attribute);
363
    }
364
365
    /**
366
     * Set value on an attribute
367
     *
368
     * Since there can be a setter for an attribute, look to see if there is one to delegate the setting.  Then see if
369
     * the attribute is supposed to be cast to a specific value before setting.  Finally, store the value on the model.
370
     *
371
     * @param string $attribute
372
     * @param mixed  $value
373
     *
374
     * @return $this
375
     */
376 7
    public function setAttribute($attribute, $value)
377
    {
378 7
        if ($this->hasSetter($attribute)) {
379 1
            return $this->{$this->setterMethodName($attribute)}($value);
380
        }
381
382 7
        if ($this->hasCast($attribute)) {
383 5
            $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

383
            $value = $this->castTo($value, /** @scrutinizer ignore-type */ $this->getCasts($attribute));
Loading history...
384
        }
385
386 6
        $this->attributes[$attribute] = $value;
387
388 6
        return $this;
389
    }
390
391
    /**
392
     * Build the name of the setter for an attribute
393
     *
394
     * @param string $attribute
395
     *
396
     * @return string
397
     */
398 7
    protected function setterMethodName($attribute)
399
    {
400 7
        return 'set' . Str::studly($attribute) . 'Attribute';
401
    }
402
403
    /**
404
     * Return the model as an array
405
     *
406
     * @return array
407
     */
408 1
    public function toArray()
409
    {
410
        // TODO: Need to actually roll through the attributes & make sure that nested objects are converted
411 1
        return $this->attributes;
412
    }
413
414
    /**
415
     * Return the model as JSON
416
     *
417
     * @param int $options
418
     *
419
     * @return string
420
     */
421 1
    public function toJson($options = 0)
422
    {
423 1
        return json_encode($this->jsonSerialize(), $options);
424
    }
425
}
426