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

stripe / stripe-php / 11129599820

01 Oct 2024 04:33PM UTC coverage: 62.613% (-1.3%) from 63.944%
11129599820

push

github

web-flow
Support for APIs in the new API version 2024-09-30.acacia (#1756)

175 of 409 new or added lines in 26 files covered. (42.79%)

3 existing lines in 3 files now uncovered.

3547 of 5665 relevant lines covered (62.61%)

2.46 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)
42✔
119
    {
120
        list($id, $this->_retrieveOptions) = Util\Util::normalizeId($id);
42✔
121
        $this->_opts = Util\RequestOptions::parse($opts);
42✔
122
        $this->_originalValues = [];
42✔
123
        $this->_values = [];
42✔
124
        $this->_unsavedValues = new Util\Set();
42✔
125
        $this->_transientValues = new Util\Set();
42✔
126
        if (null !== $id) {
42✔
127
            $this->_values['id'] = $id;
9✔
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)
10✔
172
    {
173
        // function should return a reference, using $nullval to return a reference to null
174
        $nullval = null;
10✔
175
        if (!empty($this->_values) && \array_key_exists($k, $this->_values)) {
10✔
176
            return $this->_values[$k];
9✔
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}. "
×
NEW
182
                . "HINT: The {$k} attribute was set in the past, however. "
×
NEW
183
                . 'It was then wiped when refreshing the object '
×
NEW
184
                . "with the result returned by Stripe's API, "
×
NEW
185
                . 'probably as a result of a save(). The attributes currently '
×
NEW
186
                . "available on this object are: {$attrs}";
×
UNCOV
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]
5✔
240
    public function offsetGet($k)
241
    {
242
        return \array_key_exists($k, $this->_values) ? $this->_values[$k] : null;
5✔
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
     * @param 'v1'|'v2' $apiMode
270
     *
271
     * @return static the object constructed from the given values
272
     */
273
    public static function constructFrom($values, $opts = null, $apiMode = 'v1')
28✔
274
    {
275
        $obj = new static(isset($values['id']) ? $values['id'] : null);
28✔
276
        $obj->refreshFrom($values, $opts, false, $apiMode);
28✔
277

278
        return $obj;
28✔
279
    }
280

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

293
        $this->_originalValues = self::deepCopy($values);
28✔
294

295
        if ($values instanceof StripeObject) {
28✔
296
            $values = $values->toArray();
×
297
        }
298

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

308
        foreach ($removed->toArray() as $k) {
28✔
309
            unset($this->{$k});
×
310
        }
311

312
        $this->updateAttributes($values, $opts, false, $apiMode);
28✔
313
        foreach ($values as $k => $v) {
28✔
314
            $this->_transientValues->discard($k);
26✔
315
            $this->_unsavedValues->discard($k);
26✔
316
        }
317
    }
318

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

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

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

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

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

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

450
            return $update;
10✔
451
        } else {
452
            return $value;
14✔
453
        }
454
    }
455

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

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

478
            return \is_object($value) && \method_exists($value, 'toArray') ? $value->toArray() : $value;
5✔
479
        };
5✔
480

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

492
            return $acc;
5✔
493
        }, []);
5✔
494
    }
495

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

506
    public function __toString()
1✔
507
    {
508
        $class = static::class;
1✔
509

510
        return $class . ' JSON: ' . $this->toJSON();
1✔
511
    }
512

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

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

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

552
            return $copy;
28✔
553
        }
554
        if ($obj instanceof StripeObject) {
25✔
555
            return $obj::constructFrom(
10✔
556
                self::deepCopy($obj->_values),
10✔
557
                clone $obj->_opts
10✔
558
            );
10✔
559
        }
560

561
        return $obj;
25✔
562
    }
563

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

582
        return \array_fill_keys(\array_keys($values), '');
2✔
583
    }
584

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

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

603
    /**
604
     * Indicates whether or not the resource has been deleted on the server.
605
     * Note that some, but not all, resources can indicate whether they have
606
     * been deleted.
607
     *
608
     * @return bool whether the resource is deleted
609
     */
610
    public function isDeleted()
1✔
611
    {
612
        return isset($this->_values['deleted']) ? $this->_values['deleted'] : false;
1✔
613
    }
614
}
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