A patch release fixing two correctness bugs in HasEngagements. No API or migration changes.
Disengagedis no longer dispatched when nothing was removed.disengage()guarded the delete and the counter adjustment behind a non-empty check but fired the event unconditionally, so disengaging something that was never engaged emitted a phantom event. This brings it in line withflipExclusive(), which only ever emitted the event for rows it actually deleted. Listeners can now treat everyDisengagedas a real deletion.- Closed a check-then-insert race in
engage(). Withallow_multiple_engagements = falsethe duplicate guard ran outside the transaction, so concurrent requests could both pass it, both insert, and double-count the counter. The check now runs inside the transaction underlockForUpdate(), matchingrate()andflipExclusive(). Note that gap-locking prevents the phantom insert on MySQL and SQLite serialises writes, but PostgreSQL does not gap-lock aSELECT ... FOR UPDATEmatching zero rows.
- An explicit actor on every engagement path.
engage(),disengage(), and the Verb helpers now take an optional trailingactor, defaulting to the authenticated user. Applications whose authenticated principal is not the model that owns engagements could not use the trait at all before this. The resolved actor is what is written touser_id, what scopes the duplicate guard, the exclusive-group flip and the rating upsert, and what theEngaged/Disengagedevents carry — previously the events reportedauth()->user()even where the rows were keyed onauth()->id().
- Refreshed
composer.lockonto patched releases, resolving the open Dependabot alerts. These are development-only transitive dependencies pulled in through Testbench — the package itself requires onlyilluminate/databaseandilluminate/support— so consumers were never exposed.
A major release adding ratings, view & impression tracking, Hot/Top ranking, weighted and custom engagement Verbs, and actor-side queries. Read UPGRADING.md before upgrading from v1 — there are breaking changes.
- Custom & multiple Verb enums. Define your own engagement Verbs by implementing the
EngagementTypecontract;engageify.typesnow accepts a single enum or an array of enums. - Weighted Verbs and engagement values via a new
valuecolumn onengagements, for score-based engagement. - Exclusive groups — vote-style Verbs where engaging with one option (e.g.
upvote) automatically clears the others in its group. - Ratings on a configurable scale via the
Rateablecontract, with Bayesian-average ranking (engageify.bayesian_minimum). - Hot / Top ranking backed by a denormalised
engagement_counterstable, with a Reddit-stylehot()score and time-windowedtop()queries. - Actor-side queries — the
EngagesWithtrait andwithUserEngagement()scope query and eager-load a user's own engagements without N+1s. - View tracking via the
HasViewstrait, plus opt-in time-windowed views backed by theview_bucketstable. - Impression tracking — a signed-token viewport impression endpoint, an
@impressionBlade directive, and an optional injectable browser tracker (engageify.impressions.*). engageify:recountcommand to (re)build the engagement counters from theengagementstable.- Generic
EngagedandDisengagedevents carrying the actor, engageable, and Verb (and, forEngaged, the engagement row).
- Counts and scores are now read from a denormalised
engagement_counterstable instead of being counted from theengagementstable on the fly. Existing v1 installs must runphp artisan engageify:recountonce (after publishing and running the new migrations) to backfill the counters — until then, counts, scores, and ranking queries read as0for pre-existing engagements. See Upgrading from v1 andUPGRADING.md. - Raised the minimum requirements to PHP
^8.3and Laravel^12.0|^13.0.
- The
Engageifyfacade and itsEngageifyalias — engagement is driven through theHasEngagementstrait on your models. - The five reaction-specific events (
ModelLikedEvent,ModelDislikedEvent,ModelUpvotedEvent,ModelDownvotedEvent,ModelDisengagedEvent) — replaced by the genericEngaged/Disengagedevents. - The in-memory cache layer. The
allow_cachingandcache_durationconfig keys no longer exist; counts are kept fresh inside the same transaction as each engage/disengage, so there is nothing to invalidate.
- build(deps): Bump dependabot/fetch-metadata from 1.3.6 to 1.6.0 by @dependabot in #1
- @dependabot made their first contribution in #1
Full Changelog: https://github.com/cjmellor/engageify/commits/v0.0.1