Pada chapter-chapter sebelumnya kita melihat bagaimana Laravel mempercepat pengembangan dengan konvensi dan fitur yang tersedia sejak menit pertama. Symfony mengambil filosofi yang berbeda: framework ini disusun dari komponen-komponen kecil yang saling lepas, mengutamakan konfigurasi eksplisit, dan menjanjikan stabilitas jangka panjang — tiga alasan utama aplikasi enterprise dengan siklus hidup panjang membangun di atasnya. Pada chapter ini kita mempelajari fondasi Symfony: filosofi desainnya, cara memulai proyek, routing, controller dengan Twig, services dengan autowiring, konfigurasi environment, hingga komponen Console. Di akhir chapter kita membandingkan kurva belajarnya dengan Laravel agar keputusan pada chapter pemilihan framework berpijak pada fakta, bukan stigma.

Filosofi Symfony #

Pengembang Symfony menyebut framework-nya sebagai kumpulan komponen yang kebetulan bisa disatukan menjadi framework. Setiap kemampuan inti — routing, validasi, caching, template — tinggal di paket Composer terpisah yang dapat dipakai tanpa framework penuh. Dampaknya terasa pada cara kita berpikir: memakai Symfony berarti menyusun aplikasi dari blok-blok eksplisit, dan bila suatu saat framework penuh terasa berlebihan, blok-blok tersebut tetap layak dipertahankan.

Prinsip kedua adalah konfigurasi eksplisit. Banyak hal yang di framework lain diselesaikan lewat konvensi nama, di Symfony dinyatakan secara tertulis: rute bisa hidup di berkas YAML, service didaftarkan lewat konfigurasi, dan perilaku bawaan didokumentasikan dengan saksama. Konsekuensinya, proyek baru memang membutuhkan lebih banyak berkas konfigurasi di awal. Imbalannya, perilaku aplikasi hampir seluruhnya terbaca dari kode dan konfigurasi — tidak tersembunyi di balik magic yang harus dihafal.

Prinsip ketiga adalah orientasi jangka panjang. Symfony menjaga kontrak kompatibilitas antar versi secara ketat, merilis versi LTS (long-term support) dengan ritme teratur, dan menargetkan aplikasi yang hidup bertahun-tahun dengan tim yang berganti-ganti orang. Orientasi inilah yang menjadikan Symfony pilihan umum untuk sistem berskala besar yang tidak boleh berubah drastis tiap tahun.

Memulai Proyek: skeleton dan Pola webapp #

Proyek Symfony baru dimulai dari kerangka paling ramping bernama symfony/skeleton. Kerangka ini hanya berisi kernel aplikasi, satu rute demo, dan sistem konfigurasi — tanpa template engine, tanpa ORM, tanpa sistem form. Versi lama Symfony menyediakan symfony/website-skeleton yang sudah berisi semuanya; pola modern yang disarankan sekarang adalah memasang paket webapp di atas skeleton, sehingga kita tetap melihat apa saja yang masuk ke aplikasi.

bash
composer create-project symfony/skeleton aplikasi-kerja
bash
composer create-project symfony/skeleton aplikasi-web
cd aplikasi-web
composer require webapp

Perintah pertama hanya menghasilkan kerangka minimal. Perintah composer require webapp pada blok kedua memasang metapackage yang menarik Twig, form, validasi, security, Mailer, dan Doctrine sekaligus. Perbandingan dua titik awal tersebut dirangkum di tabel berikut:

Aspeksymfony/skeleton sajaskeleton + paket webapp
Isi awalKernel, satu rute demo, sistem konfigurasiDitambah Twig, form, validasi, security, Mailer, ORM
Ukuran dependensiSangat rampingBesar, tetapi tetap terkelola Composer
Cocok untukAPI, microservice, aplikasi hibridaAplikasi web klasik dengan halaman penuh

Konsep Bundle #

Bundle adalah unit pemaketan khas Symfony: gabungan kode PHP, konfigurasi, dan definisi service yang bisa dipasang ke aplikasi mana pun. Contohnya DoctrineBundle yang menyambungkan Doctrine ORM ke service container Symfony, atau bundle komunitas seperti pagination dan admin generator. Bundle adalah mekanisme utama ekosistem Symfony berbagi fungsionalitas antar proyek.

Sejak Symfony 4, aplikasi kita sendiri tidak lagi ditulis sebagai bundle. Kode aplikasi cukup berupa class biasa di direktori src/ yang otomatis teregistrasi lewat autowiring; bundle kini berperan khusus untuk kode yang memang dimaksudkan dibagikan lintas aplikasi. Perubahan ini menyederhanakan proyek baru sekaligus memperjelas batas antara “aplikasi saya” dan “paket yang saya distribusikan”.

Routing dengan Attributes dan YAML #

Peran router Symfony sama dengan router buatan sendiri di Membuat Router : memetakan pola URL ke handler. Symfony menawarkan dua gaya deklarasi utama — PHP attributes yang menempel langsung pada method controller, dan berkas config/routes.yaml untuk rute yang hidup di luar kode.

php
<?php
// src/Controller/ArtikelController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ArtikelController extends AbstractController
{
    #[Route('/artikel/{slug}', name: 'artikel_detail', requirements: ['slug' => '[a-z0-9-]+'])]
    public function detail(string $slug): Response
    {
        return $this->render('artikel/detail.html.twig', [
            'slug' => $slug,
        ]);
    }
}
yaml
artikel_detail:
    path: /artikel/{slug}
    controller: App\Controller\ArtikelController::detail
    requirements:
        slug: '[a-z0-9-]+'

Empat bagian penting bekerja pada kedua deklarasi tersebut. Pertama, placeholder {slug} menangkap segmen URL dan nilainya otomatis mengalir ke parameter method $slug — mekanisme yang sama dengan route parameter di router buatan kita. Kedua, opsi requirements membatasi placeholder dengan regex, sehingga URL seperti /artikel/Baru%20Sekali ditolak sebelum mencapai controller. Ketiga, name: 'artikel_detail' memberi rute sebuah identitas untuk reverse routing — menghasilkan URL dari nama raut, bukan menulis string URL secara manual. Keempat, kedua gaya boleh dicampur dalam satu aplikasi: attributes untuk rute milik controller, YAML untuk rute warisan atau rute yang dibangun secara konfiguratif.

Controller dan Twig #

Controller Symfony tidak diwajibkan mewarisi class tertentu — controller hanyalah class PHP biasa yang mengembalikan object Response. Contoh berikut menunjukkan bentuk paling eksplisit: engine template Twig disuntikkan lewat constructor, persis pola dependency injection yang sudah kita kenal.

php
<?php
// src/Controller/HalamanController.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;

final class HalamanController
{
    public function __construct(private readonly Environment $twig)
    {
    }

    public function tentang(): Response
    {
        $konten = $this->twig->render('halaman/tentang.html.twig', [
            'tim' => ['Budi', 'Siti'],
        ]);

        return new Response($konten);
    }
}

Template Twig yang dirender oleh controller di atas berbentuk seperti ini:

twig
{# templates/halaman/tentang.html.twig #}
<h1>Tentang Kami</h1>
<ul>
{% for anggota in tim %}
    <li>{{ anggota }}</li>
{% endfor %}
</ul>

Method tentang() merender template menjadi string HTML, lalu membungkusnya dalam Response — alur yang identik dengan Template Rendering pada framework buatan sendiri. Sintaks {{ anggota }} pada Twig otomatis menerapkan output escaping, sehingga data yang berasal dari pengguna tidak langsung disisipkan mentah ke HTML — topik yang kita dalami di Output Escaping . Dalam praktiknya, kebanyakan controller mewarisi AbstractController agar bisa memakai shortcut $this->render() yang merangkum dua langkah di atas.

Services dan Autowiring #

Service adalah object yang melakukan satu pekerjaan: menghitung harga, mengirim email, membaca konfigurasi. Service container Symfony bertugas membangun object-object tersebut, menyimpannya, dan menyuntikkannya ke siapa pun yang membutuhkan — peran yang sama dengan Dependency Container buatan kita. Fitur andalannya bernama autowiring: container membaca type declaration pada constructor, lalu menebak service mana yang harus disuntikkan.

php
<?php
// src/Service/HargaService.php
namespace App\Service;

use Psr\Log\LoggerInterface;

class HargaService
{
    public function __construct(
        private readonly LoggerInterface $logger,
    ) {
    }

    public function totalBelanja(int $harga, int $jumlah): int
    {
        $total = $harga * $jumlah;
        $this->logger->info('Menghitung total belanja.', ['total' => $total]);

        return $total;
    }
}

Berikut class service yang ingin kita pakai, diikuti berkas config/services.yaml yang mendaftarkan seluruh class aplikasi:

yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

Konfigurasi App\ dengan resource: '../src/' mendaftarkan seluruh class di src/ sebagai service. Berkat autowire: true, saat sebuah controller menulis __construct(private readonly HargaService $harga), container otomatis membangun HargaService beserta dependensinya — termasuk mengiris LoggerInterface dari paket logging yang terpasang. Tidak ada definisi manual satu per satu; type declaration di constructor menjadi kontrak penghubung. Prinsip ini adalah Dependency Injection yang kita pelajari secara teori, kini dikerjakan oleh mesin.

Konfigurasi Environment dan Dotenv #

Aplikasi Symfony mengenali lingkungan kerja lewat variabel APP_ENV — biasanya dev, prod, atau test — yang mengubah perilaku caching, toolbar debug, dan logging. Nilai konfigurasi lain seperti kredensial database juga hidup sebagai environment variable. Berkas .env di akar proyek menjadi tempat menuliskannya:

ini
APP_ENV=dev
APP_SECRET=YOUR_APP_SECRET
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.sqlite"

Komponen symfony/dotenv membaca berkas tersebut saat aplikasi dinyalakan lalu memuat setiap pasangan KEY=VALUE ke $_ENV dan $_SERVER. Prinsip kerjanya bisa kita lihat dalam PHP murni:

php
<?php
// Ilustrasi prinsip dotenv dalam PHP murni:
// membaca teks KEY=VALUE lalu memuatnya ke $_ENV
$isiEnv = <<<ENV
APP_ENV=dev
APP_DEBUG=1
DATABASE_URL=sqlite:///var/www/app.sqlite
ENV;

foreach (parse_ini_string($isiEnv) as $kunci => $nilai) {
    $_ENV[$kunci] = (string) $nilai;
}

echo "APP_ENV={$_ENV['APP_ENV']}\n";
echo "APP_DEBUG={$_ENV['APP_DEBUG']}\n";
echo "DATABASE_URL={$_ENV['DATABASE_URL']}\n";

Output yang Dihasilkan:

text
APP_ENV=dev
APP_DEBUG=1
DATABASE_URL=sqlite:///var/www/app.sqlite

Fungsi parse_ini_string() mengubah teks berformat pasangan kunci-nilai menjadi array, dan loop foreach memuat setiap pasangan ke superglobal $_ENV. Paket symfony/dotenv melakukan pekerjaan yang sama dengan fitur tambahan: dukungan komentar, tanda kutip, dan interpolasi variabel seperti ${DATABASE_URL}. Pola berlapis juga diatur: .env berisi nilai bawaan tim, .env.local menimpa nilai untuk mesin pengembang masing-masing, dan nilai asli server produksi selalu menang.

Komponen Console #

Bagian dari aplikasi yang tidak menangani HTTP — impor data, tugas terjadwal, perintah sekali jalan — dilayani oleh komponen Console. Sebuah command adalah class yang ditandai attribute AsCommand dan mengimplementasikan satu method execute():

php
<?php
// src/Command/ImporProdukCommand.php
namespace App\Command;

use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
    name: 'app:impor-produk',
    description: 'Mengimpor daftar produk dari berkas CSV.',
)]
final class ImporProdukCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $output->writeln('Impor selesai.');

        return Command::SUCCESS;
    }
}
bash
php bin/console app:impor-produk

Attribute AsCommand mendaftarkan nama perintah app:impor-produk, dan bin/console adalah skrip pintu masuk yang menjalankannya. Method execute() menerima objek input dan output, lalu mengembalikan kode status Command::SUCCESS atau Command::FAILURE — konvensi exit code yang kita kenal dari skrip CLI. Command yang sama bisa menerima argumen dan opsi (InputArgument, InputOption), mencetak progres berwarna, dan diuji sebagai class biasa tanpa terminal.

Symfony vs Laravel #

Kedua framework sama-sama mampu membangun aplikasi besar; perbedaannya ada di titik berat. Tabel berikut merangkum kontras yang paling terasa sehari-hari:

AspekSymfonyLaravel
FilosofiKomponen saling lepas + konfigurasi eksplisitKonvensi + kenyamanan pengembang
Konfigurasi dominanYAML dan PHP attributesArray PHP di config/*.php
Menit pertamaButuh memahami beberapa konsep awalHasil cepat berkat scaffolding
Jangka panjangEksplisit menjaga kode mudah diauditMagic convention mempercepat bila dipahami tim
EkosistemBundles dan komponen reusableRibuan paket komunitas (Packagist)
PerformaSetaraSetara

Angka performa sengaja ditulis setara karena memang demikian: keduanya berjalan pada PHP dengan model request-per-proses yang sama, dan hampir selalu query database, caching, dan I/O — bukan framework — yang menentukan kecepatan aplikasi nyata. Kurva belajar pun tidak berbanding lurus: Laravel lebih cepat menghasilkan halaman pertama, sementara Symfony lebih sering membuat pemula mengerti mengapa sesuatu bekerja. Tim kecil yang mengejar kecepatan pengiriman dan tim besar yang mengejar keterlacakan jangka panjang bisa sampai pada pilihan berbeda tanpa ada yang salah.

Stabilitas dan Rilis LTS #

Symfony merilis versi minor baru setiap enam bulan (sekitar Mei dan November) dan menetapkan setiap versi x.4 sebagai LTS setiap dua tahun sekali. Versi minor biasa menerima perbaikan bug selama beberapa bulan dan perbaikan keamanan sekitar satu tahun; versi LTS menerima perbaikan keamanan bertahun-tahun, sehingga aplikasi produksi tidak dipaksa mengejar kereta rilis.

Jenis rilisIramaDukungan
Versi minor biasaSetiap 6 bulanPerbaikan bug singkat, keamanan ±1 tahun
Versi LTS (x.4)Setiap 2 tahunPerbaikan keamanan bertahun-tahun

Kombinasi semantic versioning dan disiplin backward compatibility membuat upgrade antar versi minor menjadi kegiatan berisiko rendah — dependensi diperbarui, test dijalankan, aplikasi tetap berperilaku sama. Strategi yang lazim: kembangkan di versi terbaru untuk mendapatkan perbaikan, dan naikkan versi minor segera setelah rilis agar jarak upgrade tidak menumpuk menjadi proyek besar.

Ringkasan #

Symfony menawarkan jalan yang lebih eksplisit daripada konvensi: aplikasi disusun dari komponen, rute dan service dideklarasikan terang-terangan, autowiring menghubungkan semuanya lewat type declaration, dan siklus LTS menjaga stabilitas bertahun-tahun. Filosofi “komponen dulu” menyimpan satu konsekuensi menarik yang belum kita bahas tuntas: komponen-komponen tersebut bisa dipakai tanpa framework Symfony sama sekali. Itulah topik chapter berikutnya tentang Symfony Components.

Referensi #