Event Listening and Handling?

Alright, so if any of you know stack overflow, I’m looking to incorporate a new “achievement/medal” system that they have.

Stack overflow allows users to ask questions and others to answer them and there are various “achievements” that you can unlock (for example, ask 10 questions or answer 10 questions or other ones such as filling out your profile fully or verifying your email even).

When you complete or unlock an achievement or “medal” as they’re called, a little popup comes up (looks like a nice little jQuery popup or something).

I was wondering how I’d go about this in PHP/Javascript. I imagine that it’s not suitable to have it check how many posts you have made each time you post something to see if you should unlock the achievement - instead I’m looking for an “event listening” type event or a script I could make that I could setup a CRON job for.

Has anyone got any idea of what I’m trying to say? If so, I’d love to hear how you’d go about it.

Just add up the flags to each user on each action, and then on each action/page load check those stats vs the required stats of your achievements, and show the ones that didn’t show before.

My only worry about that was if I have too many achievements and a lot of users that the system will become boggy and slow. Surely there’s a better way to do it then to make it check against the requirements on each user action/page load.

You have to check the requirements when a) you want to display the “achievement”, or b) when you achieve that “achievement”.

a)

  • on each page load, you check all achievement requirements vs the user stats, and show the ones that matched up.

b)

  • on each action you check all achievement requirements vs the user stats, and add the ones that matched up to a queue.
  • when you load a page, you show everything in that queue.

What you can do to speed it up, is a smart way to check the achievements, some kind of tree search or something. (so if you have 1000 achievements, you just check 5-10 on each action).

If server load is your concern, then some kind of caching, and a smart search algorithm / look-up table will solve your problem.

Hmm, if that’s the case I think the best way to do it would be to make a standalone script that I could setup a CRON job for every hour or something that would check for achievements.

Here is how I would do it for a high volume website:

  1. Map appropriate “actions” to the possible achievements. For example, profile form submit would be mapped to email verification achievement and avatar load achievement.
  2. When a user does something (possibly performing several logical actions per single form submit) the system takes notes of all the possible achievements in the list A.
  3. When the transaction is completed, the system runs a two-step achievement check routine where:
    a. Each achievement from the list A gathers data in a common cache C. If the necessary info is already there, no action is taken.
    b. Each achievement performs the check to see whether it’s fulfilled using the information from C. If that’s the case, there is a message generated and stored to be shown on the next page load.

This is a decoupled algorithm that can be implemented purely though hooks/plugins. To speed it up, I would add the necessary information to cache C during the action itself. (I.e. instead of doing an extra query just for seeing whether the user has avatar, you would simply get this info from the routine that adds avatars.) This may or may not be necessary depending on how you deal with your data.

Thanks for your reply. That certainly sounds better.

Would you mind helping me out by providing a bit of sample code so I could understand what you mean a little better.

It’s very difficult to write quality sample code, especially for a fairly generic approach like I’ve described above. Sloppy sample code follows:

<?php
class RankGenerator{

    //maps user action ids to related ranks (i.e. rewards)
    //probably should be initialized from the database
    static protected $action2rank = array( 
        'UserProfile.update' => array('avatar', 'biography'),
        //...
    );
    
    static protected $possibleRanks = array();
    
    static function markRelevantRanks($actionId){
        $rankNames = $action2rank[$actionId];
        $rankObjects = array();
        
        foreach ($rankNames as $name) {
            $this->possibleRanks[] = somehowLoadRank($name);
        }
        return $rankObjects;
    }
    
    static function checkForNewRanks(){
        foreach ($this->possibleRanks as $rank) {
            $rank->gatherInfo();
        }
        
        foreach ($this->possibleRanks as $rank) {
            if ($rank->isNewlyAchieved()) {
                generateMessage();
            }
        }
    }
}

abstract class Rank{
    protected static $cache = array();
    
    abstract function gatherInfo(){
    }
    
    abstract function isNewslyAchieved(){
    }
}

class AvatarRank extends Rank{
    function gatherInfo(){
        if (!isset(Rank::$cache['userProfile'])) {
            Rank::$cache['userProfile'] = getCurrentUserProfile();
        }
    }
    
    function isNewlyAchieved(){
        return isset(Rank::$cache['userProfile']['avatarPath']) 
        && !isset(Rank::$cache['userProfile']['ranksAchieved']['avatar']);
    }
}

class BiographyRank extends Rank{
    function gatherInfo(){
        if (!isset(Rank::$cache['userProfile'])) {
            Rank::$cache['userProfile'] = getCurrentUserProfile();
        }
    }
    
    function isNewlyAchieved(){
        return isset(Rank::$cache['userProfile']['biographyText']) 
        && !isset(Rank::$cache['userProfile']['ranksAchieved']['biography']);
    }
}

class UsrProfile extends SomeController{
    function update(){
        //handle updating itself
        
        //...
        
        /* The following would probably be in a plugin, hook function or whatever, 
        but I'm putting it here for illustration purposes. */
        RankGenerator::markRelevantRanks(__CLASS__.'.'.__METHOD__);
    }
}

//and this is run whenever all normal (user-requested) activity is done:
RankGenerator::checkForNewRanks();

So, basically, first you run your action (UserProfile#update) and, possibly, some other actions as well. Hooks trigger RankGenerator::markRelevantRanks(…) after each such action. The system takes note of all the ranks it needs to check. When user-requested activity is settled, you run a method that checks whether the user got any new ranks. Please not that it only deals with the ranks that could have been realistically achieved beforehand, rather than checking for all ranks in existence. Also note that even in this example, caching rank-related info saves you one database query to get current user’s profile. That is, unless the same info is cached at some other part of your system already.

After you’ve gathered all your info, you just run a check to see whether each rank is newly achieved.

Again, it’s a sloppy example, but it would take a lot of time to cook up something cleaner. Besides, you can do this in a number of ways, depending on the structure of your website.

I’ve explored your code extensively and it’s fairly easy on the server.
Now, I’m running my current project with CodeIgniter and I was hoping you could help me with integrating this system into that. There might even be some CI functions to do a lot of this.