Often it is required to realize the possibility of rating evaluation of a particular object (notes, comments, quotes, photograms, videos, etc.) by site visitors. How to program it?
First of all, we have the object of evaluation and the subject of evaluation. The latter may be, for example, registered users, unregistered users (guests), etc.
In order to provide a weak connection between specific entities of the subject area, to which we link the possibility of rating voting, with the module that implements our task, we select separate classes for the object (
Rating_Object ) and the subject (
Rating_Subject ). Both of these classes are concrete and implemented as an
active record . To be able to bind all sorts of articles and photograms to Rating_Object instances, we provide the
Rating_Ratable interface:
')
interface Rating_Ratable { public function asRatingObject(); }
This interface can now be implemented in Article classes, etc., for example, like this:
class Article extends ActiveRecord implements Rating_Ratable { public function setTableDefinition() { $this->hasReferenceColumn("rating_object_id"); } public function setUp() { $this->hasOne("Rating_Object as rating_object", array("local" => "rating_object_id", "foreign" => "id")); } public function asRatingObject() { return $this->rating_object; } }
We do the same with subjects of evaluation. It is only necessary that we store the subject identifier for each specific entity that can vote. Since we almost always have an active record for the current site visitor (the guest can and should be
identified by a complex of information and, accordingly, to create a record for him in the database - if you are not doing this yet, it's time to start), it’s enough to add class field rating_subject_id.
If in the future we need to take votes for a rating from entities that are not visitors of the site (for example, to take data from third-party ratings), we can also easily link a separate instance of Rating_Subject to each instance of such entities.
So, we have those who can evaluate, and that which can be evaluated. We need to bind them. To do this, we introduce the class
Rating_Vote , - active record with the fields object_id, subject_id, opinion. The last field represents a specific choice on the rating scale — a specific assessment that this subject has placed on this object.
It should be noted here that the spectra of possible estimates may be different. You can limit yourself to two ratings of “good” and “bad” (
my personal choice ), you can offer to put from one to five stars (or from one to ten), etc. Naturally, a specific rating scale is determined for a specific object: for example , quotes and comments are evaluated binary, and articles and photograms - on a scale of 1 ... 5.
However, it is necessary to determine the method of calculating the final rating. For example, for a binary choice of "plus or minus" you can take for the final assessment the difference between the number of pluses or the number of minuses. But you can - the ratio of the number of pluses to the total number of votes (my personal choice). For the scale of several options, you can take the arithmetic mean, harmonic mean, mode or median. Again, for a specific object we set a specific method.
The class that determines, on the one hand, the rating scale, and on the other hand, the method of calculating the final rating, we call the assessment strategy. We introduce the
Rating_Strategy interface:
interface Rating_Strategy { public function getRatingOptions(); public function getAggregatedOpinion(Rating_Vote_Collection $votes); }
Why is a float selected to reflect a specific possible estimate? This will allow us to reflect both discrete grading scales (comparing the variants to integers, which are subsets of the real numbers) and the floating scale, if we need it. The final score can be fractional even for an integer scale (for example, estimate on a scale from 1 to 10 and take the arithmetic average of the estimates). Choosing float gives us the opportunity to cover almost all the options. The opinion field in the Rating_Vote class, respectively, is also a float.
The
Rating_Vote_Collection class reflects, as you might guess, the collection of Rating_Vote objects - that is, the set of votes received for a specific object. Why do we need this class, why not just get by with the Rating_Vote array? There can be a lot of voices, several thousand, - to load them all from the database into RAM is consumable, and there is no need. Almost always, to obtain a final assessment, it is necessary and sufficient to have on hand information about the aggregate number of votes for each of the options. Therefore, in the Rating_Vote_Collection class we will make the corresponding getOpinionCounts method, which will return an array of the form: [+1 → 100, −1 → 50] (100 “good”, 50 “bad”). But we will also provide for a lazy-load in case we want to implement a non-trivial strategy (giving, for example, weights to voices depending on the karma of the voters).
A few examples of assessment strategies:
abstract class Rating_Strategy_Binary implements Rating_Strategy { const GOOD = +1; const BAD = -1; public function getRatingOptions() { return array(self::GOOD, self::BAD); } } class Rating_Strategy_Binary_Subtraction extends Rating_Strategy_Binary { public function getAggregatedOpinion(Rating_Vote_Collection $votes) { $counts = $votes->getOpinionCounts(); $good = isset($counts[self::GOOD]) ? $counts[self::GOOD] : 0; $bad = isset($counts[self::BAD]) ? $counts[self::BAD] : 0; return $good - $bad; } } class Rating_Strategy_Binary_Rational extends Rating_Strategy_Binary { public function getAggregatedOpinion(Rating_Vote_Collection $votes) { $counts = $votes->getOpinionCounts(); $good = isset($counts[self::GOOD]) ? $counts[self::GOOD] : 0; $bad = isset($counts[self::BAD]) ? $counts[self::BAD] : 0; $total = $good + $bad; return ($total > 0) ? ($good / $total) : 0; } } abstract class Rating_Strategy_Range implements Rating_Strategy { private $min, $max; public function __construct($min, $max) { $this->min = $min; $this->max = $max; } public function getRatingOptions() { return range($this->min, $this->max); } } class Rating_Strategy_Range_Arithmetic extends Rating_Strategy_Range { public function getAggregatedOpinion(Rating_Vote_Collection $votes) { $counts = $votes->getOpinionCounts(); $sum = 0; $total_count = 0; foreach ($counts as $value => $count) { $sum += $value * $count; $total_count += $count; } return ($total_count > 0) ? ($sum / $total_count) : 0; } }
It remains to add the getRatingStrategy method to the Rating_Ratable interface:
interface Rating_Ratable { public function asRatingObject(); public function getRatingStrategy(); } class Article extends ActiveRecord implements Rating_Ratable { public function setTableDefinition() { $this->hasReferenceColumn("rating_object_id"); } public function setUp() { $this->hasOne("Rating_Object as rating_object", array("local" => "rating_object_id", "foreign" => "id")); } public function asRatingObject() { return $this->rating_object; } public function getRatingStrategy() { return new Rating_Strategy_Range_Arithmetic(1, 5); } }
And in the class Rating_Object provide methods for operating the associated Rating_Vote.
Now we can easily add voice or find out the current rating in the right places:
$article->asRatingObject()->addVote($user->asRatingSubject(), 5); echo $article->getRatingStrategy()->getAggregatedOpinion($article->asRatingObject()->getVotes());