Membangun FormType dengan data binding ke entity, memahami proteksi CSRF otomatis, constraint validation berbasis attribute, custom validator dan validation groups, serta theming form di Twig dengan form_start, form_row, dan form_end.

Pada episode 6, kita menulis artikel lewat $request->request->get('title') secara manual. Episode 7 memperbaiki pendekatan itu dengan Form component: FormType yang memetakan input HTTP ke entity secara otomatis, plus lapisan validation yang menjamin data yang masuk ke database selalu bersih.
Mengapa penting? Dua masalah besar dalam aplikasi nyata: (1) data tidak valid masuk ke database (judul kosong, email format salah), dan (2) CSRF — formulir bisa disalahgunakan dari situs lain. Form + Validation Symfony menyelesaikan keduanya secara bawaan, dan ini pola yang akan kalian temui di hampir semua project Symfony produksi.
php bin/console make:form ArticleFormTypeFormType mendefinisikan field dan binding-nya ke entity:
<?php
namespace App\Form;
use App\Entity\Article;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ArticleFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('title', TextType::class)
->add('body', TextareaType::class)
->add('save', SubmitType::class, ['label' => 'Simpan']);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Article::class,
'csrf_protection' => true,
]);
}
}data_class mengikat form ke entity Article; csrf_protection (default aktif) otomatis menyuntikkan token CSRF. Nama method harus sesuai getter/setter entity: title → getTitle()/setTitle().
Pola standar handleRequest:
use App\Form\ArticleFormType;
#[Route('/articles/new', name: 'app_article_new')]
public function new(Request $request, EntityManagerInterface $em): Response
{
$article = new Article();
$form = $this->createForm(ArticleFormType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em->persist($article);
$em->flush();
return $this->redirectToRoute('app_article_show', ['id' => $article->getId()]);
}
return $this->render('article/new.html.twig', [
'form' => $form->createView(),
]);
}Mengapa pola ini penting? Karena handleRequest mengikat data request ke objek $article secara otomatis dan menjalankan validasi sebelum isValid() mengembalikan nilai. Perhatikan pola PRG (Post/Redirect/Get): setelah simpan, redirect — bukan render langsung — agar refresh halaman tidak mengirim ulang POST.
Tanpa constraint, isValid() selalu benar. Tambahkan aturan validasi pada property entity:
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Judul tidak boleh kosong.')]
#[Assert\Length(min: 3, max: 255)]
private ?string $title = null;
#[ORM\Column(type: 'text')]
#[Assert\NotBlank]
#[Assert\Length(min: 10, minMessage: 'Konten minimal 10 karakter.')]
private ?string $body = null;Constraint bawaan yang paling sering dipakai:
| Constraint | Fungsi |
|---|---|
NotBlank | Nilai tidak boleh kosong |
Length | Panjang minimal/maksimal |
Email | Format email valid |
Regex | Pola tertentu (mis. hanya alfanumerik) |
UniqueEntity | Tidak boleh duplikat di database (perlu import) |
Choice | Hanya nilai dari daftar |
Keindahan sistem ini: constraint berlaku di mana pun validasi dijalankan — form, API Platform (episode 13), atau validasi manual lewat ValidatorInterface.
Ketika constraint bawaan tidak cukup, buat constraint sendiri:
php bin/console make:validator IsValidSlugHasilnya dua file: class IsValidSlug (constraint) dan IsValidSlugValidator (logika). Constraint dipakai seperti bawaan:
use App\Validator as AppAssert;
#[AppAssert\IsValidSlug]
private ?string $slug = null;Validation groups memungkinkan aturan berbeda tergantung konteks — misalnya kolom wajib saat create tapi opsional saat update:
#[Assert\NotBlank(groups: ['create'])]
private ?string $title = null;Lalu validasi dijalankan dengan grup tertentu: $validator->validate($article, groups: ['create']). Form bisa menetapkan group via opsi validation_groups.
Render form di template:
{% extends 'base.html.twig' %}
{% block body %}
<h1>Tulis Artikel Baru</h1>
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_row(form.title) }}
{{ form_row(form.body) }}
{{ form_row(form.save) }}
{{ form_end(form) }}
{% endblock %}| Function | Fungsi |
|---|---|
form_start | Tag <form> + token CSRF |
form_end | Tag </form> + field yang belum dirender |
form_row | Label + widget + error sekaligus |
form_widget | Hanya input field |
form_errors | Error validasi |
Kesalahan umum: lupa memanggil form_end sehingga token CSRF tidak ter-render — hasilnya form akan gagal validasi CSRF saat submit.
use Symfony\Component\Validator\Constraints as Assert; dan constraint berada di property (bukan getter) — atau pakai attribute #[Assert\Callback] untuk validasi kompleks.buildForm harus sama dengan properti entity, dan form harus isSubmitted() dan isValid() sebelum diproses.validation_groups: dua form memakai entity yang sama dengan aturan berbeda — ini kasus grup paling umum.Tip
Selalu simpan data setelah isValid(), jangan pernah sebelum. Kebiasaan ini mencegah data tidak valid masuk database dan membuat error form lebih mudah ditelusuri karena error terikat ke field-nya masing-masing.
Pada episode 7 ini, kalian telah membangun input yang aman dan tervalidasi.
Inti yang harus dibawa pulang:
data_class adalah pengikatnya.handleRequest + isSubmitted() + isValid() adalah standar emas.form_row untuk keseluruhan, form_widget untuk field saja, jangan lupa form_end.Di episode 8 selanjutnya kita mengupas jantung Symfony: Dependency Injection & Services — service container, autowiring, konfigurasi services.yaml, binding interface ke implementasi, parameter, dan lazy loading. Sampai jumpa di episode 8!