简体   繁体   中英

CakePHP 2.x Achievements

I'm attempting to build an achievements system in my CakePHP app using CakeEvents. I've been using the following website for helping me put the Events together: http://martinbean.co.uk/blog/2013/11/22/getting-to-grips-with-cakephps-events-system/

In my app, achievements are called badges and can be awarded to users. These badges are awarded based on rules that link the badge to a Cake event.

So for example if a user creates a post, that will fire the Model.Post.add event which should check if any rules exist for that event, and if so do the parameters match up, and again if all checks out then award a badge connected to that rule to the user.

The schema comprises of the following tables:

users

  • id
  • username
  • password

posts

  • id
  • title
  • content
  • created
  • modified
  • user_id

badges

  • id
  • title
  • description

events

  • id
  • title
  • name

event_badges

  • id
  • event_id
  • badge_id

badge_users

  • id
  • badge_id
  • user_id

Hopefully that all makes sense. And here are the Models to show how they connect.

User.php

class User extends AppModel
{
    public $name = 'User';

    public $hasMany = array(
        'Post', 'Badge'
    );

    public $belongsTo = array(
        'BadgeUser'
    );

}

Badge.php

class Badge extends AppModel {

    public $hasMany = array(
        'BadgeUser'
    );

    public $actsAs = array('Containable');

}

Event.php

class Event extends AppModel {

    public $hasMany = array(
        'EventBadge'
    );

    public $actsAs = array('Containable');

}

EventBadge.php

class ActionBadge extends AppModel {

    public $belongsTo = array(
        'Action', 'Badge'
    );

    public $actsAs = array('Containable');

}

BadgeUser.php

class BadgeUser extends AppModel {

    public $belongsTo = array(
        'Badge', 'User'
    );

    public $actsAs = array('Containable');

}

** Feel free to comment if you think the schema is incorrect to achieve what is being described in this question.**

So example badges might be:

  • Title: First Post, Description: Awarded for creating a post
  • Title: 10 Posts, Description: Awarded for creating 10 posts
  • Title: Editor, Description: Editing a post
  • Title: Deleter, Description: Deleting a post

And some example rules in the Events table:

  • Title: Add Post, Event Model.Post.add
  • Title: Edit Post, Event: Model.Post.edit
  • Title: Delete Post, Event: Model.Post.delete
  • Title: View Post, Event: Model.Post.view

So as you can see the Events are linked to the above Badges and the Events are called using the CakeEvents system.

Okay so when a person does something, let's say saves a new post, I have the following in the Post model:

public function afterSave($created, $options = array()) {
    if ($created) {
        $event = new CakeEvent('Model.Post.add', $this, array(
            'id' => $this->id,
            'data' => $this->data[$this->alias]
        ));
        $this->getEventManager()->dispatch($event);
    }
}

The first question is, how do I pass different events to the afterSave? As both Add and Edit methods in the controller would fire this...

And then I have a PostListener.php file in /app/Event

class PostListener implements CakeEventListener {

    public function implementedEvents() {

        return array(
            'Model.Post.add' => 'postAdded',
            'Model.Post.edit' => 'postEdited',
            'Model.Post.view' => 'postViewed',
            'Model.Post.delete' => 'postDeleted',
        );

    }

    public function postAdded(CakeEvent $event) {

         $this->Badge->awardBadge($badgeId, $userId);  
    }

    public function postEdited(CakeEvent $event) {


    }

    public function postViewed(CakeEvent $event) {


    }

    public function postDeleted(CakeEvent $event) {


    }
}

So the next question is how do I link the event listener back up to my Events table?

And then award the badge connected to that action? Noting that some will need to do extra checks like a user must of created 10 posts to achieve the 10 Posts badged and not just because they have created a post.

In the Badge.php model I have the following function to award badges:

public function awardBadge($badgeId, $userId) {

    $controlFind = $this->BadgeUser->find(
        'first',
        array(
            'conditions' => array(
                'badge_id' => $badgeId,
                'user_id' => $userId,
            )
        )
    );

    if(!$controlFind) {

        $temp = array(
            'BadgeUser' => array(
                'badge_id' => $badgeId,
                'user_id' => $userId,
                'created' => date('Y-m-d H:i:s')
            )
        );

        $collection[] = $temp;

        return $this->BadgeUser->saveAll($collection, array('validate' => false));

    }

}

So I need to run the code above from the listener when things match up with the DB rules. Just struggling to make it all stick together.

I think your database schema may be complicating things by having a notion of events. Personally, I'd have a badges table that stores badge title and description (and also image path if it's needed). I'd then have an event listener that corresponds to each badge, so there will be some degree of manual labour involved.

So let's think of a sample scenario. Let's say a badge is awarded when a user posts a 100 times. So you'd have your Post model that fires an event when a post is saved:

<?php
App::uses('CakeEvent', 'Event');

class Post extends AppModel {

    public function afterSave($created, $options = array()) {
        $event = new CakeEvent('Model.User.afterSave', $this);
        $this->getEventManager()->dispatch($event);
    }
}

You can then create a corresponding handler:

<?php
// Event/BadgeListener.php
App::uses('CakeEventListener', 'Event');
App::uses('ClassRegistry', 'Utility');

class BadgeListener implements CakeEventListener {

    public function implementedEvents() {
        return array(
            'Model.Post.afterSave' => 'afterSaveListener'
        );
    }

    public function afterSaveListener(CakeEvent $event) {
        // check number of posts
        $this->Post = ClassRegistry::init('Post');
        $count = $this->Post->find('count', array(
            'Post.author_id' => $event->subject()->data[$event->subject()->alias]['author_id'];
        ));
        // award badge
        if ($count > 100) {
            // TODO: check user does not already have badge
            $this->BadgeUser = ClassRegistry::init('BadgeUser');
            $this->BadgeUser->create();
            $this->BadgeUser->set('badge_id', 'whatever_badge_id_actually_is');
            $this->BadgeUser->set('user_id', $this->Auth->user('id')); // logged in user ID
            $this->BadgeUser->save();
        }
    }
}

This is a rough example written off-the-cuff, but hopefully it should steer you in the right direction.

If there's anything you want me to clear up, let me know and I'll do my best.

The first question - differentiate between new post and edit

To differentiate between add and edit you need to pass along the $created parameter when you create your CakeEvent. Something like this:

public function afterSave($created, $options = array()) {
        //NOTICE: it's afterSave now, since this will be called after edit or new
        $event = new CakeEvent('Model.Post.afterSave', $this, array(
            'created' => $created, //NOW you will know when you process the even if this was a new post or post edit
            'id' => $this->id,
            'data' => $this->data[$this->alias]
        ));
        $this->getEventManager()->dispatch($event);
}

You should add to the data of your event all the data that you will need later on to process it and to match your rules. You can even add the model object itself, if you think you will need that to do some queries with it.

Second Question - how to link event processing to the Events table

In the Event listener you can use the ClassRegistry to get an instance of the Event model (and any other models you require). You can do your rules matching and once you have all the data you need you save the badge to the database.

Since you now know if it's an edit or a new post (from the event->data) you can take appropriate action.

The editor id I guess you can take it with the Auth component as being the logged in user. So when you have that you can use it to read what you need about the editor from the database.

To conclude: when you send your events, use the event data to pass all the information that you will need in the listener. Event can carry so much more information, not just their names and the fact that they were triggered . You can send and then look at the entire "context".

In the listener, make good use of the data you get from the event and of the currently logged in user ID or the post author, do the matching that you need to do and then save the badge for the appropriate user.

Hopefully this is what you were looking for :).

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM