"old_value"] * * If we update the object with `metadata[new]=new_value`, the server side * object now has *both* fields: * * metadata = ["old" => "old_value", "new" => "new_value"] * * This is okay in itself because usually users will want to treat it as * additive: * * $obj->metadata["new"] = "new_value"; * $obj->save(); * * However, in other cases, they may want to replace the entire existing * contents: * * $obj->metadata = ["new" => "new_value"]; * $obj->save(); * * This is where things get a little bit tricky because in order to clear * any old keys that may have existed, we actually have to send an explicit * empty string to the server. So the operation above would have to send * this form to get the intended behavior: * * metadata[old]=&metadata[new]=new_value * * This method allows us to track which parameters are considered additive, * and lets us behave correctly where appropriate when serializing * parameters to be sent. * * @return Util\Set Set of additive parameters */ public static function getAdditiveParams() { static $additiveParams = null; if ($additiveParams === null) { // Set `metadata` as additive so that when it's set directly we remember // to clear keys that may have been previously set by sending empty // values for them. // // It's possible that not every object has `metadata`, but having this // option set when there is no `metadata` field is not harmful. $additiveParams = new Util\Set([ 'metadata', ]); } return $additiveParams; } public function __construct($id = null, $opts = null) { list($id, $this->_retrieveOptions) = Util\Util::normalizeId($id); $this->_opts = Util\RequestOptions::parse($opts); $this->_originalValues = []; $this->_values = []; $this->_unsavedValues = new Util\Set(); $this->_transientValues = new Util\Set(); if ($id !== null) { $this->_values['id'] = $id; } } // Standard accessor magic methods public function __set($k, $v) { if (static::getPermanentAttributes()->includes($k)) { throw new \InvalidArgumentException( "Cannot set $k on this object. HINT: you can't set: " . join(', ', static::getPermanentAttributes()->toArray()) ); } if ($v === "") { throw new \InvalidArgumentException( 'You cannot set \''.$k.'\'to an empty string. ' .'We interpret empty strings as NULL in requests. ' .'You may set obj->'.$k.' = NULL to delete the property' ); } $this->_values[$k] = Util\Util::convertToTelnyxObject($v, $this->_opts); $this->dirtyValue($this->_values[$k]); $this->_unsavedValues->add($k); } public function __isset($k) { return isset($this->_values[$k]); } public function __unset($k) { unset($this->_values[$k]); $this->_transientValues->add($k); $this->_unsavedValues->discard($k); } public function &__get($k) { // function should return a reference, using $nullval to return a reference to null $nullval = null; if (!empty($this->_values) && array_key_exists($k, $this->_values)) { return $this->_values[$k]; } elseif (!empty($this->_transientValues) && $this->_transientValues->includes($k)) { $class = get_class($this); $attrs = join(', ', array_keys($this->_values)); $message = "Telnyx Notice: Undefined property of $class instance: $k. " . "HINT: The $k attribute was set in the past, however. " . "It was then wiped when refreshing the object " . "with the result returned by Telnyx's API, " . "probably as a result of a save(). The attributes currently " . "available on this object are: $attrs"; Telnyx::getLogger()->error($message); return $nullval; } else { $class = get_class($this); Telnyx::getLogger()->error("Telnyx Notice: Undefined property of $class instance: $k"); return $nullval; } } // Magic method for var_dump output. Only works with PHP >= 5.6 public function __debugInfo() { return $this->_values; } // ArrayAccess methods public function offsetSet($k, $v) { $this->$k = $v; } public function offsetExists($k) { return array_key_exists($k, $this->_values); } public function offsetUnset($k) { unset($this->$k); } public function offsetGet($k) { return array_key_exists($k, $this->_values) ? $this->_values[$k] : null; } // Countable method public function count() { return count($this->_values); } public function keys() { return array_keys($this->_values); } public function values() { return array_values($this->_values); } /** * This unfortunately needs to be public to be used in Util\Util * * @param array $values * @param null|string|array|Util\RequestOptions $opts * * @return static The object constructed from the given values. */ public static function constructFrom($values, $opts = null) { $obj = new static(isset($values['id']) ? $values['id'] : null); $obj->refreshFrom($values, $opts); return $obj; } /** * Refreshes this object using the provided values. * * @param array $values * @param null|string|array|Util\RequestOptions $opts * @param boolean $partial Defaults to false. */ public function refreshFrom($values, $opts, $partial = false) { $this->_opts = Util\RequestOptions::parse($opts); $this->_originalValues = self::deepCopy($values); if ($values instanceof TelnyxObject) { $values = $values->__toArray(true); } // Wipe old state before setting new. This is useful for e.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 TelnyxObject // 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") && (is_array($v))) { $this->_values[$k] = TelnyxObject::constructFrom($v, $opts); } else { $this->_values[$k] = Util\Util::convertToTelnyxObject($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 TelnyxObject. A TelnyxObject may contain modified // values within in that its parent TelnyxObject 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 TelnyxObject) { $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::convertToTelnyxObject($value, $this->_opts)->serializeParameters(); } } elseif ($value instanceof TelnyxObject) { $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::convertTelnyxObjectToArray($this->_values); } else { return $this->_values; } } /** * Sets all keys within the TelnyxObject as unsaved so that they will be * included with an update when `serializeParameters` is called. This * method is also recursive, so any TelnyxObjects 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 TelnyxObject) { $value->dirty(); } } /** * Produces a deep copy of the given object including support for arrays * and TelnyxObjects. */ 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 TelnyxObject) { 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 * TelnyxObject. */ public static function emptyValues($obj) { if (is_array($obj)) { $values = $obj; } elseif ($obj instanceof TelnyxObject) { $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 Telnyx API */ public function getLastResponse() { return $this->_lastResponse; } /** * Sets the last response from the Telnyx API * * @param ApiResponse $resp * @return void */ public function setLastResponse($resp) { $this->_lastResponse = $resp; } /** * Indicates whether or not the resource has been deleted on the server. * Note that some, but not all, resources can indicate whether they have * been deleted. * * @return bool Whether the resource is deleted. */ public function isDeleted() { return isset($this->_values['deleted']) ? $this->_values['deleted'] : false; } }