mosamy / seenable
Track whether Eloquent records were seen and by which users.
Requires
- php: >=8.1
This package is auto-updated.
Last update: 2026-04-03 21:44:47 UTC
README
Track whether Eloquent records were seen and by which users.
Features
- Add a per-record boolean flag (
is_seen) via a global scope. - Store seen rows in
seenables(viewer + model). - Filter records by viewer (
seenBy,haventSeenBy,seenByMe,haventSeenByMe). - Mark records as seen/unseen using helper methods.
- Sort by seen/unseen priority (
orderBySeen,orderByUnseen).
Requirements
- PHP 8.1+
- Laravel 10+
Installation
composer require mosamy/seenable
Then run migrations:
php artisan migrate
Quick Start
Add the trait to any Eloquent model you want to track:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Mosamy\Seenable\Seenable;
class Message extends Model
{
use Seenable;
}
How It Works
- Each seen action creates one row in
seenablesthat links:seenable_type,seenable_id(the viewed model)seener_type,seener_id(the viewer model)
- A global scope appends
is_seento model queries.is_seen = trueif the record has at least one view by any user.
- On model delete, related view rows are deleted automatically.
API
is_seen
Check whether the record has been seen by anyone:
$message = Message::find(1);
if ($message?->is_seen) {
// Seen by at least one user
}
markAsSeen()
Mark the current authenticated user as having seen the record.
$message = Message::findOrFail(1);
$message->markAsSeen();
Notes:
- Requires an authenticated user (
auth()->user()). - Duplicate rows for the same user/record are prevented by package logic.
markAsUnseen()
Remove the seen row linked to this record.
$message = Message::findOrFail(1);
$message->markAsUnseen();
seen() relation
Get the primary seen row for the record.
$message = Message::with('seen.seener')->findOrFail(1);
$viewer = $message->seen?->seener;
views() relation
Get all views for a record.
$message = Message::with('views.seener')->findOrFail(1);
foreach ($message->views as $view) {
echo $view->seener?->id;
}
Performance tip: use eager loading (with('views.seener')) when listing many records.
seenBy(string $viewerClass, ?int $viewerId = null)
Return records seen by a specific model class, optionally a specific ID.
use App\Models\Admin;
$seenByAnyAdmin = Message::seenBy(Admin::class)->get();
$seenByAdminOne = Message::seenBy(Admin::class, 1)->get();
seenByMe()
Return records seen by the current authenticated user.
$items = Message::seenByMe()->get();
If no user is authenticated, this scope does not filter the query.
haventSeenBy(string $viewerClass, ?int $viewerId = null)
Return records not seen by a specific model class, optionally a specific ID.
use App\Models\Admin;
$notSeenByAnyAdmin = Message::haventSeenBy(Admin::class)->get();
$notSeenByAdminOne = Message::haventSeenBy(Admin::class, 1)->get();
haventSeenByMe()
Return records not seen by the current authenticated user.
$items = Message::haventSeenByMe()->get();
If no user is authenticated, this scope does not filter the query.
orderBySeen()
Sort with most-seen records first.
$items = Message::orderBySeen()->get();
orderByUnseen()
Sort with least-seen records first.
$items = Message::orderByUnseen()->get();
Caveats
is_seenreflects whether a record was seen by anyone, not by the current user.- Current implementation is centered on the
seen()relation for scopes and mark/unmark helpers, so behavior is based on the primary seen row. - If you need strict duplicate protection at DB level, add a composite unique index in your app migration strategy.
License
MIT