Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Achievements Laravel Package

assada/laravel-achievements

Add achievements and badges to your Laravel app with a simple, database-backed API. Define achievements, track user progress, unlock rewards automatically, and listen for unlock events with clean, customizable Eloquent models.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is small — a trait, two Eloquent models, one migration set, and an artisan generator. Read vendor/assada/laravel-achievements/src/ before anything else; the README is thin and the trait (Achiever.php) plus AchievementProgress.php are the real API reference.

composer require assada/laravel-achievements
php artisan vendor:publish --provider="Assada\Achievements\AchievementServiceProvider"  # verify FQCN in vendor/composer/installed.json
php artisan migrate

The publish step gives you config + migrations. Open the migration file first and note the exact column names (progress, achieved_at/unlocked_at, the user foreign key) — forks of this package disagree on naming, and every snippet below depends on them.

Add the trait to your authenticatable model:

use Assada\Achievements\Achiever; // confirm namespace in the vendor source — this is a fork of gstt/laravel-achievements

class User extends Authenticatable
{
    use Achiever;
}

Achievements are classes, not rows. Each achievement extends the package model and declares its metadata as public properties; a database row is created lazily the first time progress is recorded against it.

namespace App\Achievements;

use Assada\Achievements\Achievement;

class FirstPost extends Achievement
{
    public $name        = 'First Post';
    public $description = 'You published your first post.';
    public $points      = 1;   // acts as the progress threshold / weight — check the trait source
}

First real use case — award on the event that causes it:

// app/Observers/PostObserver.php
public function created(Post $post): void
{
    $post->user->addProgress(new FirstPost, 1);

    if ($post->user->hasAchievement(new FirstPost)) {
        // first unlock — notify, broadcast, log
    }
}

Register the observer in a service provider and you have a working system in under an hour.

Implementation Patterns

Two ways to write progress — pick deliberately.

  • addProgress($achievement, $n) — incremental. Right for event-driven counters ("posted a comment", "logged in"). Not idempotent; retries double-count.
  • setProgress($achievement, $n) — absolute. Right when the truth is computable from your own tables ($user->posts()->count()). Idempotent, so it doubles as your repair/backfill mechanism.

A robust setup uses both: addProgress on the hot path, setProgress in a nightly command that recomputes from source-of-truth queries. Drift is inevitable in any counter system; make reconciliation cheap.

Wrap the trait, don't sprinkle it. The package gives you no domain events, so build the seam yourself:

final class AchievementAwarder
{
    public function progress(User $user, string $achievementClass, int $amount = 1): void
    {
        DB::transaction(fn () => $user->addProgress(new $achievementClass, $amount));

        if ($user->hasAchievement(new $achievementClass)) {
            AchievementUnlocked::dispatch($user, $achievementClass); // after commit
        }
    }
}

Every call site goes through this. It gives you one place to add logging, queues, analytics, and — critically — one place to swap the package out later.

Keep a registry of achievement classes. You need the full list to render "locked vs unlocked vs in-progress", and you can't get it from the DB (unstarted achievements have no row).

// config/achievements.php  (your app config, distinct from the package's)
return ['classes' => [
    \App\Achievements\FirstPost::class,
    \App\Achievements\FiftyPosts::class,
]];
$board = collect(config('achievements.classes'))->map(function (string $class) use ($user) {
    $achievement = new $class;
    $progress    = $user->getProgress($achievement);   // null if never started

    return [
        'name'        => $achievement->name,
        'description' => $achievement->description,
        'current'     => optional($progress)->progress ?? 0,
        'target'      => $achievement->points,
        'unlocked'    => (bool) optional($progress)->achieved_at,
    ];
});

Retroactive awards for existing users. The package only sees progress going forward. Ship a one-off achievements:backfill command that iterates users in chunks and calls setProgress — otherwise your first 10,000 users see an empty achievement page on launch day.

User::query()->chunkById(500, function ($users) {
    foreach ($users as $user) {
        $user->setProgress(new FiftyPosts, $user->posts()->count());
    }
});

Display and API. $user->achievements is the relation of unlocked achievements — eager-load it ($user->load('achievements')) when listing users, and cache your registry list in a config/singleton rather than scanning the filesystem per request.

Testing. No factories ship with the package. Use RefreshDatabase, call addProgress/setProgress directly in tests, and assert on getProgress(...)->progress and hasAchievement(...) rather than on raw table rows.

Gotchas and Tips

It is explicitly abandoned. The description says so; the 2025 release date just means the tag was pushed, not that it's maintained. Treat the Laravel version constraint as a hard ceiling — check composer.json in the vendor directory before planning an upgrade. If this is core to your product, plan to fork it into your own namespace (it's a few hundred lines) and own it. If it's peripheral, the wrapper in "Implementation Patterns" is what makes replacing it a one-file change.

Namespace shock. This is a fork of gstt/laravel-achievements; forks like this frequently keep the original Gstt\Achievements namespace for drop-in compatibility. Confirm with grep namespace vendor/assada/laravel-achievements/src/*.php before pasting any README snippet, or you'll spend twenty minutes on "class not found".

The class name is the primary key of identity. The package persists a reference to the achievement class, so renaming or moving App\Achievements\FirstPost silently orphans every user's progress. Decide on the namespace on day one, and if you must rename, write a migration that updates the stored identifier in the same deploy.

Public properties and DB rows can drift. $name and $description live in code and in a row created on first progress. Editing the description in the class won't necessarily update existing rows. If descriptions are user-visible and you tweak them, add a small command that syncs metadata, or accept the drift.

Race conditions are real. addProgress is a read-modify-write. Two concurrent requests (double-clicked like button, parallel queue workers) can lose an increment or create duplicate progress rows. Mitigate with a unique index on (user_id, achievement_id) in your own migration, and wrap sensitive awards in a transaction with lockForUpdate.

Notifications inside the transaction. If you dispatch a "you earned X" notification from a model observer, the job may fire before the transaction commits and read stale state. Use DB::afterCommit() or ShouldQueueAfterCommit.

Don't put logic in achievement classes. They're Eloquent models and get hydrated frequently; keep them to metadata. Thresholds that depend on business rules belong in the awarder service, not in an accessor.

Hardcoded user key, no polymorphic relation. The progress table is tied to a users foreign key. Multi-tenant, team-based, or "achievements for organisations" requirements will fight this schema — you'll be adding a column and patching queries rather than configuring it.

Debugging checklist when progress "doesn't stick": (1) is the trait on the model, (2) does the class extend the package Achievement (not Model), (3) is the row appearing in the progress table with the expected achievement_id, (4) is achieved_at set but your hasAchievement check happening before the write commits. Tinker the three trait calls in sequence — addProgress, getProgress, hasAchievement — and the fault localises immediately.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin