Install ratchet library using composer.
composer require cboden/ratchet
Then create a websocket server file example websocker-server.php in project root. example code
<?php
// websocket-server.php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use App\WebSockets\Chat;
require __DIR__ . '/vendor/autoload.php';
$loop = React\EventLoop\Factory::create();
// Setup the secure websocket server
$webSock = new React\Socket\Server('0.0.0.0:8092', $loop);
$secureWebSock = new React\Socket\SecureServer($webSock, $loop, [
'local_cert' => '/var/www/puc.mywebsolutions.co.in/ssl/puc.mywebsolutions.co.in-le.crt',
'local_pk' => '/var/www/puc.mywebsolutions.co.in/ssl/puc.mywebsolutions.co.in-le.key',
'allow_self_signed' => false, // Should be false for production
'verify_peer' => false
]);
$server = new IoServer(
new HttpServer(
new WsServer(
new Chat()
)
),
$secureWebSock,
$loop
);
$server->run();
Then you need to create the chat class which will have methods for socket evens like onOpen, onMessage and onClose etc. like example below. This file Chat.php is typically placed in App/Websockets folder.
<?php
namespace App\WebSockets;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface {
protected $clients;
protected $locationConnections;
public function __construct() {
$this->clients = new \SplObjectStorage;
$this->locationConnections = [];
}
public function onOpen(ConnectionInterface $conn) {
$queryParams = [];
parse_str($conn->httpRequest->getUri()->getQuery(), $queryParams);
$this->clients->attach($conn);
// Handle locationId for both Flutter clients and staff
if (isset($queryParams['locationId'])) {
$locationId = $queryParams['locationId'];
echo 'location id '.$locationId;
$this->locationConnections[$locationId][$conn->resourceId] = $conn;
echo "Connection {$conn->resourceId} subscribed to location ID: {$locationId}\n";
}
}
public function onMessage(ConnectionInterface $from, $msg) {
echo "Message received from {$from->resourceId}: $msg\n";
$decodedMsg = json_decode($msg, true);
$locationId = $decodedMsg['locationId'] ?? null;
$messageData = [
'event' => 'NewMessage',
'data' => [
'text' => $decodedMsg['text'],
'username'=>$decodedMsg['username'],
'userId'=>$decodedMsg['userId'],
'time' => date('Y-m-d H:i:s'),
]
];
if ($locationId && isset($this->locationConnections[$locationId])) {
foreach ($this->locationConnections[$locationId] as $resourceId => $client) {
if ($from->resourceId !== $resourceId) { // Skip the sender
echo 'msg sent '.json_encode($messageData);
$client->send(json_encode($messageData));
}
}
} else {
echo "Location ID not found for this connection.\n";
}
}
public function onClose(ConnectionInterface $conn) {
if ($this->clients->contains($conn)) {
$this->clients->detach($conn);
foreach ($this->locationConnections as $locationId => $connections) {
if (isset($connections[$conn->resourceId])) {
unset($this->locationConnections[$locationId][$conn->resourceId]);
echo "Connection {$conn->resourceId} disconnected from location ID: {$locationId}\n";
}
}
}
}
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "An error has occurred: {$e->getMessage()}\n";
$conn->close();
}
}
and finally the chat interface where chatbox will appear this is typically handled in chat.blade.php like below
<div class="chat-boxes-container">
@foreach($locationData as $location)
<div class="chat-container" id="chat-container-{{ $location['id'] }}">
<div class="chat-box-title">Location - {{ $location['name'] }}</div>
<div class="chat-box" id="chat-box-{{ $location['id'] }}">
</div>
<textarea id="chat-input-{{ $location['id'] }}" placeholder="Type a message..." rows="3"></textarea>
<div><button style="margin-top:10px" onclick="sendMessage({{ $location['id'] }})" class="btn btn-primary">Send</button></div>
</div>
@endforeach
</div>
<style>
/* Container holding all chat boxes */
.chat-boxes-container {
display: flex;
justify-content: space-around;
gap: 20px;
overflow-x: auto;
max-width:1400px;
}
/* Container holding all chat boxes */
.chat-boxes-container {
display: flex;
justify-content: space-around;
gap: 20px;
overflow-x: auto;
flex-wrap: wrap;
max-width: 1300px;
}
.chat-container {
flex: 1 1 300px;
max-width: 500px;
border: 1px solid #ccc;
border-radius: 5px;
margin: 10px;
padding: 20px;
display: flex;
flex-direction: column;
}
/* Responsive adjustments */
@media (max-width: 1200px) {
.chat-container {
flex-basis: 250px;
}
}
@media (max-width: 768px) {
.chat-container {
flex-basis: 200px;
}
}
.chat-box-title {
background-color: #000;
color: #fff;
text-align: center;
padding: 5px;
border-radius: 5px 5px 0 0; /* Rounded top corners */
}
.chat-box {
flex-grow: 1; /* Allow chat box to take up remaining space */
height: 400px;
overflow-y: auto;
border-bottom: 1px solid #ccc;
margin-bottom: 10px;
/* other styles... */
}
/* Other styles... */
.message {
margin-bottom: 5px;
}
#chat-input {
width: 90%;
}
.message-time{
font-size:10px;
clear:both
}
</style>
<script>
var sockets = {};
@foreach($locationData as $location)
sockets[{{ $location['id'] }}] = new WebSocket(wss://puc.mywebsolutions.co.in:8092?locationId={{ $location['id'] }});
setupWebSocket(sockets[{{ $location['id'] }}], {{ $location['id'] }});
setupEnterKeyListener({{ $location['id'] }});
@endforeach
function setupWebSocket(socket, locationId) {
socket.onopen = function(event) {
console.log("Connection established for location " + locationId);
};
socket.onmessage = function(event) {
var message = JSON.parse(event.data);
console.log('message', message);
if (message.event === 'NewMessage') {
appendMessage(message.data, locationId);
saveMessageToDatabase(message.data.text,locationId,data.userId);
}
};
socket.onclose = function(event) {
console.log("WebSocket closed for location " + locationId + ". Attempting to reconnect...");
attemptReconnect(locationId);
};
socket.onerror = function(event) {
console.error("WebSocket error observed:", event);
};
}
function attemptReconnect(locationId) {
setTimeout(function() {
console.log("Reconnecting to location " + locationId);
sockets[locationId] = new WebSocket(wss://puc.mywebsolutions.co.in:8092?locationId=${locationId});
setupWebSocket(sockets[locationId], locationId);
}, 3000); // Reconnect after 3 seconds
}
function getLastUserIdFromChatHistory(locationId) {
var chatBox = document.getElementById('chat-box-' + locationId);
var lastMessage = chatBox.querySelector('.message:last-child');
var lastUserId = lastMessage ? lastMessage.dataset.userId : null;
return lastUserId;
}
function appendMessage(data, locationId) {
var chatBox = document.getElementById('chat-box-' + locationId);
var messageElement = document.createElement('div');
messageElement.classList.add('message'); // Add class for styling
messageElement.dataset.userId = data.userId;
var messageContent = '<strong>' + (data.username || 'unknown user') + ':</strong> ' + data.text;
if (data.time) {
messageContent += '<div class="message-time">' + data.time + '</div>';
}
messageElement.innerHTML = messageContent;
chatBox.appendChild(messageElement);
// Auto-scroll to the latest message
chatBox.scrollTop = chatBox.scrollHeight;
}
function setupEnterKeyListener(locationId) {
document.getElementById('chat-input-' + locationId).addEventListener('keypress', function (e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault(); // Prevent the default newline
sendMessage(locationId);
}
});
}
function sendMessage(locationId) {
var input = document.getElementById('chat-input-' + locationId);
var message = input.value.trim();
if(message !== '') {
var messageData = {
text: message,
username: "support",
locationId: locationId,
senderType: 'staff'
};
if (sockets[locationId].readyState === WebSocket.OPEN) {
sockets[locationId].send(JSON.stringify(messageData));
var userId=getLastUserIdFromChatHistory(locationId);
appendMessage(messageData, locationId);
saveMessageToDatabase(message,locationId,userId);
}
input.value = '';
}
}
function saveMessageToDatabase(messageText,locationId,userId) {
fetch('/save-message', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.head.querySelector("[name~=csrf-token][content]").content
},
body: JSON.stringify({ text: messageText,locationId:locationId })
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('Message saved:', data);
})
.catch(error => {
console.error('There was an error saving the message!', error);
});
}
</script>
and the related controller in this case the controller is a livewire component
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Chathistory; // Assuming you have a Chathistory model
use Illuminate\Support\Facades\Auth;
use App\Models\Staff;
class Livechat extends Component
{
public $messages;
private $staffId;
public function mount()
{
// Get the ID of the logged-in staff
$this->staffId = Auth::guard('staff')->id();
// Find the Staff model instance using the staff ID
$staff = Staff::find($this->staffId);
if ($staff) {
$locations = $staff->locations()->get(['id', 'name']);
$this->locationData = $locations->map(function ($location) {
return ['id' => $location->id, 'name' => $location->name];
});
} else {
$this->locationData = collect();
}
// Fetch messages for logged-in staff
//$this->messages = Chathistory::with('user')->where('staffId', $this->staffId)->get();
//dd($this->messages);
}
public function render()
{
return view('livewire.livechat', [
'staffId' => $this->staffId,
'locationData' => $this->locationData,
]);
}
}
Leave a Reply