Completed
Pull Request — master (#22)
by ARCANEDEV
07:43
created

StripeObject::showUndefinedPropertyMsg()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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