Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

code challenge 4 solution #4

Open
wants to merge 1 commit into
base: challenge-4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading