Completed
Pull Request — master (#18)
by ARCANEDEV
07:31
created

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