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
|
|||||
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
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
![]() |
|||||
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 |
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.