28 September 2024

[Symfony • Laravel] OpenAPI

Komparasi OpenAPI di Symfony dan Laravel

Wohh tunjek point 🥳

Gak juga. Maksud saya sedikit2 aja, sampai jadi ……………………………. gak tau jadi apa nanti 🙃


Jadi rencananya saya akan melakukan komparasi dalam konteks REST-API. Dengan kasus sederhana, namun memenuhi kriteria REST-API pada umumnya. Sehingga materi komparasi ini nanti bisa digunakan sebagai modal berkode serius. Bukan hanya sekadar membandingkan dua framework, apalagi sampe berantem, kayak buzzer aja 😮‍💨

Nah, di tulisan pertama ini saya akan mulai dari membuat OpenAPI alias Swagger.

Btw, API Platform sudah support Laravel juga lo, ngapain bikin manual begini? 🙄

Masalahnya tulisan ini dimulai sebelum API Platform rilis untuk Laravel 😭. Tapi gpp, API Platform merupakan pintu, yang di dalamnya kita tetap akan bersinggungan dengan Symfony dan Laravel juga. Jadi tidak perlu ada calo di antara kita dengan Symfony dan Laravel 😶

Calonya ya kamu 😤

Ampuunnnn 😭

OpenAPI

Kebetulan pernah buat tulisan tentang OpenAPI, baca sini dulu gaes 👉 OpenAPI.

Instalasi

Persiapkan PHP versi ≥ 8.2 dulu yak, lalu dilanjutkan dengan instalasi Composer.

  • PHP
    • Untuk macOS bisa melalui Homebrew.
    • Untuk Windows bisa melalui download binary.
    • Untuk Linux bisa melalui package management bawaan.
  • Composer

Instalasi PHP di Windows boleh pakai paket hemat Laragon gak? 🤔

Boleh dongs, masak gak boleh 😘

Symfony

Selain PHP dan Composer, untuk Symfony saya sarankan juga melakukan instalasi Symfony-CLI untuk menunjang manajemen pengembangan aplikasi dengan Symfony. Setelah semua sudah siap, lakukan langkah2 instalasi berikut.

Cek kebutuhan pengembangan aplikasi dengan Symfony, sampai muncul keterangan “Your system is ready to run Symfony projects”. Jika belum berhasil, lengkapi dulu syarat2 tersebut.

symfony check:requirements

Membuat aplikasi dengan Symfony. symfony-api bisa diganti apa aja ya, terserah kalian.

symfony new symfony-api --version="7.1.*"

Coba jalankan aplikasi, lalu buka browser dan masuk ke alamat https://symfony-api.wip.

cd symfony-api && symfony server:start

Jika ada masalah, kalian bisa ikuti petunjuk detailnya di sini. Atau bisa langsung dengan mengakses http://localhost:8000 tanpa local domain.

symfony_welcome

Laravel

Laravel sebenarnya juga memiliki installer, tapi saya belum bisa merekomendasikannya karena perannya hanya sebatas sebagai installer. Jadi cukup kita manfaatkan Composer saja ya gaes.

Langsung aja lakukan instalasi. laravel-api juga bisa kalian ganti bebas.

composer create-project laravel/laravel laravel-api

Coba jalankan aplikasi, lalu buka browser dan masuk ke alamat http://localhost:8001. Diberi tambahan --port=8001 agar tidak bentrok dengan server-nya Symfony.

cd laravel-api && php artisan serve --port=8001

laravel_welcome

Instalasi OpenAPI

Baik Symfony maupun Laravel, sama2 punya paket khusus untuk menyediakan fitur OpenAPI. Sehingga implementasinya pun cukup mudah.

Hmmm cukup mudah, mencurigakan... 🙄

Symfony

Di Symfony terdapat bundle (sebutan paket di Symfony) untuk menyediakan fitur OpenAPI, yaitu NelmioApiDocBundle.

Instalasi menggunakan Composer.

composer require nelmio/api-doc-bundle twig asset

Dilanjut dengan membuka comment di config/routes/nelmio_api_doc.yaml bagian app.swagger_ui sehingga menjadi seperti berikut.

app.swagger_ui:
  path: /api/doc
  methods: GET
  defaults: { _controller: nelmio_api_doc.controller.swagger_ui }

Lakukan penyesuaian seperlunya di config/packages/nelmio_api_doc.yaml misalnya menjadi seperti ini.

nelmio_api_doc:
  documentation:
    info:
      title: Symfony API
      description: Mainan API dengan Symfony
      version: 1.0.0
    accept_type: 'application/json'
    body_format:
      formats: ['json']
      default_format: 'json'
    request_format:
      formats:
        json: 'application/json'
  areas: # to filter documented areas
    path_patterns:
      - ^/api(?!/doc$) # Accepts routes under /api except /api/doc

Selesai, buka OpenAPI melalui https://symfony-api.wip/api/doc.

symfony_nelmio

Laravel

Di Laravel, yang saya tahu, paket yang banyak digunakan dan masih aktif maintenis sampai saat ini adalah L5-Swagger.

Langsung sikat aja gaes.

composer require darkaonline/l5-swagger

Tambahkan nilai berikut ke .env.

L5_SWAGGER_GENERATE_ALWAYS=true

Publikasikan konfigurasi L5-Swagger.

php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"

Buka OpenAPI melalui http://127.0.0.1:8001/api/documentation.

laravel_swagger

Tenang, error tersebut wajar. Karena butuh sentuhan lagi untuk menghasilkan tampilan yang kurang lebih seperti yang dihasilkan NelmioApiDocBundle. Tapi untuk persiapan OpenAPI, keduanya sudah cukup, lanjut.

Bentar2, istirahat dulu 🥵

Gak sambil lari kan gaes? 😶

Database dan entity (model)

Nah, urusan database ini terserah ya mau pakai database apa. SQLite boleh, PostgreSQL boleh, MySQL boleh, SQL Server boleh, silakan tentukan sendiri. Yang saya contohkan di bawah ini menggunakan PostgreSQL.

Yaudah, saya pakai SQLite 😋

Nahhh 🔥

Symfony

Instalasi kebutuhan database management di Symfony.

composer require symfony/orm-pack
composer require --dev symfony/maker-bundle

Di file .env, tentukan koneksinya sesuai database yang kalian pilih.

DATABASE_URL="postgresql://<user>:<password>@127.0.0.1:5432/<dbName>?serverVersion=<serverVersion>&charset=utf8"

Buat database. Nama database yang terbuat akan sesuai dengan <dbName> yang ditentukan sebelumnya.

symfony console doctrine:database:create

Buat entity. Entity adalah representasi tabel di database yang akan digunakan di aplikasi Symfony. Buat dengan nama Product, dengan kolom name tipe string, price tipe decimal, createdAt tipe datetime_immutable, dan updatedAt tipe datetime_immutable.

symfony console make:entity

Hasil generasinya kurang lebih seperti berikut, menjadi file src/Entity/Product.php.

class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 50)]
    private ?string $name = null;

    #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2)]
    private ?string $price = null;

    #[ORM\Column]
    private ?\DateTimeImmutable $createdAt = null;

    #[ORM\Column]
    private ?\DateTimeImmutable $updatedAt = null;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): static
    {
        $this->name = $name;

        return $this;
    }

    public function getPrice(): ?string
    {
        return $this->price;
    }

    public function setPrice(string $price): static
    {
        $this->price = $price;

        return $this;
    }

    public function getCreatedAt(): ?\DateTimeImmutable
    {
        return $this->createdAt;
    }

    public function setCreatedAt(\DateTimeImmutable $createdAt): static
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    public function getUpdatedAt(): ?\DateTimeImmutable
    {
        return $this->updatedAt;
    }

    public function setUpdatedAt(\DateTimeImmutable $updatedAt): static
    {
        $this->updatedAt = $updatedAt;

        return $this;
    }
}

Buat file migration.

symfony console make:migration

Lakukan migrasi, database siap digunakan.

symfony console doctrine:migrations:migrate

Laravel

Di Laravel, urusan database sudah tersedia tanpa instalasi paket tambahan, tinggal sikat aja.

Pertama ubah file .env, tentukan koneksinya sesuai database yang kalian pilih. Lalu buat database manual sesuai <dbName> yang ditentukan.

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=<dbName>
DB_USERNAME=<user>
DB_PASSWORD=<password>

Reload config. Lakukan setiap kali mengubah nilai dari .env.

php artisan config:cache

Buat model Product, yang juga merepresentasikan tabel di database, melalui migration. Yups, bedanya dengan entity di Symfony, model di Laravel belum merepresentasikan tabel yang sebenarnya.

php artisan make:model Product --migration

Hasil generasi modelnya kurang lebih seperti berikut. Lihat, yang mewakili tabel hanya nama modelnya saja.

class Product extends Model
{
    use HasFactory;
}

Ubah file migration yang telah terbuat di bagian create products, menjadi seperti yang ada di Symfony. Nah, file migration ini yang sebenarnya merupakan representasi tabel di database.

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->timestamps();

    // tambahkan name dan price
    $table->string('name', 50);
    $table->decimal('price', 10, 2);
});

Lakukan migrasi, database siap digunakan.

php artisan migrate

Woke gaes, urusan database selesai 😋

Huwaaaaa 🥵

Kan, jangan sambil lari gaes 🙄

Routing dan controller

Selanjutnya kita akan buat endpoint yang tersedia untuk OpenAPI.

Symfony

Buat controller ProductController.

symfony console make:controller ProductController --no-template

Kurang lebih akan menghasilkan kode seperti ini.

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

class ProductController extends AbstractController
{
    #[Route('/product', name: 'app_product')]
    public function index(): JsonResponse
    {
        return $this->json([
            'message' => 'Welcome to your new controller!',
            'path' => 'src/Controller/ProductController.php',
        ]);
    }
}

Nah, mari kita sesuaikan beberapa hal terutama di method index() agar dapat dikenali sebagai endpoint GET /api/products di OpenAPI.

<?php

namespace App\Controller;

use App\Entity\Product;
use Nelmio\ApiDocBundle\Annotation\Model;
use OpenApi\Attributes as OA;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;

final class ProductController extends AbstractController
{
    /**
     * Retrieves the collection of product resources
     */
    #[Route('/api/products', methods: ['GET'])]
    #[OA\Tag(name: 'Products')]
    #[OA\Parameter(
        name: 'name',
        in: 'query',
        description: 'Search by product name',
        schema: new OA\Schema(type: 'string'),
    )]
    #[OA\Response(
        response: 200,
        description: 'Product collection',
        content: new OA\JsonContent(
            type: 'array',
            items: new OA\Items(ref: new Model(type: Product::class))
        )
    )]
    public function findMany(): JsonResponse
    {
        return $this->json([]);
    }
}
  1. Method index() diubah menjadi findMany(), dengan keluaran array kosong [] untuk sementara.
  2. Attribute Route diubah menjadi #[Route('/api/products', methods: ['GET'])].
  3. Tambah attribute OA\Tag sebagai segmen di OpenAPI, dengan nama Product.
  4. Tambah attribute OA\Parameter sebagai parameter yang diakomodir oleh endpoint dalam bentuk query string, kita sediakan name untuk kebutuhan pencarian data berdasarkan nama produk.
  5. Tambah attribute OA\Response sebagai gambaran response yang dihasilkan dari endpoint, dalam bentuk array dengan schema dari entity Product.

Refresh halaman https://symfony-api.wip/api/doc, buka endpoint GET /api/products, tampilannya kira2 seperti ini. Try it out!

symfony_api_products

Laravel

Dimulai dari instalasi API routes dulu. Yang kita butuhkan dari perintah ini sebenarnya hanya untuk mendaftarkan manajemen route khusus API, meskipun turut terinstal juga Laravel Sanctum yang sementara ini belum kita butuhkan.

php artisan install:api

Buat controller ProductController.

php artisan make:controller ProductController --api

Controller yang dihasilkan kurang lebih seperti ini.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProductController extends Controller
{
    // sudah ada banyak method yang tersedia
}

Mari kita sesuaikan seperti yang ada di Symfony sebelumnya.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use OpenApi\Attributes as OA;

class ProductController extends Controller
{
    /**
     * Retrieves the collection of product resources
     */
    #[OA\Get(path: '/api/products', tags: ['Product'])]
    #[OA\Parameter(
        name: 'name',
        in: 'query',
        description: 'Search by product name',
        schema: new OA\Schema(type: 'string'),
    )]
    #[OA\Response(
        response: 200,
        description: 'Product collection',
        content: new OA\JsonContent(
            type: 'array',
            items: new OA\Items(ref: '#/components/schemas/Product')
        ),
    )]
    public function findMany(): JsonResponse
    {
        return response()->json([]);
    }
}
  1. Hapus semua method, buat method baru bernama findMany().
  2. Semua struktur OpenAPI sama persis seperti yang ada di Symfony, hanya saja ada pendekatan yang berbeda. Di Laravel, deklarasi endpoint menggunakan OA\Get disertai tags.
  3. Schema Product-nya dideklarasikan di model Product dan dicantumkan dengan format #/components/schemas/<schemaName>.

Nyok buat schema-nya di model Product.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use OpenApi\Attributes as OA;

#[OA\Schema(
    properties: [
        new OA\Property(
            property: 'name',
            type: 'string',
        ),
        new OA\Property(
            property: 'price',
            type: 'string',
        ),
    ]
)]
class Product extends Model
{
    use HasFactory;
}

Ingat halaman OpenAPI dari Laravel yang masih error? Yups, mari kita beri sentuhan di app/Http/Controllers/Controller.php.

<?php

namespace App\Http\Controllers;

use OpenApi\Attributes as OA;

#[OA\OpenApi(
    info: new OA\Info(
        version: '1.0.0',
        title: 'Laravel API',
        description: 'Mainan API dengan Laravel'
    )
)]
abstract class Controller
{
    //
} 

Woke, urusan OpenAPI sudah selesai, tapi urusan routing belum. Ubah routes/api.php sesuai dengan kebutuhan seperti di Symfony. Route yang dibuat di bawah ini akan menghasilkan endpoint /api/products.

<?php

use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

Route::prefix('products')->group(function () {
    Route::get('/', [ProductController::class, 'findMany']);
});

Selesai, refresh halaman http://localhost:8001/api/documentation, buka endpoint GET /api/products, tampilannya kira2 seperti ini. Try it out!

laravel_api_products

Selesai

Sudah2, cukup… 🥵

Iya iya… 😓 Btw, kamu dapat apa dari praktik ini gaes?

Oke begini. Ternyata OpenAPI itu sekadar eksposur endpoint aja ya, bukan endpoint itu sendiri. Misalnya di Laravel, terlihat jelas bedanya antara kita menyediakan endpoint dengan menyediakan endpoint untuk OpenAPI. Deklarasi endpoint ada di file routes/api.php, sedangkan deklarasi endpoint untuk OpenAPI berada di file berbeda.

Keren sekali. Yups betul gaes, itu sebabnya OpenAPI ini bisa juga disebut dokumentasi API. Tujuannya agar pihak yang memanfaatkan API memiliki referensi teknis tentang apa dan bagaimana penggunaannya. Misalnya dalam suatu organisasi, tim teknis dibagi menjadi backend dan frontend. OpenAPI adalah jembatan komunikasi teknis antara frontend dan backend.

Ada lagi gaes?

Lanjut gaes, belum apa2 ini 😋

Wooooo katanya sudah cukup 😤