Completed
Push — master ( 8c20b1...6dda53 )
by ARCANEDEV
13s
created

StripeObject::init()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 31
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

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