Skip to content

Commit fe54b70

Browse files
committed
code challenge 6 solution
1 parent 1c2569c commit fe54b70

18 files changed

+422
-0
lines changed

CODING-CHALLENGE-7.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# RESTful Webservices in Symfony
2+
3+
## Coding Challenge 7 - Validation
4+
5+
### Tasks
6+
7+
- introduce Symfony's Validator to validate the request content used to create and update workshops and attendees
8+
9+
### Solution
10+
11+
- require the Symfony Validator component: `composer require validator`
12+
- add validation constraints to your model properties (e.g. NotBlank, Email)
13+
- inject the `Validator` Service in your `ValueResolver`s
14+
- for now throw an `UnprocessableEntityHttpException` on validation errors
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ArgumentValueResolver;
6+
7+
use App\Domain\Model\CreateAttendeeModel;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
10+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
11+
use Symfony\Component\Serializer\SerializerInterface;
12+
13+
final class CreateAttendeeModelResolver implements ArgumentValueResolverInterface
14+
{
15+
public function __construct(
16+
private SerializerInterface $serializer,
17+
) {
18+
}
19+
20+
public function supports(Request $request, ArgumentMetadata $argument): bool
21+
{
22+
return CreateAttendeeModel::class === $argument->getType() && 'POST' === $request->getMethod();
23+
}
24+
25+
/**
26+
* @return iterable<CreateAttendeeModel>
27+
*/
28+
public function resolve(Request $request, ArgumentMetadata $argument)
29+
{
30+
yield $this->serializer->deserialize(
31+
$request->getContent(),
32+
CreateAttendeeModel::class,
33+
$request->getRequestFormat(),
34+
);
35+
}
36+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ArgumentValueResolver;
6+
7+
use App\Domain\Model\UpdateAttendeeModel;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
10+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
11+
use Symfony\Component\Serializer\SerializerInterface;
12+
13+
final class UpdateAttendeeModelResolver implements ArgumentValueResolverInterface
14+
{
15+
public function __construct(
16+
private SerializerInterface $serializer,
17+
) {
18+
}
19+
20+
public function supports(Request $request, ArgumentMetadata $argument): bool
21+
{
22+
return UpdateAttendeeModel::class === $argument->getType() && 'PUT' === $request->getMethod();
23+
}
24+
25+
/**
26+
* @return iterable<UpdateAttendeeModel>
27+
*/
28+
public function resolve(Request $request, ArgumentMetadata $argument)
29+
{
30+
yield $this->serializer->deserialize(
31+
$request->getContent(),
32+
UpdateAttendeeModel::class,
33+
$request->getRequestFormat(),
34+
);
35+
}
36+
}
File renamed without changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Attendee;
6+
7+
use App\Domain\AttendeeCreator;
8+
use App\Domain\Model\CreateAttendeeModel;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Component\Routing\Annotation\Route;
12+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
13+
use Symfony\Component\Serializer\SerializerInterface;
14+
15+
#[Route('/attendees', name: 'create_attendee', methods: ['POST'])]
16+
final class CreateController
17+
{
18+
public function __construct(
19+
private AttendeeCreator $attendeeCreator,
20+
private SerializerInterface $serializer,
21+
private UrlGeneratorInterface $urlGenerator,
22+
) {
23+
}
24+
25+
public function __invoke(Request $request, CreateAttendeeModel $createAttendeeModel)
26+
{
27+
$createdAttendee = $this->attendeeCreator->create($createAttendeeModel);
28+
29+
$serializedCreatedAttendee = $this->serializer->serialize($createdAttendee, $request->getRequestFormat());
30+
31+
return new Response($serializedCreatedAttendee, Response::HTTP_CREATED, [
32+
'Location' => $this->urlGenerator->generate('read_attendee', [
33+
'identifier' => $createdAttendee->getIdentifier(),
34+
], UrlGeneratorInterface::ABSOLUTE_URL),
35+
]);
36+
}
37+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Attendee;
6+
7+
use App\Domain\AttendeeUpdater;
8+
use App\Domain\Model\UpdateAttendeeModel;
9+
use App\Entity\Attendee;
10+
use Symfony\Component\HttpFoundation\Request;
11+
use Symfony\Component\HttpFoundation\Response;
12+
use Symfony\Component\Routing\Annotation\Route;
13+
14+
#[Route('/attendees/{identifier}', name: 'update_attendee', methods: ['PUT'])]
15+
final class UpdateController
16+
{
17+
public function __construct(
18+
private AttendeeUpdater $attendeeUpdater,
19+
) {
20+
}
21+
22+
public function __invoke(Request $request, Attendee $attendee, UpdateAttendeeModel $updateAttendeeModel)
23+
{
24+
$this->attendeeUpdater->update($attendee, $updateAttendeeModel);
25+
26+
return new Response(null, Response::HTTP_NO_CONTENT);
27+
}
28+
}

src/Domain/AttendeeCreator.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Domain;
6+
7+
use App\Domain\Model\CreateAttendeeModel;
8+
use App\Entity\Attendee;
9+
use Doctrine\ORM\EntityManagerInterface;
10+
use Ramsey\Uuid\Uuid;
11+
12+
final class AttendeeCreator
13+
{
14+
public function __construct(
15+
private EntityManagerInterface $entityManager,
16+
) {
17+
}
18+
19+
public function create(CreateAttendeeModel $createAttendeeModel): Attendee
20+
{
21+
$newAttendee = new Attendee(
22+
Uuid::uuid4()->toString(),
23+
$createAttendeeModel->firstname,
24+
$createAttendeeModel->lastname,
25+
$createAttendeeModel->email,
26+
);
27+
28+
$this->entityManager->persist($newAttendee);
29+
$this->entityManager->flush();
30+
31+
return $newAttendee;
32+
}
33+
}

src/Domain/AttendeeUpdater.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Domain;
6+
7+
use App\Domain\Model\UpdateAttendeeModel;
8+
use App\Entity\Attendee;
9+
use Doctrine\ORM\EntityManagerInterface;
10+
11+
final class AttendeeUpdater
12+
{
13+
public function __construct(
14+
private EntityManagerInterface $entityManager,
15+
) {
16+
}
17+
18+
public function update(Attendee $attendee, UpdateAttendeeModel $createAttendeeModel): Attendee
19+
{
20+
if ($firstname = $createAttendeeModel->firstname) {
21+
$attendee->updateFirstname($firstname);
22+
}
23+
24+
if ($lastname = $createAttendeeModel->lastname) {
25+
$attendee->updateLastname($lastname);
26+
}
27+
28+
if ($email = $createAttendeeModel->email) {
29+
$attendee->updateEmail($email);
30+
}
31+
32+
$this->entityManager->flush();
33+
34+
return $attendee;
35+
}
36+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Domain\Model;
6+
7+
final class CreateAttendeeModel
8+
{
9+
public string $firstname;
10+
public string $lastname;
11+
public string $email;
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Domain\Model;
6+
7+
final class UpdateAttendeeModel
8+
{
9+
public ?string $firstname = null;
10+
public ?string $lastname = null;
11+
public ?string $email = null;
12+
}

0 commit comments

Comments
 (0)