Skip to content

Commit

Permalink
code challenge 8 solution
Browse files Browse the repository at this point in the history
  • Loading branch information
jschaedl committed Sep 28, 2023
1 parent c5b6b56 commit 6003738
Show file tree
Hide file tree
Showing 20 changed files with 266 additions and 32 deletions.
10 changes: 10 additions & 0 deletions CODING-CHALLENGE-9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# RESTful Webservices in Symfony

## Coding Challenge 9 - Let's implement the missing pieces

### Tasks

Let's implement the missing pieces:

- deleting attendees and workshops
- adding/removing an attendee to/from a workshop
19 changes: 7 additions & 12 deletions src/ArgumentValueResolver/CreateAttendeeModelResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class CreateAttendeeModelResolver implements ArgumentValueResolverInterface
Expand All @@ -32,21 +32,16 @@ public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
// it returns an iterable, because the controller method argument could be variadic

try {
$model = $this->serializer->deserialize(
$request->getContent(),
CreateAttendeeModel::class,
$request->getRequestFormat(),
);
} catch (\Exception $exception) {
throw new UnprocessableEntityHttpException();
}
$model = $this->serializer->deserialize(
$request->getContent(),
CreateAttendeeModel::class,
$request->getRequestFormat(),
);

$validationErrors = $this->validator->validate($model);

if (\count($validationErrors) > 0) {
// throw a UnprocessableEntityHttpException for now, we will introduce proper ApiExceptions later
throw new UnprocessableEntityHttpException();
throw new ValidationFailedException($model, $validationErrors);
}

yield $model;
Expand Down
19 changes: 7 additions & 12 deletions src/ArgumentValueResolver/UpdateAttendeeModelResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;
use Symfony\Component\Validator\Validator\ValidatorInterface;

final class UpdateAttendeeModelResolver implements ArgumentValueResolverInterface
Expand All @@ -30,21 +30,16 @@ public function supports(Request $request, ArgumentMetadata $argument): bool
*/
public function resolve(Request $request, ArgumentMetadata $argument): iterable
{
try {
$model = $this->serializer->deserialize(
$request->getContent(),
UpdateAttendeeModel::class,
$request->getRequestFormat(),
);
} catch (\Exception $exception) {
throw new UnprocessableEntityHttpException();
}
$model = $this->serializer->deserialize(
$request->getContent(),
UpdateAttendeeModel::class,
$request->getRequestFormat(),
);

$validationErrors = $this->validator->validate($model);

if (\count($validationErrors) > 0) {
// throw a UnprocessableEntityHttpException for now, we will introduce proper ApiExceptions later
throw new UnprocessableEntityHttpException();
throw new ValidationFailedException($model, $validationErrors);
}

yield $model;
Expand Down
24 changes: 24 additions & 0 deletions src/Error/ApiError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace App\Error;

final class ApiError
{
public function __construct(
private readonly string $message,
private readonly string $detail
) {
}

public function getMessage(): string
{
return $this->message;
}

public function getDetail(): string
{
return $this->detail;
}
}
22 changes: 22 additions & 0 deletions src/Error/ApiErrorCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace App\Error;

final class ApiErrorCollection
{
private array $errors;

public function addApiError(ApiError $error): self
{
$this->errors[] = $error;

return $this;
}

public function getErrors(): array
{
return $this->errors;
}
}
84 changes: 84 additions & 0 deletions src/EventListener/ExceptionListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace App\EventListener;

use App\Error\ApiError;
use App\Error\ApiErrorCollection;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Exception\ValidationFailedException;

#[AsEventListener(event: KernelEvents::EXCEPTION, method: 'onKernelException')]
class ExceptionListener
{
public function __construct(
private readonly SerializerInterface $serializer
) {
}

public function onKernelException(ExceptionEvent $event): void
{
$errorCollection = null;

$throwable = $event->getThrowable();

if ($throwable instanceof NotEncodableValueException) {
$errorCollection = $this->handleNotEncodableValueException($throwable);
}
if ($throwable instanceof ValidationFailedException) {
$errorCollection = $this->handleValidationFailedException($throwable);
}

if (null === $errorCollection) {
$errorCollection = new ApiErrorCollection();
$errorCollection->addApiError(
new ApiError(
'Error.',
$throwable->getMessage()
)
);
}

$serializedErrors = $this->serializer->serialize($errorCollection, $event->getRequest()->getRequestFormat());

$event->setResponse(new Response($serializedErrors, Response::HTTP_UNPROCESSABLE_ENTITY));
}

private function handleValidationFailedException(ValidationFailedException $validationFailedException): ApiErrorCollection
{
$errorCollection = new ApiErrorCollection();

/* @var ConstraintViolationInterface $violation */
foreach ($validationFailedException->getViolations() as $violation) {
$errorCollection->addApiError(
new ApiError(
'Validation failed.',
$violation->getPropertyPath().': '.$violation->getMessage()
)
);
}

return $errorCollection;
}

private function handleNotEncodableValueException(NotEncodableValueException $notEncodableValueException): ApiErrorCollection
{
$errorCollection = new ApiErrorCollection();

$errorCollection->addApiError(
new ApiError(
'Encoding failed.',
$notEncodableValueException->getMessage()
)
);

return $errorCollection;
}
}
8 changes: 4 additions & 4 deletions tests/Controller/Attendee/CreateControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ public function test_it_should_throw_an_UnprocessableEntityHttpException(string
$this->browser->request('POST', '/attendees', [], [], [], $requestBody);

static::assertResponseStatusCodeSame(422);
static::assertStringContainsString(
'UnprocessableEntityHttpException',
$this->browser->getResponse()->getContent()
);

$this->assertMatchesJsonSnapshot($this->browser->getResponse()->getContent());
}

public static function provideUnprocessableAttendeeData(): \Generator
{
yield 'no data' => [''];
yield 'empty data' => ['{}'];
yield 'wrong json one' => ['{'];
yield 'wrong json two' => ['}'];
yield 'missing firstname' => ['{"lastname": "Paulsen", "email": "[email protected]"}'];
yield 'missing lastname' => ['{"firstname": "Paul", "email": "[email protected]"}'];
yield 'missing email' => ['{"firstname": "Paul", "lastname": "Paulsen"}'];
Expand Down
8 changes: 4 additions & 4 deletions tests/Controller/Attendee/UpdateControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,15 @@ public function test_it_should_throw_an_UnprocessableEntityHttpException(string
$this->browser->request('PUT', '/attendees/b38aa3e4-f9de-4dca-aaeb-3ec36a9feb6c', [], [], [], $requestBody);

static::assertResponseStatusCodeSame(422);
static::assertStringContainsString(
'UnprocessableEntityHttpException',
$this->browser->getResponse()->getContent()
);

$this->assertMatchesJsonSnapshot($this->browser->getResponse()->getContent());
}

public static function provideUnprocessableAttendeeData(): \Generator
{
yield 'no data' => [''];
yield 'wrong json one' => ['{'];
yield 'wrong json two' => ['}'];
yield 'wrong email' => ['{"email": "paulpaulsende"}'];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"errors": [
{
"message": "Validation failed.",
"detail": "firstname: This value should not be blank."
},
{
"message": "Validation failed.",
"detail": "lastname: This value should not be blank."
},
{
"message": "Validation failed.",
"detail": "email: This value should not be blank."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Validation failed.",
"detail": "email: This value should not be blank."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Validation failed.",
"detail": "firstname: This value should not be blank."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Validation failed.",
"detail": "lastname: This value should not be blank."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Encoding failed.",
"detail": "Syntax error"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Validation failed.",
"detail": "email: This value is not a valid email address."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Encoding failed.",
"detail": "Syntax error"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Encoding failed.",
"detail": "Syntax error"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Encoding failed.",
"detail": "Syntax error"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Validation failed.",
"detail": "email: This value is not a valid email address."
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Encoding failed.",
"detail": "Syntax error"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"errors": [
{
"message": "Encoding failed.",
"detail": "Syntax error"
}
]
}

0 comments on commit 6003738

Please sign in to comment.