Completed
Pull Request — master (#15)
by ARCANEDEV
11:13
created

StripeObject::offsetExists()   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 1
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 940
    public function __construct($id = null, $options = null)
106
    {
107 940
        $this->values                    = [];
108 940
        self::$permanentAttributes       = new UtilSet(['opts', 'id']);
109 940
        self::$nestedUpdatableAttributes = new UtilSet([
110 940
            'metadata', 'legal_entity', 'address', 'dob', 'transfer_schedule', 'verification',
111 752
            'tos_acceptance', 'personal_address', 'evidence',
112
            // will make the array into an AttachedObject: weird, but works for now
113 752
            'additional_owners', 0, 1, 2, 3, 4 // Max 3, but leave the 4th so errors work properly
114 752
        ]);
115
116 940
        $this->unsavedValues             = new UtilSet;
117 940
        $this->transientValues           = new UtilSet;
118 940
        $this->retrieveParameters        = [];
119 940
        $this->opts                      = $options ? $options : new RequestOptions;
120 940
        $this->setId($id);
121 935
    }
122
123
    /* ------------------------------------------------------------------------------------------------
124
     |  Getters & Setters (+Magics)
125
     | ------------------------------------------------------------------------------------------------
126
     */
127
    /**
128
     * Set the Id.
129
     *
130
     * @param  array|string|null  $id
131
     *
132
     * @throws \Arcanedev\Stripe\Exceptions\ApiException
133
     *
134
     * @return self
135
     */
136 940
    private function setId($id)
137
    {
138 940
        $this->setIdIfArray($id);
139
140 935
        if ( ! is_null($id)) $this->id = $id;
141
142 935
        return $this;
143
    }
144
145
    /**
146
     * Set the Id from Array.
147
     *
148
     * @param  array|string|null  $id
149
     *
150
     * @throws \Arcanedev\Stripe\Exceptions\ApiException
151
     */
152 940
    private function setIdIfArray(&$id)
153
    {
154 940
        if ( ! is_array($id)) return;
155
156 15
        $this->checkIdIsInArray($id);
157 10
        $this->retrieveParameters = array_diff_key($id, array_flip(['id']));
158
159 10
        $id = $id['id'];
160 10
    }
161
162
    /**
163
     * Get Retrieve Parameters.
164
     *
165
     * @return array
166
     */
167 5
    protected function getRetrieveParams()
168
    {
169 5
        return $this->retrieveParameters;
170
    }
171
172
    /**
173
     * Standard get accessor.
174
     *
175
     * @param  string|int  $key
176
     *
177
     * @return mixed|null
178
     */
179 654
    public function &__get($key)
180
    {
181 654
        $nullVal = null;
182
183 654
        if (in_array($key, $this->keys())) {
184 644
            return $this->values[$key];
185
        }
186
187 15
        $this->showUndefinedPropertyMsg(get_class($this), $key);
188
189 15
        return $nullVal;
190
    }
191
192
    /**
193
     * Standard set accessor.
194
     *
195
     * @param  string  $key
196
     * @param  mixed   $value
197
     */
198 789
    public function __set($key, $value)
199
    {
200 789
        $supportedAttributes = $this->keys();
201
202 789
        $this->setValue($key, $value);
203 789
        $this->checkUnsavedAttributes($supportedAttributes);
204 789
    }
205
206
    /**
207
     * Set value.
208
     *
209
     * @param  string  $key
210
     * @param  mixed   $value
211
     *
212
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
213
     */
214 789
    private function setValue($key, $value)
215
    {
216 789
        $this->checkIfAttributeDeletion($key, $value);
217 789
        $this->checkMetadataAttribute($key, $value);
218
219
        if (
220 789
            self::$nestedUpdatableAttributes->includes($key) &&
221 789
            isset($this->$key) &&
222 789
            $this->$key instanceof AttachedObject &&
223 175
            is_array($value)
224 631
        ) {
225 17
            $this->$key->replaceWith($value);
226 13
        }
227
        else {
228
            // TODO: may want to clear from $transientValues (Won't be user-visible).
229 789
            $this->values[$key] = $value;
230
        }
231
232 789
        $this->checkPermanentAttributes($key);
233 789
    }
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 654
    public function setLastResponse(Response $response)
253
    {
254 654
        $this->lastResponse = $response;
255
256 654
        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 70
    public function __unset($key)
277
    {
278 70
        unset($this->values[$key]);
279
280 70
        $this->transientValues->add($key);
281 70
        $this->unsavedValues->discard($key);
282 70
    }
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 30
    public function toArray($recursive = false)
312
    {
313 6
        return $recursive
314 29
            ? Util::convertStripeObjectToArray($this->values)
315 30
            : $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 809
    public function keys()
340
    {
341 809
        return count($this->values) ? array_keys($this->values) : [];
342
    }
343
344
    /* ------------------------------------------------------------------------------------------------
345
     |  ArrayAccess methods
346
     | ------------------------------------------------------------------------------------------------
347
     */
348 63
    public function offsetSet($key, $value)
349
    {
350 63
        $this->$key = $value;
351 63
    }
352
353 649
    public function offsetExists($key)
354
    {
355 649
        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 501
    public function offsetGet($key)
364
    {
365 501
        return array_key_exists($key, $this->values)
366 495
            ? $this->values[$key]
367 501
            : 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 659
    public static function scopedConstructFrom($class, $values, $options)
385
    {
386
        /** @var self $obj */
387 659
        $obj = new $class(isset($values['id']) ? $values['id'] : null);
388 659
        $obj->refreshFrom($values, $options);
389
390 659
        return $obj;
391
    }
392
393
    /**
394
     * Refreshes this object using the provided values.
395
     *
396
     * @param  array                                                    $values
397
     * @param  \Arcanedev\Stripe\Http\RequestOptions|array|string|null  $opts
398
     * @param  bool                                                     $partial
399
     */
400 659
    public function refreshFrom($values, $opts, $partial = false)
401
    {
402 659
        $this->opts = $opts;
403
404 659
        $this->cleanObject($values, $partial);
405
406 659
        foreach ($values as $key => $value) {
407 659
            if (self::$permanentAttributes->includes($key) && isset($this[$key]))
408 655
                continue;
409
410 659
            $this->values[$key] = $this->constructValue($key, $value, $opts);
411
412 659
            $this->transientValues->discard($key);
413 659
            $this->unsavedValues->discard($key);
414 527
        }
415 659
    }
416
417
    /**
418
     * Clean refreshed StripeObject.
419
     *
420
     * @param  array       $values
421
     * @param  bool|false  $partial
422
     */
423 659
    private function cleanObject($values, $partial)
424
    {
425
        // Wipe old state before setting new.
426
        // This is useful for e.g. updating a customer, where there is no persistent card parameter.
427
        // Mark those values which don't persist as transient
428 132
        $removed = ! $partial
429 659
            ? array_diff($this->keys(), array_keys($values))
430 659
            : new UtilSet;
431
432 659
        foreach ($removed as $key) {
433 50
            if (self::$permanentAttributes->includes($key))
434 40
                continue;
435
436 50
            unset($this->$key);
437 527
        }
438 659
    }
439
440
    /**
441
     * Construct Value.
442
     *
443
     * @param  string  $key
444
     * @param  mixed   $value
445
     * @param  array   $opts
446
     *
447
     * @return self|\Arcanedev\Stripe\StripeResource|\Arcanedev\Stripe\Collection|array
448
     */
449 659
    private function constructValue($key, $value, $opts)
450
    {
451 659
        return (self::$nestedUpdatableAttributes->includes($key) && is_array($value))
452 647
            ? self::scopedConstructFrom(self::ATTACHED_OBJECT_CLASS, $value, $opts)
453 659
            : Util::convertToStripeObject($value, $opts);
454
    }
455
456
    /**
457
     * Pretend to have late static bindings.
458
     *
459
     * @param  string  $method
460
     *
461
     * @return mixed
462
     */
463 401
    protected function lsb($method)
464
    {
465 401
        $class  = get_class($this);
466 401
        $args   = array_slice(func_get_args(), 1);
467
468 401
        return call_user_func_array([$class, $method], $args);
469
    }
470
471
    /**
472
     * Scoped Late Static Bindings.
473
     *
474
     * @param  string  $class
475
     * @param  string  $method
476
     *
477
     * @return mixed
478
     */
479
    protected static function scopedLsb($class, $method)
480
    {
481
        $args = array_slice(func_get_args(), 2);
482
483
        return call_user_func_array([$class, $method], $args);
484
    }
485
486
    /* ------------------------------------------------------------------------------------------------
487
     |  Check Functions
488
     | ------------------------------------------------------------------------------------------------
489
     */
490
    /**
491
     * Check if array has id.
492
     *
493
     * @param  array  $array
494
     *
495
     * @throws \Arcanedev\Stripe\Exceptions\ApiException
496
     */
497 15
    private function checkIdIsInArray($array)
498
    {
499 15
        if ( ! array_key_exists('id', $array)) {
500 5
            throw new ApiException('The attribute id must be included.');
501
        }
502 10
    }
503
504
    /**
505
     * Check if object has retrieve parameters.
506
     *
507
     * @return bool
508
     */
509 5
    public function hasRetrieveParams()
510
    {
511 5
        return (bool) count($this->getRetrieveParams());
512
    }
513
514
    /**
515
     * Check if attribute deletion.
516
     *
517
     * @param  string      $key
518
     * @param  mixed|null  $value
519
     *
520
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
521
     */
522 789
    private function checkIfAttributeDeletion($key, $value)
523
    {
524
        // Don't use empty($value) instead of ($value === '')
525 789
        if ( ! is_null($value) && $value === '') {
526 5
            throw new InvalidArgumentException(
527 5
                "You cannot set '$key' to an empty string. "
528 1
                . 'We interpret empty strings as \'null\' in requests. '
529 5
                . "You may set obj->$key = null to delete the property"
530 4
            );
531
        }
532 789
    }
533
534
    /**
535
     * Check metadata attribute.
536
     *
537
     * @param  string      $key
538
     * @param  mixed|null  $value
539
     *
540
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
541
     */
542 789
    private function checkMetadataAttribute($key, $value)
543
    {
544
        if (
545 789
            $key === 'metadata' &&
546 187
            ( ! is_array($value) && ! is_null($value))
547 631
        ) {
548 5
            throw new InvalidArgumentException(
549 5
                'The metadata value must be an array or null, ' . gettype($value) . ' is given'
550 4
            );
551
        }
552 789
    }
553
554
    /**
555
     * Check permanent attributes.
556
     *
557
     * @param  string  $key
558
     */
559 789
    private function checkPermanentAttributes($key)
560
    {
561 789
        if ( ! self::$permanentAttributes->includes($key)) {
562 190
            $this->unsavedValues->add($key);
563 151
        }
564 789
    }
565
566
    /**
567
     * Check unsaved attributes.
568
     *
569
     * @param  array  $supported
570
     *
571
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
572
     */
573 789
    private function checkUnsavedAttributes($supported)
574
    {
575 789
        if ($this->checkUnsavedAttributes === false || count($supported) === 0)
576 789
            return;
577
578 40
        $this->checkNotFoundAttributesException(
579 40
            $this->unsavedValues->diffKeys($supported)
580 32
        );
581 30
    }
582
583
    /**
584
     * Check not found attributes exception.
585
     *
586
     * @param  array  $notFound
587
     *
588
     * @throws \Arcanedev\Stripe\Exceptions\InvalidArgumentException
589
     */
590 40
    private function checkNotFoundAttributesException($notFound)
591
    {
592 40
        if (count($notFound)) {
593 10
            throw new InvalidArgumentException(
594 10
                'The attributes [' . implode(', ', $notFound) . '] are not supported.'
595 8
            );
596
        }
597 30
    }
598
599
    /* ------------------------------------------------------------------------------------------------
600
     |  Other Functions
601
     | ------------------------------------------------------------------------------------------------
602
     */
603
    /**
604
     * A recursive mapping of attributes to values for this object,
605
     * including the proper value for deleted attributes.
606
     *
607
     * @return array
608
     */
609 160
    protected function serializeParameters()
610
    {
611 160
        $params = [];
612
613 160
        $this->serializeUnsavedValues($params);
614 160
        $this->serializeNestedUpdatableAttributes($params);
615
616 160
        return $params;
617
    }
618
619
    /**
620
     * Serialize unsaved values.
621
     *
622
     * @param  array  $params
623
     */
624 160
    private function serializeUnsavedValues(&$params)
625
    {
626 160
        foreach ($this->unsavedValues->toArray() as $key) {
627 140
            $params[$key] = ! is_null($value = $this->$key) ? $value : '';
628 127
        }
629 160
    }
630
631
    /**
632
     * Serialize nested updatable attributes.
633
     *
634
     * @param  array  $params
635
     */
636 160
    private function serializeNestedUpdatableAttributes(&$params)
637
    {
638 160
        foreach (self::$nestedUpdatableAttributes->toArray() as $property) {
639
            if (
640 160
                isset($this->$property) &&
641 160
                $this->$property instanceof self &&
642 152
                $serialized = $this->$property->serializeParameters()
643 127
            ) {
644 92
                $params[$property] = $serialized;
645 59
            }
646 127
        }
647 160
    }
648
649
    /**
650
     * Show undefined property warning message.
651
     *
652
     * @param  string  $class
653
     * @param  string  $key
654
     */
655 15
    private function showUndefinedPropertyMsg($class, $key)
656
    {
657 15
        $message = "Stripe Notice: Undefined property of $class instance: $key.";
658
659 15
        if ($this->transientValues->includes($key)) {
660 10
            $message .= " HINT: The [$key] attribute was set in the past, however. " .
661 10
                'It was then wiped when refreshing the object with the result returned by Stripe\'s API, ' .
662 10
                'probably as a result of a save().' .
663 10
                $this->showUndefinedPropertyMsgAttributes();
664 8
        }
665
666 15
        if ( ! is_testing()) error_log($message);
667 15
    }
668
669
    /**
670
     * Show available attributes for undefined property warning message.
671
     *
672
     * @return string
673
     */
674 10
    private function showUndefinedPropertyMsgAttributes()
675
    {
676 10
        return count($attributes = $this->keys())
677 8
            ? ' The attributes currently available on this object are: ' . join(', ', $attributes)
678 10
            : '';
679
    }
680
}
681