how to execute events asynchronously in laravel - php

Pls I'm still new to laravel and I have used events in laravel a couple of times but I'm curious and would like to know if it's possible to execute an event in laravel asynchronously. Like for instance in the code below:
<?php
namespace mazee\Http\Controllers;
class maincontroller extends Controller
{
public function index(){
Event::fire(new newaccountcreated($user)) ;
//do something
}
Is it possible for the block of code in the event listener of the "newaccountcreated" event to be executed asynchronously after the event is fired ?

Yes of course this is possible. You should read about Laravel Queues. Every driver (only not sync driver) are async. The easiest to configure is the database driver, but you can also want to try RabbitMQ server , here is Laravel bundle for it.
You can also add to your EventListener: newaccountcreated trait Illuminate\Queue\InteractsWithQueue (you can read about him here) which will helps you to connect it with Laravel Queue.

Filip's answer covers it all. I will add a bit more to it. If you push an event it will goto the default queue. You can specify a queue name as well. Have the listener class implements ShouldQueue and just include the queue method in the listener class like below.
/**
* Push a new job onto the queue.
**/
public function queue($queue, $job, $data)
{
return $queue->pushOn('queue-name', $job, $data);
}

Related

Laravel queue job with SerializesModels trait retrieves outdated model data

I'm using the Laravel queue through redis to send notifications. But whenever I pass models to the notifications, their properties are outdated when the notification is sent through the queue.
This is basically what I'm doing:
In controller (or somehwere else):
$thing = new Thing(); // Thing is a model in this case.
$thing->name = 'Whatever';
$thing->save();
request()->user()->notify(new SomethingHappened($thing))
SomethingHappened.php:
<?php
namespace App\Notifications;
use App\Thing;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class SomethingHappened extends Notification implements ShouldQueue
{
use Queueable;
public $thing;
public function __construct(Thing $thing)
{
$this->thing = $thing;
}
public function via($notifiable)
{
return ['mail'];
}
public function toMail($notifiable)
{
dump($this->thing->name); // null
return (new MailMessage())
// ...
// ...
// ...
;
}
}
Now when I add a delay of a few seconds (i.e. (new SomethingHappened($thing))->delay(now()->addSeconds(5))), the retrieved model is up-to-date. Also, before I deployed enough queue workers (and they were lagging behind on a filling queue), this issue didn't exist. Therefore, it appears now that when queue job gets processed really quickly, it doesn't retrieve the model from the database correctly or the model isn't saved yet. I have no idea why this could be happening, since the save call on the model is clearly executed (synchronously) before dispatching the notification, so there should be no way it isn't saved yet when the job is processed.
I'm running on Kubernetes and using a MySQL database. Everything is on GCP (if this is relevant).
Does anyone have an idea? Adding a delay of a few seconds is not a great solution and shouldn't be necessary.
It appears our database was using too much CPU, causing it to switch to the failover regularly. I think this was the cause of the issue. After implementing tweaks reducing the database load, the problem was resolved. A workaround for this issue would be to add a slight delay (as noted in my question).
I'm not sure why this only seemed to affect notifications, and not other queue jobs or requests. If anyone has a more detailed explanation, feel free to post a new answer.

Laravel 5.6 Job Event Queue::after not firing after queue processed

In an application I am working, I've both Job and Event Listener implemented Should Queue. In the queue, I perform a database insert and I want after the queue complete, I want to remove the previous cache. So I use Queue Job Event like this example:
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Queue::after(function (JobProcessed $event) {
Log::info('[QUEUE COMPLETE]', $event->job->getName());
});
}
public function register()
{
//
}
}
But the event is never fired and there is no log found in storage/log folder. I use daily logging channel.
Why is it not logging?
Answering my own question after solving this.
All the code is fine, I just needed to stop the queue:work and start it again (restart). After this, the Queue::after event started to fire and all worked perfectly.

Testing listeners with Queue::fake()

My Laravel 5.5 application has a Product model. The Product model has a dispatchesEvents property that looks like this:
/**
* The event map for the model.
*
* #var array
*/
protected $dispatchesEvents = [
'created' => ProductCreated::class,
'updated' => ProductUpdated::class,
'deleted' => ProductDeleted::class
];
I also have a listener that is called CreateProductInMagento which is mapped to the ProductCreated event in the EventServiceProvider. This listener implements the ShouldQueue interface.
When a product is created, the ProductCreated event is fired and the CreateProductInMagento listener is pushed to the queue and is run.
I am now trying to write a test for all of this. Here is what I have:
/** #test */
public function a_created_product_is_pushed_to_the_queue_so_it_can_be_added_to_magento()
{
Queue::fake();
factory(Product::class)->create();
Queue::assertPushed(CreateProductInMagento::class);
}
But I get a The expected [App\Listeners\Magento\Product\CreateProductInMagento] job was not pushed. error message.
How do I test queueable listeners using Laravel's Queue::fake() method?
The problem here is that the listener is not the job pushed to the queue. Instead, there's a Illuminate\Events\CallQueuedListener job that is queued and will in turn call the appropriate listener when resolved.
So you could do your assertion like this:
Queue::assertPushed(CallQueuedListener::class, function ($job) {
return $job->class == CreateProductInMagento::class;
});
Running artisan queue:work won't solve the issue because when testing, Laravel is configured to use the sync driver, which just runs jobs synchronously in your tests. I am not sure why the job is not being pushed, though I would guess it has to do with Laravel handling events differently in tests. Regardless, there is a better approach you can take to writing your tests that should both fix the issue and make your code more extendable.
In your ProductTest, rather than testing that a_created_product_is_pushed_to_the_queue_so_it_can_be_added_to_magento, you should simply test that the event is fired. Your ProductTest doesn't care what the ProductCreated event is; that is the job of a ProductCreatedTest. So, you can use Event Faking to change your test a bit:
/** #test */
public function product_created_event_is_fired_upon_creation()
{
Event::fake();
factory(Product::class)->create();
Event::assertDispatched(ProductCreated::class);
}
Then, create a new ProductCreatedTest to unit test your ProductCreated event. This is where you should place the assertion that a job is pushed to the queue:
/** #test */
public function create_product_in_magento_job_is_pushed()
{
Queue::fake();
// Code to create/handle event.
Queue::assertPushed(CreateProductInMagento::class);
}
This has the added benefit of making your code easier to change in the future, as your tests now more closely follow the practice of only testing the class they are responsible for. Additionally, it should solve the issue you're having where the events fired from a model aren't queuing up your jobs.

Phalcon fire event from controller

I am using the Phalcon micro application as my REST web service. I want to add an event to the application and fire this event from different places like controllers.
For example; if a user registers, the controller should fire a userRegistered event, and userRegistered should do some stuff.
How can I implement this?
interface IUsers
{
function onUserRegistered();
}
Event class
class UsersActivities implements IUsers
{
function onUserRegistered()
{
// TODO: Implement onUserRegistered() method.
}
}
Just check docs. It's pretty simple, create manager, creat listener(UsersActivities in your case i guess) and fire events in manager.
https://docs.phalconphp.com/pl/latest/reference/events.html

Where to register event listeners

I'm trying to use the Event System in CakePHP v2.1+
It appears to be quite powerful, but the documentation is somewhat vague. Triggering the event seems pretty straight-forward, but I'm not sure how to register the corresponding listener(s) to listen for the event. The relevant section is here and it offers the following example code:
App::uses('CakeEventListener', 'Event');
class UserStatistic implements CakeEventListener {
public function implementedEvents() {
return array(
'Model.Order.afterPlace' => 'updateBuyStatistic',
);
}
public function updateBuyStatistic($event) {
// Code to update statistics
}
}
// Attach the UserStatistic object to the Order's event manager
$statistics = new UserStatistic();
$this->Order->getEventManager()->attach($statistics);
But it does not say where this code should reside. Inside a specific controller? Inside the app controller?
In case it's relevant, the listener will be part of a plugin which I am writing.
Update:
It sounds like a popular way to do this is by placing the listener registration code in the plugin's bootstrap.php file. However, I can't figure out how to call getEventManager() from there because the app's controller classes, etc aren't available.
Update 2:
I'm also told that listeners can live inside Models.
Update 3:
Finally some traction! The following code will successfully log an event when inside of my MyPlugin/Config/bootstrap.php
App::uses('CakeEventManager', 'Event');
App::uses('CakeEventListener', 'Event');
class LegacyWsatListener implements CakeEventListener {
public function implementedEvents() {
return array(
'Controller.Attempt.complete' => 'handleLegacyWsat',
);
}
public static function handleLegacyWsat($event) { //method must be static if used by global EventManager
// Code to update statistics
error_log('event from bootstrap');
}
}
CakeEventManager::instance()->attach(array('LegacyWsatListener', 'handleLegacyWsat'), 'Controller.Attempt.complete');
I'm not sure why, but I can't get errors when I try to combine the two App::uses() into a single line.
Events
Events are callbacks that are associated to a string. An object, like a Model will trigger an event using a string even if nothing is listening for that event.
CakePHP comes pre-built with internal events for things like Models. You can attach an event listener to a Model and respond to a Model.beforeSave event.
The EventManager
Every Model in Cake has it's own EventManager, plus there is a gobal singleton EventManager. These are not all the same instance of EventManager, and they work slightly differently.
When a Model fires an event it does so using the EventManager reference it has. This means, you can attach an event listener to a specific Model. The advantages are that your listener will only receive events from that Model.
Global listeners are ones attached to the singleton instance of EventManager. Which can be accessed anywhere in your code. When you attach a listener there it's called for every event that happens no matter who triggers it.
When you attach event listener in the bootstrap.php of an app or plugin, then you can use the global manager, else you have to get a reference to the Model you need using ClassRegistry.
What EventManager To Use?
If the event you want to handle is for a specific Model, then attach the listener to that Model's EventManager. To get a reference of the model you can call the ClassRegistry::init(...).
If the event you want to handle could be triggered anywhere, then attach the listener to the global EventManager.
Only you know how your listener should be used.
Inside A Listener
Generally, you put your business logic into models. You shouldn't need to access a Controller from an event listener. Model's are much easier to access and use in Cake.
Here is a template for creating a CakeEventListener. The listener is responsible for monitoring when something happens, and then passing that information along to another Model. You should place your business logic for processing the event in Models.
<?php
App::uses('CakeEventListener', 'Event');
class MyListener implements CakeEventListener
{
/**
*
* #var Document The model.
*/
protected $Document;
/**
* Constructor
*/
public function __construct()
{
// get a reference to a Model that we'll use
$this->Document = ClassRegistry::init('Agg.Document');
}
/**
* Register the handlers.
*
* #see CakeEventListener::implementedEvents()
*/
public function implementedEvents()
{
return array(
'Model.User.afterSave'=>'UserChanged'
);
}
/**
* Use the Event to dispatch the work to a Model.
*
* #param CakeEvent $event
* The event object and data.
*/
public function UserChanged(CakeEvent $event)
{
$data = $event->data;
$subject = $event->subject();
$this->Document->SomethingImportantHappened($data,$subject);
}
}
What I like to do is place all my Events into the Lib folder. This makes it very easy to access from anywhere in the source code. The above code would go into App/Lib/Event/MyListener.php.
Attaching The EventListeners
Again, it depends on what events you need to listen for. The first thing you have to understand is that an object must be created in order to fire the event.
For example;
It's not possible for the Document model to fire Model.beforeSave event when the Calendar controller is displaying an index, because the Calendar controller never uses the Document model. Do you need to add a listener to Document in the bootstrap.php to catch when it saves? No, if Document model is only used from the Documents controller, then you only need to attach the listener there.
On the other hand, the User model is used by the Auth component almost every. If you want to handle a User being deleted. You might have to attach an event listener in the bootstrap.php to ensure no deletes sneak by you.
In the above example we can attach directly to the User model like so.
App::uses('MyListener','Lib');
$user = ClassRegistry::init('App.User');
$user->getEventManager()->attach(new MyListener());
This line will import your listener class.
App::uses('MyListener','Lib');
This line will get an instance of the User Model.
$user = ClassRegistry::init('App.User');
This line creates a listener, and attaches it to the User model.
$user->getEventManager()->attach(new MyListener());
If the User Model is used in many different places. You might have to do this in the bootstrap.php, but if it's only used by one controller. You can place that code in the beforeFilter or at the top of the PHP file.
What About Global EventManager?
Assuming we need to listen for general events. Like when ever any thing is saved. We would want to attach to the global EventManager. It would go something like this, and be placed in the bootstrap.php.
App::uses('MyListener','Lib');
CakeEventManager::instance()->attach(new MyListener());
If you want to attach an event listener inside bootstrap.php file of your plugin, everything should work fine using the hints posted in the answers. Here is my code (which works properly):
MyPlugin/Config/bootstrap.php:
App::uses('CakeEventManager', 'Event');
App::uses('MyEventListener', 'MyPlugin.Lib/Event');
CakeEventManager::instance()->attach(new MyEventListener());
MyPlugin/Lib/Event/MyEventListener.php:
App::uses('CakeEventListener', 'Event');
class MyEventListener implements CakeEventListener {
...
}
Event listeners related to MyPlugin are being registered only when the plugin is loaded. If I don't want to use the plugin, event listeners are not attached. I think this is a clean solution when you want to add some functionality in various places in your app using a plugin.
Its' not important, where the code resides. Just make sure its being executed and your events are properly registered & attached.
We're using a single file where all events are attached and include it from bootstrap.php, this ensures that all events are available from all locations in the app.
The magic happens when you dispatch an event, like from an controller action.
$event = new CakeEvent('Model.Order.afterPlace', $this, array('some'=>'data') ));
$this->getEventManager()->dispatch($event);
However, you can dispatch events from anywhere you can reach the EventManager (in Models, Controller and Views by default)

Categories