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.
Related
I have an DailyReport Entity in my Domain Layer. There are some fields in this object:
reportId
userId
date
tasks - Collection of things that user did in given day;
mood - how does the user felt during the whole day;
Also, there are some methods in my Application Service:
DailyReportService::addTaskToDailyReport
DailyReportService::setUserMoodInDailyReport
The thing is that both of these methods require DailyReport to be created earlier or created during function execution. How to deal with this situation?
I have found 2 solutions:
1 Create new DailyReport object before method dispatching, and after that pass reportId to them:
//PHP, simplified
public function __invoke() {
$taskData = getTaskData();
/** #var $dailyReport DailyReport|null **/
$dailyReport = $dailyReportRepository->getOneByDateAndUser('1234-12-12', $user);
//there were no report created today, create new one
if($dailyReport === null) {
$dailyReport = new DailyReport('1234-12-12', $user);
$dailyReportRepository->store($dailyReport);
}
$result = $dailyReportService->addTaskToDailyReport($taskData, $dailyReport->reportId);
//[...]
}
This one requires to put a more business logic to my Controller which i want to avoid.
2: Verify in method that DailyReport exists, and create new one if needed:
//my controller method
public function __invoke() {
$taskData = getTaskData();
$result = $dailyReportService->addTaskToDailyReport($taskData, '1234-12-12', $user);
//[...]
}
//in my service:
public function addTaskToDailyReport($taskData, $date, $user) {
//Ensure that daily report for given day and user exists:
/** #var $dailyReport DailyReport|null **/
$dailyReport = $dailyReportRepository->getOneByDateAndUser();
//there were no report created today, create new one
if($dailyReport === null) {
$dailyReport = new DailyReport($date, $user);
$dailyReportRepository->store($dailyReport);
}
//perform rest of domain logic here
}
This one reduces complexity of my UI layer and does not expose business logic above the Application Layer.
Maybe these example is more CRUD-ish than DDD, but i wanted to expose one of my use-case in simpler way.
Which solution should be used when in these case? Is there any better way to handle get-or-create logic in DDD?
EDIT 2020-03-05 16:21:
a 3 example, this is what i am talking about in my first comment to Savvas Answer:
//a method that listens to new requests
public function onKernelRequest() {
//assume that user is logged in
$dailyReportService->ensureThereIsAUserReportForGivenDay(
$userObject,
$currentDateObject
);
}
// in my dailyReportService:
public function ensureThereIsAUserReportForGivenDay($user, $date) {
$report = getReportFromDB();
if($report === null) {
$report = createNewReport();
storeNewReport();
}
return $report;
}
//in my controllers
public function __invoke() {
$taskData = getTaskData();
//addTaskToDailyReport() only adds the data to summary, does not creates a new one
$result = $dailyReportService->addTaskToDailyReport($taskData, '1234-12-12', $user);
//[...]
}
This will be executed only when user will log in for the first time/user were logged in yesterday but this is his first request during the new day.
There will be less complexity in my business logic, i do not need to constantly checking in services/controllers if there is a report created because this has been executed
previously in the day.
I'm not sure if this is the answer you want to hear, but basically I think you're dealing with accidental complexity, and you're trying to solve the wrong problem.
Before continuing I'd strongly suggest you consider the following questions:
What happens if someone submits the same report twice
What happens if someone submits a report two different times, but in the second one, it's slightly different?
What is the impact of actually storing the same report from the same person twice?
The answers to the above questions should guide your decision.
IMPORTANT: Also, please note that both of your methods above have a small window where two concurrent requests to store the rerport would succeed.
From personal experience I would suggest:
If having duplicates isn't that big a problem (for example you may have a script that you run manually or automatically every so often that clears duplicates), then follow your option 1. It's not that bad, and for human scale errors should work OK.
If duplicates are somewhat of a problem, have a process that runs asynchronously after reports are submited, and tries to find duplicates. Then deal with them according to how your domain experts want (for example maybe duplicates are deleted, if one is newer either the old is deleted or flagged for human decision)
If this is part of an invariant-level constraint in the business (although I highly doubt it given that we're speaking about reports), and at no point in time should there ever be two reports, then there should be an aggregate in place to enforce this. Maybe this is UserMonthlyReport or whatever, and you can enforce this during runtime. Of course this is more complicated and potentially a lot more work, but if there is a business case for an invariant, then this is what you should do. (again, I doubt it's needed for reports, but I write it here in the care reports were used as an example, or for future readers).
There i am diving into the world of queues and all of its goodness and it hit me:
Session data is lost when the application pushes a task to the queue, due to serialization of information by laravel.
Having found out how to send data to queues, a question remains:
Given that the queue pushes information to a single class,
how do i make that information persistent(such as a session) across other classes throughout the duration of this task?
Coding Example:
//Case where the user object is needed by each class
class queueme {
...
//function called by queue
function atask($job,$data)
{
//Does xyz
if(isset($data['user_id'])
{
//Push user_id to another class
anotherclass::anothertask($data['user_id']);
}
}
}
class anotherclass {
...
function anothertask($user_id)
{
//Does abc
//Yup, that anotherofanother class needs user_id, we send it again.
anotherofanotherclass::yetanothertask($user_id);
}
}
The above code illustrates my problem.
Do i have to pass the $user_id or User object around, if my classes need this information?
Isn't there a cleaner way to do it?
When you queue up a job, you should pass all data required by the job to do its work. So if it's a job to resize a user's avatar, the necessary information required is the primary key of the user so we can pull their model out in the job. Just like if you're viewing a user's profile page in the browser, the necessary information (the user's primary key) is likely provided in the request URI (e.g. users/profile/{id}).
Sessions won't work for queue jobs, because sessions are used to carry state over from browser requests, and queue jobs are run by the system, so they simply don't exist. But that's fine, because it's not good practice for every class to be responsible for looking up data. The class that handles the request (a controller for an HTTP request, or a job class for a queue job) can take the input and look up models and such, but every call thereafter can pass those objects around.
Back to the user avatar example. You would pass the ID of the user as a primitive when queueing the job. You could pass the whole user model, but if the job is delayed for a long time, the state of that user could have changed in the meanwhile, so you'd be working with inaccurate data. And also, as you mention, not all objects can be serialised, so it's best to just pass the primary key to the job and it can pull it fresh from the database.
So queue your job:
Queue::push('AvatarProcessor', [$user->id]);
When your job is fired, pull the user fresh from the database and then you're able to pass it around to other classes, just like in a web request or any other scenario.
class AvatarProcessor {
public function fire($job, $data)
{
$user_id = $data[0]; // the user id is the first item in the array
$user = User::find($user_id); // re-pull the model from the database
if ($user == null)
{
// handle the possibility the user has been deleted since
// the job was pushed
}
// Do any work you like here. For an image manipulation example,
// we'll probably do some work and upload a new version of the avatar
// to a cloud provider like Amazon S3, and change the URL to the avatar
// on the user object. The method accepts the user model, it doesn't need
// to reconstruct the model again
(new ImageManipulator)->resizeAvatar($user);
$user->save(); // save the changes the image manipulator made
$job->delete(); // delete the job since we've completed it
}
}
As mentioned by maknz, the data needs to be passed explicitly to the job. But in the job handle() method, you can use session():
public function handle()
{
session()->put('query_id', 'H123214e890890');
Then your variable is directly accessible in any class:
$query_id = session()->get('query_id')
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.
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 have a logger class (in PHP, but that does not matter) which spits out log messages. The logger is a custom implementation and works well. However, I would like to extend it to provide a event ID with each type of logger message. Such that "User logged in messages" are event ID 1, "Form validation failed" is event ID 2, for example.
The purpose of this event ID is to filter unimportant events when a long list of logs is viewed. I believe it would be quicker to categorize logs by event ID and faster to query than doing substring searches in a database.
I have the following ideas, but welcome viable alternatives. I used PHP in this example, but something that is generic enough that it could be applied to most languages such as PHP/Java/C#/etc would be useful.
1) Include an ID implicitly in the log function call:
abstract class EventId {
const LOGIN = 1;
const VALIDATION_FAILURE = 2;
// etc
}
Logger::messageDebug(EventID::LOGIN, $username . " logged in");
Logger::messageWarning(EventID::VALIDATION_FAILURE, "Form failed to validate, etc.");
The advantages of this are simplicity, but I fear it could get a little messy.
2) Pass variable parameters to log messages as separate arguments, then hash the first string
Logger::messageDebug("%s logged in", $username);
Logger::messageWarning("The %s form failed to validate", $form);
The advantages are simplicity, the disadvantages are the event ID depends upon hashing (or some other function) of the first string to differentiate the types of log messages.
3) Backtrace and do some nastyness
function messageDebug($message) {
$trace = obtainTraceOfCallToMessageDebug();
$callToMessageDebug = $trace[0];
$eventId = "";
$eventId .= $eventId->file;
$eventId .= $eventId->line;
$eventId = sha1($eventId);
messageImpl($eventId, $message);
}
This is pretty nasty, but it does have an advantage in that existing log messages do not need to be changed, additionally it is less fragile when writing new log messages. In PHP, obtaining a backtrace is easy and sort of cheap, in other languages you may have to throw an Exception which is super nasty. Anyway, it's an idea.
Looking forward to your opinions. My main requirement is maintainability rather than speed. I am currently looking towards number 1 - keep it simple.
I'd go with the first one with a bit of modification:
Assuming that you called your logger with statements like :
Logger::messageWarning($message)
I'd replace that with
Logger::messageWarning($message, EventId:someconstant)
and defining your EventId class as
abstract class EventId {
const UNSPECIFIED = 1;
const LOGIN = 2;
const VALIDATION_FAILURE = 3;
// etc
}
and you logger function would accept a second parameter, but use a default value of EventId:UNSPECIFIED for that.
class Logger {
...
function messageWarning($message, $message_type = EventId:UNSPECIFIED) {
...
}
}
This way you don't break existing code, don't do the hack outlined in #3, and can replace your log calls where and when you need by adding the event type.