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

Use validation exception instead of building response in a Controller #68

Merged
merged 1 commit into from
Aug 9, 2020
Merged
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
23 changes: 22 additions & 1 deletion src/Controller/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Paknahad\JsonApiBundle\Controller;

use Paknahad\JsonApiBundle\Exception\ValidationException;
use Paknahad\JsonApiBundle\Transformer;
use Psr\Http\Message\ResponseInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use WoohooLabs\Yin\JsonApi\JsonApi;
use WoohooLabs\Yin\JsonApi\Schema\Document\ErrorDocument;
use WoohooLabs\Yin\JsonApi\Schema\Error\Error;
Expand All @@ -15,17 +17,36 @@
class Controller extends AbstractController
{
private $jsonApi;
/**
* @var ValidatorInterface
*/
private $validator;

public function __construct(JsonApi $jsonApi)
public function __construct(JsonApi $jsonApi, ValidatorInterface $validator)
{
$this->jsonApi = $jsonApi;
$this->validator = $validator;
}

protected function jsonApi(): JsonApi
{
return $this->jsonApi;
}

/**
* @param mixed $object
*/
protected function validate($object): void
{
$errors = $this->validator->validate($object);
if ($errors->count() > 0) {
throw new ValidationException($errors);
}
}

/**
* @deprecated This function is deprecated. Use validate() instead.
*/
protected function validationErrorResponse(ConstraintViolationListInterface $errors): ResponseInterface
{
$errorDocument = new ErrorDocument();
Expand Down
43 changes: 43 additions & 0 deletions src/Document/ValidationErrorDocument.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Paknahad\JsonApiBundle\Document;

use Paknahad\JsonApiBundle\Transformer;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use WoohooLabs\Yin\JsonApi\Schema\Document\ErrorDocument;
use WoohooLabs\Yin\JsonApi\Schema\Error\Error;
use WoohooLabs\Yin\JsonApi\Schema\Error\ErrorSource;
use WoohooLabs\Yin\JsonApi\Schema\JsonApiObject;

class ValidationErrorDocument extends ErrorDocument
{
const STATUS_CODE = 422;

public function __construct(ConstraintViolationListInterface $errors)
{
parent::__construct();
$this->setJsonApi(new JsonApiObject('1.0'));
/** @var ConstraintViolation $fieldError */
foreach ($errors as $fieldError) {
$error = Error::create();
$pointer = '/data/attributes/'.$fieldError->getPropertyPath();

$errorSource = new ErrorSource(
$pointer,
Transformer::validationValueToString($fieldError->getInvalidValue())
);

$error->setSource($errorSource)
->setDetail($fieldError->getMessage())
->setStatus('');

$this->addError($error);
}
}

public function getStatusCode(?int $statusCode = null): int
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this parameter used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function overrides rather complicated base function from AbstractErrorDocument and it needs to have the same signature. Why does getter function have a parameter, you should ask the maintainers of woohoolabs\yin package :)
It is called by Responder class to determine error code of the request. In this case I can't see a use case where we need to change to a different error code for a validation error (it should always be 422). Still, I could always add code that returns that parameter if it is != null.

{
return self::STATUS_CODE;
}
}
18 changes: 15 additions & 3 deletions src/EventSubscriber/JsonApiErrorHandlerEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Paknahad\JsonApiBundle\EventSubscriber;

use Paknahad\JsonApiBundle\Document\ValidationErrorDocument;
use Paknahad\JsonApiBundle\Exception\ValidationException;
use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
Expand Down Expand Up @@ -57,7 +59,7 @@ public function onKernelException(ExceptionEvent $event)
new JsonSerializer()
);

$additionalMeta = \in_array($this->environment, ['dev', 'test']) || true === $this->debug ? $this->getExceptionMeta($exception) : [];
$additionalMeta = $this->getAdditionalMeta($exception);

$response = $responder->genericError(
$this->toErrorDocument($exception, $event->getRequest()->getRequestUri()),
Expand Down Expand Up @@ -97,7 +99,9 @@ protected function toErrorDocument(Throwable $exception, string $url)
$title = 'Internal Server Error';
$statusCode = 500;

if ($exception instanceof ValidatorException) {
if ($exception instanceof ValidationException) {
return new ValidationErrorDocument($exception->getViolations());
} elseif ($exception instanceof ValidatorException) {
$title = $exception->getMessage();
$statusCode = 422;
} elseif ($exception instanceof HttpException) {
Expand All @@ -108,7 +112,6 @@ protected function toErrorDocument(Throwable $exception, string $url)
$statusCode = 401;
}

/** @var ErrorDocument $errorDocument */
$errorDocument = new ErrorDocument();
$errorDocument->setLinks(
DocumentLinks::createWithoutBaseUri()->setSelf(
Expand All @@ -126,4 +129,13 @@ protected function toErrorDocument(Throwable $exception, string $url)

return $errorDocument;
}

private function getAdditionalMeta(Throwable $exception): array
{
if ($exception instanceof ValidationException) {
return [];
}

return \in_array($this->environment, ['dev', 'test']) || true === $this->debug ? $this->getExceptionMeta($exception) : [];
}
}
22 changes: 22 additions & 0 deletions src/Exception/ValidationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Paknahad\JsonApiBundle\Exception;

use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Exception\RuntimeException;

class ValidationException extends RuntimeException
{
private $violations;

public function __construct(ConstraintViolationListInterface $violations)
{
$this->violations = $violations;
parent::__construct($violations);
}

public function getViolations(): ConstraintViolationListInterface
{
return $this->violations;
}
}
15 changes: 4 additions & 11 deletions src/Resources/skeleton/api/controller/Controller.tpl.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use WoohooLabs\Yin\JsonApi\Exception\DefaultExceptionFactory;

/**
Expand All @@ -40,16 +39,13 @@ public function index(<?= $repository_class_name ?> $<?= $repository_var ?>, Res
/**
* @Route("/", name="<?= $route_name ?>_new", methods="POST")
*/
public function new(ValidatorInterface $validator, DefaultExceptionFactory $exceptionFactory): ResponseInterface
public function new(DefaultExceptionFactory $exceptionFactory): ResponseInterface
{
$entityManager = $this->getDoctrine()->getManager();

$<?= $entity_var_name ?> = $this->jsonApi()->hydrate(new <?= $create_hydrator_class_name ?>($entityManager, $exceptionFactory), new <?= $entity_class_name ?>());

$errors = $validator->validate($<?= $entity_var_name ?>);
if ($errors->count() > 0) {
return $this->validationErrorResponse($errors);
}
$this->validate($<?= $entity_var_name ?>);

$entityManager->persist($<?= $entity_var_name ?>);
$entityManager->flush();
Expand All @@ -76,16 +72,13 @@ public function show(<?= $entity_class_name ?> $<?= $entity_var_name ?>): Respon
/**
* @Route("/{<?= $entity_identifier ?>}", name="<?= $route_name ?>_edit", methods="PATCH")
*/
public function edit(<?= $entity_class_name ?> $<?= $entity_var_name ?>, ValidatorInterface $validator, DefaultExceptionFactory $exceptionFactory): ResponseInterface
public function edit(<?= $entity_class_name ?> $<?= $entity_var_name ?>, DefaultExceptionFactory $exceptionFactory): ResponseInterface
{
$entityManager = $this->getDoctrine()->getManager();

$<?= $entity_var_name ?> = $this->jsonApi()->hydrate(new <?= $update_hydrator_class_name ?>($entityManager, $exceptionFactory), $<?= $entity_var_name ?>);

$errors = $validator->validate($<?= $entity_var_name ?>);
if ($errors->count() > 0) {
return $this->validationErrorResponse($errors);
}
$this->validate($<?= $entity_var_name ?>);

$entityManager->flush();

Expand Down