cyrildewit/eloquent-viewable
Track page views on Eloquent models without third-party analytics. Record and count total or unique views, filter by date periods, order models by views, apply cooldowns, and optionally ignore crawlers. Stores each view as a DB record for flexible querying.
Installation:
composer require cyrildewit/eloquent-viewable
php artisan vendor:publish --provider="CyrildeWit\EloquentViewable\EloquentViewableServiceProvider" --tag="migrations"
php artisan migrate
(Optional: Publish config with --tag="config")
Model Integration:
Add the Viewable interface and InteractsWithViews trait to your Eloquent model:
use CyrildeWit\EloquentViewable\Contracts\Viewable;
use CyrildeWit\EloquentViewable\InteractsWithViews;
class Post extends Model implements Viewable {
use InteractsWithViews;
}
First Use Case: Track views in a controller:
public function show(Post $post) {
views($post)->record(); // Auto-detects crawlers
return view('post.show', compact('post'));
}
View Tracking:
views($model)->record() (ignores crawlers/DNT headers).views($post)->cooldown(now()->addHours(2))->record();
views($post)->collection('admin_dashboard')->record();
Querying Views:
views($post)->count()views($post)->period(Period::pastDays(7))->count()
views($post)->unique()->count()Model Ordering:
// Global scope (descending)
Post::orderByViews()->get();
// Periodic scope
Post::orderByViews('asc', Period::pastMonths(1))->get();
Type-Level Analytics:
// Count views for all Posts
views(new Post())->count();
// Or via class name
views(Post::class)->count();
views($model)->record() in middleware to track API/SPA routes.ModelObservers for consistency:
public function retrieving(Model $model) {
if ($model instanceof Viewable) {
views($model)->record();
}
}
remember():
views($post)->period(Period::pastWeek())->remember(3600)->count();
Crawler Detection:
views($post)->ignoreCrawlers(false)->record() to bypass.config(['eloquent-viewable.ignore_crawlers' => false]).Database Bloat:
viewable_id, viewable_type, and visitor (if unique views matter).View::where('created_at', '<=', now()->subYear())->delete();
Cooldown Quirks:
cooldown(now()->addDays(7)) for persistent delays.Caching Caveats:
remember() only caches count() results. For orderByViews, cache counts in model columns (e.g., views_count) and update via scheduled jobs.\DB::enableQueryLog();
views($post)->period(Period::pastDay())->count();
\DB::getQueryLog();
Visitor class:
$this->app->bind('eloquent-viewable.visitor', function () {
return new CustomVisitor(request()->ip());
});
Custom View Model:
Extend the View model to add metadata (e.g., user_agent):
class CustomView extends \CyrildeWit\EloquentViewable\View {
protected $fillable = ['user_agent'];
}
Bind it in AppServiceProvider:
$this->app->bind('eloquent-viewable.view', CustomView::class);
Macros:
Add reusable methods to the Views facade:
\CyrildeWit\EloquentViewable\Facades\Views::macro('trending', function () {
return $this->period(Period::pastDays(7))->orderBy('created_at', 'desc')->limit(10);
});
Usage: views($post)->trending()->get().
Crawler Detection: Replace the default detector:
$this->app->bind('eloquent-viewable.crawler-detector', CustomCrawlerDetector::class);
View::insert() instead of individual record() calls.viewable_type + viewable_id + created_at if querying by time ranges frequently.
```markdown
### Pro Tips
- **Soft Deletes**: If your models use soft deletes, ensure `View` model also implements `SoftDeletes` to avoid orphaned records.
- **API Tokens**: For API-driven views, use `Visitor::fromToken($request->bearerToken())` to track authenticated users uniquely.
- **Testing**: Mock the `Visitor` class in tests to avoid real database writes:
```php
$this->app->instance('eloquent-viewable.visitor', MockVisitor::class);
How can I help you explore Laravel packages today?