mosamy/seenable

Track whether Eloquent records were seen and by which users.

Maintainers

Package info

bitbucket.org/mohamedsamy_10/seenable

pkg:composer/mosamy/seenable

Statistics

Installs: 14

Dependents: 0

Suggesters: 0

1.0.3 2026-04-03 21:44 UTC

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 seenables that links:
    • seenable_type, seenable_id (the viewed model)
    • seener_type, seener_id (the viewer model)
  • A global scope appends is_seen to model queries.
    • is_seen = true if 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_seen reflects 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