g. updating a // customer, where there is no persistent card parameter. Mark those values // which don't persist as transient if ($partial) { $removed = new Util\Set(); } else { $removed = new Util\Set(array_diff(array_keys($this->_values), array_keys($values))); } foreach ($removed->toArray() as $k) { unset($this->$k); } $this->updateAttributes($values, $opts, false); foreach ($values as $k => $v) { $this->_transientValues->discard($k); $this->_unsavedValues->discard($k); } } /** * Mass assigns attributes on the model. * * @param array $values * @param null|string|array|Util\RequestOptions $opts * @param boolean $dirty Defaults to true. */ public function updateAttributes($values, $opts = null, $dirty = true) { foreach ($values as $k => $v) { // Special-case metadata to always be cast as a StripeObject // This is necessary in case metadata is empty, as PHP arrays do // not differentiate between lists and hashes, and we consider // empty arrays to be lists. if ($k === "metadata") { $this->_values[$k] = StripeObject::constructFrom($v, $opts); } else { $this->_values[$k] = Util\Util::convertToStripeObject($v, $opts); } if ($dirty) { $this->dirtyValue($this->_values[$k]); } $this->_unsavedValues->add($k); } } /** * @return array A recursive mapping of attributes to values for this object, * including the proper value for deleted attributes. */ public function serializeParameters($force = false) { $updateParams = []; foreach ($this->_values as $k => $v) { // There are a few reasons that we may want to add in a parameter for // update: // // 1. The `$force` option has been set. // 2. We know that it was modified. // 3. Its value is a StripeObject. A StripeObject may contain modified // values within in that its parent StripeObject doesn't know about. // $original = array_key_exists($k, $this->_originalValues) ? $this->_originalValues[$k] : null; $unsaved = $this->_unsavedValues->includes($k); if ($force || $unsaved || $v instanceof StripeObject) { $updateParams[$k] = $this->serializeParamsValue( $this->_values[$k], $original, $unsaved, $force, $k ); } } // a `null` that makes it out of `serializeParamsValue` signals an empty // value that we shouldn't appear in the serialized form of the object $updateParams = array_filter( $updateParams, function ($v) { return $v !== null; } ); return $updateParams; } public function serializeParamsValue($value, $original, $unsaved, $force, $key = null) { // The logic here is that essentially any object embedded in another // object that had a `type` is actually an API resource of a different // type that's been included in the response. These other resources must // be updated from their proper endpoints, and therefore they are not // included when serializing even if they've been modified. // // There are _some_ known exceptions though. // // For example, if the value is unsaved (meaning the user has set it), and // it looks like the API resource is persisted with an ID, then we include // the object so that parameters are serialized with a reference to its // ID. // // Another example is that on save API calls it's sometimes desirable to // update a customer's default source by setting a new card (or other) // object with `->source=` and then saving the customer. The // `saveWithParent` flag to override the default behavior allows us to // handle these exceptions. // // We throw an error if a property was set explicitly but we can't do // anything with it because the integration is probably not working as the // user intended it to. if ($value === null) { return ""; } elseif (($value instanceof APIResource) && (!$value->saveWithParent)) { if (!$unsaved) { return null; } elseif (isset($value->id)) { return $value; } else { throw new \InvalidArgumentException( "Cannot save property `$key` containing an API resource of type " . get_class($value) . ". It doesn't appear to be persisted and is " . "not marked as `saveWithParent`." ); } } elseif (is_array($value)) { if (Util\Util::isList($value)) { // Sequential array, i.e. a list $update = []; foreach ($value as $v) { array_push($update, $this->serializeParamsValue($v, null, true, $force)); } // This prevents an array that's unchanged from being resent. if ($update !== $this->serializeParamsValue($original, null, true, $force, $key)) { return $update; } } else { // Associative array, i.e. a map return Util\Util::convertToStripeObject($value, $this->_opts)->serializeParameters(); } } elseif ($value instanceof StripeObject) { $update = $value->serializeParameters($force); if ($original && $unsaved && $key && static::getAdditiveParams()->includes($key)) { $update = array_merge(self::emptyValues($original), $update); } return $update; } else { return $value; } } public function jsonSerialize() { return $this->__toArray(true); } public function __toJSON() { return json_encode($this->__toArray(true), JSON_PRETTY_PRINT); } public function __toString() { $class = get_class($this); return $class . ' JSON: ' . $this->__toJSON(); } public function __toArray($recursive = false) { if ($recursive) { return Util\Util::convertStripeObjectToArray($this->_values); } else { return $this->_values; } } /** * Sets all keys within the StripeObject as unsaved so that they will be * included with an update when `serializeParameters` is called. This * method is also recursive, so any StripeObjects contained as values or * which are values in a tenant array are also marked as dirty. */ public function dirty() { $this->_unsavedValues = new Util\Set(array_keys($this->_values)); foreach ($this->_values as $k => $v) { $this->dirtyValue($v); } } protected function dirtyValue($value) { if (is_array($value)) { foreach ($value as $v) { $this->dirtyValue($v); } } elseif ($value instanceof StripeObject) { $value->dirty(); } } /** * Produces a deep copy of the given object including support for arrays * and StripeObjects. */ protected static function deepCopy($obj) { if (is_array($obj)) { $copy = []; foreach ($obj as $k => $v) { $copy[$k] = self::deepCopy($v); } return $copy; } elseif ($obj instanceof StripeObject) { return $obj::constructFrom( self::deepCopy($obj->_values), clone $obj->_opts ); } else { return $obj; } } /** * Returns a hash of empty values for all the values that are in the given * StripeObject. */ public static function emptyValues($obj) { if (is_array($obj)) { $values = $obj; } elseif ($obj instanceof StripeObject) { $values = $obj->_values; } else { throw new \InvalidArgumentException( "empty_values got got unexpected object type: " . get_class($obj) ); } $update = array_fill_keys(array_keys($values), ""); return $update; } /** * @return object The last response from the Stripe API */ public function getLastResponse() { return $this->_lastResponse; } /** * @param ApiResponse * * @return void Set the last response from the Stripe API */ public function setLastResponse($resp) { $this->_lastResponse = $resp; } }