DTO Secepat Plain PHP: Code Generation Tanpa Overhead Reflection
PHP Architecture Performance #php #dto #data transfer object #code generation

DTO Secepat Plain PHP: Code Generation Tanpa Overhead Reflection

A
Abd. Asis
6 min read
Bagikan:

Kita sering memilih antara dua pilihan yang sama-sama tidak sempurna saat bekerja dengan Data Transfer Objects di PHP: pakai plain PHP array yang cepat tapi tidak ada type safety, atau pakai library seperti spatie/laravel-data yang nyaman tapi membawa overhead reflection di setiap instantiasi. Pada sistem yang menangani ribuan request per detik, pilihan kedua bisa terasa mahal.

Masalah utamanya ada pada reflection. Library DTO berbasis reflection seperti spatie/laravel-data dan cuyz/valinor melakukan introspeksi class metadata setiap kali objek dibuat — membaca anotasi, memetakan properti, menjalankan transformasi. Ini berguna untuk developer experience, tapi ada harga yang harus dibayar setiap kali new UserDto($data) dipanggil. Di aplikasi yang menginisiasi ribuan DTO per request, biaya itu tidak lagi bisa diabaikan.

php-collective/dto mengambil pendekatan berbeda: alih-alih melakukan introspeksi saat runtime, library ini membangkitkan plain PHP class saat build time. Hasilnya adalah kode yang bisa kita baca, review di pull request, dan langsung dipahami IDE tanpa plugin tambahan — dengan performa yang mendekati kode yang ditulis tangan.

Kenapa Reflection Mahal dan Kapan Itu Masalah

Reflection API di PHP memang powerful, tapi setiap panggilan ke ReflectionClass atau ReflectionProperty memiliki cost. Bayangkan sebuah endpoint yang mengembalikan daftar 500 produk, dan setiap produk dikonversi ke DTO lengkap dengan nested address dan list of tags — itu berarti ribuan instantiasi DTO dalam satu request.

Benchmark dengan 10.000 iterasi menggunakan PHP 8.4 menunjukkan selisih yang cukup signifikan:

Skenariophp-collective/dtospatie/laravel-dataPerbedaan
Simple DTO creation0.60 µs14.77 µs~25x lebih cepat
Complex nested DTOs3.10 µs48.83 µs~16x lebih cepat
Serialization1.20 µs26.95 µs~22x lebih cepat

Plain PHP readonly class memang masih sekitar 2x lebih cepat, tapi generated DTO mendapat fitur lengkap dengan performa yang mendekati native.

Perlu dicatat: untuk aplikasi dengan volume DTO rendah, perbedaan ini sering tidak terasa. Tapi kalau sistem kita memproses ribuan objek per request atau menjalankan batch job besar, angka-angka di atas menjadi relevan.

Cara Kerja: Define, Generate, Pakai

Pendekatan library ini sederhana: kita mendefinisikan struktur DTO dalam satu format konfigurasi, jalankan satu perintah generate, dan PHP class siap digunakan sudah tersedia di direktori output.

Install library via Composer:

composer require php-collective/dto

Mendefinisikan DTO dengan PHP Builder

Format paling fleksibel adalah PHP builder, karena IDE bisa memberikan autocomplete langsung saat kita mengetik konfigurasinya.

Buat file definisi di config/dto.php:

// config/dto.php
use PhpCollective\Dto\Builder\Dto;
use PhpCollective\Dto\Builder\Field;

return [
    Dto::create('UserDto')
        ->namespace('App\\Dto')
        ->fields(
            Field::int('id')->required(),
            Field::string('name')->required(),
            Field::string('email')->required(),
            Field::string('bio')->nullable(),
            Field::dto('address', 'AddressDto')->nullable(),
        ),

    Dto::create('AddressDto')
        ->namespace('App\\Dto')
        ->fields(
            Field::string('street')->required(),
            Field::string('city')->required(),
            Field::string('country')->required(),
            Field::string('postalCode')->nullable(),
        ),
];

Setiap Field merepresentasikan satu properti beserta tipenya. Method required() dan nullable() mengontrol apakah field harus ada saat instantiasi.

Generate PHP Class

Jalankan perintah generate untuk menghasilkan class PHP:

vendor/bin/dto generate

Perintah ini menghasilkan file PHP class di direktori output yang sudah dikonfigurasi. Kode yang dihasilkan adalah plain PHP — tidak ada magic, tidak ada proxy, tidak ada runtime introspeksi.

Generated: src/Dto/UserDto.php
Generated: src/Dto/AddressDto.php

File yang dihasilkan bisa langsung di-commit ke repository sehingga semua anggota tim bisa melihat perubahan struktural DTO di pull request seperti perubahan kode biasa.

Menggunakan DTO yang Dihasilkan

Setelah class tersedia, kita menggunakannya seperti PHP class biasa:

use App\Dto\UserDto;
use App\Dto\AddressDto;

$address = new AddressDto();
$address->setStreet('Jl. Sudirman No. 10');
$address->setCity('Jakarta');
$address->setCountry('ID');

$user = new UserDto();
$user->setId(1);
$user->setName('Budi Santoso');
$user->setEmail('budi@example.com');
$user->setAddress($address);

// Konversi ke array untuk response API
return $user->toArray();

IDE langsung mengenali semua method karena method-method tersebut benar-benar ada dalam file PHP yang dihasilkan, bukan ditangani via __call() atau magic method lainnya.

Format Konfigurasi YAML untuk Sintaks Lebih Ringkas

Jika lebih suka format deklaratif, YAML juga didukung:

# config/dto/user.yaml
UserDto:
  namespace: App\Dto
  fields:
    id:
      type: int
      required: true
    name:
      type: string
      required: true
    email:
      type: string
      required: true
    bio:
      type: string
      nullable: true

Format XML juga tersedia dengan dukungan XSD validation untuk IDE autocomplete yang lebih ketat.

Fitur yang Berguna di Situasi Nyata

Immutable DTO untuk Event Sourcing

Kita bisa mendefinisikan DTO sebagai immutable, di mana setiap modifikasi mengembalikan instance baru:

// Definisi dengan immutable mode
Dto::create('OrderDto')
    ->immutable()
    ->fields(
        Field::int('id')->required(),
        Field::string('status')->required(),
        Field::float('total')->required(),
    ),

Penggunaan setelah generate:

$order = new OrderDto();
$order->setId(42)->setStatus('pending')->setTotal(150000.0);

// withStatus() mengembalikan instance baru, tidak mengubah yang asli
$paidOrder = $order->withStatus('paid');

echo $order->getStatus();    // pending
echo $paidOrder->getStatus(); // paid

Pola ini berguna untuk event sourcing dan functional pipelines di mana mutasi tidak terduga bisa menyebabkan bug yang sulit dilacak.

Field Tracking untuk Partial Update

Saat menangani form update, seringkali kita hanya ingin menyimpan field yang benar-benar diubah user, bukan semua field:

$dto = new UserDto();
$dto->fromArray($request->all(), false, UserDto::TYPE_UNDERSCORED);

// Hanya field yang disentuh yang masuk ke database
$repository->update($userId, $dto->touchedToArray());

touchedToArray() mengembalikan array yang hanya berisi field yang benar-benar di-set sejak instantiasi, sehingga UPDATE query tidak menulis ulang kolom yang tidak diubah.

Collections Bertipe

Untuk relasi one-to-many, library ini menghasilkan typed collection:

// Definisi
Dto::create('OrderDto')
    ->fields(
        Field::int('id')->required(),
        Field::dtoCollection('items', 'OrderItemDto'),
    ),

Class yang dihasilkan memiliki method collection yang type-safe:

$order->addItem($item);      // hanya menerima OrderItemDto
$order->getItems();          // mengembalikan ArrayObject<OrderItemDto>
$order->hasItems();          // boolean check
$order->removeItem($index);  // hapus berdasarkan index

Tanpa ini, kita biasanya menggunakan array biasa yang tidak ada garansi isinya.

OrFail untuk Nullable Field

Setiap field yang nullable secara otomatis mendapat method OrFail:

$user->getBio();       // mengembalikan ?string (bisa null)
$user->getBioOrFail(); // mengembalikan string, throw jika null

Ini membantu static analysis tools seperti PHPStan dan Psalm bekerja lebih akurat — kita menyatakan secara eksplisit bahwa di titik ini nilai tidak boleh null.

Strategi Adopsi Bertahap

Tidak perlu mengkonversi seluruh codebase sekaligus. Cara yang paling efektif adalah memulai dari titik paling menyakitkan:

Mulai dengan response boundary API — konversi array yang dikembalikan controller menjadi DTO. Di sinilah kontrak paling sering berubah dan type safety paling berharga. Kemudian geser ke signature method service layer, lalu ke internal data flow antar komponen. Untuk aplikasi Laravel, tersedia package php-collective/laravel-dto yang menangani integrasi framework.

Hal yang Perlu Dipertimbangkan

  • Build step adalah trade-off. Setiap kali mengubah definisi DTO, perlu menjalankan ulang vendor/bin/dto generate. Ini harus masuk ke CI pipeline dan onboarding documentation.
  • Generated files di-commit atau tidak? Keduanya valid. Commit ke repo memudahkan review di PR; tidak commit berarti generate selalu segar tapi perlu diingat di setiap setup environment.
  • Framework-specific alternatives tetap kompetitif. Jika proyek sudah full Laravel dan tidak ada masalah performa aktual, spatie/laravel-data tetap pilihan yang solid karena integrasi dengan request validation, Eloquent, dan queue-nya sangat matang.
  • Kompatibilitas: Library ini bekerja dengan PHP 8.1+ dan mendukung PHP 8.3/8.4.

Kesimpulan

php-collective/dto membuktikan bahwa kita tidak harus memilih antara performa dan developer experience. Dengan menggeser pekerjaan introspeksi dari runtime ke build time, kita mendapat kode yang transparan, reviewable, dan cepat — sekaligus tetap memiliki type safety, IDE support, dan fitur-fitur praktis seperti immutability, field tracking, dan typed collections. Untuk sistem yang mulai merasakan bottleneck dari reflection overhead, ini bisa menjadi solusi yang worth exploring.

Referensi

  1. 1php-collective/dto — GitHub Repository
  2. 2spatie/laravel-data v4 Documentation — spatie.be
  3. 3Performance Considerations — laravel-data Documentation

Tentang Penulis

Abd. Asis

Abd. Asis

Software Developer dan Laravel Programmer dari Madura, Indonesia. Passionate tentang PHP, Laravel, dan teknologi web modern.

Artikel Terkait

Artikel lain yang mungkin menarik untuk kamu