Dans ce tutoriel nous allons découvrir comment mettre en place un système de déblocage de badge sur Laravel. Le déblocage d'un badge peut se faire à différents endroits dans notre application (création d'un commentaire, abonnement…) et on ne peut pas se permettre de mettre notre code un peu partout dans notre application. Le meilleur moyen d'organiser ce système est l'utilisation des évènements.

  • Lors d'une action sur le site nous allons émettre un évènement en lui passant le sujet sur lequel il est lancé.
  • On crée ensuite un écouteur qui va s'abonner à ces différents évènements et débloquer le badge suviant différentes condition.

Les évènements

Les évènements permettent une meilleur séparation du code en séparant la logique métier dans un écouteur. Laravel utilise déjà ce système en interne avec Eloquent par exemple mais vous pouvez aussi émettre vos propres évènements.

// Un évènement peut être une simple chaine de caractère, ou une simple classe
<?php
namespace App\Events;

use App\User;

class PremiumEvent {

    /**
     * Représente l'utilisateur qui a souscrit à un compte premium
     *
     * @var User
     */
    public $user;

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

}
// On peut ensuite émettre l'évènement depuis un controller par exemple

On peut ensuite émettre l'évènement à l'aide du helper event().

event(new PremiumEvent($user))

Ecouteurs

Une fois les évènements créés, il faut pouvoir écouter le déclenchement de ces derniers. On peut écouter ponctuellement en utilisant la façade Event

Event::listen(PremiumEvent::class, function ($event) {
  $user = $event->user; 
  // On peut alors écrire notre logique ici
});

Cette méthode peut convenir dans certains cas spécifiques mais, dans notre cas, on souhaite grouper la logique à un seul endroit. Pour cela on peut créer un Subscriber.

<?php
namespace Badge;

use Badge\Notifications\BadgeUnlocked;

class BadgeSubscriber {

    /**
     * @var Badge
     */
    private $badge;

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

    /**
     * Register the listeners for the subscriber.
     *
     * @param  Illuminate\Events\Dispatcher  $events
     */
    public function subscribe($events)
    {
        $events->listen('eloquent.created: App\Comment', [$this, 'onNewComment']);
        $events->listen('App\Events\Premium', [$this, 'onPremium']);
    }

    public function onNewComment($comment) {
        $user = $comment->user;
        $comments_count = $user->comments()->count();
        $badge = $this->badge->unlockActionFor($user, 'comments', $comments_count);
    }

    public function onPremium($event) {
        $badge = $this->badge->unlockActionFor($event->user, 'premium');
    }
}

Un subscriber est une simple classe qui dispose d'une méthode subscribe() qui va lancer l'écoute sur un ensemble d'évènements. On peut enfin ajouter ce Subscriber dans notre EventServiceProvider.

<?php

namespace App\Providers;

use Badge\BadgeSubscriber;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;

class EventServiceProvider extends ServiceProvider {
    protected $listen = [];
    protected $subscribe = [
        BadgeSubscriber::class
    ];
}

Structure des badges

Pour le déblocage des badges nous allons concevoir la table avec les champs suivants.

Schema::create('badges', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name'); // contient le nom / titre du badge
    $table->string('action'); // Catégorise l'action qui va déclencher le badge.
    // Représente le nombre de fois que l'action doit être effectuée pour débloquer le badge
    $table->integer('action_count')->unsigned(); 
});
Schema::create('badge_user', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('badge_id')->unsigned();
    $table->foreign('badge_id')->references('id')->on('badges')->onDelete('cascade');
    $table->integer('user_id')->unsigned();
    $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
    $table->timestamps();
});

Et on crée une table pivot afin de retenir les badges débloqués par un utilisateur.

Pour simplifier la logique de notre Subscriber nous allons concentrer la logique de déblocage dans le model Badge.

<?php

namespace Badge;

use App\User;
use Illuminate\Database\Eloquent\Model;

class Badge extends Model
{
    public $guarded = [];
    public $timestamps = false;

    // On crée un model intermédiaire pour représenter la table pivot
    public function unlocks () {
        return $this->hasMany(BadgeUnlock::class);
    }

    /**
     * Est-ce que l'utilisateur a déjà débloqué le badge ?
     * 
     * @param User $user
     * @return bool
     */
    public function isUnlockedFor(User $user): bool {
        return $this->unlocks()
            ->where('user_id', $user->id)
            ->exists();
    }

    /**
     * Débloque le badge correspondant à l'action pour l'utilisateur
     *
     * @param User $user
     * @param string $action
     * @param int $count
     */
    public function unlockActionFor(User $user, string $action, int $count = 0) {
        $badge = $this->newQuery()
            ->where('action', $action)
            ->where('action_count', $count)
            ->first();
        if ($badge && !$badge->isUnlockedFor($user)) {
            $user->badges()->attach($badge);
            return $badge;
        }
        return null;
    }
}

Notifications

Enfin, on souhaite notifier les utilisateurs lorsqu'ils débloquent des badges. Nous allons nous reposer pour cela sur le système de notification proposé par Laravel.

<?php

namespace Badge\Notifications;

use Badge\Badge;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class BadgeUnlocked extends Notification
{
    use Queueable;

    private $badge;

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

    public function via($notifiable) {
        return ['mail', 'database'];
    }

    public function toMail($notifiable) {
        return (new MailMessage)
                    ->line('Vous avez débloqué la badge ' . $this->badge->name)
                    ->line('Bravo !');
    }

    public function toArray($notifiable) {
        return [
            'name' => $this->badge->name
        ];
    }

    public static function toText($data) {
        return "Vous avez débloqué le badge " . $data['name'];
    }
}

Gràce à cette classe il sera alors très simple d'informer un utilisateur du déblocage.

$user->notify(new BadgeUnlocked($badge));

Il est possible d'améliorer ce système de notification en ajoutant d'autres méthodes d'envoi gràce à la méthode via(). Dans notre cas nous envoyons un email et on stocke la notification en base de données.

Le stockage en base nous permet de les afficher plus tard au niveau de notre vue.

<li class="dropdown">
    <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
        <span class="glyphicon glyphicon-bell"></span>
        <span class="badge badge-danger">{{ Auth::user()->unreadNotifications->count() }}</span>
    </a>
    <ul class="dropdown-menu list-group">
        @foreach(Auth::user()->unreadNotifications as $notification)
            <a href="{{ route('notifications.show', ['id' => $notification->id]) }}">
                {{ ($notification->type)::toText($notification->data) }}
            </a>
        @endforeach
    </ul>
</li>