Completed
Push — master ( 8bbda0...ede634 )
by ARCANEDEV
8s
created

StripeObject::checkIfAttributeDeletion()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4.679

Importance

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