cyrildewit/eloquent-viewable
Track page views on Eloquent models without external analytics. Record views (with optional cooldown), count totals or unique views, filter by period, order models by popularity, and ignore bots/crawlers. Stores each view as a DB record for flexible reporting.
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(); // Log view
return view('post.show', compact('post'));
}
View Tracking:
views($model)->record();views($model)->cooldown(30)->record(); (30 minutes)views($model)->collection('premium')->record();Querying Views:
views($post)->count();views($post)->period(Period::pastDays(7))->count();views($post)->unique()->count();Model Ordering:
// Order posts by views (descending)
Post::orderByViews()->get();
// Order by unique views (ascending)
Post::orderByUniqueViews('asc')->get();
// Periodic ordering
Post::orderByViews('asc', Period::pastMonths(1))->get();
Type-Level Analytics:
// Total views for all Posts
views(new Post())->count();
// Or via class string
views(Post::class)->count();
public function handle($request, Closure $next) {
views($request->post)->record();
return $next($request);
}
Route::get('/posts/{post}/views', function (Post $post) {
return response()->json(['count' => views($post)->count()]);
});
// Cache for 1 hour
views($post)->remember(3600)->count();
Crawler Filtering:
views($model)->ignoreCrawlers(false)->record(); to bypass.Database Bloat:
viewable_id, viewable_type, and optionally visitor.Session Cooldown:
SESSION_DRIVER is configured (e.g., file, database).Unique Views Accuracy:
DB::enableQueryLog();
views($post)->count();
dd(DB::getQueryLog());
visitor column values in the views table match expected identifiers.Custom Visitor Data:
Extend the Visitor model to store additional metadata:
class CustomVisitor extends \CyrildeWit\EloquentViewable\Visitor {
protected $casts = ['user_agent' => 'string'];
}
Bind it in config/eloquent-viewable.php:
'visitor_model' => \App\Models\CustomVisitor::class,
Override Core Classes:
Replace the default Views facade with a custom implementation:
// config/app.php
'aliases' => [
'views' => App\Services\CustomViews::class,
];
Macros:
Add custom methods to the Views class:
use CyrildeWit\EloquentViewable\Views;
Views::macro('recent', function () {
return $this->period(Period::pastDays(1));
});
// Usage: views($post)->recent()->count();
Crawler Detection: Replace the default crawler detector:
'crawler_detector' => \App\Services\CustomCrawlerDetector::class,
config/eloquent-viewable.php:
'default_collection' => 'default',
'ignore_dnt_header' => false,
protected $removeViewsOnDelete = true;
update():
Post::query()->update([
'views_count' => views($post)->count(),
]);
// app/Console/Commands/UpdateViewCounts.php
public function handle() {
Post::chunk(100, function ($posts) {
foreach ($posts as $post) {
$post->update(['views_count' => views($post)->count()]);
}
});
}
Schema::table('views', function (Blueprint $table) {
$table->index(['viewable_type', 'viewable_id', 'visitor']);
});
How can I help you explore Laravel packages today?