一键导入
level-up-development
// Build and work with cjmellor/level-up features, including XP, levels, tiers, achievements, streaks, multipliers, leaderboards, and auditing.
// Build and work with cjmellor/level-up features, including XP, levels, tiers, achievements, streaks, multipliers, leaderboards, and auditing.
| name | level-up-development |
| description | Build and work with cjmellor/level-up features, including XP, levels, tiers, achievements, streaks, multipliers, leaderboards, and auditing. |
Use this skill when working with gamification features — adding experience points, levels, tiers, achievements, streaks, multipliers, leaderboards, or auditing — using cjmellor/level-up.
composer require cjmellor/level-up
php artisan vendor:publish --tag="level-up-migrations"
php artisan migrate
php artisan vendor:publish --tag="level-up-config"
Add only the traits you need:
use LevelUp\Experience\Concerns\GiveExperience;
use LevelUp\Experience\Concerns\HasAchievements;
use LevelUp\Experience\Concerns\HasChallenges;
use LevelUp\Experience\Concerns\HasStreaks;
use LevelUp\Experience\Concerns\HasTiers;
class User extends Authenticatable
{
use GiveExperience; // Required — XP and levels
use HasAchievements; // Optional — achievements
use HasStreaks; // Optional — streaks
use HasTiers; // Optional — tiers
use HasChallenges; // Optional — challenges
}
GiveExperience is the foundation. The others are opt-in.
use LevelUp\Experience\Models\Level;
Level::add(
['level' => 1, 'next_level_experience' => null],
['level' => 2, 'next_level_experience' => 100],
['level' => 3, 'next_level_experience' => 250],
['level' => 4, 'next_level_experience' => 500],
['level' => 5, 'next_level_experience' => 1000],
);
Level 1 must have next_level_experience set to null — it is the default starting point. Users level up automatically when their XP reaches the threshold. Throws LevelExistsException if a level number already exists.
$user->getLevel(); // Current level number (int), 0 if no experience
$user->getPoints(); // Current XP total (int), 0 if no experience
$user->nextLevelAt(); // XP remaining until next level (int)
$user->nextLevelAt(checkAgainst: 5); // XP remaining until level 5
$user->nextLevelAt(showAsPercentage: true); // Progress as 0-100 percentage
$user->levelUp(to: 5); // Jump to level 5
Throws InvalidArgumentException if the level does not exist. Fires UserLevelledUp for each intermediate level gained. Respects the level cap.
Configured in config/level-up.php:
'level_cap' => [
'enabled' => env('LEVEL_CAP_ENABLED', true),
'level' => env('LEVEL_CAP', 100),
'points_continue' => env('LEVEL_CAP_POINTS_CONTINUE', true),
],
When the cap is reached, the user stops levelling. If points_continue is true, XP still accumulates. If false, XP stops accumulating too.
$user->addPoints(50);
$user->addPoints(50, reason: 'Completed tutorial');
$user->addPoints(50, multiplier: 2);
$user->addPoints(50, type: AuditType::Add->value, reason: 'Bonus');
Creates an experience record if none exists, otherwise increments. Automatically levels up if the threshold is crossed. Throws if the amount exceeds the highest level's next_level_experience.
$user->deductPoints(30);
$user->deductPoints(30, reason: 'Penalty');
Throws Exception if the user has no experience record.
$user->setPoints(500);
Directly overwrites the XP total. Throws Exception if the user has no experience record.
$user->getPoints(); // int, returns 0 if no experience record
Multipliers are database-backed records that modify point calculations. They can be scoped to specific users or tiers, scheduled with time windows, and configured to stack using different strategies.
use LevelUp\Experience\Models\Multiplier;
Multiplier::create([
'name' => 'Weekend Bonus',
'multiplier' => 2.0,
'is_active' => true,
'starts_at' => now()->startOfWeekend(),
'expires_at' => now()->endOfWeekend(),
]);
Active multipliers are automatically applied when addPoints() is called. The multiplier value must be at least 0.01. If both starts_at and expires_at are set, starts_at must be before expires_at.
Multipliers with no scopes apply to all users. Use scopeTo() to restrict:
$multiplier->scopeTo($user); // Specific user only
$multiplier->scopeTo($goldTier); // Gold tier users only
$multiplier->scopeTo($user, $tier); // Multiple scopes (variadic)
scopeTo() is idempotent — calling it twice with the same model does not create duplicates.
$user->addPoints(amount: 10, multiplier: 3); // 30 points
Inline multipliers stack with DB multipliers according to the configured strategy.
Configure in config/level-up.php:
'multiplier' => [
'enabled' => env('MULTIPLIER_ENABLED', true),
'stack_strategy' => env('MULTIPLIER_STACK', 'compound'),
// 'compound' — multipliers multiply each other: 2 × 5 = 10x
// 'additive' — multipliers sum: 2 + 5 = 7x
// 'highest' — only the largest applies: max(2, 5) = 5x
],
Multiplier::active()->get(); // Currently active
Multiplier::active()->forUser($user)->get(); // Active for a specific user
Multiplier::scheduled()->get(); // Future (starts_at > now)
Multiplier::expired()->get(); // Past (expires_at < now)
Fires when multipliers modify a point calculation. Properties: Model $user, Collection $multipliers, int $originalAmount, int $finalAmount, string $strategy.
use LevelUp\Experience\Models\Tier;
Tier::add(
['name' => 'Bronze', 'experience' => 0],
['name' => 'Silver', 'experience' => 500],
['name' => 'Gold', 'experience' => 2000],
['name' => 'Platinum', 'experience' => 5000, 'metadata' => ['color' => '#E5E4E2', 'icon' => 'crown']],
);
Tier names and experience values must be unique. Throws TierExistsException on duplicates. The metadata column is a flexible JSON field for any extra data (colours, icons, descriptions). The entire add() call is wrapped in a database transaction — if any tier fails, none are created.
Tiers update automatically when XP changes. When addPoints() causes the user to cross a tier threshold, experience.tier_id is updated and a UserTierUpdated event fires with TierDirection::Promoted.
$user->getTier(); // Current Tier model or null
$user->getNextTier(); // Next tier above current (Tier or null)
$user->tierProgress(); // Percentage 0-100 through current bracket
$user->nextTierAt(); // XP remaining until next tier
$user->isAtTier('Gold'); // Exact match (bool)
$user->isAtOrAboveTier('Silver'); // At or above (bool)
getTier() returns null if the user has no experience record or tiers are disabled.
By default, tiers use a high-water mark — once earned, they persist even if points decrease. Enable demotion to allow tier drops:
TIER_DEMOTION=true
When enabled, deductPoints() checks if the user should drop and fires UserTierUpdated with TierDirection::Demoted. The newTier property is nullable — it will be null if the user drops below all tier thresholds.
Create a multiplier and scope it to a tier so it only applies to users at that tier:
$multiplier = Multiplier::create([
'name' => 'Gold Bonus',
'multiplier' => 2.0,
'is_active' => true,
]);
$goldTier = Tier::where('name', 'Gold')->first();
$multiplier->scopeTo($goldTier);
When a Gold-tier user calls addPoints(), this multiplier is automatically included.
Restrict achievements so only users at a certain tier can earn them:
$goldTier = Tier::where('name', 'Gold')->first();
Achievement::create([
'name' => 'Golden Streak',
'tier_id' => $goldTier->id,
]);
Attempting to grant to a user below Gold throws TierRequirementNotMet.
Higher tiers get longer freeze durations:
// config/level-up.php
'tiers' => [
'streak_freeze_days' => [
'Bronze' => 1,
'Silver' => 2,
'Gold' => 3,
'Platinum' => 7,
],
],
Falls back to the global freeze_duration if the tier is not listed or tiers are disabled.
use LevelUp\Experience\Facades\Leaderboard;
Leaderboard::forTier('Gold')->generate();
Leaderboard::forTier($tierModel)->generate();
'tiers' => [
'enabled' => env('TIERS_ENABLED', true),
'demotion' => env('TIER_DEMOTION', false),
'streak_freeze_days' => [],
],
use LevelUp\Experience\Models\Achievement;
Achievement::create([
'name' => 'First Login',
'is_secret' => false,
'description' => 'Log in for the first time',
'image' => 'storage/app/achievements/first-login.png',
]);
// Secret achievement (hidden until earned)
Achievement::create([
'name' => 'Hidden Gem',
'is_secret' => true,
]);
// Tier-gated achievement
Achievement::create([
'name' => 'Gold Member Badge',
'tier_id' => $goldTier->id,
]);
$user->grantAchievement($achievement);
// With progress (0-100)
$user->grantAchievement($achievement, progress: 50);
Throws Exception if progress exceeds 100, or if the user already has the achievement. Throws TierRequirementNotMet if tier-gated and user does not meet the tier requirement. AchievementAwarded event fires only when progress is null or 100.
$user->revokeAchievement($achievement);
Throws Exception if the user does not have the achievement.
$newProgress = $user->incrementAchievementProgress($achievement, amount: 10);
$user->achievementsWithProgress()->get();
$user->achievementsWithSpecificProgress(75)->get();
incrementAchievementProgress() throws Exception if the user does not have the achievement. Grant it first. Progress is capped at 100.
$user->achievements; // Non-secret achievements
$user->secretAchievements; // Secret achievements only
$user->allAchievements; // Both
$user->getUserAchievements(); // Same as $user->achievements
use LevelUp\Experience\Models\Activity;
Activity::create(['name' => 'daily-login', 'description' => 'User logs in']);
$activity = Activity::where('name', 'daily-login')->first();
$user->recordStreak($activity);
StreakStartedStreakIncreasedStreakBroken (archives if enabled)$user->getCurrentStreakCount($activity); // int (0 if no streak)
$user->hasStreakToday($activity); // bool
$user->streaks; // HasMany relationship
$user->resetStreak($activity);
$user->freezeStreak($activity); // Uses config or tier-scaled duration
$user->freezeStreak($activity, days: 5); // Custom duration
$user->unFreezeStreak($activity);
$user->isStreakFrozen($activity); // bool
When a streak breaks, it is archived automatically (if enabled):
use LevelUp\Experience\Models\StreakHistory;
$histories = StreakHistory::where('user_id', $user->id)->get();
'archive_streak_history' => [
'enabled' => env('ARCHIVE_STREAK_HISTORY_ENABLED', true),
],
'freeze_duration' => env('STREAK_FREEZE_DURATION', 1),
Challenges are multi-condition goals that users enroll in and complete for rewards. Conditions are evaluated automatically when relevant events fire (points earned, level reached, achievement granted, streak recorded, tier changed).
use LevelUp\Experience\Models\Challenge;
Challenge::create([
'name' => 'Getting Started',
'conditions' => [
['type' => 'points_earned', 'amount' => 100],
['type' => 'level_reached', 'level' => 3],
],
'rewards' => [
['type' => 'points', 'amount' => 50],
],
'auto_enroll' => true,
'is_repeatable' => false,
]);
| Type | Required Keys | What it checks |
|---|---|---|
points_earned | amount | Points earned since enrollment (baseline delta) |
level_reached | level | User's current level >= value |
achievement_earned | achievement_id | User has the achievement |
streak_count | activity, count | Current streak count for activity >= value |
tier_reached | tier | User is at or above the named tier |
custom | class | Class implementing ChallengeCondition interface |
| Type | Required Keys | What it does |
|---|---|---|
points | amount | Awards XP via addPoints() |
achievement | achievement_id | Grants the achievement |
$user->enrollInChallenge($challenge);
$user->unenrollFromChallenge($challenge);
Throws if: challenge not started yet, expired, already enrolled, or completed and not repeatable. Completed repeatable challenges can be re-enrolled.
Auto-enroll challenges (auto_enroll: true) automatically enroll users when a relevant event fires.
$user->getChallengeProgress($challenge); // Array of condition statuses
$user->getChallengeCompletionPercentage($challenge); // 0.0 - 100.0
$user->activeChallenges; // Enrolled, not completed
$user->completedChallenges; // Completed
Challenges support optional starts_at and expires_at fields. If both are set, starts_at must be before expires_at. Expired challenges are not evaluated.
Implement the ChallengeCondition interface:
use LevelUp\Experience\Contracts\ChallengeCondition;
use Illuminate\Database\Eloquent\Model;
class HasVerifiedEmail implements ChallengeCondition
{
public function check(Model $user, array $condition): bool
{
return $user->hasVerifiedEmail();
}
}
Reference it in the condition: ['type' => 'custom', 'class' => HasVerifiedEmail::class].
'challenges' => [
'enabled' => env('CHALLENGES_ENABLED', true),
],
use LevelUp\Experience\Facades\Leaderboard;
Leaderboard::generate(); // All users by XP
Leaderboard::generate(paginate: true); // Paginated
Leaderboard::generate(limit: 10); // Top 10
Leaderboard::forTier('Gold')->generate(); // Gold tier only
Returns User models with experience relationship eager-loaded, ordered by XP descending.
Enable in config:
'audit' => [
'enabled' => env('AUDIT_POINTS', false),
],
When enabled, every addPoints(), deductPoints(), levelUp(), and tier change creates an experience_audits record.
$user->experienceHistory; // HasMany to ExperienceAudit
Audit types use the AuditType enum:
use LevelUp\Experience\Enums\AuditType;
AuditType::Add; // 'add'
AuditType::Remove; // 'remove'
AuditType::Reset; // 'reset'
AuditType::LevelUp; // 'level_up'
AuditType::TierUp; // 'tier_up'
AuditType::TierDown; // 'tier_down'
| Event | Properties | When |
|---|---|---|
PointsIncreased | int $pointsAdded, int $totalPoints, string $type, ?string $reason, Model $user, ?array $multipliers | Points added |
PointsDecreased | int $pointsDecreasedBy, int $totalPoints, ?string $reason, Model $user | Points deducted |
MultiplierApplied | Model $user, Collection $multipliers, int $originalAmount, int $finalAmount, string $strategy | Multipliers modified point calculation |
UserLevelledUp | Model $user, int $level | Level gained (fires per level) |
UserTierUpdated | Model $user, ?Tier $previousTier, ?Tier $newTier, TierDirection $direction | Tier promotion or demotion |
AchievementAwarded | Achievement $achievement, Model $user | Achievement granted at 100% |
AchievementRevoked | Achievement $achievement, Model $user | Achievement revoked |
AchievementProgressionIncreased | Achievement $achievement, Model $user, int $amount | Progress incremented |
StreakStarted | Model $user, Activity $activity, Streak $streak | First streak record |
StreakIncreased | Model $user, Activity $activity, Streak $streak | Consecutive day |
StreakBroken | Model $user, Activity $activity, Streak $streak | Streak reset |
StreakFrozen | int $frozenStreakLength, Carbon $frozenUntil | Streak frozen |
StreakUnfroze | (none) | Streak unfrozen |
ChallengeCompleted | Challenge $challenge, Model $user | Challenge conditions met, rewards dispatched |
ChallengeEnrolled | Challenge $challenge, Model $user | User enrolled in challenge |
ChallengeUnenrolled | Challenge $challenge, Model $user | User unenrolled from challenge |
$user = User::with(['experience.status', 'experience.tier'])->find($id);
$data = [
'level' => $user->getLevel(),
'points' => $user->getPoints(),
'next_level_in' => $user->nextLevelAt(),
'level_progress' => $user->nextLevelAt(showAsPercentage: true),
'tier' => $user->getTier()?->name,
'tier_progress' => $user->tierProgress(),
'next_tier_in' => $user->nextTierAt(),
];
use LevelUp\Experience\Events\UserLevelledUp;
Event::listen(UserLevelledUp::class, function (UserLevelledUp $event) {
if ($event->level === 10) {
$achievement = Achievement::where('name', 'Hit Level 10')->first();
if ($achievement) {
$event->user->grantAchievement($achievement);
}
}
});
class GamificationSeeder extends Seeder
{
public function run(): void
{
Level::add(
['level' => 1, 'next_level_experience' => null],
['level' => 2, 'next_level_experience' => 100],
['level' => 3, 'next_level_experience' => 250],
);
Tier::add(
['name' => 'Bronze', 'experience' => 0],
['name' => 'Silver', 'experience' => 500],
['name' => 'Gold', 'experience' => 2000],
);
Activity::create(['name' => 'daily-login']);
}
}
All model classes in the models config array can be overridden to use custom models. The user.foreign_key defaults to user_id and can be customised for non-standard setups.
return [
'models' => [
'achievement' => LevelUp\Experience\Models\Achievement::class,
'activity' => LevelUp\Experience\Models\Activity::class,
'experience' => LevelUp\Experience\Models\Experience::class,
'experience_audit' => LevelUp\Experience\Models\ExperienceAudit::class,
'level' => LevelUp\Experience\Models\Level::class,
'streak' => LevelUp\Experience\Models\Streak::class,
'streak_history' => LevelUp\Experience\Models\StreakHistory::class,
'achievement_user' => LevelUp\Experience\Models\Pivots\AchievementUser::class,
'tier' => LevelUp\Experience\Models\Tier::class,
'multiplier' => LevelUp\Experience\Models\Multiplier::class,
'multiplier_scope' => LevelUp\Experience\Models\MultiplierScope::class,
'challenge' => LevelUp\Experience\Models\Challenge::class,
'challenge_user' => LevelUp\Experience\Models\Pivots\ChallengeUser::class,
],
'user' => [
'foreign_key' => 'user_id',
'model' => App\Models\User::class,
'users_table' => 'users',
],
'table' => 'experiences',
'starting_level' => 1,
'multiplier' => [
'enabled' => env('MULTIPLIER_ENABLED', true),
'stack_strategy' => env('MULTIPLIER_STACK', 'compound'),
],
'level_cap' => [
'enabled' => env('LEVEL_CAP_ENABLED', true),
'level' => env('LEVEL_CAP', 100),
'points_continue' => env('LEVEL_CAP_POINTS_CONTINUE', true),
],
'audit' => [
'enabled' => env('AUDIT_POINTS', false),
],
'archive_streak_history' => [
'enabled' => env('ARCHIVE_STREAK_HISTORY_ENABLED', true),
],
'freeze_duration' => env('STREAK_FREEZE_DURATION', 1),
'tiers' => [
'enabled' => env('TIERS_ENABLED', true),
'demotion' => env('TIER_DEMOTION', false),
'streak_freeze_days' => [],
],
'challenges' => [
'enabled' => env('CHALLENGES_ENABLED', true),
],
];