Laravel Achievements

Documentation and Reference / Version 1.1


Introduction

What is Laravel Achievements?

Laravel Achievements is an implementation of an Achievement System in Laravel. It's designed to provide to Laravel developers a simple and yet robust gamification system for their website.

What are Achievements?

In game design parlance, an Achievement - also known as a Trophy or a Badge - is a meta goal, an objective for a player to accomplish that is scoped outside of the default objectives of the game. [reference]

As a gamification tool, Achievements allow you to reward your Users for accomplishing tasks within your website.

Getting Started #back to top

Requirements

Other than the default Laravel requirements, Laravel Achievements has the following requirements and recommendations:

  • PHP 5.6 or higher (7.0 or higher recommended)
  • Laravel 5.3 or higher (5.6 or higher recommended)

Laravel Achievements also requires a working database connection and at least one Eloquent model to handle unlocking.


Installation

1) Run the following Composer command on the root of your Laravel application:

composer require assada/laravel-achievements

2) If you're using Laravel 5.3 or 5.4, add the Service Provider to your config/app.php file, under the providers section.
(if you're running Laravel 5.5 or higher, you can skip this step.)

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

3) Run the artisan publish command in order to get the migrations and config files set up in your system.

php artisan vendor:publish --provider="Assada\Achievements\AchievementsServiceProvider"

4) Backup your database and run the migrations in order to install all tables required by Laravel Achievements.

php artisan migrate

You can change the table names for the database setup before migrating.
Check the newly generated config/achievements.php file for more details on the table naming conventions.

Once that's done, you're ready to start creating your Achievement system!

Achievements #back to top

Introduction

In this section you will learn how to set up your Achievement system - how to create, assign to Entities, unlock, and manage user progress.


Registering an Achiever

Before creating Achievements, you must first tell your system which Entities are supposed to be able to unlock and progress in them. You can do that by adding the Achiever trait to any Model (usually the User model).

In our example, let's add the Achiever trait to our User model, so our Users can interact with Achievements.

namespace App;

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

class User extends Authenticatable
{
    use Achiever;

    ...
}
                                

Once you have your Achiever set up, it's time to create an Achievement!


Creating a new Achievement

Similar to Laravel's implementation of Notifications, each Achievement is represented by a single class, which are typically stored in the app\Achievements directory.

You can either create a new Achievement class manually, or use the helper Artisan command php artisan make:achievement in order to create a new Achievement.

php artisan make:achievement CreatedFirstComment

This command will put a fresh Achievement in your app/Achievements directory with only has three properties defined: name (a human-readable name for your Achievement), slug (a machine-readable name for your Achievement, with only lowercase letters, numbers and hyphens) and description (a text that tells how to unlock the Achievement). You should change the default values for these properties to match the Achievement.

In our example, we will be creating a new Achievement that will be unlocked when our User creates their first comment on our website. When you're done, it should look like this:

namespace App\Achievements;

use Assada\Achievements\Achievement;

class CreatedFirstComment extends Achievement
{
    /*
     * The achievement name
     */
    public $name = "Created your first comment";

    /*
     * A unique text identifier for your Achievement.
     * It should have only lowercase characters, numbers and hyphens.
     */
    public $slug = "created-first-comment";

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

That should be all you need for your first Achievement. Now you need to setup an unlock condition.


Unlocking an Achievement

Once you have setup the Achiever trait, all you need to do is call the helper method unlock() to any instance of an Achiever, by passing the Achievement class. as a parameter.

In our example, we will add the unlock condition to our CommentsController and unlock the achievement as soon as the first comment is published:

use App\Http\Controller;
use App\Http\Requests\CreateCommentRequest; // custom Request to handle validation
use App\Comment; // Eloquent Model for Comment
use App\Achievements\CreatedFirstComment; // The Achievement we have just created

class CommentsController extends Controller
{
	public function create(CreateCommentRequest $request)
	{
		$user = Auth::user();

		$comment = new Comment();
		$comment->fill($request->all());
		$user->comments()->save($comment);


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

		...
                        

And that should be enough!
Once your User create a Comment, they will have unlocked the CreatedFirstComment Achievement.

This example showcases two important features inherent to all Achievements:

  • An Achievement can only be unlocked once.
    After they're unlocked, they should not be locked again;
  • Any attempts to unlock an Achievement that has already been unlocked will simply be ignored.

Achievement Progression

Sometimes you don't actually want to unlock and Achievement right away; instead, your Users should slowly gain points until they have enough to unlock the Achievement.
For example, let's create an Achievement for when our user creates ten posts on our website.

Again, let's create the Achievement by running the make:achievement command and change the default properties:

php artisan make:achievement CreatedTenPosts
namespace App\Achievements;

use Assada\Achievements\Achievement;

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

    /*
     * A unique text identifier for your Achievement.
     * It should have only lowercase characters, numbers and hyphens.
     */
    public $slug = "created-ten-comments";

    /*
     * A small description for the achievement
     */
    public $description = "Wow! You have already created ten comments!";

    /*
     * The amount of "points" this user need to obtain in order to complete this achievement
     */
    public $points = 10;
}
                        

Notice that we're adding a new points property, which will tell Laravel Achievements how many points (in this case, comments) a user needs to have in order to unlock this Achievement.

Since we're not unlocking the achievement right away, we won't be calling unlock(). Instead, we will call another helper method that will automatically handles how many progress the user has - addProgress().
This is how the newly updated Controller should look like:

use App\Http\Controller;
use App\Http\Requests\CreateCommentRequest;
use App\Comment;
use App\Achievements\CreatedFirstComment;
use App\Achievements\CreatedTenComments;

class CommentsController extends Controller
{
	public function create(CreateCommentRequest $request)
	{
		$user = Auth::user();

		$comment = new Comment();
		$comment->fill($request->all());
		$user->comments()->save($comment);


		$user->unlock(new CreatedFirstComment());
        // Adds 1 point of progress to CreatedTenComments
        $user->addProgress(new CreatedTenComments(), 1);

		...
                        

And that's it - no further coding is required. Laravel Achievements will handle storing and counting the progress for you.

In this example, whenever an User creates a new post, that user will receive 1 point that will count towards the progress for the Achievement CreatedTenComments.
Once this progress has reached the amount defined on the $points property defined on the Achievement class, it will be automatically unlocked.

You can also remove progress points by using removeProgress(), set a user's progress to anything by using setProgress() and reset it to 0 points of progress by using resetProgress().

Let's see some more examples:

namespace App\Achievements;

use Assada\Achievements\Achievement;

class CreatedCommentWith100Characters extends Achievement
{
    public $name = "Created comment with 100 characters";
    public $slug = "created-comment-with-100-characters";
    public $description = "You have created a single comment with 100 characters or more";
    public $points = 10;
}
                        
namespace App\Achievements;

use Assada\Achievements\Achievement;

class Commented1000Characters extends Achievement
{
    public $name = "Commented 1000 characters";
    public $slug = "commented-1000-characters";
    public $description = "Across all your comments, you have commented 1000 characters or more";
    public $points = 10;
}
                        
namespace App\Achievements;

use Assada\Achievements\Achievement;

class CreatedFiveConsecutiveCommentsWithOver100Characters extends Achievement
{
    public $name = "Created five consecutive comments with 100 characters";
    public $slug = "created-five-consecutive-comments-with-100-characters";
    public $description = "You have created five consecutive comments with 100 characters or more";
    public $points = 10;
}
                        
use App\Http\Controller;
use App\Http\Requests\CreateCommentRequest;
use App\Comment;
use App\Achievements\CreatedFirstComment;
use App\Achievements\CreatedTenComments;
use App\Achievements\Commented1000Characters;
use App\Achievements\CreatedCommentWith100Characters;
use App\Achievements\CreatedFiveConsecutiveCommentsWithOver100Characters;

class CommentsController extends Controller
{
	public function create(CreateCommentRequest $request)
	{
		$user = Auth::user();

		$comment = new Comment();
		$comment->fill($request->all());
		$user->comments()->save($comment);
        $amountOfCharacters = strlen($comment->message);

		$user->unlock(new CreatedFirstComment());
        $user->addProgress(new CreatedTenComments(), 1);
        // Adds $amountOfCharacters progress points to Commented1000Characters
        $user->addProgress(new Commented1000Characters(), $amountOfCharacters);
        // When amount of characters is equal to or higher than 100,
        // CreatedCommentWith100Characters will be unlocked.
        $user->setProgress(new CreatedCommentWith100Characters, $amountOfCharacters);
        // Since this is counting consecutive comments with 100 characters,
        // if at least one comment does not have 100 characters, we need to reset the progress to 0.
        if(strlen($comment->message) >= 100){
            $user->addProgress(new FiveConsecutiveCommentsWithOver100Characters(), 1);
        } else {
            $user->resetProgress(new FiveConsecutiveCommentsWithOver100Characters());
        }

		...
                        

That should take care of most use cases of creating and unlocking Achievements. Now let's find out how to list Achievements from the database and get details about which Achievements an Entity has unlocked.

Retrieving from DB

The Achiever trait adds many helpful features to handle Achievement retrieval and listing.

You can retrieve all Achievement related information for a single Achiever by using$achiever->achievements(). It will return a list of all Achievements this user 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 with achievementStatus().

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

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

In order to retrieve all achievements, you can use the static method Assada\Achievements\Achievement::all().

Chains #back to top

Introduction

When you have multiple Achievements that are tracking the same stats, you might have a huge block of code in a specific part of your code. For example, if you have multiple achievements for making 10, 50, 100, 200, 500 and 1000 comments, you would need to add a single line for each one of these Achievement classes.

An Achievement Chain will help you solve this issue.


Creating a Chain

An AchievementChain is a helper class that defines a list of Achievements that track the same stats.

Similar to Achievements, you can either create a new AchievementChain class manually, or use the helper Artisan command php artisan make:achievement_chain in order to create a new AchievementChain.

php artisan make:achievement_chain TrackComments

This command will put a fresh AchievementChain in your app/Achievements directory with only a method defined: chain().


Handling a Chain

After creating the AchievementChain class, you should change the chain() method.
This method should return all achievements that belong to that chain, in the same order as they should be unlocked.

Using the original example - if you have Achievements that track 10, 50, 100, 200, 500 and 1000 comments, you might create your chain similar to this:

namespace App\Achievements\Chains;

use App\Achievements\FirstPost;
use App\Achievements\TenPosts;
use App\Achievements\FiftyPosts;

use Assada\Achievements\AchievementChain;

class CommentChain extends AchievementChain
{
    public function chain()
    {
        return [
            new Comments10(), new Comments50(), new Comments100(),
            new Comments200(), new Comments500(), new Comments1000()
        ];
    }
}
                        

After declaring this class, you can track it in the same way you would if it were an Achievement - you can use all progress methods directly to the chain.

use App\Http\Controller;
use App\Http\Requests\CreateCommentRequest;
use App\Comment;
use App\Achievements\Chains\CommentChain;

class CommentsController extends Controller
{
	public function create(CreateCommentRequest $request)
	{
		$user = Auth::user();

		$comment = new Comment();
		$comment->fill($request->all());
		$user->comments()->save($comment);

        $user->addProgress(new CommentChain(), 1);

		...
                        

Event Handling #back to top

Introduction

Laravel implements an efficient Event system that allows you to trigger and listen to events as they happen.

Laravel Achievements is fully compatible with the Event system and provides a few events that can be listened to.


Available Events

  • Assada\Achievements\Event\Progress: Triggers whenever an Achiever has their progress points in an Achievement changed.
  • Assada\Achievements\Event\Unlocked: Triggers whenever an Achiever has unlocked an Achievement.

Listening to Events

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


Specific Achievement Events

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.

use Assada\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)
    {

    }
}
                        

Frontend Display #back to top

Coming soon.

Core Concepts and Entities #back to top

Introduction

This section will contain more details about the classes, entities and concepts behind the Laravel Achievements package.


Achiever

An Achiever is any Eloquent Model within your system that is able to unlock and progress in Achievements. You can define that a Model is an Achiever by including the Assada\Achievements\Achiever trait to that model.

namespace App;

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

class User extends Authenticatable
{
    use Achiever;
}
                                

Remember that this is not limited to the User model; you can have as many Achievers as you want.


Achievement

An Achievement is a class that extends from the Assada\Achievements\Achievement class and describes what the Achievement is. It's designed to be implemented in a similar way to the Laravel Notifications package - for every Achievement you use in your system, you will need to declare a class for it.

An Achievement class looks like this:

namespace App\Achievements;

use Assada\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!";

    /*
     * The amount of "points" this user need to obtain in order to complete this achievement
     */
    public $points = 1;
}
                        

The following list describes all available attributes for Achievement class. Attributes with a * are required.

  • $name*: The name of the Achievement
  • $description*: A description that tells the user how to obtain the Achievement.
  • $points: The amount of "progression points" required to unlock this Achievement. Defaults to 1.
    Some Achievements are not simply locked or unlocked and need a progression storage (for example, an Achievement that tracks whether an User has made 10 comments to the website - in that case, $points should be 10).

For more details on the usage of Achievement, refer to the Achievements - Introduction part of this documentation.


AchievementDetail

An AchievementDetail is an Eloquent Model that is a direct representation of an Achievement class in the database. It contains the same attributes as the Achievement class and is used as an intermediary model in order to map the Achievement class to an Achiever model.

It also contains two helper methods:

  • progress() (which is also a HasMany relationship) returns all users that have made some kind of progress in this Achievement
  • unlocks() returns all users that have unlocked this Achievement.

AchievementProgress

An AchievementProgress is an Eloquent Model that describer a specific Achiever progress within a Achievement class. It contains the amount of points the Achiever has gained, and the unlock timestamp if it was already unlocked.

It also contains four helper methods:

  • achiever() (which is also a MorphTo relationship) returns the specific Achiever instance that this AchievementDetail belongs to.
  • details() returns the instance for the AchievementDetails entity that this AchievementProgress refers to.
  • isLocked() return false when this Achievement is unlocked.
  • isUnlocked() return true when this Achievement is unlocked.

Chain

A Chain is a class that extends from Assada\Achievements\AchievementChain and defines a sequence of Achievements that are tracking the same thing.

A Chain looks like this:

namespace App\Achievements\Chains;

use App\Achievements\FirstPost;
use App\Achievements\TenPosts;
use App\Achievements\FiftyPosts;

use Assada\Achievements\AchievementChain;

class PostChain extends AchievementChain
{
    public function chain()
    {
        return [new FirstPost(), new TenPosts(), new FiftyPosts()];
    }
}
                        

For more details on the usage of Chain, refer to the Chains - Introduction part of this documentation.

Copyright and License #back to top

Laravel Achievements is open-source software licensed under the MIT License.


COPYRIGHT 2017-2019 Gabriel Simonetti

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.