I am building a function that will add a log of what my user has done into my database so that I can then build a activity feed for other users' activity feed.
What I imagine it to resemble so far.
Activity table
id | user_id | source | source_id | timestamp
Example Input
1 | 1 | photo_upload | 54 | 1333906150 //54 being the ID of the photo in my DB
2 | 1 | follow | 2 | 1333906159 // 2 being the id of a user
This way I can easily pull information from the database for a users activity, "user_id = {$followers} then show them according to what their source is e.g show the image for a photo_upload.
EDIT
What would be the best way to display different types of the feed.
This is what I currently have but it seems a bit much.
<?php
$grab = mysql_query("SELECT * FROM `activity` WHERE `user_id` IN (".$following.") ORDER BY `id` DESC");
while($row = mysql_fetch_array($grab)){
switch ($row['source']) {
case "photoupload":
// Display photo upload layout
break;
case "follow":
// Display follow layout
break;
}
}
?>
Does anyone see any ways to improve upon this?
Create an abstract class describing a feeditem type:
abstract class Application_Feed_Item_Abstract {
protected $_item;
public function __construct($itemData) {
$this->_item = $itemData;
}
abstract public function render();
}
Implement this class for different kinds of content:
class Application_Feed_Item_Photo extends Application_Feed_Item_Abstract {
public function render() {
//Custom rendering logic for a Photo
return '<div>Watch my photo</div>';
}
}
class Application_Feed_Item_Follow extends Application_Feed_Item_Abstract {
public function render() {
//Custom rendering logic for a Follower
return '<div>I got a new follower</div>';
}
}
So when you pull a record, you are able to create a renderer object on the fly depending on the kind of feeditem:
//Pull items from database and store them as an Array or other iterable object.
//Here i assume we call this collection of rows from the database $feeditems
//Mapping of source to renderer classes
$types = array(
"photoupload" => "Application_Feed_Item_Photo",
"follow" => "Application_Feed_Item_Request",
"somethingelse" => "Application_Feed_Item_Somethingelse"
);
//Process every item
foreach($feeditems as $item) {
$type = $item['source'];
if(!array_key_exists($type, $types)) {
throw new Exception("Unsupported feeditem type.");
}
$type = $types[$type];
$renderer = new $type($item);
echo $renderer->render();
}
Just a loose idea for a way to implement it. But as stated by another user, there are many different ways to do it, depending on your requirements. The method you used in your question would work just fine
There are many ways to skin a cat. Similarly there are many ways to achieve the same results you require.
Some suggestions for "improving" IMO would be -
1. Fetching this data as pre-generated HTML with an AJAX call.
Using an AJAX call once the page is loaded, you could have your server build the HTML segments of your users activity feed. This will allow your page to load much faster and your content will be loaded shortly after.
You don't even have to build HTML on the server - you could also pass a JSON object back from the AJAX call and build your HTML with JavaScript.
2. Creating a cached static HTML file of the users feed that is simply included into the markup.
You could have your server generate all the HTML needed for a users feed at a certain interval - every 30 minutes for example - and then save that HTML into a file to be included in the existing HTML. You can do this using PHP's include() function.
Using this method the activity feed will have a delay when refreshing so it might not be the best option - but if you make the interval smaller - every 5 minutes for example, you'll be saving a lot of calls to the DB.
Check out cron jobs - you could possibly use them to schedule the updates.
3. Place an index on the user_id field in your DB since you are using it to search by user.
This is a very important one especially if you are expecting many entries to exist in your DB. Placing an index on the fields that you use to search a table will vastly increase the performance of your queries.
I'm sure there are many more things that can be suggested here.
Related
I'm working on an application written in PHP. I decided to follow the MVC architecture.
However, as the code gets bigger and bigger, I realized that some code gets duplicated in some cases. Also, I'm still confused whether I should use static functions when quering the database or not.
Let's take an example on how I do it :
class User {
private id;
private name;
private age;
}
Now, inside this class I will write methods that operate on a single user instance (CRUD operations). On the other hand, I added general static functions to deal with multiple users like :
public static function getUsers()
The main problem that I'm facing is that I have to access fields through the results when I need to loop through users in my views. for example :
$users = User::getUsers();
// View
foreach($users as $user) {
echo $user['firstname'];
echo $user['lastname'];
}
I decided to do this because I didn't feel it's necessary to create a single user instance for all the users just to do some simple data processing like displaying their informations. But, what if I change the table fields names ? I have to go through all the code and change those fields, and this is what bothers me.
So my question is, how do you deal with database queries like that, and is it fine to use static functions when querying the database. And finally, where is it logical to store those "displaying" functions like the one I talked about ?
Your approach seems fine, howerver I would still use caching like memcached to cache values and then you can remove static.
public function getUsers() {
$users = $cacheObj->get('all_users');
if ($users === false) {
//use your query to grab users and set it to cache
$users = "FROM QUERY";
$cacheObj->set('all_users', $users);
}
return $users;
}
(M)odel (V)iew (C)ontroller is a great choice choice, but my advice is look at using a framework. The con is they can have a step learning curve, pro is it does a lot of heavy lifting. But if you want to proceed on your own fair play, it can be tough to do it yourself.
Location wise you have a choice because the model is not clearly define:
You'll hear the term "business logic" used, basically Model has everything baring views and the controllers. The controllers should be lean only moving data then returning it to the view.
You model houses DB interaction, data conversions, timezone changes, general day to day functions.
Moudle
/User
/Model
/DB or (Entities and Mapper)
/Utils
I use Zend and it uses table gateways for standard CRUD to avoid repetition.
Where you have the getUsers() method you just pass a array to it, and it becomes really reusable and you'd just have different arrays in various controller actions and it builds the queries for you from the array info.
Example:
$data = array ('id' => 26)
$userMapper->getUsers($data);
to get user 26
enter code here
$data = array ('active' => 1, 'name' => 'Darren')
$userMapper->getUsers($data);`
to get active users named Darren
I hope this help.
I am writing a small framework where my goal is to make it easy for my developers to set up a redirect to the details page of a resource after adding/inserting one.
Ordinarily this would be as easy as PDO::lastInsertID(), but I am using more than just the ID to identify the resource, I am also using a formatted version of the resource's name/title.
For example a new tournament might be called "Test Tournament" and therefore it's resource URI will be domain.com/tournament/328-test-tournament.
So when I redirect after inserting, I need to redirect to '328-test-tournament' and not just '328'. This is by design for URL integrity purposes. I don't want people accessing individual resources with mis-matched IDs and titles.
So that said, after I insert, I would like to be able to automatically return not just the ID, but the entire data set for what I entered, so that I can then format the title and redirect.
I could do this in the controller:
$this->TournamentModel->insert();
$id = PDO::lastInsertID();
$title = my_title_formatting_function($_POST['title']);
#header("Location: domain.com/tournament/{$id}-{$title}");
But I want a solution that's slightly more elegant like this:
$id = $this->TournamentModel->lastInsert();
Where lastInsert is actually a public method in the core model by retrieving not just the id of the last insert, but the entire row. I would then handle the title formatting and id concatenation right there.
Does something like this exist? Or at the very least is there a PDO method that returns the table that was inserted into so that I could construct a query using the table name and the id?
Check Doctrine or Propel.
They are two very famous Object Relational Mapper's, which have as base PDO.
ORM's can do what you asked, and have lots of other features you will enjoy, check it up.
Does a 'title' not belong to a Tournament object? i.e.
class TournamenentModel {
private $id;
private $title;
public function setTitle($title) {
$this->title = $title;
}
public function getTitleFormatted() {
return my_title_formatting_function($this->title);
}
}
in which case, your insert method might look something like
PDO::save(array('title' => $this->title));
and your workflow would be along the lines of:
$tournament = new TournamentModel();
$tournament->setTitle($_POST['title']);
$tournament->insert();
$title = $tournament->getTitleFormatted();
#header("Location: domain.com/tournament/{$id}-{$title}");
The title is persisted in memory until you release the object, there is no need to save and then retrieve it from a database?
So I'm wanting to setup an achievements system on my site. People perform tasks and upload this information which is then stored in a database (think 'time', 'date', 'task', etc.). What would be the best method of checking their information and awarding achievements? Would I just have like an achievement.php that once information is uploaded it would trigger this document to run through all the checks to determine if the user needs to be awarded an achievement? Or is there something server side I should set up to award the user?
Thanks for any help or suggestions, comments, etc. :D
EDIT: I currently have the achievements listed in the database, (id, name, class)
Tasks are stored as ('date_time','time','device','user_id[fk]')
EDIT 2: Also many achievements will be calculated based on not only the tasks the user is currently submitting but takes into account previous tasks in addition to the newly added task. EX: If the user has completed 3 tasks within 3 consecutive days, then they will be awarded for it
Your best bet is probably to create a table of point values for the tasks, and then create a stored procedure that can fetch the appropriate counts from the appropriate tables and multiply them by the point values. That's what I've done in the past - it allows you modify point values on the fly from the DB as well.
it really depends on where your preference for business logic placement lies, and how real time you want acheivements to be. if you're looking to offload a bunch of business logic on you sql server, put it in a stored procedure, otherwise, class out the calculations into a class in php, and use that class to determine what new achievements have been.
i would definitely suggest doing the processing outside of the normal page response. perhaps kick off a server-side call to the php cli, or set up a cron job to run all individuals through a check for achievements at a certain interval.
edit:
as for the actual methods of awarding achievements, i would think you're most flexible and simple implementation (you will find more simple/less flexible and more flexible/less simple options i'm sure) would be to create an AwardRunner class, an IAward interface and a bunch of individual implementations of IAward for each award you have. the basic idea would be something like:
<?php
class AwardRunner {
var $UserId = 0;
function AwardRunner($userId) {
$this->UserId = $userId;
$dir = "/path/to/your/folder/full/of/IAwards/";
$includes = read_dir($dir);
//include all files that exist
foreach($includes as $include)
{
if (is_file($include))
{
require($include);
}
}
}
public function Run() {
$classList = get_declared_classes();
foreach($classList as $key => $className)
{
if (in_array('IAward', class_implements($className))) {
$award = $className();
$award->UserId = $this->UserId;
$award->GrantIfUserQualifies();
}
}
}
//function for reading all files in a directory.
//this is recursive, so any files in subfolders will also make it in
function read_dir($dir)
{
$array = array();
$d = dir($dir);
while (false !== ($entry = $d->read())) {
if($entry!='.' && $entry!='..') {
$entry = $dir.'/'.$entry;
if(is_dir($entry)) {
$array = array_merge($array, read_dir($entry));
} else {
$array[] = $entry;
}
}
}
$d->close();
return $array;
}
}
?>
i would think the idea of what the IAward interface would look like would be pretty clear from the usage, though you'd probably add to it the Id field from your table so it would be able to insert itself into the database, as would the way to call the AwardRunner class.
this idea should work whether you have something batching the awards process looping through all your users, or just fire it off after every task submission.
How about you create a trigger on the task submission proc (or however you insert the data when the user completes a task), that then performs the necessary actions for that user to determine if he/she is awarded an achievement, and then updates the achievements table accordingly.
Then, every-time you load up the information for the user on the front end, the data will already be in for him/her in the achievements table, and you can directly access it (which I'm sure you already do).
I'm looking for a way to prevent repeated calls to the database if the item in question has already been loaded previously. The reason is that we have a lot of different areas that show popular items, latest releases, top rated etc. and sometimes it happens that one item appears in multiple lists on the same page.
I wonder if it's possible to save the object instance in a static array associated with the class and then check if the data is actually in there yet, but then how do I point the new instance to the existing one?
Here's a draft of my idea:
$baseball = new Item($idOfTheBaseballItem);
$baseballAgain = new Item($idOfTheBaseballItem);
class Item
{
static $arrItems = array();
function __construct($id) {
if(in_array($id, self::arrItems)){
// Point this instance to the object in self::arrItems[$id]
// But how?
}
else {
// Call the database
self::arrItems[id] = $this;
}
}
}
If you have any other ideas or you just think I'm totally nuts, let me know.
You should know that static variables only exist in the page they were created, meaning 2 users that load the same page and get served the same script still exist as 2 different memory spaces.
You should consider caching results, take a look at code igniter database caching
What you are trying to achieve is similar to a singleton factory
$baseball = getItem($idOfTheBaseballItem);
$baseballAgain =getItem($idOfTheBaseballItem);
function getItem($id){
static $items=array();
if(!isset($items[$id])$items[$id]=new Item($id);
return $items[$id];
}
class Item{
// this stays the same
}
P.S. Also take a look at memcache. A very simple way to remove database load is to create a /cache/ directory and save database results there for a few minutes or until you deem the data old (this can be done in a number of ways, but most approaches are time based)
You can't directly replace "this" in constructor. Instead, prepare a static function like "getById($id)" that returns object from list.
And as stated above: this will work only per page load.
I'm thinking of the best way to design an achievements system for use on my site. The database structure can be found at Best way to tell 3 or more consecutive records missing and this thread is really an extension to get the ideas from developers.
The problem I have with lots of talk about badges/achievement systems on this website is just that -- it's all talk and no code. Where's the actual code implemention examples?
I propose here a design that I hope people could contribute to and hopefully create a good design for coding extensible achievement systems. I'm not saying this is the best, far from it, but it's a possible starting block.
Please feel free to contribute your ideas.
my system design idea
It seems the general consensus is to create an "event based system" -- whenever a known event occurs like a post is created, deleted, etc it calls the event class like so..
$event->trigger('POST_CREATED', array('id' => 8));
The event class then finds out what badges are "listening" for this event, then it requires that file, and creates an instance of that class, like so:
require '/badges/' . $file;
$badge = new $class;
It then calls the default event passing the data received when trigger was called;
$badge->default_event($data);
the badges
This is then where the real magic happens. each badge has its own query/logic to determine if a badge should be awarded. Each badge is set out in e.g. this format:
class Badge_Name extends Badge
{
const _BADGE_500 = 'POST_500';
const _BADGE_300 = 'POST_300';
const _BADGE_100 = 'POST_100';
function get_user_post_count()
{
$escaped_user_id = mysql_real_escape_string($this->user_id);
$r = mysql_query("SELECT COUNT(*) FROM posts
WHERE userid='$escaped_user_id'");
if ($row = mysql_fetch_row($r))
{
return $row[0];
}
return 0;
}
function default_event($data)
{
$post_count = $this->get_user_post_count();
$this->try_award($post_count);
}
function try_award($post_count)
{
if ($post_count > 500)
{
$this->award(self::_BADGE_500);
}
else if ($post_count > 300)
{
$this->award(self::_BADGE_300);
}
else if ($post_count > 100)
{
$this->award(self::_BADGE_100);
}
}
}
award function comes from an extended class Badge which basically checks to see if the user has already be awarded that badge, if not, will update the badge db table. The badge class also takes care of retrieving all badges for a user and returning it in an array, etc (so badges can be e.g. displayed on the user profile)
what about when the system is very first implemented on an already live site?
There is also a "cron" job query that can be added to each badge. The reason for this is because when the badge system is very first implemented and initilaised, the badges that should have already been earned have not yet be awarded because this is an event based system. So a CRON job is run on demand for each badge to award anything that needs to be. For example the CRON job for the above would look like:
class Badge_Name_Cron extends Badge_Name
{
function cron_job()
{
$r = mysql_query('SELECT COUNT(*) as post_count, user_id FROM posts');
while ($obj = mysql_fetch_object($r))
{
$this->user_id = $obj->user_id; //make sure we're operating on the right user
$this->try_award($obj->post_count);
}
}
}
As the above cron class extends the main badge class, it can re-use the logic function try_award
The reason why I create a specialised query for this is although we could "simulate" previous events, i.e. go through every user post and trigger the event class like $event->trigger() it would be very slow, especially for many badges. So we instead create an optimized query.
what user gets the award? all about awarding other users based on event
The Badge class award function acts on user_id -- they will always be given the award. By default the badge is awarded to the person who CAUSED the event to happen i.e. the session user id (this is true for the default_event function, although the CRON job obviously loops through all users and awards seperate users)
So let's take an example, on a coding challenge website users submit their coding entry. The admin then judges the entries and when complete, posts the results to the challenge page for all to see. When this happens, a POSTED_RESULTS event is called.
If you want to award badges for users for all the entries posted, lets say, if they were ranked within the top 5, you should use the cron job (although bare in mind this will update for all users, not just for that challenge the results were posted for)
If you want to target a more specific area to update with the cron job, let's see if there is a way to add filtering parameters into the cron job object, and get the cron_job function to use them. For example:
class Badge_Top5 extends Badge
{
const _BADGE_NAME = 'top5';
function try_award($position)
{
if ($position <= 5)
{
$this->award(self::_BADGE_NAME);
}
}
}
class Badge_Top5_Cron extends Badge_Top5
{
function cron_job($challenge_id = 0)
{
$where = '';
if ($challenge_id)
{
$escaped_challenge_id = mysql_real_escape_string($challenge_id);
$where = "WHERE challenge_id = '$escaped_challenge_id'";
}
$r = mysql_query("SELECT position, user_id
FROM challenge_entries
$where");
while ($obj = mysql_fetch_object($r))
{
$this->user_id = $obj->user_id; //award the correct user!
$this->try_award($obj->position);
}
}
The cron function will still work even if the parameter is not supplied.
I've implemented a reward system once in what you would call a document oriented database (this was a mud for players). Some highlights from my implementation, translated to PHP and MySQL:
Every detail about the badge is stored in the users data. If you use MySQL I would have made sure that this data is in one record per user in the database for performance.
Every time the person in question does something, the code triggers the badge code with a given flag, for instance flag('POST_MESSAGE').
One event could also trigger a counter, for instance a count of number of posts. increase_count('POST_MESSAGE'). In here you could have a check (either by a hook, or just having a test in this method) that if the POST_MESSAGE count is > 300 then you should have reward a badge, for instance: flag("300_POST").
In the flag method, I'd put the code to reward badges. For instance, if the Flag 300_POST is sent, then the badge reward_badge("300_POST") should be called.
In the flag method, you should also have the users previous flags present. so you could say when the user has FIRST_COMMENT, FIRST_POST, FIRST_READ you grant badge("NEW USER"), and when you get 100_COMMENT, 100_POST, 300_READ you can grant badge("EXPERIENCED_USER")
All of these flags and badges need to be stored somehow. Use some way where you think of the flags as bits. If you want this to be stored really efficiently, you think of them as bits and use the code below: (Or you could just use a bare string "000000001111000" if you don't want this complexity.
$achievments = 0;
$bits = sprintf("%032b", $achievements);
/* Set bit 10 */
$bits[10] = 1;
$achievements = bindec($bits);
print "Bits: $bits\n";
print "Achievements: $achievements\n";
/* Reload */
$bits = sprintf("%032b", $achievments);
/* Set bit 5 */
$bits[5] = 1;
$achievements = bindec($bits);
print "Bits: $bits\n";
print "Achievements: $achievements\n";
A nice way of storing a document for the user is to use json and store the users data in a single text column. Use json_encode and json_decode to store/retrieve the data.
For tracking activity on some of the users data manipulated by some other user, add a data structure on the item and use counters there as well. For instance read count. Use the same technique as described above for awarding badges, but the update should of course go into the owning users post. (For instance article read 1000 times badge).
UserInfuser is an open source gamification platform which implements a badging/points service. You can check out its API here:
http://code.google.com/p/userinfuser/wiki/API_Documentation
I implemented it and tried to keep the number of functions minimal. Here is the API for a php client:
class UserInfuser($account, $api_key)
{
public function get_user_data($user_id);
public function update_user($user_id);
public function award_badge($badge_id, $user_id);
public function remove_badge($badge_id, $user_id);
public function award_points($user_id, $points_awarded);
public function award_badge_points($badge_id, $user_id, $points_awarded, $points_required);
public function get_widget($user_id, $widget_type);
}
The end result is to show the data in a meaningful way through the use of widgets. These widgets include: trophy case, leaderboard, milestones, live notifications, rank and points.
The implementation of the API can be found here: http://code.google.com/p/userinfuser/source/browse/trunk/serverside/api/api.py
Achievements can be burdensome and even more so if you have to add them in later, unless you have a well-formed Event class.
This segues into my technique of implementing achievements.
I like to split them first into 'categories' and within those have tiers of accomplishment. i.e. a kills category in a game may have an award at 1 for first kill, 10 ten kills, 1000 thousand kills etc.
Then to the spine of any good application, the class handling your events. Again imagining a game with kills; when a player kills something, stuff happens. The kill is noted, etc and that is best handled in a centralized location, like and Events class that can dispatch info to other places involved.
It falls perfectly into place there, that in the proper method, instantiate your Achievements class and check it the player is due one.
As building the Achievements class it is trivial, just something that checks the database to see if the player has as many kills as are required for the next achievement.
I like to store user's achievements in a BitField using Redis but the same technique can be used in MySQL. That is, you can store the player's achievements as an int and then and that int with the bit you have defined as that achievement to see if they have already gained it. That way it uses only a single int column in the database.
The downside to this is you have to have them organized well and you will likely need to make some comments in your code so you will remember what 2^14 corresponds to later. If your achievements are enumerated in their own table then you can just do 2^pk where pk is the primary key of the achievements table. That makes the check something like
if(((2**$pk) & ($usersAchInt)) > 0){
// fire off the giveAchievement() event
}
This way you can add achievements later and it will dovetail fine, just NEVER change the primary key of the achievements already awarded.