It's my first post on here but i'm desperate on finding the solution and I might need someone a little more experienced than me. Let you first give you the system specifics:
OS: Almalinux, CPanel & WHM - VPS Framework: Laravel Composer: "php": "^8.2","laravel/framework": "^8.0","beyondcode/laravel-websockets": "^1.4", Certificate: WildCard certificate, official ( not self signed ). Websocket port : 6020 (because other services are using 6001)
I have been trying for a few days now to get websockets to work. When i run the websocket artisan:serve --port 6020 the websocket opens correctly and I can connect to it through postman. I can also connect to it through other external applications (online testing tools) but from within my Laravel pages I can't access it. The WSS connection is made successfully, I can see this in the WS(under developer console) and I can also see the connection within my terminal. But whenever I broadcast an event, its not being picked up by laravel. Besides that, the laravel-websockets page, is also unable to connect.. It is giving me "Channels current state is unavailable".
I hope I am being accurate enough and providing enough information because I really am hoping for a good soul who can point me in the direction of what I am doing wrong.
Some small context: This is for a reservation that can be made online, which is then processed by the controller and a notification should be shown on the dashboard of the restaurant. Saying there is a new reservation.
**Broadcasting file: **
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Broadcaster
|--------------------------------------------------------------------------
|
| This option controls the default broadcaster that will be used by the
| framework when an event needs to be broadcast. You may set this to
| any of the connections defined in the "connections" array below.
|
| Supported: "pusher", "ably", "redis", "log", "null"
|
*/
'default' => env('BROADCAST_DRIVER', 'null'),
/*
|--------------------------------------------------------------------------
| Broadcast Connections
|--------------------------------------------------------------------------
|
| Here you may define all of the broadcast connections that will be used
| to broadcast events to other systems or over websockets. Samples of
| each available type of connection are provided inside this array.
|
*/
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
'encrypted' => true,
'host' => 'my.examplewebsite.be',
'port' => env('LARAVEL_WEBSOCKETS_PORT', 6020),
'scheme' => 'https',
'curl_options' => [
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
],
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];
Websocketsfile:
<?php
use BeyondCode\LaravelWebSockets\Dashboard\Http\Middleware\Authorize;
return [
/*
* Set a custom dashboard configuration
*/
'dashboard' => [
'port' => env('LARAVEL_WEBSOCKETS_PORT', 6020),
],
/*
* This package comes with multi tenancy out of the box. Here you can
* configure the different apps that can use the webSockets server.
*
* Optionally you specify capacity so you can limit the maximum
* concurrent connections for a specific app.
*
* Optionally you can disable client events so clients cannot send
* messages to each other via the webSockets.
*/
'apps' => [
[
'id' => env('PUSHER_APP_ID'),
'name' => env('APP_NAME'),
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'path' => env('PUSHER_APP_PATH'),
'capacity' => null,
'enable_client_messages' => false,
'enable_statistics' => true,
],
],
/*
* This class is responsible for finding the apps. The default provider
* will use the apps defined in this config file.
*
* You can create a custom provider by implementing the
* `AppProvider` interface.
*/
'app_provider' => BeyondCode\LaravelWebSockets\Apps\ConfigAppProvider::class,
/*
* This array contains the hosts of which you want to allow incoming requests.
* Leave this empty if you want to accept requests from all hosts.
*/
'allowed_origins' => [
//
],
/*
* The maximum request size in kilobytes that is allowed for an incoming WebSocket request.
*/
'max_request_size_in_kb' => 250,
/*
* This path will be used to register the necessary routes for the package.
*/
'path' => 'laravel-websockets',
/*
* Dashboard Routes Middleware
*
* These middleware will be assigned to every dashboard route, giving you
* the chance to add your own middleware to this list or change any of
* the existing middleware. Or, you can simply stick with this list.
*/
'middleware' => [
'web',
'restaurant',
'api',
Authorize::class,
],
'statistics' => [
/*
* This model will be used to store the statistics of the WebSocketsServer.
* The only requirement is that the model should extend
* `WebSocketsStatisticsEntry` provided by this package.
*/
'model' => \BeyondCode\LaravelWebSockets\Statistics\Models\WebSocketsStatisticsEntry::class,
/**
* The Statistics Logger will, by default, handle the incoming statistics, store them
* and then release them into the database on each interval defined below.
*/
'logger' => BeyondCode\LaravelWebSockets\Statistics\Logger\HttpStatisticsLogger::class,
/*
* Here you can specify the interval in seconds at which statistics should be logged.
*/
'interval_in_seconds' => 60,
/*
* When the clean-command is executed, all recorded statistics older than
* the number of days specified here will be deleted.
*/
'delete_statistics_older_than_days' => 60,
/*
* Use an DNS resolver to make the requests to the statistics logger
* default is to resolve everything to 127.0.0.1.
*/
'perform_dns_lookup' => false,
],
/*
* Define the optional SSL context for your WebSocket connections.
* You can see all available options at: http://php.net/manual/en/context.ssl.php
*/
'ssl' => [
/*
* Path to local certificate file on filesystem. It must be a PEM encoded file which
* contains your certificate and private key. It can optionally contain the
* certificate chain of issuers. The private key also may be contained
* in a separate file specified by local_pk.
*/
'local_cert' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_CERT', null),
/*
* Path to local private key file on filesystem in case of separate files for
* certificate (local_cert) and private key.
*/
'local_pk' => env('LARAVEL_WEBSOCKETS_SSL_LOCAL_PK', null),
/*
* Passphrase for your local_cert file.
*/
'passphrase' => null,
/* 'passphrase' => env('LARAVEL_WEBSOCKETS_SSL_PASSPHRASE', null), */
'verify_peer' => false,
'verify_peer_name' => false,
],
/*
* Channel Manager
* This class handles how channel persistence is handled.
* By default, persistence is stored in an array by the running webserver.
* The only requirement is that the class should implement
* `ChannelManager` interface provided by this package.
*/
'channel_manager' => \BeyondCode\LaravelWebSockets\WebSockets\Channels\ChannelManagers\ArrayChannelManager::class,
];
bootstrap.js
window._ = require('lodash');
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
import Echo from 'laravel-echo';
window.Pusher = require('pusher-js');
window.Echo = new Echo({
broadcaster: 'pusher',
key: process.env.MIX_PUSHER_APP_KEY,
cluster: process.env.MIX_PUSHER_APP_CLUSTER,
wsHost: 'my.examplewebsite.be',
wssHost: 'my.examplewebsite.be',
wsPort: 6020,
wssPort: 6020,
forceTLS: true,
disableStats: false,
enabledTransports: ['ws', 'wss'],
});
**Script that listens to the channel ( don't mind the mess i made with the comments, i'm just trying to figure things out. **
<script type="module">
const restaurantId = <?= auth()->guard('restaurant')->user()->id ?>; // Assign the restaurant id
const channel = `reservation.${restaurantId}`;
/* const channel = `reservation.105`; */
console.log('Channel:', restaurantId);
// Subscribe to the private channel
Echo.private(channel)
.listen('ReservationEvent', (ReservationEvent) => {
// Handle the event data here
console.log('ReservationEvent received:', ReservationEvent);
// Extract reservation details from the event data
const reservation = ReservationEvent.reservationData;
const numberOfPeople = reservation.number_of_people;
const date = reservation.appointment_date;
const time = reservation.appointment_time;
// Create the toast message
const message = `New reservation available for ${numberOfPeople} people on ${date} & ${time}`;
// Override the default options for toastr
toastr.options = {
closeButton: true,
progressBar: true,
positionClass: 'toast-top-left',
preventDuplicates: true,
timeOut: 0,
extendedTimeOut: 0,
tapToDismiss: false,
onShown: function () {
// Add custom buttons dynamically
$('.toast-actions').html(`
<button type="button" class="btn btn-primary" onclick="goToReservations()">Go to reservations</button>
<button type="button" class="btn btn-secondary" onclick="closeToast()">Close</button>
`);
},
onCloseClick: function () {
closeToast();
}
};
// Show the reservation toast
toastr.info(message);
// Perform any necessary actions based on the event data
});
// Custom function to redirect to the reservations page
function goToReservations() {
window.location.href = '/reservations';
}
// Custom function to close the toast notification
function closeToast() {
$('.toast-close-button').click();
}
</script>`
** This is the form that should trigger the websocket and be loaded on the dashboard (controller) **
public function store(Request $request, $slug)
{
$request['channel'] = 2;
$this->validate($request, [
'appointment_date' => 'required',
'appointment_time' => 'required',
'adults' => 'required',
'number_of_people' => 'required',
'locale' => 'required',
'first_name' => 'required',
'last_name' => 'required',
'phone' => 'required',
// 'is_terms_checked' => 'required',
// 'have_covid' => 'required'
]);
$restaurant = $this->restaurantRepository->getRestaurantFromSlug($slug);
/* \Log::info('Restaurant:', ['restaurant' => json_encode($restaurant, JSON_PRETTY_PRINT)]); */
if ($request->appointment_time && $request->appointment_date) {
$setting = $this->settingModel->where('restaurant_id', $restaurant->id)->first();
/* \Log::info('setting:', ['setting' => $restaurant]); */
$restaurant = $this->restaurantRepository->getRestaurantFromSlug($slug);
/* \Log::info('setting:', ['setting' => json_encode($setting, JSON_PRETTY_PRINT)]); */
try {
$time = strtotime($request->appointment_time);
$appointmentTime = date('H:i:s', $time);
$inputs = $request->except('_token');
$inputs['restaurant_id'] = $restaurant->id;
$inputs['appointment_date'] = Carbon::createFromFormat('d/m/Y', $request->appointment_date)->format('Y-m-d');
$inputs['appointment_time'] = $appointmentTime;
$inputs['appointment_rescheduled_date'] = Carbon::createFromFormat('d/m/Y', $request->appointment_date)->format('Y-m-d');
$inputs['appointment_scheduled_time'] = $appointmentTime;
$inputs['locale'] = strtolower($request->locale);
$inputs['language'] = strtoupper($request->locale);
$inputs['status'] = '0';
// add prefix to phone number
if (isset($request->phone) && $request->phone) {
$customerPhoneNumber = CommonFunction::formatPhoneNumber($request->phone,ltrim($request->country_code,"+"));
$inputs['phone'] = $customerPhoneNumber;
}
$reservation = $this->model->create($inputs);
if ($reservation) {
// send message to restaurant
$isSmsEnabled = config("examplewebsite.sms.is_enabled");
$smsServiceStatus = $setting->sms_service_status;
$emailServiceStatus = $setting->email_service_status;
$restaurantPhoneNumber = $setting->res_confirmation_number?$setting->res_confirmation_number:$restaurant->phone;
$restaurantPhoneNumber = CommonFunction::formatPhoneNumber($restaurantPhoneNumber);
$availableSmsCount = (int) $setting->available_sms_count;
$RestaurantLanguage = $restaurant->setting->defualt_language;
$RestaurantBackendLink = config("examplewebsite.urls.restaurant_backend_url");
$restaurantID =$restaurant->id;
if ($isSmsEnabled && $smsServiceStatus && $availableSmsCount && $availableSmsCount > 0 && $restaurantPhoneNumber) {
SendTextMessagesToRestaurant::dispatch($RestaurantBackendLink, $restaurantPhoneNumber, $RestaurantLanguage, $restaurantID)
->onConnection('textmessages')
->onQueue('textmessages');
$setting->available_sms_count = $availableSmsCount - 1;
$setting->save();
\Log::info('test2');
}
if ($emailServiceStatus) {
$status = 0;
$CustomerLanguage = $request->locale;
$countGuest = $request->adults + $request->kids;
$restaurantDetails = Restaurant::where('id', $restaurant->id)->first();
$restaurantSettings = Setting::where('restaurant_id', $restaurant->id)->first();
$customerPostFields = [
"To" => [$request->email],
"Data" => [
"Restaurant_Name" => $setting->site_name?$setting->site_name:$restaurantDetails->first_name.' '.$restaurantDetails->last_name,
"guest_name" => $request->first_name.' '.$request->last_name,
"day" => $request->appointment_date,
"time" => $request->appointment_time,
"booking_amount" => $countGuest,
"Restaurant_PhoneNumber" => $restaurantPhoneNumber,
"Restaurant_E-mail" => $restaurantDetails->email,
"Restaurant_Address_Street_And_Number" => $restaurantDetails->house_number." ".$restaurantDetails->street,
"Restaurant_Address_City" => $restaurantDetails->province,
"Restaurant_Address_ZipCode" => $restaurantDetails->zip_code,
"Restaurant_Address" => $restaurantDetails->house_number.' '.$restaurantDetails->street.' '.$restaurantDetails->province.' '.$restaurantDetails->zip_code.' '.$restaurantDetails->country,
"Restaurant_Facebook_Link" => $restaurantSettings->fb_url,
"Restaurant_Website_Link" => $restaurantSettings->website_url?$restaurantSettings->website_url:'https://www.smartresto.be',
"Restaurant_VAT_Number" => $restaurantDetails->VAT_number
],
];
// IF EMAIL TO CUSTOMER IS ENABLED IN SETTINGS//
if ($setting->res_cust_email) {
SendEmailJobCustomer::dispatch($customerPostFields, $CustomerLanguage, $status, $countGuest)->onQueue('emails');
$setting->mail_count = $setting->mail_count+1;
$setting->save();
}
// IF EMAIL TO RESTAURANT OWNER IS ENABLED IN SETTINGS//
if ($setting->res_owner_email) {
$CountOfGuests = $request->adults + $request->kids;
$languageOfRestaurant = $setting->defualt_language;
$RestaurantOwnerEmail = [$setting->res_confirmation_email?$setting->res_confirmation_email:$restaurantDetails->email];
$RestaurantPostField = [
"To" => $RestaurantOwnerEmail,
"Data" => [
"DateOfReservation" => date('Y-m-d'),
"RestaurantOwnerFirstName" => $setting->site_name?$setting->site_name:$restaurantDetails->first_name,
"CountOfGuests" => $CountOfGuests,
"Date" => $request->appointment_date,
"Time" => $request->appointment_time,
"FirstName" => $request->first_name,
"LastName" => $request->last_name,
"CustomerEmail" => $request->email,
"CustomerTelephone" => $request->phone,
"AlreadyVisitedRestaurant" => $request->already_visited,
"AdditionalInfo" => $request->additional_information,
"Accept_Link_To_This_Reservation" => route('restaurant.reservations.index').'/view/'.$reservation->id,
"deny_Link_To_This_Reservation" => route('restaurant.reservations.index').'/view/'.$reservation->id,
],
];
SendEmailJobRestaurant::dispatch($RestaurantPostField, $status, $languageOfRestaurant, $CountOfGuests)->onQueue('emails');
$setting->mail_count = $setting->mail_count+1;
$setting->save();
}
}
$reservation['appointment_date_formatted'] = Carbon::createFromFormat('Y-m-d', $reservation->appointment_date)->format('d-m-Y');
$notification = $this->notificationModel->create([
'restaurant_id' => $restaurant->id,
'reservation_id' => $reservation->id,
'notification_type' => 'new_reservation',
'notification_data' => json_encode($reservation),
]);
$data = [
'message' => __('Registration successful.'),
];
\Log::info('Before broadcasting ReservationEvent. Reservation:', ['reservation' => $reservation->toArray(), 'restaurantID' => $restaurantID]);
$reservationData = $reservation->toArray();
$reservationData['channel'] = 2;
broadcast(new ReservationEvent($reservationData, $restaurantID))->toOthers();
\Log::info('ReservationEvent broadcasted.');
\Log::info('ReservationController restaurantID: ' . $restaurantID);
\Log::info('ReservationController reservation:', $reservation->toArray());
return response()->json($data, $this->statusCodes['success']);
}
return response()->json($data, $this->statusCodes['serverSide']);
} catch (\Exception $e) {
\Log::error('Exception occurred: '.$e->getMessage());
$data = [
'message' => $e->getMessage()
];
return response()->json($data, $this->statusCodes['serverSide']);
}
} else {
$setting = $this->settingModel->where('restaurant_id', $restaurant->id)->first();
try {
$inputs = $request->except('_token');
$inputs['restaurant_id'] = $restaurant->id;
$isSaved = $this->model->create($inputs);
if ($isSaved) {
// send message to restaurant
$isSmsEnabled = config("examplewebsite.sms.is_enabled");
$smsServiceStatus = $setting->sms_service_status;
$restaurantPhoneNumber = $restaurant->phone;
$restaurantPhoneNumber = CommonFunction::formatPhoneNumber($restaurantPhoneNumber);
$availableSmsCount = (int) $setting->available_sms_count;
if ($isSmsEnabled && $smsServiceStatus && $availableSmsCount && $availableSmsCount > 0 && $restaurantPhoneNumber) {
$spryngUsername = config("examplewebsite.sms.username");
$spryngPassword = config("examplewebsite.sms.password");
$spryngCompany = config("examplewebsite.sms.company");
$spryngKey = config("examplewebsite.sms.key");
$message = __("New Customer Registered!");
$spryng = new Client($spryngUsername, $spryngKey, $spryngCompany, $spryngKey);
// $balance = $spryng->sms->checkBalance();
try {
$spryng->sms->send($restaurantPhoneNumber, $message, [
'route' => 'business',
'allowlong' => true,
]);
$setting->available_sms_count = $availableSmsCount - 1;
$setting->save();
} catch (InvalidRequestException $e) {
Log::info($e->getMessage());
}
}
$data = [
'message' => __('Registration successful.'),
];
return response()->json($data, $this->statusCodes['success']);
}
\Log::info('Before broadcasting ReservationEvent. Reservation:', ['reservation' => $reservation, 'restaurantID' => $restaurantID]);
\Log::info('Type of reservation before broadcasting:', gettype($reservation));
\Log::info('Type of restaurantID before broadcasting:', gettype($restaurantID));
broadcast(new ReservationEvent($reservation, $restaurantID))->toOthers();
\Log::info('ReservationEvent broadcasted.');
return response()->json($data, $this->statusCodes['serverSide']);
} catch (\Exception $e) {
\Log::error('Exception occurred: '.$e->getMessage());
$data = [
'message' => $e->getMessage()
];
return response()->json($data, $this->statusCodes['serverSide']);
}
}
}
Hopefully there's somebody out there, who can (probably, point me on my own mistakes).. Grateful thanks :)
I have changed all possible settings and tried debugging but i'm not getting any information and Google is also not helping me.
Restarted the whole configuration twice, rebooted the server. Checked the VPS firewall, checked Cpanel/WHM firewall everything seems to work but it seems to be an issue within my code of laravel I think.