<?php
namespace Stripe\StripeTaxForWooCommerce\SDK\lib;
/**
* Class StripeObject.
*
* @property null|string $id
*/
class StripeObject implements \ArrayAccess, \Countable, \JsonSerializable {
/** @var Util\RequestOptions */
protected $_opts;
/** @var array */
protected $_originalValues;
/** @var array */
protected $_values;
/** @var Util\Set */
protected $_unsavedValues;
/** @var Util\Set */
protected $_transientValues;
/** @var null|array */
protected $_retrieveOptions;
/** @var null|ApiResponse */
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 ( null === $permanentAttributes ) {
$permanentAttributes = new Util\Set(
array(
'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 ( null === $additiveParams ) {
// 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(
array(
'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 = array();
$this->_values = array();
$this->_unsavedValues = new Util\Set();
$this->_transientValues = new Util\Set();
if ( null !== $id ) {
$this->_values['id'] = $id;
}
}
// Standard accessor magic methods
public function __set( $k, $v ) {
if ( static::getPermanentAttributes()->includes( $k ) ) {
throw new Exception\InvalidArgumentException(
"Cannot set {$k} on this object. HINT: you can't set: " .
\implode( ', ', static::getPermanentAttributes()->toArray() )
);
}
if ( '' === $v ) {
throw new Exception\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::convertToStripeObject( $v, $this->_opts );
$this->dirtyValue( $this->_values[ $k ] );
$this->_unsavedValues->add( $k );
}
/**
* @param mixed $k
*
* @return bool
*/
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 ];
}
if ( ! empty( $this->_transientValues ) && $this->_transientValues->includes( $k ) ) {
$class = static::class;
$attrs = \implode( ', ', \array_keys( $this->_values ) );
$message = "Stripe\StripeTaxForWooCommerce\SDK\lib 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 Stripe\StripeTaxForWooCommerce\SDK\lib's API, "
. 'probably as a result of a save(). The attributes currently '
. "available on this object are: {$attrs}";
Stripe::getLogger()->error( $message );
return $nullval;
}
$class = static::class;
Stripe::getLogger()->error( "Stripe\StripeTaxForWooCommerce\SDK\lib Notice: Undefined property of {$class} instance: {$k}" );
return $nullval;
}
/**
* Magic method for var_dump output. Only works with PHP >= 5.6.
*
* @return array
*/
public function __debugInfo() {
return $this->_values;
}
// ArrayAccess methods
/**
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet( $k, $v ) {
$this->{$k} = $v;
}
/**
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists( $k ) {
return \array_key_exists( $k, $this->_values );
}
/**
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset( $k ) {
unset( $this->{$k} );
}
/**
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet( $k ) {
return \array_key_exists( $k, $this->_values ) ? $this->_values[ $k ] : null;
}
/**
* @return int
*/
#[\ReturnTypeWillChange]
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|array|string|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|array|string|Util\RequestOptions $opts
* @param bool $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 StripeObject ) {
$values = $values->toArray();
}
// 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|array|string|Util\RequestOptions $opts
* @param bool $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 ( ( 'metadata' === $k ) && ( \is_array( $v ) ) ) {
$this->_values[ $k ] = self::constructFrom( $v, $opts );
} else {
$this->_values[ $k ] = Util\Util::convertToStripeObject( $v, $opts );
}
if ( $dirty ) {
$this->dirtyValue( $this->_values[ $k ] );
}
$this->_unsavedValues->add( $k );
}
}
/**
* @param bool $force defaults to false
*
* @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 = array();
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
return \array_filter(
$updateParams,
function ( $v ) {
return null !== $v;
}
);
}
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 ( null === $value ) {
return '';
}
if ( ( $value instanceof ApiResource ) && ( ! $value->saveWithParent ) ) {
if ( ! $unsaved ) {
return null;
}
if ( isset( $value->id ) ) {
return $value;
}
throw new Exception\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`.'
);
}
if ( \is_array( $value ) ) {
if ( Util\Util::isList( $value ) ) {
// Sequential array, i.e. a list
$update = array();
foreach ( $value as $v ) {
$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;
}
}
/**
* @return mixed
*/
#[\ReturnTypeWillChange]
public function jsonSerialize() {
return $this->toArray();
}
/**
* Returns an associative array with the key and values composing the
* Stripe\StripeTaxForWooCommerce\SDK\lib object.
*
* @return array the associative array
*/
public function toArray() {
$maybeToArray = function ( $value ) {
if ( null === $value ) {
return null;
}
return \is_object( $value ) && \method_exists( $value, 'toArray' ) ? $value->toArray() : $value;
};
return \array_reduce(
\array_keys( $this->_values ),
function ( $acc, $k ) use ( $maybeToArray ) {
if ( '_' === \substr( (string) $k, 0, 1 ) ) {
return $acc;
}
$v = $this->_values[ $k ];
if ( Util\Util::isList( $v ) ) {
$acc[ $k ] = \array_map( $maybeToArray, $v );
} else {
$acc[ $k ] = $maybeToArray( $v );
}
return $acc;
},
array()
);
}
/**
* Returns a pretty JSON representation of the Stripe\StripeTaxForWooCommerce\SDK\lib object.
*
* @return string the JSON representation of the Stripe\StripeTaxForWooCommerce\SDK\lib object
*/
public function toJSON() {
return \json_encode( $this->toArray(), \JSON_PRETTY_PRINT );
}
public function __toString() {
$class = static::class;
return $class . ' JSON: ' . $this->toJSON();
}
/**
* 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.
*
* @param mixed $obj
*/
protected static function deepCopy( $obj ) {
if ( \is_array( $obj ) ) {
$copy = array();
foreach ( $obj as $k => $v ) {
$copy[ $k ] = self::deepCopy( $v );
}
return $copy;
}
if ( $obj instanceof StripeObject ) {
return $obj::constructFrom(
self::deepCopy( $obj->_values ),
clone $obj->_opts
);
}
return $obj;
}
/**
* Returns a hash of empty values for all the values that are in the given
* StripeObject.
*
* @param mixed $obj
*/
public static function emptyValues( $obj ) {
if ( \is_array( $obj ) ) {
$values = $obj;
} elseif ( $obj instanceof StripeObject ) {
$values = $obj->_values;
} else {
throw new Exception\InvalidArgumentException(
'empty_values got unexpected object type: ' . \get_class( $obj )
);
}
return \array_fill_keys( \array_keys( $values ), '' );
}
/**
* @return null|ApiResponse The last response from the Stripe\StripeTaxForWooCommerce\SDK\lib API
*/
public function getLastResponse() {
return $this->_lastResponse;
}
/**
* Sets the last response from the Stripe\StripeTaxForWooCommerce\SDK\lib API.
*
* @param ApiResponse $resp
*/
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;
}
}