Completed
Pull Request — master (#59)
by ARCANEDEV
18:05 queued 08:21
created

StripeObject::jsonSerialize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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