Real time Chat application with PHP & Websockets - php

I want to create chat application in a webapp where user can chat with different site users. This will be available on web and also on iOS.
Instead of using traditional polling technique (send ajax hit to server in 1 sec interval), i want to use websockets.
Gone through couple of tutorials, but in all of them they have made PUBLIC GROUP chat. (Sample URL : https://www.sanwebe.com/2013/05/chat-using-websocket-php-socket)
Can anyone have idea to how to develop private chat using PHP & Websockets.
I have basic idea of websockets but how to use them to publish data on specific channel? And if we have 40 users so do we need to create 40 different channels?
Thanks in advance.

There are not much different from doing one single global chat and multiple private channel. First, you need to design a protocol. Let create a simple protocol:
// client send to server
JOIN <channel_id>
LEAVE <channel_id>
MSG <channel_id> <message>
// server send to client
JOIN <channel_id> <username>
LEAVE <channel_id> <username>
MSG <channel_id> <username> <message>
So when a user connect to a server, you can randomly assign his username. You have an array to store all connection.
Create array of channel. Each channel hold an array of user inside the channel.
When client send JOIN <channel_id> to server. Broadcast JOIN <channel_id> <username> to all the connection in that channel.
When client send MSG <channel_id> <message> to server. Broadcast MSG <channel_id> <username> <message> to all connection in that channel.
so on and on ....
So basically, WebSocket provides a basic way of communicate, it is upto you to be creative to do thing.

For private (room) chat systems you really have to develop your own logics.
I would recommend you to use the following library:
http://socketo.me/
Go through their documentation at http://socketo.me/docs/ and start coding.
If you get stuck then post your code and the community is here to help

This is how I have done in Laravel, You need to install Predis , socket.io , ratchet and other dependencies . Please check https://laracasts.com/discuss/channels/general-discussion/step-by-step-guide-to-installing-socketio-and-broadcasting-events-with-laravel-51
Make one custom artisan command to run a websockets on some port using ratchet
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Ratchet\Server\IoServer;
class webSockets extends Command
{
/**
* The name and signature of the console command.
*
* #var string
*/
protected $signature = 'run:socket {port?}';
/**
* The console command description.
*
* #var string
*/
protected $description = 'Run websockets for specified port';
/**
* Create a new command instance.
*
* #return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* #return mixed
*/
public function handle()
{
$port = $this->argument('port');
$server = IoServer::factory(
new ChatController(),$port
$server->run();
}
}
Your controller should be like below
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class ChatController implements MessageComponentInterface {
public function onOpen(ConnectionInterface $conn) {
}
public function onMessage(ConnectionInterface $from, $msg) {
//FIRE A BROADCAST EVENT HERE
event(new MessageBroadcast(
$message,
$datetime,
$user_id
)
);
}
public function onClose(ConnectionInterface $conn) {
}
public function onError(ConnectionInterface $conn, \Exception $e) {
}
}
THE BROADCAST CLASS SHOULD LOOK LIKE BELOW
namespace App\Events;
use App\Events\Event;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Queue\SerializesModels;
class MessageBroadcast extends Event implements ShouldBroadcast
{
use SerializesModels;
public $message,$datetime,$userid;
public function __construct($message,$datetime,$userid)
{
$this->message = $message;
$this->datetime = $datetime;
$this->userid = $userid;
}
public function broadcastOn()
{
return ['test-channel'.$this->user_id];
}
}
Javascript part to subscribe a channel
<script src="{ { asset('js/socket.io.js') } }"></script>
<script>
//var socket = io('http://localhost:3000');
var socket = io('http://yourip:5000');
socket.on("test-channel1:App\\Events\\EventName", function(message){
// get user on console
console.log(message);
});
</script>
You need to run following command in backgroud
1. php artisan run:socket <port_no>
2. Node yourjavascript.js

Related

Laravel websocket send message to a specific user

Goal: I want a user to trigger an event which sends a message to another specific user related to that event.
The app: Several users are online and logged in at the same time. User A writes a message to user B. Only user B should receive this message.
Question: How can I tell the event handler that only user B gets a message? And how does the websocket know who the message goes to?
Where i can get all Sockets ids in the beyondcode Package?
I use Laravel 9 and the websocket package https://github.com/beyondcode/laravel-websockets.
use App\Models\User;
class ReputationUpdate implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $message;
public $targetUser;
public function __construct($message, User $targetUser)
{
$this->message = $message;
$this->tragetUser = $targetUser;
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
// How to handle it that only target User receive the message?
return new Channel('home');
}
}

How I make a Laravel Job to fail without restarting so I can use sentry to log the exception?

In my multi-server application I use laravel's Queueing system in order to run background jobs. Sometimes in my logic I want to make my job to throw an exception so I can log it via a sentry using the laravel library that offers.
So in my job:
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use App\Model\Etable\User;
use App\Model\User;
class MyJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
/**
* #var int
*/
private $user_id;
/**
* #param int $user The user that has opted or deopted for newsletter consent
*/
public function __construct(int $id)
{
$this->user_id = $user_id;
}
public function handle(): void
{
/**
* #var User
*/
$user=User::useWritePdo()->find($this->user_id);
if(empty($user)){
throw new \Exception("No such a user with user id: {$this->user_id}");
}
// Rest of logic here
}
}
Once the exception is thrown, I will to be logged into sentry but also it will keep on respawning as laravel's logic for jobs is supposed to do so.
In my case I think it as waste of resources to keep an respawning the MyJob in case that the user does not exist no value to keep on spawning because the logic itself cannot be performed in case that no user exists. On the other hand on any other error I want my job to keep on retrying till be able to sucessfully run again.
So how I can make my job not to respawn on specific errors? Even better would be as well if I can use the default logging method that laravel ofers in order to arbitary log an error into sentry as well via a sentry dedicated channel.
The best and easier approach is once the code failt to check how many times has already be executed:
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use App\Model\Etable\User;
use App\Model\User;
class MyJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
/**
* #var int
*/
private $user_id;
/**
* #param int $user The user that has opted or deopted for newsletter consent
*/
public function __construct(int $id)
{
$this->user_id = $user_id;
}
public function handle(): void
{
/**
* #var User
*/
$user=User::useWritePdo()->find($this->user_id);
if(empty($user)){
if ($this->attempts() > 1) {
return;
}
throw new \Exception("No such a user with user id: {$this->user_id}");
}
// Rest of logic here
}
}
This is being achieved via the:
if ($this->attempts() > 1) {
return;
}
So you throw once the exception, the exception is being logged into the Sentry and then on the second time that will be executed it will just exit and never respawn.
Please note that failed jobs won't be re-run unless you explicitly run
php artisan queue:failed
You can throw an exception and handle it in boot method of AppServiceProvider, deleting the job to avoid its further re-tries. See documentation on how to do it.

Laravel - Serialization of Job to be processed on remote system

Introduction / System Architecture
Server A - Laravel Installation, Database A
Server B - RabbitMQ Server
Server C - Laravel Installation, Database B
Server A is an API Endpoint, only receiving calls from remote sources. Depending on the call, it'll add a job to the RabbitMQ Server (Server B), which in turn is processed/listened to by Server C.
Server A contains a local copy of the exact same job that is processed by Server C. The job handler code and constructor are shown below (Job Code).
The Issue:
Server A can not serialize the job and add it to the queue, as it attempts to access information about a monitor that only exists in Database B (Server C). Server A has a copy of the model, but does not contain the actual database tables or records as it has no use for them - it's only meant to serialize the job and say "This is what you (Server C) should be doing."
However, upon issuing the job, it's also attempting to fetch database information (likely to serialize the exact data that will be required), which it fails to do as the records don't exist there.
My understanding of Laravel's SerializesModels was specifically that it would only serialize the model call itself, without actually doing anything database related. This does not appear to function, or I am misunderstanding/using it incorrectly - although very little documentation appears to be available.
Workarounds: One possible workaround would be to simply give Server A access to the database on Server C. This is in this case not an option, as it would break the design which is intended for high availability (where the API endpoint and queue should never be unavailable, but where the queue processor might be).
The code
Relevant Job Code
// Models
use App\UptimeMonitor;
// Notifications
use App\Notifications\StatusQueue as StatusNotification;
class StatusQueue implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $monitor_source;
protected $monitor_data;
protected $monitor_repository;
/**
* Create a new job instance.
*
* #param string[] monitor_source (Eg: HetrixTools)
* #param array[] monitor_data (All data passed from the source)
*
*/
public function __construct($monitor_source, array $monitor_data)
{
$this->monitor_source = $monitor_source;
$this->monitor_data = $monitor_data;
if($this->monitor_source === 'centric')
return $this->centric();
}
/**
* Centric Uptime Monitoring
*/
public function centric()
{
$result = ($this->monitor_data['monitor_status'] == 'online') ? 'online' : 'timeout';
try {
$monitor = UptimeMonitor::where('identifier', '=', $this->monitor_data['monitor_id'])->firstOrFail();
$status = $monitor->status()->firstOrFail();
$contacts = $monitor->contacts()->get();
} catch (Exception $e) {
return Log::error('[JOBS::StatusQueue::centric] - ' . $e);
}
$status->state = $result;
if(!$contacts)
return true;
foreach($contacts as $contact) {
$contact->notify(new StatusNotification($monitor, $status));
}
}
}
Other code
If you do require any other code, let me know! This should however cover the entire functionality of the job class itself. Other than that, all that's happening is issuing that job - and how that's done is obvious based on the constructor.
Question
The final question from all of this: Why is this failing (as in; why can it not serialize the job, without needing the database information?) - and do you see a way to work around this issue, to where I do not need access to the database from Server C to queue the job from Server A, still using Laravel's Queue mechanics?
Much obliged, as always!
Turns out, the easiest solution is almost always the right one.
Server A does not need to have a replica of the job that Server B will process - it can have a completely empty job with the same class, and server B will still process it correctly.
As a result, this is now the Job on Server A:
class AlertQueue implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $job;
/**
* Create a new job instance.
*
*/
public function __construct()
{
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
//
}
}
Whilst Server B also has an AlertQueue job, which has all of the logic that will actually get performed.

How to Implement Asynchronous Queue to run Method in Symfony 3

First off, some basic information about my project: I have a website built with Symfony 3. For some tasks I'm thinking about implementing to run asynchronous PHP methods. Some events use a lot of time but their results need not be immediately evident.
For instance: in method newOrder I have function addUserLTV who do few steps. The customer does not have to wait for all the steps to complete, only to get immediately the confirmation after the basic operation - 'newOrder' will add addUserLTV to queue and show immediately confirmation (finished run).
The queue tasks will be run when the server have time to do it.
public function addUserLTV( $userID, $addLTV )
{ //same code
}
How to do it? It is possible in symphony 3?
This is something you can easily do with enqueue bundle. Just a few words on why should you choose it:
It supports a lot of transports from the simplest one (filesystem) to enterprise ones (RabbitMQ or Amazon SQS).
It comes with a very powerful bundle.
It has a top level abstraction which could be used with the greatest of ease.
There are a lot more which might come in handy.
Regarding your question. Here's how you can do this with the enqueue bundle. Follow setup instructions from the doc.
Now the addUserLTV method will look like this:
<?php
namespace Acme;
use Enqueue\Client\ProducerInterface;
class AddUserLTVService
{
/**
* #var ProducerInterface
*/
private $producer;
/**
* #param ProducerInterface $producer
*/
public function __construct(ProducerInterface $producer)
{
$this->producer = $producer;
}
public function addUserLTV( $userID, $addLTV )
{
$this->producer->sendCommand('add_user_ltv', [
'userId' => $userID,
'ltv' => $addLTV]
);
}
}
It sends the message to a message queue using the client (top level abstraction I've mentioned before). The service has to be registered to the Symfony container:
services:
Acme\AddUserLTVService:
arguments: ['#enqueue.producer']
Now let look at the consumption side. You need a command processor that do the job:
<?php
namespace Acme;
use Enqueue\Client\CommandSubscriberInterface;
use Enqueue\Psr\PsrContext;
use Enqueue\Psr\PsrMessage;
use Enqueue\Psr\PsrProcessor;
use Enqueue\Util\JSON;
class AddUserTVAProcessor implements PsrProcessor, CommandSubscriberInterface
{
public function process(PsrMessage $message, PsrContext $context)
{
$data = JSON::decode($message->getBody());
$userID = $data['userID'];
$addLTV = $data['ltv'];
// do job
return self::ACK;
}
public static function getSubscribedCommand()
{
return 'add_user_ltv';
}
}
Register it as a service with a enqueue.client.processor tag:
services:
Acme\AddUserTVAProcessor:
tags:
- {name: 'enqueue.client.processor'}
That's it for coding. Run the consume command and you are done:
./bin/console enqueue:consume --setup-broker -vvv

Laravel echo pusher not receiving broadcast events

I've a problem that I'm trying to fix all day now. I've followed
this tutorial. The goal is to make a chat with Laravel echo, vue.js and pusher.
I've done everything exactly like the tutorial but for some reason I do not receive any events in my pusher console. Only the connection shows up:
But no events. The event that I fire looks like this:
<?php
namespace App\Events;
use App\Message;
use App\User;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MessageSent implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* #var
*/
public $user;
/**
* #var
*/
public $message;
/**
* MessageSent constructor.
* #param User $user
* #param Message $message
*/
public function __construct(User $user, Message $message)
{
$this->user = $user;
$this->message = $message;
}
/**
* Get the channels the event should broadcast on.
*
* #return Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('chat');
}
}
I fire the event like this:
broadcast(new MessageSent($user, $message))->toOthers();
When I dd('test'); like this in my MessageSent event class:
public function broadcastOn()
{
dd('test');
return new PrivateChannel('chat');
}
The dd('test'); shows up in my network tab.
I'm using Laravel 5.4 and Vue.js 2.0 with Homestead. What could be going on here?!
It looks likes you are following this tutorial. I also had a hard time to figure it out. I already answered here. Can you please check it out?
I worked on typing feature in the chat system. Please take a look at the code on GitHub.
Let me know if you have any questions. Thanks :)
By the looks of your debug console screenshot you are never managing to subscribe to any channels, have you set up the necessary authentication for the private channel subscription?
The complete demo code for the tutorial that you've been following is on github so you might want to take a look at that and see where yours differs.
If you are on Laravel 5.4, make sure you have set-up the channel authentication.
For example, in your routes/channels.php file, there should be something like this:
Broadcast::channel('chat', function ($user) {
return true; // change this to your authentication logic
});

Categories