Code Monkey home page Code Monkey logo

laravel-achievements's Introduction

Laravel Achievements Logo

Build Status Total Downloads License

An implementation of an Achievement System in Laravel, inspired by Laravel's Notification system.

Table of Contents

  1. Requirements
  2. Installation
  3. Creating Achievements
  4. Unlocking Achievements
  5. Adding Progress
  6. Retrieving Achievements
  7. Event Listeners
  8. License

Requirements

  • Laravel 5.3 or higher
  • PHP 5.6 or higher

Installation

Default installation is via Composer.

composer require gstt/laravel-achievements

Add the Service Provider to your config/app file in the providers section.

'providers' => [
    ...
    Gstt\Achievements\AchievementsServiceProvider::class,

Backup your database and run the migrations in order to setup the required tables on the database.

php artisan migrate

Creating Achievements

Similar to Laravel's implementation of Notifications, each Achievement is represented by a single class (typically stored in the app\Achievements directory.) This directory will be created automatically for you when you run the make:achievement command.

php artisan make:achievement UserMadeAPost

This command will put a fresh Achievement in your app/Achievements directory with only has two properties defined: name and description. You should change the default values for these properties to something that better explains what the Achievement is and how to unlock it. When you're done, it should look like this:

<?php

namespace App\Achievements;

use Gstt\Achievements\Achievement;

class UserMadeAPost extends Achievement
{
    /*
     * The achievement name
     */
    public $name = "Post Created";

    /*
     * A small description for the achievement
     */
    public $description = "Congratulations! You have made your first post!";
}

Unlocking Achievements

Achievements can be unlocked by using the Achiever trait.

<?php

namespace App;

use Gstt\Achievements\Achiever;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Achiever;
}

This trait contains the unlock method, that can be used to unlock an Achievement. The unlock method expects an Achievement instance:

use App\Achievements\UserMadeAPost

$user->unlock(new UserMadeAPost());

Remember that you're not restricted to the User class. You may add the Achiever trait to any entity that could unlock Achievements.

Adding Progress

Instead of directly unlocking an achievement, you can add a progress to it. For example, you may have an achievement UserMade10Posts and you want to keep track of how the user is progressing on this Achievement.

In order to do that, you must set an additional parameter on your UserMade10Posts class, called $points:

<?php

namespace App\Achievements;

use Gstt\Achievements\Achievement;

class UserMade10Posts extends Achievement
{
    /*
     * The achievement name
     */
    public $name = "10 Posts Created";

    /*
     * A small description for the achievement
     */
    public $description = "Wow! You have already created 10 posts!";
    
    /*
     * The amount of "points" this user need to obtain in order to complete this achievement
     */
    public $points = 10;
}

You may now control the progress by the methods addProgress and removeProgress on the Achiever trait. Both methods expect an Achievement instance and an amount of points to add or remove:

use App\Achievements\UserMade10Posts;

$user->addProgress(new UserMade10Posts(), 1); // Adds 1 point of progress to the UserMade10Posts achievement

In addition, you can use the methods resetProgress to set the progress back to 0 and setProgress to set it to a specified amount of points:

use App\Achievements\FiveConsecutiveSRanks;

if($rank != 'S'){
    $user->resetProgress(new FiveConsecutiveSRanks());
} else {
    $user->addProgress(new FiveConsecutiveSRanks(), 1);
}
use App\Achievements\Have1000GoldOnTheBag;

$user->setProgress(new Have100GoldOnTheBag(), $user->amountOfGoldOnTheBag);

Once an Achievement reach the defined amount of points, it will be automatically unlocked.

Retrieving Achievements

The Achiever trait also adds a convenient relationship to the entity implementing it: achievements(). You can use it to retrieve progress for all achievements the entity has interacted with. Since achievements() is a relationship, you can use it as a QueryBuilder to filter data even further.

$achievements   = $user->achievements;
$unlocked_today = $user->achievements()->where('unlocked_at', '>=', Carbon::yesterday())->get();

You can also search for a specific achievement using the achievementStatus() method.

$details = $user->achievementStatus(new UserMade10Posts());

There are also three additional helpers on the Achiever trait: lockedAchievements(), inProgressAchievements() and unlockedAchievements().

Event Listeners

Listening to all Achievements

Laravel Achievements provides two events that can be listened to in order to provide "Achievement Unlocked" messages or similar. Both events receive the instance of AchievementProgress that triggered them.

The Gstt\Achievements\Event\Progress event triggers whenever an Achiever makes progress, but doesn't unlock an Achievement. The Gstt\Achievements\Event\Unlocked event triggers whenever an Achiever actually unlocks an achievement.

Details on how to listen to those events are explained on Laravel's Event documentation.

Listening to specific Achievements

The event listeners mentioned above triggers for all Achievements. If you would like to add an event listener for only a specific Achievement, you can do so by implementing the methods whenUnlocked or whenProgress on the Achievement class.

<?php

namespace App\Achievements;

use Gstt\Achievements\Achievement;

class UserMade50Posts extends Achievement
{
    /*
     * The achievement name
     */
    public $name = "50 Posts Created";

    /*
     * A small description for the achievement
     */
    public $description = "Wow! You have already created 50 posts!";
    
    /*
     * The amount of "points" this user need to obtain in order to complete this achievement
     */
    public $points = 50;
    
    /*
     * Triggers whenever an Achiever makes progress on this achievement
    */
    public function whenProgress($progress)
    {
        
    }
    
    /*
     * Triggers whenever an Achiever unlocks this achievement
    */
    public function whenUnlocked($progress)
    {
        
    }
}

License

Laravel Achievements is open-sourced software licensed under the MIT license.

laravel-achievements's People

Contributors

alexgodbehere avatar butschster avatar gabs-simon avatar silverqx avatar wall-e-psr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-achievements's Issues

[Question] Cron Job for achivement progress?

Several Achievements use some DB data for their progress, how can I update the point progress of the achievements?

I was thinking on a cron job every X time that checks the DB and sets the progress. How can I implement something like that in Laravel?

Thank you, great job!

how to unlock the achievement within a month

Currently i am working on new project and i have to create Made 20 appointments in one month achievement. So how can i set the unlocked_at field in achievement_progress table of one month timestamp from current date of making an appointment

Implement onUnlock handler based on Laravel event system

Currently, the Achievements have an undocumented (and untested) functionality: They can implement a onUnlock method, receiving an instance of AchievementProgress, that will be called whenever this specific Achievement is unlocked.

This is a bad idea because it does not implement default Laravel conventions. This should be replaced with an implementation that calls Laravel Events.

uuid on progress_details table

Hi

Im trying to understand why the id of table achievement_progress uses a unique identifier:

Uuid::uuid4()->toString();

Wouldnt a regular incremented integer suffice?

Thanks

Achievement Categories

Make it so Achievements can be categorized.
Create methods to show only achievements of a specific category.

Documentation Improvement

Hello to all users of the laravel-achievements package!

I'm currently writing a new version of the documentation and would like to hear feedback from everyone.

Please consider leaving a comment letting me know about:

  • How did you like the current README.md file? Is it clear? Is it beginner-friendly?
  • Did you experience any problems while using this package that the documentation wasn't clear enough?
  • How did you like the examples provided in the current documentation?
  • Do you have anything else to add?

Thank you very much!

Laravel 6 support?

@gabriel-simonetti
Thanks for this awesome library.

Any hope for Laravel 6 support? Thanks

Multiple Models as Achiever

I have two models that I'd like to earn achievements on, is that possible? I'm getting unexpected behavior.

ModelOne

use Gstt\Achievements\Achiever;
use Illuminate\Database\Eloquent\Model;

class ModelOne extends Model
{
    use Achiever;
}

ModelTwo

use Gstt\Achievements\Achiever;
use Illuminate\Database\Eloquent\Model;

class ModelTwo extends Model
{
    use Achiever;
}

Achievements are working as expected for ModelOne, but ModelTwo is not. This, for example, is getting wrongly saved as being ModelOne...

$model = ModelTwo::first();
$model->unlockIfLocked(new ThisAchievement());

And the achievements relationship is screwy, as well. App\ModelTwo::has('achievements')->first() does this...


Illuminate/Database/QueryException with message 'SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'achiever_id' cannot be null (SQL: insert into `achievement_progress` (`achievement_id`, `achiever_id`, `achiever_type`, `points`, `id`, `updated_at`, `created_at`) values (1, , App/ModelTwo, 0, d319c898-482c-4833-bc3d-a2c588ba1545, 2018-08-23 12:13:28, 2018-08-23 12:13:28))'

It's possible that it's related to my custom trait that I made:

namespace App\Traits;

use Gstt\Achievements\Achievement;

trait Achievable
{
    /**
     * Unlock If Locked
     * 
     * @param Achievement $achievement
     * @return void
     */
    public function unlockIfLocked(Achievement $achievement)
    {
        if(! $this->hasUnlocked($achievement))
        {
            $this->unlock($achievement);
        }
    }
}

Unlocked Achievements Count (SUGGESTION)

built in unlocked Achievements counter so can be easily displayed in master/default view. Maybe like so:

<?php $count = Auth::user()->unlockedAchievementsCount(); ?>

with will in return display the count of all the unlocked Achievements that user has?

Samples Views/Controller?

Possible to add some samples that will work with this package out the box so noobs like me have a base starting point?

Unlock/Progress Events

Hello. How am I able to use sweetalert2 for these events?I would like a popup like this when progress or a unlock is made.

https://codepen.io/anon/pen/yXXGPy

Is this possible and if not what to you recommend for alerts? I use toastr for my sites alerts but wanted something little pronounced for achievement alerts.

Thanks.

Class 'Gstt\Achievements\AchievementsServiceProvider' not found

Hi,

when i run composer update i got the following error:

  [Symfony\Component\Debug\Exception\FatalThrowableError]
  Class 'Gstt\Achievements\AchievementsServiceProvider' not found

Script php artisan optimize handling the post-update-cmd event returned with error code 1

composer.json
...
"gstt/laravel-achievements": "^0.0.3",
...

config/app.php
...
Gstt\Achievements\AchievementsServiceProvider::class,
...

Laravel Framework 5.4.15

Looks like php artisan optimize crashed

Locking

I am looking for a way to lock achievement. Is is possible?

Achievement description

Hi again,

I've seen there is only a $description property which seems, according to the examples, to be used to display a message to the user when they reach the achievement.
This is misleading, IMHO, since the description should describe (sic) the achievement and, therefore, be displayed in the list of achievements, so that users know what they have to do to reach the achievement.
The $description should then describe the achievement, and a $message property should be displayed when the user unlocks the achievement.

Since the property is public, we can obviously add another property to achieve this. But the examples in the documentation are not very explicit about the contexts.

Also, since the package is designed for Laravel, what about adding localization? Rather than using description property, we could have a getDescription() method which would return the description translated (or not, depending on configuration). Also, a getMessage() method would display the message.
A getter would allow developer to add some logic before returning the message/description (maybe you want to return the text with a different vocabulary, for example for adults or children users).

I'd be happy to submit proposals and PRs after discussing this.

getOrCreateProgressForAchiever ignores achiever?

Problem:
Only for user A a database record was created in the "achievement_progress" table.

$progress = AchievementProgress::where('achiever_type', $className)
->where('achievement_id', $achievementId)
->where('achiever_id', $achiever->id) // i think you forgot this line?
->first();

Inefficient use of database

I saw that achievement generation is quite inefficient when it comes to database usage.
The culprit is getModel method that is called 3 times for each achievement progress modification:

  • in constructor
  • in getOrCreateProgressForAchiever to fetch achievement id
  • in getOrCreateProgressForAchiever to associate newly created progress with details model

Then in this getModel method we have 2 database calls: read and save
That makes 6 database calls (8 if there is an update) for each achievement update.

My suggestion would be to make a dedicated command for synchronizing achievement details with database and remove save call from getModel as well as getModel call from constructor (possibly as a config option for backword compatibility) .

An extra improvement would be to reuse results of getModel instead of calling it each time.

Another suggestion would be to add an option for having stable ids
(eg. calculated from md5 of achievement fully qualified class name or just provided by user)
and assume that those will match model's id,
but that is something to be discussed
and since its a super breaking change, definitely require a config option.

Add Achievements (SUGGESTION)

Is there a way down the road to make logic/frontend so one can create/edit achievements on the fly which could be integrated into a existing admin panel or something. That way staff of site could add new Achievements from site and not have to use terminal and edit code in like sublime text?

I know this is long shot and probably not easy todo with how the Achievements system works now but thought i would throw the idea out there as it would make it more user friendly.

Locked Achievements (SUGGESTION)

A way to display a list of all achievements that are locked so users can see what they have yet to accomplish.

something like so:
http://imgur.com/tJf5DQE

Progress for Locked Achievements would always be Locked by Default. As once its not locked its removed from locked list and added to Completed Achievements list

Achievement Chain

Sometimes, there are multiple achievements that track the exact same thing, but with different points to obtain.

For example, an user can obtain a "Newcomer" achievement when creating their first comment on the site, a "Regular Commenter" achievement when they reached 10 comments and a "Regular Commenter" achievement when they reached 100 comments.

Currently, these achievements need to be tracked separately, something like this:

<?php
use App\Achievements\Newcomer;
use App\Achievements\RegularCommenter;
use App\Achievements\VeteranCommenter;

public function createComment($user, $comment){
     // Some logic to be processed when the user creates a comment...
    $user->addProgress(Newcomer, 1);
    $user->addProgress(RegularCommenter, 1);
    $user->addProgress(VeteranCommenter, 1);
}

And this would quickly grow out of hand when there are many achievements tracking the same thing. In order to solve this problem, I propose a feature called "Achievement Chain". It would work in the following way:

  1. A new abstract class, AchievementChain will be created. This class will be defined only by a list of achievements.
  2. All methods that can be used to add, remove, set reset progress to Achievements can also receive instances of AchievementChain. When one of this methods receive an instance of AchievementChain, it will call the same method for every Achievement described in the definition of the AchievementChain instance.

Unit Testing and Travis CI

In order to create stable releases, laravel-achievements will require testing and continuous integration.
A simple way to achieve that is to implement PHPUnit tests and Travis CI.

Reset Specific Achievement

I'm wondering if there is a way to edit an item in the achievement_progress table specifically? The problem that I have is that I need to reset the number of points back down to 0 if a user gets something wrong in whatever unlocked achievement they currently are working on.

If I just go into the DB table itself and edit the specific row with the code below, I'm able to reset the points, but it also loads every single achievement into the achievement_progress table for the user, and I don't want to clutter this table up with achievements that aren't valid for some time.

        $progress = $this->inProgressAchievements()->first();
        if($progress) {
            $progress->points = 0;
            $progress->save()
        }

This sends all possible achievements to the achievement_progress table, even though it does reset the achievement in progress.

Gamerscore implementation

Gamerscore is a feature used on Xbox Live tied to the Achievement System.

Achievers are rewarded with a specified amount of points (usually a number between 5 and 100 which reflects how hard the achievement is to obtain) whenever they unlock an Achievement. The score is added to the Achiever's Gamerscore, defined as a sum of all points obtained upon unlocking Achievements.

Past Timestamps / Pass a Timestamp

Is there a way to pass a timestamp to the achievements? I'm trying to add achievements to an existing site and would like to pass a timestamp, so the achievements are recorded as being earned when the old activity happened.

Command "make:achievement" is not defined.

Hi, I have recently installed the package, I when I tried to run the following command, the terminal return mi the next error:
domain@hl361:~/www$ php artisan make:achievement UserMadeAPost
Command "make:achievement" is not defined.
Did you mean one of these?
make:auth
make:chart
make:command
make:controller
make:event
make:exception
make:factory
make:job
make:listener
make:mail
make:middleware
make:migration
make:model
make:notification
make:policy
make:provider
make:request
make:resource
make:rule
make:seeder
make:test

Multilanguage Achiefment

How do i make the achiefment multilanguage? I see the achiefment as somethign that is saved in the database, but how do i translate this achiefment?

[REQUEST] Improve returned data

When doing a query for user achievements and unlocked ones, the returned data is not pretty optimized. (Personal point of view).

Example of $user->unlockedAchievements()->all():

Array ( [0] => Gstt\Achievements\Model\AchievementProgress Object ( [incrementing] => [table:protected] => achievement_progress [guarded:protected] => Array ( ) [casts:protected] => Array ( [unlocked_at] => datetime ) [connection:protected] => mysql [primaryKey:protected] => id [keyType:protected] => int [with:protected] => Array ( ) [withCount:protected] => Array ( ) [perPage:protected] => 15 [exists] => 1 [wasRecentlyCreated] => [attributes:protected] => Array ( [id] => 77352394-32ec-4122-a970-dc2614ae51f6 [achievement_id] => 4 [achiever_id] => 1 [achiever_type] => App\User [points] => 0 [unlocked_at] => 2018-02-21 19:02:54 [created_at] => 2018-03-02 13:32:00 [updated_at] => 2018-03-02 13:32:00 ) [original:protected] => Array ( [id] => 77352394-32ec-4122-a970-dc2614ae51f6 [achievement_id] => 4 [achiever_id] => 1 [achiever_type] => App\User [points] => 0 [unlocked_at] => 2018-02-21 19:02:54 [created_at] => 2018-03-02 13:32:00 [updated_at] => 2018-03-02 13:32:00 ) [changes:protected] => Array ( ) [dates:protected] => Array ( ) [dateFormat:protected] => [appends:protected] => Array ( ) [dispatchesEvents:protected] => Array ( ) [observables:protected] => Array ( ) [relations:protected] => Array ( ) [touches:protected] => Array ( ) [timestamps] => 1 [hidden:protected] => Array ( ) [visible:protected] => Array ( ) [fillable:protected] => Array ( ) ) [1] => Gstt\Achievements\Model\AchievementProgress Object ( [incrementing] => [table:protected] => achievement_progress [guarded:protected] => Array ( ) [casts:protected] => Array ( [unlocked_at] => datetime ) [connection:protected] => mysql [primaryKey:protected] => id [keyType:protected] => int [with:protected] => Array ( ) [withCount:protected] => Array ( ) [perPage:protected] => 15 [exists] => 1 [wasRecentlyCreated] => [attributes:protected] => Array ( [id] => 95d4e3c1-64f2-4649-be50-fa81d90e98fc [achievement_id] => 1 [achiever_id] => 1 [achiever_type] => App\User [points] => 1 [unlocked_at] => 2018-02-21 19:02:54 [created_at] => 2018-02-21 19:02:54 [updated_at] => 2018-02-21 19:02:54 ) [original:protected] => Array ( [id] => 95d4e3c1-64f2-4649-be50-fa81d90e98fc [achievement_id] => 1 [achiever_id] => 1 [achiever_type] => App\User [points] => 1 [unlocked_at] => 2018-02-21 19:02:54 [created_at] => 2018-02-21 19:02:54 [updated_at] => 2018-02-21 19:02:54 ) [changes:protected] => Array ( ) [dates:protected] => Array ( ) [dateFormat:protected] => [appends:protected] => Array ( ) [dispatchesEvents:protected] => Array ( ) [observables:protected] => Array ( ) [relations:protected] => Array ( ) [touches:protected] => Array ( ) [timestamps] => 1 [hidden:protected] => Array ( ) [visible:protected] => Array ( ) [fillable:protected] => Array ( ) ) )

Is really all that data necessary for just 2 achievements? Could be refactored on simpler and more usefull way?

Achievement and permission

Hi,

I'm about using this package in a new project and I have one suggestion: what about granting permissions when some achievements are reached?
For example, on StackOverflow, you need to have at least 125 reputation to downvote, or 1500 to create tags.
I guess it's rather easy to set it up with a policy, but maybe it could be made with the achievements api?

Choose which database the tables reside in

Is there a way to configure the used database for the achievements (details and progress)?

I use a system containing multiple databases, and the achievement tables should not necessarily reside in the default database.

I got a working version by directly editing the source code, but this would not be future proof, as newer versions would just overwrite it.

Support

Hi Gabriel,
is there another way to get in contact with you?
BR
Jerry

Images

Is there an expected or suggested way to include images to represent the badges? My solution at present is basically

<img src="{{ asset('img/badges/'.str_replace(' ', '_', $achievement->details->name). '.png') }}">

But maybe there's a better way? If so, it seems worth mentioning it in the docs, or maybe including a few different examples of tactics for associating images with your badges. :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.