Teil 3 – Livewire-Integration: Tracking im Frontend auslösen
Einordnung
In Teil 1 haben wir die visits-Tabelle mit polymorpher Beziehung und Tages-Unique-Index angelegt. In Teil 2 haben wir die RecordVisitAction implementiert, die Aufrufe per upsert/increment als Tages-Rollup speichert – ohne personenbezogene Daten. Jetzt verbinden wir diese Logik mit dem öffentlichen Frontend: Konkret rufen wir die Action in den Livewire-Komponenten für die Artikel-Detailseite und die Projekt-Detailseite auf, sodass jeder Seitenaufruf zuverlässig und performant gezählt wird.
Vorüberlegung: Welcher Lifecycle-Hook ist der richtige?
Livewire bietet mehrere Hooks an, in denen man Code ausführen kann. Für das Tracking kommen vor allem zwei in Frage:
| Hook | Wird ausgeführt … | Eignet sich für Tracking? |
|---|---|---|
mount() |
Einmal beim ersten Rendern der Komponente auf dem Server | ✅ Ja – genau einmal pro Seitenaufruf |
booted() |
Bei jedem Server-Roundtrip (auch bei Updates, Events, …) | ❌ Nein – würde bei jeder Interaktion erneut zählen |
mount() ist die richtige Wahl. Er wird genau einmal ausgeführt, wenn der Nutzer die Seite aufruft – und nicht erneut, wenn Livewire später einen Update-Request schickt (z. B. für den Scrollspy der ToC-Sidebar).
Was ist mit wire:navigate?
Das Pixelklicker-Frontend nutzt wire:navigate für SPA-ähnliche Seitenübergänge. Dabei wird die neue Seite per AJAX geladen und die Livewire-Komponente neu gemountet – mount() wird also erneut aufgerufen. Das klingt zunächst nach einer Doppelzählungsgefahr, ist es aber strukturell nicht: Wenn ein Nutzer von Artikel A zu Artikel B navigiert, ist das tatsächlich ein neuer Aufruf von Artikel B, der gezählt werden soll.
Die eigentliche Absicherung gegen Doppelzählungen liegt in der Rollup-Logik aus Teil 2: Der Tages-Unique-Index auf (trackable_type, trackable_id, date) stellt sicher, dass mehrfaches Aufrufen derselben Seite am selben Tag nur einen Datensatz erzeugt und lediglich den Zähler erhöht – kein Datenmüll, keine aufgeblähten Zahlen.
Schritt 1: Artikel-Detailseite tracken
Die Livewire-Komponente für die Artikel-Detailseite befindet sich unter app/Livewire/ArticleShow.php. Wir injizieren die RecordVisitAction direkt in mount().
// app/Livewire/ArticleShow.php <?php namespace App\Livewire; use App\Actions\RecordVisitAction; use App\Models\Article; use Livewire\Component; class ArticleShow extends Component { public Article $article; public function mount(Article $article, RecordVisitAction $recordVisit): void { $this->article = $article; $recordVisit->execute($article); } public function render() { return view('livewire.article-show') ->layout('layouts.app'); } }
Was passiert hier?
- Laravel löst
RecordVisitActionautomatisch per Dependency Injection auf – keinnew, keinapp(). $recordVisit->execute($article)ruft die in Teil 2 implementierte Logik auf:upsertmitincrementauf den Tages-Rollup-Datensatz für diesen Artikel.- Der Aufruf ist synchron, aber extrem schnell (ein einziges
INSERT … ON DUPLICATE KEY UPDATE), sodass er die Render-Zeit nicht spürbar beeinflusst.
Schritt 2: Projekt-Detailseite tracken
Für die Projekt-Detailseite gehen wir identisch vor. Die Komponente liegt unter app/Livewire/ProjectShow.php.
// app/Livewire/ProjectShow.php <?php namespace App\Livewire; use App\Actions\RecordVisitAction; use App\Models\Project; use Livewire\Component; class ProjectShow extends Component { public Project $project; public function mount(Project $recordVisit, RecordVisitAction $recordVisit): void { // Hinweis: Parameternamen müssen eindeutig sein – siehe Stolpersteine unten } // … }
Korrigierte, vollständige Version:
// app/Livewire/ProjectShow.php <?php namespace App\Livewire; use App\Actions\RecordVisitAction; use App\Models\Project; use Livewire\Component; class ProjectShow extends Component { public Project $project; public function mount(Project $project, RecordVisitAction $recordVisit): void { $this->project = $project; $recordVisit->execute($project); } public function render() { return view('livewire.project-show') ->layout('layouts.app'); } }
Warum funktioniert das polymorph? Die RecordVisitAction aus Teil 2 erwartet ein Model-Objekt und liest $trackable->getMorphClass() sowie $trackable->getKey() aus. Ob wir ihr ein Article- oder ein Project-Objekt übergeben, ist ihr gleichgültig – solange das Model die Morph-Map korrekt registriert hat (wie in Teil 1 eingerichtet).
Schritt 3: Morph-Map verifizieren
Damit die polymorphe Beziehung stabil und typsicher funktioniert, sollte die Morph-Map in AppServiceProvider oder einem dedizierten MorphMapServiceProvider registriert sein. Prüfe, dass Article und Project darin enthalten sind:
// app/Providers/AppServiceProvider.php use Illuminate\Database\Eloquent\Relations\Relation; public function boot(): void { Relation::enforceMorphMap([ 'article' => \App\Models\Article::class, 'project' => \App\Models\Project::class, // … weitere Modelle ]); }
Warum enforceMorphMap? Die enforce-Variante wirft eine Exception, wenn ein Model ohne Eintrag in der Map gespeichert werden soll. Das schützt vor versehentlichem Speichern von voll qualifizierten Klassennamen (App\Models\Article) in der Datenbank – was Refactorings später erschwert.
Schritt 4: Ergebnis prüfen
4a – Browser-Test
- Starte den lokalen Entwicklungsserver (
php artisan serve) und öffne eine Artikel-Detailseite im Browser. - Lade die Seite einmal neu.
- Navigiere über
wire:navigatezu einer anderen Artikelseite. - Rufe eine Projekt-Detailseite auf.
4b – Datenbankprüfung per Tinker
php artisan tinker
// In Tinker: use App\Models\Visit; // Alle heutigen Einträge anzeigen Visit::whereDate('date', today())->get(['trackable_type', 'trackable_id', 'count']);
Erwartete Ausgabe (Beispiel):
Illuminate\Database\Eloquent\Collection {
all: [
App\Models\Visit {
trackable_type: "article",
trackable_id: 3,
count: 2, // zweimal aufgerufen
},
App\Models\Visit {
trackable_type: "article",
trackable_id: 7,
count: 1,
},
App\Models\Visit {
trackable_type: "project",
trackable_id: 1,
count: 1,
},
],
}
4c – Doppelzählung absichtlich testen
Rufe dieselbe Artikel-Detailseite fünfmal hintereinander auf. Der count-Wert für diesen Artikel sollte um genau 5 steigen – aber es darf nur ein Datensatz für heute in der Tabelle existieren (kein zweiter Zeile für denselben Artikel am selben Tag).
// In Tinker – nach fünf Aufrufen prüfen: Visit::whereDate('date', today()) ->where('trackable_type', 'article') ->where('trackable_id', 3) ->value('count'); // Erwartetes Ergebnis: 7 (2 vorherige + 5 neue) Visit::whereDate('date', today()) ->where('trackable_type', 'article') ->where('trackable_id', 3) ->count(); // Erwartetes Ergebnis: 1 (nur ein Datensatz!)
Stolpersteine
Parameternamen-Konflikt in mount()
Benenne die Parameter immer unterschiedlich. Ein häufiger Tippfehler:
// ❌ Falsch – beide Parameter heißen $project public function mount(Project $project, RecordVisitAction $project): void
// ✅ Richtig public function mount(Project $project, RecordVisitAction $recordVisit): void
Route-Model-Binding muss aktiv sein
Die Livewire-Komponente erwartet, dass das Model bereits aufgelöst wurde. Stelle sicher, dass die Route Route-Model-Binding nutzt:
// routes/web.php Route::get('/devlog/{article:slug}', ArticleShow::class)->name('articles.show'); Route::get('/projekte/{project:slug}', ProjectShow::class)->name('projects.show');
Ohne das Binding würde Livewire versuchen, ein Model anhand der rohen URL-Parameter zu erzeugen – was in einem Fehler endet.
Tracking in Queue auslagern – wann lohnt es sich?
Für eine einfache upsert-Operation ist ein synchroner Aufruf in mount() absolut ausreichend. Falls du später zusätzliche Logik hinzufügst (z. B. Geolocation, User-Agent-Auswertung), kannst du die Action in einen Job auslagern:
// Optionale spätere Erweiterung – jetzt noch nicht nötig dispatch(new RecordVisitJob($article));
Im aktuellen Stand des Projekts bleibt der synchrone Aufruf die sauberere und einfachere Lösung.
Tracking im Admin-Panel unterdrücken
Falls du nicht möchtest, dass Admin-Besuche im Tracking auftauchen, kannst du in der Action einen einfachen Guard einbauen:
// app/Actions/RecordVisitAction.php – optionale Ergänzung public function execute(Model $trackable): void { if (auth()->check() && auth()->user()->hasRole('admin')) { return; } // … bestehende Upsert-Logik }
Zusammenfassung
Mit diesen Änderungen zählt das System jeden Aufruf einer Artikel- oder Projekt-Detailseite zuverlässig:
mount()wird genau einmal pro Seitenaufruf ausgeführt – auch beiwire:navigate.- Die polymorphe
RecordVisitActionaus Teil 2 deckt beide Typen ohne Codeduplizierung ab. - Der Tages-Unique-Index aus Teil 1 schließt Doppelzeilen strukturell aus; Mehrfachaufrufe erhöhen nur den
count. - Die Prüfung per Tinker bestätigt, dass Datensätze korrekt angelegt und inkrementiert werden.
Im letzten Teil der Serie visualisieren wir diese gesammelten Daten im Filament-Admin-Dashboard – als StatsOverview- oder Chart-Widget, das die aggregierten Tagesaufrufe übersichtlich darstellt.