Skip to content
Snippets Groups Projects
Commit 07bfb8b5 authored by Matt Glaman's avatar Matt Glaman Committed by Matt Glaman
Browse files

Issue #3094195 by mglaman: Support pagination of index results

parent 9aa0a958
Branches
Tags
No related merge requests found
......@@ -3,6 +3,11 @@
namespace Drupal\jsonapi_search_api\Resource;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Http\Exception\CacheableBadRequestHttpException;
use Drupal\Core\Url;
use Drupal\jsonapi\JsonApiResource\Link;
use Drupal\jsonapi\JsonApiResource\LinkCollection;
use Drupal\jsonapi\Query\OffsetPage;
use Drupal\jsonapi\ResourceResponse;
use Drupal\jsonapi_resources\Resource\EntityResourceBase;
use Drupal\search_api\IndexInterface;
......@@ -31,7 +36,18 @@ final class IndexResource extends EntityResourceBase {
*/
public function process(Request $request, IndexInterface $index): ResourceResponse {
$cacheability = new CacheableMetadata();
// Ensure that different pages will be cached separately.
$cacheability->addCacheContexts(['url.query_args:page']);
$query = $index->query();
// Derive any pagination options from the query params or use defaults.
$pagination = $this->getPagination($request);
if ($pagination->getSize() <= 0) {
throw new CacheableBadRequestHttpException($cacheability, sprintf('The page size needs to be a positive integer.'));
}
$query->range($pagination->getOffset(), $pagination->getSize());
// Get the results and convert to JSON:API resource object data.
$results = $query->execute();
$result_entities = array_map(static function (ItemInterface $item) {
......@@ -39,9 +55,146 @@ final class IndexResource extends EntityResourceBase {
}, \iterator_to_array($results));
$primary_data = $this->createCollectionDataFromEntities(array_values($result_entities));
$response = $this->createJsonapiResponse($primary_data, $request);
$pager_links = $this->getPagerLinks($request, $pagination, (int) $results->getResultCount(), count($result_entities));
$response = $this->createJsonapiResponse($primary_data, $request, 200, [], $pager_links);
$response->addCacheableDependency($cacheability);
return $response;
}
/**
* Get pagination for the request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Drupal\jsonapi\Query\OffsetPage
* The pagination object.
*/
private function getPagination(Request $request): OffsetPage {
return $request->query->has('page')
? OffsetPage::createFromQueryParameter($request->query->get('page'))
: new OffsetPage(OffsetPage::DEFAULT_OFFSET, OffsetPage::SIZE_MAX);
}
/**
* Get pager links.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
* @param \Drupal\jsonapi\Query\OffsetPage $pagination
* The pagination object.
* @param int $total_count
* The total count.
* @param int $result_count
* The result count.
*
* @return \Drupal\jsonapi\JsonApiResource\LinkCollection
* The link collection.
*/
protected function getPagerLinks(Request $request, OffsetPage $pagination, int $total_count, int $result_count): LinkCollection {
$pager_links = new LinkCollection([]);
$size = (int) $pagination->getSize();
$offset = $pagination->getOffset();
$query = (array) $request->query->getIterator();
// Check if this is not the last page.
if (($pagination->getOffset() + $result_count) < $total_count) {
$next_url = static::getRequestLink($request, static::getPagerQueries('next', $offset, $size, $query));
$pager_links = $pager_links->withLink('next', new Link(new CacheableMetadata(), $next_url, 'next'));
$last_url = static::getRequestLink($request, static::getPagerQueries('last', $offset, $size, $query, $total_count));
$pager_links = $pager_links->withLink('last', new Link(new CacheableMetadata(), $last_url, 'last'));
}
// Check if this is not the first page.
if ($offset > 0) {
$first_url = static::getRequestLink($request, static::getPagerQueries('first', $offset, $size, $query));
$pager_links = $pager_links->withLink('first', new Link(new CacheableMetadata(), $first_url, 'first'));
$prev_url = static::getRequestLink($request, static::getPagerQueries('prev', $offset, $size, $query));
$pager_links = $pager_links->withLink('prev', new Link(new CacheableMetadata(), $prev_url, 'prev'));
}
return $pager_links;
}
/**
* Get the query param array.
*
* @param string $link_id
* The name of the pagination link requested.
* @param int $offset
* The starting index.
* @param int $size
* The pagination page size.
* @param array $query
* The query parameters.
* @param int $total
* The total size of the collection.
*
* @return array
* The pagination query param array.
*/
protected static function getPagerQueries($link_id, $offset, $size, array $query = [], $total = 0) {
$extra_query = [];
switch ($link_id) {
case 'next':
$extra_query = [
'page' => [
'offset' => $offset + $size,
'limit' => $size,
],
];
break;
case 'first':
$extra_query = [
'page' => [
'offset' => 0,
'limit' => $size,
],
];
break;
case 'last':
if ($total) {
$extra_query = [
'page' => [
'offset' => (ceil($total / $size) - 1) * $size,
'limit' => $size,
],
];
}
break;
case 'prev':
$extra_query = [
'page' => [
'offset' => max($offset - $size, 0),
'limit' => $size,
],
];
break;
}
return array_merge($query, $extra_query);
}
/**
* Get the full URL for a given request object.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param array|null $query
* The query parameters to use. Leave it empty to get the query from the
* request object.
*
* @return \Drupal\Core\Url
* The full URL.
*/
public static function getRequestLink(Request $request, $query = NULL) {
if ($query === NULL) {
return Url::fromUri($request->getUri());
}
$uri_without_query_string = $request->getSchemeAndHttpHost() . $request->getBaseUrl() . $request->getPathInfo();
return Url::fromUri($uri_without_query_string)->setOption('query', $query);
}
}
......@@ -11,7 +11,6 @@ use Drupal\Tests\jsonapi\Functional\JsonApiRequestTestTrait;
use Drupal\Tests\jsonapi\Functional\ResourceResponseTestTrait;
use Drupal\Tests\search_api\Functional\ExampleContentTrait;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
use GuzzleHttp\RequestOptions;
/**
......@@ -58,18 +57,17 @@ final class IndexResourceTest extends BrowserTestBase {
$index->indexItems();
$this->container->get('router.builder')->rebuildIfNeeded();
$this->grantPermissions(Role::load(Role::ANONYMOUS_ID), [
'view test entity',
]);
}
/**
* Tests the results contain the index values.
*/
public function testCollection() {
$this->grantPermissions(Role::load(Role::ANONYMOUS_ID), [
'view test entity',
]);
$url = Url::fromRoute('jsonapi_search_api.index_database_search_index', [
'index' => 'database_search_index',
]);
$url = Url::fromRoute('jsonapi_search_api.index_database_search_index');
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$response = $this->request('GET', $url, $request_options);
......@@ -81,4 +79,50 @@ final class IndexResourceTest extends BrowserTestBase {
}, $response_document['data']));
}
/**
* Tests the results contain the index values.
*/
public function testCollectionPagination() {
$request_options = [];
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
$url = Url::fromRoute('jsonapi_search_api.index_database_search_index', [], [
'query' => [
'page' => [
'limit' => 2,
'offset' => 0,
],
],
]);
$response = $this->request('GET', $url, $request_options);
$this->assertSame(200, $response->getStatusCode(), var_export(Json::decode((string) $response->getBody()), TRUE));
$response_document = Json::decode((string) $response->getBody());
$this->assertCount(2, $response_document['data'], var_export($response_document, TRUE));
$this->assertSame([1, 2], array_map(static function (array $data) {
return $data['attributes']['drupal_internal__id'];
}, $response_document['data']));
$this->assertArrayHasKey('next', $response_document['links']);
$this->assertArrayHasKey('last', $response_document['links']);
$url = Url::fromRoute('jsonapi_search_api.index_database_search_index', [], [
'query' => [
'page' => [
'limit' => 2,
'offset' => 2,
],
],
]);
$response = $this->request('GET', $url, $request_options);
$this->assertSame(200, $response->getStatusCode(), var_export(Json::decode((string) $response->getBody()), TRUE));
$response_document = Json::decode((string) $response->getBody());
$this->assertCount(2, $response_document['data'], var_export($response_document, TRUE));
$this->assertSame([3, 4], array_map(static function (array $data) {
return $data['attributes']['drupal_internal__id'];
}, $response_document['data']));
$this->assertArrayHasKey('next', $response_document['links']);
$this->assertArrayHasKey('last', $response_document['links']);
$this->assertArrayHasKey('prev', $response_document['links']);
$this->assertArrayHasKey('first', $response_document['links']);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment