Completed
Push — master ( 59f5ff...a760dd )
by ARCANEDEV
06:12
created

StripeObject::showUndefinedPropertyMsg()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0417

Importance

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