Inventory JTI
Overview
Inventory JTI is a full inventory and loan management system for the JTI department. Staff and students borrow equipment, track what they took, and return it with condition reports. I built it for a Web Dasar course that banned frameworks and ORMs, so I wrote the entire engine myself in native PHP. No Composer, no Laravel, no packages beyond a bundled PHPMailer. The result is a working system with 45+ routes, 10 relational tables, email OTP verification, and a 7-state loan workflow.
Problem
- The coursework forbade frameworks and ORMs, so every team built from scratch
- The JTI department tracked borrowed equipment on paper, which lost items and return dates
- Most teams produced flat scripts that grew unmanageable past a few pages
- Five developers needed one structure they could all follow without a framework manual
Goal
- Build a native PHP MVC engine from Laravel's ideas, without using Laravel
- Ship a complete loan workflow: borrow, approve, return, and condition tracking
- Add secure auth with email verification and password recovery
- Keep the engine database-agnostic so it can run on MySQL or MSSQL
Role
I led the architecture and core implementation for a team of 4, with 3 of us writing code. I designed the Router, the middleware pipeline, the repository and service layers, and the database driver abstraction the whole system ran on. I built the auth flow end to end. Team: Achmad Raihan Fahrezi Effendy (Lead Developer) + 3 teammates
Architecture
flowchart TD
A["Browser (Bootstrap + jQuery)"] --> B[".htaccess rewrite"]
B --> C["Front Controller (public/route.php)"]
C --> D["Router"]
D --> E["Middleware pipeline"]
E --> F["Controller"]
F --> G["Service"]
G --> H["Repository"]
H --> I[("Database: MySQL or MSSQL")]
G --> J["Validation"]
F --> K["View (PHP templates)"]
The engine mirrors Laravel's flow: a front controller receives every request through .htaccess, the Router matches it to a controller, middleware runs before the controller, and the controller delegates to Service and Repository layers. The View layer renders PHP templates with Bootstrap.
Database Design
Ten tables model users, equipment, and loans. Transaksi records each loan with a purpose and a collateral entry, and DetailTransaksi tracks the condition of each returned item.
erDiagram
Level ||--o{ Pengguna : "has"
Pengguna ||--o{ OTP : "receives"
Pengguna ||--o{ Transaksi : "borrows"
Maintainer ||--o{ Transaksi : "approves"
Status ||--o{ Transaksi : "has"
Transaksi ||--|{ DetailTransaksi : "contains"
Inventaris ||--o{ DetailTransaksi : "listed_in"
Kategori ||--o{ Inventaris : "categorizes"
Maintainer ||--o{ MaintainerInventaris : "maintains"
Inventaris ||--o{ MaintainerInventaris : "maintained_by"
Level {
string ID_Level PK
string Nama
}
Status {
string ID_Status PK
string Nama
}
Kategori {
string ID_Kategori PK
string Nama
}
Pengguna {
string ID_Pengguna PK
string ID_Level FK
string Nomor_Identitas
string Password
string Nama
string Email
string Nomor_HP
string Foto
string Status "AKTIF / TIDAK AKTIF"
string Salt
}
OTP {
string ID_OTP PK
string ID_Pengguna FK
string Kode "6 digit"
datetime Expired
}
Maintainer {
string ID_Maintainer PK
string Nama
}
Inventaris {
string ID_Inventaris PK
string Nama
int Stok
string ID_Kategori FK
string Asal "HIBAH / BELI"
text Deskripsi
string Gambar
}
MaintainerInventaris {
string ID_Maintainer FK
string ID_Inventaris FK
}
Transaksi {
string ID_Transaksi PK
string ID_Pengguna FK
string ID_Admin FK
string ID_Status FK
datetime StartDate
datetime EndDate
text Deskripsi_Keperluan
string Jaminan "collateral"
text Pesan
}
DetailTransaksi {
string ID_DetailTrc PK
string ID_Transaksi FK
string ID_Inventaris FK
string Kondisi "Normal / Rusak / Hilang"
int Jumlah
}
Auth Flow
Registration requires email verification before the account activates. The system generates a 6-digit code, emails it through PHPMailer, and only flips the user to active after the code matches. Passwords are hashed with bcrypt plus a per-user salt, and the salt is bound to the user's identity number during verification.
sequenceDiagram
participant U as User
participant W as Web (AuthController)
participant S as AuthService
participant D as Database
participant M as Mailer (PHPMailer)
U->>W: Submit register form
W->>S: register(data)
S->>D: Create user (TIDAK AKTIF, bcrypt+salt)
S->>D: Create OTP (6-digit, expiry)
S->>M: Send verification email
M-->>U: Email with code
U->>W: Enter OTP
W->>S: verifyOTP(code)
S->>D: Match code + check expiry
S->>D: Set status AKTIF
S-->>U: Account activated
U->>W: Login with email + password
W->>S: login(credentials)
S->>D: Fetch user by email
S->>D: Verify bcrypt(salt + identity)
S-->>U: Session created
Loan Workflow
Every loan moves through a 7-state machine. A member submits a request with a purpose and collateral. An admin accepts or rejects it, the item leaves with a start date, and the return records each item's condition as Normal, Rusak (damaged), or Hilang (lost). A damaged or lost item sends the loan into a replacement state.
stateDiagram-v2
[*] --> Menunggu: Member submits loan
Menunggu --> Diterima: Admin accepts
Menunggu --> Ditolak: Admin rejects
Diterima --> Proses: Item handed over
Proses --> Selesai: Returned, condition recorded
Diterima --> Dibatalkan: Member cancels
Menunggu --> Menunggu Ganti: Item damaged or lost
Menunggu Ganti --> Selesai: Replacement received
Ditolak --> [*]
Dibatalkan --> [*]
Selesai --> [*]

Key Features
- Custom PHP MVC Engine: Router, View, and middleware pipeline written from scratch, with zero Composer dependencies
- Repository + Service + Validation Layers: 11 models, 11 repositories, 8 services, and 8 validation classes
- RBAC Middleware: GuestOnly, AuthOnly, AdminOnly, and MemberOnly guards across 45+ routes
- OTP Email Auth: Register and forgot-password flows verified by a 6-digit code sent via PHPMailer
- Secure Passwords: bcrypt with a per-user salt bound to the identity number
- Loan Management: Borrow with purpose and collateral, approve or reject, track start and end dates
- Condition Tracking: Each returned item records Normal, Rusak, or Hilang
- Multi-Database: MySQL and MSSQL behind a driver interface, switchable through config
- Inventory CRUD: Items with category, stock, origin (HIBAH or BELI), and image upload
Routes
The route table covers three areas: public auth, member inventory actions, and admin management.
Technical Decisions
- Native PHP because the course banned frameworks, and the constraint forced me to learn how a router, a container, and middleware work
- Zero Composer: every class loads through explicit
require_once; the only bundled code is PHPMailer. Nothing to audit, nothing to update - bcrypt + per-user salt instead of plain hashing, with the salt bound to the identity number so two identical passwords hash differently
- OTP by email over phone verification, since the department runs on email and PHPMailer was already reliable
- MySQL + MSSQL drivers behind one interface, so the app works in the campus labs (MySQL) and in any client environment (MSSQL)
- Repository pattern to keep SQL out of controllers and services, modeled on the separation Laravel teams use

Challenges
- Writing a Router and middleware pipeline from scratch meant matching every request to a pattern, and getting the
.htaccessrewrite to feed clean paths to the front controller - Enforcing code quality across a team working without framework conventions demanded written standards and regular review
- Wiring OTP email end to end required handling SMTP failures, expiry windows, and resend paths
- Keeping the loan state machine consistent across member, admin, and history views took the most careful design
Outcome
The system shipped with 45+ routes, a full OTP auth flow, and a working loan workflow for the JTI department. The engine ran the course project and proved that a framework-grade structure is possible in plain PHP. The lecturer recognized the project for its architecture, and it earned top marks.

Final Thoughts
Inventory JTI taught me that frameworks are tools, not crutches. Once you understand what Laravel does under the hood, routing, middleware, repositories, you can build the same structure by hand. The zero-dependency constraint made every layer intentional. The native PHP engine I built here became the foundation for how I read and evaluate every framework afterward.