Passed
Pull Request — develop (#33)
by Jimmy
04:28
created

Model::toArray()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

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

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