Skip to content

Commit 009755e

Browse files
committed
code challenge 4 solution
1 parent 3e756f9 commit 009755e

19 files changed

+483
-48
lines changed

CODING-CHALLENGE-5.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# RESTful Webservices in Symfony
2+
3+
## Coding Challenge 5 - Content Negotiation
4+
5+
### Tasks
6+
7+
- set the correct format option (JSON or XML) of the current Request
8+
- read the `Accept` request header and negotiate the content-type using Will Durand's negotiation library
9+
10+
### Solution
11+
12+
- require the willdurand/negotiation library: `composer require willdurand/negotiation`
13+
- create a `ContentNegotiator` class, use the `RequestStack` and implement a method to retrieve
14+
the negotiated request format (`json` should be the default request format)
15+
- create a `RequestFormatListener`, subscribe on the `kernel.request` event (priority: 8) and
16+
use the `ContentNegotiator` to set the request's request format
17+
- adjust all your Controllers, Normalizers and Data Transfer Objects to provide your representation of
18+
your resources in the format accepted by the client
19+
20+
#### Hints
21+
22+
You can get the best fitting format by using:
23+
24+
```
25+
$negotiator = new Negotiator();
26+
$acceptHeader = $negotiator->getBest($request->getAcceptableContentTypes(), self::ACCEPTED_CONTENT_TYPES);
27+
```

src/Pagination/AttendeeCollectionFactory.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,24 @@
66

77
use App\Repository\AttendeeRepository;
88
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
9+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
910

1011
final class AttendeeCollectionFactory extends PaginatedCollectionFactory
1112
{
1213
public function __construct(
13-
private AttendeeRepository $attendeeRepository
14+
private AttendeeRepository $attendeeRepository,
15+
UrlGeneratorInterface $urlGenerator,
1416
) {
17+
parent::__construct($urlGenerator);
1518
}
1619

1720
public function getRepository(): ServiceEntityRepositoryInterface
1821
{
1922
return $this->attendeeRepository;
2023
}
24+
25+
public function getRouteName(): string
26+
{
27+
return 'list_attendee';
28+
}
2129
}

src/Pagination/PaginatedCollection.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,30 @@
44

55
namespace App\Pagination;
66

7+
use Symfony\Component\Serializer\Annotation\SerializedName;
8+
79
final class PaginatedCollection
810
{
911
private array $items;
1012
private int $total;
1113
private int $count;
1214

15+
private array $links;
16+
1317
public function __construct(\Iterator $items, int $total)
1418
{
1519
$this->items = iterator_to_array($items);
1620
$this->total = $total;
1721
$this->count = \count($this->items);
1822
}
1923

24+
public function addLink(string $rel, string $href): self
25+
{
26+
$this->links[$rel]['href'] = $href;
27+
28+
return $this;
29+
}
30+
2031
public function getItems(): array
2132
{
2233
return $this->items;
@@ -31,4 +42,10 @@ public function getCount(): int
3142
{
3243
return $this->count;
3344
}
45+
46+
#[SerializedName('_links')]
47+
public function getLinks(): array
48+
{
49+
return $this->links ?? [];
50+
}
3451
}

src/Pagination/PaginatedCollectionFactory.php

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,62 @@
66

77
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
88
use Doctrine\ORM\Tools\Pagination\Paginator;
9+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
910

1011
abstract class PaginatedCollectionFactory
1112
{
13+
public function __construct(
14+
private UrlGeneratorInterface $urlGenerator
15+
) {
16+
}
17+
1218
abstract public function getRepository(): ServiceEntityRepositoryInterface;
1319

20+
abstract public function getRouteName(): string;
21+
1422
public function create(int $page, int $size): PaginatedCollection
1523
{
16-
$query = $this->getRepository()
17-
->createQueryBuilder('u')
24+
$repository = $this->getRepository();
25+
26+
$query = $repository->createQueryBuilder('u')
1827
->orderBy('u.id', 'asc')
1928
->getQuery()
2029
;
2130

2231
$paginator = new Paginator($query);
2332
$total = count($paginator);
33+
$pageCount = ceil($total / $size);
2434

2535
$paginator
2636
->getQuery()
2737
->setFirstResult($size * ($page - 1))
28-
->setMaxResults($size);
38+
->setMaxResults($size)
39+
;
40+
41+
$paginatedCollection = new PaginatedCollection($paginator->getIterator(), $total);
42+
43+
$routeName = $this->getRouteName();
44+
45+
$paginatedCollection
46+
->addLink('self', $this->urlGenerator->generate($routeName, ['page' => $page, 'size' => $size]));
47+
48+
if (1 < $pageCount) {
49+
$paginatedCollection
50+
->addLink('first', $this->urlGenerator->generate($routeName, ['page' => 1, 'size' => $size]))
51+
->addLink('last', $this->urlGenerator->generate($routeName, ['page' => $pageCount, 'size' => $size]))
52+
;
53+
}
54+
55+
if ($page < $pageCount) {
56+
$paginatedCollection
57+
->addLink('next', $this->urlGenerator->generate($routeName, ['page' => $page + 1, 'size' => $size]));
58+
}
59+
60+
if ($page > 1) {
61+
$paginatedCollection
62+
->addLink('prev', $this->urlGenerator->generate($routeName, ['page' => $page - 1, 'size' => $size]));
63+
}
2964

30-
return new PaginatedCollection($paginator->getIterator(), $total);
65+
return $paginatedCollection;
3166
}
3267
}

src/Pagination/WorkshopCollectionFactory.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,24 @@
66

77
use App\Repository\WorkshopRepository;
88
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
9+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
910

1011
final class WorkshopCollectionFactory extends PaginatedCollectionFactory
1112
{
1213
public function __construct(
13-
private WorkshopRepository $workshopRepository
14+
private WorkshopRepository $workshopRepository,
15+
UrlGeneratorInterface $urlGenerator,
1416
) {
17+
parent::__construct($urlGenerator);
1518
}
1619

1720
public function getRepository(): ServiceEntityRepositoryInterface
1821
{
1922
return $this->workshopRepository;
2023
}
24+
25+
public function getRouteName(): string
26+
{
27+
return 'list_workshop';
28+
}
2129
}

src/Serializer/AttendeeNormalizer.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
namespace App\Serializer;
66

77
use App\Entity\Attendee;
8+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
89
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
910
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
1011
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
1112

1213
final class AttendeeNormalizer implements ContextAwareNormalizerInterface
1314
{
1415
public function __construct(
15-
private ObjectNormalizer $normalizer
16+
private ObjectNormalizer $normalizer,
17+
private UrlGeneratorInterface $urlGenerator,
1618
) {
1719
}
1820

@@ -35,6 +37,16 @@ public function normalize($object, string $format = null, array $context = [])
3537

3638
$context = array_merge($context, $customContext);
3739

38-
return $this->normalizer->normalize($object, $format, $context);
40+
$data = $this->normalizer->normalize($object, $format, $context);
41+
42+
if (\is_array($data)) {
43+
$data['_links']['self']['href'] = $this->urlGenerator->generate('read_attendee', [
44+
'identifier' => $object->getIdentifier(),
45+
]);
46+
47+
$data['_links']['collection']['href'] = $this->urlGenerator->generate('list_attendee');
48+
}
49+
50+
return $data;
3951
}
4052
}

src/Serializer/WorkshopNormalizer.php

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@
55
namespace App\Serializer;
66

77
use App\Entity\Workshop;
8+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
89
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
910
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
1011
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
1112

1213
final class WorkshopNormalizer implements ContextAwareNormalizerInterface
1314
{
1415
public function __construct(
15-
private ObjectNormalizer $normalizer
16+
private ObjectNormalizer $normalizer,
17+
private UrlGeneratorInterface $urlGenerator,
1618
) {
1719
}
1820

@@ -35,6 +37,16 @@ public function normalize($object, string $format = null, array $context = [])
3537

3638
$context = array_merge($context, $customContext);
3739

38-
return $this->normalizer->normalize($object, $format, $context);
40+
$data = $this->normalizer->normalize($object, $format, $context);
41+
42+
if (\is_array($data)) {
43+
$data['_links']['self']['href'] = $this->urlGenerator->generate('read_workshop', [
44+
'identifier' => $object->getIdentifier(),
45+
]);
46+
47+
$data['_links']['collection']['href'] = $this->urlGenerator->generate('list_workshop');
48+
}
49+
50+
return $data;
3951
}
4052
}

tests/Controller/Attendee/__snapshots__/ListControllerTest__test_it_should_list_all_attendees__1.json

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,32 @@
1212
"workshop_date": "2021-12-07",
1313
"attendees": [
1414
"Jan Sch\u00e4dlich"
15-
]
15+
],
16+
"_links": {
17+
"self": {
18+
"href": "\/workshops\/abba667a-96ae-4f75-9b71-97819b682e8d"
19+
},
20+
"collection": {
21+
"href": "\/workshops"
22+
}
23+
}
1624
}
17-
]
25+
],
26+
"_links": {
27+
"self": {
28+
"href": "\/attendees\/803449f4-9a4c-4ecb-8ce4-cebc804fe70a"
29+
},
30+
"collection": {
31+
"href": "\/attendees"
32+
}
33+
}
1834
}
1935
],
2036
"total": 1,
21-
"count": 1
37+
"count": 1,
38+
"_links": {
39+
"self": {
40+
"href": "\/attendees?page=1&size=10"
41+
}
42+
}
2243
}

tests/Controller/Attendee/__snapshots__/ListControllerTest__test_it_should_paginate_attendees with data set show 1st page, 3 items each__1.json

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,61 @@
55
"firstname": "a",
66
"lastname": "1",
77
"email": "a1@test.de",
8-
"workshops": []
8+
"workshops": [],
9+
"_links": {
10+
"self": {
11+
"href": "\/attendees\/4878f198-36ab-4fe3-8189-19662a9764fa"
12+
},
13+
"collection": {
14+
"href": "\/attendees"
15+
}
16+
}
917
},
1018
{
1119
"identifier": "e942ce16-27c2-494f-9d93-03412da980c5",
1220
"firstname": "b",
1321
"lastname": "2",
1422
"email": "b2@test.de",
15-
"workshops": []
23+
"workshops": [],
24+
"_links": {
25+
"self": {
26+
"href": "\/attendees\/e942ce16-27c2-494f-9d93-03412da980c5"
27+
},
28+
"collection": {
29+
"href": "\/attendees"
30+
}
31+
}
1632
},
1733
{
1834
"identifier": "4714fb8a-83d8-49af-abbf-7c68fc6c9656",
1935
"firstname": "c",
2036
"lastname": "3",
2137
"email": "c3@test.de",
22-
"workshops": []
38+
"workshops": [],
39+
"_links": {
40+
"self": {
41+
"href": "\/attendees\/4714fb8a-83d8-49af-abbf-7c68fc6c9656"
42+
},
43+
"collection": {
44+
"href": "\/attendees"
45+
}
46+
}
2347
}
2448
],
2549
"total": 5,
26-
"count": 3
50+
"count": 3,
51+
"_links": {
52+
"self": {
53+
"href": "\/attendees?page=1&size=3"
54+
},
55+
"first": {
56+
"href": "\/attendees?page=1&size=3"
57+
},
58+
"last": {
59+
"href": "\/attendees?page=2&size=3"
60+
},
61+
"next": {
62+
"href": "\/attendees?page=2&size=3"
63+
}
64+
}
2765
}

0 commit comments

Comments
 (0)