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

Bookmark Laravel Package

laravel-interaction/bookmark

Laravel Interaction Bookmark adds bookmarking to any Eloquent model via a simple trait. Users can bookmark posts, comments, or other records, with toggle, count, and query helpers. Polymorphic, configurable, and easy to drop into existing Laravel apps.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require laravel-interaction/interactions
php artisan vendor:publish --tag="interactions-config"
php artisan vendor:publish --tag="interactions-migrations"
php artisan migrate

If the tagged publishes don't resolve, fall back to --provider="LaravelInteraction\Interactions\InteractionsServiceProvider" and pick the config/migration entries interactively.

Wire up the two sides of the relationship

// The actor (usually User)
use LaravelInteraction\Interactions\Concerns\HasInteractions;

class User extends Authenticatable
{
    use HasInteractions;
}

// The target (Post, Comment, Video…)
use LaravelInteraction\Interactions\Concerns\CanBeInteracted;

class Post extends Model
{
    use CanBeInteracted;
}

First use case — likes on a Post

$user->like($post);            // create
$user->toggleLike($post);      // create or remove (what your endpoint should call)
$user->unlike($post);          // remove

$user->hasLiked($post);        // bool, for the button state
$post->likes()->count();       // read side
$post->isLikedBy($user);       // inverse convenience check

Where to look first

  1. config/interactions.php — the types map is the whole public surface. Every feature (like, dislike, love, bookmark, subscribe, follow) is just a type name; methods like like()/hasLiked()/likes() are generated from those names.
  2. The published migration — confirms table name, column names, morph key types, and the uniqueness index.
  3. The two traits in src/Concerns — the definitive list of available methods for this version (don't trust blog snippets for 1.x).
  4. README quick-start for the exact publish tags and supported Laravel/PHP matrix.

Implementation Patterns

One generic endpoint, many features. Because types are config-driven, avoid writing LikeController, BookmarkController, SubscribeController. Use a single controller action with a whitelisted type and toggleInteraction/attachInteraction/detachInteraction:

public function __invoke(Request $request, Post $post)
{
    $type = $request->enum('type', InteractionType::class); // or validated string

    $post->isInteractedBy($request->user(), $type)   // or hasInteraction($type)
        ? $request->user()->detachInteraction($post, $type)
        : $request->user()->attachInteraction($post, $type);

    return response()->json([
        'active' => $request->user()->hasInteraction($post, $type),
        'count'  => $post->interactions()->where('type', $type->value)->count(),
    ]);
}

Cast the type. Keep a App\Enums\InteractionType: string (or the package's enum if it ships one) and pass ->value into the DB but ->name to the UI. Raw strings scattered across Blade, controllers, and tests are the #1 source of "why is my count zero" bugs — a single typo in 'bookmarked' vs 'bookmark' silently creates a second type.

Read-side: counts without N+1. On index pages, eager-load per type:

$posts = Post::query()
    ->withCount(['likes', 'bookmarks'])
    ->paginate();

// then, for the current user's state, one query for the whole page:
$interacted = Interaction::query()
    ->where('user_id', $request->user()->id)
    ->whereIn('interactable_id', $posts->pluck('id'))
    ->get()
    ->groupBy('interactable_id');

Rendering $post->isLikedBy(auth()->user()) inside a loop is a query per row — the classic performance trap with this package.

Blade component for the button. Extract <x-interaction-button :model="$post" type="like" :active="$post->is_liked" :count="$post->likes_count" /> so the JS/optimistic-update logic lives in one place. Alpine + fetch to the single endpoint, swap active and count from the JSON response.

Multiple types on one model. The single interactions table is polymorphic on both sides, so a Post can carry likes and bookmarks and a User can like a Comment — no extra tables. Group UI by type and always scope reads by type explicitly (->where('type', 'like')) rather than relying on $post->interactions() which returns all types mixed.

Testing. Factories are trivial because the API is fluent; assert against the table, not the count:

$this->actingAs($user)->post("/posts/{$post->id}/like")->assertOk();

$this->assertDatabaseHas('interactions', [
    'user_id' => $user->id,
    'interactable_id' => $post->id,
    'interactable_type' => $post->getMorphClass(),
    'type' => 'like',
]);

Notifications / side effects. Keep them out of the package hooks and in a small InteractionService you own — you'll want "don't notify on unlike", "debounce spam", and "only notify on the first like", none of which belong in a controller.

Gotchas and Tips

  • Morph keys must match your models. If you use UUIDs/ULIDs, set the migration's key type (or the config option for UUIDs) before running the migration. Mixing a bigint interactable_id with UUID-keyed models produces silently empty relations, not errors.
  • Unique index is your friend and your enemy. The migration's unique constraint on (user, type, interactable) is what prevents double-likes, but attachInteraction on a duplicate will throw a QueryException rather than no-op. Use toggle semantics in the controller, or firstOrCreate-style checks, and wrap in a transaction if the UI can double-submit.
  • count() counts every type. $post->interactions()->count() is not the like count. Always ->where('type', 'like') or the generated likes() relation. This bites hardest right after you add a second type to a page that already "worked".
  • Relation names are derived from config. withCount('likes') only exists if like is in types. Adding a type later requires a config change and (in cached config environments) php artisan config:clear, otherwise the new relation method doesn't appear in production but does locally.
  • Case sensitivity. Store and compare lowercase type strings consistently; MySQL's default collation hides the bug locally and Postgres/utf8mb4_bin exposes it. Enums + a single ->value conversion avoids it entirely.
  • Deleting models leaves orphan rows. The package won't know about a User deleted outside Eloquent (raw delete, or a Post hard-deleted by a cleanup job). Add FK cascades in a follow-up migration or a scheduled prune on interactions where the morph target no longer exists.
  • hasLiked() is a query. Accept a loaded $user and pre-fetch state for lists; if you're rendering 50 rows, you want one query, not 50. Consider withExists for boolean columns in API resources.
  • Caching. Don't cache raw counts indefinitely — interactions are high-churn. If you must (leaderboards), cache for seconds and invalidate on write from your own service layer, not from the package.
  • Extension points, in order of usefulness: publish and edit config/interactions.php (add types, adjust table/column names, model bindings); publish the migration to add extra columns (e.g. created_at indexes, a metadata JSON) — do this in a new migration rather than editing the published one if you already deployed; override the model class via config if you need Interaction with casts or scopes; and finally, extend the traits in your own Concerns namespace if you need a domain wrapper.
  • Pin ^1.0. This is the first stable tag, so the public API (trait names, generated method names, table name) should now hold — but a 1.0 from a single-maintainer package still warrants a lockfile-pinned deploy and a quick composer update --dry-run review before jumping minors, especially around config key renames.
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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
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