Test Failed
Push — master ( 3dd85e...34f16b )
by Devin
04:34 queued 10s
created

StripeObject   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 514
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
dl 0
loc 514
rs 2
c 0
b 0
f 0
wmc 80
lcom 1
cbo 6

21 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
A __set() 0 21 3
A __isset() 0 4 1
A __unset() 0 6 1
A __get() 0 23 5
A __debugInfo() 0 4 1
A offsetSet() 0 4 1
A offsetExists() 0 4 1
A offsetUnset() 0 4 1
A offsetGet() 0 4 2
A keys() 0 4 1
A constructFrom() 0 6 2
A refreshFrom() 0 29 5
B serializeParameters() 0 37 6
A jsonSerialize() 0 4 1
A __toJSON() 0 4 1
A __toString() 0 5 1
A __toArray() 0 8 2
A getLastResponse() 0 4 1
A setLastResponse() 0 4 1
A isDeleted() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like StripeObject often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StripeObject, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Stripe;
4
5
/**
6
 * Class StripeObject
7
 *
8
 * @package Stripe
9
 */
10
class StripeObject implements \ArrayAccess, \Countable, \JsonSerializable
11
{
12
    protected $_opts;
13
    protected $_originalValues;
14
    protected $_values;
15
    protected $_unsavedValues;
16
    protected $_transientValues;
17
    protected $_retrieveOptions;
18
    protected $_lastResponse;
19
20
    /**
21
     * @return Util\Set Attributes that should not be sent to the API because
22
     *    they're not updatable (e.g. ID).
23
     */
24
    public static function getPermanentAttributes()
25
    {
26
        static $permanentAttributes = null;
27
        if ($permanentAttributes === null) {
28
            $permanentAttributes = new Util\Set([
29
                'id',
30
            ]);
31
        }
32
        return $permanentAttributes;
33
    }
34
35
    /**
36
     * Additive objects are subobjects in the API that don't have the same
37
     * semantics as most subobjects, which are fully replaced when they're set.
38
     * This is best illustrated by example. The `source` parameter sent when
39
     * updating a subscription is *not* additive; if we set it:
40
     *
41
     *     source[object]=card&source[number]=123
42
     *
43
     * We expect the old `source` object to have been overwritten completely. If
44
     * the previous source had an `address_state` key associated with it and we
45
     * didn't send one this time, that value of `address_state` is gone.
46
     *
47
     * By contrast, additive objects are those that will have new data added to
48
     * them while keeping any existing data in place. The only known case of its
49
     * use is for `metadata`, but it could in theory be more general. As an
50
     * example, say we have a `metadata` object that looks like this on the
51
     * server side:
52
     *
53
     *     metadata = ["old" => "old_value"]
54
     *
55
     * If we update the object with `metadata[new]=new_value`, the server side
56
     * object now has *both* fields:
57
     *
58
     *     metadata = ["old" => "old_value", "new" => "new_value"]
59
     *
60
     * This is okay in itself because usually users will want to treat it as
61
     * additive:
62
     *
63
     *     $obj->metadata["new"] = "new_value";
64
     *     $obj->save();
65
     *
66
     * However, in other cases, they may want to replace the entire existing
67
     * contents:
68
     *
69
     *     $obj->metadata = ["new" => "new_value"];
70
     *     $obj->save();
71
     *
72
     * This is where things get a little bit tricky because in order to clear
73
     * any old keys that may have existed, we actually have to send an explicit
74
     * empty string to the server. So the operation above would have to send
75
     * this form to get the intended behavior:
76
     *
77
     *     metadata[old]=&metadata[new]=new_value
78
     *
79
     * This method allows us to track which parameters are considered additive,
80
     * and lets us behave correctly where appropriate when serializing
81
     * parameters to be sent.
82
     *
83
     * @return Util\Set Set of additive parameters
84
     */
85
    public static function getAdditiveParams()
86
    {
87
        static $additiveParams = null;
88
        if ($additiveParams === null) {
89
            // Set `metadata` as additive so that when it's set directly we remember
90
            // to clear keys that may have been previously set by sending empty
91
            // values for them.
92
            //
93
            // It's possible that not every object has `metadata`, but having this
94
            // option set when there is no `metadata` field is not harmful.
95
            $additiveParams = new Util\Set([
96
                'metadata',
97
            ]);
98
        }
99
        return $additiveParams;
100
    }
101
102
    public function __construct($id = null, $opts = null)
103
    {
104
        list($id, $this->_retrieveOptions) = Util\Util::normalizeId($id);
105
        $this->_opts = Util\RequestOptions::parse($opts);
106
        $this->_originalValues = [];
107
        $this->_values = [];
108
        $this->_unsavedValues = new Util\Set();
109
        $this->_transientValues = new Util\Set();
110
        if ($id !== null) {
111
            $this->_values['id'] = $id;
112
        }
113
    }
114
115
    // Standard accessor magic methods
116
    public function __set($k, $v)
117
    {
118
        if (static::getPermanentAttributes()->includes($k)) {
119
            throw new \InvalidArgumentException(
120
                "Cannot set $k on this object. HINT: you can't set: " .
121
                join(', ', static::getPermanentAttributes()->toArray())
122
            );
123
        }
124
125
        if ($v === "") {
126
            throw new \InvalidArgumentException(
127
                'You cannot set \''.$k.'\'to an empty string. '
128
                .'We interpret empty strings as NULL in requests. '
129
                .'You may set obj->'.$k.' = NULL to delete the property'
130
            );
131
        }
132
133
        $this->_values[$k] = Util\Util::convertToStripeObject($v, $this->_opts);
0 ignored issues
show
Documentation introduced by
$this->_opts is of type object<Stripe\Util\RequestOptions>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
134
        $this->dirtyValue($this->_values[$k]);
135
        $this->_unsavedValues->add($k);
136
    }
137
138
    public function __isset($k)
139
    {
140
        return isset($this->_values[$k]);
141
    }
142
143
    public function __unset($k)
144
    {
145
        unset($this->_values[$k]);
146
        $this->_transientValues->add($k);
147
        $this->_unsavedValues->discard($k);
148
    }
149
150
    public function &__get($k)
151
    {
152
        // function should return a reference, using $nullval to return a reference to null
153
        $nullval = null;
154
        if (!empty($this->_values) && array_key_exists($k, $this->_values)) {
155
            return $this->_values[$k];
156
        } else if (!empty($this->_transientValues) && $this->_transientValues->includes($k)) {
157
            $class = get_class($this);
158
            $attrs = join(', ', array_keys($this->_values));
159
            $message = "Stripe Notice: Undefined property of $class instance: $k. "
160
                    . "HINT: The $k attribute was set in the past, however. "
161
                    . "It was then wiped when refreshing the object "
162
                    . "with the result returned by Stripe's API, "
163
                    . "probably as a result of a save(). The attributes currently "
164
                    . "available on this object are: $attrs";
165
            Stripe::getLogger()->error($message);
166
            return $nullval;
167
        } else {
168
            $class = get_class($this);
169
            Stripe::getLogger()->error("Stripe Notice: Undefined property of $class instance: $k");
170
            return $nullval;
171
        }
172
    }
173
174
    // Magic method for var_dump output. Only works with PHP >= 5.6
175
    public function __debugInfo()
176
    {
177
        return $this->_values;
178
    }
179
180
    // ArrayAccess methods
181
    public function offsetSet($k, $v)
182
    {
183
        $this->$k = $v;
184
    }
185
186
    public function offsetExists($k)
187
    {
188
        return array_key_exists($k, $this->_values);
189
    }
190
191
    public function offsetUnset($k)
192
    {
193
        unset($this->$k);
194
    }
195
196
    public function offsetGet($k)
197
    {
198
        return array_key_exists($k, $this->_values) ? $this->_values[$k] : null;
199
    }
200
201
    // Countable method
202
    public function count()
203
    {
204
        return count($this->_values);
205
    }
206
207
    public function keys()
208
    {
209
        return array_keys($this->_values);
210
    }
211
212
    public function values()
213
    {
214
        return array_values($this->_values);
215
    }
216
217
    /**
218
     * This unfortunately needs to be public to be used in Util\Util
219
     *
220
     * @param array $values
221
     * @param null|string|array|Util\RequestOptions $opts
222
     *
223
     * @return static The object constructed from the given values.
224
     */
225
    public static function constructFrom($values, $opts = null)
226
    {
227
        $obj = new static(isset($values['id']) ? $values['id'] : null);
228
        $obj->refreshFrom($values, $opts);
229
        return $obj;
230
    }
231
232
    /**
233
     * Refreshes this object using the provided values.
234
     *
235
     * @param array $values
236
     * @param null|string|array|Util\RequestOptions $opts
237
     * @param boolean $partial Defaults to false.
238
     */
239
    public function refreshFrom($values, $opts, $partial = false)
240
    {
241
        $this->_opts = Util\RequestOptions::parse($opts);
242
243
        $this->_originalValues = self::deepCopy($values);
244
245
        if ($values instanceof StripeObject) {
246
            $values = $values->__toArray(true);
247
        }
248
249
        // Wipe old state before setting new.  This is useful for e.g. updating a
250
        // customer, where there is no persistent card parameter.  Mark those values
251
        // which don't persist as transient
252
        if ($partial) {
253
            $removed = new Util\Set();
254
        } else {
255
            $removed = new Util\Set(array_diff(array_keys($this->_values), array_keys($values)));
256
        }
257
258
        foreach ($removed->toArray() as $k) {
259
            unset($this->$k);
260
        }
261
262
        $this->updateAttributes($values, $opts, false);
263
        foreach ($values as $k => $v) {
264
            $this->_transientValues->discard($k);
265
            $this->_unsavedValues->discard($k);
266
        }
267
    }
268
269
    /**
270
     * Mass assigns attributes on the model.
271
     *
272
     * @param array $values
273
     * @param null|string|array|Util\RequestOptions $opts
274
     * @param boolean $dirty Defaults to true.
275
     */
276
    public function updateAttributes($values, $opts = null, $dirty = true)
277
    {
278
        foreach ($values as $k => $v) {
279
            // Special-case metadata to always be cast as a StripeObject
280
            // This is necessary in case metadata is empty, as PHP arrays do
281
            // not differentiate between lists and hashes, and we consider
282
            // empty arrays to be lists.
283
            if (($k === "metadata") && (is_array($v))) {
284
                $this->_values[$k] = StripeObject::constructFrom($v, $opts);
285
            } else {
286
                $this->_values[$k] = Util\Util::convertToStripeObject($v, $opts);
287
            }
288
            if ($dirty) {
289
                $this->dirtyValue($this->_values[$k]);
290
            }
291
            $this->_unsavedValues->add($k);
292
        }
293
    }
294
295
    /**
296
     * @return array A recursive mapping of attributes to values for this object,
297
     *    including the proper value for deleted attributes.
298
     */
299
    public function serializeParameters($force = false)
300
    {
301
        $updateParams = [];
302
303
        foreach ($this->_values as $k => $v) {
304
            // There are a few reasons that we may want to add in a parameter for
305
            // update:
306
            //
307
            //   1. The `$force` option has been set.
308
            //   2. We know that it was modified.
309
            //   3. Its value is a StripeObject. A StripeObject may contain modified
310
            //      values within in that its parent StripeObject doesn't know about.
311
            //
312
            $original = array_key_exists($k, $this->_originalValues) ? $this->_originalValues[$k] : null;
313
            $unsaved = $this->_unsavedValues->includes($k);
314
            if ($force || $unsaved || $v instanceof StripeObject) {
315
                $updateParams[$k] = $this->serializeParamsValue(
316
                    $this->_values[$k],
317
                    $original,
318
                    $unsaved,
319
                    $force,
320
                    $k
321
                );
322
            }
323
        }
324
325
        // a `null` that makes it out of `serializeParamsValue` signals an empty
326
        // value that we shouldn't appear in the serialized form of the object
327
        $updateParams = array_filter(
328
            $updateParams,
329
            function ($v) {
330
                return $v !== null;
331
            }
332
        );
333
334
        return $updateParams;
335
    }
336
337
338
    public function serializeParamsValue($value, $original, $unsaved, $force, $key = null)
339
    {
340
        // The logic here is that essentially any object embedded in another
341
        // object that had a `type` is actually an API resource of a different
342
        // type that's been included in the response. These other resources must
343
        // be updated from their proper endpoints, and therefore they are not
344
        // included when serializing even if they've been modified.
345
        //
346
        // There are _some_ known exceptions though.
347
        //
348
        // For example, if the value is unsaved (meaning the user has set it), and
349
        // it looks like the API resource is persisted with an ID, then we include
350
        // the object so that parameters are serialized with a reference to its
351
        // ID.
352
        //
353
        // Another example is that on save API calls it's sometimes desirable to
354
        // update a customer's default source by setting a new card (or other)
355
        // object with `->source=` and then saving the customer. The
356
        // `saveWithParent` flag to override the default behavior allows us to
357
        // handle these exceptions.
358
        //
359
        // We throw an error if a property was set explicitly but we can't do
360
        // anything with it because the integration is probably not working as the
361
        // user intended it to.
362
        if ($value === null) {
363
            return "";
364
        } elseif (($value instanceof APIResource) && (!$value->saveWithParent)) {
365
            if (!$unsaved) {
366
                return null;
367
            } elseif (isset($value->id)) {
368
                return $value;
369
            } else {
370
                throw new \InvalidArgumentException(
371
                    "Cannot save property `$key` containing an API resource of type " .
372
                    get_class($value) . ". It doesn't appear to be persisted and is " .
373
                    "not marked as `saveWithParent`."
374
                );
375
            }
376
        } elseif (is_array($value)) {
377
            if (Util\Util::isList($value)) {
378
                // Sequential array, i.e. a list
379
                $update = [];
380
                foreach ($value as $v) {
381
                    array_push($update, $this->serializeParamsValue($v, null, true, $force));
382
                }
383
                // This prevents an array that's unchanged from being resent.
384
                if ($update !== $this->serializeParamsValue($original, null, true, $force, $key)) {
385
                    return $update;
386
                }
387
            } else {
388
                // Associative array, i.e. a map
389
                return Util\Util::convertToStripeObject($value, $this->_opts)->serializeParameters();
0 ignored issues
show
Documentation introduced by
$this->_opts is of type object<Stripe\Util\RequestOptions>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
390
            }
391
        } elseif ($value instanceof StripeObject) {
392
            $update = $value->serializeParameters($force);
393
            if ($original && $unsaved && $key && static::getAdditiveParams()->includes($key)) {
394
                $update = array_merge(self::emptyValues($original), $update);
395
            }
396
            return $update;
397
        } else {
398
            return $value;
399
        }
400
    }
401
402
    public function jsonSerialize()
403
    {
404
        return $this->__toArray(true);
405
    }
406
407
    public function __toJSON()
408
    {
409
        return json_encode($this->__toArray(true), JSON_PRETTY_PRINT);
410
    }
411
412
    public function __toString()
413
    {
414
        $class = get_class($this);
415
        return $class . ' JSON: ' . $this->__toJSON();
416
    }
417
418
    public function __toArray($recursive = false)
419
    {
420
        if ($recursive) {
421
            return Util\Util::convertStripeObjectToArray($this->_values);
422
        } else {
423
            return $this->_values;
424
        }
425
    }
426
427
    /**
428
     * Sets all keys within the StripeObject as unsaved so that they will be
429
     * included with an update when `serializeParameters` is called. This
430
     * method is also recursive, so any StripeObjects contained as values or
431
     * which are values in a tenant array are also marked as dirty.
432
     */
433
    public function dirty()
434
    {
435
        $this->_unsavedValues = new Util\Set(array_keys($this->_values));
436
        foreach ($this->_values as $k => $v) {
437
            $this->dirtyValue($v);
438
        }
439
    }
440
441
    protected function dirtyValue($value)
442
    {
443
        if (is_array($value)) {
444
            foreach ($value as $v) {
445
                $this->dirtyValue($v);
446
            }
447
        } elseif ($value instanceof StripeObject) {
448
            $value->dirty();
449
        }
450
    }
451
452
    /**
453
     * Produces a deep copy of the given object including support for arrays
454
     * and StripeObjects.
455
     */
456
    protected static function deepCopy($obj)
457
    {
458
        if (is_array($obj)) {
459
            $copy = [];
460
            foreach ($obj as $k => $v) {
461
                $copy[$k] = self::deepCopy($v);
462
            }
463
            return $copy;
464
        } elseif ($obj instanceof StripeObject) {
465
            return $obj::constructFrom(
466
                self::deepCopy($obj->_values),
467
                clone $obj->_opts
468
            );
469
        } else {
470
            return $obj;
471
        }
472
    }
473
474
    /**
475
     * Returns a hash of empty values for all the values that are in the given
476
     * StripeObject.
477
     */
478
    public static function emptyValues($obj)
479
    {
480
        if (is_array($obj)) {
481
            $values = $obj;
482
        } elseif ($obj instanceof StripeObject) {
483
            $values = $obj->_values;
484
        } else {
485
            throw new \InvalidArgumentException(
486
                "empty_values got got unexpected object type: " . get_class($obj)
487
            );
488
        }
489
        $update = array_fill_keys(array_keys($values), "");
490
        return $update;
491
    }
492
493
    /**
494
     * @return object The last response from the Stripe API
495
     */
496
    public function getLastResponse()
497
    {
498
        return $this->_lastResponse;
499
    }
500
501
    /**
502
     * Sets the last response from the Stripe API
503
     *
504
     * @param ApiResponse $resp
505
     * @return void
506
     */
507
    public function setLastResponse($resp)
508
    {
509
        $this->_lastResponse = $resp;
510
    }
511
512
    /**
513
     * Indicates whether or not the resource has been deleted on the server.
514
     * Note that some, but not all, resources can indicate whether they have
515
     * been deleted.
516
     *
517
     * @return bool Whether the resource is deleted.
518
     */
519
    public function isDeleted()
520
    {
521
        return isset($this->_values['deleted']) ? $this->_values['deleted'] : false;
522
    }
523
}
524