Completed
Push — master ( 388c50...c3616e )
by ARCANEDEV
8s
created

StripeObject   D

Complexity

Total Complexity 82

Size/Duplication

Total Lines 655
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 95.05%

Importance

Changes 0
Metric Value
dl 0
loc 655
ccs 192
cts 202
cp 0.9505
rs 4.6452
c 0
b 0
f 0
wmc 82
lcom 1
cbo 5

39 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 2
A toArray() 0 6 2
A cleanObject() 0 16 4
A checkIfAttributeDeletion() 0 10 3
A init() 0 17 1
A setId() 0 8 2
A setIdIfArray() 0 9 2
A getRetrieveParams() 0 4 1
A __get() 0 11 2
A __set() 0 7 1
B setValue() 0 20 5
A getLastResponse() 0 4 1
A setLastResponse() 0 6 1
A __isset() 0 4 1
A __unset() 0 7 1
A __toString() 0 4 1
A jsonSerialize() 0 4 1
A toJson() 0 7 3
A keys() 0 4 2
A offsetSet() 0 4 1
A offsetExists() 0 4 1
A offsetUnset() 0 4 1
A offsetGet() 0 6 2
A scopedConstructFrom() 0 8 2
B refreshFrom() 0 16 5
A constructValue() 0 6 3
A lsb() 0 7 1
A scopedLsb() 0 7 1
A checkIdIsInArray() 0 5 2
A hasRetrieveParams() 0 4 1
A checkMetadataAttribute() 0 10 4
A checkPermanentAttributes() 0 5 2
A checkUnsavedAttributes() 0 9 3
A checkNotFoundAttributesException() 0 7 2
A serializeParameters() 0 9 1
A serializeUnsavedValues() 0 6 3
B serializeNestedUpdatableAttributes() 0 12 5
A showUndefinedPropertyMsg() 0 13 4
A showUndefinedPropertyMsgAttributes() 0 6 2

How to fix   Complexity   

Complex Class

Complex classes like StripeObject 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 StripeObject, and based on these observations, apply Extract Interface, too.

1
<?php namespace Arcanedev\Stripe;
2
3
use Arcanedev\Stripe\Contracts\StripeObject as StripObjectContract;
4
use Arcanedev\Stripe\Contracts\Utilities\Arrayable;
5
use Arcanedev\Stripe\Contracts\Utilities\Jsonable;
6
use Arcanedev\Stripe\Exceptions\ApiException;
7
use Arcanedev\Stripe\Exceptions\InvalidArgumentException;
8
use Arcanedev\Stripe\Http\RequestOptions;
9
use Arcanedev\Stripe\Http\Response;
10
use Arcanedev\Stripe\Utilities\Util;
11
use Arcanedev\Stripe\Utilities\UtilSet;
12
use ArrayAccess;
13
use JsonSerializable;
14
15
/**
16
 * Class     StripeObject
17
 *
18
 * @package  Arcanedev\Stripe
19
 * @author   ARCANEDEV <[email protected]>
20
 *
21
 * @property  string  id
22
 * @property  string  object
23
 */
24
class StripeObject implements StripObjectContract, ArrayAccess, JsonSerializable, Arrayable, Jsonable
25
{
26
    /* ------------------------------------------------------------------------------------------------
27
     |  Properties
28
     | ------------------------------------------------------------------------------------------------
29
     */
30
    /**
31
     * @var \Arcanedev\Stripe\Http\RequestOptions|string|array
32
     */
33
    protected $opts;
34
35
    /** @var array */
36
    protected $values;
37
38
    /**
39
     * Unsaved Values.
40
     *
41
     * @var \Arcanedev\Stripe\Utilities\UtilSet
42
     */
43
    protected $unsavedValues;
44
45
    /**
46
     * Transient (Deleted) Values.
47
     *
48
     * @var \Arcanedev\Stripe\Utilities\UtilSet
49
     */
50
    protected $transientValues;
51
52
    /**
53
     * Retrieve parameters used to query the object.
54
     *
55
     * @var array
56
     */
57
    protected $retrieveParameters;
58
59
    /**
60
     * Attributes that should not be sent to the API because
61
     * they're not updatable (e.g. API key, ID).
62
     *
63
     * @var \Arcanedev\Stripe\Utilities\UtilSet
64
     */
65
    public static $permanentAttributes;
66
67
    /**
68
     * Attributes that are nested but still updatable from the
69
     * parent class's URL (e.g. metadata).
70
     *
71
     * @var \Arcanedev\Stripe\Utilities\UtilSet
72
     */
73
    public static $nestedUpdatableAttributes;
74
75
    /**
76
     * Allow to check attributes while setting.
77
     *
78
     * @var bool
79
     */
80
    protected $checkUnsavedAttributes = false;
81
82
    /**
83
     * The last response.
84
     *
85
     * @var  \Arcanedev\Stripe\Http\Response
86
     */
87
    protected $lastResponse;
88
89
    /* ------------------------------------------------------------------------------------------------
90
     |  Constructor
91
     | ------------------------------------------------------------------------------------------------
92
     */
93
    /**
94
     * Make a Stripe object instance.
95
     *
96
     * @param  string|null        $id
97
     * @param  string|array|null  $options
98
     */
99 458
    public function __construct($id = null, $options = null)
100
    {
101 458
        $this->init();
102 458
        $this->opts = $options ? $options : new RequestOptions;
103 458
        $this->setId($id);
104 456
    }
105
106
    /**
107
     * Init the stripe object.
108
     */
109 458
    private function init()
110
    {
111 458
        $this->values                    = [];
112 458
        self::$permanentAttributes       = new UtilSet(['opts', 'id']);
113 458
        self::$nestedUpdatableAttributes = new UtilSet([
114 458
            'metadata', 'legal_entity', 'address', 'dob', 'transfer_schedule', 'verification',
115 458
            'tos_acceptance', 'personal_address',
116
            // will make the array into an AttachedObject: weird, but works for now
117 458
            'additional_owners', 0, 1, 2, 3, 4, // Max 3, but leave the 4th so errors work properly
118 458
            'inventory',
119 458
            'owner',
120 458
        ]);
121
122 458
        $this->unsavedValues             = new UtilSet;
123 458
        $this->transientValues           = new UtilSet;
124 458
        $this->retrieveParameters        = [];
125 458
    }
126
127
    /* ------------------------------------------------------------------------------------------------
128
     |  Getters & Setters (+Magics)
129
     | ------------------------------------------------------------------------------------------------
130
     */
131
    /**
132
     * Set the Id.
133
     *
134
     * @param  array|string|null  $id
135
     *
136
     * @throws \Arcanedev\Stripe\Exceptions\ApiException
137
     *
138
     * @return self
139
     */
140 458
    private function setId($id)
141
    {
142 458
        $this->setIdIfArray($id);
143
144 456
        if ( ! is_null($id)) $this->id = $id;
145
146 456
        return $this;
147
    }
148
149
    /**
150
     * Set the Id from Array.
151
     *
152
     * @param  array|string|null  $id
153
     *
154
     * @throws \Arcanedev\Stripe\Exceptions\ApiException
155
     */
156 458
    private function setIdIfArray(&$id)
157
    {
158 458
        if ( ! is_array($id)) return;
159
160 8
        $this->checkIdIsInArray($id);
161 6
        $this->retrieveParameters = array_diff_key($id, array_flip(['id']));
162
163 6
        $id = $id['id'];
164 6
    }
165
166
    /**
167
     * Get Retrieve Parameters.
168
     *
169
     * @return array
170
     */
171 2
    protected function getRetrieveParams()
172
    {
173 2
        return $this->retrieveParameters;
174
    }
175
176
    /**
177
     * Standard get accessor.
178
     *
179
     * @param  string|int  $key
180
     *
181
     * @return mixed|null
182
     */
183 334
    public function &__get($key)
184
    {
185 334
        $nullVal = null;
186
187 334
        if (in_array($key, $this->keys()))
188 334
            return $this->values[$key];
189
190 6
        $this->showUndefinedPropertyMsg(get_class($this), $key);
191
192 6
        return $nullVal;
193
    }
194
195
    /**
196
     * Standard set accessor.
197
     *
198
     * @param  string  $key
199
     * @param  mixed   $value
200
     */
201 396
    public function __set($key, $value)
202
    {
203 396
        $supportedAttributes = $this->keys();
204
205 396
        $this->setValue($key, $value);
206 396
        $this->checkUnsavedAttributes($supportedAttributes);
207 396
    }
208
209
    /**
210
     * Set value.
211
     *
212
     * @param  string  $key
213
     * @param  mixed   $value
214
     *
215
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
216
     */
217 396
    private function setValue($key, $value)
218
    {
219 396
        $this->checkIfAttributeDeletion($key, $value);
220 396
        $this->checkMetadataAttribute($key, $value);
221
222
        if (
223 396
            self::$nestedUpdatableAttributes->includes($key) &&
224 396
            isset($this->$key) &&
225 396
            $this->$key instanceof AttachedObject &&
226 12
            is_array($value)
227 396
        ) {
228 10
            $this->$key->replaceWith($value);
229 10
        }
230
        else {
231
            // TODO: may want to clear from $transientValues (Won't be user-visible).
232 396
            $this->values[$key] = $value;
233
        }
234
235 396
        $this->checkPermanentAttributes($key);
236 396
    }
237
238
    /**
239
     * Get the last response from the Stripe API.
240
     *
241
     * @return \Arcanedev\Stripe\Http\Response
242
     */
243 6
    public function getLastResponse()
244
    {
245 6
        return $this->lastResponse;
246
    }
247
248
    /**
249
     * Set the last response from the Stripe API.
250
     *
251
     * @param  \Arcanedev\Stripe\Http\Response  $response
252
     *
253
     * @return self
254
     */
255 336
    public function setLastResponse(Response $response)
256
    {
257 336
        $this->lastResponse = $response;
258
259 336
        return $this;
260
    }
261
262
    /**
263
     * Check has a value by key.
264
     *
265
     * @param  string  $key
266
     *
267
     * @return bool
268
     */
269 96
    public function __isset($key)
270
    {
271 96
        return isset($this->values[$key]);
272
    }
273
274
    /**
275
     * Unset element from values.
276
     *
277
     * @param  string  $key
278
     */
279 36
    public function __unset($key)
280
    {
281 36
        unset($this->values[$key]);
282
283 36
        $this->transientValues->add($key);
284 36
        $this->unsavedValues->discard($key);
285 36
    }
286
287
    /**
288
     * Convert StripeObject to string.
289
     *
290
     * @return string
291
     */
292 2
    public function __toString()
293
    {
294 2
        return get_class($this).' JSON: '.$this->toJson();
295
    }
296
297
    /**
298
     * Json serialize.
299
     *
300
     * @return array
301
     */
302 2
    public function jsonSerialize()
303
    {
304 2
        return $this->toArray(true);
305
    }
306
307
    /**
308
     * Convert StripeObject to array.
309
     *
310
     * @param  bool  $recursive
311
     *
312
     * @return array
313
     */
314 10
    public function toArray($recursive = false)
315
    {
316
        return $recursive
317 10
            ? Util::convertStripeObjectToArray($this->values)
318 10
            : $this->values;
319
    }
320
321
    /**
322
     * Convert StripeObject to JSON.
323
     *
324
     * @param  int  $options
325
     *
326
     * @return string
327
     */
328 2
    public function toJson($options = 0)
329
    {
330 2
        if ($options === 0 && defined('JSON_PRETTY_PRINT'))
331 2
            $options = JSON_PRETTY_PRINT;
332
333 2
        return json_encode($this->toArray(true), $options);
334
    }
335
336
    /**
337
     * Get only value keys.
338
     *
339
     * @return array
340
     */
341 406
    public function keys()
342
    {
343 406
        return empty($this->values) ? [] : array_keys($this->values);
344
    }
345
346
    /* ------------------------------------------------------------------------------------------------
347
     |  ArrayAccess methods
348
     | ------------------------------------------------------------------------------------------------
349
     */
350 32
    public function offsetSet($key, $value)
351
    {
352 32
        $this->$key = $value;
353 32
    }
354
355 332
    public function offsetExists($key)
356
    {
357 332
        return array_key_exists($key, $this->values);
358
    }
359
360 2
    public function offsetUnset($key)
361
    {
362 2
        unset($this->$key);
363 2
    }
364
365 254
    public function offsetGet($key)
366
    {
367 254
        return array_key_exists($key, $this->values)
368 254
            ? $this->values[$key]
369 254
            : null;
370
    }
371
372
    /* ------------------------------------------------------------------------------------------------
373
     |  Main Functions
374
     | ------------------------------------------------------------------------------------------------
375
     */
376
    /**
377
     * This unfortunately needs to be public to be used in Util.php
378
     * Return The object constructed from the given values.
379
     *
380
     * @param  string                                                   $class
381
     * @param  array                                                    $values
382
     * @param  \Arcanedev\Stripe\Http\RequestOptions|array|string|null  $options
383
     *
384
     * @return self
385
     */
386 328
    public static function scopedConstructFrom($class, $values, $options)
387
    {
388
        /** @var self $obj */
389 328
        $obj = new $class(isset($values['id']) ? $values['id'] : null);
390 328
        $obj->refreshFrom($values, $options);
391
392 328
        return $obj;
393
    }
394
395
    /**
396
     * Refreshes this object using the provided values.
397
     *
398
     * @param  array                                                    $values
399
     * @param  \Arcanedev\Stripe\Http\RequestOptions|array|string|null  $options
400
     * @param  bool                                                     $partial
401
     */
402 338
    public function refreshFrom($values, $options, $partial = false)
403
    {
404 338
        $this->opts = is_array($options) ? RequestOptions::parse($options) : $options;
405
406 338
        $this->cleanObject($values, $partial);
407
408 338
        foreach ($values as $key => $value) {
409 338
            if (self::$permanentAttributes->includes($key) && isset($this[$key]))
410 338
                continue;
411
412 338
            $this->values[$key] = $this->constructValue($key, $value, $this->opts);
413
414 338
            $this->transientValues->discard($key);
415 338
            $this->unsavedValues->discard($key);
416 338
        }
417 338
    }
418
419
    /**
420
     * Clean refreshed StripeObject.
421
     *
422
     * @param  array       $values
423
     * @param  bool|false  $partial
424
     */
425 338
    private function cleanObject($values, $partial)
426
    {
427
        // Wipe old state before setting new.
428
        // This is useful for e.g. updating a customer, where there is no persistent card parameter.
429
        // Mark those values which don't persist as transient
430
        $removed = ! $partial
431 338
            ? array_diff($this->keys(), array_keys($values))
432 338
            : new UtilSet;
433
434 338
        foreach ($removed as $key) {
435 28
            if (self::$permanentAttributes->includes($key))
436 28
                continue;
437
438 28
            unset($this->$key);
439 338
        }
440 338
    }
441
442
    /**
443
     * Construct Value.
444
     *
445
     * @param  string                                                   $key
446
     * @param  mixed                                                    $value
447
     * @param  \Arcanedev\Stripe\Http\RequestOptions|array|string|null  $options
448
     *
449
     * @return self|\Arcanedev\Stripe\StripeResource|\Arcanedev\Stripe\Collection|array
450
     */
451 338
    private function constructValue($key, $value, $options)
452
    {
453 338
        return (self::$nestedUpdatableAttributes->includes($key) && is_array($value))
454 338
            ? self::scopedConstructFrom(AttachedObject::class, $value, $options)
455 338
            : Util::convertToStripeObject($value, $options);
456
    }
457
458
    /**
459
     * Pretend to have late static bindings.
460
     *
461
     * @param  string  $method
462
     *
463
     * @return mixed
464
     */
465
    protected function lsb($method)
466
    {
467
        return call_user_func_array(
468
            [get_class($this), $method],
469
            array_slice(func_get_args(), 1)
470
        );
471
    }
472
473
    /**
474
     * Scoped Late Static Bindings.
475
     *
476
     * @param  string  $class
477
     * @param  string  $method
478
     *
479
     * @return mixed
480
     */
481
    protected static function scopedLsb($class, $method)
482
    {
483
        return call_user_func_array(
484
            [$class, $method],
485
            array_slice(func_get_args(), 2)
486
        );
487
    }
488
489
    /* ------------------------------------------------------------------------------------------------
490
     |  Check Functions
491
     | ------------------------------------------------------------------------------------------------
492
     */
493
    /**
494
     * Check if array has id.
495
     *
496
     * @param  array  $array
497
     *
498
     * @throws \Arcanedev\Stripe\Exceptions\ApiException
499
     */
500 8
    private function checkIdIsInArray($array)
501
    {
502 8
        if ( ! array_key_exists('id', $array))
503 8
            throw new ApiException('The attribute id must be included.');
504 6
    }
505
506
    /**
507
     * Check if object has retrieve parameters.
508
     *
509
     * @return bool
510
     */
511 2
    public function hasRetrieveParams()
512
    {
513 2
        return (bool) count($this->getRetrieveParams());
514
    }
515
516
    /**
517
     * Check if attribute deletion.
518
     *
519
     * @param  string      $key
520
     * @param  mixed|null  $value
521
     *
522
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
523
     */
524 396
    private function checkIfAttributeDeletion($key, $value)
525
    {
526
        // Don't use empty($value) instead of ($value === '')
527 396
        if ( ! is_null($value) && $value === '')
528 396
            throw new InvalidArgumentException(
529 2
                "You cannot set '{$key}' to an empty string. "
530
                . 'We interpret empty strings as \'null\' in requests. '
531 2
                . "You may set obj->{$key} = null to delete the property"
532 2
            );
533 396
    }
534
535
    /**
536
     * Check metadata attribute.
537
     *
538
     * @param  string      $key
539
     * @param  mixed|null  $value
540
     *
541
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
542
     */
543 396
    private function checkMetadataAttribute($key, $value)
544
    {
545
        if (
546 396
            $key === 'metadata' &&
547 18
            ( ! is_array($value) && ! is_null($value))
548 18
        )
549 396
            throw new InvalidArgumentException(
550 2
                'The metadata value must be an array or null, '.gettype($value).' is given'
551 2
            );
552 396
    }
553
554
    /**
555
     * Check permanent attributes.
556
     *
557
     * @param  string  $key
558
     */
559 396
    private function checkPermanentAttributes($key)
560
    {
561 396
        if ( ! self::$permanentAttributes->includes($key))
562 396
            $this->unsavedValues->add($key);
563 396
    }
564
565
    /**
566
     * Check unsaved attributes.
567
     *
568
     * @param  array  $supported
569
     *
570
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
571
     */
572 396
    private function checkUnsavedAttributes($supported)
573
    {
574 396
        if ($this->checkUnsavedAttributes === false || count($supported) === 0)
575 396
            return;
576
577 16
        $this->checkNotFoundAttributesException(
578 16
            $this->unsavedValues->diffKeys($supported)
579 16
        );
580 12
    }
581
582
    /**
583
     * Check not found attributes exception.
584
     *
585
     * @param  array  $notFound
586
     *
587
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
588
     */
589 16
    private function checkNotFoundAttributesException($notFound)
590
    {
591 16
        if (count($notFound))
592 16
            throw new InvalidArgumentException(
593 4
                'The attributes ['.implode(', ', $notFound).'] are not supported.'
594 4
            );
595 12
    }
596
597
    /* ------------------------------------------------------------------------------------------------
598
     |  Other Functions
599
     | ------------------------------------------------------------------------------------------------
600
     */
601
    /**
602
     * A recursive mapping of attributes to values for this object,
603
     * including the proper value for deleted attributes.
604
     *
605
     * @return array
606
     */
607 80
    protected function serializeParameters()
608
    {
609 80
        $params = [];
610
611 80
        $this->serializeUnsavedValues($params);
612 80
        $this->serializeNestedUpdatableAttributes($params);
613
614 80
        return $params;
615
    }
616
617
    /**
618
     * Serialize unsaved values.
619
     *
620
     * @param  array  $params
621
     */
622 80
    private function serializeUnsavedValues(&$params)
623
    {
624 80
        foreach ($this->unsavedValues->toArray() as $key) {
625 70
            $params[$key] = ! is_null($value = $this->$key) ? $value : '';
626 80
        }
627 80
    }
628
629
    /**
630
     * Serialize nested updatable attributes.
631
     *
632
     * @param  array  $params
633
     */
634 80
    private function serializeNestedUpdatableAttributes(&$params)
635
    {
636 80
        foreach (self::$nestedUpdatableAttributes->toArray() as $property) {
637
            if (
638 80
                isset($this->$property) &&
639 80
                $this->$property instanceof self &&
640 74
                $serialized = $this->$property->serializeParameters()
641 80
            ) {
642 40
                $params[$property] = $serialized;
643 40
            }
644 80
        }
645 80
    }
646
647
    /**
648
     * Show undefined property warning message.
649
     *
650
     * @param  string  $class
651
     * @param  string  $key
652
     */
653 6
    private function showUndefinedPropertyMsg($class, $key)
654
    {
655 6
        $message = "Stripe Notice: Undefined property of {$class} instance: {$key}.";
656
657 6
        if ( ! $this->transientValues->isEmpty() && $this->transientValues->includes($key)) {
658 4
            $message .= " HINT: The [{$key}] attribute was set in the past, however. " .
659 4
                'It was then wiped when refreshing the object with the result returned by Stripe\'s API, ' .
660 4
                'probably as a result of a save().'.$this->showUndefinedPropertyMsgAttributes();
661 4
        }
662
663 6
        if ( ! is_testing())
664 6
            error_log($message);
665 6
    }
666
667
    /**
668
     * Show available attributes for undefined property warning message.
669
     *
670
     * @return string
671
     */
672 4
    private function showUndefinedPropertyMsgAttributes()
673
    {
674 4
        return count($attributes = $this->keys())
675 4
            ? ' The attributes currently available on this object are: '.implode(', ', $attributes)
676 4
            : '';
677
    }
678
}
679