diff --git a/src/Link/PaginationInjector.php b/src/Link/PaginationInjector.php new file mode 100644 index 0000000..0ea6be5 --- /dev/null +++ b/src/Link/PaginationInjector.php @@ -0,0 +1,122 @@ +getCollection(); + if (! $collection instanceof Paginator) { + return false; + } + + $this->configureCollection($halCollection); + + $pageCount = count($collection); + if ($pageCount === 0) { + return true; + } + + $page = $halCollection->getPage(); + + if ($page < 1 || $page > $pageCount) { + return new ApiProblem(409, 'Invalid page provided'); + } + + $this->injectLinks($halCollection); + + return true; + } + + private function configureCollection(Collection $halCollection) + { + $collection = $halCollection->getCollection(); + $page = $halCollection->getPage(); + $pageSize = $halCollection->getPageSize(); + + $collection->setItemCountPerPage($pageSize); + $collection->setCurrentPageNumber($page); + } + + private function injectLinks(Collection $halCollection) + { + $this->injectSelfLink($halCollection); + $this->injectFirstLink($halCollection); + $this->injectLastLink($halCollection); + $this->injectPrevLink($halCollection); + $this->injectNextLink($halCollection); + } + + private function injectSelfLink(Collection $halCollection) + { + $page = $halCollection->getPage(); + $link = $this->createPaginationLink('self', $halCollection, $page); + $halCollection->getLinks()->add($link, true); + } + + private function injectFirstLink(Collection $halCollection) + { + $link = $this->createPaginationLink('first', $halCollection); + $halCollection->getLinks()->add($link); + } + + private function injectLastLink(Collection $halCollection) + { + $page = $halCollection->getCollection()->count(); + $link = $this->createPaginationLink('last', $halCollection, $page); + $halCollection->getLinks()->add($link); + } + + private function injectPrevLink(Collection $halCollection) + { + $page = $halCollection->getPage(); + $prev = ($page > 1) ? $page - 1 : false; + + if ($prev) { + $link = $this->createPaginationLink('prev', $halCollection, $prev); + $halCollection->getLinks()->add($link); + } + } + + private function injectNextLink(Collection $halCollection) + { + $page = $halCollection->getPage(); + $pageCount = $halCollection->getCollection()->count(); + $next = ($page < $pageCount) ? $page + 1 : false; + + if ($next) { + $link = $this->createPaginationLink('next', $halCollection, $next); + $halCollection->getLinks()->add($link); + } + } + + private function createPaginationLink($relation, Collection $halCollection, $page = null) + { + $options = ArrayUtils::merge( + $halCollection->getCollectionRouteOptions(), + ['query' => ['page' => $page]] + ); + + return Link::factory([ + 'rel' => $relation, + 'route' => [ + 'name' => $halCollection->getCollectionRoute(), + 'params' => $halCollection->getCollectionRouteParams(), + 'options' => $options, + ], + ]); + } +} diff --git a/src/Link/PaginationInjectorInterface.php b/src/Link/PaginationInjectorInterface.php new file mode 100644 index 0000000..9ba73f0 --- /dev/null +++ b/src/Link/PaginationInjectorInterface.php @@ -0,0 +1,20 @@ +events) { + if (! $this->events) { $this->setEventManager(new EventManager()); } return $this->events; @@ -222,7 +228,7 @@ public function getHydratorManager() */ public function getMetadataMap() { - if (!$this->metadataMap instanceof MetadataMap) { + if (! $this->metadataMap instanceof MetadataMap) { $this->setMetadataMap(new MetadataMap()); } return $this->metadataMap; @@ -240,6 +246,27 @@ public function setMetadataMap(MetadataMap $map) return $this; } + /** + * @return PaginationInjectorInterface + */ + public function getPaginationInjector() + { + if (! $this->paginationInjector instanceof PaginationInjectorInterface) { + $this->setPaginationInjector(new PaginationInjector()); + } + return $this->paginationInjector; + } + + /** + * @param PaginationInjectorInterface $injector + * @return self + */ + public function setPaginationInjector(PaginationInjectorInterface $injector) + { + $this->paginationInjector = $injector; + return $this; + } + /** * @param ServerUrl $helper * @return self @@ -287,7 +314,7 @@ public function setLinkCollectionExtractor(LinkCollectionExtractorInterface $ext */ public function addHydrator($class, $hydrator) { - if (!$hydrator instanceof ExtractionInterface) { + if (! $hydrator instanceof ExtractionInterface) { $hydrator = $this->hydrators->get($hydrator); } @@ -466,7 +493,7 @@ public function getHydratorForEntity($entity) * * * $params = $e->getParams(); - * $params['routeOptions']['query'] = array('format' => 'json'); + * $params['routeOptions']['query'] = ['format' => 'json']; * * * @param Collection $halCollection @@ -593,7 +620,7 @@ public function renderEntity(Entity $halEntity, $renderEntity = true, $depth = 0 } } - if (!$renderEntity || ($maxDepth !== null && $depth > $maxDepth)) { + if (! $renderEntity || ($maxDepth !== null && $depth > $maxDepth)) { $entity = []; } @@ -778,7 +805,7 @@ public function createEntityFromMetadata($object, Metadata $metadata, $renderEmb $id = ($entityIdentifierName) ? $data[$entityIdentifierName]: null; - if (!$renderEmbeddedEntities) { + if (! $renderEmbeddedEntities) { $object = []; } @@ -788,7 +815,7 @@ public function createEntityFromMetadata($object, Metadata $metadata, $renderEmb $this->marshalMetadataLinks($metadata, $links); $forceSelfLink = $metadata->getForceSelfLink(); - if ($forceSelfLink && !$links->has('self')) { + if ($forceSelfLink && ! $links->has('self')) { $link = $this->marshalLinkFromMetadata($metadata, $object, $id, $metadata->getRouteIdentifierName()); $links->add($link); } @@ -846,7 +873,7 @@ public function createEntity($entity, $route, $routeIdentifierName) break; } $metadata = (!is_array($entity) && $metadataMap->has($entity)) ? $metadataMap->get($entity) : false; - if (!$metadata || ($metadata && $metadata->getForceSelfLink())) { + if (! $metadata || ($metadata && $metadata->getForceSelfLink())) { $this->injectSelfLink($halEntity, $route, $routeIdentifierName); } return $halEntity; @@ -866,12 +893,12 @@ public function createCollection($collection, $route = null) $collection = $this->createCollectionFromMetadata($collection, $metadataMap->get($collection)); } - if (!$collection instanceof Collection) { + if (! $collection instanceof Collection) { $collection = new Collection($collection); } $metadata = $metadataMap->get($collection); - if (!$metadata || ($metadata && $metadata->getForceSelfLink())) { + if (! $metadata || ($metadata && $metadata->getForceSelfLink())) { $this->injectSelfLink($collection, $route); } return $collection; @@ -895,7 +922,7 @@ public function createCollectionFromMetadata($object, Metadata $metadata) $this->marshalMetadataLinks($metadata, $links); $forceSelfLink = $metadata->getForceSelfLink(); - if ($forceSelfLink && !$links->has('self') + if ($forceSelfLink && ! $links->has('self') && ($metadata->hasUrl() || $metadata->hasRoute()) ) { $link = $this->marshalLinkFromMetadata($metadata, $object); @@ -950,83 +977,11 @@ public function injectSelfLink(LinkCollectionAwareInterface $resource, $route, $ * Generate HAL links for a paginated collection * * @param Collection $halCollection - * @return boolean + * @return boolean|ApiProblem */ protected function injectPaginationLinks(Collection $halCollection) { - $collection = $halCollection->getCollection(); - $page = $halCollection->getPage(); - $pageSize = $halCollection->getPageSize(); - $route = $halCollection->getCollectionRoute(); - $params = $halCollection->getCollectionRouteParams(); - $options = $halCollection->getCollectionRouteOptions(); - - $collection->setItemCountPerPage($pageSize); - $collection->setCurrentPageNumber($page); - - $count = count($collection); - if (!$count) { - return true; - } - - if ($page < 1 || $page > $count) { - return new ApiProblem(409, 'Invalid page provided'); - } - - $links = $halCollection->getLinks(); - $next = ($page < $count) ? $page + 1 : false; - $prev = ($page > 1) ? $page - 1 : false; - - // self link - $link = new Link('self'); - $link->setRoute($route); - $link->setRouteParams($params); - $link->setRouteOptions(ArrayUtils::merge($options, [ - 'query' => ['page' => $page], - ])); - $links->add($link, true); - - // first link - $link = new Link('first'); - $link->setRoute($route); - $link->setRouteParams($params); - $link->setRouteOptions(ArrayUtils::merge($options, [ - 'query' => ['page' => null], - ])); - $links->add($link); - - // last link - $link = new Link('last'); - $link->setRoute($route); - $link->setRouteParams($params); - $link->setRouteOptions(ArrayUtils::merge($options, [ - 'query' => ['page' => $count], - ])); - $links->add($link); - - // prev link - if ($prev) { - $link = new Link('prev'); - $link->setRoute($route); - $link->setRouteParams($params); - $link->setRouteOptions(ArrayUtils::merge($options, [ - 'query' => ['page' => $prev], - ])); - $links->add($link); - } - - // next link - if ($next) { - $link = new Link('next'); - $link->setRoute($route); - $link->setRouteParams($params); - $link->setRouteOptions(ArrayUtils::merge($options, [ - 'query' => ['page' => $next], - ])); - $links->add($link); - } - - return true; + return $this->getPaginationInjector()->injectPaginationLinks($halCollection); } /** @@ -1292,7 +1247,7 @@ protected function marshalLinkFromMetadata( return $link; } - if (!$metadata->hasRoute()) { + if (! $metadata->hasRoute()) { throw new Exception\RuntimeException(sprintf( 'Unable to create a self link for resource of type "%s"; metadata does not contain a route or a url', get_class($object) diff --git a/test/Link/PaginationInjectorTest.php b/test/Link/PaginationInjectorTest.php new file mode 100644 index 0000000..3f8bcf2 --- /dev/null +++ b/test/Link/PaginationInjectorTest.php @@ -0,0 +1,127 @@ +setCollectionRoute('foo'); + $halCollection->setPage($currentPage); + $halCollection->setPageSize(1); + + return $halCollection; + } + + public function testInjectPaginationLinksGivenIntermediatePageShouldInjectAllLinks() + { + $halCollection = $this->getHalCollection(5, 2); + + $injector = new PaginationInjector(); + $injector->injectPaginationLinks($halCollection); + + $links = $halCollection->getLinks(); + $this->assertTrue($links->has('self')); + $this->assertTrue($links->has('first')); + $this->assertTrue($links->has('last')); + $this->assertTrue($links->has('prev')); + $this->assertTrue($links->has('next')); + } + + public function testInjectPaginationLinksGivenFirstPageShouldInjectLinksExceptForPrevious() + { + $halCollection = $this->getHalCollection(5, 1); + + $injector = new PaginationInjector(); + $injector->injectPaginationLinks($halCollection); + + $links = $halCollection->getLinks(); + $this->assertTrue($links->has('self')); + $this->assertTrue($links->has('first')); + $this->assertTrue($links->has('last')); + $this->assertFalse($links->has('prev')); + $this->assertTrue($links->has('next')); + } + + public function testInjectPaginationLinksGivenLastPageShouldInjectLinksExceptForNext() + { + $halCollection = $this->getHalCollection(5, 5); + + $injector = new PaginationInjector(); + $injector->injectPaginationLinks($halCollection); + + $links = $halCollection->getLinks(); + $this->assertTrue($links->has('self')); + $this->assertTrue($links->has('first')); + $this->assertTrue($links->has('last')); + $this->assertTrue($links->has('prev')); + $this->assertFalse($links->has('next')); + } + + public function testInjectPaginationLinksGivenEmptyCollectionShouldNotInjectAnyLink() + { + $halCollection = $this->getHalCollection(0, 1); + + $injector = new PaginationInjector(); + $injector->injectPaginationLinks($halCollection); + + $links = $halCollection->getLinks(); + $this->assertFalse($links->has('self')); + $this->assertFalse($links->has('first')); + $this->assertFalse($links->has('last')); + $this->assertFalse($links->has('prev')); + $this->assertFalse($links->has('next')); + } + + public function testInjectPaginationLinksGivenPageGreaterThanPageCountShouldReturnApiProblem() + { + $halCollection = $this->getHalCollection(5, 6); + + $injector = new PaginationInjector(); + $result = $injector->injectPaginationLinks($halCollection); + + $this->assertInstanceOf('ZF\ApiProblem\ApiProblem', $result); + $this->assertEquals(409, $result->status); + } + + public function testInjectPaginationLinksGivenCollectionRouteNameShouldInjectLinksWithSameRoute() + { + $halCollection = $this->getHalCollection(5, 2); + + $injector = new PaginationInjector(); + $injector->injectPaginationLinks($halCollection); + + $collectionRoute = $halCollection->getCollectionRoute(); + + $links = $halCollection->getLinks(); + $this->assertEquals($collectionRoute, $links->get('self')->getRoute()); + $this->assertEquals($collectionRoute, $links->get('first')->getRoute()); + $this->assertEquals($collectionRoute, $links->get('last')->getRoute()); + $this->assertEquals($collectionRoute, $links->get('prev')->getRoute()); + $this->assertEquals($collectionRoute, $links->get('next')->getRoute()); + } +}