Download the PHP package cjmellor/level-up without Composer

On this page you can find all versions of the php package cjmellor/level-up. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package level-up

Latest Version on Packagist GitHub Tests Action Status Total Downloads Packagist PHP Version Laravel Version

This package allows users to gain experience points (XP) and progress through levels by performing actions on your site. It can provide a simple way to track user progress and implement gamification elements into your application

Banner

Installation

You can install the package via composer:

You can publish and run the migrations with:

You can publish the config file with:

This is the contents of the published config file:

Customizing Table Names

If you're installing into an app that already has tables called experiences, levels, tiers, multipliers, challenges, or any of the package's other defaults, you can rename them via config — no need to patch published migrations.

Option 1 — Apply a single prefix to every package table:

Set an env var (no config publish required):

…or publish the config and edit the prefix line:

All package tables now use the prefix: levelup_experiences, levelup_levels, levelup_tiers, levelup_multipliers, levelup_challenges, and so on.

Option 2 — Rename specific tables:

Edit the tables array in the published config:

Combining both: any value left equal to the default receives the table_prefix; any value you change is taken verbatim and the prefix is NOT applied. This lets you prefix everything but override one or two outliers:

Upgrading from v1.x or earlier v2: the previous top-level 'table' config key (used to override only the experiences table) still works as a fallback. New installations should prefer 'tables.experiences' instead.

Usage

💯 Experience Points (XP)

Add the GiveExperience trait to your User model.

Give XP points to a User

A new record will be added to the experiences table which stores the Users’ points. If a record already exists, it will be updated instead. All new records will be given a level_id of 1.

[!NOTE] If you didn't set up your Level structure yet, a default Level of 1 will be added to get you started.

Deduct XP points from a User

[!NOTE] Both deductPoints and setPoints will throw an exception if the User has no experience record.

Set XP points to a User

For an event where you just want to directly add a certain number of points to a User. Points can only be set if the User has an Experience Model.

Retrieve a Users’ points

Multipliers

Multipliers modify the experience points a User earns. They are managed via the database, so you can create, schedule, and toggle multipliers at runtime — no code deployments needed. This makes them ideal for building admin panels or managing promotional events.

Create a Multiplier

Active multipliers are automatically applied when a User earns points via addPoints(). Time-based multipliers activate and deactivate based on starts_at and expires_at. Non-time-based multipliers (like "Man UTD Win the League") are toggled via is_active.

Scope Multipliers to Users or Tiers

By default, a multiplier applies to all Users. You can restrict it to specific Users or Tiers:

If a multiplier has no scopes, it applies to everyone. If it has scopes, it only applies to Users who match (either directly or via their Tier).

The tiers() and users() relations are standard belongsToMany — you can drop down to attach() / detach() / sync() for full control, but scopeToUser / scopeToTier are the recommended convenience methods because they're idempotent (they use syncWithoutDetaching under the hood, so calling them twice with the same model doesn't create duplicates).

Query Multipliers

Stacking Strategy

When multiple multipliers are active, you can control how they combine via the stack_strategy config:

Inline Multipliers

You can also pass a one-off multiplier directly when adding points. This participates in the configured stacking strategy alongside any active DB multipliers:

Events

PointsIncreased - When points are added.

MultiplierApplied - When multipliers are applied during point addition.

PointsDecreased - When points are decreased.

⬆️ Levelling

[!NOTE] If you add points before setting up your levelling structure, a default Level of 1 will be added to get you started.

Set up your levelling structure

The package has a handy facade to help you create your levels.

Level 1 should always be null for the next_level_experience as it is the default starting point.

As soon as a User gains the correct number of points listed for the next level, they will level-up.

[!TIP] a User gains 50 points, they’ll still be on Level 1, but gets another 50 points, so the User will now move onto Level 2

See how many points until the next level

Get the Users’ current Level

Level Cap

A level cap sets the maximum level that a user can reach. Once a user reaches the level cap, they will not be able to gain any more levels, even if they continue to earn experience points. The level cap is enabled by default and capped to level 100. These options can be changed in the packages config file at config/level-up.php or by adding them to your .env file.

By default, even when a user hits the level cap, they will continue to earn experience points. To freeze this, so points do not increase once the cap is hit, turn on the points_continue option in the config file, or set it in the .env.

Events

UserLevelledUp - When a User levels-up

🏆 Achievements

This is a feature that allows you to recognise and reward users for completing specific tasks or reaching certain milestones. You can define your own achievements and criteria for earning them. Achievements can be static or have progression. Static meaning the achievement can be earned instantly. Achievements with progression can be earned in increments, like an achievement can only be obtained once the progress is 100% complete.

Creating Achievements

There is no built-in methods for creating achievements, there is just an Achievement model that you can use as normal:

Gain Achievement

To use Achievements in your User model, you must first add the Trait.

Then you can start using its methods, like to grant a User an Achievement:

To retrieve your Achievements:

Revoke Achievement

You can revoke an achievement from a user using the revokeAchievement method:

The method will throw an exception if you try to revoke an achievement that the user doesn't have. You can revoke both standard and secret achievements, and it will also remove any associated progress.

When an achievement is revoked, a AchievementRevoked event is dispatched.

Add progress to Achievement

[!NOTE] Achievement progress is capped to 100%

Track an absolute count

Progress is a percentage, so it can't tell you the absolute number behind it — how many games were played, articles read, and so on. Pass an optional count alongside the progress to track that number on the same achievement:

Unlike progress, the count is open-ended — it isn't capped at 100.

Check Achievement Progression

Check at what progression your Achievements are at.

Check Achievements that have a certain amount of progression:

Increase Achievement Progression

You can increment the progression of an Achievement up to 100.

Pass a count to also increment the achievement's absolute count by that amount:

A AchievementProgressionIncreased Event runs on method execution.

Secret Achievements

Secret achievements are achievements that are hidden from users until they are unlocked.

Secret achievements are made secret when created. If you want to make a non-secret Achievement secret, you can just update the Model.

You can retrieve the secret Achievements.

To view all Achievements, both secret and non-secret:

Events

AchievementAwarded - When an Achievement is attached to the User

[!NOTE] This event only runs if the progress of the Achievement is 100%

AchievementRevoked - When an Achievement is detached from the User

AchievementProgressionIncreased - When a Users’ progression for an Achievement is increased.

📈 Leaderboard

The package includes a metric-driven leaderboard. A leaderboard ranks users by a metric — experience points by default — and returns LeaderboardEntry objects exposing the user, their score, and their rank.

Pass paginate: true for a paginator of entries, or limit: to cap the result count. Ranks are always board-wide — entry 16 on page 2 still carries rank 16.

Ranks and ties

Ranks use competition semantics: users with equal scores share a rank, and the next rank is skipped — two users tied for first are both rank 1, and the next user is rank 3. Tied rows are ordered deterministically (score descending, then user key ascending) so pagination boundaries stay stable between requests.

Ranks are computed in the database with SQL window functions, so the leaderboard requires a database that supports them: SQLite 3.25+, MySQL 8+ / MariaDB 10.2+, or PostgreSQL.

A user's rank

Ask for a single user's exact rank with rankOf(), or fetch the slice of the board around them with around():

rankOf() returns null — and around() returns an empty collection — when the user is absent from the board (no experience record, or excluded by the metric's constraints). around() clamps at the edges: for the leader it returns the user plus the range entries below, and each entry keeps its board-wide rank. Both compose with by():

Choosing a metric

Metrics are registered in the config under level-up.leaderboard.metrics, and the default lives at level-up.leaderboard.default_metric. Select one explicitly with by() — it accepts a registry key, a class-string, or a metric instance:

An unknown key throws MetricNotFoundException; a metric whose underlying feature is disabled throws MetricDisabledException rather than returning an empty board.

Built-in metrics

Five metrics ship with the package:

Key Ranks by
xp Experience points (the default)
level Current level
streak Current streak count for an Activity
achievements Number of achievements earned
challenges Number of challenges completed

level ranks users by their current level number — users on the same level share a rank:

streak ranks users by their current streak count for one Activity, so it needs to know which one. Construct the metric with the Activity and pass the instance:

Generating a streak board without an Activity — for example via the bare registry key, by('streak') — throws MetricRequiresActivityException.

Both are state metrics: they rank by a current snapshot rather than an accumulation. Users without the relevant record are absent from the board — no level (no experience record) means no entry on the level board; no streak for the given Activity means no entry on that streak board.

achievements and challenges are flow metrics: they rank by an accumulation over time, so they support time periods (see below) as well as all-time boards:

achievements counts earned achievements; on a periodic board, only achievements earned within the window count. challenges counts completions recorded in the challenge_completions ledger — being enrolled isn't enough — so a repeatable challenge counts once per completion, and periodic boards window on when each completion happened, not when the user enrolled. If the challenges system is turned off (level-up.challenges.enabled), the challenges metric throws MetricDisabledException. As with every metric, users with a count of zero are absent from the board rather than ranked at zero.

[!NOTE] The achievements count includes secret achievements. This is deliberate: a count reveals nothing about which achievements were earned, and excluding them would let users be punished on the leaderboard for earning a secret. Keep secrecy in what you display, not in the score.

Time periods

Scope a board to a bounded time window with period() — "top earners this week" instead of "highest totals ever":

Or pick a custom range with since() — an open-ended start, or a bounded [start, until) window:

Periodic boards rank by activity within the window, sourced from the experience_audits ledger: the windowed score is the sum of points added minus points removed in the window. State-change audit rows (reset, level_up, tier_up, tier_down) never count. Users with no qualifying audit rows in the window are absent from the board. Everything composes as usual — rankOf(), around(), ties, limit:, paginate: all work on a periodic board:

Periodic XP boards require auditing (on by default since v3 — see level-up.audit.enabled). If you have explicitly disabled auditing, requesting a periodic XP board throws MetricRequiresAuditingException rather than returning a silently empty board. Boards without a period are unaffected: the all-time board keeps reading the cheap experiences.experience_points column and never scans the ledger. Periodic achievements and challenges boards read their own timestamps and work with or without auditing.

Only metrics that implement the LevelUp\Experience\Contracts\Windowable interface support periods. The built-in flow metrics — xp, achievements, and challenges — all do: xp windows on audit rows, achievements on when each achievement was earned, and challenges on when each challenge was completed. level and streak are state metrics — a current level or streak count isn't "earned within a window" — so selecting a period for them throws MetricNotWindowableException.

[!NOTE] setPoints() is an administrative override, not earned activity — it writes no audit record, so it deliberately never moves a periodic board. The all-time board sees the new total immediately.

Week boundaries and timezones are configurable under level-up.leaderboard:

week_starts_on sets which day Period::Week starts on. timezone controls the timezone period boundaries (start of day/week/month) are computed in — useful when your app stores UTC but your users' "today" starts at midnight local time.

Friends boards and custom populations

The package never owns your social graph — you supply who, it supplies the ranking. restrictTo() takes a closure that narrows the board's base user query to any population you can express as a query constraint:

Ranks are computed within the restricted set, not filtered down from the global board: a user who is rank 40 globally but ahead of all their friends is rank 1 on their friends board. The restriction composes with everything else — by(), period()/since(), forTier(), rankOf(), and around() — in any order:

Friends boards are the headline use, but any host-defined population works the same way — users in a guild, an organisation, a tournament bracket.

Named Boards

Everything above composes an ad-hoc query: you build it fluently, execute it, and it's forgotten. A Board is different — a declared leaderboard, registered by name in the config as a metric/period(/tier) combination:

Each declaration takes a required metric (a registry key from level-up.leaderboard.metrics), an optional period ('day', 'week', or 'month'), and an optional tier (a tier name). Resolve a Board by name with board() — it returns the same fluent query, pre-composed, so every refinement still works on top:

Declarations are validated loudly at resolution rather than producing a silently wrong board: an unknown board name throws BoardNotFoundException, a missing or unknown metric throws MetricNotFoundException, a period declared for a non-Windowable metric (such as level) throws MetricNotWindowableException, an invalid period value throws a ValueError, and a tier name with no matching tier throws a ModelNotFoundException.

Why declare a board instead of just querying? Boards are the leaderboards the package tracks over time — snapshots, rank-change events, and leagues apply only to declared Boards. Ad-hoc queries stay exactly what they are: composed, executed, forgotten. Declare no boards and none of that machinery activates.

Snapshots and rank events

A Snapshot is the leaderboard's memory: a persisted record of a Board's top entries at a point in time. Diffing consecutive snapshots produces rank deltas — "you climbed from #5 to #2" — which a stateless query can never compute. The level-up:snapshot-boards command writes one snapshot run per declared Board, diffs it against the previous run, dispatches rank events, and prunes old runs.

Schedule the command from your application — the package never auto-registers scheduler entries:

Each run stores the top tracked depth entries per Board — track_top in the board declaration, default 100:

Diffing two runs dispatches three events, each carrying the board name:

Event Properties When
LeaderboardRankChanged Model $user, string $board, int $from, int $to A user moved rank within the tracked depth
UserEnteredTrackedDepth Model $user, string $board, int $rank A user broke into the tracked depth
UserLeftTrackedDepth Model $user, string $board, int $previousRank A user dropped out of the tracked depth

Below the tracked depth a Board is silent by design: no snapshot rows, no events. "You dropped from #6,389 to #6,412" is not a thing the package emits — raise track_top if you want deeper tracking and are happy to own the cost. Ties use the same competition semantics as live queries, so a tie breaking only events the users whose rank number actually changed.

Two semantics worth knowing:

Old runs are pruned by the same command per level-up.leaderboard.snapshots.retention_days (default 30):

[!NOTE] Snapshots are not a cache for live queries — rankOf() and around() always compute fresh, for any user at any depth. Snapshots exist solely to remember past runs so rank deltas can be evented.

Custom metrics

Rank by anything you can express as a SQL score: implement LevelUp\Experience\Contracts\RankingMetric — a stable key(), a label(), an enabled() check, a constrain() that scopes the user query to eligible users, and a scoreExpression() subquery yielding one numeric score per user — then register the class in level-up.leaderboard.metrics.

To support time periods too, also implement LevelUp\Experience\Contracts\Windowable — a windowedScoreExpression($start, $end) subquery yielding one numeric score per user for activity between the two timestamps ($end may be null for an open-ended since() range).

Leagues

A League is a competitive cycle built on one periodic Board: each period, active users are grouped into small Cohorts within a Division, and ranked against their cohort-mates only. Declare it in config by binding a Board and a ladder of Divisions, ordered bottom to top:

Leave board as null (the default) and the league machinery stays dormant. The configuration is validated loudly: binding a Board that isn't declared throws BoardNotFoundException, binding a Board without a period throws LeagueBoardNotPeriodicException (a league is a cycle — an all-time board cannot host one), and declaring a league with no divisions throws LeagueDivisionsNotDeclaredException. Each division's promote and relegate counts are consumed by the period rollover.

A Division is not a Tier

The two ladders look similar but answer different questions. A Tier is status: a pure function of your current XP, recalculated whenever your points change. A Division is competition history: you hold it because of where you placed in last period's cohort, regardless of what your XP is today. A user holds a Tier and competes in a Division simultaneously and independently — adding leagues changes nothing about HasTiers, tier columns, or tier events. It's perfectly normal (Duolingo-style) for an app to show a permanent Gold tier badge while the user grinds through the Silver division this week.

Lazy enrollment

Nobody is pre-assigned. A user joins the current period's league on their first score-earning action of the period (the PointsIncreased event path) — they're placed into the open cohort of their Division, cohorts fill in arrival order, and a new cohort opens when one reaches cohort_size. That means:

The division ladder rows are seeded automatically from config the first time they're needed.

User API

Add the HasLeagues trait to your user model:

cohortStandings() runs the league's Board restricted to the user's cohort-mates, so scores and ranks use the Board's own metric and period — rank 1 means first in the cohort, not globally. It returns an empty collection for users not in a cohort (and when no league is configured).

Rollover: promotion and relegation

The level-up:league-rollover command closes out finished periods: for every Cohort of the closed period it computes the final standings live (same metric, same window — not from snapshots) and moves users along the ladder. Schedule it from your application to run just after the period boundary — the package never auto-registers scheduler entries:

Within each cohort, movement follows the Division's configured counts:

Cohort finish Movement
Top promote finishers Move up one Division
Bottom relegate finishers Move down one Division
Everyone else Stay put

The semantics in detail:

Each movement dispatches a single event — mirroring the tier event grammar, with a direction enum instead of separate promoted/relegated classes:

Event Properties When
UserDivisionChanged Model $user, string $board, Division $previousDivision, Division $newDivision, DivisionDirection $direction A rollover moved the user up (DivisionDirection::Promoted) or down (DivisionDirection::Relegated) the ladder

Non-movers and ghosts dispatch nothing.

The divisions, cohorts, and cohort_user tables ship as package migrations — re-publish migrations and migrate when upgrading.

Recipes: what the package doesn't build

The package owns the ranking logic — scores, ranks, ties, periods, snapshots, leagues. Display and app-specific queries belong to your application, and some "leaderboard" needs don't need leaderboard machinery at all. A few patterns:

Users ordered by raw XP. If you just want users sorted by points — no rank numbers, no tie semantics, no time windows — one orderByDesc on the experiences table does it:

Top-N boards are one-liners. When you do want ranks and ties, compose metrics and periods instead of writing queries:

Percentile / "top 10%" display. Derive it from a rank and a total count — the package supplies the rank; your app defines the population:

A friends board. Your app owns the social graph; pass it through restrictTo() and ranks are computed within the friend set — not filtered down from the global board.

The package ships no UI — Blade views, Livewire components, and API resources for displaying any of this are deliberately your job.

🔍 Auditing

Auditing keeps track each time a User gains points, levels up and what level to. It is enabled by default (since v3) because periodic leaderboards source their scores from the audit ledger — set AUDIT_POINTS=false (or level-up.audit.enabled) to turn it off if you don't need point history or time-windowed boards.

The type and reason fields will be populated automatically based on the action taken, but you can overwrite these when adding points to a User

[!NOTE] Auditing happens when the addPoints and deductPoints methods are called. Auditing must be enabled in the config file.

View a Users’ Audit Experience

🔥 Streaks

With the Streaks feature, you can track and motivate user engagement by monitoring consecutive daily activities. Whether it's logging in, completing tasks, or any other daily activity, maintaining streaks encourages users to stay active and engaged.

Streaks are controlled in a Trait, so only use the trait if you want to use this feature. Add the Trait to you User model

Activities

Use the Activies model to add new activities that you want to track. Here’s some examples:

Record a Streak

This will increment the streak count for the User on this activity. An `Event is ran on increment.

Break a Streak

Streaks can be broken, both automatically and manually. This puts the count back to 1 to start again. An Event is ran when a streak is broken.

For example, if your streak has had a successful run of 5 days, but a day is skipped and you run the activity on day 7, the streak will be broken and reset back to 1. Currently, this happens automatically.

Reset a Streak

You can reset a streak manually if you desire. If level-up.archive_streak_history.enabled is true, the streak history will be recorded.

Archive Streak Histories

Streaks are recorded, or “archived” by default. When a streak is broken, a record of the streak is recorded. A Model is supplied to use this data.

Get Current Streak Count

See the streak count for an activity for a User

Check User Streak Activity

Check if the User has performed a streak for the day

Events

StreakIncreased - If an activity happens on a day after the previous day, the streak is increased.

StreakBroken - When a streak is broken and the counter is reset.

🥶 Streak Freezing

Streaks can be frozen, which means they will not be broken if a day is skipped. This is useful for when you want to allow users to take a break from an activity without losing their streak.

The freeze duration is a configurable option in the config file.

Freeze a Streak

Fetch the activity you want to freeze and pass it to the freezeStreak method. A second parameter can be passed to set the duration of the freeze. The default is 1 day (as set in the config)

A StreakFrozen Event is ran when a streak is frozen.

Unfreeze a Streak

The opposite of freezing a streak is unfreezing it. This will allow the streak to be broken again.

A StreakUnfrozen Event is run when a streak is unfrozen.

Check if a Streak is Frozen

Events

StreakFrozen - When a streak is frozen.

StreakUnfrozen - When a streak is unfrozen.

🏅 Tiers

Tiers provide named status brackets based on experience points — think Bronze, Silver, Gold, Platinum. Unlike levels (which are numeric progression), tiers represent status and can integrate with multipliers, achievements, streaks, and leaderboards.

Add the HasTiers trait to your User model:

Define Tiers

The metadata column is a flexible JSON field — store whatever you need (colours, icons, descriptions).

Query Tiers

Demotion

By default, tiers use a high-water mark — once a user reaches Gold, they stay Gold even if points decrease. To enable demotion (tier drops when points drop):

Tier-Scoped Multipliers

You can create multipliers that only apply to Users in specific tiers using scopeToTier() (see the Multipliers section above):

Tier-Gated Achievements

Restrict achievements to users who have reached a certain tier:

Attempting to grant this achievement to a user below Gold will throw TierRequirementNotMet.

Tier-Scaled Streak Freezes

Higher tiers can get longer freeze durations:

Tier-Scoped Leaderboards

Filter leaderboards by tier:

Events

UserTierUpdated — When a user's tier changes (promotion or demotion).

🎯 Challenges

Challenges are multi-condition goals that users can enroll in and complete for rewards. Think "Earn 100 XP and reach Level 5 to unlock a bonus." Challenges support auto-enrollment, time windows, repeatable completion, and custom condition logic.

Add the HasChallenges trait to your User model:

Creating Challenges

[!NOTE] Conditions and rewards are validated on creation. Invalid types or missing required keys will throw an InvalidArgumentException.

Condition Types

Type Required Keys What it checks
points_earned amount Points earned since enrollment
level_reached level User's current level >= target
achievement_earned achievement_id User has the achievement
streak_count activity, count Current streak count for the activity
tier_reached tier User is at or above the named tier
leaderboard_rank board, rank User's rank on the named Board is at or above the target
custom class Your own class implementing ChallengeCondition

Leaderboard Rank Conditions

The leaderboard_rank condition is "finish top N on a named Board" — it is met when the user's rank on the Board, as recorded by the latest snapshot run, is at or above the target (rank <= N):

[!IMPORTANT] This condition only progresses when level-up:snapshot-boards runs — schedule it (see Snapshots and rank events). Progress is evaluated on the rank events the snapshot run dispatches, and the rank is read from the run's snapshot rows, so a board that is never snapshotted never satisfies the condition.

Validation on challenge creation is strict, so a misconfigured condition fails loudly instead of silently never completing:

Reward Types

Type Required Keys What happens
points amount Adds XP to the user
achievement achievement_id Grants an achievement

Enrolling Users

Auto-enroll — Set auto_enroll to true on the challenge. Users are enrolled automatically when a relevant event fires (e.g. earning points, levelling up). Enrollment starts the clock: "earn 100 points" means 100 more points from the moment of enrollment, not total lifetime points.

Manual enroll:

Manual enrollment throws if the challenge hasn't started yet, has expired, or the user is already enrolled.

Unenroll:

Throws if the user is not enrolled, or if the challenge is already completed.

Querying Progress

Custom Conditions

Implement the ChallengeCondition contract for your own logic:

Then reference it in your challenge conditions:

Repeatable Challenges

Set is_repeatable to true. When all conditions are met, rewards are dispatched, then the challenge resets with a fresh baseline. The user can complete it again.

Every completion — repeatable or not — is recorded as a row in the challenge_completions ledger, exposed as the challengeCompletions() relation on the user. A non-repeatable challenge contributes a single row; a repeatable challenge contributes one row per completion. This ledger is the source of truth for the challenges leaderboard metric, so repeated completions correctly increase a user's score and periodic challenge boards window on each completion's completed_at. The completedChallenges relation stays distinct — a challenge the user has finished appears there once, however many times it has been repeated.

The challenge_completions table ships as a package migration — re-publish migrations and migrate when upgrading. The migration backfills one row per already-completed challenge, so existing completions are counted from the start.

Events

ChallengeCompleted — When all conditions are met and rewards are dispatched.

ChallengeEnrolled — When a user enrolls in a challenge (manual or auto).

ChallengeUnenrolled — When a user unenrolls from a challenge.

Configuration

Challenges are enabled by default. To disable:

Customizing Identifiers

By default, the package's tables use auto-incrementing bigint primary keys. Set level-up.entities.id_type to uuid or ulid if you want package IDs to be opaque — useful when exposing Experience or Achievement records on a public API without leaking row counts.

This applies to every package primary key (experiences, levels, achievements, streaks, tiers, multipliers, challenges, and the pivot tables) and every internal foreign key between them. One set of columns is intentionally unaffected:

[!IMPORTANT] This setting is for fresh installs. Existing installs cannot be flipped automatically — column types are baked in at migration time. The accordion below contains an AI prompt that generates the conversion migrations for your specific schema.

AI prompt: convert an existing install to uuid or ulid Paste this into your AI assistant. Replace `` with `uuid` or `ulid` and `` with `postgres`, `mysql`, or `sqlite`. Review the generated migration carefully against your schema and data volume before running it on production.

Testing

Changelog

Please see CHANGELOG for more information on what has changed recently.

License

The MIT Licence (MIT). Please see Licence File for more information.


All versions of level-up with dependencies

PHP Build Version
Package Version
Requires php Version ^8.3
illuminate/support Version ^12.6|^13.0
spatie/laravel-package-tools Version ^1.15
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package cjmellor/level-up contains the following files

Loading the files please wait ...