Broadcasting eloquent events using observers - laravel

Currently I am using observers to handle some stuff after creation and updating of my models.
I want to update my app by making it real-time using laravel-echo but I am not able to find documentation regarding the use of laravel-echo in combination with observers (instead of events).
You can use events and their broadcast functionality in combination with their respective listeners to get this functionality but I like the more clean code of observers (less "magic").
Looking at the code of the laravel framework I can see that the observable still uses eloquent events so I do suspect that there is a way to broadcast these.
So my question: is there a way to broadcast eloquent events using laravel-echo without creating individual events or manually adding broadcast statements on every event?

Interesting question! We can create a reusable, general-purpose observer that broadcasts events fired from the models that it observes. This removes the need to create individual events for each scenario, and we can continue to use existing observers:
class BroadcastingModelObserver
{
public function created(Model $model)
{
event(new BroadcastingModelEvent($model, 'created'));
}
public function updated(Model $model) { ... }
public function saved(Model $model) { ... }
public function deleted(Model $model) { ... }
}
class BroadcastingModelEvent implements ShouldBroadcast
{
public $model;
public $eventType;
public function __construct(Model $model, $eventType)
{
$this->model = $model;
$this->eventType = $eventType;
}
public function broadcastOn() { ... }
}
Then, simply instruct the observer to observe any models that you need to broadcast events to Echo for:
User::observe(BroadcastingModelObserver::class);
Post::observe(BroadcastingModelObserver::class);
...
As you know, multiple observers can observe the same model. This is a very simple example. We can do a lot of neat things with this pattern. For instance, we could declare which attributes we want to broadcast on each model and configure the event to filter out any that the model doesn't explicitly allow. Each model might also declare the channel that the event publishes to or the type of events that it should broadcast.
Alternatively, we could broadcast the event from your existing observers, but it sounds like you want to avoid adding these statements to each one.

Related

Laravel broadcasting - use job ID as an 'order' property

I want to send some sort of (unique, auto-incrementing) number as part of the payload of an event - so that the consumer can, for example, know it should ignore an 'updated' event if the event is older than a previous 'update' event it received.
I see I can add a broadcastWith method to my event, where I could add such a number, which I'm storing in some table.
But, I don't really need to create a new number. The ID of the job in the jobs table will work just fine. So, how can I make Laravel automatically add a property, say order, to this event before it is broadcast and make the value of order to id column from the jobs table? Is there a way to get it in the broadcastWith method?
I had previously thought of using a timestamp as the 'order' but of course that won't help me or the consumer when two events have been created in a short-a timeframe as a computer can create two events.
UPDATE
Looks like I haven't worded it well and people are confused as to what I'm looking for. In hindsight, I shouldn't of added the criteria that it must be the job id that gets included in the payload. The main thing I'm after is a unique, auto-incrementing ID in each broadcast event. For example, I have an UserUpdated event. Say the a user is updated twice - my SPA that is consuming the events needs to know which event is the newer one, otherwise the SPA might display outdated info. If the events are delivered sequentially, then this problem won't happen. But, especially as I'm relying on a third-party service (Pusher) to deliver the events to the SPA, I don't want to assume / trust that the events will always be delivered in the same order they were sent to Pusher.
Hi such a nice requirement, i have been working on a POCO and here are some snippets, you do not need broadcast at all. Of course you need to have the queue worker up and running
Running The Queue Worker
On your order controller, i guess you need the update method to dispatch the job before commiting.
function update(Request $req)
{
$data= Order::find($req->id);
$data->amount=$req->amount; //example field
PostUpdateOrderJob::dispatch($data)->beforeCommit();
$data->save();
}
Important: Why before commit and not after commit: Setting the after_commit configuration option to true will also cause any queued event listeners, mailables, notifications, and broadcast events to be dispatched after all open database transactions have been committed.
Your PostUpdateOrderJob class
?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use App\Models\Order;
use Throwable;
class PostUpdateOrderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* The number of times the job may be attempted.
*
* #var int
*/
public $tries = 25;
/**
* The maximum number of unhandled exceptions to allow before failing.
*
* #var int
*/
public $maxExceptions = 3;
protected $order;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct(Order $order)
{
$this->order=$order;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$this->order->update(['jobid'=>$this->job->getJobId()]);
}
public function failed(Throwable $exception)
{
// Send user notification of failure, etc...
//Several options on link below
//https://laravel.com/docs/9.x/queues#dealing-with-failed-jobs
}
}
Well php has a function called time() which returns the current unix time in seconds, so you can just use that in your broadcast event.
In your broadcast event, you can add this time to the public class properties, which would then be available through the event payload:
class MyBroadcastEvent implements ShouldBroadcast
{
public $time;
public function __construct()
{
$this->time = time();
}
}
I'm not sure if this is exactly what you need, your question is kind of confusing to be honest.

How to avoid user take long time to wait while controller is processing

let's me explain example
User click button
run function trigger in Controller
User wait 30sec because MyModel::doSomeThing take long time to process
MyModel::doSomeThing do many thing. I don't want user to wait for it.
Is it possible to run MyModel::doSomeThing by don't care about result and return to user immediately?
function trigger(Request $request){
$id= $request->get('id');
MyModel::doSomeThing($id); // this one take 30 sec.
return response()->json([], 200);
}
If the result of doSomeThing() method isn't necessary for your response & can be done in background, I suggest using Events and Listeners, which will use queues to run in the background, and the user won't need to wait for this procces to finish. The process is fairly simple. Create event and it's listened with these two commands:
php artisan make:event YourEvent
php artisan make:listener YourListener --event=YourEvent
After that, register your event and listener in the App\Providers\EventServiceProvider, under the $listen array:
protected $listen = [
YourEvent::class => [
YourListener::class,
],
];
Now, when you have that sorted out, you need to build your event instance. Inside your newly created method, in the construct method, add this:
public $yourModel;
public function __construct(YourModel $yourModel)
{
$this->yourModel = $yourModel;
}
After you created your model, time to edit your listener, which will hanlde all the logic ghat you need. Inside this handle method, you will have the access to $yourModel instance that we defined in our event:
public function handle(YourEvent $event)
{
// Access your model using $event->yourModel...
YourModel::doSomeThing($event->yourModel);
}
The only thing left do to is to make your listener queueable. You can do this by adding implements ShouldQueue your listened definition:
class YourListener implements ShouldQueue
{
//
}
Now when we have everything setup, you can change your controller code to call this newly created event, and let the queue handle all the logic:
function trigger(Request $request){
$id= $request->get('id');
YourEvent::dispatch($id); //Calling event which will handle all the logic
return response()->json([], 200);
}
And that should be it. I haven't tested this code, so if you encounter any problems, let me know.

Attaching many to many relations while still binding to created event

So I've run into this issue a few times and now I've decided that I want to find a better solution.
For examples sake, I have two models, Order & Product. There is a many to many relation so that an order can have multiple products and a product can of course have multiple orders. Table structure looks like the below -
orders
id
more fields...
products
id
more fields...
product_orders
order_id
product_id
So when an order is created I run the following -
$order = Order::create($request->validated())
$order->products()->attach([1,2,3,4...]);
So this creates an order and attaches the relevant products to it.
However, I want to use an observer, to determine when the order is created and send out and perform related tasks off the back (send an order confirmation email, etc.) The problem being, at the time the order created observer is triggered, the products aren't yet attached.
Is there any way to do the above, establishing all the many to many relationships and creating the order at the same time so I can access linked products within the Order created observer?
Use case 1
AJAX call hits PUT /api/order which in turn calls Order::place() method. Once an order is created, an email is sent to the customer who placed the order. Now I could just put an event dispatch within this method that in turn triggers the email send but this just feels a bit hacky.
public static function place (SubmitOrderRequest $request)
{
$order = Order::create($request->validated());
$order->products()->attach($request->input('products'));
return $order;
}
Use case 2
I'm feature testing to make sure that an email is sent when an order is created. Now, this test passes (and email sends work), but it's unable to output the linked products at this point in execution.
/**
* #test
**/
public function an_email_is_sent_on_order_creation()
{
Mail::fake();
factory(Order::class)->create();
Mail::assertSent(OrderCreatedMailable::class);
}
Thanks,
Chris.
I think the solution to your problem could be transaction events as provided by this package from fntneves.
Personally, I stumbled upon the idea of transactional events for another reason. I had the issue that my business logic required the execution of some queued jobs after a specific entity had been created. Because my entities got created in batches within a transaction, it was possible that an event was fired (and the corresponding event listener was queued), although the transaction was rolled back because of an error shortly after. The result were queued listeners that always failed.
Your scenario seems comparable to me as you don't want to execute your event listeners immediately due to missing data which is only attached after the model was actually created. For this reason, I suggest wrapping your order creation and all other tasks that manipulate the order within a transaction. Combined with the usage of said package, you can then fire the model created event as the actual event listener will only be called after the transaction has been committed. The code for all this basically comes down to what you already described:
DB::transaction(function() {
$order = Order::create($request->validated());
$order->products()->attach($request->input('products'));
});
In your model, you'd simply define an OrderCreated event or use an observer as suggested in the other answer:
class Order
{
protected $dispatchesEvents = [
'created' => OrderCreated::class,
];
}
class OrderCreated implements TransactionalEvent
{
public $order;
/**
* Create a new event instance.
*
* #param \App\Order $order
* #return void
*/
public function __construct(Order $order)
{
$this->order = $order;
}
}
You can redefine boot method in your model, if product ids is static
class Order extends Eloquent {
protected static function boot() {
parent::boot();
static::saving(function ($user) {
$this->products()->attach([1,2,3,4...]);
});
}
}
Or use observers
class OrderObserver
{
public function created($model)
{
//
}
}
And register this
class EventServiceProvider extends ServiceProvider
{
public function boot(DispatcherContract $events)
{
parent::boot($events);
Order::observe(new OrderObserver());
}
}

In Laravel, Where I should fire events and emails in repo or in controller?

I am using repository pattern in develop an application using laravel, my question is where I have to write code for fire events, send email or send an notification? and why?
This is really a broad question and many will come with their own opinions. In my opinion, form the context of Laravel, I would define types of my events depending on the operations.
So for example, as you mentioned email/notification events, I would like to think this way (This is a hypothetical example):
class UserController
{
public function register(Request $request, UserRepository $user)
{
if ($user = $user->register($request->all())) {
Email::send(...);
}
}
}
In this case, an email should be sent to the user after the registration so I can use an event to do the same thing within the controller for example:
class UserController
{
public function register(Request $request, UserRepository $user)
{
try {
$user = $user->register($request->all());
Event::fire('user_registered', $user);
} catch(RegistrationException $e) {
// Handle the exception
}
}
}
In this case, I think, the event dispatching should be in the controller because it's the part of my application layer to control the application flow so, email sending event should be dispatched from controller. The UserRepository should not care about your application's flow, sending email to the user is not part of your UserRepository, so that's it.
Now, think about another hypothetical example, say you've a delete method in your UserController as given below:
class UserController
{
public function delete(UserRepository $user, $id)
{
if($user->findOrFail($id)->delete()) {
Post::where('user_id', $id)->delete();
}
}
}
Well, in this case, the deletion of the user involves some domain related operations so I would rewrite the method as given below:
public function delete(UserRepository $user, $id)
{
try {
$user->delete($id);
return redirect('/users'); // show users index page
} catch (InvalidOperationException $e) {
// Handle the custom exception thrown from UserRepository
}
}
Notice that, there is no related operations took place in the delete method because I would probably fire an event inside the UserRepository because this delete action involves some other domain/business related operation and application layer should not care about it (in this case) because the deleting an user effects some other domain objects so I'll handle that event this way.
Anyways, this is just my way of thinking and it's just an opinion. Maybe in a real world situation, I could come up with a different idea so it's up to you, depending on the context you should think about it and finally there's no recommended way in Laravel, you can even use Models to fire events, so just keep it simple, make the decision depending on your context that fits well.

Relationship not being passed to notification?

I created a notification that I am passing a model to:
class NewMessage extends Notification implements ShouldQueue
{
use Queueable;
protected $message;
public function __construct(Message $message)
{
$this->message = $message;
}
public function via()
{
return ['database'];
}
public function toArray()
{
Log::info($this->message);
return [
'message_id' => $this->message->id,
];
}
}
And this is how I call the notification:
$message = Message::where('user_id', Auth::user()->id)
->where('message_id', $message_id)
->with('topic')
->get();
$user->notify(new NewMessage($message));
The problem is that when the notification prints the log (Log::info($this->message);), the topic relationship doesn't show up.
However, I found that if I change the toArray() function in the nofitication class to this, it prints out fine:
public function toArray()
{
$this->message->topic;
Log::info($this->message);
return [
'message_id' => $this->message->id,
];
}
Why? How do I fix this?
Note: this question/answer is only relevant for Laravel < 5.6. Starting in Laravel 5.6, loaded relationships are also serialized, so the issue in this question is no longer an issue.
Your notification is set to queue, and the Notification class you're extending uses the SerializesModels trait. When an object with the SerializesModels trait is serialized to be put on the queue, any Models contained on that object (e.g. your message) are replaced with just the id of that model (the message id). When the queue worker unserializes your notification to process it, it will use that message id to re-retrieve the message from the database. Unfortunately, when this happens, no relationships are included.
So, even though your message had the topic relationship loaded when it was serialized, it will not have the topic relationship loaded when the queue worker processes the notification. If you need the topic inside of your notification, you will need to reload it, as you have seen.
You can read more about this in the documentation here. The relevant part is quoted below:
In this example, note that we were able to pass an Eloquent model directly into the queued job's constructor. Because of the SerializesModels trait that the job is using, Eloquent models will be gracefully serialized and unserialized when the job is processing. If your queued job accepts an Eloquent model in its constructor, only the identifier for the model will be serialized onto the queue. When the job is actually handled, the queue system will automatically re-retrieve the full model instance from the database. It's all totally transparent to your application and prevents issues that can arise from serializing full Eloquent model instances.

Resources