StripeObject.php 19 KB

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