Belajar Symfony - Doctrine ORM & Database
Episode 6 of 27

Belajar Symfony - Doctrine ORM & Database

Membangun lapisan database dengan Doctrine ORM: menginstall driver, membuat entity, menggenerasi dan menjalankan migrasi, memahami repository dan relasi antar entity, mengisi data dengan fixtures, serta praktik CRUD blog dari entity hingga controller.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Setelah di episode 5 kalian merender HTML dari data array, saatnya data itu datang dari sumber yang benar: database. Episode 6 memperkenalkan Doctrine ORM — lapisan model dalam arsitektur MVC Symfony.

Mengapa memakai ORM, bukan query SQL langsung? Karena ORM mengubah row database menjadi objek (entity) yang bisa diolah sebagai kode PHP biasa, sekaligus menjaga keamanan dari SQL injection (prepared statement otomatis). Namun ORM bukan sihir tanpa biaya: tanpa memahami bagaimana ia menggenerasi SQL, kalian tidak akan bisa memperbaiki query lambat — masalah yang kita bedah mendalam di episode 21.

Instalasi dan Konfigurasi Database

Install Doctrine ORM
composer require doctrine/orm

Flex otomatis menambahkan bundle dan konfigurasi. Selanjutnya atur koneksi di .env:

.env — DATABASE_URL
DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# PostgreSQL: postgresql://app:pass@127.0.0.1:5432/app
# MySQL:      mysql://app:pass@127.0.0.1:3306/app

Lalu buat database-nya:

Buat database
php bin/console doctrine:database:create

Untuk SQLite, perintah di atas cukup membuat file database kosong.

Entity: Peta dari Objek ke Tabel

Entity adalah class PHP yang dipetakan ke tabel. Buat dengan MakerBundle:

Buat entity Article
php bin/console make:entity Article

Perintah interaktif ini menghasilkan class dengan atribut mapping:

src/Entity/Article.php
<?php
 
namespace App\Entity;
 
use Doctrine\ORM\Mapping as ORM;
 
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;
 
    #[ORM\Column(length: 255)]
    private ?string $title = null;
 
    #[ORM\Column(type: 'text')]
    private ?string $body = null;
 
    #[ORM\Column]
    private ?\DateTimeImmutable $createdAt = null;
 
    // getter dan setter untuk setiap property...
}

Setiap #[ORM\Column] adalah kolom tabel; #[ORM\Id] + #[ORM\GeneratedValue] menandai primary key auto-increment. Doctrine menyimpulkan tipe SQL dari tipe PHP: ?stringVARCHAR, ?\DateTimeImmutableDATETIME.

Migrasi: Perubahan Skema yang Terverifikasi

Setelah entity berubah, jangan pernah menulis SQL manual untuk mengubah skema — gunakan migrasi:

Generate dan jalankan migrasi
php bin/console make:migration
php bin/console doctrine:migrations:migrate

make:migration membandingkan skema terakhir dengan entity saat ini, lalu menulis file di migrations/ — kalian bisa meninjau SQL-nya sebelum dijalankan:

Hasil make:migration
[OK] Generated migration "Version20260816120000" ...
     "migrations/Version20260816120000.php"

Migrasi adalah riwayat perubahan skema yang bisa direview dan di-rollback (doctrine:migrations:migrate prev). Inilah alasan tim Symfony jarang bertengkar soal struktur database: semuanya versi dalam kode, termasuk di production (kita jalankan saat deploy di episode 23).

Repository: Cara Bertanya ke Database

ArticleRepository yang di-generate adalah gerbang query:

src/Repository/ArticleRepository.php
public function findLatest(int $limit): array
{
    return $this->createQueryBuilder('a')
        ->orderBy('a.createdAt', 'DESC')
        ->setMaxResults($limit)
        ->getQuery()
        ->getResult();
}

Metode bawaan yang sering dipakai:

MethodFungsi
find($id)Cari berdasarkan primary key
findAll()Semua record
findBy(['published' => true])Filter sederhana
findOneBy(['slug' => $slug])Satu record berdasarkan kriteria
createQueryBuilder()Query kompleks (episode 21)

CRUD Blog: Contoh Lengkap

Rangkaian minimal CRUD: controller menampilkan daftar, membuat artikel, dan menampilkan detail.

src/Controller/ArticleController.php
use App\Entity\Article;
use App\Repository\ArticleRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
 
#[Route('/articles', name: 'app_article_')]
class ArticleController extends AbstractController
{
    #[Route('', name: 'index')]
    public function index(ArticleRepository $repo): Response
    {
        return $this->render('article/index.html.twig', [
            'articles' => $repo->findLatest(10),
        ]);
    }
 
    #[Route('/new', name: 'new', methods: ['POST'])]
    public function new(Request $request, EntityManagerInterface $em): Response
    {
        $article = new Article();
        $article->setTitle($request->request->get('title'));
        $article->setBody($request->request->get('body'));
        $article->setCreatedAt(new \DateTimeImmutable());
 
        $em->persist($article);
        $em->flush();
 
        return $this->redirectToRoute('app_article_index');
    }
 
    #[Route('/{id}', name: 'show', requirements: ['id' => '\d+'])]
    public function show(Article $article): Response
    {
        return $this->render('article/show.html.twig', [
            'article' => $article,
        ]);
    }
}

Perhatikan dua hal penting:

  1. persist() + flush() — pola wajib Doctrine: persist memberi tahu unit of work, flush menulis ke database dalam satu transaksi.
  2. Auto-resolve parameter — type-hint Article $article pada route /articles/{id} membuat Symfony mencari entity dengan id dari URL (param converter). Jika tidak ada, otomatis 404.

Relasi Antar Entity

Untuk blog yang realistis, Artikel dimiliki Penulis. Tambahkan relasi:

Tambah relasi ManyToOne
php bin/console make:entity Article

Pilih opsi relasi ManyToOne menuju User. Hasilnya di entity:

Relasi ManyToOne di Article
#[ORM\ManyToOne(inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
private ?User $author = null;

Pola relasi utama: ManyToOne/OneToMany (banyak artikel → satu penulis), ManyToMany (tag), dan OneToOne (profil pengguna). Kita dalami query dan optimization relasi ini di episode 21 — terutama masalah N+1 yang muncul persis karena relasi.

Data Fixtures: Mengisi Data Awal

Data development diisi lewat fixtures — bukan manual via SQL:

Install dan buat fixtures
composer require --dev doctrine/doctrine-fixtures-bundle
php bin/console make:fixtures
src/DataFixtures/AppFixtures.php
use App\Entity\Article;
 
public function load(ObjectManager $manager): void
{
    for ($i = 0; $i < 20; $i++) {
        $article = new Article();
        $article->setTitle("Artikel demo ke-{$i}");
        $article->setBody('Konten artikel...');
        $article->setCreatedAt(new \DateTimeImmutable("-{$i} days"));
        $manager->persist($article);
    }
    $manager->flush();
}

Jalankan dengan php bin/console doctrine:fixtures:load — data 20 artikel siap dipakai di episode 7.

Warning

Jangan pernah memakai make:entity untuk menambah kolom tanpa membuat migrasi baru. Skema production dan dev akan menyimpang, dan doctrine:migrations:migrate di production bisa gagal karena skema tak sinkron. Pola disiplin: ubah entity → make:migration → review SQL → migrate.

Penutup

Pada episode 6 ini, kalian telah membangun lapisan database penuh dengan Doctrine.

Inti yang harus dibawa pulang:

  • Install dengan composer require doctrine/orm; atur DATABASE_URL di .env.
  • Entity = class PHP yang dipetakan ke tabel; make:entity untuk boilerplate.
  • Migrasi adalah satu-satunya cara sah mengubah skema — review sebelum migrate.
  • Query lewat Repository; findBy() untuk sederhana, createQueryBuilder() untuk kompleks.
  • persist() + flush() untuk menulis; param converter mengubah {id} menjadi entity otomatis.
  • Fixtures untuk mengisi data dev yang bisa di-ulang.

Di episode 7 selanjutnya, kita mengubah input dari sembarangan menjadi tervalidasi: Forms & Validation — FormType dengan data binding, proteksi CSRF otomatis, constraint validation (attribute), custom validator, validation groups, dan theming form di Twig. Sampai jumpa di episode 7!

Belajar Symfony - Doctrine ORM & Database | Belajar Symfony