Bei Pixelklicker zählen wir Seitenaufrufe bewusst anonym und fassen sie pro Tag zusammen (Tagesrollup). So sehen wir Trends, ohne personenbezogene Daten zu sammeln – und das Dashboard bleibt schlank. In diesem Tutorial baust du genau das nach: eine Visit-Erfassung mit Tages-Aggregation und ein StatsOverview-Widget in Filament.
Hinweis: Dieses Tutorial zeigt einen praxisnahen, an der Pixelklicker-Architektur (Laravel, Filament,
Visit-Modell, StatsOverview-Widget) orientierten Aufbau. Wo die interne Implementierung im Detail abweichen kann, weise ich darauf hin.
1. Ziel & Lernergebnis
Am Ende hast du:
- eine Tabelle, die Aufrufe pro Tag aggregiert speichert (statt jede einzelne Anfrage zu loggen),
- ein Middleware-/Service-gestütztes Tracking, das anonym ist (keine IP-Speicherung, keine Cookies),
- ein Filament-
StatsOverview-Widget, das die Kennzahlen (z. B. heutige Aufrufe, Summe, Trend) anzeigt.
Du kannst danach das Muster auf andere zählbare Ereignisse (z. B. Demo-Klicks) übertragen.
2. Voraussetzungen
- Ein lauffähiges Laravel-Projekt mit Filament-Admin-Panel unter
/admin. - Grundkenntnisse in Migrations, Eloquent-Modellen und Artisan.
- Filament im Projekt installiert und Zugriff auf das Dashboard.
3. Schritte
Schritt 1 – Datenmodell für den Tagesrollup anlegen
Wir speichern pro Tag und pro Ziel (z. B. eine Seite oder ein Artikel) eine Zeile mit einem Zähler. Das hält die Tabelle klein und Auswertungen schnell.
php artisan make:model Visit -m
In der erzeugten Migration definieren wir die Rollup-Struktur:
// database/migrations/xxxx_xx_xx_create_visits_table.php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('visits', function (Blueprint $table) { $table->id(); $table->date('day'); // der Tag (Rollup-Schlüssel) $table->string('trackable_type')->nullable(); $table->unsignedBigInteger('trackable_id')->nullable(); $table->unsignedBigInteger('count')->default(0); $table->timestamps(); // pro Tag + Ziel nur eine Zeile $table->unique(['day', 'trackable_type', 'trackable_id']); }); } public function down(): void { Schema::dropIfExists('visits'); } };
Warum so? Das unique-Constraint garantiert genau eine Zeile pro Tag und Ziel. Den Zähler erhöhen wir atomar – das ist der Kern des „Tagesrollups". Über trackable_type/trackable_id (eine polymorphe Verknüpfung) lassen sich Seiten, Artikel oder Projekte tracken. Brauchst du nur globale Aufrufe, kannst du die trackable_*-Spalten weglassen.
Schritt 2 – Modell vorbereiten
// app/Models/Visit.php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphTo; class Visit extends Model { protected $fillable = ['day', 'trackable_type', 'trackable_id', 'count']; protected $casts = [ 'day' => 'date', 'count' => 'integer', ]; public function trackable(): MorphTo { return $this->morphTo(); } }
Warum? Das date-Cast sorgt für saubere Tagesvergleiche, das integer-Cast für korrekte Summen.
Schritt 3 – Tracking-Service mit atomarem Increment
Wir kapseln das Hochzählen in einer kleinen, wiederverwendbaren Methode. So bleibt die Logik an einer Stelle.
// app/Services/VisitTracker.php namespace App\Services; use App\Models\Visit; use Illuminate\Database\Eloquent\Model; class VisitTracker { public function record(?Model $trackable = null): void { $visit = Visit::firstOrCreate( [ 'day' => now()->toDateString(), 'trackable_type' => $trackable?->getMorphClass(), 'trackable_id' => $trackable?->getKey(), ], ['count' => 0], ); // atomar erhöhen, vermeidet Race Conditions $visit->increment('count'); } }
Warum increment()? Es führt ein UPDATE ... SET count = count + 1 aus. Damit gehen bei gleichzeitigen Anfragen keine Zählungen verloren. firstOrCreate legt die Tageszeile bei Bedarf an.
Schritt 4 – Aufrufe anonym erfassen (Middleware)
Damit nichts Personenbezogenes gespeichert wird, übergeben wir keine IP, keinen User-Agent und setzen keinen Cookie. Wir erhöhen schlicht den Tageszähler.
php artisan make:middleware TrackPageView
// app/Http/Middleware/TrackPageView.php namespace App\Http\Middleware; use App\Services\VisitTracker; use Closure; use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; class TrackPageView { public function __construct(private VisitTracker $tracker) {} public function handle(Request $request, Closure $next): Response { $response = $next($request); // nur erfolgreiche GET-Seitenaufrufe zählen if ($request->isMethod('GET') && $response->getStatusCode() < 400) { $this->tracker->record(); } return $response; } }
Registriere die Middleware für deine Frontend-Routen (z. B. in bootstrap/app.php über ->withMiddleware() oder einer Route-Group). Bei Pixelklicker wird das anonyme Tracking nur fürs öffentliche Frontend genutzt – das Admin-Panel solltest du ausnehmen.
Warum nach $next($request)? So zählen wir erst, wenn die Antwort steht, und können Fehlerseiten (Status ≥ 400) ausschließen.
Schritt 5 – Gezieltes Tracking auf Detailseiten (optional)
Wenn du z. B. Artikelaufrufe getrennt zählen willst, rufst du den Service im Controller mit dem konkreten Modell auf:
// im Artikel-Controller public function show(Article $article, VisitTracker $tracker) { $tracker->record($article); return view('articles.show', compact('article')); }
Warum? So landet die Zählung in der polymorphen Spalte und du kannst später „Top-Artikel" auswerten.
Schritt 6 – Das Filament-StatsOverview-Widget bauen
Jetzt visualisieren wir die Zahlen. Erzeuge ein Widget:
php artisan make:filament-widget VisitsOverview --stats-overview
// app/Filament/Widgets/VisitsOverview.php namespace App\Filament\Widgets; use App\Models\Visit; use Filament\Widgets\StatsOverviewWidget as BaseWidget; use Filament\Widgets\StatsOverviewWidget\Stat; class VisitsOverview extends BaseWidget { protected function getStats(): array { $today = Visit::whereDate('day', now()->toDateString())->sum('count'); $last7 = Visit::whereDate('day', '>=', now()->subDays(6)->toDateString())->sum('count'); $total = Visit::sum('count'); return [ Stat::make('Aufrufe heute', number_format($today, 0, ',', '.')) ->description('Anonyme Seitenaufrufe') ->color('success'), Stat::make('Letzte 7 Tage', number_format($last7, 0, ',', '.')) ->description('Summe der Tagesrollups'), Stat::make('Gesamt', number_format($total, 0, ',', '.')) ->description('Alle erfassten Aufrufe'), ]; } }
Warum sum('count')? Da wir pro Tag aggregieren, summieren wir nur die Tageszeilen – das ist deutlich günstiger, als einzelne Events zu zählen. Das Widget wird vom Dashboard automatisch erkannt; je nach Konfiguration registrierst du es in deiner Dashboard-Page oder es wird über Auto-Discovery eingebunden.
Schritt 7 – Optional: Mini-Trend mit Chart
Stat unterstützt einen kleinen Sparkline-Chart über ->chart([...]). Du kannst die Tageswerte der letzten Woche übergeben:
$chart = Visit::whereDate('day', '>=', now()->subDays(6)->toDateString()) ->selectRaw('day, SUM(count) as total') ->groupBy('day') ->orderBy('day') ->pluck('total') ->all(); // ... Stat::make('Aufrufe heute', $today)->chart($chart);
Warum? Der kleine Verlauf gibt sofort ein Gefühl für den Trend, ohne ein vollwertiges Chart-Widget.
4. Ergebnis prüfen
-
Migration ausführen:
bash php artisan migrate
-
Eine Frontend-Seite mehrfach im Browser aufrufen.
-
Über Tinker prüfen, ob genau eine Tageszeile mit hochgezähltem
countexistiert:bash php artisan tinker >>> \App\Models\Visit::whereDate('day', now())->get(['day','count']);
-
Im Filament-Dashboard unter
/adminsollte dasVisitsOverview-Widget die heutigen Aufrufe und die Summen anzeigen.
Wenn die Zahl pro Reload um 1 steigt und es keine zweite Zeile für denselben Tag gibt, funktioniert der Tagesrollup.
5. Stolpersteine / Troubleshooting
- Mehrere Zeilen pro Tag: Fehlt das
unique-Constraint oder weicht der Tageswert (Zeitzone!) ab. Stelle sicher, dassnow()->toDateString()konsistent dieselbe Zeitzone nutzt (config/app.php→timezone). - Race Conditions / verlorene Zählungen: Nicht manuell
count = count + 1in PHP rechnen und speichern – immerincrement()verwenden, das atomar im SQL erfolgt. - Admin-Aufrufe werden mitgezählt: Middleware nur an Frontend-Routen hängen, das Panel ausnehmen.
- Bots/Assets blähen die Zahlen auf: Nur
GETmit Status < 400 zählen; bei Bedarf zusätzlich auf HTML-Antworten einschränken oder Asset-Routen ausschließen. - Widget erscheint nicht: Prüfe, ob das Widget im Dashboard registriert bzw. per Discovery erkannt wird, und ob dein Benutzer Zugriff auf das Panel hat.
- Datenschutz: Speichere weder IP noch User-Agent. Sobald du diese erfasst, ist es nicht mehr „anonym".
6. Fazit & nächste Schritte
Du hast ein datensparsames, anonymes Aufruf-Tracking mit Tagesrollup gebaut und die Kennzahlen direkt im Filament-Dashboard sichtbar gemacht. Das Muster ist robust (atomares Increment), günstig (Aggregation statt Event-Flut) und DSGVO-freundlich.
Sinnvolle nächste Schritte:
- Aufrufe über Zeit als Chart-Widget und Top-Projekte/Artikel auswerten.
- Demo-Klick-Statistik nach demselben Prinzip erfassen.
So wächst aus dem schlanken Zähler nach und nach ein vollwertiges Analytics-Dashboard.