Track User Online Activity Accurately In Laravel
Mahfuzur Rahman Saber
Posted on January 2, 2023
When you are working with laravel applications you may need to track users online activity such as are they currently logged in or not or how many user's are currently active. There are so many solution available in the internet also in ChatGPT 😏 by the way, however I found this approach which is returns more accurate result.
To get started you need to prepare the following structure
- Migration (One to One - Polymorphic Relationship)
- A Middleware (i.e. LoginActivity.php)
- Events and Listener (i.e. LoginDetailEvent.php, LoginDetailListener.php)
- A Model (i.e. LoginDetails.php)
- Map events to the listener (i.e. EventServiceProvider.php)
Migration (One to One - Polymorphic Relationship)
- Create a migration file named
CreateLoginDetailsTable
- add this code in the up method
Schema::create('login_details', function (Blueprint $table) {
$table->id();
$table->boolean('loggedin_status')->default(0);
$table->timestamp('last_seen')->nullable();
$table->unsignedBigInteger('loginable_id');
$table->string('loginable_type');
$table->string('type', 255)->nullable();
$table->timestamps();
});
Middleware
This middleware will check if user is authenticated or not, if authenticated then logic will run or it will send the request to next.
- Let's take a look at the code snippet.
use App\Events\LoginDetailEvent;
use Closure;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
class LoginActivity
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure(Request): (Response|RedirectResponse) $next
* @return Response|RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
if (Auth::check()) {
// keep online for 2 min
$expireAt = now()->addMinutes(2);
// if isOnline cache is found
if (Cache::has('isOnline')) {
// modify the isOnline cache data
$onlineUsers = json_decode(Cache::get('isOnline', []), true);
}
// if isOnline cache is not found
// add auth user id and current time in key, value pair to the cache
$onlineUsers[Auth::id()] = now();
// put modified/new value to isOnline cache with expiry time
Cache::put('isOnline', json_encode($onlineUsers), $expireAt);
// update last seen status
event(new LoginDetailEvent(Auth::user()));
}
return $next($request);
}
}
In this code we are setting an expiry time at the beginning after checking authentication. We are storing online user's activity in database and cache to get accurate result, here we are checking if there is any online user from the cache named isOnline
. If user is available modify the isOnline cache data, add auth user id and current time in key, value pair to the cache, put modified value to isOnline
cache with expiry time and lastly update last seen status. If user is not available then add auth user id and current time in key, value pair to the cache, put new value to isOnline
cache with expiry time and lastly update last seen status.
Event and Listener
These events will fire and will be automatically listened when the user is authenticated or logged out. We have one listener who handles multiple tasks.
Such as,
- fire Login event when user is authenticated
- fire Logout event when user is logged out
- fire LoginDetailEvent event to update last seen status
- Let's take a look at the code snippet for that listener.
use App\Events\LoginDetailEvent;
use App\Models\LoginDetails;
use Illuminate\Auth\Events as LaravelEvents;
use Illuminate\Support\Facades\Auth;
class LoginDetailListener
{
/**
* This will store activity when login event is triggered
*
* @param LaravelEvents\Login $event
*/
public function login(LaravelEvents\Login $event)
{
$this->fireAuthActivityEvent($event);
}
/**
* This will store activity when logout event is triggered
*
* @param LaravelEvents\Logout $event
*/
public function logout(LaravelEvents\Logout $event)
{
$this->fireAuthActivityEvent($event);
}
/**
* Handle user's last seen status only
*
* @param LoginDetailEvent $event
*/
public function handleLastSeenStatus(LoginDetailEvent $event)
{
if (!\request()->ajax()) {
if (Auth::check()) {
$event->user->loginDetails()->update([
'last_seen' => now(),
'type' => $event->user->roles[0]->name
]);
}
}
}
/**
* Update login activity based on user event login or logout
*
* @param $event
*/
private function fireAuthActivityEvent($event)
{
if (Auth::check()) {
if ($event instanceof LaravelEvents\Login) {
$event->user->loginDetails()->updateOrCreate(
[
'loginable_id' => $event->user->id,
'loginable_type' => $event->user->loginDetails()->getMorphClass()
],
[
'loggedin_status' => LoginDetails::STATUS['ONLINE'],
'last_seen' => now(),
'type' => $event->user->roles[0]->name
]
);
} elseif ($event instanceof LaravelEvents\Logout) {
$event->user->loginDetails()->update([
'loggedin_status' => LoginDetails::STATUS['OFFLINE'],
'last_seen' => now(),
'type' => $event->user->roles[0]->name
]);
}
}
}
}
- Let's take a look at the code snippet.
use Illuminate\Queue\SerializesModels;
class LoginDetailEvent
{
use SerializesModels;
public $user;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($user)
{
$this->user = $user;
}
}
Model
- Here is the code snippet for the model.
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
class LoginDetails extends Model
{
public const STATUS = [
'ONLINE' => 1,
'OFFLINE' => 0
];
protected $guarded = [];
protected $casts = [
'loggedin_status' => 'int'
];
public function loginable()
{
return $this->morphTo();
}
/**
* @param null $type
* @return Collection
*/
public function getOnlineUsers($type = null): Collection
{
// get all users from cache
$cachedUsers = json_decode(Cache::get('isOnline', []), true);
$onlineUsers = null;
// if there are any
if ($cachedUsers && count($cachedUsers)) {
// get all online users by their user id except own id
$onlineUsers = $this->whereIn('loginable_id', array_keys($cachedUsers))
->where('loginable_id', '!=', Auth::id())
->where('loggedin_status', '=', 1)
->when(!is_null($type), function ($query) use ($type) {
$query->where('type', '=', $type);
})
->get();
}
return $onlineUsers;
}
}
- add this code in
app/Models/User.php
/**
* @return MorphOne
*/
public function loginDetails()
{
return $this->morphOne(LoginDetails::class, 'loginable');
}
Register to EventServiceProvider
- In the
$listens
property ofapp/Providers/EventServiceProvider.php
we have to register the event and listener.
protected $listen = [
Login::class => [
LoginDetailListener::class . '@login'
],
Logout::class => [
LoginDetailListener::class . '@logout'
],
LoginDetailEvent::class => [
LoginDetailListener::class . '@handleLastSeenStatus'
]
];
I hope this will help you and save your tons of hours. Cheers ... 🍻
Posted on January 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.