Heroic Webman
Framework untuk membangun aplikasi SPA berbasis PHP + Alpine.js di atas Webman (Workerman). Server-side rendering pada first load, client-side navigation untuk halaman berikutnya.
Struktur Direktori
Setiap halaman adalah sebuah folder di dalam app/pages/ yang berisi setidaknya dua file:
app/pages/ ├── _layouts/ │ └── index.php ← HTML shell (satu file untuk semua halaman) ├── home/ │ ├── PageController.php ← Controller halaman │ └── template.php ← Template Alpine.js / HTML ├── about/ │ ├── PageController.php │ └── template.php └── dashboard/ ├── PageController.php └── template.php
Nama folder = segmen URL. Folder home → route / (root), folder lain → /nama-folder.
Membuat Halaman Baru
1. Buat PageController
<?php namespace app\pages\about; use Yllumi\Sayagi\attributes\FrontendRoute; use Yllumi\Sayagi\BaseController; #[FrontendRoute(route: '/about')] class PageController extends BaseController { public $data = []; public function getData() { $this->data['title'] = 'About Us'; $this->data['content'] = 'Deskripsi halaman about.'; } }
2. Buat template.php
<!-- app/pages/about/template.php --> <div x-data="$sayagi.page()"> <h1 x-text="data.title"></h1> <p x-text="data.content"></p> </div>
Selesai. Buka /about — Heroic otomatis mendeteksi controller dan merender halaman.
BaseController
Yllumi\Sayagi\BaseController adalah kelas induk semua PageController. Ia menyediakan tiga method otomatis:
| Method | HTTP | Deskripsi |
|---|---|---|
getIndex() |
GET /page |
Render full HTML (SSR) untuk first load |
getTemplate() |
GET /page/template |
Kembalikan fragment template untuk SPA navigation |
getData() |
GET /page/data |
Override ini untuk menyediakan data JSON ke Alpine |
Property $data
Semua data yang diset pada $this->data di dalam getData() akan:
- Di-inject ke template sebagai variabel PHP saat SSR (
getIndex) - Dikirim sebagai JSON ke Alpine saat SPA navigation melalui endpoint
/data
public function getData() { // Data ini otomatis tersedia di template sebagai $this->data $this->data['users'] = User::all(); $this->data['count'] = User::count(); }
/data terpisah dari SSR, override getData() saja — BaseController otomatis memanggilnya dari getIndex() dan endpoint GET /data diarahkan ke method getData() yang sama.Attribute #[FrontendRoute]
Pasang attribute ini di atas class PageController untuk mengkonfigurasi route frontend di Pinecone Router.
| Parameter | Tipe | Default | Deskripsi |
|---|---|---|---|
route |
string | auto | Path URL frontend, mis. /about. Jika kosong, otomatis dari nama folder. |
template |
string | auto | Path template relatif ke app/pages/, jika berbeda dari konvensi. |
preload |
bool | false |
Jika true, template di-preload oleh Pinecone Router saat app dimuat. |
handler |
array | [] |
Array nama fungsi Alpine untuk x-handler (mis. guard autentikasi). |
Contoh Penggunaan
// Auto-route dari nama folder (/about) #[FrontendRoute] // Route eksplisit #[FrontendRoute(route: '/tentang-kami')] // Route root (/) #[FrontendRoute(route: '/')] // Preload + custom template path #[FrontendRoute(route: '/dashboard', template: 'dashboard/main', preload: true)] // Dengan guard handler (fungsi Alpine global) #[FrontendRoute(route: '/profile', handler: ['isLoggedIn'])]
Template
File template.php adalah fragment HTML murni yang di-render oleh Pinecone Router. Untuk halaman yang butuh data, bungkus dengan x-data="$sayagi.page()":
<div x-data="$sayagi.page()"> <!-- Loading state --> <div x-show="ui.loading">Loading...</div> <!-- Data dari endpoint /page/data --> <h1 x-text="data.title"></h1> <ul> <template x-for="item in data.items || []" :key="item.id"> <li x-text="item.name"></li> </template> </ul> </div>
Template Statis (tanpa data)
Jika halaman tidak memerlukan data dinamis, cukup tulis HTML biasa tanpa x-data:
<div id="page-about">
<h1>Tentang Kami</h1>
<p>Konten statis di sini.</p>
</div>
PHP di Template (SSR)
Variabel dari getData() tersedia langsung saat SSR, namun hindari echo PHP di template agar SPA navigation tetap konsisten — gunakan Alpine x-text / x-bind saja.
$sayagi.page()
Factory function Alpine yang mengelola pengambilan data, caching, dan state halaman.
Opsi Konfigurasi
x-data="$sayagi.page({
title: 'Judul Halaman', // Set document.title
url: 'custom/data', // Override URL endpoint data
clearCachePath: 'other/data', // Hapus cache path lain saat init
headers: { 'X-Custom': '1' }, // Custom request headers
})"
Properties yang Tersedia di Template
| Property | Tipe | Deskripsi |
|---|---|---|
data |
object | Data dari endpoint JSON /page/data |
ui.loading |
bool | true saat sedang fetch data |
ui.error |
bool | true jika fetch gagal |
ui.errorMessage |
string | Pesan error dari response |
meta |
object | Data custom dari opsi meta: {} |
Methods
// Muat ulang data dari URL berbeda this.loadPage('other/data'); // Trigger fetch ulang this.fetchData(); // Assign response dan cache this.assignResponseData(response);
SSR Hydration
Saat first load, getIndex() menyuntikkan data ke window.__HEROIC_SSR_DATA__. $sayagi.page() mendeteksi ini dan langsung mengisi data tanpa fetch ke server — membuat halaman terasa instan.
Saat navigasi SPA ke halaman berikutnya, data di-fetch dari endpoint /page/data dan di-cache di memori browser.
Utilitas $sayagi
$sayagi.fetch(url, headers?)
Wrapper fetch() yang secara otomatis menambahkan Authorization: Bearer {token} dari localStorage dan base URL.
$sayagi.fetch('api/products') .then(res => this.data.products = res.data);
$sayagi.post(url, data?, headers?)
HTTP POST menggunakan FormData. Mendukung file upload, array, dan nested object.
$sayagi.post('api/save', { name: this.form.name, file: this.file }) .then(res => console.log(res.data));
Cache Manual
$sayagi.setCache('key', data); // Simpan ke cache $sayagi.getCache('key'); // Ambil dari cache (null jika tidak ada) $sayagi.clearCache('key'); // Hapus cache
Token Auth
Token disimpan di localStorage dengan key heroic_token. Semua request fetch/post otomatis menyertakannya.
localStorage.setItem('heroic_token', token); // Set token localStorage.removeItem('heroic_token'); // Hapus (logout)
Helper Functions (PHP)
partial($view, $data?)
Include file partial dari app/pages/. Cocok untuk header, footer, komponen yang dipakai banyak halaman.
<?php partial('_layouts/partials/head') ?> <?php partial('_components/card', ['title' => 'Hello']) ?>
pageView($view, $data?)
Render view ke string (untuk SSR injection). Digunakan internal oleh BaseController.
$html = pageView('home/template', ['title' => 'Hello']);
asset_url($filePath)
Generate URL aset dengan cache-busting otomatis berdasarkan waktu modifikasi file.
<script src="<?= asset_url('js/main.js') ?>"></script> <!-- Output: js/main.js?v=1716000000 -->
base_url($path?)
Kembalikan URL absolut berdasarkan config('app.url').
base_url('api/products'); // Output: https://example.com/api/products
Routing
Heroic menggunakan dua layer routing:
1. Backend Routing (PageRouter)
Webman menangkap semua request melalui fallback route. PageRouter mencocokkan path URL dengan struktur folder app/pages/ dan mendelegasikan ke method controller yang sesuai:
| URL | Method | Controller Method |
|---|---|---|
GET /about |
GET | getIndex() |
GET /about/template |
GET | getTemplate() |
GET /about/data |
GET | getData() |
POST /about/save |
POST | postSave() |
2. Frontend Routing (Pinecone Router)
Pinecone Router berjalan di browser, menangani navigasi SPA. Setiap halaman yang punya #[FrontendRoute] otomatis didaftarkan sebagai route.
<template x-route="/" x-template="/home/template"></template> <template x-route="/about" x-template="/about/template"></template> <!-- Di-generate otomatis oleh FERouter::getRouter() -->
Link antar halaman cukup menggunakan <a href="/about"> biasa — Pinecone Router akan mengintersepsi dan melakukan SPA navigation tanpa reload.
FERouter (PHP)
Yllumi\Sayagi\FERouter menghasilkan HTML router yang di-inject ke layout.
FERouter::getRouter()
FERouter::getRouter(
string $ssrRoute = '', // Route yang di-SSR, mis. '/'
string $ssrContent = '', // HTML hasil SSR
?array $ssrData = null // Data JSON untuk hydration
): string
Digunakan di _layouts/index.php:
<div id="router" x-data="router()"> <?= \Yllumi\Sayagi\FERouter::getRouter( $ssr_route ?? '', $ssr_content ?? '', $ssr_data ?? null ) ?> </div>
FERouter::ssrDataScript()
Generate script inline untuk menyuntikkan data SSR ke window.__HEROIC_SSR_DATA__.
<script>
<?= \Yllumi\Sayagi\FERouter::ssrDataScript($ssr_data ?? null) ?>
</script>
Method Controller Kustom
Selain method standar, kamu bisa menambahkan method apapun di controller. Konvensi penamaan: {httpVerb}{ActionName}().
class PageController extends BaseController { // GET /users/data → getData() public function getData() { return json(['users' => User::all()]); } // POST /users/store → postStore() public function postStore(Request $request) { User::create($request->post()); return json(['ok' => true]); } // GET /users/123 → getIndex($id) public function getIndex(Request $request, string $id) { return json(User::find($id)); } }