The v3 major line: a metric-driven leaderboard, leagues, tiers, typed multiplier scopes, and an optional absolute count on achievements.
Upgrading from v2.x? See UPGRADE.md.
Breaking changes
Multiplier::scopeTo(Model ...$models)removed. Replaced with typed methods:scopeToUser(Model ...$users),scopeToTier(Tier ...$tiers), plus companionunscopeFromUser/unscopeFromTier/isGlobal. See UPGRADE.md.multiplier_scopespolymorphic table replaced with two typed pivots —multiplier_userandmultiplier_tier. A backfill migration runs automatically duringphp artisan migrateand migrates existing data into the new schema. Removes theMorphToManyWithTextCastPostgres workaround that shipped in v2.1.MultiplierScopemodel removed. Bothconfig('level-up.models.multiplier_scope')andconfig('level-up.tables.multiplier_scopes')config keys are no longer read.'level-up.table'legacy config key removed. Deprecated since v2.0 in favour of'level-up.tables.experiences'.LevelUp\Experience\Support\UserForeignKeyhelper class removed. Replaced by a$table->userForeignId()Blueprint macro that reads the same config and routes toforeignId()/foreignUuid()/foreignUlid().- Trait method aliasing helpers removed (cherry-picked from v2.1's revert of PR #123). Host User models with colliding
challenges()/streaks()/experience()/experienceHistory()methods need to rename or compose into a wrapper model. setPoints()recalculates level and tier. Previously a raw column write with no side effects; now firesUserLevelledUp/UserTierUpdatedevents when the new point total implies a different placement.level-up.audit.enablednow defaults totrue(wasfalse). Time-windowed leaderboards source their scores from theexperience_auditsledger, so auditing is on out of the box. SetAUDIT_POINTS=falseto opt out — but periodic XP boards then throwMetricRequiresAuditingException. See UPGRADE.md. (#163)Leaderboard::generate()returnsLeaderboardEntryobjects —$entry->user,$entry->score,$entry->rank— instead of a bare collection ofUsermodels. Score and rank live on the entry, never on the User model. See UPGRADE.md. (#160)- Leaderboard ranks require a window-function-capable database — SQLite 3.25+ or MySQL 8+; MariaDB 10.2+ and PostgreSQL support window functions natively. MySQL 5.7 (EOL) is not supported. (#161)
- Excess points cap at the top level instead of throwing.
addPoints($amount)where$amountexceeds the highest defined level's threshold no longer throws — the user is capped at the highest level.
Added
- Metric-driven leaderboard — rank by any metric via the
RankingMetriccontract andLeaderboard::by(); built-in metrics are registered inlevel-up.leaderboard.metrics, withMetricNotFoundException/MetricDisabledExceptionon bad input. (#160) - Rank numbers and ties — competition semantics (1, 1, 3) computed with SQL window functions, deterministic tiebreak ordering, plus
rankOf()andaround(). (#161) levelandstreakstate metrics — rank by current level, or by streak count for an Activity vianew StreakMetric(activity: $activity). (#162)- Time Periods —
period(Period::Day|Week|Month)andsince(start:, until:)window a board to activity inside the range; XP windows are sourced from theexperience_auditsledger;week_starts_on/timezoneconfig controls boundaries. (#163) achievementsandchallengesflow metrics — rank by achievements earned (secret ones count) or challenges completed, all-time or per Period. (#164)restrictTo()— host-defined populations (friends boards, guilds, tournament brackets); ranks are computed within the restricted set. (#165)- Named Boards — declare metric/period(/tier) combinations under
level-up.leaderboard.boardsand resolve them withLeaderboard::board(); only declared Boards are tracked over time. (#166) - Snapshots and rank events — the
level-up:snapshot-boardscommand persists each Board's top entries down to its Tracked Depth (track_top, default 100), diffs consecutive runs, dispatchesLeaderboardRankChanged/UserEnteredTrackedDepth/UserLeftTrackedDepth, and prunes runs persnapshots.retention_days. (#167) leaderboard_rankchallenge condition — "finish top N on a named Board", progressed by snapshot runs and validated against the Board's Tracked Depth at creation. (#168)- Leagues — a Division ladder with lazy Cohort enrollment on one periodic Board;
HasLeaguestrait withcurrentDivision(),currentCohort(), andcohortStandings(). (#169) - League rollover — the
level-up:league-rollovercommand computes each Cohort's final standings live, promotes and relegates per the Division's configured counts, and dispatchesUserDivisionChanged. (#170) Multiplier::scopeToUser,scopeToTier,unscopeFromUser,unscopeFromTier,isGlobalmethods.$table->userForeignId()Blueprint macro alongsideentityId()/entityForeignId().migrate_multiplier_scopes_to_typed_pivotsmigration — backfills v2.x data into the v3 schema, no-op on fresh installs.- Optional absolute count on Achievements — track the real number behind a percentage (games played, articles read) via an open-ended
countset withgrantAchievement()and increased alongside progress withincrementAchievementProgress(); the increment is carried on theAchievementProgressionIncreasedevent. (#179)
Fixed
addPoints(),deductPoints(), andsetPoints()are now wrapped inDB::transaction()so the points/level/tier writes are atomic. The package's points/level/tier/multiplier events (PointsIncreased,PointsDecreased,UserLevelledUp,UserTierUpdated,MultiplierApplied) now implementShouldDispatchAfterCommit, so listeners run only once the surrounding transaction has committed — external side effects (HTTP, mail, non-ShouldQueueAfterCommitqueue jobs) no longer fire on a transaction that later rolls back.grantAchievement(),incrementAchievementProgress(), andrevokeAchievement()clear the cachedachievementsrelation (andallAchievements,achievementsWithProgress,secretAchievements) so subsequent reads on the same instance reflect the mutation without needing a manualrefresh().addPoints()first-time-experience branch resolves the starting level viaorderByDesc('level')to matchPointsIncreasedListener. Previously it ordered bynext_level_experience, which would diverge from the listener when level thresholds aren't strictly monotonic.alter_experience_audits_type_to_stringmigration'sdown()now works on PostgreSQL (reported by @christoph-kluge in discussion #121). Driver-aware fallback splits theALTER COLUMN TYPEandADD CONSTRAINTinto separate statements onpgsql; MySQL and SQLite use the existing Blueprint path.
Removed
Multiplier::scopeTo,Multiplier::scopes(HasManytoMultiplierScope).src/Models/MultiplierScope.php.src/Support/UserForeignKey.php.database/migrations/create_multiplier_scopes_table.php.stub.config('level-up.table')(legacy experiences-table key).config('level-up.models.multiplier_scope')config key.config('level-up.tables.multiplier_scopes')config key (replaced bytables.multiplier_user+tables.multiplier_tier).