Skip to content

Commit 3e756f9

Browse files
committed
code challenge 3 solution
1 parent 2ff7b21 commit 3e756f9

23 files changed

+440
-48
lines changed

CODING-CHALLENGE-4.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# RESTful Webservices in Symfony
2+
3+
## Coding Challenge 4 - HATEOAS
4+
5+
### Tasks
6+
7+
- introduce HATEOAS links for your read and list representations of workshops and attendees
8+
- use the JSON-HAL format
9+
10+
### Solution
11+
12+
- add a `links` property to the `PaginatedCollection` class (annotate the getter with `#[SerializedName('_links')]`)
13+
- add `UrlGeneratorInterface` as dependency of `PaginatedCollectionFactory`
14+
- introduce a `addLink(string $rel, string $href)` method in the `PaginatedCollection` class
15+
- add links to the created `PaginatedCollection` (self, next, prev, first, last)
16+
- adjust the AttendeeNormalizer and WorkshopNormalizer and add
17+
- `$data['_links']['self']['href']` (remember to check for is_array($data))
18+
- `$data['_links']['collection']['href']` (remember to check for is_array($data))

src/Controller/Attendee/ListController.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
namespace App\Controller\Attendee;
66

7-
use App\Repository\AttendeeRepository;
7+
use App\Pagination\AttendeeCollectionFactory;
8+
use Symfony\Component\HttpFoundation\Request;
89
use Symfony\Component\HttpFoundation\Response;
910
use Symfony\Component\Routing\Annotation\Route;
1011
use Symfony\Component\Serializer\SerializerInterface;
@@ -13,18 +14,21 @@
1314
final class ListController
1415
{
1516
public function __construct(
16-
private AttendeeRepository $attendeeRepository,
17+
private AttendeeCollectionFactory $attendeeCollectionFactory,
1718
private SerializerInterface $serializer,
1819
) {
1920
}
2021

21-
public function __invoke(): Response
22+
public function __invoke(Request $request): Response
2223
{
23-
$allAttendees = $this->attendeeRepository->findAll();
24+
$attendeeCollection = $this->attendeeCollectionFactory->create(
25+
$request->query->getInt('page', 1),
26+
$request->query->getInt('size', 10)
27+
);
2428

25-
$serializedAttendees = $this->serializer->serialize($allAttendees, 'json');
29+
$serializedAttendeeCollection = $this->serializer->serialize($attendeeCollection, 'json');
2630

27-
return new Response($serializedAttendees, Response::HTTP_OK, [
31+
return new Response($serializedAttendeeCollection, Response::HTTP_OK, [
2832
'Content-Type' => 'application/json',
2933
]);
3034
}

src/Controller/Workshop/ListController.php

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
namespace App\Controller\Workshop;
66

7-
use App\Repository\WorkshopRepository;
7+
use App\Pagination\WorkshopCollectionFactory;
8+
use Symfony\Component\HttpFoundation\Request;
89
use Symfony\Component\HttpFoundation\Response;
910
use Symfony\Component\Routing\Annotation\Route;
1011
use Symfony\Component\Serializer\SerializerInterface;
@@ -13,18 +14,21 @@
1314
final class ListController
1415
{
1516
public function __construct(
16-
private WorkshopRepository $workshopRepository,
17+
private WorkshopCollectionFactory $workshopCollectionFactory,
1718
private SerializerInterface $serializer,
1819
) {
1920
}
2021

21-
public function __invoke(): Response
22+
public function __invoke(Request $request): Response
2223
{
23-
$allWorkshops = $this->workshopRepository->findAll();
24+
$workshopCollection = $this->workshopCollectionFactory->create(
25+
$request->query->getInt('page', 1),
26+
$request->query->getInt('size', 10)
27+
);
2428

25-
$serializedWorkshops = $this->serializer->serialize($allWorkshops, 'json');
29+
$serializedWorkshopCollection = $this->serializer->serialize($workshopCollection, 'json');
2630

27-
return new Response($serializedWorkshops, Response::HTTP_OK, [
31+
return new Response($serializedWorkshopCollection, Response::HTTP_OK, [
2832
'Content-Type' => 'application/json',
2933
]);
3034
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Pagination;
6+
7+
use App\Repository\AttendeeRepository;
8+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
9+
10+
final class AttendeeCollectionFactory extends PaginatedCollectionFactory
11+
{
12+
public function __construct(
13+
private AttendeeRepository $attendeeRepository
14+
) {
15+
}
16+
17+
public function getRepository(): ServiceEntityRepositoryInterface
18+
{
19+
return $this->attendeeRepository;
20+
}
21+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Pagination;
6+
7+
final class PaginatedCollection
8+
{
9+
private array $items;
10+
private int $total;
11+
private int $count;
12+
13+
public function __construct(\Iterator $items, int $total)
14+
{
15+
$this->items = iterator_to_array($items);
16+
$this->total = $total;
17+
$this->count = \count($this->items);
18+
}
19+
20+
public function getItems(): array
21+
{
22+
return $this->items;
23+
}
24+
25+
public function getTotal(): int
26+
{
27+
return $this->total;
28+
}
29+
30+
public function getCount(): int
31+
{
32+
return $this->count;
33+
}
34+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Pagination;
6+
7+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
8+
use Doctrine\ORM\Tools\Pagination\Paginator;
9+
10+
abstract class PaginatedCollectionFactory
11+
{
12+
abstract public function getRepository(): ServiceEntityRepositoryInterface;
13+
14+
public function create(int $page, int $size): PaginatedCollection
15+
{
16+
$query = $this->getRepository()
17+
->createQueryBuilder('u')
18+
->orderBy('u.id', 'asc')
19+
->getQuery()
20+
;
21+
22+
$paginator = new Paginator($query);
23+
$total = count($paginator);
24+
25+
$paginator
26+
->getQuery()
27+
->setFirstResult($size * ($page - 1))
28+
->setMaxResults($size);
29+
30+
return new PaginatedCollection($paginator->getIterator(), $total);
31+
}
32+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Pagination;
6+
7+
use App\Repository\WorkshopRepository;
8+
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
9+
10+
final class WorkshopCollectionFactory extends PaginatedCollectionFactory
11+
{
12+
public function __construct(
13+
private WorkshopRepository $workshopRepository
14+
) {
15+
}
16+
17+
public function getRepository(): ServiceEntityRepositoryInterface
18+
{
19+
return $this->workshopRepository;
20+
}
21+
}

tests/Controller/Attendee/ListControllerTest.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,28 @@ public function test_it_should_list_all_attendees(): void
2020

2121
$this->assertMatchesJsonSnapshot($this->browser->getResponse()->getContent());
2222
}
23+
24+
/**
25+
* @dataProvider paginationQueryParameterValues
26+
*/
27+
public function test_it_should_paginate_attendees(int $page, int $size): void
28+
{
29+
$this->loadFixtures([
30+
__DIR__.'/fixtures/paginate_attendee.yaml',
31+
]);
32+
33+
$this->browser->request('GET', sprintf('/attendees?page=%d&size=%d', $page, $size));
34+
35+
static::assertResponseIsSuccessful();
36+
37+
$this->assertMatchesJsonSnapshot($this->browser->getResponse()->getContent());
38+
}
39+
40+
public function paginationQueryParameterValues(): \Generator
41+
{
42+
yield 'show 1st page, 3 items each' => [1, 3];
43+
yield 'show 2nd page, 3 items each' => [2, 3];
44+
yield 'show 1st page, 5 items each' => [1, 5];
45+
yield 'show 2nd page, 5 items each' => [2, 5];
46+
}
2347
}
Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1-
[
2-
{
3-
"identifier": "803449f4-9a4c-4ecb-8ce4-cebc804fe70a",
4-
"firstname": "Jan",
5-
"lastname": "Sch\u00e4dlich",
6-
"email": "jan.schadlich@siemens.com",
7-
"workshops": [
8-
{
9-
"identifier": "abba667a-96ae-4f75-9b71-97819b682e8d",
10-
"title": "RESTful Webservices in Symfony",
11-
"workshop_date": "2021-12-07",
12-
"attendees": [
13-
"Jan Sch\u00e4dlich"
14-
]
15-
}
16-
]
17-
}
18-
]
1+
{
2+
"items": [
3+
{
4+
"identifier": "803449f4-9a4c-4ecb-8ce4-cebc804fe70a",
5+
"firstname": "Jan",
6+
"lastname": "Sch\u00e4dlich",
7+
"email": "jan.schadlich@siemens.com",
8+
"workshops": [
9+
{
10+
"identifier": "abba667a-96ae-4f75-9b71-97819b682e8d",
11+
"title": "RESTful Webservices in Symfony",
12+
"workshop_date": "2021-12-07",
13+
"attendees": [
14+
"Jan Sch\u00e4dlich"
15+
]
16+
}
17+
]
18+
}
19+
],
20+
"total": 1,
21+
"count": 1
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"items": [
3+
{
4+
"identifier": "4878f198-36ab-4fe3-8189-19662a9764fa",
5+
"firstname": "a",
6+
"lastname": "1",
7+
"email": "a1@test.de",
8+
"workshops": []
9+
},
10+
{
11+
"identifier": "e942ce16-27c2-494f-9d93-03412da980c5",
12+
"firstname": "b",
13+
"lastname": "2",
14+
"email": "b2@test.de",
15+
"workshops": []
16+
},
17+
{
18+
"identifier": "4714fb8a-83d8-49af-abbf-7c68fc6c9656",
19+
"firstname": "c",
20+
"lastname": "3",
21+
"email": "c3@test.de",
22+
"workshops": []
23+
}
24+
],
25+
"total": 5,
26+
"count": 3
27+
}

0 commit comments

Comments
 (0)