Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Visitor Laravel Package

shetabit/visitor

Laravel package to extract and access visitor information like browser, IP, platform, and device. It also lets you recognize online users, check if a user is online, and log visits for your models using the Visitable trait.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and publish

composer require shetabit/visitor
php artisan vendor:publish --provider="Shetabit\Visitor\Provider\VisitorServiceProvider"
php artisan migrate

Laravel 5.5+ uses auto-discovery, so you normally don't need to touch config/app.php. If your app has discovery disabled, register Shetabit\Visitor\Provider\VisitorServiceProvider::class and the Visitor alias from Shetabit\Visitor\Facade\Visitor::class.

2. Read the config before writing any code

config/visitor.php is the first place to look. It controls the agent/parser driver, the route/path exclusion list used by automatic logging, the cache store and TTL used for "online" detection, and the optional GeoIP enrichment added in v4.5. Most "why is this logging / why isn't this online" questions are answered there.

3. Smoke-test the helper

// routes/web.php
Route::get('/whoami', function () {
    return [
        'ip'       => visitor()->ip(),
        'browser'  => visitor()->browser(),
        'device'   => visitor()->device(),
        'platform' => visitor()->platform(),
    ];
});

If that returns sane values, the package is wired correctly and you can start layering logging on top.

First real use case: put Shetabit\Visitor\Traits\Visitable on the model you care about (Post, Product, Listing) and log a view in the show action:

public function show(Post $post)
{
    $post->createVisitLog();          // or visitor()->visit($post)

    return view('posts.show', compact('post'));
}

Then render {{ $post->visitLogs()->count() }} as your first "views" counter. That's the whole loop — everything else is refinement.

Implementation Patterns

Metadata without persistence. $request->visitor()->browser() and friends are pure reads — no DB writes. Use them for lightweight personalization (hint banners, locale defaults from languages()), for tagging support tickets with browser/device, or for enriching exception reports. Cache the result per request if you call several accessors in a view composer.

Model visit tracking — two equivalent styles. Pick one and stay consistent:

// Explicit, controller-driven
visitor()->visit($post);

// Model-driven, easier to reuse in observers or policies
$post->createVisitLog();

Both write a polymorphic log row. $post->visitLogs() is the query builder relation (for counting and filtering); $post->visits is the loaded relation.

Attributing visits to a user. When someone is authenticated, tell the package who the visitor was so you can distinguish guest traffic:

visitor()->setVisitor($user)->visit($post);
$post->createVisitLog($user);

Automatic logging via middleware. For "log everything on these routes", append Shetabit\Visitor\Middlewares\LogVisits to a route group. It logs only models that (a) are resolved by route–model binding and (b) use the Visitable trait — it is not a generic request logger. Combine it with wildcard exclusions (supported since v4.1.0) in config:

// config/visitor.php
'except' => [
    'admin/*',
    'api/*',
    'login',
    'register',
],

Put the middleware after StartSession and your auth middleware in the group so setVisitor resolves the current user.

Online users. Add Shetabit\Visitor\Traits\Visitor to your User model, then:

visitor()->onlineVisitors(User::class);  // Collection<User>
User::online()->get();                   // same, as a scope
visitor()->isOnline($user);              // bool
$user->isOnline();                       // bool

This is cache-backed, not a websocket — treat "online" as "active within TTL" and set the TTL to match your product language (2 minutes for a live dashboard, 15 for an admin presence widget).

Counting and reporting. The three numbers you'll actually ship:

$post->visitLogs()->count();                              // all views
$post->visitLogs()->distinct('ip')->count('ip');          // unique guests by IP
$post->visitLogs()->visitor()->count();                   // unique known users (scope)

For anything richer, query the log table directly and group:

DB::table('visit_logs')
    ->select('browser', DB::raw('count(*) as hits'))
    ->where('visited_type', $post->getMorphClass())
    ->where('visited_id', $post->getKey())
    ->where('created_at', '>=', now()->subDays(30))
    ->groupBy('browser')
    ->orderByDesc('hits')
    ->get();

Use getMorphClass() rather than a hardcoded class string so morph maps don't bite you.

GeoIP. v4.5 added optional enrichment. Enable it in config and pick a local database path; keep it optional in local/CI environments so tests don't depend on a geo database being present. If you need city-level fidelity beyond what it offers, resolve country yourself in a model accessor and skip the feature.

Write-behind for hot pages. Don't call visit() inline on a page that gets hammered. Either guard with a per-session cache key:

$key = "visited:{$post->id}:" . visitor()->ip();
if (Cache::add($key, true, now()->addHour())) {
    $post->createVisitLog();
}

…or dispatch your own job from terminate() in a middleware so the insert happens after the response is flushed.

Testing. Use actingAs($user) plus a get() request so a real request object exists, then assert $post->visitLogs()->count(). Freeze time and flush the online-users cache between tests — presence assertions are the flakiest part of a suite built on this package.

Gotchas and Tips

  • visitor() needs a request. In console commands, queued jobs, and artisan tinker, there is no request bound and accessors can return null (v4.4.2 moved to the nullsafe operator, so you get null rather than a fatal — but still null). Guard with app()->runningInConsole() or pass IP/useragent explicitly from the caller.
  • Publish migrations explicitly. v4.5.3 fixed a provider bug where the migration wasn't published and GeoIP stubs were empty. If you installed on an older 4.5.x, re-publish and diff before running migrate — don't assume vendor:publish created visit_logs.
  • Pin your parser dependency. v4.5.6 fixed an Agent::__call signature break with mobiledetectlib 4.10, and v4.5.4 fixed a PHP 8.4 nullable-type deprecation. On PHP 8.4 or with a floating mobiledetectlib constraint, require ^4.5.6 or you'll get hard failures rather than warnings.
  • Real IP behind a proxy. ip() reads the request's client IP. Configure TrustProxies/APP_TRUSTED_PROXIES correctly first, or every unique-visitor count collapses to your load balancer and every GeoIP lookup resolves to a datacenter.
  • request() stores the payload. The package has excluded password fields since v2.2.0, but the full input array still lands in the log row. If you're under GDPR/CCPA scrutiny, either drop that column from the migration or add a model mutator/observer that strips it. Don't rely on the exclusion list as your only safeguard.
  • Deduplication is your job. distinct('ip') is exact but slow without an index. Add composite indexes on (visited_type, visited_id, created_at) and (visited_type, visited_id, ip). Note that IP-based uniqueness breaks for shared NAT/corporate networks — pair it with the visitor() scope when you have authenticated users.
  • Table growth is unbounded. Nothing prunes visit_logs. Schedule a command that deletes rows older than your retention window (or archives them) before the table reaches millions of rows on a page-view-heavy site.
  • Bots count as visitors. Presence/logging will include crawlers unless you filter. Check visitor()->device()/useragent() for known bot signatures in your own middleware, or exclude the routes that bots hammer most.
  • Extension points: the driver/resolver is resolvable from the container, so you can bind a custom implementation in a service provider to change how browsers are parsed or to swap in a different GeoIP service. For logging behaviour, subclass LogVisits rather than editing vendor/. If you need a different log schema, publish the migration and edit it before the first migrate — retrofitting columns later means a manual migration plus backfill.
  • Keep the facade for static analysis. If your IDE can't resolve visitor(), add a _ide_helper stub or use Visitor::ip() from the facade — same instance, better autocomplete.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator