DEV Community

Cover image for Track User Online Activity Accurately In Laravel
Mahfuzur Rahman Saber
Mahfuzur Rahman Saber

Posted on

Track User Online Activity Accurately In Laravel

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

  1. Migration (One to One - Polymorphic Relationship)
  2. A Middleware (i.e. LoginActivity.php)
  3. Events and Listener (i.e. LoginDetailEvent.php, LoginDetailListener.php)
  4. A Model (i.e. LoginDetails.php)
  5. 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();
});
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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,

  1. fire Login event when user is authenticated
  2. fire Logout event when user is logged out
  3. 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
                ]);
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode
  • 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode
  • add this code in app/Models/User.php
/**
* @return MorphOne
*/
public function loginDetails()
{
    return $this->morphOne(LoginDetails::class, 'loginable');
}
Enter fullscreen mode Exit fullscreen mode

Register to EventServiceProvider

  • In the $listens property of app/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'
      ]
];
Enter fullscreen mode Exit fullscreen mode

I hope this will help you and save your tons of hours. Cheers ... 🍻

Twitter | LinkedIn | GitHub | Portfolio | Upwork

Top comments (0)