1

I have an app in Laravel for the backend and Vue.js and Quasar for the front end. I have a view in Vue.js that sends a request every 5 seconds. I would like to know how I can implement web sockets. I already configured Pusher and Laravel Echo. I understand that WebSockets work as a protocol that switches the HTTP protocol and creates an event by which the socket communicates.

At the moment, I have only tried communicating the back socket with the front, but I want the socket to handle or listen to every updated data in my table so that I don't have to send requests every 5 seconds. What I do in my table is pagination, which is done in the front. The request is sent to the back, and then it shows the data.

Another thing, I have implemented a loading logic so that every time a request is made, the loading is shown, and when the request is processed, the loading is hidden, so the request is made every 5 seconds, and the loading is shown every 5 seconds.

Here's a snippet of my Vue.js view:

async init() {
      this.onRequest({
        pagination: this.pagination,
        filter: undefined
      })
      this.timeout = setTimeout(this.init, 5000);

and this is the "onRequest" function

onRequest(props) {
      const { page, rowsPerPage } = props.pagination
      const { search } = this
      this.loading = true
      DataService.index({ params: { page, rowsPerPage, search } }).then((payrolls) => {
        this.data = payrolls.data
        this.pagination.rowsPerPage = payrolls.per_page
        this.pagination.page = payrolls.current_page
        this.pagination.rowsNumber = payrolls.total
        this.loading = false
      }).catch(() => {
        this.loading = false
      })
    },

websocket on browser

2

2 Answers 2

0

As far as I understand, you're making repetitive AJAX requests within specified time intervals (e.g. 5 seconds) to fetch the data from the server (this is also known as "AJAX polling"), but this methodology creates an HTTP overhead because the client has to repeatedly ask the server for data.

Since you're on Laravel, you can use Laravel broadcasting features. It requires a quick setup process that I'll list here briefly just for you to make sure that you've setup everything correctly.

The installation process on the back-end part requires the following steps:

  • Enable the App\Providers\BroadcastServiceProvider provider in your config/app.php by uncommenting it.

  • Install the Pusher Channels PHP SDK:

composer require pusher/pusher-php-server
  • Configure the Pusher Channels credentials in your .env file:
PUSHER_APP_ID=your-pusher-app-id
PUSHER_APP_KEY=your-pusher-key
PUSHER_APP_SECRET=your-pusher-secret
PUSHER_APP_CLUSTER=mt1
  • Update the BROADCAST_DRVIER variable in your project and set it to pusher:
BROADCAST_DRIVER=pusher
  • Optionally, you may want to configure a queue worker for the message broadcasting process, and you're highly encouraged to do so, but for now, let's just leave it disabled by setting the QUEUE_CONNECTION variable to be sync:
QUEUE_CONNECTION=sync

The installation process on the front-end part requires the following steps:

  • You need to install both the pusher-js library and the laravel-echo client:
npm install --save-dev laravel-echo pusher-js
  • In your resources/js/bootstrap.js file, make sure to uncomment the following code:
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
 
window.Pusher = Pusher;
 
window.Echo = new Echo({
    broadcaster: 'pusher',
    key: import.meta.env.VITE_PUSHER_APP_KEY,
    cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
    forceTLS: true
});
  • Compile the application assets using the command:
npm run build
  • And of course, don't forget to include the resultant JS bundle file in your front-end.

Now, to send updates to a client you need to broadcast an event holding this data to the client instead of doing repeated AJAX requests, so you have to do the following:

  • Create an event class using the command:
php artisan make:event MyEvent
  • Make sure that your event class implements the Illuminate\Contracts\Broadcasting\ShouldBroadcast interface, and make sure that the broadcastOn() method returns the names of the channels where the event will be broadcasted, in this example we're broadcasting on a public channel called my-channel:
namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class MyEvent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     */
    public function __construct(
        public array $data = []
    )
    {
        //
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return array<int, \Illuminate\Broadcasting\Channel>
     */
    public function broadcastOn(): array
    {
        return [
            new Channel('my-channel'),
        ];
    }
}
  • In your front-end, you have to listen to the event being broadcast. You just need to use the Echo object which provides you with several methods to interact with the sockets:
Echo.channel('my-channel')      // The channel name you want to subscribe to
    .listen('MyEvent', (e) => { // The event name you want to listen to
        console.log(e.data);    // This (data) property is the same one declared inside the constructor of the event
    });
  • Notice that within the callback of the listen() method you can specify what you want to happen when you receive such an event, and the parameter of the callback holds an object that contains the data broadcasted inside the event.

  • On your back-end, test and broadcast the event using the broadcast() method:

$data = [
    "x" => "y",
    "foo" => "bar"
];

broadcast(new App\Events\MyEvent($data));

This is merely just an example, for better documentation and in-depth explanation you should check the documentation at: https://laravel.com/docs/10.x/broadcasting

0

I have all that configured but my question is that, for example, from the back to the front, 100 data are paged. Do I have to send those 100 data to the socket so that the socket is listening to them and verifying in real time if there have been changes? In the back I have my controller I have several methods that page but in the front I only have pages, let's say 10-20, 50 or whatever I choose

This is a part of my controller

public function index(Request $request)
    {
        try {

            $only_validated = $request->only_validated ?? null;
            $payrollType = $request->input('payroll_status');
            if (isset($payrollType) && $payrollType == 'in_progress') {
                $payrolls = Payroll::with('processed', 'user')
                    ->where('cat_payroll_status_id', CatPayrollStatus::IN_PROGRESS)
                    ->where('cat_payroll_calculation_type_id', CatPayrollCalculationType::PAYROLL)
                    ->get();

                return response()->json([
                    'success' => true,
                    'payrolls' => $payrolls,
                ]);
            } else {
                $rowsPerPage = $request->input('rowsPerPage');
                $search = $request->input('search');

                $where_query = [
                    ['cat_payroll_status_id', "!=", CatPayrollStatus::IN_PROGRESS],
                    ['cat_payroll_calculation_type_id', '=', CatPayrollCalculationType::PAYROLL]
                ];

                if ($only_validated) {
                    $where_query[] = ['cat_payroll_status_id', "=", CatPayrollStatus::VALIDATED];
                }

                $payrolls = Payroll::with('status', 'processed')->search($search)
                    ->where($where_query)
                    ->orderBy('created_at', 'desc')
                    ->paginate($rowsPerPage == 0 ? 500 : $rowsPerPage);
//here i put the socket but idk if that is on right way 
                broadcast(new SocketPayroll($payrolls));


                return response()->json([
                    'success' => true,
                    'payrolls' => $payrolls,
                ]);
            }
        } catch (\Exception $e) {
            error_log($e->getMessage());
            return response()->json([
                'success' => false,
                'message' => 'Error'
            ]);
        }

    }

this is my event for the socket

<?php

namespace App\Events;

use App\Models\Payroll;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class SocketPayroll implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public Payroll $payroll;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public function __construct($payrolls)
    {
        $this->payrolls = $payrolls;
    }

    /**
     * Get the channels the event should broadcast on.
     *
     * @return \Illuminate\Broadcasting\Channel|array
     */
    public function broadcastOn()
    {
        return new PrivateChannel('payrollUpdated');
    }
}

on my table view i have this

export default {
  mounted() {
    // get initial data from server (1st page)
    this.init()
    this.setupEcho()

and in the same table view

destroyed() {
    clearTimeout(this.timeout);
  },
  methods: {
    setupEcho() {
      window.Echo.channel('payrollUpdated')
        .listen('SocketPayroll', (event) => {
          if (event.data && event.data.length > 0) {
            this.data = event.data;
            console.log('Message received from the server.:', event.message);
          }
        });
    },
    async init() {
      this.onRequest({
        pagination: this.pagination,
        filter: undefined
      })
      this.timeout = setTimeout(this.init, 100000);
    },
    showPayroll(id) {
      this.$router.push(`/prepayroll/${id}/show`);
    },
    editPrePayroll(id) {
      this.$router.push(`/prepayroll/${id}/edit`)
    },
    viewPrePayrollErrors(id) {
      this.$router.push(`/prepayroll/${id}/errors`)
    },
    showPayrollReports(id) {
      this.$router.push(`/prepayroll/${id}/reports`)
    },
    downloadPayroll(id, code) {
      this.$q.loading.show();
      console.log('code', code);
      const idArray = [id];
      const params = { ids: idArray, code };
      DataService.exportExcelPayroll(params).then((response) => {
        console.log('response', response);
        if (response.success) {
          notifySuccess();
          this.$router.push(`/prepayroll/${idArray[0]}/reports`)
        } else {
          notifyError()
        }
      }).finally(() => {
        this.$q.loading.hide();
      });
    },
    downloadPayroll2(id, code) {
      const params = { id }
      DataService.exportExcel(params).then((response) => {
        const linkUrl = window.URL.createObjectURL(new Blob([response.data]))
        const link = document.createElement('a')
        link.href = linkUrl
        link.setAttribute('download', `${code}.xlsx`)
        document.body.appendChild(link)
        link.click()
      });
    },

    // eslint-disable-next-line no-unused-vars
    deletePrePayroll(id, index) {
      this.loading = true;
      DataService.destroy({ params: { id } }).then((data) => {
        if (data.success) {
          notifySuccess();
          this.data.splice(index, 1);
        } else {
          notifyError();
        }
        this.loading = false;
      }).catch(() => {
        this.loading = false
      })
    },
    executePrePayroll(id) {
      this.$q.loading.show();
      DataService.execute(id).then((response) => {
        if (response.success) {
          notifySuccess();
          this.onRequest({
            pagination: this.pagination,
            filter: undefined
          })
        } else {
          notifyError();
        }
      }).finally(() => {
        this.$q.loading.hide();
      })
    },
    blockPrePayroll(block, id) {
      this.$q.loading.show();
      const payload = {
        is_blocked: block,
        payroll_id: id
      }
      DataService.blockPrePayroll(payload).then(() => {
        notifySuccess();
        this.onRequest({
          pagination: this.pagination,
          filter: undefined
        })
      }).catch((err) => {
        notifyError(err);
      }).finally(() => {
        this.$q.loading.hide();
      })
    },
    onRequest(props) {
      const { page, rowsPerPage } = props.pagination
      const { search } = this
      this.loading = true
      DataService.index({ params: { page, rowsPerPage, search } }).then((payrolls) => {
        this.data = payrolls.data
        this.pagination.rowsPerPage = payrolls.per_page
        this.pagination.page = payrolls.current_page
        this.pagination.rowsNumber = payrolls.total
        this.loading = false
      }).catch(() => {
        this.loading = false
      })
    },
  },
  computed: {

i'm new at laravel sorry for the inconveniences

Not the answer you're looking for? Browse other questions tagged or ask your own question.