Files
sides/.planning/codebase/ARCHITECTURE.md
2026-05-28 16:25:22 +08:00

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() or DB::select() — not Eloquent models
  • Only User model (app/Models/User.php) extends Authenticatable — 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: DB facade, Carbon, Excel/Pdf facades, 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/auth package, Laravel Http facade

Export Layer:

  • Purpose: Generate downloadable files (Excel, PDF)
  • Location: app/Exports/*.php + resources/views/pdf/*.blade.php
  • Contains: Excel exports using Maatwebsite\Excel with FromCollection, WithHeadings, ShouldAutoSize; PDF exports using Barryvdh\DomPDF
  • Patterns: Export classes receive station/date parameters via constructor, queries data in collection(), defines headings in headings()

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 in auth/, nav in nav/, shared components in components/
  • Key inheritance: layout.homeapplayout.home (public); layout.app → all authenticated pages

Data Flow

Primary Request Path — Dashboard Page

  1. Request hits GET /dashboard or GET / — mapped in routes/web.php:60-61
  2. MapController::getCurrentData() app/Http/Controllers/MapController.php:23
  3. Executes a raw query joining station with latest rainfall, waterlevel, siren records using subqueries
  4. Returns view('layout.dashboard', compact('data'))
  5. Blade view extends layout.app which includes nav.header (head, CSS, Leaflet), nav.navbar (navigation), and sections content1 (station data table + map) and content2 (info section)
  6. Response includes embedded JavaScript with window.translations for i18n and Leaflet map rendering

Auth Flow — Login

  1. POST /loginAuthenticatedSessionController::store() app/Http/Controllers/Auth/AuthenticatedSessionController.php:55
  2. Uses LoginRequest app/Http/Requests/Auth/LoginRequest.php for rate limiting (5 attempts)
  3. Custom pre-check: queries User::where('name', $name) checks is_blocked flag
  4. If blocked → returns error; if login_attempts >= 3 → auto-block on failed attempt
  5. If success → reset login_attempts, regenerate session, redirect to /
  6. Login page (GET /login) also renders the dashboard map with station data (create() method)

Alert Flow — External IoT Alert → FCM Push

  1. External system POST /api/alertapi.php:18AlertController::send() app/Http/Controllers/Api/AlertController.php:17
  2. Receives stationid, level, stationtype (1=Rainfall, 2=WaterLevel, 3=Siren)
  3. Injects FcmService via constructor
  4. Calls $this->fcm->sendToTopic($topic, $title, $body)
  5. FcmService::sendToTopic() app/Services/FcmService.php:23:
    • Reads FIREBASE_PROJECT_ID and FIREBASE_CREDENTIALS from 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

Export Flow — Download Excel

  1. GET /rainfall/historical/exportRainfallController::exportHourlyRainfallExcel() app/Http/Controllers/RainfallController.php:360
  2. Reads query params (station, startdate, enddate)
  3. Instantiates HourlyRainfallExport($stationid, $startDate, $endDate) app/Exports/HourlyRainfallExport.php
  4. Export class queries PostgreSQL with pivot (24 hour columns) and returns Excel download

Export Flow — Download PDF

  1. GET /export/rainfall-history/pdfNotificationController::exportHistoryRfPDF() app/Http/Controllers/NotificationController.php:91
  2. Queries notification join station where stationtype=1
  3. Pdf::loadView('pdf.rfhistory', compact('rfHistory')) → renders resources/views/pdf/rfhistory.blade.php
  4. 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, defines rules(), may include authorize() and custom authenticate() 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 in render()

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: web group → auth or guest or admin → controller
  • Custom middleware: admin alias → AdminMiddleware; LocalizationMiddleware appended to web group

API:

  • Location: routes/api.php
  • Triggers: External IoT systems, mobile clients
  • Middleware stack: api group 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() or DB::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(), ::date casts, ::time casts, 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 (see RainfallController.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.app uses @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(): Explicit try/catch for ValidationException and general \Exception — returns first error message to session flash
  • AuthenticatedSessionController::store(): Returns back()->with('error', ...) for block/failed states
  • FcmService: Throws \Exception if token fetch fails — caught by AlertController implicitly (no try/catch)
  • Validation on form requests: Uses $request->validate() inline in controllers (most cases) or dedicated FormRequest classes

Cross-Cutting Concerns

Logging:

  • No application-level logging detected beyond default Laravel log channel
  • FIREBASE_CREDENTIALS JSON 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 /alert endpoint (raw $request->stationid access without validation)

Authentication:

  • Web: Session-based with Laravel Breeze scaffolding, custom is_blocked check and login_attempts tracking
  • API: Custom username/password verification via raw SQL + Hash::check() — returns JSON with error boolean
  • Admin: Middleware gate AdminMiddleware checks access_level !== 1
  • No token-based or OAuth API auth

Localization:

  • Two locales: en (English) and bm (Bahasa Malaysia)
  • Language files: lang/{en,bm}/{messages,toast,auth,validation,pagination,passwords}.php
  • Route: GET /locale/{locale}LocaleController::setLocale() — sets App::setLocale() and Session::put('locale')
  • Middleware: LocalizationMiddleware reads locale from session on every request and sets App::setLocale()

Architecture analysis: 2026-05-28