18/08/2018 - SYMFONY
Bu örnek, bir önceki request, response ve exception yönetimi yaptığımız örneğe çok benziyor. Aradaki tek fark, controller içinde manuel olarak istek doğrulama metodunu kullanmıyoruz. Bu işlem bir event listener ile yapılıyor. Ben şahsen bu örneği kullanırım.
Composer ile jms/serializer-bundle
ve symfony/validator
paketlerini yükleyin. Ayrıca bundles.php
dosyasına JMS\SerializerBundle\JMSSerializerBundle::class => ['all' => true]
ekini ekleyin.
Tüm controller classları bu classı kullanmalılar.
declare(strict_types=1);
namespace App\Controller;
use App\Model\RequestModelInterface;
use App\Model\ResponseModelInterface;
use App\Util\Request\SerializerUtilInterface;
use Symfony\Component\HttpFoundation\Response;
abstract class AbstractController
{
public const REQUEST_CONTENT_TYPE = 'application/json';
public const RESPONSE_FORMAT = 'json';
private $serializerUtil;
private $queryString;
private $payload;
public function __construct(SerializerUtilInterface $serializerUtil)
{
$this->serializerUtil = $serializerUtil;
}
public function setQueryString(?RequestModelInterface $queryString): void
{
$this->queryString = $queryString;
}
protected function getQueryString(): ?RequestModelInterface
{
return $this->queryString;
}
public function setPayload(?RequestModelInterface$payload): void
{
$this->payload = $payload;
}
protected function getPayload(): ?RequestModelInterface
{
return $this->payload;
}
protected function createResponse(?ResponseModelInterface $content, int $status = Response::HTTP_OK): Response
{
return new Response(
$this->serializerUtil->serializeResponseData($content),
$status,
['Content-Type' => self::REQUEST_CONTENT_TYPE]
);
}
}
Burada sizi ilgilendiren tek şey getQueryString()
, getPayload()
ve createResponse()
metodlarıdır. Geriye kalan sadece gösteri amaçlıdır.
declare(strict_types=1);
namespace App\Controller;
use App\Model\Apple\Result;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\Response;
/**
* @Route("/apples")
*/
class AppleController extends AbstractController
{
/**
* @Route("", methods="GET")
*/
public function getAll(): Response
{
$result = new Result();
$result->data = $this->getQueryString();
$result->createdAt = date('H:i:s');
return $this->createResponse($result);
}
/**
* @Route("", methods="POST")
*/
public function create(): Response
{
$result = new Result();
$result->data = $this->getPayload();
$result->createdAt = date('H:i:s');
return $this->createResponse($result);
}
}
Yeni istek modelleriniz olduğunda onları buraya eklemelisiniz.
parameters:
controllers:
AppleController:
actions:
create: 'App\Model\Apple\Create'
getAll: 'App\Model\Apple\QueryString'
Request ile alakalı tüm modeller bunu kullanmalılar.
declare(strict_types=1);
namespace App\Model;
interface RequestModelInterface
{
}
declare(strict_types=1);
namespace App\Model\Apple;
use App\Model\RequestModelInterface;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
class QueryString implements RequestModelInterface
{
/**
* @Assert\NotBlank(message="Required field.")
*
* @Serializer\Type("integer")
*/
public $page;
/**
* @Serializer\Type("integer")
*/
public $limit;
}
declare(strict_types=1);
namespace App\Model\Apple;
use App\Model\RequestModelInterface;
use JMS\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
class Create implements RequestModelInterface
{
/**
* @Assert\NotBlank(message="Required field.")
*
* @Serializer\Type("string")
*/
public $name;
/**
* @Serializer\Type("string")
*/
public $colour;
}
Response ile alakalı tüm modeller bunu kullanmalılar.
declare(strict_types=1);
namespace App\Model;
interface ResponseModelInterface
{
}
declare(strict_types=1);
namespace App\Model\Apple;
use App\Model\ResponseModelInterface;
class Result implements ResponseModelInterface
{
public $data;
public $createdAt;
}
Kendi yarattığınız tüm exception classlar bunu kullanmalılar.
declare(strict_types=1);
namespace App\Exception;
use Exception;
use Symfony\Component\HttpKernel\Exception\HttpException;
abstract class AbstractApplicationException extends HttpException
{
public function __construct(string $message, int $code, Exception $previous = null)
{
parent::__construct($code, $message, $previous);
}
}
declare(strict_types=1);
namespace App\Exception;
class ValidationException extends AbstractApplicationException
{
}
İstekleri otomatik olarak seri hale getirme ve doğrulama işlemi burada gerçekleşiyor.
declare(strict_types=1);
namespace App\Event\Listener;
use App\Controller\AbstractController;
use App\Util\Request\SerializerUtilInterface;
use App\Util\Request\ValidatorUtilInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class ControllerListener
{
private $serializerUtil;
private $validatorUtil;
public function __construct(
SerializerUtilInterface $serializerUtil,
ValidatorUtilInterface $validatorUtil
) {
$this->serializerUtil = $serializerUtil;
$this->validatorUtil = $validatorUtil;
}
public function onKernelController(FilterControllerEvent $event)
{
if (!$event->isMasterRequest()) {
return;
}
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if (!$controller[0] instanceof AbstractController) {
return;
}
$request = $event->getRequest();
$this->validatorUtil->validateContentType($request->headers->get('content_type'));
$queryStringData = $this->serializerUtil->deserializeQueryString($request, $controller[0]);
$payloadData = $this->serializerUtil->deserializePayload($request, $controller[0]);
if ($queryStringData) {
$this->validatorUtil->validateRequestData($queryStringData);
}
if ($payloadData) {
$this->validatorUtil->validateRequestData($payloadData);
}
$controller[0]->setQueryString($queryStringData);
$controller[0]->setPayload($payloadData);
}
}
services:
App\Event\Listener\ControllerListener:
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
declare(strict_types=1);
namespace App\Event\Listener;
use App\Controller\AbstractController;
use App\Exception\AbstractApplicationException;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
class ExceptionListener
{
private $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function onKernelException(GetResponseForExceptionEvent $event): void
{
if ($event->getException() instanceof AbstractApplicationException) {
$response = $this->handleKnownExceptions($event->getException());
} else {
$response = $this->handleUnknownExceptions($event->getException());
}
$event->setResponse($response);
}
private function handleKnownExceptions(Exception $exception): Response
{
$header = [];
if (Response::HTTP_BAD_REQUEST === $exception->getStatusCode()) {
$header = ['Content-Type' => AbstractController::REQUEST_CONTENT_TYPE];
} else {
$this->logger->error($exception);
}
return new Response($exception->getMessage(), $exception->getStatusCode(), $header);
}
private function handleUnknownExceptions(Exception $exception): Response
{
$this->logger->error($exception);
return new Response('An unknown exception occurred.', Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
services:
App\Event\Listener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
declare(strict_types=1);
namespace App\Util\Request;
use App\Controller\AbstractController;
use App\Model\RequestModelInterface;
use App\Model\ResponseModelInterface;
use Symfony\Component\HttpFoundation\Request;
interface SerializerUtilInterface
{
public function deserializeQueryString(Request $request, AbstractController $controller): ?RequestModelInterface;
public function deserializePayload(Request $request, AbstractController $controller): ?RequestModelInterface;
public function serializeResponseData(?ResponseModelInterface $content): string;
}
declare(strict_types=1);
namespace App\Util\Request;
use App\Controller\AbstractController;
use App\Model\RequestModelInterface;
use App\Model\ResponseModelInterface;
use Exception;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
use Symfony\Component\HttpFoundation\Request;
class SerializerUtil implements SerializerUtilInterface
{
private $serializer;
private $controllers;
public function __construct(
SerializerInterface $serializer,
iterable $controllers
) {
$this->serializer = $serializer;
$this->controllers = $controllers;
}
public function deserializeQueryString(Request $request, AbstractController $controller): ?RequestModelInterface
{
$data = null;
if ($request->query->all()) {
$data = $this->createRequestModel(
$this->getRequstModelName($request, $controller),
json_encode($request->query->all())
);
}
return $data;
}
public function deserializePayload(Request $request, AbstractController $controller): ?RequestModelInterface
{
$data = null;
if ($request->getContent()) {
$data = $this->createRequestModel(
$this->getRequstModelName($request, $controller),
$request->getContent()
);
}
return $data;
}
public function serializeResponseData(?ResponseModelInterface $content): string
{
$context = new SerializationContext();
$context->setSerializeNull(true);
return $this->serializer->serialize($content, AbstractController::RESPONSE_FORMAT, $context);
}
private function getRequstModelName(Request $request, AbstractController $controller): ?string
{
$parts = explode('\\', get_class($controller));
$controllerName = end($parts);
$parts = explode('::', $request->attributes->get('_controller'));
$actionName = end($parts);
try {
return $this->controllers[$controllerName]['actions'][$actionName];
} catch (Exception $e) {
throw new InvalidConfigurationException(sprintf(
'Configuration is missing for %s::%s',
$controllerName,
$actionName
));
}
}
private function createRequestModel(string $requestModelName, string $data): RequestModelInterface
{
return $this->serializer->deserialize($data, $requestModelName, AbstractController::RESPONSE_FORMAT);
}
}
declare(strict_types=1);
namespace App\Util\Request;
use App\Model\RequestModelInterface;
interface ValidatorUtilInterface
{
public function validateContentType(?string $contentType): void;
public function validateRequestData(RequestModelInterface $model): void;
}
declare(strict_types=1);
namespace App\Util\Request;
use App\Controller\AbstractController;
use App\Exception\ValidationException;
use App\Model\RequestModelInterface;
use Doctrine\Common\Inflector\Inflector;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class ValidatorUtil implements ValidatorUtilInterface
{
private $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function validateContentType(?string $contentType): void
{
if ($contentType && AbstractController::REQUEST_CONTENT_TYPE !== $contentType) {
throw new ValidationException(
'Invalid content type header.',
Response::HTTP_UNSUPPORTED_MEDIA_TYPE
);
}
}
public function validateRequestData(RequestModelInterface $model): void
{
$errors = $this->validator->validate($model);
if ($errors->count() > 0) {
throw new ValidationException($this->createErrorMessage($errors), Response::HTTP_BAD_REQUEST);
}
}
private function createErrorMessage(ConstraintViolationListInterface $violations): string
{
$errors = [];
/** @var ConstraintViolation $violation */
foreach ($violations as $violation) {
$errors[Inflector::tableize($violation->getPropertyPath())] = $violation->getMessage();
}
return json_encode(['errors' => $errors]);
}
}
$ curl -i -X POST \
> http://192.168.99.20:81/api/v1/apples \
> -d '{
> "name": "Pink Lady",
> "colour": "pink"
> }'
HTTP/1.1 415 Unsupported Media Type
Content-Type: text/html; charset=UTF-8
Invalid content type header
$ curl -i -X POST \
> http://192.168.99.20:81/api/v1/apples \
> -H 'Content-Type: application/json' \
> -d ''
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": null,
"created_at": "21:27:03"
}
$ curl -i -X POST \
> http://192.168.99.20:81/api/v1/apples \
> -H 'Content-Type: application/json' \
> -d '{
> "name": "Pink Lady",
> "colour": "pink"
> }'
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"name": "Pink Lady",
"colour": "pink"
},
"created_at": "21:26:03"
}
$ curl -i -X POST \
> http://192.168.99.20:81/api/v1/apples \
> -H 'Content-Type: application/json' \
> -d '{
> "name": "",
> "colour": "pink"
> }'
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"errors":{"name":"Required field."}}
$ curl -i -X GET \
> 'http://192.168.99.20:81/api/v1/apples' \
> -H 'Content-Type: application/xml'
HTTP/1.1 415 Unsupported Media Type
Content-Type: text/html; charset=UTF-8
Invalid content type header.
$ curl -i -X GET \
> 'http://192.168.99.20:81/api/v1/apples'
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": null,
"created_at": "21:30:37"
}
$ curl -i -X GET \
> 'http://192.168.99.20:81/api/v1/apples?page=1&limit=10'
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {
"page": 1,
"limit": 10
},
"created_at": "21:30:00"
}