Skip to content

Commit

Permalink
code challenge 4 solution
Browse files Browse the repository at this point in the history
  • Loading branch information
jschaedl committed Sep 28, 2023
1 parent 1d5573f commit 22c8ed1
Show file tree
Hide file tree
Showing 19 changed files with 475 additions and 46 deletions.
27 changes: 27 additions & 0 deletions CODING-CHALLENGE-5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# RESTful Webservices in Symfony

## Coding Challenge 5 - Content Negotiation

### Tasks

- set the correct format option (JSON or XML) of the current Request
- read the `Accept` request header and negotiate the content-type using Will Durand's negotiation library

### Solution

- require the willdurand/negotiation library: `composer require willdurand/negotiation`
- create a `ContentNegotiator` class, use the `RequestStack` and implement a method to retrieve
the negotiated request format (`json` should be the default request format)
- create a `RequestFormatListener`, subscribe on the `kernel.request` event (priority: 8) and
use the `ContentNegotiator` to set the request's request format
- adjust all your Controllers, Normalizers and Data Transfer Objects to provide your representation of
your resources in the format accepted by the client

#### Hints

You can get the best fitting format by using:

```
$negotiator = new Negotiator();
$acceptHeader = $negotiator->getBest($request->getAcceptableContentTypes(), self::ACCEPTED_CONTENT_TYPES);
```
10 changes: 9 additions & 1 deletion src/Pagination/AttendeeCollectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@

use App\Repository\AttendeeRepository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

final class AttendeeCollectionFactory extends PaginatedCollectionFactory
{
public function __construct(
private readonly AttendeeRepository $attendeeRepository
private readonly AttendeeRepository $attendeeRepository,
UrlGeneratorInterface $urlGenerator,
) {
parent::__construct($urlGenerator);
}

public function getRepository(): ServiceEntityRepositoryInterface
{
return $this->attendeeRepository;
}

public function getRouteName(): string
{
return 'list_attendee';
}
}
12 changes: 12 additions & 0 deletions src/Pagination/PaginatedCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,28 @@

namespace App\Pagination;

use Symfony\Component\Serializer\Annotation\SerializedName;

final class PaginatedCollection
{
public readonly array $items;
public readonly int $total;
public readonly int $count;

#[SerializedName('_links')]
public array $links;

public function __construct(\Iterator $items, int $total)
{
$this->items = iterator_to_array($items);
$this->total = $total;
$this->count = \count($this->items);
}

public function addLink(string $rel, string $href): self
{
$this->links[$rel]['href'] = $href;

return $this;
}
}
38 changes: 36 additions & 2 deletions src/Pagination/PaginatedCollectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,19 @@

use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

abstract class PaginatedCollectionFactory
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator
) {
}

abstract public function getRepository(): ServiceEntityRepositoryInterface;

abstract public function getRouteName(): string;

public function create(int $page, int $size): PaginatedCollection
{
$query = $this->getRepository()
Expand All @@ -21,12 +29,38 @@ public function create(int $page, int $size): PaginatedCollection

$paginator = new Paginator($query);
$total = count($paginator);
$pageCount = (int) ceil($total / $size);

$paginator
->getQuery()
->setFirstResult($size * ($page - 1))
->setMaxResults($size);
->setMaxResults($size)
;

$paginatedCollection = new PaginatedCollection($paginator->getIterator(), $total);

$routeName = $this->getRouteName();

$paginatedCollection
->addLink('self', $this->urlGenerator->generate($routeName, ['page' => $page, 'size' => $size]));

if (1 < $pageCount) {
$paginatedCollection
->addLink('first', $this->urlGenerator->generate($routeName, ['page' => 1, 'size' => $size]))
->addLink('last', $this->urlGenerator->generate($routeName, ['page' => $pageCount, 'size' => $size]))
;
}

if ($page < $pageCount) {
$paginatedCollection
->addLink('next', $this->urlGenerator->generate($routeName, ['page' => $page + 1, 'size' => $size]));
}

if ($page > 1) {
$paginatedCollection
->addLink('prev', $this->urlGenerator->generate($routeName, ['page' => $page - 1, 'size' => $size]));
}

return new PaginatedCollection($paginator->getIterator(), $total);
return $paginatedCollection;
}
}
10 changes: 9 additions & 1 deletion src/Pagination/WorkshopCollectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@

use App\Repository\WorkshopRepository;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepositoryInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

final class WorkshopCollectionFactory extends PaginatedCollectionFactory
{
public function __construct(
private readonly WorkshopRepository $workshopRepository
private readonly WorkshopRepository $workshopRepository,
UrlGeneratorInterface $urlGenerator,
) {
parent::__construct($urlGenerator);
}

public function getRepository(): ServiceEntityRepositoryInterface
{
return $this->workshopRepository;
}

public function getRouteName(): string
{
return 'list_workshop';
}
}
16 changes: 14 additions & 2 deletions src/Serializer/AttendeeNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Entity\Attendee;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

Expand All @@ -14,7 +15,8 @@ final class AttendeeNormalizer implements NormalizerInterface
public function __construct(
// see: https://github.com/symfony/maker-bundle/issues/1252
#[Autowire(service: 'serializer.normalizer.object')]
private readonly NormalizerInterface $normalizer
private readonly NormalizerInterface $normalizer,
private UrlGeneratorInterface $urlGenerator
) {
}

Expand All @@ -37,7 +39,17 @@ public function normalize($object, string $format = null, array $context = [])

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

return $this->normalizer->normalize($object, $format, $context);
$data = $this->normalizer->normalize($object, $format, $context);

if (\is_array($data)) {
$data['_links']['self']['href'] = $this->urlGenerator->generate('read_workshop', [
'identifier' => $object->getIdentifier(),
]);

$data['_links']['collection']['href'] = $this->urlGenerator->generate('list_workshop');
}

return $data;
}

// see: https://github.com/symfony/symfony-docs/issues/18042
Expand Down
16 changes: 14 additions & 2 deletions src/Serializer/WorkshopNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use App\Entity\Workshop;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;

Expand All @@ -14,7 +15,8 @@ final class WorkshopNormalizer implements NormalizerInterface
public function __construct(
// see: https://github.com/symfony/maker-bundle/issues/1252
#[Autowire(service: 'serializer.normalizer.object')]
private readonly NormalizerInterface $normalizer
private readonly NormalizerInterface $normalizer,
private UrlGeneratorInterface $urlGenerator,
) {
}

Expand All @@ -37,7 +39,17 @@ public function normalize($object, string $format = null, array $context = [])

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

return $this->normalizer->normalize($object, $format, $context);
$data = $this->normalizer->normalize($object, $format, $context);

if (\is_array($data)) {
$data['_links']['self']['href'] = $this->urlGenerator->generate('read_attendee', [
'identifier' => $object->getIdentifier(),
]);

$data['_links']['collection']['href'] = $this->urlGenerator->generate('list_attendee');
}

return $data;
}

// see: https://github.com/symfony/symfony-docs/issues/18042
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,32 @@
"workshop_date": "2022-06-14",
"attendees": [
"Jan Sch\u00e4dlich"
]
],
"_links": {
"self": {
"href": "\/attendees\/abba667a-96ae-4f75-9b71-97819b682e8d"
},
"collection": {
"href": "\/attendees"
}
}
}
]
],
"_links": {
"self": {
"href": "\/workshops\/803449f4-9a4c-4ecb-8ce4-cebc804fe70a"
},
"collection": {
"href": "\/workshops"
}
}
}
],
"total": 1,
"count": 1
"count": 1,
"_links": {
"self": {
"href": "\/attendees?page=1&size=10"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,61 @@
"firstname": "a",
"lastname": "1",
"email": "[email protected]",
"workshops": []
"workshops": [],
"_links": {
"self": {
"href": "\/workshops\/4878f198-36ab-4fe3-8189-19662a9764fa"
},
"collection": {
"href": "\/workshops"
}
}
},
{
"identifier": "e942ce16-27c2-494f-9d93-03412da980c5",
"firstname": "b",
"lastname": "2",
"email": "[email protected]",
"workshops": []
"workshops": [],
"_links": {
"self": {
"href": "\/workshops\/e942ce16-27c2-494f-9d93-03412da980c5"
},
"collection": {
"href": "\/workshops"
}
}
},
{
"identifier": "4714fb8a-83d8-49af-abbf-7c68fc6c9656",
"firstname": "c",
"lastname": "3",
"email": "[email protected]",
"workshops": []
"workshops": [],
"_links": {
"self": {
"href": "\/workshops\/4714fb8a-83d8-49af-abbf-7c68fc6c9656"
},
"collection": {
"href": "\/workshops"
}
}
}
],
"total": 5,
"count": 3
"count": 3,
"_links": {
"self": {
"href": "\/attendees?page=1&size=3"
},
"first": {
"href": "\/attendees?page=1&size=3"
},
"last": {
"href": "\/attendees?page=2&size=3"
},
"next": {
"href": "\/attendees?page=2&size=3"
}
}
}
Loading

0 comments on commit 22c8ed1

Please sign in to comment.