Completed
Push — develop ( b198f5...835e6f )
by Jimmy
18s queued 10s
created

Model::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 1
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(
90
                        $method,
91
                        $this->client->get(Str::replaceFirst($this->client->getUrl(), '', $v))
92
                    );
93
94
                    return $this->{$method};
95
                }
96
            }
97
        }
98
99
        trigger_error('Call to undefined method ' . __CLASS__ . '::' . $method . '()', E_USER_ERROR);
100
    }
101
102
    /**
103
     * Only return the attributes for a var_dump
104
     *
105
     * This object proxies the properties to the keys in the attributes array, so only
106
     * expose it when doing a var_dump as the other properties are not needed in debugging.
107
     *
108
     * @return array
109
     */
110 1
    public function __debugInfo()
111
    {
112 1
        return $this->attributes;
113
    }
114
115
    /**
116
     * Allow the attributes of the model to be accessed like a public property
117
     *
118
     * @param string $attribute
119
     *
120
     * @return mixed
121
     */
122 4
    public function __get($attribute)
123
    {
124 4
        return $this->getAttribute($attribute);
125
    }
126
127
    /**
128
     * Allow checking to see if the model has an attribute set
129
     *
130
     * @param string $attribute
131
     *
132
     * @return bool
133
     */
134 4
    public function __isset($attribute)
135
    {
136 4
        return array_key_exists($attribute, $this->attributes);
137
    }
138
139
    /**
140
     * Set a property on the model in the attributes
141
     *
142
     * @param string $attribute
143
     * @param mixed $value
144
     */
145 1
    public function __set($attribute, $value)
146
    {
147 1
        $this->setAttribute($attribute, $value);
148
    }
149
150
    /**
151
     * Convert the model to its string representation.
152
     *
153
     * @return string
154
     */
155 1
    public function __toString()
156
    {
157 1
        return $this->toJson();
158
    }
159
160
    /**
161
     * Unset a property on the model in the attributes
162
     *
163
     * @param string $attribute
164
     */
165 1
    public function __unset($attribute)
166
    {
167 1
        unset($this->attributes[$attribute]);
168
    }
169
170
    /**
171
     * Cast a item to a specific object or type
172
     *
173
     * @param mixed $value
174
     * @param string $cast
175
     *
176
     * @return mixed
177
     */
178 13
    public function castTo($value, $cast)
179
    {
180 13
        if (is_null($value) || is_object($value)) {
181 11
            return $value;
182
        }
183
184 13
        if (Carbon::class === $cast) {
185 11
            return Carbon::parse($value);
186
        }
187
188 13
        if (class_exists($cast)) {
189 11
            return new $cast((array)$value);
190
        }
191
192 13
        if (strcasecmp('json', $cast) == 0) {
193 11
            return json_encode((array)$value);
194
        }
195
196 13
        if (strcasecmp('collection', $cast) == 0) {
197 11
            return new Collection((array)$value);
198
        }
199
200 13
        if (in_array($cast, ['bool', 'boolean'])) {
201 13
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
202
        }
203
204
        $cast_types = [
205 13
            'array',
206
            'bool',
207
            'boolean',
208
            'double',
209
            'float',
210
            'int',
211
            'integer',
212
            'null',
213
            'object',
214
            'string',
215
        ];
216
217 13
        if (!in_array($cast, $cast_types)) {
218 1
            throw new InvalidArgumentException(sprintf("Attributes cannot be casted to [%s] type.", $cast));
219
        }
220
221 13
        settype($value, $cast);
222
223
        // settype returns true/false for pass/fail, not the value
224 13
        return $value;
225
    }
226
227
    /**
228
     * Count the number of properties.
229
     *
230
     * @return int
231
     */
232 1
    public function count()
233
    {
234 1
        return count($this->attributes);
235
    }
236
237
    /**
238
     * Store the collection of attributes on the model
239
     *
240
     * @param array $attributes
241
     *
242
     * @return $this
243
     */
244 13
    public function fill(array $attributes)
245
    {
246 13
        foreach ($attributes as $attribute => $value) {
247 13
            $this->setAttribute($attribute, $value);
248
        }
249
250 13
        return $this;
251
    }
252
253
    /**
254
     * Check to see if there is a getter for the attribute
255
     *
256
     * @param string $attribute
257
     *
258
     * @return bool
259
     */
260 4
    public function hasGetter($attribute)
261
    {
262 4
        return method_exists($this, $this->getterMethodName($attribute));
263
    }
264
265
    /**
266
     * Check to see if there is a setter for the attribute
267
     *
268
     * @param string $attribute
269
     *
270
     * @return bool
271
     */
272 13
    public function hasSetter($attribute)
273
    {
274 13
        return method_exists($this, $this->setterMethodName($attribute));
275
    }
276
277
    /**
278
     * Is the attribute supposed to be cast
279
     *
280
     * @param string $attribute
281
     *
282
     * @return bool
283
     */
284 13
    public function hasCast($attribute)
285
    {
286 13
        $cast = $this->getCasts($attribute);
287
288 13
        return !empty($cast) && is_string($cast);
289
    }
290
291
    /**
292
     * Get the attribute from the model
293
     *
294
     * @param string $attribute
295
     *
296
     * @return mixed
297
     */
298 4
    public function getAttribute($attribute)
299
    {
300
        // Guard against no attribute
301 4
        if (!$attribute) {
302
            return;
303
        }
304
305
        // Use getter on model if there is one
306 4
        if ($this->hasGetter($attribute)) {
307 1
            return $this->{$this->getterMethodName($attribute)}();
308
        }
309
310
        // Allow for making related calls for "extra" properties in the "_info" property.
311
        // Cache the results so only 1 call is made
312 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...
313
            $this->setAttribute(
314
                $attribute,
315
                $this->client->get(
316
                    Str::replaceFirst($this->client->getUrl(), '', $this->_info->{$attribute . '_href'})
0 ignored issues
show
Bug introduced by
The method getUrl() 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

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

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