16 KiB
Architecture
Analysis Date: 2026-05-28
System Overview
This is a Laravel 12 MVC application — the Sabo Integrated Debris Flow Monitoring and Early Warning System (SIDES) for Sungai Kupang, Baling, Malaysia. It monitors rainfall, water level, and siren station data from debris flow monitoring stations, serves a dashboard with Leaflet maps + Chart.js graphs, sends Firebase Cloud Messaging (FCM) alerts, and exports data to Excel/PDF.
┌─────────────────────────────────────────────────────────────────────┐
│ Presentation Layer │
│ Blade Layouts (layout.app, layout.homeapp) / PDF Templates / JSON │
│ `resources/views/layout/`, `resources/views/pdf/` │
├──────────────────┬────────────────────┬─────────────────────────────┤
│ Web Routes │ API Routes │ Console / Artisan │
│ `routes/web.php`│ `routes/api.php` │ `routes/console.php` │
├─────────┬────────┴─────────┬──────────┴──────────┬──────────────────┤
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │Controllers │ │ Auth Ctrlrs │ │ API Controllers │ │
│ │ Web │ │ (Breeze) │ │ (JSON responses) │ │
│ └──────┬─────┘ └──────┬───────┘ └────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Service Layer │ │
│ │ FcmService (`app/Services/FcmService.php`) │ │
│ │ → Firebase Cloud Messaging (topic-based push) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Eloquent Model Layer (minimal — only User model used) │ │
│ │ All other data access via DB::table() / DB::select() │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL Database │ │
│ │ Tables: users, station, rainfall, waterlevel, siren, │ │
│ │ notification, password_reset_tokens, sessions, │ │
│ │ cache, jobs, job_batches │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Component Responsibilities
| Component | Responsibility | File(s) |
|---|---|---|
| Web Controllers | Serve Blade views with paginated/aggregated station data | app/Http/Controllers/{Map,Rainfall,WaterLevel,Siren,Notification,cctv,Admin,Profile,Locale}Controller.php |
| Auth Controllers | Login/Register/Password Reset/Verify Email via Laravel Breeze | app/Http/Controllers/Auth/*.php (9 files) |
| API Controllers | Return JSON for external IoT/second-screen consumption | app/Http/Controllers/Api/{Station,Auth,Alert}Controller.php |
| MapController | Dashboard entry — joins station + latest rainfall + latest waterlevel + latest siren | app/Http/Controllers/MapController.php |
| AdminController | CRUD for stations and users (access_level=1 gate) | app/Http/Controllers/AdminController.php |
| FcmService | Google OAuth2 token fetch → FCM v1 HTTP API topic push | app/Services/FcmService.php |
| Export Layer | Excel (Maatwebsite) + PDF (DomPDF) generation | app/Exports/*.php, resources/views/pdf/*.blade.php |
| Middleware | Authentication gate + admin role check + locale | app/Http/Middleware/{Admin,Localization}Middleware.php |
Pattern Overview
Overall: Model-View-Controller with heavy use of raw SQL (PostgreSQL dialect) and zero Eloquent ORM outside the User model.
Key Characteristics:
- All database queries use
DB::table()orDB::select()— not Eloquent models - Only
Usermodel (app/Models/User.php) extendsAuthenticatable— used for auth + password reset notification override - Controllers return Blade views (
view()) for web, JSON (response()->json()) for API - No repository/DAO abstraction layer — queries live directly in controllers
- Single service class (
FcmService) handles Firebase Cloud Messaging - Localization via session-based middleware with two locales: English (
en) and Bahasa Malaysia (bm)
Layers
Web Controllers (Presentation + Logic):
- Purpose: Handle HTTP requests, query data via raw SQL, return Blade views or JSON
- Location:
app/Http/Controllers/(11 files) +app/Http/Controllers/Auth/(9 files) - Contains: Request handling, SQL queries, view/JSON responses, form validation (inline
$request->validate()) - Depends on:
DBfacade,Carbon,Excel/Pdffacades,FcmService(AlertController only) - Used by: Web routes (
routes/web.php,routes/auth.php)
API Controllers:
- Purpose: External data endpoints for mobile/IoT clients, always return JSON
- Location:
app/Http/Controllers/Api/(3 files) - Contains: Station data queries, custom auth (username/password via raw SQL), FCM alert proxy
- Used by: API routes (
routes/api.php) - Key difference from web controllers: No middleware/auth gates on most API routes (except login)
Service Layer:
- Purpose: Encapsulate external service integrations
- Location:
app/Services/FcmService.php - Contains: Firebase OAuth2 token generation, FCM v1 HTTP API calls
- Dependencies:
google/authpackage, LaravelHttpfacade
Export Layer:
- Purpose: Generate downloadable files (Excel, PDF)
- Location:
app/Exports/*.php+resources/views/pdf/*.blade.php - Contains: Excel exports using
Maatwebsite\ExcelwithFromCollection,WithHeadings,ShouldAutoSize; PDF exports usingBarryvdh\DomPDF - Patterns: Export classes receive station/date parameters via constructor, queries data in
collection(), defines headings inheadings()
View Layer:
- Purpose: Render HTML and PDF output
- Location:
resources/views/ - Contains: Blade layouts (
layout.app,layout.homeapp), section-based views, component templates, PDF templates - Organization: Layouts in
layout/, auth screens inauth/, nav innav/, shared components incomponents/ - Key inheritance:
layout.homeapp→layout.home(public);layout.app→ all authenticated pages
Data Flow
Primary Request Path — Dashboard Page
- Request hits
GET /dashboardorGET /— mapped inroutes/web.php:60-61 MapController::getCurrentData()app/Http/Controllers/MapController.php:23- Executes a raw query joining
stationwith latestrainfall,waterlevel,sirenrecords using subqueries - Returns
view('layout.dashboard', compact('data')) - Blade view extends
layout.appwhich includesnav.header(head, CSS, Leaflet),nav.navbar(navigation), and sectionscontent1(station data table + map) andcontent2(info section) - Response includes embedded JavaScript with
window.translationsfor i18n and Leaflet map rendering
Auth Flow — Login
POST /login→AuthenticatedSessionController::store()app/Http/Controllers/Auth/AuthenticatedSessionController.php:55- Uses
LoginRequestapp/Http/Requests/Auth/LoginRequest.phpfor rate limiting (5 attempts) - Custom pre-check: queries
User::where('name', $name)checksis_blockedflag - If blocked → returns error; if
login_attempts >= 3→ auto-block on failed attempt - If success → reset
login_attempts, regenerate session, redirect to/ - Login page (GET /login) also renders the dashboard map with station data (
create()method)
Alert Flow — External IoT Alert → FCM Push
- External system
POST /api/alert→api.php:18→AlertController::send()app/Http/Controllers/Api/AlertController.php:17 - Receives
stationid,level,stationtype(1=Rainfall, 2=WaterLevel, 3=Siren) - Injects
FcmServicevia constructor - Calls
$this->fcm->sendToTopic($topic, $title, $body) FcmService::sendToTopic()app/Services/FcmService.php:23:- Reads
FIREBASE_PROJECT_IDandFIREBASE_CREDENTIALSfrom env - Loads service account JSON, fetches OAuth2 token via
google/auth - POSTs to
https://fcm.googleapis.com/v1/projects/{projectId}/messages:send - Returns HTTP status code
- Reads
Export Flow — Download Excel
GET /rainfall/historical/export→RainfallController::exportHourlyRainfallExcel()app/Http/Controllers/RainfallController.php:360- Reads query params (
station,startdate,enddate) - Instantiates
HourlyRainfallExport($stationid, $startDate, $endDate)app/Exports/HourlyRainfallExport.php - Export class queries PostgreSQL with pivot (24 hour columns) and returns Excel download
Export Flow — Download PDF
GET /export/rainfall-history/pdf→NotificationController::exportHistoryRfPDF()app/Http/Controllers/NotificationController.php:91- Queries
notificationjoinstationwherestationtype=1 Pdf::loadView('pdf.rfhistory', compact('rfHistory'))→ rendersresources/views/pdf/rfhistory.blade.php- Returns PDF download response
State Management:
- Session-based auth state (standard Laravel session driver)
- Locale stored in session (
Session::put('locale', ...)) - No client-side state management (Alpine.js for minimal interactivity in navigation dropdowns)
Key Abstractions
Controller Base Class:
- Purpose: Empty abstract class for controller type-hinting
- File:
app/Http/Controllers/Controller.php - Pattern:
abstract class Controller {}— no shared logic
Form Request Classes:
- Purpose: Encapsulate validation and auth logic for form submissions
- Files:
app/Http/Requests/ProfileUpdateRequest.php,app/Http/Requests/Auth/LoginRequest.php - Pattern: Extends
FormRequest, definesrules(), may includeauthorize()and customauthenticate()methods
Export Classes (Excel):
- Purpose: Query + format data for spreadsheet download
- Files:
app/Exports/HourlyRainfallExport.php,app/Exports/WaterLevelExport.php - Pattern: Implement
FromCollection,WithHeadings,ShouldAutoSize; constructor receives filter params;collection()returns query result
View Components:
- Purpose: Blade component classes for layout rendering
- Files:
app/View/Components/AppLayout.php,app/View/Components/GuestLayout.php - Pattern: Extends
Component, returns view inrender()
Entry Points
Web Application:
- Location:
routes/web.php(main),routes/auth.php(auth routes, included from web.php:85) - Triggers: Browser HTTP requests
- Middleware stack:
webgroup →authorguestoradmin→ controller - Custom middleware:
adminalias →AdminMiddleware;LocalizationMiddlewareappended towebgroup
API:
- Location:
routes/api.php - Triggers: External IoT systems, mobile clients
- Middleware stack:
apigroup only (no auth middleware on station data endpoints) - No authentication required on station data endpoints (public JSON)
Console:
- Location:
routes/console.php - Triggers:
php artisan inspire(only default, no custom commands)
Architectural Constraints
- No Eloquent ORM usage: All data access (except User auth) uses raw SQL via
DB::table()orDB::select()with named/positional bindings. This means: no model relationships, no scopes, no eager loading, no model events. - PostgreSQL-specific SQL: The codebase uses
TO_CHAR(),EXTRACT(),::datecasts,::timecasts,DATE_TRUNC(),INTERVAL,DISTINCT ON— this is not portable to other databases. - SQL injection risk from string interpolation: Several controllers use string interpolation (
"WHERE s.stationid = '{$stationFilter}'") instead of parameterized bindings (seeRainfallController.php:36,WaterLevelController.php:30). - No automated tests for domain controllers: Tests only exist for Laravel Breeze auth scaffolding (
tests/Feature/Auth/— most files are empty). - Service layer is thin: Only one service class (
FcmService) exists. All business logic lives in controllers. - Mixed view inheritance:
layout.appuses@include()for header and@yield()for content sections;layouts.app(Laravel Breeze default) uses component-based slots ({{ $slot }}). These are two separate layout systems.
Error Handling
Strategy: Per-controller try/catch with redirect()->back()->with('error', $message) pattern.
Patterns:
AdminController::storeUser(): Explicittry/catchforValidationExceptionand general\Exception— returns first error message to session flashAuthenticatedSessionController::store(): Returnsback()->with('error', ...)for block/failed statesFcmService: Throws\Exceptionif token fetch fails — caught byAlertControllerimplicitly (no try/catch)- Validation on form requests: Uses
$request->validate()inline in controllers (most cases) or dedicatedFormRequestclasses
Cross-Cutting Concerns
Logging:
- No application-level logging detected beyond default Laravel log channel
FIREBASE_CREDENTIALSJSON read from file, no credential caching
Validation:
- Inline
$request->validate([...])in most controllers (AdminController,RainfallController,WaterLevelController) - Form request classes for profile update (
ProfileUpdateRequest) and login (LoginRequest) - No validation on API POST
/alertendpoint (raw$request->stationidaccess without validation)
Authentication:
- Web: Session-based with Laravel Breeze scaffolding, custom
is_blockedcheck andlogin_attemptstracking - API: Custom username/password verification via raw SQL +
Hash::check()— returns JSON witherrorboolean - Admin: Middleware gate
AdminMiddlewarechecksaccess_level !== 1 - No token-based or OAuth API auth
Localization:
- Two locales:
en(English) andbm(Bahasa Malaysia) - Language files:
lang/{en,bm}/{messages,toast,auth,validation,pagination,passwords}.php - Route:
GET /locale/{locale}→LocaleController::setLocale()— setsApp::setLocale()andSession::put('locale') - Middleware:
LocalizationMiddlewarereads locale from session on every request and setsApp::setLocale()
Architecture analysis: 2026-05-28