<?php
namespace App\Controller;
use App\Doctrine\Repository\SearchableRepositoryInterface;
use App\Model\Searching\AbstractSearch;
use App\Util\Pagination;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Trait to be used with controllers that deal with paginating results and using things like searches.
*/
trait PaginationTrait
{
/**
* Helper method for handling common searching logic used throughout the system.
* This will generate an array of Twig vars to be used in a controller view.
*
* @param string $entityClass
* @param string $var
* @param AbstractSearch|string $search
* @param FormInterface|string $form
* @param int|null $page
* @return array|RedirectResponse
*/
protected function doSearch(
string $entityClass,
string $var,
$search,
$form,
?int $page = null
) {
// make a fresh search if we were just given a class
if (is_string($search)) {
$search = new $search();
}
if ( ! $search instanceof AbstractSearch) {
throw new \Exception();
}
// search form logic
$this->handleSearch(
$form = is_string($form) ? $this->createForm(
$form,
$search,
) : $form,
);
// querying
$repo = $this->getEntityManager()->getRepository($entityClass);
if ( ! $repo instanceof SearchableRepositoryInterface) {
throw new \Exception();
}
$results = $repo->findBySearch(
$search,
($page !== null) ? $this->getPageSize($search) : null,
($page !== null) ? $this->getPageOffset($page, $search) : null,
);
// determine if we are out of bounds on the pagination
if ($page !== null && $this->isPageOutOfBounds($results, $page, $search)) {
return $this->handlePageOutOfBounds($results, $search);
}
return [
$var => $results,
'search' => $search,
'form' => $form->createView(),
'pagination' => ($page !== null) ? Pagination::controller(
$results,
count($results),
$page,
$this->getPageSize($search)
) : null,
];
}
/**
* Helper method for handling common API searching logic used throughout the system.
* This will generate an array of vars to be used in a controller view.
*
* @param string $entityClass
* @param AbstractSearch|string $search
* @param FormInterface|string $form
* @return array|RedirectResponse
* @throws \Exception
*/
protected function doApiSearch(
string $entityClass,
$search,
$form
) {
// make a fresh search if we were just given a class
if (is_string($search)) {
$search = new $search();
}
if ( ! $search instanceof AbstractSearch) {
throw new \Exception();
}
// search form logic
$this->handleSearch(
is_string($form) ? $this->createForm(
$form,
$search,
) : $form,
);
// querying
$repo = $this->getEntityManager()->getRepository($entityClass);
if ( ! $repo instanceof SearchableRepositoryInterface) {
throw new \Exception();
}
$results = $repo->findBySearch($search);
$limit = $this->getPageSize($search);
$prevOffset = $this->getPagePrevOffset($search->getOffset(), $this->getPageSize($search));
$nextOffset = $this->getPageNextOffset($search->getOffset(), count($results), $this->getPageSize($search));
return [
'data' => $results,
'meta' => [
'prev' => $prevOffset !== null ? $this->getPaginationUrl(['offset' => $prevOffset, 'limit' => $limit]
) : null,
'next' => $nextOffset !== null ? $this->getPaginationUrl(['offset' => $nextOffset, 'limit' => $limit]
) : null,
],
];
}
/**
* Determines an appropriate limit based on various inputs.
* Tries to find a fallback in the controller using a PAGE_LIMIT constant.
* If that is not found, a final fallback using the Pagination tool will be used.
*
* @param int|AbstractSearch|QueryBuilder|Query|null $limit
* @return int
*/
protected function getPageSize($limit = null): int
{
switch (true) {
case is_int($limit):
break;
case $limit instanceof AbstractSearch:
$limit = $limit->getLimit();
break;
case $limit instanceof QueryBuilder:
$limit = $limit->getMaxResults() ?: 0;
break;
case $limit instanceof Query:
$limit = $limit->getMaxResults() ?: 0;
break;
default:
$limit = 0;
}
$limit = max(0, $limit);
if ( ! $limit && defined(static::class . '::PAGE_LIMIT') && static::PAGE_LIMIT) {
$limit = max(0, static::PAGE_LIMIT);
}
if ( ! $limit) {
$limit = Pagination::PAGE_LIMIT;
}
return $limit;
}
/**
* Calculates the appropriate query offset given a page and a limit.
* Limit will be calculated based on input if not given specifically.
*
* @param int $page
* @param mixed $limit
* @return int
*/
protected function getPageOffset(
int $page,
$limit = null
): int {
return Pagination::offset(
$page,
$this->getPageSize($limit),
);
}
/**
* @param int $currentOffset
* @param $limit
* @return int|null
*/
protected function getPagePrevOffset(int $currentOffset, $limit = null): ?int
{
if ($currentOffset === 0) {
return null;
}
return max($currentOffset - $this->getPageSize($limit), 0);
}
/**
* @param int $currentOffset
* @param int $max
* @param $limit
* @return int|null
*/
protected function getPageNextOffset(int $currentOffset, int $max, $limit = null): ?int
{
$nextOffset = $currentOffset + $this->getPageSize($limit);
return $nextOffset < $max ? $nextOffset : null;
}
/**
* Determines if the given result set is outside the bounds of valid paging.
* i.e. if the page being requested extends beyond the "maximum" possible page.
*
* @param array|iterable $items
* @param int $page
* @param mixed $limit
* @return bool
*/
protected function isPageOutOfBounds(
$items,
int $page,
$limit = null
): bool {
return Pagination::outOfBounds(
$items,
$page,
$this->getPageSize($limit),
);
}
/**
* If an out-of-bounds page has been requested, this method can be used to return a redirect that goes to the last possible page.
*
* @param $items
* @param mixed $limit
* @return RedirectResponse
*/
protected function handlePageOutOfBounds(
$items,
$limit = null
): RedirectResponse {
$request = $this->getRequest();
return $this->redirectToRoute(
$request->attributes->get('_route'),
array_merge(
$request->attributes->get('_route_params'),
$request->query->all(),
[
Pagination::PAGE_VAR => Pagination::maxPage(
$items,
$this->getPageSize($limit),
),
]
)
);
}
/**
* Renders an array of information to be used in Twig files for the pagination UIs.
*
* TODO: this should probably have like a class instead of just relying on an array...
*
* @param $items
* @param int $page
* @param mixed $limit
* @return array
*/
protected function generatePagination(
$items,
int $page,
$limit = null
): array {
return Pagination::controller(
$items,
count($items),
$page,
$this->getPageSize($limit),
);
}
/**
* @param array $params
* @return string
*/
protected function getPaginationUrl(array $params): string
{
$request = $this->getRequest();
return $this->generateUrl(
$request->attributes->get('_route'),
array_merge(
$request->attributes->get('_route_params'),
$request->query->all(),
$params,
),
UrlGeneratorInterface::ABSOLUTE_URL
);
}
}