Bu örnekte, tüm özel durumları yakalayacağız ve bunları bir günlük dosyasına kaydedeceğiz. Bunun dışında, kullanıcıya uygun bir HTTP durum kodu ve metin mesajı göndereceğiz.


Bir API uygulaması geliştirirken, tüm özel durumları yakalayın ve kendi versiyonunuzu gönderin. Bu şekilde, bu örnekte de gösterildiği gibi özel durum işleme üzerinde tam denetime sahip olursunuz. Buradaki ana amaç, son kullanıcıya anlamlı ve mantıklı cevaplar göndermektir, yanlışlıkla 500 kodları değil!


monolog.yaml


Tüm "monolog.yaml" dosyalarına channels: ['api'] ekini girerek özel durum hatalarını "test.log", "dev.log" ve "prod.log" dosyalarına kaydediyoruz. Diğer bir seçenek ise, yeni bir monolog girdisi oluşturup, özel durum hatalarını farklı bir "log" dosyasına kaydedebilirsiniz.


monolog:
channels: ['api']
handlers:
...

ApiExceptionInterface


Tüm özel exception classlarınız bunu uygulamalıdır.


declare(strict_types=1);

namespace App\Exception;

interface ApiExceptionInterface
{
}

CountryException


Tüm özel exception classlarınız bunun aynısı olmalıdır.


declare(strict_types=1);

namespace App\Exception;

use Exception;
use Symfony\Component\HttpKernel\Exception\HttpException;

class CountryException extends HttpException implements ApiExceptionInterface
{
public function __construct(string $message, int $code, Exception $previous = null)
{
parent::__construct($code, $message, $previous);
}
}

ExceptionListener


Eğer API'niz JSON biçiminde istek doğrulama hataları döndürüyorsa, aşağıdaki yanıt mesajını JSON olarak formatlayıp geri döndürebilirsiniz.


declare(strict_types=1);

namespace App\Event;

use App\Exception\ApiExceptionInterface;
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)
{
if (!$event->getException() instanceof ApiExceptionInterface) {
return;
}

$response = new Response($event->getException()->getMessage(), $event->getException()->getStatusCode());

$event->setResponse($response);

$this->log($event->getException());
}

private function log(ApiExceptionInterface $exception)
{
$log = [
'code' => $exception->getStatusCode(),
'message' => $exception->getMessage(),
'called' => [
'file' => $exception->getTrace()[0]['file'],
'line' => $exception->getTrace()[0]['line'],
],
'occurred' => [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
],
];

if ($exception->getPrevious() instanceof Exception) {
$log += [
'previous' => [
'message' => $exception->getPrevious()->getMessage(),
'exception' => get_class($exception->getPrevious()),
'file' => $exception->getPrevious()->getFile(),
'line' => $exception->getPrevious()->getLine(),
],
];
}

$this->logger->error(json_encode($log));
}
}

services:
App\Event\ExceptionListener:
arguments:
- '@monolog.logger.api'
tags:
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }

CountryController


declare(strict_types=1);

namespace App\Controller;

use App\Service\CountryServiceInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

/**
* @Route("/countries")
*/
class CountryController
{
private $countryService;

public function __construct(CountryServiceInterface $countryService)
{
$this->countryService = $countryService;
}

/**
* @Route("/{id}")
* @Method({"GET"})
*/
public function getOneById(int $id): Response
{
$country = $this->countryService->getOneById($id);

return new JsonResponse($country, Response::HTTP_OK);
}

/**
* @Route("/{id}")
* @Method({"PATCH"})
*/
public function updateOneById(int $id): Response
{
$this->countryService->updateOneById($id);

return new JsonResponse(null, Response::HTTP_NO_CONTENT);
}
}

CountryServiceInterface


declare(strict_types=1);

namespace App\Service;

interface CountryServiceInterface
{
public function getOneById(int $id);

public function updateOneById(int $id);
}

CountryService


declare(strict_types=1);

namespace App\Service;

use App\Entity\Country;
use App\Exception\CountryException;
use RuntimeException;
use Symfony\Component\HttpFoundation\Response;

class CountryService implements CountryServiceInterface
{
public function getOneById(int $id)
{
$country = 'I am not a Country object';
if (!$country instanceof Country) {
throw new CountryException(
sprintf('Country "%d" was not found.', $id),
Response::HTTP_NOT_FOUND
);
}

return $country;
}

public function updateOneById(int $id)
{
try {
$this->internalException();
} catch (RuntimeException $e) {
throw new CountryException(
sprintf('Country "%d" state was changed before.', $id),
Response::HTTP_CONFLICT,
$e
);
}
}

private function internalException()
{
throw new RuntimeException('An internal error was encountered.');
}
}

Testler


Son kullanıcının alacağı cevap.


$ curl -i -X GET http://192.168.99.20:81/api/v1/countries/11

HTTP/1.1 404 Not Found
Server: nginx/1.6.2
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, private
Date: Fri, 06 Jul 2018 22:14:07 GMT

Country "11" was not found.

$ curl -i -X PATCH http://192.168.99.20:81/api/v1/countries/1 -H 'Content-Type: application/json' -d '{"code":"gb"}'

HTTP/1.1 409 Conflict
Server: nginx/1.6.2
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Cache-Control: no-cache, private
Date: Fri, 06 Jul 2018 22:14:56 GMT

Country "1" state was changed before.

Log dosyalarındaki hata girdilerimiz.


[2018-07-07 09:39:35] api.ERROR: {"code":404,"message":"Country \"1\" was not found.","called":{"file":"\/srv\/www\/api\/src\/Controller\/CountryController.php","line":31},"occurred":{"file":"\/srv\/www\/api\/src\/Service\/CountryService.php","line":18}} [] []
[2018-07-07 09:39:48] api.ERROR: {"code":409,"message":"Country \"1\" state was changed before.","called":{"file":"\/srv\/www\/api\/src\/Controller\/CountryController.php","line":42},"occurred":{"file":"\/srv\/www\/api\/src\/Service\/CountryService.php","line":32},"previous":{"message":"An internal error was encountered.","exception":"RuntimeException","file":"\/srv\/www\/api\/src\/Service\/CountryService.php","line":42}} [] []

Anlaşılabilir versiyon.


{
"code": 404,
"message": "Country \"1\" was not found.",
"called": {
"file": "\/srv\/www\/api\/src\/Controller\/CountryController.php",
"line": 31
},
"occurred": {
"file": "\/srv\/www\/api\/src\/Service\/CountryService.php",
"line": 18
}
}

{
"code": 409,
"message": "Country \"1\" state was changed before.",
"called": {
"file": "\/srv\/www\/api\/src\/Controller\/CountryController.php",
"line": 42
},
"occurred": {
"file": "\/srv\/www\/api\/src\/Service\/CountryService.php",
"line": 32
},
"previous": {
"message": "An internal error was encountered.",
"exception": "RuntimeException",
"file": "\/srv\/www\/api\/src\/Service\/CountryService.php",
"line": 42
}
}