Completed
Pull Request — master (#9)
by ARCANEDEV
02:34
created

StripeObject::scopedConstructFrom()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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