When developing the ClientEngage Project Platform, I implemented a quick and easy way to indicate users‘ online status. Since then, I have come across a number of users asking about how such a function can be implemented using CakePHP, so I thought I’d share my approach.
Preparing the Database for the User Online Status
First of all, you need to slightly tweak the users-table of your app. It does not matter whether your user-model is called User, Member or Subscriber – just add the following field to the table:
- Name: “last_activity” | Type: “DATETIME”
Setting the Default Timezone in CakePHP
Now that this is done, I am going to share another tip with you which you should implement in all your web-development projects, and that is setting UTC as the default timezone: always.
This is to ensure that all of your and your users’ DATETIME data is saved in a unified format. Instead of saving user-specific dates, you will store each DATETIME’S UTC variant. Then, upon rendering, you will convert it to the user’s local timezone.
Luckily, CakePHP offers you all of the facilities you need to automate this sort of thing. So, let’s set the default timezone by adding the following to our “app/Config/core.php” file:
1 | date_default_timezone_set('UTC'); |
Next, you need to tell your application which timezone should be used when rendering DATETIMEs with the TimeHelper. For a list of supported timezones, take a look at the PHP documentation. You can do this by adding the following to your “core.php” file:
1 | Configure::write('Config.timezone', 'Europe/London'); |
If your app is only ever going to be used in one specific timezone, then the above will suffice. Of course, in reality, things are never that simple. For example, I always add a “timezone” field to my users-table and use each users’ individual timezone for the app’s timezone configuration.
Recording Your Users’ Activity
The next step is to ensure that the “last_activity” field is updated upon each and every request. This is fairly simple by adding the following method to your CakePHP app’s User model:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * Records the current user's activity on every request * @return void */ public function recordActivity() { $this->id = AuthComponent::user('id'); $this->set(array( 'last_activity' => date('Y-m-d H:i:s'), 'modified' => $this->field('modified') )); $this->save(null, array('callbacks' => false, 'validate' => false)); } |
The above is very simple and the only thing that may require some further clarification is the bit where “modified” is read and written back to the user record. This is to ensure that the user’s last-modified date is not updated and only applies if you make use of CakePHP’s automagic tracking of created & modified dates.
Moreover, you can see that I am deactivating all model-callbacks when recording the activity. This is because I have quite a bit of model-layer logic which I do not want to execute on each and every request. In your case, you may wish to also deactivate validation, but this entirely depends on your individual validation rules.
Next, you will need to actually call this method. In my case, I have added the logic to the AppController’s afterFilter method. By doing this, the output is rendered to your users without having to wait for the activity to be recorded. I like it better this way since there is no reason for the user to have to wait for this sort of business logic – even if it is just a couple of milliseconds.
So, here we go. Add this to your AppController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /** * Indicates whether the user's activity was recorded for the current request * @var boolean */ private static $activityRecorded = false; public function afterFilter() { $this->recordUserActivity(); } /** * Records the current user's activity on every request * @return void */ private function recordUserActivity() { if (!AuthComponent::user() || self::$activityRecorded || $this->viewClass !== 'View') // only record for views, not media { return; } ClassRegistry::init('User')->recordActivity(); self::$activityRecorded = true; } |
Showing a Specific User’s Online State
Now we can go ahead and easily display a user’s online state by checking their “last_activity” value against the current UTC DATETIME. Personally, I do this by adding something like the following to a custom helper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /** * Renders the passed user's online-status bullet * @param array $user The user for which to display the online-status * @return string The online-status bullet of the passed user */ public function renderStatusBullet($user = null) { if (isset($user['last_activity'])) { $current_time = strtotime(date("Y-m-d H:i:s")); $last_visit = strtotime($user['last_activity']); $time_period = floor(round(abs($current_time - $last_visit) / 60, 2)); if ($time_period <= 10) return __('Currently online'); } return __('Not currently online') . ' | ' . __('Last seen: ') . $this->Time->timeAgoInWords($user['last_activity']); } |
Getting a List of Users who are Online
And finally, you may want to query the database for a list of users who are currently online. This is as simple as querying the “last_activity” field to be greater than the current DATETIME minus 10 minutes.
Clearing the Online Status When Logging-Out
To be consistent, you may also want to clear a user’s online status when they log-out. This is as setting the current user’s “last_activity” field to null just before they log out – you can do this in your UserController’s logout-method.
So, there you go: you should now be able to quickly add an online status to your CakePHP app. Moreover, you can also find a list of users who are currently logged-in.
I hope this was useful and feel free to give me a shout in the comments if you have any questions or other feedback.
2 Comments
Peter Härder says:
December 9, 2013 at 7:56 pm
Great post, thanks!
To get it to work I had to put
private static $activityRecorded = false;
in AppController instead of the User model.
To avoid hitting the database more than necessary, I would suggest re-writing recordActivity to:
public function recordActivity() {
$data = array(
'id' => AuthComponent::user('id'),
'last_activity' => date('Y-m-d H:i:s'),
'modified' => false
);
$this->save($data, array('callbacks' => false, 'validate' => false));
}
'modified' => false
is the documented way of disabling CakePhp’s auto-magic. And this mean you avoid hitting the db just to get hand on the old modified date on every request.ClientEngage says:
December 10, 2013 at 8:01 pm
Hi Peter,
Thanks for the comment – great catch! 🙂
Indeed, the AppController is where “activityRecorded” should go. That was a Freudian Slip on my side and I shall edit the post.
Thank you for pointing-out
'modified' => false
.I didn’t know about this (maybe it’s a new feature?) and that’s a lot better than reading-out the modified-value everytime – thanks!