Sepanjang section ini data sering kita lewatkan sebagai array: kiriman $_POST, hasil fetchAll(), dan balasan berbentuk array dari service. Array memang lentur, tetapi kontraknya tak tertulis — tidak ada yang menjamin kunci email ada atau usia berupa integer sampai kode benar-benar berjalan. Data Transfer Object (DTO) adalah class kecil yang mengemas data untuk perjalanan antar batas: dari request ke service, dari service ke respons. Pada chapter ini kita membangun DTO pendaftaran dengan readonly property dan constructor promotion, melengkapinya dengan mapper dariArray(), menempatkan validasi pada pemiliknya yang sah, dan membentuknya menjadi respons API.

Array Mentah dan Kontrak Tak Tertulis #

Array asosiatif adalah bahasa pengantar bawaan PHP untuk data kiriman — chapter Arrays telah menunjukkan kelenturannya. Kelenturan itu punya harga: kontrak array hanya hidup di ingatan penulisnya. Bandingkan dua tanda tangan berikut:

php
<?php

// Array mentah: kontrak tak tertulis — kunci dan tipe hanya hidup di ingatan penulis.
/** @param array{nama: string, email: string, usia: int} $data */
function prosesPendaftaranArray(array $data): void
{
    // setiap pemanggil harus beriman kunci ada dan tipenya benar
}

// DTO: kontrak tertulis yang diperiksa interpreter saat runtime.
function prosesPendaftaranDto(DataPendaftaran $data): void
{
    // $data->nama pasti string, $data->usia pasti int
}

Anotasi PHPDoc pada fungsi pertama hanyalah saran; interpreter tidak memeriksanya, sehingga kunci yang lupa dikirim baru ketahuan sebagai Warning: Undefined array key saat baris itu berjalan. Salah ketik kunci juga lolos begitu saja, dan nilai "34" berupa string bisa diam-diam dipakai sebagai integer. Fungsi kedua memindahkan seluruh janji itu ke tipe: pemanggil yang lupa mengisi usia tertangkap saat konstruksi, bukan tiga lapisan di bawah. Inilah alasan DTO disebut kontrak tertulis untuk data.

DTO Readonly dengan Constructor Promotion #

DTO pendaftaran kita hanya butuh tiga property dan dua method pembantu:

php
<?php
final class DataPendaftaran
{
    public function __construct(
        public readonly string $nama,
        public readonly string $email,
        public readonly int $usia,
    ) {
    }

    /** @param array<string, mixed> $kiriman */
    public static function dariArray(array $kiriman): self
    {
        return new self(
            nama: trim((string) ($kiriman["nama"] ?? "")),
            email: strtolower(trim((string) ($kiriman["email"] ?? ""))),
            usia: (int) ($kiriman["usia"] ?? 0),
        );
    }

    /** @return array<string, mixed> */
    public function toArray(): array
    {
        return [
            "nama" => $this->nama,
            "email" => $this->email,
            "usia" => $this->usia,
        ];
    }
}

Tiga keputusan desain perlu diperhatikan. Pertama, seluruh property memakai public readonly dengan constructor promotion: field DTO tampil sebagai kontrak yang terlihat, dan sekali dibuat isinya tidak berubah — data yang melintasi batas tidak seharusnya berubah di tengah jalan. Kedua, property sengaja public tanpa getter: DTO adalah pengangkut data yang dibaca langsung oleh lapisan pemakai, dan getter satu-untuk-satu hanya menambah upacara tanpa manfaat. Ketiga, tidak ada satu pun method berisi aturan bisnis — dariArray() dan toArray() hanyalah penerjemah bentuk, bukan pengambil keputusan. Pemakaiannya:

php
<?php
require __DIR__ . "/pendaftaran-dto.php";

$data = new DataPendaftaran(
    nama: "Siti Rahayu",
    email: "[email protected]",
    usia: 29,
);

echo "nama: ", $data->nama, "\n";
echo "email: ", $data->email, "\n";
printf("usia: %d\n", $data->usia);

Output yang Dihasilkan:

text
nama: Siti Rahayu
email: [email protected]
usia: 29

Definisi lengkap tersimpan sebagai berkas pendaftaran-dto.php pada bundle chapter ini. Named arguments membuat konstruksi terbaca jelas tanpa mengingat urutan parameter, dan tipe property menuntaskan kontrak yang dulu harus diimani pada versi array.

Mapper dariArray, Satu Pintu dari Dunia Array #

Sisi luar aplikasi tetap berbicara array — $_POST dan hasil json_decode() memang begitu adanya. Mapper dariArray() menjadi satu-satunya tempat penerjemahan bentuk itu dilakukan:

php
<?php
require __DIR__ . "/pendaftaran-dto.php";

$kiriman = [
    "nama" => "  Budi Santoso  ",
    "email" => "[email protected] ",
    "usia" => "34",
];

$data = DataPendaftaran::dariArray($kiriman);

printf("nama: '%s'\n", $data->nama);
printf("email: '%s'\n", $data->email);
printf("usia: %d\n", $data->usia);

$kosong = DataPendaftaran::dariArray([]);
printf("kiriman kosong -> usia: %d\n", $kosong->usia);

Output yang Dihasilkan:

text
nama: 'Budi Santoso'
email: '[email protected]'
usia: 34
kiriman kosong -> usia: 0

Perhatikan pekerjaan yang terjadi dalam satu method: spasi berlebih di potongan nama dipangkas, huruf besar pada email diseragamkan menjadi huruf kecil, string "34" dikonversi menjadi integer, dan kunci yang hilang ditampung ?? menjadi nilai aman alih-alih memicu warning. Setelah DTO terbentuk, seluruh sisa aplikasi menerima object yang selalu berbentuk — tidak ada lagi trim() dan (int) yang tersebar di banyak tempat. Bila aturan normalisasi berubah, misalnya email tidak lagi dipaksa huruf kecil, yang disentuh satu method saja.

DTO atau Entity #

DTO dan entity sering tertukar karena keduanya “class berisi data”. Perbedaannya mendasar:

AspekDTOEntity
Identitastidak ada; dibandingkan lewat isi fieldada; dibandingkan lewat nomor atau id
Mutasireadonly, sekali dibuatberubah sepanjang use case
Perilakupenerjemah bentuk sajaaturan bisnis hidup di dalamnya
Wilayah hidupbatas antar lapisanjantung lapisan domain
ContohDataPendaftaranFaktur dari chapter Domain Modeling

Keduanya justru saling melengkapi dalam satu alur: controller membaca $_POST dan memanggil dariArray(), service menerima DataPendaftaran lalu memvalidasi isinya, dan jika lolos, domain membentuk Faktur — object yang berbeda, berumur panjang, dan berperilaku. Arah baliknya juga DTO: hasil use case dikemas kembali menjadi DTO respons, bukan entity mentah.

Di Mana Validasi Berada #

Constructor DTO menjamin tipe — usia pasti integer — tetapi tidak menjamin kebenaran bisnis: integer 0 untuk usia sah secara tipe, salah secara aturan. Mengikuti pembagian yang kita tetapkan di chapter Controller-Service-Repository , validasi aturan use case tetap milik service:

php
<?php
require __DIR__ . "/pendaftaran-dto.php";

/** @return list<string> */
function kesalahanPendaftaran(DataPendaftaran $data): array
{
    $errors = [];
    if ($data->nama === "") {
        $errors[] = "nama wajib diisi";
    }
    if (!filter_var($data->email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = "email tidak valid";
    }
    if ($data->usia < 13) {
        $errors[] = "usia minimal 13 tahun";
    }

    return $errors;
}

$calon1 = new DataPendaftaran("", "[email protected]", 29);
$calon2 = new DataPendaftaran("Budi", "bukan-email", 10);

foreach ([$calon1, $calon2] as $i => $calon) {
    $errors = kesalahanPendaftaran($calon);
    printf("calon %d: %s\n", $i + 1, $errors === [] ? "lolos" : implode("; ", $errors));
}

Output yang Dihasilkan:

text
calon 1: nama wajib diisi
calon 2: email tidak valid; usia minimal 13 tahun

Calon pertama gagal karena nama kosong; calon kedua gagal dua aturan sekaligus. Validasi berbentuk daftar pesan agar controller bebas memutuskan cara melaporkannya — sebagai daftar di halaman HTML atau sebagai respons JSON berstatus 422, persis seperti pola pada chapter sebelumnya.

Bentuk API, toArray sebagai Pratinjau #

Arah terakhir perjalanan data adalah keluar dari aplikasi. Method toArray() menyiapkan bentuk itu:

php
<?php
require __DIR__ . "/pendaftaran-dto.php";

$data = new DataPendaftaran("Siti Rahayu", "[email protected]", 29);

echo json_encode($data->toArray(), JSON_PRETTY_PRINT), "\n";

Output yang Dihasilkan:

text
{
    "nama": "Siti Rahayu",
    "email": "[email protected]",
    "usia": 29
}

Respons JSON kini punya kunci dan urutan yang terkontrol penuh — tidak ada property entity yang tidak sengaja ikut terekspos, dan perubahan bentuk internal aplikasi tidak serta-merta mengubah kontrak API. Pola DTO respons seperti ini adalah bekal utama saat membangun REST API sungguhan di section REST API Development , di mana kontrak request dan response menjadi satu-satunya bahasa bersama antara server dan klien.

Ringkasan #

Data Transfer Object mengubah kontrak data dari ingatan menjadi tipe: property readonly dengan constructor promotion menjamin bentuk, dariArray() menjadi satu pintu penerjemahan dari dunia array, validasi aturan tetap berada di service, dan toArray() menyiapkan bentuk keluar yang stabil. DTO bukan entity — ia pengangkut tanpa identitas dan tanpa perilaku bisnis, dan justru karena itulah ia aman melintasi lapisan. Sepanjang chapter ini kita juga menyuntik dependency dan membaca mode lingkungan dari variabel biasa; nilai yang berubah antar lingkungan membutuhkan pengelolaan yang lebih tertata, dan chapter Configuration Management membahas persis itu.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

pendaftaran-dto.php

Unduh berkas
php
<?php

// Bundle chapter Data Transfer Objects: DTO pendaftaran beserta mapper-nya.

final class DataPendaftaran
{
    public function __construct(
        public readonly string $nama,
        public readonly string $email,
        public readonly int $usia,
    ) {
    }

    /** @param array<string, mixed> $kiriman */
    public static function dariArray(array $kiriman): self
    {
        return new self(
            nama: trim((string) ($kiriman["nama"] ?? "")),
            email: strtolower(trim((string) ($kiriman["email"] ?? ""))),
            usia: (int) ($kiriman["usia"] ?? 0),
        );
    }

    /** @return array<string, mixed> */
    public function toArray(): array
    {
        return [
            "nama" => $this->nama,
            "email" => $this->email,
            "usia" => $this->usia,
        ];
    }
}