Collection.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. <?php
  2. namespace Stripe;
  3. /**
  4. * Class Collection.
  5. *
  6. * @template TStripeObject of StripeObject
  7. * @template-implements \IteratorAggregate<TStripeObject>
  8. *
  9. * @property string $object
  10. * @property string $url
  11. * @property bool $has_more
  12. * @property TStripeObject[] $data
  13. */
  14. class Collection extends StripeObject implements \Countable, \IteratorAggregate
  15. {
  16. const OBJECT_NAME = 'list';
  17. use ApiOperations\Request;
  18. /** @var array */
  19. protected $filters = [];
  20. /**
  21. * @return string the base URL for the given class
  22. */
  23. public static function baseUrl()
  24. {
  25. return Stripe::$apiBase;
  26. }
  27. /**
  28. * Returns the filters.
  29. *
  30. * @return array the filters
  31. */
  32. public function getFilters()
  33. {
  34. return $this->filters;
  35. }
  36. /**
  37. * Sets the filters, removing paging options.
  38. *
  39. * @param array $filters the filters
  40. */
  41. public function setFilters($filters)
  42. {
  43. $this->filters = $filters;
  44. }
  45. #[\ReturnTypeWillChange]
  46. public function offsetGet($k)
  47. {
  48. if (\is_string($k)) {
  49. return parent::offsetGet($k);
  50. }
  51. $msg = "You tried to access the {$k} index, but Collection " .
  52. 'types only support string keys. (HINT: List calls ' .
  53. 'return an object with a `data` (which is the data ' .
  54. "array). You likely want to call ->data[{$k}])";
  55. throw new Exception\InvalidArgumentException($msg);
  56. }
  57. /**
  58. * @param null|array $params
  59. * @param null|array|string $opts
  60. *
  61. * @throws Exception\ApiErrorException
  62. *
  63. * @return Collection<TStripeObject>
  64. */
  65. public function all($params = null, $opts = null)
  66. {
  67. self::_validateParams($params);
  68. list($url, $params) = $this->extractPathAndUpdateParams($params);
  69. list($response, $opts) = $this->_request('get', $url, $params, $opts);
  70. $obj = Util\Util::convertToStripeObject($response, $opts);
  71. if (!($obj instanceof \Stripe\Collection)) {
  72. throw new \Stripe\Exception\UnexpectedValueException(
  73. 'Expected type ' . \Stripe\Collection::class . ', got "' . \get_class($obj) . '" instead.'
  74. );
  75. }
  76. $obj->setFilters($params);
  77. return $obj;
  78. }
  79. /**
  80. * @param null|array $params
  81. * @param null|array|string $opts
  82. *
  83. * @throws Exception\ApiErrorException
  84. *
  85. * @return TStripeObject
  86. */
  87. public function create($params = null, $opts = null)
  88. {
  89. self::_validateParams($params);
  90. list($url, $params) = $this->extractPathAndUpdateParams($params);
  91. list($response, $opts) = $this->_request('post', $url, $params, $opts);
  92. return Util\Util::convertToStripeObject($response, $opts);
  93. }
  94. /**
  95. * @param string $id
  96. * @param null|array $params
  97. * @param null|array|string $opts
  98. *
  99. * @throws Exception\ApiErrorException
  100. *
  101. * @return TStripeObject
  102. */
  103. public function retrieve($id, $params = null, $opts = null)
  104. {
  105. self::_validateParams($params);
  106. list($url, $params) = $this->extractPathAndUpdateParams($params);
  107. $id = Util\Util::utf8($id);
  108. $extn = \urlencode($id);
  109. list($response, $opts) = $this->_request(
  110. 'get',
  111. "{$url}/{$extn}",
  112. $params,
  113. $opts
  114. );
  115. return Util\Util::convertToStripeObject($response, $opts);
  116. }
  117. /**
  118. * @return int the number of objects in the current page
  119. */
  120. #[\ReturnTypeWillChange]
  121. public function count()
  122. {
  123. return \count($this->data);
  124. }
  125. /**
  126. * @return \ArrayIterator an iterator that can be used to iterate
  127. * across objects in the current page
  128. */
  129. #[\ReturnTypeWillChange]
  130. public function getIterator()
  131. {
  132. return new \ArrayIterator($this->data);
  133. }
  134. /**
  135. * @return \ArrayIterator an iterator that can be used to iterate
  136. * backwards across objects in the current page
  137. */
  138. public function getReverseIterator()
  139. {
  140. return new \ArrayIterator(\array_reverse($this->data));
  141. }
  142. /**
  143. * @return \Generator|TStripeObject[] A generator that can be used to
  144. * iterate across all objects across all pages. As page boundaries are
  145. * encountered, the next page will be fetched automatically for
  146. * continued iteration.
  147. */
  148. public function autoPagingIterator()
  149. {
  150. $page = $this;
  151. while (true) {
  152. $filters = $this->filters ?: [];
  153. if (\array_key_exists('ending_before', $filters)
  154. && !\array_key_exists('starting_after', $filters)) {
  155. foreach ($page->getReverseIterator() as $item) {
  156. yield $item;
  157. }
  158. $page = $page->previousPage();
  159. } else {
  160. foreach ($page as $item) {
  161. yield $item;
  162. }
  163. $page = $page->nextPage();
  164. }
  165. if ($page->isEmpty()) {
  166. break;
  167. }
  168. }
  169. }
  170. /**
  171. * Returns an empty collection. This is returned from {@see nextPage()}
  172. * when we know that there isn't a next page in order to replicate the
  173. * behavior of the API when it attempts to return a page beyond the last.
  174. *
  175. * @param null|array|string $opts
  176. *
  177. * @return Collection
  178. */
  179. public static function emptyCollection($opts = null)
  180. {
  181. return Collection::constructFrom(['data' => []], $opts);
  182. }
  183. /**
  184. * Returns true if the page object contains no element.
  185. *
  186. * @return bool
  187. */
  188. public function isEmpty()
  189. {
  190. return empty($this->data);
  191. }
  192. /**
  193. * Fetches the next page in the resource list (if there is one).
  194. *
  195. * This method will try to respect the limit of the current page. If none
  196. * was given, the default limit will be fetched again.
  197. *
  198. * @param null|array $params
  199. * @param null|array|string $opts
  200. *
  201. * @return Collection<TStripeObject>
  202. */
  203. public function nextPage($params = null, $opts = null)
  204. {
  205. if (!$this->has_more) {
  206. return static::emptyCollection($opts);
  207. }
  208. $lastId = \end($this->data)->id;
  209. $params = \array_merge(
  210. $this->filters ?: [],
  211. ['starting_after' => $lastId],
  212. $params ?: []
  213. );
  214. return $this->all($params, $opts);
  215. }
  216. /**
  217. * Fetches the previous page in the resource list (if there is one).
  218. *
  219. * This method will try to respect the limit of the current page. If none
  220. * was given, the default limit will be fetched again.
  221. *
  222. * @param null|array $params
  223. * @param null|array|string $opts
  224. *
  225. * @return Collection<TStripeObject>
  226. */
  227. public function previousPage($params = null, $opts = null)
  228. {
  229. if (!$this->has_more) {
  230. return static::emptyCollection($opts);
  231. }
  232. $firstId = $this->data[0]->id;
  233. $params = \array_merge(
  234. $this->filters ?: [],
  235. ['ending_before' => $firstId],
  236. $params ?: []
  237. );
  238. return $this->all($params, $opts);
  239. }
  240. /**
  241. * Gets the first item from the current page. Returns `null` if the current page is empty.
  242. *
  243. * @return null|TStripeObject
  244. */
  245. public function first()
  246. {
  247. return \count($this->data) > 0 ? $this->data[0] : null;
  248. }
  249. /**
  250. * Gets the last item from the current page. Returns `null` if the current page is empty.
  251. *
  252. * @return null|TStripeObject
  253. */
  254. public function last()
  255. {
  256. return \count($this->data) > 0 ? $this->data[\count($this->data) - 1] : null;
  257. }
  258. private function extractPathAndUpdateParams($params)
  259. {
  260. $url = \parse_url($this->url);
  261. if (!isset($url['path'])) {
  262. throw new Exception\UnexpectedValueException("Could not parse list url into parts: {$url}");
  263. }
  264. if (isset($url['query'])) {
  265. // If the URL contains a query param, parse it out into $params so they
  266. // don't interact weirdly with each other.
  267. $query = [];
  268. \parse_str($url['query'], $query);
  269. $params = \array_merge($params ?: [], $query);
  270. }
  271. return [$url['path'], $params];
  272. }
  273. }