Skip to content

Commit

Permalink
Add support for SearchResult objects. (#1251)
Browse files Browse the repository at this point in the history
  • Loading branch information
dcr-stripe authored Mar 11, 2022
1 parent 2a5f3ec commit 9c7e27a
Show file tree
Hide file tree
Showing 7 changed files with 573 additions and 0 deletions.
2 changes: 2 additions & 0 deletions init.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
require __DIR__ . '/lib/ApiOperations/NestedResource.php';
require __DIR__ . '/lib/ApiOperations/Request.php';
require __DIR__ . '/lib/ApiOperations/Retrieve.php';
require __DIR__ . '/lib/ApiOperations/Search.php';
require __DIR__ . '/lib/ApiOperations/Update.php';

// Plumbing
Expand Down Expand Up @@ -142,6 +143,7 @@
require __DIR__ . '/lib/Reporting/ReportRun.php';
require __DIR__ . '/lib/Reporting/ReportType.php';
require __DIR__ . '/lib/Review.php';
require __DIR__ . '/lib/SearchResult.php';
require __DIR__ . '/lib/SetupAttempt.php';
require __DIR__ . '/lib/SetupIntent.php';
require __DIR__ . '/lib/ShippingRate.php';
Expand Down
37 changes: 37 additions & 0 deletions lib/ApiOperations/Search.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Stripe\ApiOperations;

/**
* Trait for searchable resources.
*
* This trait should only be applied to classes that derive from StripeObject.
*/
trait Search
{
/**
* @param string $searchUrl
* @param null|array $params
* @param null|array|string $opts
*
* @throws \Stripe\Exception\ApiErrorException if the request fails
*
* @return \Stripe\SearchResult of ApiResources
*/
protected static function _searchResource($searchUrl, $params = null, $opts = null)
{
self::_validateParams($params);

list($response, $opts) = static::_staticRequest('get', $searchUrl, $params, $opts);
$obj = \Stripe\Util\Util::convertToStripeObject($response->json, $opts);
if (!($obj instanceof \Stripe\SearchResult)) {
throw new \Stripe\Exception\UnexpectedValueException(
'Expected type ' . \Stripe\SearchResult::class . ', got "' . \get_class($obj) . '" instead.'
);
}
$obj->setLastResponse($response);
$obj->setFilters($params);

return $obj;
}
}
24 changes: 24 additions & 0 deletions lib/BaseStripeClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,30 @@ public function requestCollection($method, $path, $params, $opts)
return $obj;
}

/**
* Sends a request to Stripe's API.
*
* @param string $method the HTTP method
* @param string $path the path of the request
* @param array $params the parameters of the request
* @param array|\Stripe\Util\RequestOptions $opts the special modifiers of the request
*
* @return \Stripe\SearchResult of ApiResources
*/
public function requestSearchResult($method, $path, $params, $opts)
{
$obj = $this->request($method, $path, $params, $opts);
if (!($obj instanceof \Stripe\SearchResult)) {
$received_class = \get_class($obj);
$msg = "Expected to receive `Stripe\\SearchResult` object from Stripe API. Instead received `{$received_class}`.";

throw new \Stripe\Exception\UnexpectedValueException($msg);
}
$obj->setFilters($params);

return $obj;
}

/**
* @param \Stripe\Util\RequestOptions $opts
*
Expand Down
230 changes: 230 additions & 0 deletions lib/SearchResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php

namespace Stripe;

/**
* Search results for an API resource.
*
* This behaves similarly to <code>Collection</code> in that they both wrap
* around a list of objects and provide pagination. However the
* <code>SearchResult</code> object paginates by relying on a
* <code>next_page</code> token included in the response rather than using
* object IDs and a <code>starting_before</code>/<code>ending_after</code>
* parameter. Thus, <code>SearchResult</code> only supports forwards pagination.
*
* @template TStripeObject of StripeObject
* @template-implements \IteratorAggregate<TStripeObject>
*
* @property string $object
* @property string $url
* @property string $next_page
* @property bool $has_more
* @property TStripeObject[] $data
*/
class SearchResult extends StripeObject implements \Countable, \IteratorAggregate
{
const OBJECT_NAME = 'search_result';

use ApiOperations\Request;

/** @var array */
protected $filters = [];

/**
* @return string the base URL for the given class
*/
public static function baseUrl()
{
return Stripe::$apiBase;
}

/**
* Returns the filters.
*
* @return array the filters
*/
public function getFilters()
{
return $this->filters;
}

/**
* Sets the filters, removing paging options.
*
* @param array $filters the filters
*/
public function setFilters($filters)
{
$this->filters = $filters;
}

#[\ReturnTypeWillChange]
public function offsetGet($k)
{
if (\is_string($k)) {
return parent::offsetGet($k);
}
$msg = "You tried to access the {$k} index, but SearchResult " .
'types only support string keys. (HINT: Search calls ' .
'return an object with a `data` (which is the data ' .
"array). You likely want to call ->data[{$k}])";

throw new Exception\InvalidArgumentException($msg);
}

/**
* @param null|array $params
* @param null|array|string $opts
*
* @throws Exception\ApiErrorException
*
* @return SearchResult<TStripeObject>
*/
public function all($params = null, $opts = null)
{
self::_validateParams($params);
list($url, $params) = $this->extractPathAndUpdateParams($params);

list($response, $opts) = $this->_request('get', $url, $params, $opts);
$obj = Util\Util::convertToStripeObject($response, $opts);
if (!($obj instanceof \Stripe\SearchResult)) {
throw new \Stripe\Exception\UnexpectedValueException(
'Expected type ' . \Stripe\SearchResult::class . ', got "' . \get_class($obj) . '" instead.'
);
}
$obj->setFilters($params);

return $obj;
}

/**
* @return int the number of objects in the current page
*/
#[\ReturnTypeWillChange]
public function count()
{
return \count($this->data);
}

/**
* @return \ArrayIterator an iterator that can be used to iterate
* across objects in the current page
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new \ArrayIterator($this->data);
}

/**
* @return \Generator|TStripeObject[] A generator that can be used to
* iterate across all objects across all pages. As page boundaries are
* encountered, the next page will be fetched automatically for
* continued iteration.
*/
public function autoPagingIterator()
{
$page = $this;

while (true) {
foreach ($page as $item) {
yield $item;
}
$page = $page->nextPage();

if ($page->isEmpty()) {
break;
}
}
}

/**
* Returns an empty set of search results. This is returned from
* {@see nextPage()} when we know that there isn't a next page in order to
* replicate the behavior of the API when it attempts to return a page
* beyond the last.
*
* @param null|array|string $opts
*
* @return SearchResult
*/
public static function emptySearchResult($opts = null)
{
return SearchResult::constructFrom(['data' => []], $opts);
}

/**
* Returns true if the page object contains no element.
*
* @return bool
*/
public function isEmpty()
{
return empty($this->data);
}

/**
* Fetches the next page in the resource list (if there is one).
*
* This method will try to respect the limit of the current page. If none
* was given, the default limit will be fetched again.
*
* @param null|array $params
* @param null|array|string $opts
*
* @return SearchResult<TStripeObject>
*/
public function nextPage($params = null, $opts = null)
{
if (!$this->has_more) {
return static::emptySearchResult($opts);
}

$params = \array_merge(
$this->filters ?: [],
['page' => $this->next_page],
$params ?: []
);

return $this->all($params, $opts);
}

/**
* Gets the first item from the current page. Returns `null` if the current page is empty.
*
* @return null|TStripeObject
*/
public function first()
{
return \count($this->data) > 0 ? $this->data[0] : null;
}

/**
* Gets the last item from the current page. Returns `null` if the current page is empty.
*
* @return null|TStripeObject
*/
public function last()
{
return \count($this->data) > 0 ? $this->data[\count($this->data) - 1] : null;
}

private function extractPathAndUpdateParams($params)
{
$url = \parse_url($this->url);

if (!isset($url['path'])) {
throw new Exception\UnexpectedValueException("Could not parse list url into parts: {$url}");
}

if (isset($url['query'])) {
// If the URL contains a query param, parse it out into $params so they
// don't interact weirdly with each other.
$query = [];
\parse_str($url['query'], $query);
$params = \array_merge($params ?: [], $query);
}

return [$url['path'], $params];
}
}
5 changes: 5 additions & 0 deletions lib/Service/AbstractService.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ protected function requestCollection($method, $path, $params, $opts)
return $this->getClient()->requestCollection($method, $path, static::formatParams($params), $opts);
}

protected function requestSearchResult($method, $path, $params, $opts)
{
return $this->getClient()->requestSearchResult($method, $path, static::formatParams($params), $opts);
}

protected function buildPath($basePath, ...$ids)
{
foreach ($ids as $id) {
Expand Down
1 change: 1 addition & 0 deletions lib/Util/ObjectTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class ObjectTypes
\Stripe\Reporting\ReportRun::OBJECT_NAME => \Stripe\Reporting\ReportRun::class,
\Stripe\Reporting\ReportType::OBJECT_NAME => \Stripe\Reporting\ReportType::class,
\Stripe\Review::OBJECT_NAME => \Stripe\Review::class,
\Stripe\SearchResult::OBJECT_NAME => \Stripe\SearchResult::class,
\Stripe\SetupAttempt::OBJECT_NAME => \Stripe\SetupAttempt::class,
\Stripe\SetupIntent::OBJECT_NAME => \Stripe\SetupIntent::class,
\Stripe\ShippingRate::OBJECT_NAME => \Stripe\ShippingRate::class,
Expand Down
Loading

0 comments on commit 9c7e27a

Please sign in to comment.