Symfony : Handling Exception pada API
posted at
Assalamualaikum
Salah satu hal yang paling penting pada saat membangun aplikasi adalah bagaimana cara menghandle exception-nya. Sedikit informasi exception secara singkat adalah bagian dari program yang kita ingin berikan perlakukan khusus di situasi khusus pula. Kondisi ini mirip error, tapi bukan error hanya tidak sesuai dengan ekspektasi kita. Jadi kita perlu menghandle situasi seperti ini (secara istilah disebut handling exception).
Khusus untuk tulisan ini studi kasus yang dilakukan adalah handling exception pada API (Application Programming Interface) dengan menggunakan PHP sebagai bahasa dan Symfony sebagai framework. Sementara untuk exception yang akan kita handle satu saja sebagai contoh, meski pada prakteknya ada banyak exception yang harus dihandle, yakni Bad Request Exception.
Baca Juga : PHP – Annotion Tidak Bekerja
Skenarionya sederhana, yaitu dikirimkan request beserta payload berupa umur:integer ke endpoint dimana terdapat invalid argument yang akan dikirim. Misalnya, argument yang akan diterima adalah argument yang hanya bernilai dibawah 50. Jika diluar ketentuan maka akan diberikan exception. Dengan contoh skenario inilah selanjutnya kita melakukan skenario yang lebih kompleks.
POST /api/number HTTP/1.1 ... value=51
Sementara untuk response exception akan mengikuti pola yang umum digunakan yaitu terdapat error, code dan message.
{ "errors" : "array", "code" : "integer", "message" : "string" }
Selanjutnya mari eksekusi ke project kita.
Membuat Response Model
Sesuai dengan skenario response yang sudah dirancang diatas, kita akan membuat response model. Untuk seluruh response model boleh ditaruh di folder Response di src, atau disesuaikan practice masing-masing. Sementara untuk nama file sendiri akan kita buat ApiProblemResponse
.
<?php namespace App\Response; class ApiProblemResponse { public $code; public $message; public $errors = []; public function __construct($errors, $code, $message) { $this->errors = $errors; $this->code = $code; $this->message = $message; } }
Membuat Exception
Pada source project kita di folder src
silahkan buat folder Exception untuk meletakkan semua exception yang akan kita gunakan. Tahap pertama kita buat interface sebagai kontrak untuk exception kita, sebagai contoh beri saja nama ApiExceptionInterface
.
<?php namespace App\Exception; interface ApiExceptionInteraface extends \Throwable {}
Selanjutnya, sesuai dengan contoh kasus yang akan dibuat yaitu menghandle request untuk umur, maka perlu dibuat exception untuk umur. Tentu pada prakteknya exception bisa lebih kompleks.
Exception ini akan meng-extend class Exception dari built-in PHP dan sekaligus mengimplementasikan kontrak ApiExceptionInterface.
Sekedar informasi, selain exception kontrak yang manual kita buat, symfony juga telah menyediakan client-contracts yang telah datang bersamaan dengan symfony seperti ClientExceptionInterface
, HttpExceptionInterface
dan sebagainya. (cek dokumentasi atau repo untuk selengkapnya)
<?php namespace App\Exception; use Symfony\Component\HttpFoundation\Response; class AgeInvalidException extends \Exception implements ApiExceptionInterface { private $statusCode; public function __construct(string $message = null, int $statusCode = null) { $this->statusCode = ($statusCode) ?? Response::HTTP_BAD_REQUEST; $message = ($message) ?? "Usia melebihi batas yang ditentukan."; parent::__construct($message, $this->statusCode); } public function getStatusCode() { return $this->statusCode; } }
Event Listener
Untuk menangkap exception kita sendiri akan menggunakan fitur event listener milik Symfony. Pertama, buat folder EventListener
di source folder kita, dan untuk listener sebagai contoh kita buat dengan nama ApiExceptionListener
Sesuai dokumentasi symfony, listener akan mengeksekusi secara default sebuah method bernama onKernelException
dan seluruh logic exception kita akan diletakkan disini.
<?php namespace App\EventListener; use App\Exception\ApiExceptionInterface; use App\Response\ApiProblemResponse; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; class ApiExceptionListener { public function onKernelException(ExceptionEvent $event) { if (!$event->getThrowable() instanceof ApiExceptionInterface) { return; } $exception = $event->getThrowable(); $response = new JsonResponse(); $response->setStatusCode($exception->getStatusCode()); $content = $this->createApiResponse($exception); $response->setContent( json_encode($content) ); $event->setResponse($response); } private function createApiResponse(\Throwable $exception) { return new ApiProblemResponse( $exception->getMessage(), $exception->getStatusCode(), Response::$statusTexts[$exception->getStatusCode()] ); } }
Untuk dapat menggunakan Event Listener yang baru kita buat, kita perlu mendaftarkan listener kita agar dikenali oleh symfony. Pada services.yaml
di folder config, cukup tambahkan konfigurasi berikut.
services: # konfigurasi lain App\EventListener\ApiExceptionListener: tags: - { name: kernel.event_listener, event: kernel.exception } # konfigurasi lain
Untuk memastikan listener kita terdaftar, bisa memakai debugger symfony php bin/console debug:event-dispatcher
maka seharusnya pada part kernel.exception listener kita sudah terdaftar.
Implementasi
Untuk penggunaan sederhana saja, cukup throw konkrit class dari exception yang telah kita buat, yaitu AgeInvalidException
. Sesuai skenario sederhana diatas, kita akan memunculkan exception jika value dari age lebih dari 50.
Sebagai contoh implementasi pada sebuah controller yaitu Api\AgeController
dan method index
yang menghandle sebuah route.
<?php namespace App\Controller\Api; use App\Exception\AgeInvalidException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class AgeController extends AbstractController { /** * @Route("/api/age", name="api_age", methods={"POST"}) */ public function index(Request $request): Response { $age = $request->toArray()["age"]; if ($age >= 50) { throw new AgeInvalidException(); // di-throw jika kondisi tidak meenuhi } return $this->json([ "age" => $age ]); } }
Untuk implementasi yang lebih kompleks bisa saja dilakukan di layer logic seperti di service dan bisa dikombinasikan dengan library validator seperti symfony validator, tinggal set rule-nya.
Demo
Untuk pengetesan punya dua skenario, yaitu skenario pertaman dengan usia yang memenuhi kriteria, misal 20 tahun, sedangkan skenario kedua dengan usia 60 tahun.
Request 1:
$ curl -i -H POST http://localhost:8000/api/age -H 'Content-Type: application/json' -d '{"age":20}' HTTP/1.1 200 OK Cache-Control: no-cache, private Content-Type: application/json Date: Fri, 05 Mar 2021 11:15:13 GMT X-Powered-By: PHP/7.3.0 X-Robots-Tag: noindex Content-Length: 10 {"age":20}
Request 2:
$ curl -i -H POST http://localhost:8000/api/age -H 'Content-Type: application/json' -d '{"age":60}' HTTP/1.1 400 Bad Request Cache-Control: no-cache, private Content-Type: application/json Date: Fri, 05 Mar 2021 11:18:36 GMT X-Powered-By: PHP/7.3.0 X-Robots-Tag: noindex Content-Length: 86 {"code":400,"message":"Bad Request","errors":["Usia melebihi batas yang ditentukan."]}
Jika kita lihat payload response yang kita dapatkan sudah seperti yang kita inginkan, dimana setiap ada request age
yang melebihi kriteria (>50) maka akan exception akan ditampilkan beserta struktur json response nya.
{ "code": 400, "message": "Bad Request", "errors": [ "Usia melebihi batas yang ditentukan." ] }
Semoga bermanfaat, terima kasih
Catatan : versi symfony yang digunakan yaitu symfony5, akan sedikit berbeda dengan symfony4