laravel event dispatch from controller - laravel

Alright so I have been trying to set up a spreadsheet application with concurrent editing. I went down the laravel echo, redis, sockets route. (Any advice to just use pusher will be summarily dismissed). Now for the most part I have this working and I can call my event from tinker and see the data I want flow through redis to sockets and get picked up by my frontend. However the call simply does not work when placed in a controller; its like its just being ignored. For simplicity sake I removed all code from the controller function except for event(new SalesPricingReportEdit($request)); I can use this exact code in tinker storing the exact source of the request from dev tools {"change":[[0,"future contract price",null,750]],"customer_id":101443,"item_id":"MOFT0602-1-550"} and my ws responds on multiple instances of the page across different computers with the data attached to the WS. I can post any code anyone needs to help just let me know what you want to see.
EDIT(resolved): Alright so now that I am mostly finished with this project I have my write up I figured Id post it for posterity this is a process guide that lead me to successfully using laravel events with redis and socket-io via echo.
Register your new Event in laravel go to app/Providers/EventServiceProvider.php and put in the fully namespaced path for your Event that doesnt exist yet. Using the default namespace will save you a headache in implemntation but you can use a non-default
if you wish to gouge your own eyes out while you attempt to route the events on your frontend.
protected $listen = [ 'App\Events\EventName.php' => [
//you would put a handler here but for my case i was only dealing with an
//Event generated by server and handled by the client(browser)
//so it wasn't needed. KISS
],
Once you save that you just use the artisan generate event command and it will read the listener array and scaffold you up an event.
note: While we are in the providers folder lets just note the broadcastServiceProvider and its reference to the routes/channel file this isn't very important for me now but its good to remember
In your app/Events/EventName.php we will be configuring our event. Most of this is standard but there will be a few gotchas I dont want to forget about.
your class should by default implement ShouldBroadcast ... I changed this to shouldBroadcastNow as well as setting my default QUE_DRIVER=sync in global .env and config to sync .. this just avoids an extra overhead of queing the jobs..probably dumb in the long term but, for now queing is a bit overkill.
continue in app/Events/EventName.php
Next notice your construct function. To pass data through an event you need to declare a public variable in the class and set the value for that inside your constructor. By Default laravel will/should/suppposed to pass any public variable in the
class with the event.
public $change;
public function __construct($request){
$this->change = json_decode($request, true);
}
Next in your broadcastOn() function you want to return a new channel...essencially
return new [Type]Channel([channel-name]);
make sure you have all the facades you need in your header.
the broadcastWith() function will let you manipulate the data sent with the event.
I did not use the broadcastAs() function but, it is supposed to allow you to change the Event Name rather than using the default class name.
Now we are going to go to our controller file where we plan to actually fire the event from app/Http/Controllers/xxxxxxxxxController.php
use the syntax
event(new EventName($data))
when you want the event to get kicked off. this will not work as we have not set up redis or sockets but to test this is all in working order go to your .env and change the BROADCAST_DRIVER=log. once done wipe cache and do whatever it is you need to do to get the controller function to run(either by using tinker or by a frontend path you plan on using with the event). you should then be able to see the storage/app/logs/laravel.log contain the information from your event.
Go ahead and install redis on your server and use basic laravel configuration in .env and in config/database.php the only thing to change is to add in the .env BROADCAST_DRIVER=redis.
To avoid some complications go ahead and make sure your CACHE_DRIVER to file.
I have not yet set up multi connections with redis to let redis work both as broadcast and cache; ik its possible but I havent done it. so for now cache_driver can go away.
Also I tried to set this up using PHPredis but I admitted defeat and just used predis. Anyone that can figure this out I will venmo 50 bucks.
Finally to test that you have done this correctly go into
redis-cli > PING ...resp: PONG
Also you can use redis-cli monitor to verify that event data is indeed being pushed to redis.
Now we will start setup with npm we are going install laravel-echo-server, I will also get the pm2 package to act as my daemonizer for echo server.bash it up with
` #!/var/www/repoting/ bash
laravel-echo-server start`
then just run pm2 start socket.sh
when testing you might want to run laravel-echo-server start straight from the cli because the output stream will let you know when it observed an event broadcast in redis.
we will also need the
npm --save socket.io-client
or websockets don't work.
Lets go to resources/assets/js/bootstrap.js
import Echo from "laravel-echo";
window.io = require('socket.io-client');
if (typeof io !== 'undefined') {
window.Echo = new Echo({
broadcaster: 'socket.io',
host: window.location.hostname + ':6001',
});
}
This is initializing our websocket connection.
Now in our blade file we are going to subscribe to the channel and listen for events. Make sure to include the :6001/socket.io/socket.io.js file or your callbacks will fail
Why are we not doing this in the bootstrap.js you ask? because part of the channel name I am using is being passed by the controller with the view so to subscribe to the right channel I need something like
channel-name-{{$customer_id}}.
What you say I should have properly set up vue and done things the way they were designed to be done in vue....go home your drunk.
So now in resources/views/xxxxx.blade.php
I am using this in conjunction with handsontables so I am going to insert my listener in the afterRender hook.
Echo.channel('EventName-{{$customer_id}}')
.listen('EventName',function(data){
console.log(data);
});
any confusion here about what goes in the channel vs what goes in the listen clause can think of it as using the channelName and then the eventName by default the class name. There seems like in the past to have been some issues
with the class name being namespaced properly but I did not experience any problems.
this should all lead to seeing a console.log with your event data on any page subscribed to the the channel. But for prosperity sake here is the testing order to help verify each step along the way.
Troubleshooting steps
Is the event configured properly in laravel? Change the BROADCAST_DRIVER=log and verify you get a log entry when you run your controller.
Is the data actually being pushed to redis? with the right channel, etc? open redis-cli montior while you trigger an event and you should see feedback like "PUBLISH" "channel-name" "message"
Is echo server/socketio-server registering the event? look at your terminal that is running the laravel-echo-server start command you should see the event propagate from redis to echo there
Is your bootstrap.js file giving you an open web socket? go to dev-tools and check the network->ws if you see an entry click it and then on messages; make sure you see sent and recieved data. If you dont something is wrong in bootstrap.js
Is your clientside js retrieving data from the websocket event? if not you probably forgot to include you socket.io.js file.
Next Steps
-Implement whisper channels to notify the page of who is currently using the page as well as to unsubscribe the from the echo channel and remove any unneeded listeners
-pass handsontable selected data through the whisper channel and set those cells to readonly where the user is different from the current user

I ended up just adding the redis facade and calling Redis::publish(). My guess here is that I couldn't call an event without having a listener setup. I don't need a listener I just needed the event to fire and push it to redis. the real confusing part is why this works without a hitch in tinker and not in a controller is still a mystery but, I solved the immediate problem so this just gets chalked up to an edge case where I implemented something differently than it was probably designed to be.

Related

Laravel Jobs are not processed when triggered by a Redis subscribed message

I've some mistake with a job that I try to dispatch when my Redis subscribe command receive a message.
I launch the Redis subscribe inside an "artisan console" command :
Redis::subscribe(['channel'], function ($message) {
dipatch((new MyJob($message)
->onQueue('default')
->onConnection('redis'));
}
Job is created and I can see it on my Laravel Horizon dashboard. But it's never processed... "handle" function is never called and the job stay in "pending" tab on Horizon.
But when I dispatch it from a tinker session, that's work fine!
Maybe I have to call another artisan command to launch the job outside the redis subscribe function, but hope there is a better solution...
Any ideas?
Solution :
create another connection on the database.php file for your Redis database (same params as the default one, just change the name, eg name it "subscribe"), so after that change, the code must look like this :
$redis = Redis::connection('subscribe');
$redis->subscribe(['channel'], function () {});
I had the same problem, The job will never be dispatched on Redis queue. When I used another queue driver (database or beanstalkd) it worked fine.

Prefixing all broadcast channels when in dev environment in my Laravel app

I use Pusher with my Laravel app to notify of changes in real-time to web browsers to update pages accordingly.
I have 2 environments where (local & production) where events are broadcasted, I have found because both sites might be active and because they subscribe and push to the same channel names they cause undesired updates.
I was wondering if there is a way to prefix any broadcast channels with a value somehow? The alternative is that I go through each event and prefix the channel names (which is possible but I feel there could be a more "efficient" way).
If you are using public channels then you check the environment usingapp()->environment()
And if it is using private channels then use uuid instead of id of the user.
it will return the value of APP_ENV from .env file
If it is local then you can prefix your channel name like shown below
Broadcast::channel((app()->environment() =='local' ? 'prefix-': '').'channel-{id}',
function ($user,$id)
{return true}
);
Now you will do the same in EventClass in broadcastAs method
return new PrivateChannel((app()->environment() =='local' ? 'prefix-': '').'channel-'.$this->receiverId);
Now in production make sure you change APP_ENV=production and APP_ENV=local in your local development

Where to check if an User is logged in in a Laravel Application?

I've been using your advice and View::sharing all of my important data to all views. However, there is one issue I have encountered.
This code:
if(!Auth::guest()){
$user=Auth::user()->id;
}
else $user=0;
$temp=DB::select('query');
View::share('cartnumber', count($temp));
View::share('cartitems', $temp);
doesn't work when put in AppServiceProvider. Or better, it always sets $user=0, even if I am logged in. I thought it is because AppServiceProvider's boot function executes before the site checks if someone is logged in.
I then tried to use a BaseController with a construct function but that doesn't work either. The only solution that seems to work correctly is putting the code in every single Controller for every view! That actually works, which kind of confirms my theory.
But is there anywhere I can put this code without having to copy/paste it in every single Controller? Thanks in advance!
You'd likely want to put this code later in the request life cycle to guarantee an auth user because as others have mentioned middleware/session code has not occured during this part of the framework booting up. You could use a service class to call in all your controllers to avoid the copy pasting. Or If you'd like to achieve this using code in your service provider you could use a View Composer instead of a share this allows you to define a callback/or class that will be called right before the view is returned
view()->composer(['/uri-that-needs-data'], function ($view) {
if (Auth::check()) {
$cart = DB::query(...)->get();
$view->with('cartitems', $cart);
}
});
Check out https://laravel.com/docs/5.7/views#view-composers for more details.
Auth::user() will be empty until the session middleware has run.
The reason you can't access the user inside your service provider is because that code is run during the "bootstrapping" phase of the application lifecycle, when it's doing things like loading filesystem or cache drivers, long before the request is sent through response handlers (including middleware).
Once the application has been bootstrapped and all service providers
have been registered, the Request will be handed off to the router
for dispatching. The router will dispatch the request to a route or
controller, as well as run any route specific middleware.
Source: https://laravel.com/docs/5.7/lifecycle
If you don't want to copy/paste that code everywhere, then one place to put it is in custom route middleware. You can list it after the auth middleware to guarantee a logged-in user.
Edit: View composers are another really good option, as suggested by #surgiie. The reason these can be set up inside a service provider (unlike your example) is because the view composer registers a callback, but doesn't execute it until a much later stage in the application lifecycle.

Why does the events I created with Laravel have the 'App\Events' appended in the beginning?

I created events using Laravel and am utilizing it in conjunction with Pusher. On my Pusher Debug Console, every time I fire off a event from my Laravel application, it shows up as 'App\Events\testEvent', whereas I need it to show up as just 'testEvent' like it does when I fire off a dummy event using the tool in the Pusher Debug Console.
https://www.screencast.com/t/cxQ30SZh5x
How do I type the code into my Laravel application so that it fires off the event as 'testEvent' instead of 'App\Events\testEvent'?
To fire the event, I am just using web.php in routes folder as follows:
Route::get('/testEvent', function() {
event(new testEvent());
});
You may customize the event name that gets broadcasted using the broadcastAs function on your Event class.
See: https://laravel.com/docs/5.4/broadcasting#broadcast-name
public function broadcastAs()
{
return 'testEvent';
}
It is the namespace the event classes are created under. Look at the top of your event files and you will see namespace App\Events;
Remove the namespace if you don't need it there, although why do you need it removed? I suspect there is another issue. Laravel events and Pusher have been working well together for quite some time.

Laravel event not being Broadcast

I am working on a chat/message feature for an app. When a User sends a message, I want a specific Event to be triggered and to be broadcasted. I am then wanting Laravel Echo to listen on the event's broadcast channel to an event via Laravel Echo, using the pusher driver.
I am wanting the Laravel Echo implementation to be in a Vue component and to retrieve the data that was broadcasted via the event. Currently, I can't get an event to broadcast to either a private channel or just a channel.
Here is what I have done:
1) Added the config to .env.
2) Added config to bootstrap.js :
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'mykey',
encypted: false
});
3) Added auth check in App\Providers\BroadcastServiceProvider (just returning true if User is signed in for now):
Broadcast::channel('chat', function ($user) {
// verify that user is recipient of message
return \Auth::check();
});
4) Added App\Providers\BroadcastServiceProvider::class, to App/config/app.php.
5) Created the Event App\Events\UserSentMessage, which broadcasts on a PrivateChannel named chat. I have also tried to broadcast to a Channel. I checked and the event is being fired from a call to the event() helper in a controller, it just seems to not be broadcast. I have also tried using the broadcast method in the controller mentioned.
6) Added the following within the mounted Vue life cycle hook in a valid Vue component (component is rendering data etc as expected, just not working with Laravel Echo atm):
Echo.private('chat').listen('UserSentMessage', (data) => {
console.log(data);
}, (e) => {
console.log(e);
});
Have also tried:
Echo.channel('chat').listen('UserSentMessage', (data) => {
console.log(data);
}, (e) => {
console.log(e);
});
The Vue component is successfully subscribing to the channel specified. The pusher debug console recognized that it is being subscribed to, but the Event, when triggered never gets broadcasted.
Seems to be that there is a problem with broadcasting the event. I have also tried using the broadcast() helper, rather than the event() helper.
Also, when subscribing to the event with Laravel Echo via Echo.private(). It seems to prefix the channel name with private, so I have also tried broadcasting (in the event) to a channel named private-chat.
I can recommend You to check Your .env file and make sure BROADCAST_DRIVER is not log (try pusher) and also don't forget to keep running queue listener:
php artisan queue:listen
UPD.: if You look for more advanced solution, read about laravel horizon
the event will not be broadcasted immediately, it will be send to the queue, only when you listen or work on the queue, the event will be consumed. If you want execute jobs immediately, set QUEUE_DRIVER=sync . see: laravel queue
In my case, my event class file is generated by "make:event", it does have broadcastOn method... But have no implements ShouldBroadcast~
It causes the laravel-echo-server output nothing~
Laravel Framework 8.13.0
I have upvoted Num8er's reply but i also found some other issues so maybe they can help you too.
1) Remove all 'private-' from the beginning of your channel names. These get handled behind the scenes by the various libraries (eg socket.io/laravel-echo). If you look at any debug output you will see 'private-' prepended to the front of the channel names but you DO NOT need to add these.
2) If you're using redis then make sure the REDIS_PREFIX is set to an empty string in your .env file. By default it's set to something like 'laravel_database' and this messes up the channel names.
REDIS_PREFIX=
3) Make sure you've got your laravel-echo server running (and redis/pusher). Also you need to make sure you have the queue worker running:
php artisan queue:work
4) It's worth enabling debugging output on your Echo Server as this will tell you useful info about traffic (eg channel names) so modify the laravel-echo-server.json file (in your project root dir) so that devMode is set to true:
"devMode": true,
#materliu: that was a good hint. In my case i did not create a queue with the working directory set to the project i am working on. Using circus. The event was triggered in code but no job listener was available. As soon as i run "php artisan queue:listen" suddenly all previous broadcasts have been processed.
Usually its difficult to figure out which component of a complex system does not work.

Resources