• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-php / 6471862601

10 Oct 2023 04:02PM UTC coverage: 69.665% (-0.5%) from 70.141%
6471862601

push

github

web-flow
Merge pull request #1570 from localheinz/feature/coveralls

Enhancement: Use `coverallsapp/github-action` to report code coverage

2393 of 3435 relevant lines covered (69.67%)

3.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

85.17
/lib/StripeObject.php
1
<?php
2

3
namespace Stripe;
4

5
/**
6
 * Class StripeObject.
7
 *
8
 * @property null|string $id
9
 */
10
class StripeObject implements \ArrayAccess, \Countable, \JsonSerializable
11
{
12
    /** @var Util\RequestOptions */
13
    protected $_opts;
14

15
    /** @var array */
16
    protected $_originalValues;
17

18
    /** @var array */
19
    protected $_values;
20

21
    /** @var Util\Set */
22
    protected $_unsavedValues;
23

24
    /** @var Util\Set */
25
    protected $_transientValues;
26

27
    /** @var null|array */
28
    protected $_retrieveOptions;
29

30
    /** @var null|ApiResponse */
31
    protected $_lastResponse;
32

33
    /**
34
     * @return Util\Set Attributes that should not be sent to the API because
35
     *    they're not updatable (e.g. ID).
36
     */
37
    public static function getPermanentAttributes()
24✔
38
    {
39
        static $permanentAttributes = null;
24✔
40
        if (null === $permanentAttributes) {
24✔
41
            $permanentAttributes = new Util\Set([
×
42
                'id',
×
43
            ]);
×
44
        }
45

46
        return $permanentAttributes;
24✔
47
    }
48

49
    /**
50
     * Additive objects are subobjects in the API that don't have the same
51
     * semantics as most subobjects, which are fully replaced when they're set.
52
     *
53
     * This is best illustrated by example. The `source` parameter sent when
54
     * updating a subscription is *not* additive; if we set it:
55
     *
56
     *     source[object]=card&source[number]=123
57
     *
58
     * We expect the old `source` object to have been overwritten completely. If
59
     * the previous source had an `address_state` key associated with it and we
60
     * didn't send one this time, that value of `address_state` is gone.
61
     *
62
     * By contrast, additive objects are those that will have new data added to
63
     * them while keeping any existing data in place. The only known case of its
64
     * use is for `metadata`, but it could in theory be more general. As an
65
     * example, say we have a `metadata` object that looks like this on the
66
     * server side:
67
     *
68
     *     metadata = ["old" => "old_value"]
69
     *
70
     * If we update the object with `metadata[new]=new_value`, the server side
71
     * object now has *both* fields:
72
     *
73
     *     metadata = ["old" => "old_value", "new" => "new_value"]
74
     *
75
     * This is okay in itself because usually users will want to treat it as
76
     * additive:
77
     *
78
     *     $obj->metadata["new"] = "new_value";
79
     *     $obj->save();
80
     *
81
     * However, in other cases, they may want to replace the entire existing
82
     * contents:
83
     *
84
     *     $obj->metadata = ["new" => "new_value"];
85
     *     $obj->save();
86
     *
87
     * This is where things get a little bit tricky because in order to clear
88
     * any old keys that may have existed, we actually have to send an explicit
89
     * empty string to the server. So the operation above would have to send
90
     * this form to get the intended behavior:
91
     *
92
     *     metadata[old]=&metadata[new]=new_value
93
     *
94
     * This method allows us to track which parameters are considered additive,
95
     * and lets us behave correctly where appropriate when serializing
96
     * parameters to be sent.
97
     *
98
     * @return Util\Set Set of additive parameters
99
     */
100
    public static function getAdditiveParams()
3✔
101
    {
102
        static $additiveParams = null;
3✔
103
        if (null === $additiveParams) {
3✔
104
            // Set `metadata` as additive so that when it's set directly we remember
105
            // to clear keys that may have been previously set by sending empty
106
            // values for them.
107
            //
108
            // It's possible that not every object has `metadata`, but having this
109
            // option set when there is no `metadata` field is not harmful.
110
            $additiveParams = new Util\Set([
×
111
                'metadata',
×
112
            ]);
×
113
        }
114

115
        return $additiveParams;
3✔
116
    }
117

118
    public function __construct($id = null, $opts = null)
40✔
119
    {
120
        list($id, $this->_retrieveOptions) = Util\Util::normalizeId($id);
40✔
121
        $this->_opts = Util\RequestOptions::parse($opts);
40✔
122
        $this->_originalValues = [];
40✔
123
        $this->_values = [];
40✔
124
        $this->_unsavedValues = new Util\Set();
40✔
125
        $this->_transientValues = new Util\Set();
40✔
126
        if (null !== $id) {
40✔
127
            $this->_values['id'] = $id;
8✔
128
        }
129
    }
130

131
    // Standard accessor magic methods
132
    public function __set($k, $v)
24✔
133
    {
134
        if (static::getPermanentAttributes()->includes($k)) {
24✔
135
            throw new Exception\InvalidArgumentException(
1✔
136
                "Cannot set {$k} on this object. HINT: you can't set: " .
1✔
137
                \implode(', ', static::getPermanentAttributes()->toArray())
1✔
138
            );
1✔
139
        }
140

141
        if ('' === $v) {
23✔
142
            throw new Exception\InvalidArgumentException(
1✔
143
                'You cannot set \'' . $k . '\'to an empty string. '
1✔
144
                . 'We interpret empty strings as NULL in requests. '
1✔
145
                . 'You may set obj->' . $k . ' = NULL to delete the property'
1✔
146
            );
1✔
147
        }
148

149
        $this->_values[$k] = Util\Util::convertToStripeObject($v, $this->_opts);
22✔
150
        $this->dirtyValue($this->_values[$k]);
22✔
151
        $this->_unsavedValues->add($k);
22✔
152
    }
153

154
    /**
155
     * @param mixed $k
156
     *
157
     * @return bool
158
     */
159
    public function __isset($k)
3✔
160
    {
161
        return isset($this->_values[$k]);
3✔
162
    }
163

164
    public function __unset($k)
2✔
165
    {
166
        unset($this->_values[$k]);
2✔
167
        $this->_transientValues->add($k);
2✔
168
        $this->_unsavedValues->discard($k);
2✔
169
    }
170

171
    public function &__get($k)
8✔
172
    {
173
        // function should return a reference, using $nullval to return a reference to null
174
        $nullval = null;
8✔
175
        if (!empty($this->_values) && \array_key_exists($k, $this->_values)) {
8✔
176
            return $this->_values[$k];
7✔
177
        }
178
        if (!empty($this->_transientValues) && $this->_transientValues->includes($k)) {
1✔
179
            $class = static::class;
×
180
            $attrs = \implode(', ', \array_keys($this->_values));
×
181
            $message = "Stripe Notice: Undefined property of {$class} instance: {$k}. "
×
182
                    . "HINT: The {$k} attribute was set in the past, however. "
×
183
                    . 'It was then wiped when refreshing the object '
×
184
                    . "with the result returned by Stripe's API, "
×
185
                    . 'probably as a result of a save(). The attributes currently '
×
186
                    . "available on this object are: {$attrs}";
×
187
            Stripe::getLogger()->error($message);
×
188

189
            return $nullval;
×
190
        }
191
        $class = static::class;
1✔
192
        Stripe::getLogger()->error("Stripe Notice: Undefined property of {$class} instance: {$k}");
1✔
193

194
        return $nullval;
1✔
195
    }
196

197
    /**
198
     * Magic method for var_dump output. Only works with PHP >= 5.6.
199
     *
200
     * @return array
201
     */
202
    public function __debugInfo()
×
203
    {
204
        return $this->_values;
×
205
    }
206

207
    // ArrayAccess methods
208

209
    /**
210
     * @return void
211
     */
212
    #[\ReturnTypeWillChange]
3✔
213
    public function offsetSet($k, $v)
214
    {
215
        $this->{$k} = $v;
3✔
216
    }
217

218
    /**
219
     * @return bool
220
     */
221
    #[\ReturnTypeWillChange]
1✔
222
    public function offsetExists($k)
223
    {
224
        return \array_key_exists($k, $this->_values);
1✔
225
    }
226

227
    /**
228
     * @return void
229
     */
230
    #[\ReturnTypeWillChange]
2✔
231
    public function offsetUnset($k)
232
    {
233
        unset($this->{$k});
2✔
234
    }
235

236
    /**
237
     * @return mixed
238
     */
239
    #[\ReturnTypeWillChange]
4✔
240
    public function offsetGet($k)
241
    {
242
        return \array_key_exists($k, $this->_values) ? $this->_values[$k] : null;
4✔
243
    }
244

245
    /**
246
     * @return int
247
     */
248
    #[\ReturnTypeWillChange]
1✔
249
    public function count()
250
    {
251
        return \count($this->_values);
1✔
252
    }
253

254
    public function keys()
1✔
255
    {
256
        return \array_keys($this->_values);
1✔
257
    }
258

259
    public function values()
1✔
260
    {
261
        return \array_values($this->_values);
1✔
262
    }
263

264
    /**
265
     * This unfortunately needs to be public to be used in Util\Util.
266
     *
267
     * @param array $values
268
     * @param null|array|string|Util\RequestOptions $opts
269
     *
270
     * @return static the object constructed from the given values
271
     */
272
    public static function constructFrom($values, $opts = null)
27✔
273
    {
274
        $obj = new static(isset($values['id']) ? $values['id'] : null);
27✔
275
        $obj->refreshFrom($values, $opts);
27✔
276

277
        return $obj;
27✔
278
    }
279

280
    /**
281
     * Refreshes this object using the provided values.
282
     *
283
     * @param array $values
284
     * @param null|array|string|Util\RequestOptions $opts
285
     * @param bool $partial defaults to false
286
     */
287
    public function refreshFrom($values, $opts, $partial = false)
27✔
288
    {
289
        $this->_opts = Util\RequestOptions::parse($opts);
27✔
290

291
        $this->_originalValues = self::deepCopy($values);
27✔
292

293
        if ($values instanceof StripeObject) {
27✔
294
            $values = $values->toArray();
×
295
        }
296

297
        // Wipe old state before setting new.  This is useful for e.g. updating a
298
        // customer, where there is no persistent card parameter.  Mark those values
299
        // which don't persist as transient
300
        if ($partial) {
27✔
301
            $removed = new Util\Set();
×
302
        } else {
303
            $removed = new Util\Set(\array_diff(\array_keys($this->_values), \array_keys($values)));
27✔
304
        }
305

306
        foreach ($removed->toArray() as $k) {
27✔
307
            unset($this->{$k});
×
308
        }
309

310
        $this->updateAttributes($values, $opts, false);
27✔
311
        foreach ($values as $k => $v) {
27✔
312
            $this->_transientValues->discard($k);
25✔
313
            $this->_unsavedValues->discard($k);
25✔
314
        }
315
    }
316

317
    /**
318
     * Mass assigns attributes on the model.
319
     *
320
     * @param array $values
321
     * @param null|array|string|Util\RequestOptions $opts
322
     * @param bool $dirty defaults to true
323
     */
324
    public function updateAttributes($values, $opts = null, $dirty = true)
27✔
325
    {
326
        foreach ($values as $k => $v) {
27✔
327
            // Special-case metadata to always be cast as a StripeObject
328
            // This is necessary in case metadata is empty, as PHP arrays do
329
            // not differentiate between lists and hashes, and we consider
330
            // empty arrays to be lists.
331
            if (('metadata' === $k) && (\is_array($v))) {
25✔
332
                $this->_values[$k] = StripeObject::constructFrom($v, $opts);
3✔
333
            } else {
334
                $this->_values[$k] = Util\Util::convertToStripeObject($v, $opts);
24✔
335
            }
336
            if ($dirty) {
25✔
337
                $this->dirtyValue($this->_values[$k]);
1✔
338
            }
339
            $this->_unsavedValues->add($k);
25✔
340
        }
341
    }
342

343
    /**
344
     * @param bool $force defaults to false
345
     *
346
     * @return array a recursive mapping of attributes to values for this object,
347
     *    including the proper value for deleted attributes
348
     */
349
    public function serializeParameters($force = false)
20✔
350
    {
351
        $updateParams = [];
20✔
352

353
        foreach ($this->_values as $k => $v) {
20✔
354
            // There are a few reasons that we may want to add in a parameter for
355
            // update:
356
            //
357
            //   1. The `$force` option has been set.
358
            //   2. We know that it was modified.
359
            //   3. Its value is a StripeObject. A StripeObject may contain modified
360
            //      values within in that its parent StripeObject doesn't know about.
361
            //
362
            $original = \array_key_exists($k, $this->_originalValues) ? $this->_originalValues[$k] : null;
19✔
363
            $unsaved = $this->_unsavedValues->includes($k);
19✔
364
            if ($force || $unsaved || $v instanceof StripeObject) {
19✔
365
                $updateParams[$k] = $this->serializeParamsValue(
18✔
366
                    $this->_values[$k],
18✔
367
                    $original,
18✔
368
                    $unsaved,
18✔
369
                    $force,
18✔
370
                    $k
18✔
371
                );
18✔
372
            }
373
        }
374

375
        // a `null` that makes it out of `serializeParamsValue` signals an empty
376
        // value that we shouldn't appear in the serialized form of the object
377
        return \array_filter(
19✔
378
            $updateParams,
19✔
379
            function ($v) {
19✔
380
                return null !== $v;
17✔
381
            }
19✔
382
        );
19✔
383
    }
384

385
    public function serializeParamsValue($value, $original, $unsaved, $force, $key = null)
18✔
386
    {
387
        // The logic here is that essentially any object embedded in another
388
        // object that had a `type` is actually an API resource of a different
389
        // type that's been included in the response. These other resources must
390
        // be updated from their proper endpoints, and therefore they are not
391
        // included when serializing even if they've been modified.
392
        //
393
        // There are _some_ known exceptions though.
394
        //
395
        // For example, if the value is unsaved (meaning the user has set it), and
396
        // it looks like the API resource is persisted with an ID, then we include
397
        // the object so that parameters are serialized with a reference to its
398
        // ID.
399
        //
400
        // Another example is that on save API calls it's sometimes desirable to
401
        // update a customer's default source by setting a new card (or other)
402
        // object with `->source=` and then saving the customer. The
403
        // `saveWithParent` flag to override the default behavior allows us to
404
        // handle these exceptions.
405
        //
406
        // We throw an error if a property was set explicitly but we can't do
407
        // anything with it because the integration is probably not working as the
408
        // user intended it to.
409
        if (null === $value) {
18✔
410
            return '';
6✔
411
        }
412
        if (($value instanceof ApiResource) && (!$value->saveWithParent)) {
18✔
413
            if (!$unsaved) {
3✔
414
                return null;
1✔
415
            }
416
            if (isset($value->id)) {
2✔
417
                return $value;
1✔
418
            }
419

420
            throw new Exception\InvalidArgumentException(
1✔
421
                "Cannot save property `{$key}` containing an API resource of type " .
1✔
422
                    \get_class($value) . ". It doesn't appear to be persisted and is " .
1✔
423
                    'not marked as `saveWithParent`.'
1✔
424
            );
1✔
425
        }
426
        if (\is_array($value)) {
15✔
427
            if (Util\Util::isList($value)) {
6✔
428
                // Sequential array, i.e. a list
429
                $update = [];
6✔
430
                foreach ($value as $v) {
6✔
431
                    $update[] = $this->serializeParamsValue($v, null, true, $force);
6✔
432
                }
433
                // This prevents an array that's unchanged from being resent.
434
                if ($update !== $this->serializeParamsValue($original, null, true, $force, $key)) {
6✔
435
                    return $update;
6✔
436
                }
437
            } else {
438
                // Associative array, i.e. a map
439
                return Util\Util::convertToStripeObject($value, $this->_opts)->serializeParameters();
×
440
            }
441
        } elseif ($value instanceof StripeObject) {
15✔
442
            $update = $value->serializeParameters($force);
10✔
443
            if ($original && $unsaved && $key && static::getAdditiveParams()->includes($key)) {
10✔
444
                $update = \array_merge(self::emptyValues($original), $update);
2✔
445
            }
446

447
            return $update;
10✔
448
        } else {
449
            return $value;
14✔
450
        }
451
    }
452

453
    /**
454
     * @return mixed
455
     */
456
    #[\ReturnTypeWillChange]
1✔
457
    public function jsonSerialize()
458
    {
459
        return $this->toArray();
1✔
460
    }
461

462
    /**
463
     * Returns an associative array with the key and values composing the
464
     * Stripe object.
465
     *
466
     * @return array the associative array
467
     */
468
    public function toArray()
4✔
469
    {
470
        $maybeToArray = function ($value) {
4✔
471
            if (null === $value) {
4✔
472
                return null;
1✔
473
            }
474

475
            return \is_object($value) && \method_exists($value, 'toArray') ? $value->toArray() : $value;
4✔
476
        };
4✔
477

478
        return \array_reduce(\array_keys($this->_values), function ($acc, $k) use ($maybeToArray) {
4✔
479
            if ('_' === \substr((string) $k, 0, 1)) {
4✔
480
                return $acc;
×
481
            }
482
            $v = $this->_values[$k];
4✔
483
            if (Util\Util::isList($v)) {
4✔
484
                $acc[$k] = \array_map($maybeToArray, $v);
2✔
485
            } else {
486
                $acc[$k] = $maybeToArray($v);
4✔
487
            }
488

489
            return $acc;
4✔
490
        }, []);
4✔
491
    }
492

493
    /**
494
     * Returns a pretty JSON representation of the Stripe object.
495
     *
496
     * @return string the JSON representation of the Stripe object
497
     */
498
    public function toJSON()
1✔
499
    {
500
        return \json_encode($this->toArray(), \JSON_PRETTY_PRINT);
1✔
501
    }
502

503
    public function __toString()
1✔
504
    {
505
        $class = static::class;
1✔
506

507
        return $class . ' JSON: ' . $this->toJSON();
1✔
508
    }
509

510
    /**
511
     * Sets all keys within the StripeObject as unsaved so that they will be
512
     * included with an update when `serializeParameters` is called. This
513
     * method is also recursive, so any StripeObjects contained as values or
514
     * which are values in a tenant array are also marked as dirty.
515
     */
516
    public function dirty()
9✔
517
    {
518
        $this->_unsavedValues = new Util\Set(\array_keys($this->_values));
9✔
519
        foreach ($this->_values as $k => $v) {
9✔
520
            $this->dirtyValue($v);
8✔
521
        }
522
    }
523

524
    protected function dirtyValue($value)
24✔
525
    {
526
        if (\is_array($value)) {
24✔
527
            foreach ($value as $v) {
7✔
528
                $this->dirtyValue($v);
7✔
529
            }
530
        } elseif ($value instanceof StripeObject) {
24✔
531
            $value->dirty();
9✔
532
        }
533
    }
534

535
    /**
536
     * Produces a deep copy of the given object including support for arrays
537
     * and StripeObjects.
538
     *
539
     * @param mixed $obj
540
     */
541
    protected static function deepCopy($obj)
27✔
542
    {
543
        if (\is_array($obj)) {
27✔
544
            $copy = [];
27✔
545
            foreach ($obj as $k => $v) {
27✔
546
                $copy[$k] = self::deepCopy($v);
25✔
547
            }
548

549
            return $copy;
27✔
550
        }
551
        if ($obj instanceof StripeObject) {
24✔
552
            return $obj::constructFrom(
10✔
553
                self::deepCopy($obj->_values),
10✔
554
                clone $obj->_opts
10✔
555
            );
10✔
556
        }
557

558
        return $obj;
24✔
559
    }
560

561
    /**
562
     * Returns a hash of empty values for all the values that are in the given
563
     * StripeObject.
564
     *
565
     * @param mixed $obj
566
     */
567
    public static function emptyValues($obj)
2✔
568
    {
569
        if (\is_array($obj)) {
2✔
570
            $values = $obj;
×
571
        } elseif ($obj instanceof StripeObject) {
2✔
572
            $values = $obj->_values;
2✔
573
        } else {
574
            throw new Exception\InvalidArgumentException(
×
575
                'empty_values got unexpected object type: ' . \get_class($obj)
×
576
            );
×
577
        }
578

579
        return \array_fill_keys(\array_keys($values), '');
2✔
580
    }
581

582
    /**
583
     * @return null|ApiResponse The last response from the Stripe API
584
     */
585
    public function getLastResponse()
×
586
    {
587
        return $this->_lastResponse;
×
588
    }
589

590
    /**
591
     * Sets the last response from the Stripe API.
592
     *
593
     * @param ApiResponse $resp
594
     */
595
    public function setLastResponse($resp)
×
596
    {
597
        $this->_lastResponse = $resp;
×
598
    }
599

600
    /**
601
     * Indicates whether or not the resource has been deleted on the server.
602
     * Note that some, but not all, resources can indicate whether they have
603
     * been deleted.
604
     *
605
     * @return bool whether the resource is deleted
606
     */
607
    public function isDeleted()
1✔
608
    {
609
        return isset($this->_values['deleted']) ? $this->_values['deleted'] : false;
1✔
610
    }
611
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc