Collection.php 8.5 KB

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