x-laravel/likeable
A Laravel package that adds like and dislike behavior to Eloquent models via a reusable trait, letting you attach reactions to posts, comments, or any model and query counts and liked items through a simple API.
Architecture fit. The package is a thin, trait-based polymorphic relationship layer — a good architectural fit for Laravel applications that already lean on Eloquent, service container config, model events, and the scheduler. It does not introduce a new persistence model or a service boundary; it adds two contracts (CanLike/Liker, HasLikes/Likeable), a pivot-style likes table, and a handful of query scopes. That is the right shape for a "likes" capability in a monolith or modular monolith, and it keeps the domain logic in the consuming models where most Laravel teams expect it. It is not a social-graph, reaction, or engagement-analytics product — if the roadmap includes reactions, weighted ranking, or feed ranking, treat this as a component rather than a platform.
Integration feasibility. High. Installation is additive: composer require, publish config, run migration, add traits, optionally set a default liker model. The withLikedBy() scope and isLikedBy() are the two features that decide whether this is usable in production list views — without them, every index page becomes an N+1 problem, so their presence in v1.0.0 is the difference between "demo-ready" and "ship-ready." like() returning the existing like on repeat makes the API effectively idempotent at the semantics level.
Technical risk. Concentrated in four places:
liker_type / likeable_type are string columns. If the app does not enforce a morph map, stored values are FQCNs and any namespace refactor silently orphans likes. Enforcing Relation::enforceMorphMap() should be a hard prerequisite, not a nice-to-have.(liker_type, liker_id, likeable_type, likeable_id) exists or add it in the migration before rollout.Liked/Unliked dispatched after commit is the correct choice, but it means consumers must tolerate at-least-once-ish delivery, ordering gaps, and no event on rollback. Anything that counts, notifies, or feeds a recommendation pipeline off these events needs idempotency keys; the payload shape isn't documented in the notes and should be treated as an unknown.Key questions.
likes() / likers() filter them?Schedule:: entry? Does it use onOneServer() for multi-node safety?likers() and givenLikes() paginated or cursor-friendly, or do they return full collections?Liked/Unliked, and are the events queued or synchronous after commit?Stack fit. Assumes a conventional Laravel stack with MySQL/Postgres, queue workers, and the Laravel scheduler running. The package is framework-idiomatic and imposes no infrastructure: no Redis requirement, no external service, no search index. Configurable connection means it can be pinned to a specific database in a multi-database or read/write-split setup — worth using deliberately rather than defaulting, since like writes can be high-volume and should not compete with primary transaction traffic on a shared connection.
Migration path. Greenfield adoption is trivial: add traits to User and to the likeable models, publish config, migrate. For an application with an existing likes/favorites table, plan a real migration, not a swap: dual-write or backfill into the package's schema, normalize legacy type values to morph aliases, reconcile duplicates before adding the unique constraint, then cut reads over, then decommission the old table. Budget for a backfill of the row count plus the deduplication pass — that is the schedule risk, not the code change.
Compatibility. The two-combined-traits case (a model that is both liker and likeable) is the scenario to test explicitly, along with the default liker model config when an app has multiple user-like entities (User, Team, Organization). If the app uses UUID/ULID keys, verify the shipped migration's key column types before running it in a shared environment. If the app uses a morph map, confirm the package does not write raw class names.
Sequencing.
withLikedBy() into the primary list view; measure query count.Liked/Unliked listeners (analytics, notifications) with idempotency handling; validate in staging that events do not fire on rolled-back transactions.Maintenance. Small surface area — two traits, a few scopes, one table — which keeps maintenance cost low and upgrade risk modest. The counterweight is that a v1.0.0 has no upgrade history: the first minor release is where breaking changes usually surface, so pin the version and read changelogs rather than floating. The scheduled pruning job is the piece most likely to need tuning after go-live.
Support. No support tier is described in the release notes. For a capability that touches user-facing state and a scheduled job, decide internally who owns the likes table: schema changes, pruning health, and event contract changes need one named owner. If the package is a single-maintainer open-source project, plan for the possibility of vendoring or forking; the code footprint is small enough that this is a realistic fallback.
Scaling. Two pressure points. Write volume: each like is an insert plus a Liked event; a viral surface can produce bursty insert load and an event storm. Read volume: likers() counts and isLikedBy() checks per rendered item are the classic cost — withLikedBy() mitigates per-list state, but aggregate counts will need a cached counter (denormalized column, Redis, or a materialized count) rather than repeated count() queries. Pruning cost scales with orphan rate and chunk size; tune chunk size so the job stays inside its window without long locks. Polymorphic indexes must lead with the columns actually filtered in hot queries.
Failure modes to plan for.
Liked/Unliked if listeners are not idempotent.Unliked.Ramp-up. For a Laravel developer, roughly half a day to working code and one to two days to a production-grade integration that includes authorization, rate limiting, eager-loaded liked-state, event consumers, and verified pruning. The real lead time is not engineering — it is product decisions the package deliberately leaves open: whether repeated likes are idempotent, whether users can see who liked them, whether counts are public and eventually consistent, how likes behave on soft-deleted content, and what the retention policy is for engagement events. Settle those before the first sprint, because they determine whether a cached counter and an event pipeline are in scope.
How can I help you explore Laravel packages today?