a92694d2 |
<?php
namespace Telnyx;
/**
* Class TelnyxObject
*
* @package Telnyx
*/
class TelnyxObject implements \ArrayAccess, \Countable, \JsonSerializable
{
protected $_opts;
protected $_originalValues;
protected $_values;
protected $_unsavedValues;
protected $_transientValues;
protected $_retrieveOptions;
protected $_lastResponse;
/**
* @return Util\Set Attributes that should not be sent to the API because
* they're not updatable (e.g. ID).
*/
public static function getPermanentAttributes()
{
static $permanentAttributes = null;
if ($permanentAttributes === null) {
$permanentAttributes = new Util\Set([
'id',
]);
}
return $permanentAttributes;
}
/**
* Additive objects are subobjects in the API that don't have the same
* semantics as most subobjects, which are fully replaced when they're set.
* This is best illustrated by example. The `source` parameter sent when
* updating a subscription is *not* additive; if we set it:
*
* source[object]=card&source[number]=123
*
* We expect the old `source` object to have been overwritten completely. If
* the previous source had an `address_state` key associated with it and we
* didn't send one this time, that value of `address_state` is gone.
*
* By contrast, additive objects are those that will have new data added to
* them while keeping any existing data in place. The only known case of its
* use is for `metadata`, but it could in theory be more general. As an
* example, say we have a `metadata` object that looks like this on the
* server side:
*
* metadata = ["old" => "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;
}
}
|