Sakuci Framework
Panduan lengkap: CLI, Routing, Model, Controller, View, Bootstrap UI, Validation, Database, Session, Middleware, dan Multi-Role Login.
Dokumentasi Sakuci
Kerangka PHP ringan bergaya Laravel tanpa Composer. Fitur: Route, Model, View, Controller, Validation, Session, Middleware, CSRF protection, Query Builder dengan eager loading, dan Bootstrap 5.3.8 built-in.
Instalasi cepat: Clone dari GitHub atau salin folder ke web server Anda.
# Clone dari GitHub
git clone https://github.com/indrabsus/sakuci-framework.git
cd sakuci-framework
# Atau download dan ekstrak folder
# Jalankan server
php sakuci serve
# Buka di browser
http://127.0.0.1:8000
Coba akun demo: admin / rahasia123 (atau staff, atau budi)
CLI -- Perintah sakuci
Jalankan semua perintah dari folder project:
php sakuci serve # Jalankan server (127.0.0.1:8000)
php sakuci migrate # Jalankan migrasi
php sakuci migrate:fresh # Hapus semua tabel
php sakuci db:check # Uji koneksi database
php sakuci route:list # Lihat semua route
php sakuci make:model Nama # Buat model
php sakuci make:model Nama -m # Buat model + migrasi
php sakuci make:controller Nama # Buat controller
php sakuci make:view nama.view # Buat view
php sakuci make:migration nama # Buat migrasi
php sakuci view:clear # Bersihkan cache view
Routing
Daftarkan route di routes/web.php:
use App\Controllers\PostController;
use Sakuci\Route;
// Route dasar
Route::get('/', function () { return view('welcome'); })->name('home');
Route::get('/posts', [PostController::class, 'index'])->name('posts.index');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
// Parameter
Route::get('/posts/{id}', [PostController::class, 'show'])->name('posts.show');
Route::get('/posts/{id}/edit', [PostController::class, 'edit'])->name('posts.edit');
// 7 route CRUD sekaligus
Route::resource('posts', PostController::class);
// Group dengan middleware
Route::group(['prefix' => 'admin', 'middleware' => 'admin'], function () {
Route::get('/', [AdminController::class, 'index'])->name('admin.dashboard');
});
Route dapat diakses lewat nama:
route('home') # /
route('posts.index') # /posts
route('posts.show', ['id' => 1]) # /posts/1
route('admin.dashboard') # /admin/
Model & Query Builder
Model mewakili satu tabel di database. Buat dengan:
php sakuci make:model Post -m
<?php
namespace App\Models;
use Sakuci\Database\Model;
class Post extends Model
{
protected static ?string $table = 'posts'; // opsional, ditebak dari nama
protected array $fillable = ['title', 'body', 'status'];
protected array $hidden = []; // tidak tampil saat JSON
}
Query dasar:
Post::all(); // Semua post
Post::find(1); // Post id=1
Post::findOrFail(1); // Atau 404
Post::where('status', 'published')->get();
Post::where('status', 'published')->first();
Post::latest()->limit(5)->get();
Post::count();
// Join
Post::leftJoin('users', 'users.id', '=', 'posts.user_id')
->select('posts.title', 'users.nama')
->get();
// Eager loading (cegah N+1)
Post::with('user', 'comments')->get();
// Paging
Post::paginate(15); // 15 per halaman
CRUD:
// Create
Post::create(['title' => 'Hello', 'body' => '...', 'status' => 'published']);
// Update
$post = Post::find(1);
$post->update(['title' => 'New title']);
// Delete
$post->delete();
Controller
Buat dengan: php sakuci make:controller PostController
<?php
namespace App\Controllers;
use App\Models\Post;
use Sakuci\Controller;
use Sakuci\Http\Request;
class PostController extends Controller
{
// GET /posts
public function index()
{
$posts = Post::latest()->paginate(10);
return view('posts.index', ['posts' => $posts]);
}
// GET /posts/create
public function create()
{
return view('posts.create');
}
// POST /posts
public function store(Request $request)
{
$data = $request->validate([
'title' => 'required|min:3|max:100',
'body' => 'required|min:10',
]);
Post::create($data);
return redirect(route('posts.index'))
->with('success', 'Post berhasil dibuat.');
}
// GET /posts/{id} -- Route model binding
public function show(Post $post)
{
return view('posts.show', ['post' => $post]);
}
// GET /posts/{id}/edit
public function edit(Post $post)
{
return view('posts.edit', ['post' => $post]);
}
// PUT /posts/{id}
public function update(Request $request, Post $post)
{
$data = $request->validate([
'title' => 'required|min:3|max:100',
'body' => 'required|min:10',
]);
$post->update($data);
return redirect(route('posts.index'))
->with('success', 'Post berhasil diubah.');
}
// DELETE /posts/{id}
public function destroy(Post $post)
{
$post->delete();
return redirect(route('posts.index'))
->with('success', 'Post berhasil dihapus.');
}
}
Penjelasan: Parameter bertipe (Post $post) otomatis diambil dari database berdasarkan {id}. Ini namanya route model binding.
View (Template)
File view ada di resources/views/ dengan akhiran .sakuci.php. Pakai sintaks Blade-like:
@extends('layouts.app')
@section('title', 'Daftar Post')
@section('content')
<h1>Daftar Post</h1>
{{-- Output aman (escape) --}}
<h2>{{ $post->title }}</h2>
{{-- Output tanpa escape --}}
{!! $html !!}
{{-- Kondisi --}}
@if ($posts->count() > 0)
Ada {{ $posts->count() }} post
@else
Belum ada post
@endif
{{-- Loop --}}
@foreach ($posts as $post)
<h3>{{ $post->title }}</h3>
@endforeach
{{-- Loop dengan else --}}
@forelse ($posts as $post)
<article>{{ $post->title }}</article>
@empty
<p>Belum ada post</p>
@endforelse
{{-- Include --}}
@include('components.post-card', ['post' => $post])
{{-- CSRF (wajib di form) --}}
<form method="POST" action="/posts">
@csrf
<input type="text" name="title">
</form>
{{-- Flash message --}}
@if (session('success'))
<div class="alert">{{ session('success') }}</div>
@endif
{{-- Validation error --}}
@error('title')
<span class="error">{{ $message }}</span>
@enderror
{{-- Old input --}}
<input type="text" value="{{ old('title') }}">
@endsection
Validation
Di controller, gunakan $request->validate():
$data = $request->validate([
'nama' => 'required|min:3|max:100',
'email' => 'required|email|unique:users,email',
'umur' => 'nullable|integer|min:18',
'status' => 'required|in:aktif,tidak-aktif',
]);
// Kalau validasi gagal, auto-redirect dengan error & old input
// Kalau validasi lulus, $data berisi nilai yang sudah validated
Rule tersedia: required, nullable, email, url, numeric, integer, min, max, between, in, not_in, regex, alpha, alpha_num, alpha_dash, confirmed, same, different, unique, exists, date, size, array, boolean, string
Bootstrap Components
Bootstrap 5.3.8 sudah built-in. Ganti warna khas di public/css/app.css:
:root {
--brand: #c2410c; /* Warna utama */
--brand-dark: #9a3412; /* Saat hover */
}
Component Bootstrap yang sering dipakai:
| Kelas | Fungsi |
|---|---|
| card | Kotak putih dengan bayangan |
| btn btn-primary | Tombol biru |
| btn btn-brand | Tombol warna khas Sakuci |
| form-label, form-control | Label & input form |
| is-invalid, invalid-feedback | Garis merah saat validasi gagal |
| alert alert-success | Notifikasi hijau |
| table table-hover | Tabel |
| mb-3, mt-4, p-4, gap-2 | Spacing (margin/padding) |
| row, col-md-6 | Grid (responsive) |
| navbar navbar-expand-lg | Navigation bar |
Contoh form pakai Bootstrap:
<form method="POST">
@csrf
<div class="mb-3">
<label class="form-label" for="nama">Nama</label>
<input type="text" id="nama" name="nama"
class="form-control {{ errors()->has('nama') ? 'is-invalid' : '' }}"
value="{{ old('nama') }}">
@error('nama')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button class="btn btn-brand" type="submit">Simpan</button>
</form>
Database Setup
Edit .env:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=sakuci_belajar
DB_USERNAME=root
DB_PASSWORD=
Lalu buat database (lewat phpMyAdmin atau SQL):
CREATE DATABASE sakuci_belajar CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
Uji koneksi:
php sakuci db:check
Untuk SQLite (tanpa setup), cukup ubah .env:
DB_CONNECTION=sqlite
Session & Flash Message
Di controller atau view:
use Sakuci\Session;
// Simpan
Session::put('user_id', 123);
Session::put('cart', ['item1', 'item2']);
// Baca
$id = Session::get('user_id');
$cart = Session::get('cart', []); // Default []
// Cek ada
if (Session::has('user_id')) { ... }
// Hapus
Session::forget('user_id');
// Flash (sekali pakai, auto-hapus setelah ditampilkan)
return redirect('/')->with('success', 'Berhasil login!');
Di view:
{{ session('success') }} {{-- atau null --}}
@if (session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
Middleware
Middleware adalah filter yang menjalankan kode sebelum/sesudah request masuk ke controller. Daftarkan di config/app.php:
'middleware' => [
'auth' => App\Middleware\Authenticate::class,
'guest' => App\Middleware\RedirectIfAuthenticated::class,
'admin' => App\Middleware\AdminOnly::class,
],
Pakai di route:
Route::get('/dashboard', [DashboardController::class, 'index'])
->middleware('auth');
Route::group(['middleware' => 'admin'], function () {
Route::get('/admin', [AdminController::class, 'index']);
});
Buat middleware:
<?php
namespace App\Middleware;
use Sakuci\Http\Request;
use Sakuci\Middleware;
class MyMiddleware extends Middleware
{
public function handle(Request $request, \Closure $next): mixed
{
// Sebelum masuk controller
if (/* kondisi gagal */) {
abort(403, 'Tidak diizinkan');
}
$response = $next($request);
// Setelah controller selesai
return $response;
}
}
Multi-Role Login System
Sistem login dengan 3 role: admin, staff, user. Lihat awal dokumentasi untuk detailnya.
Coba login dengan salah satu akun demo:
| Username | Password | Akses |
|---|---|---|
| admin | rahasia123 |
/admin
|
| staff | rahasia123 |
/staff
|
| budi | rahasia123 |
/dashboard
|
Tips & Trik
Helper global yang tersedia:
view('name', ['var' => 'value']) // Render view
route('name', ['id' => 1]) // Generate URL
redirect('/path') // Redirect
redirect(route('home'))
back() // Kembali ke halaman sebelumnya
old('field') // Nilai input sebelumnya
errors() // Object error dari validasi
session('key') // Baca session
dd($var) // Dump & die
abort(404, 'Not found') // Abort dengan status code
Eager loading (cegah N+1):
// Buruk: 11 query (1 posts + 10 comments)
foreach (Post::all() as $post) {
echo $post->comments;
}
// Bagus: 2 query (1 posts + 1 comments)
foreach (Post::with('comments')->get() as $post) {
echo $post->comments;
}
// Nested
Post::with('comments.user')->get();
Join data dari tabel lain:
User::leftJoin('profil', 'profil.user_id', '=', 'users.id')
->select('users.nama', 'profil.alamat')
->get();
Untuk pertanyaan atau kontribusi, kunjungi GitHub repository.