Completed
Pull Request — master (#60)
by ARCANEDEV
08:31
created

StripeObject::showUndefinedPropertyMsg()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 2
dl 0
loc 13
ccs 8
cts 9
cp 0.8889
crap 4.0218
rs 9.2
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 448
    public function __construct($id = null, $options = null)
102
    {
103 448
        $this->init();
104 448
        $this->opts = $options ? $options : new RequestOptions;
105 448
        $this->setId($id);
106 446
    }
107
108
    /**
109
     * Init the stripe object.
110
     */
111 448
    private function init()
112
    {
113 448
        $this->values                    = [];
114 448
        self::$permanentAttributes       = new UtilSet(['opts', 'id']);
115 448
        self::$nestedUpdatableAttributes = new UtilSet([
116 448
            'metadata', 'legal_entity', 'address', 'dob', 'payout_schedule', 'transfer_schedule',
117
            'verification', 'tos_acceptance', 'personal_address', 'address_kana', 'address_kanji',
118
            // will make the array into an AttachedObject: weird, but works for now
119
            'additional_owners', 0, 1, 2, 3, 4, // Max 3, but leave the 4th so errors work properly
120
            'shipping', 'inventory',
121
            'owner',
122
        ]);
123
124 448
        $this->unsavedValues             = new UtilSet;
125 448
        $this->transientValues           = new UtilSet;
126 448
        $this->retrieveParameters        = [];
127 448
    }
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 448
    private function setId($id)
144
    {
145 448
        $this->setIdIfArray($id);
146
147 446
        if ( ! is_null($id)) $this->id = $id;
148
149 446
        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 448
    private function setIdIfArray(&$id)
160
    {
161 448
        if ( ! is_array($id)) return;
162
163 8
        $this->checkIdIsInArray($id);
164 6
        $this->retrieveParameters = array_diff_key($id, array_flip(['id']));
165
166 6
        $id = $id['id'];
167 6
    }
168
169
    /**
170
     * Get Retrieve Parameters.
171
     *
172
     * @return array
173
     */
174 2
    protected function getRetrieveParams()
175
    {
176 2
        return $this->retrieveParameters;
177
    }
178
179
    /**
180
     * Standard get accessor.
181
     *
182
     * @param  string|int  $key
183
     *
184
     * @return mixed|null
185
     */
186 338
    public function &__get($key)
187
    {
188 338
        $nullVal = null;
189
190 338
        if (in_array($key, $this->keys()))
191 334
            return $this->values[$key];
192
193 6
        $this->showUndefinedPropertyMsg(get_class($this), $key);
194
195 6
        return $nullVal;
196
    }
197
198
    /**
199
     * Standard set accessor.
200
     *
201
     * @param  string  $key
202
     * @param  mixed   $value
203
     */
204 396
    public function __set($key, $value)
205
    {
206 396
        $supportedAttributes = $this->keys();
207
208 396
        $this->setValue($key, $value);
209 396
        $this->checkUnsavedAttributes($supportedAttributes);
210 396
    }
211
212
    /**
213
     * Set value.
214
     *
215
     * @param  string  $key
216
     * @param  mixed   $value
217
     *
218
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
219
     */
220 396
    private function setValue($key, $value)
221
    {
222 396
        $this->checkIfAttributeDeletion($key, $value);
223 396
        $this->checkMetadataAttribute($key, $value);
224
225
        if (
226 396
            self::$nestedUpdatableAttributes->includes($key) &&
227 396
            isset($this->$key) &&
228 396
            $this->$key instanceof AttachedObject &&
229 396
            is_array($value)
230
        ) {
231 10
            $this->$key->replaceWith($value);
232
        }
233
        else {
234
            // TODO: may want to clear from $transientValues (Won't be user-visible).
235 396
            $this->values[$key] = $value;
236
        }
237
238 396
        $this->checkPermanentAttributes($key);
239 396
    }
240
241
    /**
242
     * Get the last response from the Stripe API.
243
     *
244
     * @return \Arcanedev\Stripe\Http\Response
245
     */
246 6
    public function getLastResponse()
247
    {
248 6
        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 338
    public function setLastResponse(Response $response)
259
    {
260 338
        $this->lastResponse = $response;
261
262 338
        return $this;
263
    }
264
265
    /**
266
     * Check has a value by key.
267
     *
268
     * @param  string  $key
269
     *
270
     * @return bool
271
     */
272 104
    public function __isset($key)
273
    {
274 104
        return isset($this->values[$key]);
275
    }
276
277
    /**
278
     * Unset element from values.
279
     *
280
     * @param  string  $key
281
     */
282 38
    public function __unset($key)
283
    {
284 38
        unset($this->values[$key]);
285
286 38
        $this->transientValues->add($key);
287 38
        $this->unsavedValues->discard($key);
288 38
    }
289
290
    /**
291
     * Convert StripeObject to string.
292
     *
293
     * @return string
294
     */
295 2
    public function __toString()
296
    {
297 2
        return get_class($this).' JSON: '.$this->toJson();
298
    }
299
300
    /**
301
     * Json serialize.
302
     *
303
     * @return array
304
     */
305 2
    public function jsonSerialize()
306
    {
307 2
        return $this->toArray(true);
308
    }
309
310
    /**
311
     * Convert StripeObject to array.
312
     *
313
     * @param  bool  $recursive
314
     *
315
     * @return array
316
     */
317 10
    public function toArray($recursive = false)
318
    {
319 10
        return $recursive
320 8
            ? Util::convertStripeObjectToArray($this->values)
321 10
            : $this->values;
322
    }
323
324
    /**
325
     * Convert StripeObject to JSON.
326
     *
327
     * @param  int  $options
328
     *
329
     * @return string
330
     */
331 2
    public function toJson($options = 0)
332
    {
333 2
        if ($options === 0 && defined('JSON_PRETTY_PRINT'))
334 2
            $options = JSON_PRETTY_PRINT;
335
336 2
        return json_encode($this->toArray(true), $options);
337
    }
338
339
    /**
340
     * Get only value keys.
341
     *
342
     * @return array
343
     */
344 406
    public function keys()
345
    {
346 406
        return empty($this->values) ? [] : array_keys($this->values);
347
    }
348
349
    /* -----------------------------------------------------------------
350
     |  ArrayAccess methods
351
     | -----------------------------------------------------------------
352
     */
353
354 34
    public function offsetSet($key, $value)
355
    {
356 34
        $this->$key = $value;
357 34
    }
358
359 338
    public function offsetExists($key)
360
    {
361 338
        return array_key_exists($key, $this->values);
362
    }
363
364 2
    public function offsetUnset($key)
365
    {
366 2
        unset($this->$key);
367 2
    }
368
369 262
    public function offsetGet($key)
370
    {
371 262
        return array_key_exists($key, $this->values)
372 250
            ? $this->values[$key]
373 262
            : 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 332
    public static function scopedConstructFrom($class, $values, $options)
392
    {
393
        /** @var self $obj */
394 332
        $obj = new $class(isset($values['id']) ? $values['id'] : null);
395 332
        $obj->refreshFrom($values, $options);
396
397 332
        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 344
    public function refreshFrom($values, $options, $partial = false)
408
    {
409 344
        $this->opts = is_array($options) ? RequestOptions::parse($options) : $options;
410
411 344
        $this->cleanObject($values, $partial);
412
413 344
        foreach ($values as $key => $value) {
414 344
            if (self::$permanentAttributes->includes($key) && isset($this[$key]))
415 334
                continue;
416
417 344
            $this->values[$key] = $this->constructValue($key, $value, $this->opts);
418
419 344
            $this->transientValues->discard($key);
420 344
            $this->unsavedValues->discard($key);
421
        }
422 344
    }
423
424
    /**
425
     * Clean refreshed StripeObject.
426
     *
427
     * @param  array       $values
428
     * @param  bool|false  $partial
429
     */
430 344
    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 344
            ? array_diff($this->keys(), array_keys($values))
437 344
            : new UtilSet;
438
439 344
        foreach ($removed as $key) {
440 30
            if (self::$permanentAttributes->includes($key))
441 2
                continue;
442
443 30
            unset($this->$key);
444
        }
445 344
    }
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 344
    private function constructValue($key, $value, $options)
457
    {
458 344
        return (self::$nestedUpdatableAttributes->includes($key) && is_array($value))
459 284
            ? self::scopedConstructFrom(AttachedObject::class, $value, $options)
460 344
            : 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 8
    private function checkIdIsInArray($array)
507
    {
508 8
        if ( ! array_key_exists('id', $array))
509 2
            throw new ApiException('The attribute id must be included.');
510 6
    }
511
512
    /**
513
     * Check if object has retrieve parameters.
514
     *
515
     * @return bool
516
     */
517 2
    public function hasRetrieveParams()
518
    {
519 2
        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 396
    private function checkIfAttributeDeletion($key, $value)
531
    {
532
        // Don't use empty($value) instead of ($value === '')
533 396
        if ( ! is_null($value) && $value === '')
534 2
            throw new InvalidArgumentException(
535 2
                "You cannot set '{$key}' to an empty string. "
536 2
                . 'We interpret empty strings as \'null\' in requests. '
537 2
                . "You may set obj->{$key} = null to delete the property"
538
            );
539 396
    }
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 396
    private function checkMetadataAttribute($key, $value)
550
    {
551
        if (
552 396
            $key === 'metadata' &&
553 396
            ( ! is_array($value) && ! is_null($value))
554
        )
555 2
            throw new InvalidArgumentException(
556 2
                'The metadata value must be an array or null, '.gettype($value).' is given'
557
            );
558 396
    }
559
560
    /**
561
     * Check permanent attributes.
562
     *
563
     * @param  string  $key
564
     */
565 396
    private function checkPermanentAttributes($key)
566
    {
567 396
        if ( ! self::$permanentAttributes->includes($key))
568 98
            $this->unsavedValues->add($key);
569 396
    }
570
571
    /**
572
     * Check unsaved attributes.
573
     *
574
     * @param  array  $supported
575
     *
576
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
577
     */
578 396
    private function checkUnsavedAttributes($supported)
579
    {
580 396
        if ($this->checkUnsavedAttributes === false || count($supported) === 0)
581 396
            return;
582
583 16
        $this->checkNotFoundAttributesException(
584 16
            $this->unsavedValues->diffKeys($supported)
585
        );
586 12
    }
587
588
    /**
589
     * Check not found attributes exception.
590
     *
591
     * @param  array  $notFound
592
     *
593
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
594
     */
595 16
    private function checkNotFoundAttributesException($notFound)
596
    {
597 16
        if (count($notFound))
598 4
            throw new InvalidArgumentException(
599 4
                'The attributes ['.implode(', ', $notFound).'] are not supported.'
600
            );
601 12
    }
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 88
    protected function serializeParameters()
615
    {
616 88
        $params = [];
617
618 88
        $this->serializeUnsavedValues($params);
619 88
        $this->serializeNestedUpdatableAttributes($params);
620
621 88
        return $params;
622
    }
623
624
    /**
625
     * Serialize unsaved values.
626
     *
627
     * @param  array  $params
628
     */
629 88
    private function serializeUnsavedValues(&$params)
630
    {
631 88
        foreach ($this->unsavedValues->toArray() as $key) {
632 74
            $params[$key] = ! is_null($value = $this->$key) ? $value : '';
633
        }
634 88
    }
635
636
    /**
637
     * Serialize nested updatable attributes.
638
     *
639
     * @param  array  $params
640
     */
641 88
    private function serializeNestedUpdatableAttributes(&$params)
642
    {
643 88
        foreach (self::$nestedUpdatableAttributes->toArray() as $property) {
644
            if (
645 88
                isset($this->$property) &&
646 88
                $this->$property instanceof self &&
647 88
                $serialized = $this->$property->serializeParameters()
648
            ) {
649 88
                $params[$property] = $serialized;
650
            }
651
        }
652 88
    }
653
654
    /**
655
     * Show undefined property warning message.
656
     *
657
     * @param  string  $class
658
     * @param  string  $key
659
     */
660 6
    private function showUndefinedPropertyMsg($class, $key)
661
    {
662 6
        $message = "Stripe Notice: Undefined property of {$class} instance: {$key}.";
663
664 6
        if ( ! $this->transientValues->isEmpty() && $this->transientValues->includes($key)) {
665 4
            $message .= " HINT: The [{$key}] attribute was set in the past, however. " .
666 4
                'It was then wiped when refreshing the object with the result returned by Stripe\'s API, ' .
667 4
                'probably as a result of a save().'.$this->showUndefinedPropertyMsgAttributes();
668
        }
669
670 6
        if ( ! is_testing())
671
            Stripe::getLogger()->error($message);
672 6
    }
673
674
    /**
675
     * Show available attributes for undefined property warning message.
676
     *
677
     * @return string
678
     */
679 4
    private function showUndefinedPropertyMsgAttributes()
680
    {
681 4
        return count($attributes = $this->keys())
682
            ? ' The attributes currently available on this object are: '.implode(', ', $attributes)
683 4
            : '';
684
    }
685
}
686