| Current Path : /var/www/html/dev/reddsis-dev/docs/ |
| Current File : /var/www/html/dev/reddsis-dev/docs/VISITOR-COUNTER.md |
# Visitor Counter — Custom Module
## Overview
Custom visitor tracking system built without third-party packages. Logs every page visit with device/browser/platform detection, supports authenticated user tracking and per-content view counting.
---
## Table of Contents
1. [Database Structure](#database-structure)
2. [Model & Methods](#model--methods)
3. [Traits](#traits)
4. [Usage Examples](#usage-examples)
5. [Admin Panel](#admin-panel)
6. [File Locations](#file-locations)
---
## Database Structure
### `visits` table
| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned (PK, auto) | Primary key |
| `method` | varchar(255) | HTTP method (`GET`, `POST`, `PUT`, etc.) |
| `request` | mediumtext | Form/query data (auto-cast to array) |
| `url` | mediumtext | Full visited URL |
| `referer` | mediumtext | Previous page URL (HTTP_REFERER) |
| `languages` | text | Browser languages e.g. `["ms-MY","en-US"]` (auto-cast to array) |
| `useragent` | text | Full User-Agent string |
| `headers` | text | All HTTP headers (auto-cast to array) |
| `device` | text | `Desktop`, `Mobile`, `Tablet`, `Android` |
| `platform` | text | `Windows`, `macOS`, `Linux`, `Android`, `iOS`, `Unknown` |
| `browser` | text | `Chrome`, `Firefox`, `Safari`, `Edge`, `Opera`, `IE`, `Unknown` |
| `ip` | varchar(45) | Visitor IP (supports IPv6) |
| `geo_raw` | json | Optional GeoIP payload (auto-cast to array) |
| `visitable_type` | varchar(255) | Morph — model class of visited content |
| `visitable_id` | bigint unsigned | Morph — ID of visited content |
| `visitor_type` | varchar(255) | Morph — model class of the visitor user |
| `visitor_id` | bigint unsigned | Morph — ID of the visitor user |
| `created_at` | timestamp | Visit timestamp |
| `updated_at` | timestamp | Last update timestamp |
**Indexes:**
- `visits_visitable_type_visitable_id_index` — for content-based queries
- `visits_visitor_type_visitor_id_index` — for user-based queries
### `visitor_settings` table
| Column | Type | Description |
|--------|------|-------------|
| `id` | bigint unsigned (PK) | Primary key |
| `is_active` | boolean (default `true`) | Toggle logging on/off globally |
| `created_at` | timestamp | |
| `updated_at` | timestamp | |
Only **one row** exists — fetched via `VisitorSetting::first()`.
---
## Model & Methods
### `App\Models\Backend\Visit`
#### Logging
**Guard check — always log through `VisitorSetting`:**
```php
use App\Models\Backend\Visit;
use App\Models\Backend\VisitorSetting;
if (VisitorSetting::first()?->is_active) {
Visit::log();
}
```
**Available log calls:**
```php
// Auto — uses current request + Auth::user()
Visit::log();
// With custom visitor (e.g. guest without auth)
Visit::log(visitor: $user);
// With visited content
Visit::log(visitable: $article);
// Full
Visit::log(request: $request, visitor: $user, visitable: $article);
```
**Parameters:**
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `$request` | `?Request` | `request()` | HTTP request instance |
| `$visitor` | `?Model` | `Auth::user()` | The user making the visit |
| `$visitable` | `?Model` | `null` | The content/page being visited |
Returns `?Visit` — `null` if no request available.
---
#### Static Stats Methods
| Method | Return | Example Result |
|--------|--------|----------------|
| `Visit::totalVisits()` | `int` | `1500` |
| `Visit::uniqueIps()` | `int` | `342` |
| `Visit::todayVisits()` | `int` | `47` |
| `Visit::yesterdayVisits()` | `int` | `52` |
| `Visit::thisWeekVisits()` | `int` | `310` |
| `Visit::thisMonthVisits()` | `int` | `1200` |
| `Visit::last3MonthsVisits()` | `int` | `3500` |
| `Visit::last6MonthsVisits()` | `int` | `7200` |
| `Visit::thisYearVisits()` | `int` | `15000` |
| `Visit::lastYearVisits()` | `int` | `18000` |
| `Visit::uniqueVisitors()` | `int` | `89` |
---
#### Scopes (chainable `Builder`)
```php
// Time-based
Visit::today()->count();
Visit::yesterday()->count();
Visit::thisWeek()->get();
Visit::thisMonth()->get();
Visit::last3Months()->count();
Visit::last6Months()->count();
Visit::thisYear()->get();
Visit::lastYear()->count();
Visit::between('2026-01-01', '2026-05-18')->count();
Visit::online(180)->count(); // active within last 3 minutes
// Filter by visitor / visitable
Visit::byVisitor($user)->count(); // visits by a specific user
Visit::byVisitable($article)->count(); // visits on specific content
// Filter by attributes
Visit::byDevice('Mobile')->count();
Visit::byBrowser('Chrome')->count();
Visit::byPlatform('macOS')->count();
Visit::byIp('192.168.1.1')->get();
Visit::byMethod('POST')->count();
```
**Chainable example:**
```php
Visit::today()
->byDevice('Mobile')
->byBrowser('Chrome')
->count();
// → Number of Chrome mobile visits today
```
---
#### Relations
```php
$visit = Visit::find(1);
$visit->visitor; // → BackendUser / FrontendUser (the visitor) or null
$visit->visitable; // → ContentArticle / etc (the visited page) or null
```
Both are `morphTo` — polymorphic. The actual model depends on the `*_type` / `*_id` columns.
---
## Traits
### `App\Models\Traits\HasVisits`
Use on models that **act as visitors** (users).
```php
use App\Models\Traits\HasVisits;
class BackendUser extends Authenticatable
{
use HasVisits;
}
```
**Available methods:**
| Method | Return | Description |
|--------|--------|-------------|
| `$user->visits()` | `MorphMany` | Query builder for user's visits |
| `$user->logVisit($visitable = null)` | `?Visit` | Log a visit for this user |
| `$user->totalVisits()` | `int` | Total visits by this user |
| `$user->uniqueIps()` | `int` | Unique IPs this user visited from |
| `$user->visitHistory($limit = 20)` | `Collection` | Last N visits |
**Examples:**
```php
// Log login (always guard with VisitorSetting)
if (VisitorSetting::first()?->is_active) {
$user->logVisit();
}
// Log visit to article
if (VisitorSetting::first()?->is_active) {
$user->logVisit($article);
}
// Get user's visit history
foreach ($user->visitHistory() as $visit) {
echo $visit->url . ' — ' . $visit->created_at;
}
```
---
### `App\Models\Traits\Visitable`
Use on models that **are visited** (content/articles/pages).
```php
use App\Models\Traits\Visitable;
class ContentArticle extends Model
{
use Visitable;
}
```
**Available methods:**
| Method | Return | Description |
|--------|--------|-------------|
| `$page->visitLogs()` | `MorphMany` | Query builder for visits on this content |
| `$page->totalViews()` | `int` | Total views |
| `$page->uniqueViewers()` | `int` | Unique authenticated visitors |
| `$page->uniqueViewerIps()` | `int` | Unique viewer IPs |
| `$page->dailyViews($days = 30)` | `Collection` | `['date' => 'total']` for charting |
**Examples:**
```php
$article->totalViews(); // → 240
$article->uniqueViewers(); // → 85
$article->uniqueViewerIps(); // → 120
// Chart data for last 7 days
$chart = $article->dailyViews(7);
// [
// '2026-05-11' => 12,
// '2026-05-12' => 8,
// ...
// ]
```
---
## Usage Examples
### Track all page visits (global)
In `routes/web.php` or a middleware:
```php
if (VisitorSetting::first()?->is_active) {
Visit::log();
}
```
### Track authenticated user login
```php
use App\Models\Backend\Visit;
if (VisitorSetting::first()?->is_active) {
Visit::log(visitor: Auth::guard('admin')->user());
}
```
### Track article page views
```php
// In ContentArticleController@show
if (VisitorSetting::first()?->is_active) {
Visit::log(visitable: $article);
}
// Or combine with authenticated user
if (VisitorSetting::first()?->is_active) {
Visit::log(visitor: Auth::user(), visitable: $article);
}
```
### Top 10 most viewed articles
```php
use App\Models\Backend\Visit;
use App\Models\ContentArticle;
Visit::selectRaw('visitable_id, COUNT(*) as views')
->where('visitable_type', ContentArticle::class)
->groupBy('visitable_id')
->orderByDesc('views')
->limit(10)
->get()
->map(fn($v) => [
'article' => ContentArticle::find($v->visitable_id),
'views' => $v->views,
]);
```
### Dashboard stats widget
```php
$stats = [
'total' => Visit::totalVisits(),
'today' => Visit::todayVisits(),
'yesterday' => Visit::yesterdayVisits(),
'unique_ips' => Visit::uniqueIps(),
'active_now' => Visit::online(180)->count(),
'this_month' => Visit::thisMonthVisits(),
'last_3mo' => Visit::last3MonthsVisits(),
'last_6mo' => Visit::last6MonthsVisits(),
'this_year' => Visit::thisYearVisits(),
];
```
### Browser / device breakdown
```php
$byBrowser = Visit::selectRaw('browser, COUNT(*) as total')
->groupBy('browser')
->orderByDesc('total')
->pluck('total', 'browser');
$byDevice = Visit::selectRaw('device, COUNT(*) as total')
->groupBy('device')
->orderByDesc('total')
->pluck('total', 'device');
```
### Clear all logs
```php
Visit::truncate();
// OR via admin panel → /admin/visitor → Clear All
```
---
## Admin Panel
URL: `/admin/visitor` (requires `visitor.view` permission)
**Features:**
- **Stats cards:** Total Visits, Unique IPs, Today Visits
- **Toggle Active/Inactive:** Disables all logging globally (does not delete data)
- **Clear All:** Truncates all visit logs (with confirmation prompt)
**Permissions:**
| Permission | Route | Action |
|------------|-------|--------|
| `visitor.view` | GET `/admin/visitor` | View stats page |
| `visitor.update` | POST `/admin/visitor/toggle` | Toggle active/inactive |
| `visitor.delete` | POST `/admin/visitor/clear` | Clear all logs |
---
## File Locations
| File | Role |
|------|------|
| `app/Models/Backend/Visit.php` | Main model with log(), stats, scopes, detection |
| `app/Models/Backend/VisitorSetting.php` | Toggle setting model |
| `app/Models/Traits/HasVisits.php` | Trait for user/visitor models |
| `app/Models/Traits/Visitable.php` | Trait for content/page models |
| `app/Http/Controllers/Backend/VisitorController.php` | Admin panel controller |
| `resources/views/backend/module/visitor/index.blade.php` | Admin panel view |
| `database/migrations/2026_05_18_190730_create_visits_table.php` | Visits table migration |
| `database/migrations/2026_05_18_194337_create_visitor_settings_table.php` | Settings table migration |
| `database/seeders/BackendMenuSeeder.php` | Menu + permission seeding |
| `routes/web.php` | Route definitions + active check in welcome route |