Application works, solves real problems, demonstrates clinical domain knowledge
Several patterns will make you look amateur in senior/architect interviews
2 fatal security/data integrity issues that MUST be fixed immediately
SIMKESGI - Clinical Dental Management System
Prepared by: Senior Laravel Developer Perspective
Review Date: February 2026
Codebase: SIMKESGI (Clinical Dental Management System)
Application works, solves real problems, demonstrates clinical domain knowledge
Several patterns will make you look amateur in senior/architect interviews
2 fatal security/data integrity issues that MUST be fixed immediately
Data orphans WILL happen when concurrent deletes occur. No database-level protection against referential integrity violations.
// Migration
$table->foreignId('kartupasien_id'); // Just creates integer column
// NO ->constrained() or ->references()
"Candidate doesn't understand database fundamentals. Would create data integrity disasters in production."
// Migration - ADD THIS
Schema::create('vitalitas', function (Blueprint $table) {
$table->id();
$table->foreignId('odontogram_id')
->constrained('odontograms')
->onDelete('cascade'); // Or 'restrict' for safety
// ... rest
});
Potential Cross-Site Request Forgery vulnerability if routes are excluded from VerifyCsrfToken middleware.
// View code - Missing CSRF token in AJAX
$.ajax({
url: '/getPenyebabs',
type: 'POST',
data: { askepgilut: askepgilut }, // NO _token!
cache: false,
success: function(msg) { ... }
});
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajaxSetup() is per-page setupVerifyCsrfToken.php)"Show me your VerifyCsrfToken middleware exceptions list."
If you excluded /getPenyebabs, /getGejalas from CSRF protection β Cross-Site Request Forgery attacks are possible!
// ALWAYS include token per request (safer than global setup)
$.ajax({
url: '/getPenyebabs',
type: 'POST',
data: {
_token: $('meta[name="csrf-token"]').attr('content'),
askepgilut: askepgilut
},
success: function(msg) { ... }
});
Include _token explicitly in each AJAX request rather than relying on global $.ajaxSetup(). This ensures CSRF protection is never accidentally omitted.
Using string NIM instead of proper foreign key relationship
// Migration
$table->string('pembimbing'); // Stores NIM as string!!!
-- This query is SLOW (string comparison)
SELECT * FROM diagnosas
WHERE pembimbing = '202301234';
-- vs proper FK (integer comparison + indexed)
SELECT * FROM diagnosas
WHERE pembimbing_id = 15;
// Migration
$table->foreignId('pembimbing_id')
->constrained('users')
->onDelete('restrict'); // Prevent deleting active supervisor
// Model
public function pembimbing() {
return $this->belongsTo(User::class, 'pembimbing_id');
}
String comparisons are significantly slower than integer comparisons, especially with large datasets. Proper foreign keys also enable database-level indexing and query optimization.
Violates First Normal Form - atomic values rule
// diagnosas table
'askepgilut' => '1|2|3',
'penyebab' => '1,2|3,4|5,6',
'gejala' => '7,8|9,10|11,12'
LIKE '%1%' (no indexes)explode('|') logic fragile"How do you query: Show all diagnoses using penyebab_id = 5?"
// Horrible - full table scan
$diagnosas = Diagnosa::where('penyebab', 'LIKE', '%5%')->get();
// Also matches '15', '25', '51' β WRONG RESULTS!
// Create pivot tables
Schema::create('diagnosa_askepgilut', function($table) {
$table->foreignId('diagnosa_id')->constrained()->onDelete('cascade');
$table->foreignId('askepgilut_id')->constrained()->onDelete('cascade');
$table->primary(['diagnosa_id', 'askepgilut_id']);
});
// Model
class Diagnosa extends Model {
public function askepgiluts() {
return $this->belongsToMany(Askepgilut::class);
}
}
// Clean queries
$diagnosa->askepgiluts; // Eloquent handles it
Askepgilut::find(5)->diagnosas; // Reverse query works
Good that you recognized this as technical debt in your presentation!
Pollutes global namespace and mixes concerns
// helper.php
if (!function_exists('getPatients')) {
function getPatients($user_id) {
// Returns HTML string
return '<option>...</option>';
}
}
// app/Services/PatientService.php
namespace App\Services;
class PatientService {
public function getPatientsByUser(int $userId): Collection {
return Kartupasien::where('user_id', $userId)->get();
}
}
// app/View/Components/PatientSelect.php
class PatientSelect extends Component {
public function __construct(public Collection $patients) {}
public function render() {
return view('components.patient-select');
}
}
// Usage in Blade
<x-patient-select :patients="$patientService->getPatientsByUser($userId)" />
Validation rules scattered in controllers
// Controller
public function store(Request $request) {
// Inline validation or none at all
$diagnosa = new Diagnosa();
$diagnosa->askepgilut = $request->input('askepgilut');
$diagnosa->save();
}
// app/Http/Requests/StoreDiagnosaRequest.php
class StoreDiagnosaRequest extends FormRequest {
public function authorize() {
return Gate::allows('mahasiswa');
}
public function rules() {
return [
'diagnosa.*.askepgilut' => 'required|array|min:1',
'diagnosa.*.askepgilut.*' => 'exists:askepgiluts,id',
'diagnosa.*.penyebab' => 'required|array|min:1',
'diagnosa.*.penyebab.*' => 'exists:penyebabs,id',
'kartupasien_id' => 'required|exists:kartupasiens,id',
];
}
public function messages() {
return [
'diagnosa.*.askepgilut.required' => 'Pilih minimal 1 diagnosis',
];
}
}
// Controller - CLEAN
public function store(StoreDiagnosaRequest $request) {
// Request already validated, authorized
$validated = $request->validated();
// Business logic only
}
FormRequest classes keep controllers thin, centralize validation logic, and make code more maintainable and testable.
Manual cascade operations are not atomic
public function destroy(Eksplakkal $eksplakkal) {
Eksplakkal::destroy($eksplakkal->id);
Periodontal::where('eksplakkal_id', $eksplakkal->id)->delete();
}
| Time | User A | User B |
|---|---|---|
| 10:00 | DELETE eksplakkal #5 | |
| 10:01 | (controller starts cascade) | INSERT periodontal (eksplakkal_id=5) |
| 10:02 | DELETE periodontals WHERE... | (Success - no FK constraint!) |
| 10:03 | (cascade complete) | |
| 10:04 | (periodontal saved with dead eksplakkal) |
// Migration - Let DB handle it
$table->foreignId('eksplakkal_id')
->constrained()
->onDelete('cascade'); // Database handles cascade atomically
// Controller - Simple & Safe
public function destroy(Eksplakkal $eksplakkal) {
$eksplakkal->delete(); // DB cascades automatically
}
public function destroy(Eksplakkal $eksplakkal) {
DB::transaction(function() use ($eksplakkal) {
Periodontal::where('eksplakkal_id', $eksplakkal->id)->delete();
$eksplakkal->delete();
});
}
These issues won't break your application but will impact code maintainability, scalability, and team collaboration. They're important to address for professional-grade applications.
public function store(Request $req) {
$data = Kartupasien::where('user_id', auth()->id())
->with(['diagnosas', 'odontogram'])
->first();
// Complex business logic here
$dmft = $this->calculateDMFT($data);
$ohis = $this->calculateOHIS($data);
return view('result', compact('data', 'dmft'));
}
// Controller
public function store(
Request $req,
PatientService $service
) {
$patient = $service->getPatientData(auth()->id());
$assessment = $service->calculateAssessment($patient);
return view('result', compact('patient', 'assessment'));
}
// Service
class PatientService {
public function calculateAssessment($patient) {
return [
'dmft' => $this->calculateDMFT($patient),
'ohis' => $this->calculateOHIS($patient),
];
}
}
// Mixed conventions
$table->string('nama_pasien'); // snake_case
public function getPasiens() { } // camelCase + Indonesian
class kartupasien extends Model { } // lowercase + Indonesian
// Should be
$table->string('patient_name'); // snake_case English
public function getPatients() { } // camelCase English
class PatientCard extends Model { } // PascalCase English
// Repository Pattern
interface PatientRepositoryInterface {
public function findByUser(int $userId): ?Kartupasien;
public function getAllWithDiagnosis(): Collection;
}
class EloquentPatientRepository implements PatientRepositoryInterface {
public function findByUser(int $userId): ?Kartupasien {
return Kartupasien::where('user_id', $userId)
->with(['diagnosas', 'odontogram'])
->first();
}
}
// β Controller doing calculations
public function show($id) {
$data = Periodontal::find($id);
// Complex ACC calculation in controller
$acc = 0;
if ($data->pl_kuadran_1 + $data->di_kuadran_1 > 0) {
$acc += 1;
}
// ... more calculation logic
return view('show', compact('data', 'acc'));
}
// β
Move to Model or Service
class Periodontal extends Model {
public function calculateACC(): int {
$acc = 0;
if ($this->pl_kuadran_1 + $this->di_kuadran_1 > 0) {
$acc += 1;
}
// ... calculation logic
return $acc;
}
}
// Controller becomes clean
public function show($id) {
$data = Periodontal::find($id);
return view('show', [
'data' => $data,
'acc' => $data->calculateACC()
]);
}
// β Without transaction
public function store(Request $request) {
$kartupasien = Kartupasien::create($data);
Odontogram::create(['kartupasien_id' => $kartupasien->id]);
Periodontal::create(['kartupasien_id' => $kartupasien->id]);
// If third create fails, you have orphaned records!
}
// β
With transaction
public function store(Request $request) {
DB::transaction(function() use ($request) {
$kartupasien = Kartupasien::create($data);
Odontogram::create(['kartupasien_id' => $kartupasien->id]);
Periodontal::create(['kartupasien_id' => $kartupasien->id]);
// All or nothing - data integrity preserved
});
}
// β Magic numbers
if (auth()->user()->role == 1) { }
Gate::define('adminpembimbing', fn($user) => in_array($user->role, [1,2]));
// β
Use Enums (PHP 8.1+)
enum UserRole: int {
case ADMIN = 1;
case PEMBIMBING = 2;
case MAHASISWA = 3;
case PATIENT = 4;
case KARYAWAN = 5;
}
// Migration
$table->tinyInteger('role')->default(UserRole::MAHASISWA->value);
// Model
protected $casts = ['role' => UserRole::class];
// Usage
if (auth()->user()->role === UserRole::ADMIN) { ... }
Route::resource('/user', UserController::class)->middleware('admin');
public function kartupasien() {
return $this->belongsTo(Kartupasien::class);
}
Gate::define('adminpembimbing', fn($user) => in_array($user->role, [1,2]));
Route::resource('/diagnosa', DiagnosaController::class);
public function store(Request $request) // Laravel injects Request
These practices show you understand Laravel fundamentals and modern web development. Build on this foundation by addressing the critical and severe issues.
| Severity | Count | Issues |
|---|---|---|
| π΄ CRITICAL | 2 | No FK constraints, CSRF potential |
| π SEVERE | 5 | String FKs, delimited storage, helpers, no FormRequest, race conditions |
| π‘ MODERATE | 7 | No service layer, naming, no repos, ACC logic, transactions, N+1, magic numbers |
| π’ GOOD | 8 | Middleware, relationships, gates, REST, DI, client calc, Select2, domain knowledge |
Current: 6.5/10 β After fixes: 8.5/10
1. Add Foreign Key Constraints (2 hours)
2. Fix CSRF on AJAX (30 mins)
3. Wrap Deletes in Transactions (1 hour)
1. Convert pembimbing to FK (4 hours)
2. Create FormRequest Classes (3 hours)
3. Fix Naming Conventions (2 hours)
php artisan make:migration add_foreign_keys_to_all_tables
VerifyCsrfToken exceptions_token to all AJAX callsDB::transaction(function() { ... });
"Yeah, I know I should've used foreign keys but I forgot."
"In this project, I initially chose application-level referential integrity for development flexibility. In retrospect, this was a mistakeβdatabase constraints would provide better data safety and are a lesson learned for production systems. In my next project, I implemented proper FK constraints from day one."
"This was a conscious trade-off. The diagnosis data structure (multiple askepgiluts β penyebabs β gejala) required flexibility during early development when requirements changed frequently. I acknowledge this violates 1NF and isn't queryable efficiently. If I were to refactor, I'd implement proper pivot tablesβI've documented the migration path in my technical debt backlog."
"These helper functions were a rapid prototyping decision. They work but aren't testable or reusable. I've since learned about Laravel's View Components and Service classesβmy more recent code follows those patterns."
| Level | Status |
|---|---|
| Junior Developer | β Definitely above this |
| Mid-Level Developer | β οΈ You're here, with rough edges |
| Senior Developer | β Not yetβneed architecture skills |
Your code shows potential but has critical flaws that will hurt you in interviews. Good News: All fixable within 3 months of focused work.
Every senior dev has written code like this. The difference is they learned from it. Show that growth in interviews and you'll do great!
Have answers ready. Not excusesβlearning stories. π