# Symfony 7: API Platform dan Praktik Terbaik > Panduan lengkap membangun REST API profesional dengan Symfony 7 dan API Platform 4. State Providers, Processors, validasi, dan serialisasi dijelaskan dengan contoh praktis. - Published: 2026-01-12 - Updated: 2026-04-07 - Author: SharpSkill - Tags: symfony, api platform, php, rest api, api development - Reading time: 14 min --- API Platform 4 merevolusi pembuatan REST API dan GraphQL dengan Symfony 7. Versi terbaru ini membawa filosofi yang dirancang ulang secara menyeluruh: pemisahan tanggung jawab yang jelas, State Providers dan Processors yang disederhanakan, serta integrasi native Symfony Object Mapper. Membangun API profesional dengan symfony-7-api-platform belum pernah semudah dan seintuitif ini. > **Apa yang Baru di API Platform 4.2** > > Versi 4.2 memperkenalkan JSON Streamer untuk peningkatan performa hingga +32% RPS, sistem filter yang dirancang ulang, serta Mutators untuk menyesuaikan operasi tanpa menyentuh inti framework. Dukungan Symfony 7 dan 8 bersifat native. ## Instalasi dan Konfigurasi Awal API Platform diinstal dalam beberapa perintah menggunakan Symfony Flex. Konfigurasi default mencakup sebagian besar kasus penggunaan sekaligus tetap sepenuhnya dapat dikustomisasi. ```bash # terminal # Create a new Symfony project with API Platform composer create-project symfony/skeleton my-api cd my-api # Install API Platform with Doctrine ORM composer require api # Verify installation php bin/console debug:router | grep api ``` Symfony Flex secara otomatis mengkonfigurasi routes, dokumentasi OpenAPI, serta antarmuka Swagger UI yang dapat diakses di `/api`. ```yaml # config/packages/api_platform.yaml api_platform: title: 'My API' version: '1.0.0' # Supported response formats formats: jsonld: ['application/ld+json'] json: ['application/json'] # OpenAPI documentation swagger: versions: [3] # Default pagination defaults: pagination_items_per_page: 30 pagination_maximum_items_per_page: 100 ``` Konfigurasi ini mendefinisikan format serialisasi, paginasi global, dan metadata dokumentasi API. ## Membuat API Resource Sederhana Atribut `#[ApiResource]` mengekspos entitas Doctrine sebagai resource REST. API Platform secara otomatis menghasilkan endpoint CRUD, dokumentasi OpenAPI, dan validasi dasar. ```php 'DESC'], // Pagination configuration for this resource paginationItemsPerPage: 20 )] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank(message: 'Title is required')] #[Assert\Length(min: 2, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] private ?string $description = null; #[ORM\Column(length: 13, unique: true)] #[Assert\Isbn] private ?string $isbn = null; #[ORM\Column] private ?\DateTimeImmutable $publishedAt = null; // Getters and setters... public function getId(): ?int { return $this->id; } public function getTitle(): ?string { return $this->title; } public function setTitle(string $title): static { $this->title = $title; return $this; } public function getDescription(): ?string { return $this->description; } public function setDescription(string $description): static { $this->description = $description; return $this; } public function getIsbn(): ?string { return $this->isbn; } public function setIsbn(string $isbn): static { $this->isbn = $isbn; return $this; } public function getPublishedAt(): ?\DateTimeImmutable { return $this->publishedAt; } public function setPublishedAt(\DateTimeImmutable $publishedAt): static { $this->publishedAt = $publishedAt; return $this; } } ``` Entitas ini menghasilkan enam endpoint: `GET /api/books`, `POST /api/books`, `GET /api/books/{id}`, `PUT /api/books/{id}`, `PATCH /api/books/{id}`, dan `DELETE /api/books/{id}`. > **UUID v7 Direkomendasikan** > > API Platform mendukung UUID v7 secara native sebagai identifier. Pendekatan ini meningkatkan keamanan (identifier yang tidak dapat diprediksi) dan performa (pengurutan alami berdasarkan tanggal pembuatan). ## Serialization Groups untuk Mengontrol Data yang Diekspos Serialization groups mendefinisikan secara presisi properti mana yang diekspos untuk pembacaan (normalization) dan penulisan (denormalization). Pemisahan ini esensial untuk keamanan dan fleksibilitas API. ```php ['Default', 'user:create']]), new Get(), new Put(processor: UserPasswordHasher::class), new Patch(processor: UserPasswordHasher::class), new Delete(), ], // Properties exposed when reading normalizationContext: ['groups' => ['user:read']], // Properties accepted when writing denormalizationContext: ['groups' => ['user:create', 'user:update']], )] class User implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['user:read'])] private ?int $id = null; #[ORM\Column(length: 180, unique: true)] #[Assert\NotBlank] #[Assert\Email] #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $email = null; #[ORM\Column] private ?string $password = null; // Never exposed when reading, only when writing #[Assert\NotBlank(groups: ['user:create'])] #[Groups(['user:create', 'user:update'])] private ?string $plainPassword = null; #[ORM\Column(length: 100)] #[Groups(['user:read', 'user:create', 'user:update'])] private ?string $fullName = null; #[ORM\Column(type: 'json')] #[Groups(['user:read'])] private array $roles = []; #[ORM\Column] #[Groups(['user:read'])] private ?\DateTimeImmutable $createdAt = null; public function __construct() { $this->createdAt = new \DateTimeImmutable(); } // UserInterface implementation public function getUserIdentifier(): string { return (string) $this->email; } public function getRoles(): array { $roles = $this->roles; $roles[] = 'ROLE_USER'; return array_unique($roles); } public function getPassword(): string { return $this->password; } public function eraseCredentials(): void { $this->plainPassword = null; } // Getters and setters... public function getId(): ?int { return $this->id; } public function getEmail(): ?string { return $this->email; } public function setEmail(string $email): static { $this->email = $email; return $this; } public function setPassword(string $password): static { $this->password = $password; return $this; } public function getPlainPassword(): ?string { return $this->plainPassword; } public function setPlainPassword(?string $plainPassword): static { $this->plainPassword = $plainPassword; return $this; } public function getFullName(): ?string { return $this->fullName; } public function setFullName(string $fullName): static { $this->fullName = $fullName; return $this; } public function setRoles(array $roles): static { $this->roles = $roles; return $this; } public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; } } ``` Dengan konfigurasi ini, `plainPassword` tidak pernah diekspos dalam respons tetapi dapat dikirimkan saat pembuatan atau pembaruan akun. ## State Processors untuk Logika Bisnis State Processors mencegat operasi persistensi untuk menambahkan logika bisnis. API Platform 4 menyederhanakan pembuatannya melalui dependency injection berbasis atribut. ```php */ final class UserPasswordHasher implements ProcessorInterface { public function __construct( // Injection of standard Doctrine processor #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, private UserPasswordHasherInterface $passwordHasher, ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User { // Hash password if provided if ($data->getPlainPassword()) { $hashedPassword = $this->passwordHasher->hashPassword( $data, $data->getPlainPassword() ); $data->setPassword($hashedPassword); $data->eraseCredentials(); } // Delegate persistence to standard processor return $this->persistProcessor->process($data, $operation, $uriVariables, $context); } } ``` Pola komposisi ini memungkinkan penambahan logika apa pun—pengiriman email, event, logging—sambil mempertahankan perilaku persistensi standar. ### Processor dengan Logika Kondisional Sebuah processor dapat mengadaptasi perilakunya berdasarkan jenis operasi yang sedang dijalankan. ```php */ final class BookProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] private ProcessorInterface $persistProcessor, #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] private ProcessorInterface $removeProcessor, private NotificationService $notifications, private SearchIndexer $searchIndexer, ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed { // Deletion: use remove processor if ($operation instanceof DeleteOperationInterface) { $this->searchIndexer->remove($data); return $this->removeProcessor->process($data, $operation, $uriVariables, $context); } // Creation: set publication date if ($operation instanceof Post) { $data->setPublishedAt(new \DateTimeImmutable()); } // Standard persistence $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); // Post-processing: indexing and notification $this->searchIndexer->index($result); if ($operation instanceof Post) { $this->notifications->notifyNewBook($result); } return $result; } } ``` ## State Providers untuk Sumber Data Kustom State Providers mengambil data dari sumber mana pun: API eksternal, cache, file, atau logika bisnis yang kompleks. Fleksibilitas ini merupakan keunggulan utama arsitektur API Platform 4. ```php */ final class PopularBooksProvider implements ProviderInterface { public function __construct( private BookRepository $bookRepository, private CacheInterface $cache, ) { } public function provide(Operation $operation, array $uriVariables = [], array $context = []): array { // 5-minute cache for popular books return $this->cache->get('popular_books', function (ItemInterface $item) { $item->expiresAfter(300); return $this->bookRepository->findPopular(limit: 10); }); } } ``` Provider ini digunakan pada operasi khusus yang didefinisikan secara terpisah di entitas. ```php ['Default', 'article:create']]), // More lenient validation for updates new Put(validationContext: ['groups' => ['Default', 'article:update']]), ], )] class Article { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] #[Assert\NotBlank] #[Assert\Length(min: 10, max: 255)] private ?string $title = null; #[ORM\Column(type: 'text')] #[Assert\NotBlank] // Minimum 500 characters on creation #[Assert\Length(min: 500, groups: ['article:create'])] // Minimum 100 characters for updates #[Assert\Length(min: 100, groups: ['article:update'])] private ?string $content = null; #[ORM\Column(length: 50)] #[Assert\NotBlank(groups: ['article:create'])] #[Assert\Choice(choices: ['draft', 'published', 'archived'])] private ?string $status = 'draft'; #[ORM\Column(nullable: true)] // Required only if status is "published" #[Assert\NotBlank(groups: ['article:publish'])] private ?\DateTimeImmutable $publishedAt = null; // Getters and setters... } ``` ### Validasi Dinamis dengan Service Untuk aturan validasi yang kompleks, generator grup kustom menawarkan fleksibilitas total tanpa batasan konfigurasi statis. ```php security->isGranted('ROLE_ADMIN')) { $groups[] = 'admin'; return $groups; } // Additional validation if publishing if ($object->getStatus() === 'published') { $groups[] = 'article:publish'; } return $groups; } } ``` > **Performa Validasi** > > Validasi yang kompleks dapat berdampak pada performa. Untuk impor massal, pertimbangkan untuk menonaktifkan sementara validasi tertentu atau menggunakan constraint asinkron. ## Filter untuk Query yang Fleksibel API Platform 4.2 merancang ulang sistem filter secara menyeluruh dengan pemisahan tanggung jawab yang jelas. Filter memungkinkan klien API untuk mencari dan mengurutkan data dengan mudah. ```php 'partial', // LIKE %value% 'description' => 'partial', 'category.name' => 'exact', // Search on relation 'sku' => 'exact', // Exact match ])] // Range filtering #[ApiFilter(RangeFilter::class, properties: ['price', 'stock'])] // Date filtering #[ApiFilter(DateFilter::class, properties: ['createdAt', 'updatedAt'])] // Boolean filtering #[ApiFilter(BooleanFilter::class, properties: ['isActive', 'isFeatured'])] // Customizable sorting #[ApiFilter(OrderFilter::class, properties: [ 'name', 'price', 'createdAt', ], arguments: ['orderParameterName' => 'sort'])] class Product { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; #[ORM\Column(length: 50, unique: true)] private ?string $sku = null; #[ORM\Column(type: 'decimal', precision: 10, scale: 2)] private ?string $price = null; #[ORM\Column] private ?int $stock = null; #[ORM\Column] private ?bool $isActive = true; #[ORM\Column] private ?bool $isFeatured = false; #[ORM\ManyToOne(targetEntity: Category::class)] private ?Category $category = null; #[ORM\Column] private ?\DateTimeImmutable $createdAt = null; #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $updatedAt = null; // Getters and setters... } ``` Filter-filter ini secara otomatis menghasilkan dokumentasi OpenAPI dan mengaktifkan query seperti: ``` GET /api/products?name=phone&price[gte]=100&price[lte]=500&isActive=true&sort[price]=asc ``` ## Relasi dan Subresource API Platform menangani relasi antar entitas dengan elegan melalui opsi serialisasi dan subresource yang terkonfigurasi dengan baik. ```php ['author:read']], )] class Author { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['author:read', 'book:read'])] private ?int $id = null; #[ORM\Column(length: 255)] #[Groups(['author:read', 'book:read'])] private ?string $name = null; #[ORM\Column(type: 'text', nullable: true)] #[Groups(['author:read'])] private ?string $biography = null; #[ORM\OneToMany(mappedBy: 'author', targetEntity: Book::class)] #[Groups(['author:read'])] private Collection $books; public function __construct() { $this->books = new ArrayCollection(); } // Getters and setters... } ``` ```php ['book:read']], )] // Subresource: GET /api/authors/{authorId}/books #[ApiResource( uriTemplate: '/authors/{authorId}/books', operations: [new GetCollection()], uriVariables: [ 'authorId' => new Link( fromProperty: 'books', fromClass: Author::class ), ], normalizationContext: ['groups' => ['book:read']], )] class Book { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] #[Groups(['book:read', 'author:read'])] private ?int $id = null; #[ORM\Column(length: 255)] #[Groups(['book:read', 'author:read'])] private ?string $title = null; #[ORM\ManyToOne(targetEntity: Author::class, inversedBy: 'books')] #[ORM\JoinColumn(nullable: false)] #[Groups(['book:read'])] private ?Author $author = null; // Getters and setters... } ``` ## Keamanan dan Kontrol Akses API Platform terintegrasi secara mulus dengan sistem keamanan Symfony. Voter dan ekspresi keamanan mengontrol akses ke resource dengan granularitas tinggi per operasi. ```php customer; } } ``` Ekspresi `object.getCustomer() == user` memberikan akses ke entitas saat ini dan pengguna yang terkoneksi untuk pemeriksaan yang sangat granular. ## Pengujian API Secara Otomatis API Platform menyediakan trait PHPUnit untuk menguji endpoint dengan mudah dan efisien. Setiap fitur API sebaiknya dilindungi oleh test fungsional. ```php request('GET', '/api/books'); // Assertions $this->assertResponseIsSuccessful(); $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); $this->assertJsonContains([ '@context' => '/api/contexts/Book', '@type' => 'Collection', 'totalItems' => 30, ]); // Verify pagination (20 items per page) $this->assertCount(20, $response->toArray()['member']); } public function testCreateBook(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $this->getToken($user), 'json' => [ 'title' => 'Clean Code', 'description' => 'A Handbook of Agile Software Craftsmanship', 'isbn' => '9780132350884', ], ]); $this->assertResponseStatusCodeSame(201); $this->assertJsonContains([ '@type' => 'Book', 'title' => 'Clean Code', ]); } public function testCreateBookValidationFails(): void { $user = UserFactory::createOne(['roles' => ['ROLE_ADMIN']]); static::createClient()->request('POST', '/api/books', [ 'auth_bearer' => $this->getToken($user), 'json' => [ 'title' => '', // Empty title = error 'isbn' => 'invalid-isbn', ], ]); $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ '@type' => 'ConstraintViolationList', 'violations' => [ ['propertyPath' => 'title', 'message' => 'Title is required'], ], ]); } private function getToken(object $user): string { // Implementation depends on your authentication system return 'test_token'; } } ``` ## Kesimpulan API Platform 4 dengan Symfony 7 merepresentasikan standar terkini dalam pembuatan REST API profesional dengan PHP. Pemisahan yang jelas antara State Providers (pembacaan) dan State Processors (penulisan), dikombinasikan dengan serialization groups dan sistem validasi, memungkinkan pembangunan API yang kokoh dan mudah dipelihara. ### Checklist untuk API Berkualitas - Gunakan serialization groups yang berbeda untuk pembacaan dan penulisan - Implementasikan State Processors untuk logika bisnis (hashing kata sandi, notifikasi) - Konfigurasikan filter untuk pencarian dan pengurutan data - Terapkan validasi per operasi dengan grup yang sesuai - Amankan endpoint dengan ekspresi security per operasi - Tulis test fungsional untuk setiap endpoint penting - Dokumentasikan API melalui metadata OpenAPI Filosofi API Platform 4 mendorong komposisi di atas pewarisan, serta konfigurasi di atas konvensi. Hasilnya adalah API yang skalabel, mudah diuji, dan sesuai standar REST/JSON-LD—siap produksi sejak hari pertama pengembangan. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/symfony/symfony-7-api-platform-best-practices