StripeObject.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. <?php
  2. namespace Stripe;
  3. /**
  4. * Class StripeObject
  5. *
  6. * @package Stripe
  7. */
  8. class StripeObject implements \ArrayAccess, \Countable, \JsonSerializable
  9. {
  10. protected $_opts;
  11. protected $_originalValues;
  12. protected $_values;
  13. protected $_unsavedValues;
  14. protected $_transientValues;
  15. protected $_retrieveOptions;
  16. protected $_lastResponse;
  17. /**
  18. * @return Util\Set Attributes that should not be sent to the API because
  19. * they're not updatable (e.g. ID).
  20. */
  21. public static function getPermanentAttributes()
  22. {
  23. static $permanentAttributes = null;
  24. if ($permanentAttributes === null) {
  25. $permanentAttributes = new Util\Set([
  26. 'id',
  27. ]);
  28. }
  29. return $permanentAttributes;
  30. }
  31. /**
  32. * Additive objects are subobjects in the API that don't have the same
  33. * semantics as most subobjects, which are fully replaced when they're set.
  34. * This is best illustrated by example. The `source` parameter sent when
  35. * updating a subscription is *not* additive; if we set it:
  36. *
  37. * source[object]=card&source[number]=123
  38. *
  39. * We expect the old `source` object to have been overwritten completely. If
  40. * the previous source had an `address_state` key associated with it and we
  41. * didn't send one this time, that value of `address_state` is gone.
  42. *
  43. * By contrast, additive objects are those that will have new data added to
  44. * them while keeping any existing data in place. The only known case of its
  45. * use is for `metadata`, but it could in theory be more general. As an
  46. * example, say we have a `metadata` object that looks like this on the
  47. * server side:
  48. *
  49. * metadata = ["old" => "old_value"]
  50. *
  51. * If we update the object with `metadata[new]=new_value`, the server side
  52. * object now has *both* fields:
  53. *
  54. * metadata = ["old" => "old_value", "new" => "new_value"]
  55. *
  56. * This is okay in itself because usually users will want to treat it as
  57. * additive:
  58. *
  59. * $obj->metadata["new"] = "new_value";
  60. * $obj->save();
  61. *
  62. * However, in other cases, they may want to replace the entire existing
  63. * contents:
  64. *
  65. * $obj->metadata = ["new" => "new_value"];
  66. * $obj->save();
  67. *
  68. * This is where things get a little bit tricky because in order to clear
  69. * any old keys that may have existed, we actually have to send an explicit
  70. * empty string to the server. So the operation above would have to send
  71. * this form to get the intended behavior:
  72. *
  73. * metadata[old]=&metadata[new]=new_value
  74. *
  75. * This method allows us to track which parameters are considered additive,
  76. * and lets us behave correctly where appropriate when serializing
  77. * parameters to be sent.
  78. *
  79. * @return Util\Set Set of additive parameters
  80. */
  81. public static function getAdditiveParams()
  82. {
  83. static $additiveParams = null;
  84. if ($additiveParams === null) {
  85. // Set `metadata` as additive so that when it's set directly we remember
  86. // to clear keys that may have been previously set by sending empty
  87. // values for them.
  88. //
  89. // It's possible that not every object has `metadata`, but having this
  90. // option set when there is no `metadata` field is not harmful.
  91. $additiveParams = new Util\Set([
  92. 'metadata',
  93. ]);
  94. }
  95. return $additiveParams;
  96. }
  97. public function __construct($id = null, $opts = null)
  98. {
  99. list($id, $this->_retrieveOptions) = Util\Util::normalizeId($id);
  100. $this->_opts = Util\RequestOptions::parse($opts);
  101. $this->_originalValues = [];
  102. $this->_values = [];
  103. $this->_unsavedValues = new Util\Set();
  104. $this->_transientValues = new Util\Set();
  105. if ($id !== null) {
  106. $this->_values['id'] = $id;
  107. }
  108. }
  109. // Standard accessor magic methods
  110. public function __set($k, $v)
  111. {
  112. if (static::getPermanentAttributes()->includes($k)) {
  113. throw new \InvalidArgumentException(
  114. "Cannot set $k on this object. HINT: you can't set: " .
  115. join(', ', static::getPermanentAttributes()->toArray())
  116. );
  117. }
  118. if ($v === "") {
  119. throw new \InvalidArgumentException(
  120. 'You cannot set \''.$k.'\'to an empty string. '
  121. .'We interpret empty strings as NULL in requests. '
  122. .'You may set obj->'.$k.' = NULL to delete the property'
  123. );
  124. }
  125. $this->_values[$k] = Util\Util::convertToStripeObject($v, $this->_opts);
  126. $this->dirtyValue($this->_values[$k]);
  127. $this->_unsavedValues->add($k);
  128. }
  129. public function __isset($k)
  130. {
  131. return isset($this->_values[$k]);
  132. }
  133. public function __unset($k)
  134. {
  135. unset($this->_values[$k]);
  136. $this->_transientValues->add($k);
  137. $this->_unsavedValues->discard($k);
  138. }
  139. public function &__get($k)
  140. {
  141. // function should return a reference, using $nullval to return a reference to null
  142. $nullval = null;
  143. if (!empty($this->_values) && array_key_exists($k, $this->_values)) {
  144. return $this->_values[$k];
  145. } else if (!empty($this->_transientValues) && $this->_transientValues->includes($k)) {
  146. $class = get_class($this);
  147. $attrs = join(', ', array_keys($this->_values));
  148. $message = "Stripe Notice: Undefined property of $class instance: $k. "
  149. . "HINT: The $k attribute was set in the past, however. "
  150. . "It was then wiped when refreshing the object "
  151. . "with the result returned by Stripe's API, "
  152. . "probably as a result of a save(). The attributes currently "
  153. . "available on this object are: $attrs";
  154. Stripe::getLogger()->error($message);
  155. return $nullval;
  156. } else {
  157. $class = get_class($this);
  158. Stripe::getLogger()->error("Stripe Notice: Undefined property of $class instance: $k");
  159. return $nullval;
  160. }
  161. }
  162. // Magic method for var_dump output. Only works with PHP >= 5.6
  163. public function __debugInfo()
  164. {
  165. return $this->_values;
  166. }
  167. // ArrayAccess methods
  168. public function offsetSet($k, $v)
  169. {
  170. $this->$k = $v;
  171. }
  172. public function offsetExists($k)
  173. {
  174. return array_key_exists($k, $this->_values);
  175. }
  176. public function offsetUnset($k)
  177. {
  178. unset($this->$k);
  179. }
  180. public function offsetGet($k)
  181. {
  182. return array_key_exists($k, $this->_values) ? $this->_values[$k] : null;
  183. }
  184. // Countable method
  185. public function count()
  186. {
  187. return count($this->_values);
  188. }
  189. public function keys()
  190. {
  191. return array_keys($this->_values);
  192. }
  193. public function values()
  194. {
  195. return array_values($this->_values);
  196. }
  197. /**
  198. * This unfortunately needs to be public to be used in Util\Util
  199. *
  200. * @param array $values
  201. * @param null|string|array|Util\RequestOptions $opts
  202. *
  203. * @return StripeObject The object constructed from the given values.
  204. */
  205. public static function constructFrom($values, $opts = null)
  206. {
  207. $obj = new static(isset($values['id']) ? $values['id'] : null);
  208. $obj->refreshFrom($values, $opts);
  209. return $obj;
  210. }
  211. /**
  212. * Refreshes this object using the provided values.
  213. *
  214. * @param array $values
  215. * @param null|string|array|Util\RequestOptions $opts
  216. * @param boolean $partial Defaults to false.
  217. */
  218. public function refreshFrom($values, $opts, $partial = false)
  219. {
  220. $this->_opts = Util\RequestOptions::parse($opts);
  221. $this->_originalValues = self::deepCopy($values);
  222. if ($values instanceof StripeObject) {
  223. $values = $values->__toArray(true);
  224. }
  225. // Wipe old state before setting new. This is useful for e.g. updating a
  226. // customer, where there is no persistent card parameter. Mark those values
  227. // which don't persist as transient
  228. if ($partial) {
  229. $removed = new Util\Set();
  230. } else {
  231. $removed = new Util\Set(array_diff(array_keys($this->_values), array_keys($values)));
  232. }
  233. foreach ($removed->toArray() as $k) {
  234. unset($this->$k);
  235. }
  236. $this->updateAttributes($values, $opts, false);
  237. foreach ($values as $k => $v) {
  238. $this->_transientValues->discard($k);
  239. $this->_unsavedValues->discard($k);
  240. }
  241. }
  242. /**
  243. * Mass assigns attributes on the model.
  244. *
  245. * @param array $values
  246. * @param null|string|array|Util\RequestOptions $opts
  247. * @param boolean $dirty Defaults to true.
  248. */
  249. public function updateAttributes($values, $opts = null, $dirty = true)
  250. {
  251. foreach ($values as $k => $v) {
  252. // Special-case metadata to always be cast as a StripeObject
  253. // This is necessary in case metadata is empty, as PHP arrays do
  254. // not differentiate between lists and hashes, and we consider
  255. // empty arrays to be lists.
  256. if ($k === "metadata") {
  257. $this->_values[$k] = StripeObject::constructFrom($v, $opts);
  258. } else {
  259. $this->_values[$k] = Util\Util::convertToStripeObject($v, $opts);
  260. }
  261. if ($dirty) {
  262. $this->dirtyValue($this->_values[$k]);
  263. }
  264. $this->_unsavedValues->add($k);
  265. }
  266. }
  267. /**
  268. * @return array A recursive mapping of attributes to values for this object,
  269. * including the proper value for deleted attributes.
  270. */
  271. public function serializeParameters($force = false)
  272. {
  273. $updateParams = [];
  274. foreach ($this->_values as $k => $v) {
  275. // There are a few reasons that we may want to add in a parameter for
  276. // update:
  277. //
  278. // 1. The `$force` option has been set.
  279. // 2. We know that it was modified.
  280. // 3. Its value is a StripeObject. A StripeObject may contain modified
  281. // values within in that its parent StripeObject doesn't know about.
  282. //
  283. $original = array_key_exists($k, $this->_originalValues) ? $this->_originalValues[$k] : null;
  284. $unsaved = $this->_unsavedValues->includes($k);
  285. if ($force || $unsaved || $v instanceof StripeObject) {
  286. $updateParams[$k] = $this->serializeParamsValue(
  287. $this->_values[$k],
  288. $original,
  289. $unsaved,
  290. $force,
  291. $k
  292. );
  293. }
  294. }
  295. // a `null` that makes it out of `serializeParamsValue` signals an empty
  296. // value that we shouldn't appear in the serialized form of the object
  297. $updateParams = array_filter(
  298. $updateParams,
  299. function ($v) {
  300. return $v !== null;
  301. }
  302. );
  303. return $updateParams;
  304. }
  305. public function serializeParamsValue($value, $original, $unsaved, $force, $key = null)
  306. {
  307. // The logic here is that essentially any object embedded in another
  308. // object that had a `type` is actually an API resource of a different
  309. // type that's been included in the response. These other resources must
  310. // be updated from their proper endpoints, and therefore they are not
  311. // included when serializing even if they've been modified.
  312. //
  313. // There are _some_ known exceptions though.
  314. //
  315. // For example, if the value is unsaved (meaning the user has set it), and
  316. // it looks like the API resource is persisted with an ID, then we include
  317. // the object so that parameters are serialized with a reference to its
  318. // ID.
  319. //
  320. // Another example is that on save API calls it's sometimes desirable to
  321. // update a customer's default source by setting a new card (or other)
  322. // object with `->source=` and then saving the customer. The
  323. // `saveWithParent` flag to override the default behavior allows us to
  324. // handle these exceptions.
  325. //
  326. // We throw an error if a property was set explicitly but we can't do
  327. // anything with it because the integration is probably not working as the
  328. // user intended it to.
  329. if ($value === null) {
  330. return "";
  331. } elseif (($value instanceof APIResource) && (!$value->saveWithParent)) {
  332. if (!$unsaved) {
  333. return null;
  334. } elseif (isset($value->id)) {
  335. return $value;
  336. } else {
  337. throw new \InvalidArgumentException(
  338. "Cannot save property `$key` containing an API resource of type " .
  339. get_class($value) . ". It doesn't appear to be persisted and is " .
  340. "not marked as `saveWithParent`."
  341. );
  342. }
  343. } elseif (is_array($value)) {
  344. if (Util\Util::isList($value)) {
  345. // Sequential array, i.e. a list
  346. $update = [];
  347. foreach ($value as $v) {
  348. array_push($update, $this->serializeParamsValue($v, null, true, $force));
  349. }
  350. // This prevents an array that's unchanged from being resent.
  351. if ($update !== $this->serializeParamsValue($original, null, true, $force, $key)) {
  352. return $update;
  353. }
  354. } else {
  355. // Associative array, i.e. a map
  356. return Util\Util::convertToStripeObject($value, $this->_opts)->serializeParameters();
  357. }
  358. } elseif ($value instanceof StripeObject) {
  359. $update = $value->serializeParameters($force);
  360. if ($original && $unsaved && $key && static::getAdditiveParams()->includes($key)) {
  361. $update = array_merge(self::emptyValues($original), $update);
  362. }
  363. return $update;
  364. } else {
  365. return $value;
  366. }
  367. }
  368. public function jsonSerialize()
  369. {
  370. return $this->__toArray(true);
  371. }
  372. public function __toJSON()
  373. {
  374. return json_encode($this->__toArray(true), JSON_PRETTY_PRINT);
  375. }
  376. public function __toString()
  377. {
  378. $class = get_class($this);
  379. return $class . ' JSON: ' . $this->__toJSON();
  380. }
  381. public function __toArray($recursive = false)
  382. {
  383. if ($recursive) {
  384. return Util\Util::convertStripeObjectToArray($this->_values);
  385. } else {
  386. return $this->_values;
  387. }
  388. }
  389. /**
  390. * Sets all keys within the StripeObject as unsaved so that they will be
  391. * included with an update when `serializeParameters` is called. This
  392. * method is also recursive, so any StripeObjects contained as values or
  393. * which are values in a tenant array are also marked as dirty.
  394. */
  395. public function dirty()
  396. {
  397. $this->_unsavedValues = new Util\Set(array_keys($this->_values));
  398. foreach ($this->_values as $k => $v) {
  399. $this->dirtyValue($v);
  400. }
  401. }
  402. protected function dirtyValue($value)
  403. {
  404. if (is_array($value)) {
  405. foreach ($value as $v) {
  406. $this->dirtyValue($v);
  407. }
  408. } elseif ($value instanceof StripeObject) {
  409. $value->dirty();
  410. }
  411. }
  412. /**
  413. * Produces a deep copy of the given object including support for arrays
  414. * and StripeObjects.
  415. */
  416. protected static function deepCopy($obj)
  417. {
  418. if (is_array($obj)) {
  419. $copy = [];
  420. foreach ($obj as $k => $v) {
  421. $copy[$k] = self::deepCopy($v);
  422. }
  423. return $copy;
  424. } elseif ($obj instanceof StripeObject) {
  425. return $obj::constructFrom(
  426. self::deepCopy($obj->_values),
  427. clone $obj->_opts
  428. );
  429. } else {
  430. return $obj;
  431. }
  432. }
  433. /**
  434. * Returns a hash of empty values for all the values that are in the given
  435. * StripeObject.
  436. */
  437. public static function emptyValues($obj)
  438. {
  439. if (is_array($obj)) {
  440. $values = $obj;
  441. } elseif ($obj instanceof StripeObject) {
  442. $values = $obj->_values;
  443. } else {
  444. throw new \InvalidArgumentException(
  445. "empty_values got got unexpected object type: " . get_class($obj)
  446. );
  447. }
  448. $update = array_fill_keys(array_keys($values), "");
  449. return $update;
  450. }
  451. /**
  452. * @return object The last response from the Stripe API
  453. */
  454. public function getLastResponse()
  455. {
  456. return $this->_lastResponse;
  457. }
  458. /**
  459. * @param ApiResponse
  460. *
  461. * @return void Set the last response from the Stripe API
  462. */
  463. public function setLastResponse($resp)
  464. {
  465. $this->_lastResponse = $resp;
  466. }
  467. /**
  468. * Indicates whether or not the resource has been deleted on the server.
  469. * Note that some, but not all, resources can indicate whether they have
  470. * been deleted.
  471. *
  472. * @return bool Whether the resource is deleted.
  473. */
  474. public function isDeleted()
  475. {
  476. return isset($this->_values['deleted']) ? $this->_values['deleted'] : false;
  477. }
  478. }