Track User Online Activity Accurately In Laravel
Tags: laravel, programming, database, beginners
Published: 2023-01-02 03:44:21

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
```php
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.
```php
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,
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.
```php
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.
```php
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.
```php
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`
```php
/**
* @return MorphOne
*/
public function loginDetails()
{
    return $this->morphOne(LoginDetails::class, 'loginable');
}
```

#### Register to EventServiceProvider
* In the `$listens` property of `app/Providers/EventServiceProvider.php` we have to register the event and listener.
```php
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 ... 🍻

[Twitter](https://twitter.com/SaaberDev) | [LinkedIn](https://www.linkedin.com/in/saaberdev/) | [GitHub](https://github.com/SaaberDev) | [Portfolio](https://saaberdev.com) | [Upwork](https://cutt.ly/4RLx5OL)