Developer Java yang terbiasa dengan Spring Boot sering kali merasa PHP terlalu verbose dan kurang terstruktur. Tidak ada annotation-driven routing, tidak ada container DI yang otomatis, dan ORM yang tersedia biasanya membawa puluhan dependensi. Kamu harus pasang Composer, konfigurasi service provider, daftarkan binding — sebelum bisa menulis satu endpoint pun.
PointArt hadir dengan tesis yang menarik: ambil pola terbaik Spring Boot, implementasikan dalam PHP murni, dan jangan tambahkan satu dependensi eksternal pun. Hasilnya adalah micro-framework yang berjalan di shared hosting biasa, tanpa Composer, tanpa build step — cukup copy file dan deploy.
Apa yang Membuat PointArt Berbeda
Mayoritas PHP framework modern bergantung pada Composer sebagai fondasi. Ini masuk akal untuk production server yang terkontrol, tapi menjadi hambatan di lingkungan shared hosting atau saat prototipe cepat. PointArt memilih jalan berbeda: zero dependency, distribusi langsung via file copy.
Yang lebih menarik bukan hanya minimalismenya, tapi cara framework ini mengadopsi pola Spring Boot secara konsisten:
- Attribute-based routing — Controller dan endpoint didefinisikan dengan
#[Router]dan#[Route], bukan dengan config file atau route registration manual - Dependency injection otomatis —
#[Wired]menginject service ke property tanpa perlu constructor atau binding manual - Dynamic repository — Deklarasikan abstract method
findByNameAndEmail(), framework generate implementasinya saat runtime - Entity mapping — Attribute
#[Entity],#[Column], dan#[Id]memetakan class ke tabel database
PointArt membutuhkan PHP 8.1+ dan Apache dengan mod_rewrite aktif. PDO driver juga diperlukan untuk fitur ORM-nya. Untuk shared hosting modern yang sudah menjalankan PHP 8.x, ini sudah terpenuhi secara default.
PHP Attributes adalah fitur native PHP 8.0+ yang diproses via Reflection API. Berbeda dari docblock annotations yang hanya komentar biasa, attributes adalah syntax resmi dengan type checking. Artikel PHP 8 Attributes di Laravel 13 membahas lebih dalam bagaimana pola ini diadopsi di ekosistem PHP modern.
Instalasi dan Struktur Direktori
Tidak ada composer install di sini. Cara memulai PointArt adalah dengan mengkloning repository lalu menyesuaikan konfigurasi environment:
git clone https://github.com/Cn8001/PointArt.git project-kamu
cd project-kamu
cp .env.example .env
Edit .env dengan kredensial database kamu:
DB_HOST=localhost
DB_NAME=manajemen_tugas
DB_USER=root
DB_PASS=secret
DB_DRIVER=mysql
Struktur direktori PointArt mengikuti konvensi yang familiar:
app/
├── components/ # controller dan service
├── models/ # entity class
├── repositories/ # repository class
├── views/ # template PHP biasa
└── public/ # static assets (CSS, JS, gambar)
Semua file publik harus berada di app/public/. File .htaccess secara default memblokir akses langsung ke direktori lain — ini penting untuk keamanan karena logic dan konfigurasi database berada di luar public root.
Satu hal yang perlu diingat: setiap kali menambahkan controller atau service baru, cache registry perlu dibersihkan:
ClassLoader::clearCache();
PointArt menyimpan hasil route discovery ke cache untuk menghindari overhead reflection di setiap request. Tanpa ini, controller baru tidak akan terdeteksi.
Routing dengan PHP Attributes
Di sinilah pengaruh Spring Boot paling terasa. Alih-alih mendaftarkan route di file terpisah, kamu mendefinisikan routing langsung di controller menggunakan attributes.
Buat file app/components/TaskController.php:
<?php
#[Router(name: 'task', path: '/task')]
class TaskController {
#[Wired]
private TaskRepository $taskRepository;
#[Route('/all', HttpMethod::GET)]
public function listAll(): string {
$tasks = $this->taskRepository->findAll();
return Renderer::render('task.index', ['tasks' => $tasks]);
}
#[Route('/{id}', HttpMethod::GET)]
public function detail(int $id): string {
$task = Task::find($id);
return Renderer::render('task.detail', ['task' => $task]);
}
#[Route('/create', HttpMethod::POST)]
public function store(
#[RequestParam] string $title,
#[RequestParam] string $description
): string {
$task = new Task();
$task->title = $title;
$task->description = $description;
$task->save();
return Renderer::render('task.created', ['task' => $task]);
}
}
#[Router] mendefinisikan base path /task untuk seluruh controller. Setiap method yang diberi #[Route] menjadi endpoint — path relatif terhadap base path controller-nya. Jadi #[Route('/all', HttpMethod::GET)] akan merespons request GET /task/all.
#[RequestParam] pada parameter method secara otomatis memetakan request parameter ke argument function. Tidak perlu parsing $_POST secara manual.
ORM: Entity dan Model
PointArt menggunakan attribute untuk mendefinisikan mapping antara PHP class dan tabel database. Buat file app/models/Task.php:
<?php
#[Entity('tasks')]
class Task extends Model {
#[Id]
public ?int $id = null;
#[Column('title', 'varchar')]
public string $title;
#[Column('description', 'text')]
public string $description;
#[Column('is_done', 'boolean')]
public bool $isDone = false;
#[Column('created_at', 'datetime')]
public ?string $createdAt = null;
}
#[Entity('tasks')] memberi tahu framework bahwa class ini dipetakan ke tabel tasks. #[Id] menandai primary key, dan #[Column] mendefinisikan nama kolom beserta tipe datanya.
Setelah entity didefinisikan, operasi dasar bisa langsung digunakan:
// Ambil semua record
$tasks = Task::findAll();
// Cari by ID
$task = Task::find(42);
// Simpan record baru
$task = new Task();
$task->title = 'Refactor auth module';
$task->description = 'Extract token validation ke service tersendiri';
$task->save();
// Hapus record
$task->delete();
Framework menangani SQL generation di balik layar berdasarkan attribute metadata. Tidak ada query builder yang perlu dipelajari untuk operasi standar ini.
Repository Pattern dengan Dynamic Finder
Fitur paling menarik PointArt adalah dynamic repository — fitur yang langsung terinspirasi dari Spring Data JPA. Kamu hanya perlu mendeklarasikan abstract method dengan nama yang mengikuti konvensi, dan framework akan generate implementasinya saat runtime.
Buat file app/repositories/TaskRepository.php:
<?php
abstract class TaskRepository extends Repository {
protected string $entityClass = Task::class;
// Framework generate: SELECT * FROM tasks WHERE title = ?
abstract public function findByTitle(string $title): array;
// Framework generate: SELECT * FROM tasks WHERE is_done = ?
abstract public function findByIsDone(bool $isDone): array;
// Framework generate: SELECT * FROM tasks WHERE title = ? AND is_done = ?
abstract public function findByTitleAndIsDone(string $title, bool $isDone): array;
// Custom query untuk kebutuhan spesifik
#[Query("SELECT * FROM tasks WHERE created_at > ? ORDER BY created_at DESC")]
abstract public function findRecentTasks(string $sinceDate): array;
}
Konvensi penamaan findBy{Field} dan findBy{Field}And{OtherField} diparse oleh framework untuk menghasilkan query yang sesuai. Untuk query yang tidak bisa diekspresikan dengan konvensi ini, #[Query] memungkinkan custom SQL langsung di method declaration.
findAll() sudah tersedia di base class Repository tanpa perlu dideklarasikan ulang.
Untuk inject TaskRepository ke controller, framework perlu mengetahui class mana yang diinstansiasi. Pastikan nama class repository sudah match dengan type hint pada property yang diberi #[Wired], karena container DI PointArt menggunakan class name sebagai identifier.
Dependency Injection dan Service Layer
Untuk logika bisnis yang lebih kompleks, PointArt mendukung service class yang bisa diregistrasi ke container dan diinject ke mana saja.
Buat file app/components/TaskService.php:
<?php
#[Service]
class TaskService {
#[Wired]
private TaskRepository $taskRepository;
public function markAsDone(int $taskId): bool {
$task = Task::find($taskId);
if ($task === null) {
return false;
}
$task->isDone = true;
$task->save();
return true;
}
public function getPendingTasks(): array {
return $this->taskRepository->findByIsDone(false);
}
}
#[Service] mendaftarkan class ini ke service container sebagai singleton. Di controller, service ini bisa diinject dengan cara yang sama seperti repository:
#[Router(name: 'task', path: '/task')]
class TaskController {
#[Wired]
private TaskService $taskService;
#[Route('/pending', HttpMethod::GET)]
public function pending(): string {
$tasks = $this->taskService->getPendingTasks();
return Renderer::render('task.pending', ['tasks' => $tasks]);
}
}
Tidak ada binding manual di service container. Framework menemukan class yang diberi #[Service] secara otomatis saat boot, menyimpannya ke cache registry, dan meresolve dependency-nya saat dibutuhkan.
View: Plain PHP sebagai Template
PointArt sengaja tidak menyertakan template engine. View adalah file .php biasa di direktori app/views/, dirender menggunakan Renderer::render().
Nama view menggunakan dot notation — task.index akan memuat app/views/task/index.php. Variabel yang diteruskan melalui array parameter kedua akan tersedia langsung di scope view.
File app/views/task/index.php:
<!DOCTYPE html>
<html>
<head>
<title>Daftar Tugas</title>
</head>
<body>
<h1>Tugas yang Belum Selesai</h1>
<?php foreach ($tasks as $task): ?>
<div>
<h3><?= htmlspecialchars($task->title) ?></h3>
<p><?= htmlspecialchars($task->description) ?></p>
</div>
<?php endforeach; ?>
</body>
</html>
Ini adalah pilihan yang disengaja: dengan tidak menambahkan layer Blade atau Twig, framework tetap zero dependency. Trade-off-nya adalah tidak ada template inheritance bawaan — kamu perlu mengimplementasikan layout pattern sendiri jika dibutuhkan.
Kapan PointArt Masuk Akal
PointArt bukan pengganti Laravel untuk aplikasi besar. Ini adalah tool dengan scope yang jelas:
| Skenario | Cocok? |
|---|---|
| Prototipe cepat tanpa setup overhead | Ya |
| Deploy ke shared hosting tanpa akses SSH | Ya |
| Tim yang familiar dengan pola Spring Boot | Ya |
| Aplikasi enterprise dengan auth, queue, event | Tidak |
| Project yang butuh ekosistem package besar | Tidak |
Untuk aplikasi yang membutuhkan middleware yang kaya, autentikasi, queue processing, dan ekosistem package yang matang, Laravel atau Symfony tetap pilihan yang lebih solid.
Kesimpulan
PointArt membuktikan bahwa pola arsitektur yang bagus tidak bergantung pada ukuran framework. Attribute-based routing, DI container, dan dynamic repository yang selama ini identik dengan Spring Boot atau Symfony ternyata bisa diimplementasikan dalam PHP murni tanpa dependensi eksternal. Bagi developer yang sering kerja di lingkungan shared hosting, atau yang ingin memahami cara framework besar bekerja di bawah kapnya, PointArt layak dijelajahi — kodenya bisa dibaca langsung di GitHub repository.