SearchResult.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. <?php
  2. namespace Stripe;
  3. /**
  4. * Search results for an API resource.
  5. *
  6. * This behaves similarly to <code>Collection</code> in that they both wrap
  7. * around a list of objects and provide pagination. However the
  8. * <code>SearchResult</code> object paginates by relying on a
  9. * <code>next_page</code> token included in the response rather than using
  10. * object IDs and a <code>starting_before</code>/<code>ending_after</code>
  11. * parameter. Thus, <code>SearchResult</code> only supports forwards pagination.
  12. *
  13. * The {@see $total_count} property is only available when
  14. * the `expand` parameter contains `total_count`.
  15. *
  16. * @template TStripeObject of StripeObject
  17. * @template-implements \IteratorAggregate<TStripeObject>
  18. *
  19. * @property string $object
  20. * @property string $url
  21. * @property string $next_page
  22. * @property int $total_count
  23. * @property bool $has_more
  24. * @property TStripeObject[] $data
  25. */
  26. class SearchResult extends StripeObject implements \Countable, \IteratorAggregate
  27. {
  28. const OBJECT_NAME = 'search_result';
  29. use ApiOperations\Request;
  30. /** @var array */
  31. protected $filters = [];
  32. /**
  33. * @return string the base URL for the given class
  34. */
  35. public static function baseUrl()
  36. {
  37. return Stripe::$apiBase;
  38. }
  39. /**
  40. * Returns the filters.
  41. *
  42. * @return array the filters
  43. */
  44. public function getFilters()
  45. {
  46. return $this->filters;
  47. }
  48. /**
  49. * Sets the filters, removing paging options.
  50. *
  51. * @param array $filters the filters
  52. */
  53. public function setFilters($filters)
  54. {
  55. $this->filters = $filters;
  56. }
  57. /**
  58. * @return mixed
  59. */
  60. #[\ReturnTypeWillChange]
  61. public function offsetGet($k)
  62. {
  63. if (\is_string($k)) {
  64. return parent::offsetGet($k);
  65. }
  66. $msg = "You tried to access the {$k} index, but SearchResult " .
  67. 'types only support string keys. (HINT: Search calls ' .
  68. 'return an object with a `data` (which is the data ' .
  69. "array). You likely want to call ->data[{$k}])";
  70. throw new Exception\InvalidArgumentException($msg);
  71. }
  72. /**
  73. * @param null|array $params
  74. * @param null|array|string $opts
  75. *
  76. * @throws Exception\ApiErrorException
  77. *
  78. * @return SearchResult<TStripeObject>
  79. */
  80. public function all($params = null, $opts = null)
  81. {
  82. self::_validateParams($params);
  83. list($url, $params) = $this->extractPathAndUpdateParams($params);
  84. list($response, $opts) = $this->_request('get', $url, $params, $opts);
  85. $obj = Util\Util::convertToStripeObject($response, $opts);
  86. if (!($obj instanceof \Stripe\SearchResult)) {
  87. throw new \Stripe\Exception\UnexpectedValueException(
  88. 'Expected type ' . \Stripe\SearchResult::class . ', got "' . \get_class($obj) . '" instead.'
  89. );
  90. }
  91. $obj->setFilters($params);
  92. return $obj;
  93. }
  94. /**
  95. * @return int the number of objects in the current page
  96. */
  97. #[\ReturnTypeWillChange]
  98. public function count()
  99. {
  100. return \count($this->data);
  101. }
  102. /**
  103. * @return \ArrayIterator an iterator that can be used to iterate
  104. * across objects in the current page
  105. */
  106. #[\ReturnTypeWillChange]
  107. public function getIterator()
  108. {
  109. return new \ArrayIterator($this->data);
  110. }
  111. /**
  112. * @throws Exception\ApiErrorException
  113. *
  114. * @return \Generator|TStripeObject[] A generator that can be used to
  115. * iterate across all objects across all pages. As page boundaries are
  116. * encountered, the next page will be fetched automatically for
  117. * continued iteration.
  118. */
  119. public function autoPagingIterator()
  120. {
  121. $page = $this;
  122. while (true) {
  123. foreach ($page as $item) {
  124. yield $item;
  125. }
  126. $page = $page->nextPage();
  127. if ($page->isEmpty()) {
  128. break;
  129. }
  130. }
  131. }
  132. /**
  133. * Returns an empty set of search results. This is returned from
  134. * {@see nextPage()} when we know that there isn't a next page in order to
  135. * replicate the behavior of the API when it attempts to return a page
  136. * beyond the last.
  137. *
  138. * @param null|array|string $opts
  139. *
  140. * @return SearchResult
  141. */
  142. public static function emptySearchResult($opts = null)
  143. {
  144. return SearchResult::constructFrom(['data' => []], $opts);
  145. }
  146. /**
  147. * Returns true if the page object contains no element.
  148. *
  149. * @return bool
  150. */
  151. public function isEmpty()
  152. {
  153. return empty($this->data);
  154. }
  155. /**
  156. * Fetches the next page in the resource list (if there is one).
  157. *
  158. * This method will try to respect the limit of the current page. If none
  159. * was given, the default limit will be fetched again.
  160. *
  161. * @param null|array $params
  162. * @param null|array|string $opts
  163. *
  164. * @throws Exception\ApiErrorException
  165. *
  166. * @return SearchResult<TStripeObject>
  167. */
  168. public function nextPage($params = null, $opts = null)
  169. {
  170. if (!$this->has_more) {
  171. return static::emptySearchResult($opts);
  172. }
  173. $params = \array_merge(
  174. $this->filters ?: [],
  175. ['page' => $this->next_page],
  176. $params ?: []
  177. );
  178. return $this->all($params, $opts);
  179. }
  180. /**
  181. * Gets the first item from the current page. Returns `null` if the current page is empty.
  182. *
  183. * @return null|TStripeObject
  184. */
  185. public function first()
  186. {
  187. return \count($this->data) > 0 ? $this->data[0] : null;
  188. }
  189. /**
  190. * Gets the last item from the current page. Returns `null` if the current page is empty.
  191. *
  192. * @return null|TStripeObject
  193. */
  194. public function last()
  195. {
  196. return \count($this->data) > 0 ? $this->data[\count($this->data) - 1] : null;
  197. }
  198. private function extractPathAndUpdateParams($params)
  199. {
  200. $url = \parse_url($this->url);
  201. if (!isset($url['path'])) {
  202. throw new Exception\UnexpectedValueException("Could not parse list url into parts: {$url}");
  203. }
  204. if (isset($url['query'])) {
  205. // If the URL contains a query param, parse it out into $params so they
  206. // don't interact weirdly with each other.
  207. $query = [];
  208. \parse_str($url['query'], $query);
  209. $params = \array_merge($params ?: [], $query);
  210. }
  211. return [$url['path'], $params];
  212. }
  213. }