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 Gamify Laravel Package

qcod/laravel-gamify

Quickly add reputation points and badges to your Laravel app. Create point classes, award them to users, and unlock badges with an easy-to-use trait and migrations.

View on GitHub
Deep Wiki
Context7

Getting Started

qcod/laravel-gamify is a small, conventional Laravel package: one service provider, publishable config/migrations, a Gamify trait on your payee model, and generated PointType / BadgeType classes. Day-to-day, treat it as your reputation domain layer rather than scattering reputation++ calls through controllers. Current releases track modern Laravel versions, so it is not a legacy-only dependency.

Minimum install

composer require qcod/laravel-gamify

# Needed by the published migration that alters the users table on many Laravel versions
composer require doctrine/dbal

php artisan vendor:publish --provider="QCod\Gamify\GamifyServiceProvider" --tag="migrations"
php artisan vendor:publish --provider="QCod\Gamify\GamifyServiceProvider" --tag="config"
php artisan migrate

On Laravel 5.4 or lower, register QCod\Gamify\GamifyServiceProvider manually. On 5.5+ auto-discovery handles it.

Add the trait to your user/payee model:

use QCod\Gamify\Gamify;

class User extends Authenticatable
{
    use Gamify;
}

If you do not need badges, use QCod\Gamify\HasReputation instead of Gamify to avoid badge evaluation overhead.

First point type

php artisan gamify:point PostCreated

Edit app/Gamify/Points/PostCreated.php:

namespace App\Gamify\Points;

use QCod\Gamify\PointType;

class PostCreated extends PointType
{
    public $points = 20;

    protected $payee = 'user';

    public function __construct($subject)
    {
        $this->subject = $subject;
    }
}

First award

$post = $request->user()->posts()->create($validated);

$request->user()->givePoint(new PostCreated($post));

First undo

$request->user()->undoPoint(new PostCreated($post));
$post->delete();

Look first at:

  • config/gamify.php — payee model, badge levels, broadcast channel.
  • app/Gamify/Points and app/Gamify/Badges — your domain definitions.
  • The published migrations — especially the added reputation column on users.
  • vendor/qcod/laravel-gamify/src/PointType.php and BadgeType.php — base-class contracts.
  • Trait methods: givePoint, undoPoint, getPoints, addPoint, reducePoint, resetPoint.

A good first use case is deliberately small: award points when a post is created, show $user->getPoints() on the profile, and add badges only after the point flow is stable.

Implementation Patterns

Award from one boundary. For simple apps, awarding in the controller is fine. For anything with multiple entry points, use a model observer, domain event listener, or service so points are never missed.

namespace App\Observers;

use App\Gamify\Points\PostCreated;
use App\Models\Post;

class PostObserver
{
    public function created(Post $post): void
    {
        $post->user->givePoint(new PostCreated($post));
    }

    public function deleting(Post $post): void
    {
        $post->user->undoPoint(new PostCreated($post));
    }
}

Use deleting, not deleted, so the subject relationship is still available. Wrap award/undo logic in a transaction when the point and the domain write must stay consistent.

Configure the point type, not the call site. Most variation belongs in the PointType class:

  • Fixed value: public $points = 20;
  • Dynamic value: implement getPoints(): int
  • Simple payee: protected $payee = 'user';
  • Complex payee: override payee()
  • Conditional award: implement qualifier() and return a boolean
  • One-off awards: public $allowDuplicates = false;
class PostCreated extends PointType
{
    protected $payee = 'user';
    public $allowDuplicates = false;

    public function getPoints(): int
    {
        return $this->getSubject()->is_featured ? 50 : 20;
    }

    public function qualifier($user): bool
    {
        return $user !== null;
    }
}

Use the reputation log as an audit trail. $user->getPoints() returns the integer total; $user->getPoints(true) returns the formatted UI value such as 1K+. Do not use the formatted value for sorting or business logic.

History is available through $user->reputations. Each reputation exposes name, payee, point, and subject. To query points by subject, add a morph relation on the subject model:

public function reputations()
{
    return $this->morphMany(\QCod\Gamify\Reputation::class, 'subject');
}

Then $post->reputations gives you the points awarded on that post. For dashboards, query the reputation model directly, eager-load payee and subject, paginate, and group by point name. This table grows forever, so plan indexes and pagination early.

Badges are reactive. Generate with php artisan gamify:badge FirstContribution, then define the qualifier:

namespace App\Gamify\Badges;

use QCod\Gamify\BadgeType;

class FirstContribution extends BadgeType
{
    protected $description = 'Awarded for the first accepted contribution';
    protected $name = 'First Contribution';
    protected $icon = 'images/badges/first-contribution.svg';
    protected $level = 1;

    public function qualifier($user)
    {
        return $user->getPoints() >= 1000;
    }
}

The package evaluates badges when reputation changes and assigns those the user qualifies for. Read them via $user->badges. Badge names derive from the class name unless you set $name; icons and levels can also be set by property or getName() / getIcon() / getLevel() overrides. Clear the badge cache after adding, removing, or renaming badges:

php artisan cache:forget gamify.badges.all

If that command is unavailable on your Laravel version, use Cache::forget('gamify.badges.all') in Tinker or a deployment script.

Backfill badges explicitly. Because badges are evaluated on reputation change, users who already qualify before a badge is introduced will not receive it until their next point event. Write a one-off command that loops users, runs qualifier(), and attaches missing badges through your badge relation. The package does not ship a general backfill command.

Live updates. The package fires QCod\Gamify\Events\ReputationChanged with $user, $point, and $increment. It broadcasts on the configured channel prefix, by default user.reputation.{userId}. Listen on the frontend with Echo on that private channel, and ensure your queue worker is running if the event is queued. Private channels require broadcasting auth.

Simple counters without history. If you only need a reputation number and do not want the audit log:

$user->addPoint(5);
$user->reducePoint(2);
$user->resetPoint();

This bypasses PointType generation. It is useful for imports, migrations, or high-volume counters where an audit trail is not required. Verify whether your badge workflow still triggers on these methods before relying on them for badge awarding.

Test the types directly. Use RefreshDatabase, create a user and subject, call givePoint, then assert the aggregate and history:

$user = User::factory()->create();
$post = Post::factory()->for($user)->create();

$user->givePoint(new PostCreated($post));

$this->assertSame(20, $user->fresh()->getPoints());
$this->assertCount(1, $user->fresh()->reputations);

Also test duplicate prevention, undo behavior, and badge qualification. Disable model observers in tests if they would double-award points.

Custom models are possible. You can swap payee_model, reputation_model, and badge_model in config. Custom reputation and badge models should extend the package models and support the same relations/columns. Update the config before writing custom queries against the package tables.

Gotchas and Tips

  • Update payee_model for modern Laravel. The default config may point at \App\User, while Laravel 8+ commonly uses App\Models\User. If the payee model is wrong, point awarding and relations can fail or resolve the wrong class.
  • Run composer require doctrine/dbal before migrating. The published migration adds a reputation column to users. On many Laravel versions, the schema change needs Doctrine DBAL. If migration fails, install it and rerun, or hand-write the column migration.
  • Clear badge cache after badge changes. The package caches all badges under gamify.badges.all. Adding, removing, renaming, or changing a badge class without clearing this cache leads to confusing “badge not awarded” behavior.
  • Badges are not retroactive. They are evaluated on reputation change. New badges, imported users, or manually adjusted points may need a backfill command. This is the biggest operational gap to plan for.
  • Helpers default to the authenticated user. givePoint(new PostCreated($post)) uses auth()->user() as the payee. In jobs, commands, webhooks, impersonation, or admin actions, pass the payee explicitly or call $post->user->givePoint(...).
  • Use deleting, not deleted, for undo. Once the subject is gone, its relations and morph target may be unavailable. Undo the reputation first, then delete. Pass the original payee explicitly if an admin is performing the delete.
  • Duplicate control has two layers. The global allow_reputation_duplicate config and the per-point $allowDuplicates = false property both matter. Set them intentionally. Duplicate checks are generally tied to the point type and subject, so define one point class per logical award.
  • getPoints() must return an integer. Dynamic point calculations should not return floats or formatted strings. Keep formatting at the presentation layer.
  • A missing payee relation will break awards. protected $payee = 'user' assumes the subject has a user relation. For anonymous content, guest posts, or soft-deleted owners, use qualifier() to reject the award or override payee() with safe fallback logic.
  • Badge levels are stored as tinyints. Keep config values as small integers. If you rename a level later, existing badge rows still store the old numeric value; map labels in the UI rather than assuming the config name is persisted.
  • Badge icons behave differently when set on the class. A $icon property needs the full path with extension; config’s badge_icon_folder and badge_icon_extension are ignored in that case. Otherwise the config folder/extension are used.
  • Renaming a badge class can create a new logical badge. Names default to a humanized class name. If you rename the class, existing user_badges rows may still point at the old badge record. Plan a data migration if badge identity matters.
  • Reputation history grows quickly. Add indexes for payee and subject lookups, paginate history, and avoid loading full histories on every profile request. For high-traffic apps, consider a custom reputation model with extra snapshot columns for display.
  • Concurrent awards can race. Duplicate checks and reputation increments are separate operations unless you protect them. Wrap award/undo in a transaction and consider locking the user row for high-concurrency actions.
  • Broadcasting needs infrastructure. The reputation event broadcasts on a private channel. Configure broadcasting, channel authorization, and a queue worker if the event is queued. If you do not need live updates, you can ignore the broadcast side, but verify it does not fail in your environment.
  • Use HasReputation when badges are unnecessary. The full Gamify trait evaluates badges on reputation changes. If you only need points and history, the smaller trait avoids unnecessary queries and cache dependency.
  • Beware generated namespaces. Artisan generators assume App\Gamify\Points and App\Gamify\Badges. If your application namespace is not App, move the generated files and update namespaces manually.
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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator