Skip to main content

level-up-upgrade-v3

Automatically upgrade cjmellor/level-up from v2.x to v3.0, applying all breaking changes to the user's codebase.

Source facts

Repository
cjmellor/level-up
Last source activity
June 18, 2026 at 21:01
Detected SKILL.md language
English
Stars
676
Forks
55

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
level-up-upgrade-v3
description
Automatically upgrade cjmellor/level-up from v2.x to v3.0, applying all breaking changes to the user's codebase.
## When to use this skill Use this skill when a user needs to upgrade from cjmellor/level-up v2.x to v3.0. If the user is on v1.x, run the `level-up-upgrade-v2` skill first, then come back here. ## What's in v3 The headline is a metric-driven leaderboard: rank by any metric (`xp`, `level`, `streak`, `achievements`, `challenges`, or a custom `RankingMetric`), time Periods, `rankOf()`/`around()`, `restrictTo()` for friends boards, named Boards declared in config, Snapshots with rank-change events, a `leaderboard_rank` challenge condition, and Leagues (a Division ladder with Cohorts, promotion, and relegation). Three leaderboard breaking changes ride along: - `Leaderboard::generate()` returns `LeaderboardEntry` objects (`$entry->user`, `$entry->score`, `$entry->rank`) instead of a bare collection of `User` models. - `level-up.audit.enabled` defaults to `true` (was `false`) — periodic leaderboards source their scores from the `experience_audits` ledger. - Leaderboard ranks are computed with SQL window functions, requiring SQLite 3.25+ or MySQL 8+ (MariaDB 10.2+ and PostgreSQL support them natively). Plus cleanup: - Replaces the polymorphic `multiplier_scopes` table with two typed pivots — `multiplier_user` and `multiplier_tier`. Eliminates the need for v2.1's Postgres-specific morph-cast workaround. - Replaces `Multiplier::scopeTo($model)` with `scopeToUser($user)` / `scopeToTier($tier)` (plus `unscopeFromUser`, `unscopeFromTier`, `isGlobal`). - Removes the deprecated `'level-up.table'` config key (use `'tables.experiences'`). - Replaces the `UserForeignKey::on($table)` migration helper with a `$table->userForeignId()` Blueprint macro. - Removes the incomplete trait method aliasing helpers added in PR #123. - `setPoints()` now recalculates level and tier (and fires events) instead of just writing the raw column. - `addPoints()` past the highest level threshold now caps at the top level instead of throwing. - `addPoints()` is wrapped in `DB::transaction()` for atomicity. - `revokeAchievement()` clears the cached relation collection so subsequent reads see the detached state. - Adds a `challenge_completions` ledger — every completion is recorded as its own row, so the `challenges` leaderboard metric counts repeatable completions correctly; a migration backfills existing completions. - Fixes a Postgres rollback bug on `alter_experience_audits_type_to_string`. PHP and Laravel version requirements are unchanged (PHP 8.3+, Laravel 12 or 13). ## Step 1: Pre-flight checks 1. Confirm the user has backed up their database. The `multiplier_scopes` table will be transformed by a backfill migration — if anything goes wrong, they need a restore path. 2. Confirm the user is on v2.x and not v1.x. If `composer show cjmellor/level-up | grep versions` shows v1.x, redirect to `level-up-upgrade-v2` first. 3. Check the database version if the app uses the leaderboard: ranks need window functions — SQLite 3.25+ or MySQL 8+ (MariaDB 10.2+ and PostgreSQL are fine). MySQL 5.7 cannot run v3 leaderboard queries. 4. Run `php artisan migrate:status` to ensure no pending migrations. 5. Run `php artisan test` to ensure the test suite passes before starting. ## Step 2: Update the composer constraint Update `composer.json`: ```json "cjmellor/level-up": "^3.0" ``` Then run `composer update cjmellor/level-up`. ## Step 3: Update application code that uses `Multiplier::scopeTo()` Search the codebase for `scopeTo(`. For each call, change it to `scopeToUser` or `scopeToTier` depending on the model type: ```php // Before $multiplier->scopeTo($user); $multiplier->scopeTo($goldTier); $multiplier->scopeTo($user, $goldTier); // After $multiplier->scopeToUser($user); $multiplier->scopeToTier($goldTier); $multiplier->scopeToUser($user)->scopeToTier($goldTier); ``` If a single `scopeTo` call mixes types — `scopeTo($user, $goldTier)` — split it into one call per type. If the application uses `$multiplier->scopes()` (the old `HasMany` to `MultiplierScope`), that relation is gone. Use `$multiplier->users` and `$multiplier->tiers` (now `BelongsToMany`) instead, or `$multiplier->isGlobal()` for the "no scopes" check. ## Step 4: Update any direct references to `MultiplierScope` Search for `MultiplierScope`. The model is removed in v3. Any code instantiating, querying, or type-hinting it needs to migrate to the typed pivot tables (`DB::table('multiplier_user')` / `DB::table('multiplier_tier')` for raw queries). ## Step 5: Update `level-up.table` config (if present) Open the user's published `config/level-up.php`. If it has: ```php 'table' => 'experiences', ``` …delete that line and add or update the `tables` block: ```php 'tables' => [ 'experiences' => 'experiences', // or whatever value 'table' was set to // ... other table overrides ], ``` ## Step 6: Update `level-up.tables.multiplier_scopes` config (if present) If the published config has `'multiplier_scopes' => '...'` under `'tables'`, remove that key and add the two new ones: ```php 'tables' => [ // ... 'multiplier_user' => 'multiplier_user', 'multiplier_tier' => 'multiplier_tier', // ... ], ``` If you had a custom name for `multiplier_scopes` (e.g. `'gamification_scopes'`), pick appropriate names for the two new tables (e.g. `'gamification_user'` and `'gamification_tier'`). ## Step 7: Update any custom migrations using `UserForeignKey::on()` Search the application's migrations for `UserForeignKey`. For each: ```php // Before use LevelUp\Experience\Support\UserForeignKey; // ... UserForeignKey::on($table)->constrained(config('level-up.user.users_table'))->cascadeOnDelete(); // After $table->userForeignId()->constrained(config('level-up.user.users_table'))->cascadeOnDelete(); ``` Delete the `use LevelUp\Experience\Support\UserForeignKey;` import — the class is gone. ## Step 8: Audit `setPoints()` usage Search for `setPoints(`. Confirm with the user that the new behavior (recompute level + tier, fire events) is desired at each callsite. If any callsite needs the v2.x raw-write semantics, replace with: ```php $user->experience->update(['experience_points' => $amount]); ``` ## Step 9: Audit `addPoints()` exception handlers Search for `catch.*Points exceed` or any try/catch around `addPoints` that handles the "Points exceed the last level's experience points" exception. The exception is no longer thrown in v3 — the user is capped at the highest level instead. Remove the catch block, or convert it to a level-cap check. ## Step 10: Trait method collisions If the host's User model defines any of `challenges()`, `streaks()`, `experience()`, `experienceHistory()`, those methods now collide with the package's trait methods (PR #123's aliasing helpers were removed). The user needs to either: - Rename their User method (e.g. `userChallenges()`). - Move the level-up traits onto a separate `UserProfile` / `Gamification` model and compose it. ## Step 11: Update leaderboard consumers Search for `Leaderboard::`. Every terminal call (`generate()`, `around()`) now returns `LeaderboardEntry` objects instead of `User` models — update code that iterated users directly: ```php // Before (v2.x) foreach (Leaderboard::generate() as $user) { $user->name; $user->experience->experience_points; } // After (v3.0) foreach (Leaderboard::generate() as $entry) { $entry->user->name; // the User model (experience still eager-loaded) $entry->score; // the user's score on the board's metric $entry->rank; // competition rank — tied scores share a rank (1, 1, 3) } ``` Blade templates, API resources, and tests that consumed the old shape need the same treatment. ## Step 12: Decide on the audit default `level-up.audit.enabled` now defaults to `true`. Ask the user: - If they want time-windowed leaderboards (`period()` / `since()` on XP), auditing must stay on — and note that windowed boards only see activity recorded after auditing was enabled. - If they relied on the old default to keep `experience_audits` empty, set `AUDIT_POINTS=false` in `.env` — but periodic XP boards will then throw `MetricRequiresAuditingException`. - If their published config pins `'enabled' => env('AUDIT_POINTS', false)`, nothing changes until they update it. ## Step 13: Add the new model bindings (if the config is published) The new league, snapshot, and challenge-completion features resolve their model classes straight from `level-up.models.*`, and Laravel merges a published config **shallowly** — so if the user published `config/level-up.php` in v1/v2, their `models` array overrides the package default and the new bindings are missing (they resolve to `null` and the feature throws). `tables` are safe (they fall back to built-in defaults) and the `leaderboard` settings block is a new top-level key that merges in automatically — it is specifically the `models` map that needs the additions. If `config/level-up.php` is **not** published, skip this step. If it **is**, add the new keys to the `models` array (or re-publish with `--force` after backing up customisations): ```php 'multiplier_user' => LevelUp\Experience\Models\Pivots\MultiplierUser::class, 'multiplier_tier' => LevelUp\Experience\Models\Pivots\MultiplierTier::class, 'challenge_completion' => LevelUp\Experience\Models\ChallengeCompletion::class, 'leaderboard_snapshot' => LevelUp\Experience\Models\LeaderboardSnapshot::class, 'division' => LevelUp\Experience\Models\Division::class, 'cohort' => LevelUp\Experience\Models\Cohort::class, 'cohort_user' => LevelUp\Experience\Models\Pivots\CohortUser::class, ``` The old `models.multiplier_scope` key (pointing at the removed `MultiplierScope`) is dead — drop it. To declare Boards or a league, also copy the new `leaderboard` block from the package's `config/level-up.php`. ## Step 14: Publish and run migrations ```bash php artisan vendor:publish --tag="level-up-migrations" php artisan migrate ``` Publishing picks up the new v3 tables: `leaderboard_snapshots`, `divisions`, `cohorts`, `cohort_user`, and `challenge_completions`. The `migrate_multiplier_scopes_to_typed_pivots` migration runs automatically, and `create_challenge_completions_table` backfills the new ledger with one row per already-completed challenge (so the `challenges` metric counts prior completions). If the user has existing `multiplier_scopes` data, it'll print a summary like: ``` level-up: migrated 47 multiplier scope rows (32 user, 15 tier) into typed pivot tables; dropped multiplier_scopes. ``` If the migration encounters rows with a `scopeable_type` it doesn't recognise (not the configured user model and not the configured Tier class), it logs a warning and skips them — those rows are lost. Surface this to the user if it happens. ## Step 15: Run tests ```bash php artisan test ``` If anything fails, walk back through the steps above — most v3 breakage is straightforward find-and-replace. ## Step 16: Verify Spot-check that scoped multipliers still apply correctly: ```php $user = User::find(1); $multiplier = Multiplier::active()->forUser($user)->first(); dump($multiplier?->name); ``` Done. The user is on v3.
View on GitHub