Imagine a sequence of actions to be performed "in the background", in addition to creating the main logic.
For example, after user registration, you want to prepare demo data for their dashboard. You may want to put it into the background queue so the user won't wait for the operation to finish.
This is where Job classes come to help you.
Jobs are similar to Actions we discussed earlier, but Jobs may be put in a Queue. And they have Artisan command to create them:
php artisan make:job NewUserDataJob
And our job would look like this:
app/Jobs/NewUserDataJob.php:
class NewUserDataJob implements ShouldQueue{ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct(public User $user) {} public function handle() { Project::create([ 'user_id' => $this->user->id, 'name' => 'Demo project 1', ]); Category::create([ 'user_id' => $this->user->id, 'name' => 'Demo category 1', ]); Category::create([ 'user_id' => $this->user->id, 'name' => 'Demo category 2', ]); }}
All that's left is to dispatch the Job in the Controller:
use App\Jobs\NewUserDataJob; // ... public function store(StoreUserRequest $request){ $user = (new CreateUserAction())->execute($request->validated()); NewUserDataJob::dispatch($user); // ...}
Then, you need to configure everything separately around the queue. This course is about the structure of the code and not about queues specifically, so for that topic, I have two separate courses:
There's not much more to say about Jobs. Let's just take a look at more practical examples.
Open-Source Examples
Example Project 1. crater-invoice/crater
The first job example is from crater-invoice/crater open-source project for generating a PDF.
This example is very common, as generating PDFs can take time, especially if generating many PDF invoices is a monthly task.
So, in this example, the Job accepts the invoice as a parameter and generates the invoice in the handle()
method.
app/Jobs/GenerateInvoicePdfJob.php:
class GenerateInvoicePdfJob implements ShouldQueue{ use Dispatchable; use InteractsWithQueue; use Queueable; use SerializesModels; public $invoice; public $deleteExistingFile; public function __construct($invoice, $deleteExistingFile = false) { $this->invoice = $invoice; $this->deleteExistingFile = $deleteExistingFile; } public function handle() { $this->invoice->generatePDF('invoice', $this->invoice->invoice_number, $this->deleteExistingFile); return 0; }}
Then, in the Controller, the GenerateInvoicePdfJob
Job is dispatched when the invoice is created.
app/Http/Controllers/V1/Admin/Invoice/InvoicesController.php:
class InvoicesController extends Controller{ // ... public function store(Requests\InvoicesRequest $request) { $this->authorize('create', Invoice::class); $invoice = Invoice::createInvoice($request); if ($request->has('invoiceSend')) { $invoice->send($request->subject, $request->body); } GenerateInvoicePdfJob::dispatch($invoice); return new InvoiceResource($invoice); } // ...}
Example Project 2. protonemedia/eddy-server-management
The second example is from protonemedia/eddy-server-management, an open-source project. This project has many jobs.
One of the jobs is to add an SSH key to a server, which could take some time while connecting to the server.
app/Jobs/AddSshKeyToServer.php:
class AddSshKeyToServer implements ShouldQueue{ use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct(public SshKey $sshKey, public Server $server) { // } public function handle(): void { $this->server->runTask( new AuthorizePublicKey($this->server, $this->sshKey->public_key) )->asUser()->inBackground()->dispatch(); }}
Then, in the Controller for each server, the Job is dispatched to add the SSH key to a server.
app/Http/Controllers/AddSshKeyToServerController.php:
class AddSshKeyToServerController extends Controller{ // ... public function store(Request $request, SshKey $sshKey) { $request->validate([ 'servers' => ['required', 'array', 'min:1'], 'servers.*' => ['required', Rule::exists('servers', 'id')->where(function ($query) { $query->where('team_id', $this->team()->id); })], ]); $request->collect('servers')->each(function ($serverId) use ($sshKey) { $server = $this->team()->servers()->findOrFail($serverId); dispatch(new AddSshKeyToServer($sshKey, $server)); }); Toast::message(__('The SSH Key will be added to the selected servers. This may take a few minutes.')); return to_route('ssh-keys.index'); }}
So, a Job is a class you actively dispatch to perform some action. But what if you want to just "inform the system" that something happened and then let others decide what jobs to perform? This is where the Events and Listeners come into the scene in the next lesson.
The question may be out of the scope of this lesson but how to make sure to keep data integrity when the job fails in this code :
let's suppose the user is created but the job fail creating related post. How to handle that ? Should we put the user creation and job dispatching of the project creation in database transaction ?
So in this case, I would actually move everything to the inside of a job with a transaction. That way it will be handled mostly automatically, instead of manually reverting :)
Of course, you could use batch jobs and chain them with catch condition, but... that is quite more work to do!
This is also outside of this course's scope but the last comment/answer got me wondering : So I guess testing becomes difficult when the DB::transaction() is involved as the transaction will include multiple responsibilities, right? How would you approach such situation? Thanks!
Hi, I'm not sure why this would make testing hard.
For example, your test assumes that when an action is called - there is
N
things that happen. Right? Well, it doesn't matter if there was a DB::transaction() or not, as you still check for end result. And if there is no desired end result - your tests will fail.In other words, you don't care what happens under the hood most of the time. You care about:
If anything fails inside of there - you will still fail to see #3 pass. This does include all 500 errors or db failures/malformed data
How should we handle the scenario where a user needs to access information about pending queues in a Laravel application—queues that have not been executed yet? Should this logic be placed directly within the controller, or is there a more appropriate approach to manage this?
You can request that in the controller - I don't see any issues with that
if you need to generate something in the background after creating a user, it is probably more appropriate to put it in the observer
Not always. Imagine that after creating a user, you need to run a setup script that takes 30 or more seconds to complete. This would mean that the whole setup is
sync
and the request would hang for that long (even with observers). That's where Jobs come in - they areasync
meaning that the user loads the page fast, while the Job is processed in the background.I misspoke, I don't mean that you don't need to queue the task, I mean running it not in the controller, but in its own task observer ))