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

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.

Related

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());
}
}

Laravel: Send 1000 emails every hour using queue

I have a table users with 5000 records (5000 users) and in my server i can just send 1000 emails every hour.
How i can send 1000 emails every hour using queue ?
or how to make queues sleep inside loop?
EmailController:
class EmailController extends Controller
{
public function sendEmail(Request $request){
$event=(object)['content' => "Hello Laravel fans",'subject' => 'Test Email'];
$users=User::all();
App\Jobs\SendReminderEmail::dispatch($users,$event)
}
}
SendReminderEmail
class SendReminderEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $event;
public $email;
public $users;
public function __construct($users,$event)
{
$this->users = $users;
$this->event = $event;
}
public function handle()
{
foreach ($this->users as $user) {
Mail::to($user->email)->queue(new Reminder($this->event));
}
}
}
Laravel has a neat feature which fits your case perfectly, it's called Task Scheduling, instead of making the job sleep for one hour, you could, instead, call it every hour.
To do so add the job schedule to the schedule() method located on App\Console\Kernel like this:
protected function schedule(Schedule $schedule)
{
$schedule->job(new SendReminderEmail)->hourly();
}
I also would recommend you to make the job self contained, that will make this task much simpler, I'm thinking in something like this:
class SendReminderEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $event;
public $users;
public function __construct()
{
$this->users = User::all();
$this->event = (object)['content' => "Hello Laravel fans",'subject' => 'Test Email'];;
}
public function handle()
{
foreach ($this->users as $user) {
Mail::to($user->email)->queue(new Reminder($this->event));
}
}
}
Now you can just get rid of your controller because this job will be executed every hour automatically.
Remember that you will need to run a cron on your server to check if the job needs to be executed. You can also run it manually if you want to test it using php artisan schedule:run.
Hope this helps you.
Laravel has a built-in throttle feature for rate-limited jobs. From the docs:
If your application interacts with Redis, you may throttle your queued
jobs by time or concurrency. This feature can be of assistance when
your queued jobs are interacting with APIs that are also rate limited.
For example, using the throttle method, you may throttle a given type
of job to only run 10 times every 60 seconds. If a lock can not be
obtained, you should typically release the job back onto the queue so
it can be retried later:
Redis::throttle('key')->allow(10)->every(60)->then(function () {
// Job logic...
}, function () {
// Could not obtain lock...
return $this->release(10);
});
In your case, that might look like Redis::throttle(...)->allow(1000)->every(3600)->...
If you're not using Redis, another possible solution which is specific to queued mail is to add a delay. Again, from the docs:
Delayed Message Queueing
If you wish to delay the delivery of a queued
email message, you may use the later method. As its first argument,
the later method accepts a DateTime instance indicating when the
message should be sent:
$when = now()->addMinutes(10);
Mail::to($request->user())
->cc($moreUsers)
->bcc($evenMoreUsers)
->later($when, new OrderShipped($order));
Using this method would require you to calculate the delay for each email during the loop that dispatches emails to the queue. For example, every 1000th iteration of the loop you could increase the delay by 1 hour.
If you're using the Amazon SQS queue service, the same maximum delay of 15 minutes that applies to other queued jobs might also apply here (I'm not sure on this part), in which case you would have to come up with another solution for checking if you're over your rate limit and releasing the job back to the queue.

Laravel 5.5: how to get failed job ID and link it to the related object?

In Laravel 5.5, I've created a queue job like this:
class EmailJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $emailModel;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct(MyEmail $emailModel)
{
//
$this->emailModel = $emailModel;
}
// ...
}
MyEmail is a model to store email data.
I use database driver on queue.
Now, I want to catch the failed job (in failed_jobs table), and link them to related MyEmail object (e.g: store in an attribute failed_job_id), then later on my app can manually retry specific email's failed job. How can I achieve this ?
I have a workaround approach, in which I grab all failed jobs through failed_jobs table, and check the payload one by one, determine if the MyEmail object has the id we want. but I don't think it's efficient.

How can I change SMTP details globally at runtime?

I'm using Laravel 5.5. The nature of the website is a 'multisite' architecture where multiple websites/domains are run from the same codebase.
I've come across an issue when sending email. I need to change the from name and address as well as the transport (SMTP, etc) options depending on which website is being viewed. I have these details stored in a config file.
The easiest way is to just pull those details in the Controller before I call Mail::send/Mail::queue and to update them. However, this brings back 2 issues:
There is a heavy reliance on remembering to actually do that every time I send any email in the code. In short, it's not abiding by DRY.
I'd be forced to use Mail::send instead of Mail::queue, because the queue wouldn't have any idea of the config update from the time it was queued only from when it is processed .
How can I achieve what I am looking to do here in a clean way?
I thought about extending all of my 'Mailable' classes with a custom class that updates the SMTP details, but it doesn't look like you can update the SMTP/Transport information after the class is initiated; you can only update the from name and address.
I managed to find a way to do this.
I had my mailable class (ContactFormMailable) extend a custom class, as follows:
<?php
namespace CustomGlobal\Mail;
use CustomGlobal\Mail\CustomMailable;
use CustomGlobal\ContactForm;
class ContactFormMailable extends CustomMailable
{
public $contact_form;
/**
* Create a new message instance.
*
* #return void
*/
public function __construct(ContactForm $contact_form)
{
$this->contact_form = $contact_form;
}
/**
* Build the message.
*
* #return $this
*/
public function build()
{
$view = $this->get_custom_mail_view('contact_form', $this->contact_form);
return $this->subject('Contact Form Enquiry')
->view($view);
}
}
You'll notice I'm calling get_custom_mail_view. This is in my extended class and used to calculate the view and template I need to use for my mail, depending on the website being viewed. In here I also set the location of my config folder.
<?php
namespace CustomGlobal\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;
use Swift_Mailer;
use Swift_SmtpTransport;
use CustomGlobal\Website;
use CustomGlobal\Territory;
class CustomMailable extends Mailable
{
use Queueable, SerializesModels;
public $layout_view_to_serve;
public $host_folder;
/**
* Override Mailable functionality to support per-user mail settings
*
* #param \Illuminate\Contracts\Mail\Mailer $mailer
* #return void
*/
public function send(Mailer $mailer)
{
app()->call([$this, 'build']);
$config = config($this->host_folder .'.mail');
// Set SMTP details for this host
$host = $config['host'];
$port = $config['port'];
$encryption = $config['encryption'];
$transport = new Swift_SmtpTransport( $host, $port, $encryption );
$transport->setUsername($config['username']);
$transport->setPassword($config['password']);
$mailer->setSwiftMailer(new Swift_Mailer($transport));
$mailer->send($this->buildView(), $this->buildViewData(), function ($message) use($config) {
$message->from([$config['from']['address'] => $config['from']['name']]);
$this->buildFrom($message)
->buildRecipients($message)
->buildSubject($message)
->buildAttachments($message)
->runCallbacks($message);
});
}
/**
* Calculate the template we need to serve.
* $entity can be any object but it must contain a
* $website_id and $territory_id, as that is used
* to calculate the path.
*/
public function get_custom_mail_view($view_filename, $entity)
{
if(empty($view_filename)) {
throw new Exception('The get_custom_mail_view method requires a view to be passed as parameter 1.');
}
if(empty($entity->website_id) || empty($entity->territory_id)) {
throw new Exception('The get_custom_mail_view method must be passed an object containing a website_id and territory_id value.');
}
// Get the website and territory
$website = Website::findOrFail($entity->website_id);
$territory = Territory::findOrFail($entity->territory_id);
$view_to_serve = false;
$layout_view_to_serve = false;
// Be sure to replace . with _, as Laravel doesn't play nice with dots in folder names
$host_folder = str_replace('.', '_', $website->website_domain);
$this->host_folder = $host_folder; // Used for mail config later
/***
Truncated for readability. What's in this area isn't really important to this answer.
***/
$this->layout_view_to_serve = $layout_view_to_serve;
return $view_to_serve;
}
}
It's important to remember that mail can be queued. If you do this is another way, such as setting a config at runtime, then you'll find that the process that runs the queue has no visibility/scope of your runtime config changes, and you'll end up firing out email from your default values.
I found a few answers similar to this one, which helped me out, but none of them worked completely, and some are out-dated (Swift_SmtpTransport is changed considerably since those answers).
Hopefully this helps someone else out.

Slow down Laravel worker

I have hooked up Redis with Laravel for queuing emails and all is fine...
but in dev environment I use mailtrap.io (free version).
And the problem is that mailtrap allows to receive only 2 emails per second, so I never get all the emails that are queued, because redis sends emails like crazy... maybe 10 per/s
Is there a way somehow to slow down the queue so that it sends max 2 mails per second?
Yes It is a solution and they're called jobs :)
you can create a file to send emails exclusively in the Jobs Folder and a class like this
class SendPushNotification extends Job implements ShouldQueue
{
use InteractsWithQueue, SerializesModels;
protected $pushNotification;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct(PushNotification $pushNotification)
{
$this->pushNotification = $pushNotification;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$this->pushNotification->send();
}
}
And then call the class in the controller
$this->dispatch(new SendPushNotification($pushNotification))->delay(1);
Delay is for seconds you can create a constructor with an array to receive two emails o many you want maybe some var than can be changed by the .env to change the number of emails per second

Resources