laravel-interaction/interactions
Laravel Interaction lets Eloquent models interact polymorphically—likes, favorites, votes, follows, bookmarks, subscriptions, and more. Attach reusable interaction types to any model, query who interacted and what they interacted with, and build social features with clean, extendable APIs.
Install and publish
composer require laravel-interaction/interactions
php artisan vendor:publish --provider="LaravelInteraction\Interaction\InteractionServiceProvider"
php artisan migrate
Publish both the migrations and the config file — the config lets you swap the interaction model and rename tables, and it is much harder to retrofit once rows exist.
Add the traits
use LaravelInteraction\Interaction\Concerns\CanInteract; // the actor (usually User)
use LaravelInteraction\Interaction\Concerns\HasInteractions; // the subject (Post, Video, Comment…)
class User extends Model { use CanInteract; }
class Post extends Model { use HasInteractions; }
Look here first, in this order
config/interactions.php — table name, model binding, morph key type (morphs vs uuidMorphs).src/Concerns/ — the public API is small and the method names are the fastest way to understand the mental model. Confirm exact method names against the README for your pinned version rather than relying on memory.First use case
Record a single interaction from a user on a post, then read the state back on a page render:
$user->interact($post, 'like'); // actor → subject, typed
$user->hasInteracted($post, 'like'); // boolean, for "did I like this?" buttons
$post->interactors('like'); // who liked it
$post->interactions()->count(); // how many, all types
If that loop works end to end, everything else in the package is a variation on it.
Treat type as a first-class enum. The package stores interactions in one table discriminated by a string type. Back it with a PHP enum and cast it, then wrap the trait methods in small domain methods so controllers never pass raw strings:
enum InteractionType: string {
case Like = 'like';
case Bookmark = 'bookmark';
case View = 'view';
}
// In a service or the model itself
public function like(Post $post): void { $this->interact($post, InteractionType::Like->value); }
This is the single highest-leverage decision: it keeps where('type', ...) scattered everywhere from leaking into your codebase.
Enforce a morph map early. If you let FQCNs land in interactionable_type, renaming or moving a model silently orphans every row. Register Relation::enforceMorphMap([...]) in a service provider before the first interaction is written — migrating existing rows afterwards is a data backfill, not a config change.
Idempotency for toggles. Like buttons get double-clicked and requests get retried. The unique index will reject the duplicate, so either:
try {
$user->interact($post, InteractionType::Like->value);
} catch (QueryException $e) {
// already liked — safe to ignore
}
…or use firstOrCreate/insertOrIgnore semantics if you wrap the package rather than calling the trait directly. Never rely on a hasInteracted() check followed by a write; it's a race.
Batch the per-user state. Rendering a feed of 50 posts with an isLikedBy-style call per post is 50 queries. Either eager-load the relation constrained to the current user, or pre-resolve a set of interacted IDs in one query and pass it to your view/resource. This is the most common performance regression with this package.
Pair counts with withCount rather than calling ->count() on a relation inside a loop, and consider a cached counter column on the subject for hot listings — then invalidate on interact/uninteract via a model observer.
Shape API resources consistently. Expose is_liked, likes_count, can_like from one resource class so the frontend never guesses at the shape and you keep the N+1 fix in one place.
Test with factories and assertDatabaseHas. Assert against the interactions table, not just the boolean helper — the helper can be satisfied by a stale cache while the row is missing.
orchestra/testbench constraint. Nothing in your application code changes, but your CI matrix and any pinned testbench/phpunit dev dependencies must line up, or the suite will fail on constraint resolution rather than on real errors. If you're still on Laravel 10/11 the previous 3.x line is fine; nothing forces the upgrade.interactions()->count() unless you constrain the relation. Decide deliberately whether counts are "all time" or "live only", and put it in a scope so it's consistent.like, bookmark, and view alongside this one, verify whether they share the same table with a type discriminator or create their own — and check the config for each. Getting this wrong produces duplicate migrations and confusing counts.QueryException around the write; don't let a double-tap 500 the request.SELECT type, count(*) FROM interactions GROUP BY type almost always reveals a type-string typo or a duplicated write.vendor/ edits — the traits are used directly into your models and trait collisions with your own interactions() method are the most common breakage.Overall, 3.5.0 is a compatibility release: upgrade for the Laravel 13 / PHP 8.5 support if you're moving your stack forward, otherwise there is no functional reason to rush.
How can I help you explore Laravel packages today?