awssat/laravel-visits
Laravel Visits lets you track and count visits to Eloquent models, pages, or any item with a simple, cache-friendly API. It supports unique visits, IP and user-agent filtering, custom events, and fast stats for dashboards and analytics.
Install and publish the config plus the migrations (needed for the Eloquent driver):
composer require awssat/laravel-visits
php artisan vendor:publish --provider="Awssat\Visits\VisitsServiceProvider"
php artisan migrate
Open config/visits.php first — it is the only thing you normally need to touch. The two decisions that matter are:
driver — eloquent (durable, SQL-backed, one row per visit) or redis (fast counters, ideal for high write volume). Redis requires a working redis connection in config/database.php plus predis/phpredis.ip_protection / ip_protection_time — whether the package should suppress repeat visits from the same IP within a window.Attach the trait to any model you want to count:
use Awssat\Visits\Traits\Visitable;
class Post extends Model
{
use Visitable;
}
First use case — a page-view counter in a controller:
public function show(Post $post)
{
$post->visits()->increment();
return view('posts.show', [
'post' => $post,
'views' => $post->visits()->count(),
]);
}
If you don't want the trait, instantiate directly: (new \Awssat\Visits\Visits($post))->increment().
Separate counters with tags. Tags let one model carry multiple independent metrics without extra columns:
$post->visits()->increment(); // views
$post->visits()->tag('like')->increment(); // likes
$post->visits()->tag('share')->increment(); // shares
$post->visits()->tag('like')->count();
Keep tag strings in constants or an enum — a typo silently creates a new counter.
Time-windowed / trending queries. Period counts are built in:
$post->visits()->visits('day');
$post->visits()->visits('week');
$post->visits()->visits('month');
$post->visits()->visits('year');
Use these for "trending this week" widgets rather than querying the raw visits table yourself.
Leaderboards. Use the package's top-query rather than orderBy on a denormalized column:
$top = \Awssat\Visits\Visits::make(Post::class)->top(10);
Explicit IP identity. When you want control beyond the global ip_protection setting (e.g. only dedupe logged-in users), chain the IP yourself:
$post->visits()->IP(request()->ip()) ?? $post->visits()->increment();
Check isVisited() (or equivalent) when you need to display "already viewed" without incrementing.
Weighted visits. increment($n) lets you record engagement rather than raw loads — useful for read-time or scroll-depth scores:
$post->visits()->increment($minutesRead);
Decouple writes from the request. For high-traffic endpoints, push the increment onto a queue so a slow Redis/SQL write never blocks the response:
dispatch(new RecordVisit($post))->onQueue('metrics');
Wrap tracking in middleware when the rule is uniform ("every GET on /posts/* counts"). Keep the controller clean and skip it for bots/prefetch via a user-agent check before dispatching.
Prefer Redis in production, Eloquent for small apps / local. Eloquent gives you auditability and easy SQL reporting; Redis gives you INCR speed but needs a persistence story.
visits table grows quickly. Plan a prune job or move to Redis before the table becomes a reporting burden.increment() from 127.0.0.1 silently does nothing. Disable ip_protection in local/testing, or vary the IP in tests.morphMap). Renaming a model or changing its morph alias orphans existing counts — migrate the stored identifiers in the same deploy.cache enabled, count() can lag by the configured timeout. Disable the cache (or flush it) after bulk imports, resets, or admin-side corrections.eloquent → redis (or back) starts from zero; there is no automatic import. Decide the driver before launch, or write your own backfill.deleting hook or a scheduled cleanup for orphaned rows/keys.$post->visits()->tag('like') and $post->visits()->count() are separate instances — don't assume a tag set on one call carries to the next.connection/prefix in config/visits.php so a FLUSHDB or cache clear elsewhere doesn't wipe your counters, and keep the prefix identical across every app server.RefreshDatabase clears SQL but not Redis. Point tests at a dedicated Redis database and flush it in setUp, or you'll get order-dependent failures.throttle middleware, otherwise a refresh loop inflates counts (and IP protection only helps within its time window).How can I help you explore Laravel packages today?