Determine user of job
Hello. I currently have a job that works well. I'm dispatching it directly from the controller, and passing data to it, and it works like a charm.
Now, I was trying to add extra functionality to it, like notify the user when a job that he requested has completed, and came across Job events. The problem is that I am not able to find a way to pass data from the job to the function
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
Queue::after(function (JobProcessed $event) {
// I don't know hoy to gather/pass/retrieve data from job/session to identify
// the user that needs to be notified
});
}
How can/should this be achieved?
Thank you very much!
@vincent15000 Thank you, but this is not what I was asking. I was asking about the Queue::after event at the Service Provider that is fired after the job is done. My job already has data injected, as I've posted. What I am not able to find, is a way to pass data from job, to service provider's function "after" when job is done.
// App\Http\Controller\InventoryController
public function DispatchJob(Request $req) {
// do some stuff
JobStoreItems::dispatch($items, Auth::user());
}
// App\Jobs\JobStoreItems
class JobStoreItems implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $items;
public $user;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct($items, $user)
{
$this->items= $items;
$this->user = $user;
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
// perfom required actions using $this->items and $this->user
}
}
But my problem if with the event that fires in the boot() method of the service provider that gets dispatched AFTER the job is processed successfully.
class InventoryServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
Queue::after(function (JobProcessed $job) {
// A job has been processed successfully
if ($job->job->resolveName() === JobStoreItems::class) {
// Check if the job is the one that needs to be notified
// but cannot find a way to check to which user send the notification
}
});
}
}
you pass the data to the job , eg, the user, store the user in a public property of the job. Then in the job event you are given the job and it contains the user
@Snapey Hello. Unfortunatelly, it is not the case. I am currently logging this to check properties and values
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
Queue::after(function (JobProcessed $job) {
if ($job->job->resolveName() === JobStoreItems::class) {
Log::info('Job processed: ' . $job->job->getName() . ': ' . $job->job->getQueue() . ': ' . $job->job->resolveName());
}
Log::info('[' . $job->user->id . ']');
});
}
And on the log I have these 2 entries at the bottom.
[2022-06-26 06:00:54] local.INFO: Job processed: Illuminate\Queue\CallQueuedHandler@call: default: App\Jobs\JobStoreItems
[2022-06-26 06:00:55] local.ERROR: Undefined property: Illuminate\Queue\Events\JobProcessed::$user {"exception":"[object] (ErrorException(code: 0): Undefined property: Illuminate\Queue\Events\JobProcessed::$user at C:\laragon\www\DD\app\Providers\InventoryServiceProvider.php:37)
I have also tried the following variants with no success.
$this->user->id
$this->job->user->id
$this->job->payload()->user->id
$this->job->payload()['user']->id
$this->job->payload()['user']['id']
And as you can see on my other comment, I do have public properties inside the job and set them in the constructor.
@Fabro your example is a different job to the one you showed earlier when you set the user as a public property
your code would be less confusing if you used $event
public function boot()
{
//
Queue::after(function (JobProcessed $event) {
if ($event->job->resolveName() === JobStoreInventory::class) {
Log::info('Job processed: ' . $event->job->getName() . ': ' . $event->job->getQueue() . ': ' . $event->job->resolveName());
}
Log::info('[' . $event->job->user->id . ']');
});
}
@Snapey I was refactoring while posting here, that's why the confusion, sorry, my bad. I've decided to make name shorter since I find debugging with a large name really annoying. Later, I will refactor code again to the original names. Currently, all jobs and events are in sync with their names and dependencies.
I will edit that post and change that to prevent confusion.
@Snapey I have founded a way to retrieve the User model. Here is what I have done. I do not know if it is the best way, or if there are others, but as for now, this is the only way that I have found that works.
Log::info(print_r(unserialize($event->job->payload()['data']['command'])->user, true));
This line dumps the UserModel to the log. Thank you for your comment, it helped me out to think outside the box with the public properties. If by any chance, you know a better way to do this, or you think that this way could lead to any problem (data leaks, data loss, load issues, etc) please let me know and I will keep trying to find a different approach. I am still new to Laravel, 6 months are nothing giving the size of the frameworks, and I am still learning about the things that happens behind the scene and how to take the most out of them.
@Fabro If the job has a user, then just notify the user at the end of the job:
FooJob::dispatch($request->user());
public function handle()
{
// Main job body...
// Now notify user job completed successfully
$this->user->notify(new FooNotification());
}
@martinbean Hello, thank you for your help. Yes, I've thought about doing it that way, but I also thought the notification system is busy, this will hang the job to completing and starting the next, until the notification has been sent, and if the notification fails, the job could also be marked as failed. Am I incorrect about that? I am new to jobs in laravel, and still getting to know them. I can perform tests but as we all know, you can never replicate a real life production server and all the variables of load and network latency. So basically, I'm asking your opinion. Does this method in fact prevents the next job in the queue to run until the notification has been sent as I am assuming? Also, is the notification fails, wouldn't the job be also marked as failed?
@Fabro not it you queue the notification.
@Snapey isn't it like a bad practice to queue another job inside an already queued job? Aren't "Queue::before" and "Queue::after" events designed to prevent that?
@Snapey Also, using it inside the "Queue::after" method, allows me to perform the task for multiple jobs in a future without manually adding the notification inside each job.
@Fabro You seem to be over-complicating this.
Perform the logic. Then queue a notification.
@Fabro pass the notification sequence after the job. Then you can add implements ShouldQueue in the notification class so that it queues the notification sending, when the system is busy, because basing on the way you named the job, I think the user or customer, whatever, should be notified if the job is handled successfully. Its just good for the user experience
@martinbean It is not "over-complicating", it is a matter of understanding. If things like notify user after a job, or do another task not related to the job, is supposed to be inside the job code... then why do the Laravel team bother to develop events for the queue inside the framework in the first place? Just tell everyone to put all inside the job, right?
Nonetheless, I have managed to get the user, and send a notification in the event using
$user = unserialize($event->job->payload()['data']['command'])->user;
Once again, since I am learning about all the tools that Laravel provides, I do not understand why people are telling me to not use them. Those tools are there for a reason, I assume, and that reason is possibly separating job logic from notification logic, so each part only performs one task. Then why do several people encourage others not to use them?
Please, if someone cares to explain, maybe I can see another type of logic that I am currently not viewing, but as for now, the Queue::after event seems to pretty much be made for a use exactly like the one that I need.
@Fabro these queue events are really there so that you can monitor the performance of the queue itself, not monitor the work performed by the queue jobs.
@Snapey Then I do not understand the definition the developers gave to them.
Extracted from the Laravel Docs:
These callbacks are a great opportunity to perform additional logging or increment statistics for a dashboard
The logging and statistics increment can also be performed inside the job directly, or queueing another job, or event firing an event at the end of the job, but the docs says that "before" and "after" events are great opportunities to perform those kind of actions. So, why are those events great for those kind of things, but not for sending notifications? I am missing something here, and I cannot understand the difference between what the docs itself says and what I am trying to accomplish. Both tasks can be done manually inside a job, both tasks can be done bi queueing another job, both tasks can be done by firing an event and listen to it, so how is it come that one "should" be done inside "after" but not the other?
@Fabro if you wanted a dashboard that allows you to monitor queues (success, failure, duration, attempts etc) then you would not want to add something into every job. As I said before the intention of the events is for queue instrumentation.
The need to inform a user about the outcome of a job is very job specific, and also only applies to some of your jobs so it makes sense that this is a responsibility of the job itself.
@Snapey Okay, it does sound weird to me to do that inside a job, even when I will do it with multiple jobs, and will have to repeat code between them, but okay, I will try to di it that way.
If things like notify user after a job, or do another task not related to the job, is supposed to be inside the job code... then why do the Laravel team bother to develop events for the queue inside the framework in the first place?
@Fabro To avoid ugly code like this:
$user = unserialize($event->job->payload()['data']['command'])->user;
@martinbean But that can be easily replaced by a global helper function.
@Fabro So can any awful-looking code. That’s just hiding it somewhere else though, rather than addressing it.
@martinbean But that's exactly what laravel does behind scenes with jobs, just that they split it into various methods.
If you look at Queue\Jobs\Job you will find this
// Illuminate\Queue\Jobs\Job
/**
* Get the resolved name of the queued job class.
*
* Resolves the name of "wrapped" jobs such as class-based handlers.
*
* @return string
*/
public function resolveName()
{
return JobName::resolve($this->getName(), $this->payload());
}
So, inside "resolveName" method, there are actually three methods being called (resolve, getName and payload), two of which are on the same class, so looking at the first you can see:
// Illuminate\Queue\Jobs\Job
/**
* Get the name of the queued job class.
*
* @return string
*/
public function getName()
{
return $this->payload()['job'];
}
So, basically, payload is called again, and it returns an array. If you check that method:
// Illuminate\Queue\Jobs\Job
/**
* Get the decoded body of the job.
*
* @return array
*/
public function payload()
{
return json_decode($this->getRawBody(), true);
}
And another method is called, but that method comes from an abstract class, so depending on the implementation, is what the method does. Since I am using database, this is the content:
// Illuminate\Queue\Jobs\DatabaseJob
/**
* Get the raw body string for the job.
*
* @return string
*/
public function getRawBody()
{
return $this->job->payload;
}
So, in essence, calling "$name = $job->resolveName();" will be equal to use "$name = json_decode($job->payload)['job']". It is the same type of "ugly code", just that it is splitted into several methods. There are some steps that you cannot avoid doing, like decoding a json, or unserialize objects, and the fact that you have splitted it into several methods doesn't remove the facts that you are in deed performing those steps, but you are just doing them in a more fancy way.
Please or to participate in this conversation.