Disclaimer: I am by no means a PHP, CakePHP or databases pro. I just like to get my hands dirty! If you have any recommendations on the code below or you want to tell me why you hate it, why it really stinks or (hopefully) why you love it, leave a comment below to let me know!
CakePHP - MVC Framework
I just recently started using CakePHP. Version 3.0 is very close to the release as for the time being but I chose to work with 2.6.1 so as to have more resources to learn from and work on a more stable environment. The friendly guys at #cakephp on Freenode informed me that 3.0 is a vast improvement to CakePHP, so if you want to give it a try, most would recommend waiting till CakePHP3.0 is finally stable.
The problem: Implement a comment system similar to the one that Reddit uses. This means that only registered users can vote on every comment of any post. Users can vote as many times as they like. Only their final vote counts to the total score of the comment. Users must be able to distinguish the comments they have voted on and the type (upvote/downvote) of the vote as well.
The solution I present below uses a way that sometimes saves many requests to the server and makes the preview of the comments fast, because it keeps the current score of each comment in the database and thus it doesn't need to calculate it on the fly.
The database structure as well as the names of the fields are important for CakePHP. It can link models together without explicitly specifying the keys used (providing that the keys have the appropriate name for each relationship type), it can easily create authentication systems if your database has the right fields etc. Here is the queries needed to generate the needed tables for our comment system:
The comments table will contain the post they belong to (post_id), the parent comment (parent_id), the user that created the comment (user_id), the body of the comment and the total score of it. The votes table will not be used in order to count the total score of the comment. It will be used only in order to show the user the type of the vote they have casted on the various comments.
The idea is: When a user loads a post, read all the comments of that post. Fetch only the votes that the currently logged in user has voted on and present him the votes that he has casted. That's the purpose of the votes table: It contains the comment id that the vote refers to (comment_id) as well as the user that casted that vote (user_id). As you can see, those two constitue the composite primary key, because a user can cast only one vote on a specific comment. The type of the vote (upvote/downvote) is held on the type field.
Showing the comments
When a user clicks on a post, he wants to see the comments as well as the votes he has casted. The code that requests the appropriate comments from my CommentsController is the following:
I have enabled the comment to act as a Containable. This way I only get what I really need: All the comments of the specific post, the type of the casted votes of the logged in user on these comments as well as the id and the username of the author of each comment, so as to be able to show who wrote every comment and be able to link to their profile via their id. 'threaded' is very comfortable here because it will return a nested array and I don't need to do anything in order to find whether a comment has children or if a comment has some parent etc. CakePHP does that for me :)
Actually presenting the resulted nested array as comments to the post requires a recursive function call, because there is no limit of the 'depth'. A comment can have an infinite number of ancestors no matter how distant they are. I present the code, just because I can:
This function is called for every 'comment-thread' and the final result string will contain the complete HTML needed for the thread to be presented correctly. It works pretty simply. It adds to the '$finalString' the code of the current comment (the creator of it, whether the currently logged in user has voted on it, its score) and then it calls itself with all the comment's children BEFORE closing the comment-group and hide-on-toggle divs. That is because, similar to Reddit, each comment "contains" all the children comments in a huge box. The hide-on-toggle is needed in order to hide the current comment's body and vote buttons as well as completely hide its children, in case the user chooses to hide the comment group by pressing the hide button. That's why those 2 divs close after adding the comment's children.
The comment structure and the hide toggle
When the user clicks on the hide toggle, then the whole 'hide-on-toggle' div will hide itself and only the username and the points will be left. Clicking it again will show that div. That is simply done through some simple jQuery magic.
This is the part that I devoted the most time on, simply because I know that users are silly and I don't want to send many requests to the server for each vote they cast. A similar technique is used by Reddit: When you start clicking on the upvote button furiously, many times per second, the requests that will be sent will be far less than the clicks that you did.
Reddit sents out only a few vote requests
By the way, while I was seeing how Reddit casts its votes, I think I found a bug on their system.
So, back to the comment system we develop. Many things happen when a user hits the upvote/downvote buttons. First of all, the user immediately needs to get a visual feedback that the vote has been casted. This means that the score count in the 'comment-details' div needs to change and the arrows need to change color (upvote = red, downvote = blue, no vote = gray). In the background, the vote of the user is added to a queue. When a user doesn't cast a vote for more than 1 second, then all the pending votes (the ones in the queue) will be sent to the server at once. The votes that have the same initial and final state will not be sent to the server (consider a user hitting the upvote or the downvote button an even number of times concurrently because they are bored). This is the complete heavily-commented code:
Now, when a user votes multiple times quickly, the votes will be sent altogether.
Sending out only one request for 4 votes
So, what is happening server side when a user votes? What is this "success" message? Well, the server starts a transaction and then for each vote in the bunch it will:
Find if the vote exists (user has upvoted/downvoted the comment in the past)
Update the vote value with the new value (or add the new vote if it doesn't already exist)
Change the score from the corresponding comment as needed. E.g. if the user had downvoted the comment in the past and now upvotes it, add 2 to the score. If the user has voted on the past, then the type of the vote is know from the votes table.
My method sends out less requests to the server when many votes are casted in a short period of time. This means that the server will be able to handle more users voting because less requests will be sent at several occasions. But this method has another hidden goodie! By giving out many votes altogether to the server, it has the ability to perform InnoDB transactions! So, the database server will be able to process bunches of votes from a specific user altogether. This is a significant performance boost in comparison with processing the votes one by one. The time it takes for the database server to process one vote without transaction is about the same with the time it takes the server to process 10 votes that have come as a bunch.