10 class StripeObject
implements \ArrayAccess
, \Countable
, \JsonSerializable
13 protected $_originalValues;
15 protected $_unsavedValues;
16 protected $_transientValues;
17 protected $_retrieveOptions;
18 protected $_lastResponse;
21 * @return Util\Set Attributes that should not be sent to the API because
22 * they're not updatable (e.g. ID).
24 public static function getPermanentAttributes()
26 static $permanentAttributes = null;
27 if ($permanentAttributes === null) {
28 $permanentAttributes = new Util\
Set([
32 return $permanentAttributes;
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:
41 * source[object]=card&source[number]=123
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.
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
53 * metadata = ["old" => "old_value"]
55 * If we update the object with `metadata[new]=new_value`, the server side
56 * object now has *both* fields:
58 * metadata = ["old" => "old_value", "new" => "new_value"]
60 * This is okay in itself because usually users will want to treat it as
63 * $obj->metadata["new"] = "new_value";
66 * However, in other cases, they may want to replace the entire existing
69 * $obj->metadata = ["new" => "new_value"];
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:
77 * metadata[old]=&metadata[new]=new_value
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.
83 * @return Util\Set Set of additive parameters
85 public static function getAdditiveParams()
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
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([
99 return $additiveParams;
102 public function __construct($id = null, $opts = null)
104 list($id, $this->_retrieveOptions
) = Util\Util
::normalizeId($id);
105 $this->_opts
= Util\RequestOptions
::parse($opts);
106 $this->_originalValues
= [];
108 $this->_unsavedValues
= new Util\
Set();
109 $this->_transientValues
= new Util\
Set();
111 $this->_values
['id'] = $id;
115 // Standard accessor magic methods
116 public function __set($k, $v)
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())
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'
133 $this->_values
[$k] = Util\Util
::convertToStripeObject($v, $this->_opts
);
134 $this->dirtyValue($this->_values
[$k]);
135 $this->_unsavedValues
->add($k);
138 public function __isset($k)
140 return isset($this->_values
[$k]);
143 public function __unset($k)
145 unset($this->_values
[$k]);
146 $this->_transientValues
->add($k);
147 $this->_unsavedValues
->discard($k);
150 public function &__get($k)
152 // function should return a reference, using $nullval to return a reference to 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);
168 $class = get_class($this);
169 Stripe
::getLogger()->error("Stripe Notice: Undefined property of $class instance: $k");
174 // Magic method for var_dump output. Only works with PHP >= 5.6
175 public function __debugInfo()
177 return $this->_values
;
180 // ArrayAccess methods
181 public function offsetSet($k, $v)
186 public function offsetExists($k)
188 return array_key_exists($k, $this->_values
);
191 public function offsetUnset($k)
196 public function offsetGet($k)
198 return array_key_exists($k, $this->_values
) ?
$this->_values
[$k] : null;
202 public function count()
204 return count($this->_values
);
207 public function keys()
209 return array_keys($this->_values
);
212 public function values()
214 return array_values($this->_values
);
218 * This unfortunately needs to be public to be used in Util\Util
220 * @param array $values
221 * @param null|string|array|Util\RequestOptions $opts
223 * @return StripeObject The object constructed from the given values.
225 public static function constructFrom($values, $opts = null)
227 $obj = new static(isset($values['id']) ?
$values['id'] : null);
228 $obj->refreshFrom($values, $opts);
233 * Refreshes this object using the provided values.
235 * @param array $values
236 * @param null|string|array|Util\RequestOptions $opts
237 * @param boolean $partial Defaults to false.
239 public function refreshFrom($values, $opts, $partial = false)
241 $this->_opts
= Util\RequestOptions
::parse($opts);
243 $this->_originalValues
= self
::deepCopy($values);
245 if ($values instanceof StripeObject
) {
246 $values = $values->__toArray(true);
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
253 $removed = new Util\
Set();
255 $removed = new Util\
Set(array_diff(array_keys($this->_values
), array_keys($values)));
258 foreach ($removed->toArray() as $k) {
262 $this->updateAttributes($values, $opts, false);
263 foreach ($values as $k => $v) {
264 $this->_transientValues
->discard($k);
265 $this->_unsavedValues
->discard($k);
270 * Mass assigns attributes on the model.
272 * @param array $values
273 * @param null|string|array|Util\RequestOptions $opts
274 * @param boolean $dirty Defaults to true.
276 public function updateAttributes($values, $opts = null, $dirty = true)
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") {
284 $this->_values
[$k] = StripeObject
::constructFrom($v, $opts);
286 $this->_values
[$k] = Util\Util
::convertToStripeObject($v, $opts);
289 $this->dirtyValue($this->_values
[$k]);
291 $this->_unsavedValues
->add($k);
296 * @return array A recursive mapping of attributes to values for this object,
297 * including the proper value for deleted attributes.
299 public function serializeParameters($force = false)
303 foreach ($this->_values
as $k => $v) {
304 // There are a few reasons that we may want to add in a parameter for
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.
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(
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(
334 return $updateParams;
338 public function serializeParamsValue($value, $original, $unsaved, $force, $key = null)
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.
346 // There are _some_ known exceptions though.
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
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.
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) {
364 } elseif (($value instanceof APIResource
) && (!$value->saveWithParent
)) {
367 } elseif (isset($value->id
)) {
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`."
376 } elseif (is_array($value)) {
377 if (Util\Util
::isList($value)) {
378 // Sequential array, i.e. a list
380 foreach ($value as $v) {
381 array_push($update, $this->serializeParamsValue($v, null, true, $force));
383 // This prevents an array that's unchanged from being resent.
384 if ($update !== $this->serializeParamsValue($original, null, true, $force, $key)) {
388 // Associative array, i.e. a map
389 return Util\Util
::convertToStripeObject($value, $this->_opts
)->serializeParameters();
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);
402 public function jsonSerialize()
404 return $this->__toArray(true);
407 public function __toJSON()
409 return json_encode($this->__toArray(true), JSON_PRETTY_PRINT
);
412 public function __toString()
414 $class = get_class($this);
415 return $class . ' JSON: ' . $this->__toJSON();
418 public function __toArray($recursive = false)
421 return Util\Util
::convertStripeObjectToArray($this->_values
);
423 return $this->_values
;
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.
433 public function dirty()
435 $this->_unsavedValues
= new Util\
Set(array_keys($this->_values
));
436 foreach ($this->_values
as $k => $v) {
437 $this->dirtyValue($v);
441 protected function dirtyValue($value)
443 if (is_array($value)) {
444 foreach ($value as $v) {
445 $this->dirtyValue($v);
447 } elseif ($value instanceof StripeObject
) {
453 * Produces a deep copy of the given object including support for arrays
456 protected static function deepCopy($obj)
458 if (is_array($obj)) {
460 foreach ($obj as $k => $v) {
461 $copy[$k] = self
::deepCopy($v);
464 } elseif ($obj instanceof StripeObject
) {
465 return $obj::constructFrom(
466 self
::deepCopy($obj->_values
),
475 * Returns a hash of empty values for all the values that are in the given
478 public static function emptyValues($obj)
480 if (is_array($obj)) {
482 } elseif ($obj instanceof StripeObject
) {
483 $values = $obj->_values
;
485 throw new \
InvalidArgumentException(
486 "empty_values got got unexpected object type: " . get_class($obj)
489 $update = array_fill_keys(array_keys($values), "");
494 * @return object The last response from the Stripe API
496 public function getLastResponse()
498 return $this->_lastResponse
;
504 * @return void Set the last response from the Stripe API
506 public function setLastResponse($resp)
508 $this->_lastResponse
= $resp;