src/App/Controller/PaginationTrait.php line 30

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Doctrine\Repository\SearchableRepositoryInterface;
  4. use App\Model\Searching\AbstractSearch;
  5. use App\Util\Pagination;
  6. use Doctrine\ORM\Query;
  7. use Doctrine\ORM\QueryBuilder;
  8. use Symfony\Component\Form\FormInterface;
  9. use Symfony\Component\HttpFoundation\RedirectResponse;
  10. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  11. /**
  12.  * Trait to be used with controllers that deal with paginating results and using things like searches.
  13.  */
  14. trait PaginationTrait
  15. {
  16.     /**
  17.      * Helper method for handling common searching logic used throughout the system.
  18.      * This will generate an array of Twig vars to be used in a controller view.
  19.      *
  20.      * @param string $entityClass
  21.      * @param string $var
  22.      * @param AbstractSearch|string $search
  23.      * @param FormInterface|string $form
  24.      * @param int|null $page
  25.      * @return array|RedirectResponse
  26.      */
  27.     protected function doSearch(
  28.         string $entityClass,
  29.         string $var,
  30.         $search,
  31.         $form,
  32.         ?int $page null
  33.     ) {
  34.         // make a fresh search if we were just given a class
  35.         if (is_string($search)) {
  36.             $search = new $search();
  37.         }
  38.         if ( ! $search instanceof AbstractSearch) {
  39.             throw new \Exception();
  40.         }
  41.         // search form logic
  42.         $this->handleSearch(
  43.             $form is_string($form) ? $this->createForm(
  44.                 $form,
  45.                 $search,
  46.             ) : $form,
  47.         );
  48.         // querying
  49.         $repo $this->getEntityManager()->getRepository($entityClass);
  50.         if ( ! $repo instanceof SearchableRepositoryInterface) {
  51.             throw new \Exception();
  52.         }
  53.         $results $repo->findBySearch(
  54.             $search,
  55.             ($page !== null) ? $this->getPageSize($search) : null,
  56.             ($page !== null) ? $this->getPageOffset($page$search) : null,
  57.         );
  58.         // determine if we are out of bounds on the pagination
  59.         if ($page !== null && $this->isPageOutOfBounds($results$page$search)) {
  60.             return $this->handlePageOutOfBounds($results$search);
  61.         }
  62.         return [
  63.             $var => $results,
  64.             'search' => $search,
  65.             'form' => $form->createView(),
  66.             'pagination' => ($page !== null) ? Pagination::controller(
  67.                 $results,
  68.                 count($results),
  69.                 $page,
  70.                 $this->getPageSize($search)
  71.             ) : null,
  72.         ];
  73.     }
  74.     /**
  75.      * Helper method for handling common API searching logic used throughout the system.
  76.      * This will generate an array of  vars to be used in a controller view.
  77.      *
  78.      * @param string $entityClass
  79.      * @param AbstractSearch|string $search
  80.      * @param FormInterface|string $form
  81.      * @return array|RedirectResponse
  82.      * @throws \Exception
  83.      */
  84.     protected function doApiSearch(
  85.         string $entityClass,
  86.         $search,
  87.         $form
  88.     ) {
  89.         // make a fresh search if we were just given a class
  90.         if (is_string($search)) {
  91.             $search = new $search();
  92.         }
  93.         if ( ! $search instanceof AbstractSearch) {
  94.             throw new \Exception();
  95.         }
  96.         // search form logic
  97.         $this->handleSearch(
  98.             is_string($form) ? $this->createForm(
  99.                 $form,
  100.                 $search,
  101.             ) : $form,
  102.         );
  103.         // querying
  104.         $repo $this->getEntityManager()->getRepository($entityClass);
  105.         if ( ! $repo instanceof SearchableRepositoryInterface) {
  106.             throw new \Exception();
  107.         }
  108.         $results $repo->findBySearch($search);
  109.         $limit $this->getPageSize($search);
  110.         $prevOffset $this->getPagePrevOffset($search->getOffset(), $this->getPageSize($search));
  111.         $nextOffset $this->getPageNextOffset($search->getOffset(), count($results), $this->getPageSize($search));
  112.         return [
  113.             'data' => $results,
  114.             'meta' => [
  115.                 'prev' => $prevOffset !== null $this->getPaginationUrl(['offset' => $prevOffset'limit' => $limit]
  116.                 ) : null,
  117.                 'next' => $nextOffset !== null $this->getPaginationUrl(['offset' => $nextOffset'limit' => $limit]
  118.                 ) : null,
  119.             ],
  120.         ];
  121.     }
  122.     /**
  123.      * Determines an appropriate limit based on various inputs.
  124.      * Tries to find a fallback in the controller using a PAGE_LIMIT constant.
  125.      * If that is not found, a final fallback using the Pagination tool will be used.
  126.      *
  127.      * @param int|AbstractSearch|QueryBuilder|Query|null $limit
  128.      * @return int
  129.      */
  130.     protected function getPageSize($limit null): int
  131.     {
  132.         switch (true) {
  133.             case is_int($limit):
  134.                 break;
  135.             case $limit instanceof AbstractSearch:
  136.                 $limit $limit->getLimit();
  137.                 break;
  138.             case $limit instanceof QueryBuilder:
  139.                 $limit $limit->getMaxResults() ?: 0;
  140.                 break;
  141.             case $limit instanceof Query:
  142.                 $limit $limit->getMaxResults() ?: 0;
  143.                 break;
  144.             default:
  145.                 $limit 0;
  146.         }
  147.         $limit max(0$limit);
  148.         if ( ! $limit && defined(static::class . '::PAGE_LIMIT') && static::PAGE_LIMIT) {
  149.             $limit max(0, static::PAGE_LIMIT);
  150.         }
  151.         if ( ! $limit) {
  152.             $limit Pagination::PAGE_LIMIT;
  153.         }
  154.         return $limit;
  155.     }
  156.     /**
  157.      * Calculates the appropriate query offset given a page and a limit.
  158.      * Limit will be calculated based on input if not given specifically.
  159.      *
  160.      * @param int $page
  161.      * @param mixed $limit
  162.      * @return int
  163.      */
  164.     protected function getPageOffset(
  165.         int $page,
  166.         $limit null
  167.     ): int {
  168.         return Pagination::offset(
  169.             $page,
  170.             $this->getPageSize($limit),
  171.         );
  172.     }
  173.     /**
  174.      * @param int $currentOffset
  175.      * @param $limit
  176.      * @return int|null
  177.      */
  178.     protected function getPagePrevOffset(int $currentOffset$limit null): ?int
  179.     {
  180.         if ($currentOffset === 0) {
  181.             return null;
  182.         }
  183.         return max($currentOffset $this->getPageSize($limit), 0);
  184.     }
  185.     /**
  186.      * @param int $currentOffset
  187.      * @param int $max
  188.      * @param $limit
  189.      * @return int|null
  190.      */
  191.     protected function getPageNextOffset(int $currentOffsetint $max$limit null): ?int
  192.     {
  193.         $nextOffset $currentOffset $this->getPageSize($limit);
  194.         return $nextOffset $max $nextOffset null;
  195.     }
  196.     /**
  197.      * Determines if the given result set is outside the bounds of valid paging.
  198.      * i.e. if the page being requested extends beyond the "maximum" possible page.
  199.      *
  200.      * @param array|iterable $items
  201.      * @param int $page
  202.      * @param mixed $limit
  203.      * @return bool
  204.      */
  205.     protected function isPageOutOfBounds(
  206.         $items,
  207.         int $page,
  208.         $limit null
  209.     ): bool {
  210.         return Pagination::outOfBounds(
  211.             $items,
  212.             $page,
  213.             $this->getPageSize($limit),
  214.         );
  215.     }
  216.     /**
  217.      * 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.
  218.      *
  219.      * @param $items
  220.      * @param mixed $limit
  221.      * @return RedirectResponse
  222.      */
  223.     protected function handlePageOutOfBounds(
  224.         $items,
  225.         $limit null
  226.     ): RedirectResponse {
  227.         $request $this->getRequest();
  228.         return $this->redirectToRoute(
  229.             $request->attributes->get('_route'),
  230.             array_merge(
  231.                 $request->attributes->get('_route_params'),
  232.                 $request->query->all(),
  233.                 [
  234.                     Pagination::PAGE_VAR => Pagination::maxPage(
  235.                         $items,
  236.                         $this->getPageSize($limit),
  237.                     ),
  238.                 ]
  239.             )
  240.         );
  241.     }
  242.     /**
  243.      * Renders an array of information to be used in Twig files for the pagination UIs.
  244.      *
  245.      * TODO: this should probably have like a class instead of just relying on an array...
  246.      *
  247.      * @param $items
  248.      * @param int $page
  249.      * @param mixed $limit
  250.      * @return array
  251.      */
  252.     protected function generatePagination(
  253.         $items,
  254.         int $page,
  255.         $limit null
  256.     ): array {
  257.         return Pagination::controller(
  258.             $items,
  259.             count($items),
  260.             $page,
  261.             $this->getPageSize($limit),
  262.         );
  263.     }
  264.     /**
  265.      * @param array $params
  266.      * @return string
  267.      */
  268.     protected function getPaginationUrl(array $params): string
  269.     {
  270.         $request $this->getRequest();
  271.         return $this->generateUrl(
  272.             $request->attributes->get('_route'),
  273.             array_merge(
  274.                 $request->attributes->get('_route_params'),
  275.                 $request->query->all(),
  276.                 $params,
  277.             ),
  278.             UrlGeneratorInterface::ABSOLUTE_URL
  279.         );
  280.     }
  281. }