Detecting if a job has been dispatched using dispatchNow - php

I have a job that under certain circumstances calls another job
<?php namespace App\Jobs;
use App\Models\Account;
class EnqueueScheduledDownloads implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $account;
public function __construct(Account $account)
{
$this->account = $account;
}
public function handle()
{
foreach($this->account->pending_downloads as $file)
{
DownloadFile::dispatch($file);
}
}
}
While the download job is usually executed in a queue; there are times, for example during testing, where it would make my life much easier if the whole chain was processed synchronously in a blocking fashion. I would like to be able to do something like this:
public function handle()
{
foreach($this->account->pending_downloads as $file)
{
if($this->getDispatchMode() == 'sync') {
DownloadFile::dispatchNow($file);
} else {
DownloadFile::dispatch($file);
}
}
}
Is this possible?

After a bit of poking around I was able to answer my own question. Yes it is possible; if a job is dispatched via dispatchNow() the job property of the Queueable object will be null, whereas if it is dispatched on a connection using dispatch() it will be set to an implementation of Illuminate\Contracts\Queue\Job. So the handle method can be changed as such:
public function handle()
{
foreach($this->account->pending_downloads as $file)
{
if(is_null($this->job)) {
DownloadFile::dispatchNow($file);
} else {
DownloadFile::dispatch($file);
}
}
}
And it will work as expected. I was able to find this solution by creating a new job:
<?php namespace App\Jobs;
class TestJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct()
{
}
public function handle()
{
dump(get_object_vars($this));
}
}
and dispatching it on various queue and connections as well as with dispatchNow() and observing the output. Furthermore it is possible to retreive the connection and queue the job was dispatched on from the $this->job:
public function handle()
{
echo $this->job->getConnectionName();
echo $this->job->getQueue();
}

Related

Laravel Job for Bulk Mail

this is the first time I am using laravel queue jobs, and somehow i could not get it working.
This is my mail class:
class TopluKabulMektubu extends Mailable
{
use Queueable, SerializesModels;
public $letter;
public function __construct(AcceptLetter $letter)
{
$this->letter = $letter;
}
public function build()
{
$letter = $this->letter;
return $this->subject('Mail Title')
->view('emails.topluKabulSon')
->attach(public_path($letter->pdf), [
'as' => 'AcceptanceLetter.pdf',
'mime' => 'application/pdf',
]);
}
}
And I created a function inside my AcceptanceLetter model to use mail easier :
public function sendAcceptanceLetter(){
Mail::to('******#gmail.com')->queue(new TopluKabulMektubu($this));
if(Mail::failures()){
$this->email_send = 2;
$this->save();
}else{
$this->email_send = 1;
$this->save();
}
}
I created a queue table with php artisan queue:table and migrated, also changed queue connection to database from env file.
And my job file:
class QueueJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $letter;
public function __construct($letter)
{
//
$this->letter = $letter;
}
public function handle()
{
$this->letter->sendAcceptanceLetter();
}
}
Web route triggers my job :
Route::get('/topluDeneme', [PaginationController::class, 'topluQueue']);
And the controller:
public function topluQueue(){
$letters = AcceptLetter::where('email', '!=', null)->where('passport_number','!=','0');
foreach($letters as $letter){
QueueJob::dispatch($letter);
}
}
I expect when i run php artisan queue:listen on terminal and go to /topluDeneme route, mails to be sent. But nothing happens on terminal, mails not sent and nothing changes on job datatable.
I found whats wrong with my code. It seems I forgot to use get() in the controller, here is the correct version :
public function topluQueue(){
$letters = AcceptLetter::where('email', '!=', null)->where('passport_number','!=','0')->get();
foreach($letters as $letter){
QueueJob::dispatch($letter);
}
}

Is it bad design to call a controller method from a job class

I recently wanted to implement queue functionality to my Laravel project, and as of now, it works. However, I'm not sure what the proper design pattern for a solution like this is since I'm new to Laravel.
I have a sync() method inside a ProductController class which is a void method that calls to an API, gets products, and inserts/updates records in a database. Since it takes around 2-5 minutes for the function to execute, I decided to try and implement a job to do it in the background.
I wasn't sure whether to copy the whole method and paste it into the handle() method inside the "SyncProducts" job class or call it from the controller class.
As of now, my job class looks like this.
class SyncProducts implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* #return void
*/
public $timeout = 1800;
public function __construct()
{
//
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
try {
(new \App\Http\Controllers\ProductController)->sync();
} catch (Exception $e) {
} catch (ResourceException $e) {
}
}
}
Inside the ProductController class, I added a new method that dispatches the job and redirects the user.
public function syncRun()
{
SyncProducts::dispatch();
return back();
}
Is this bad design? What is the proper way to implement it?
I consider it as bad design, and instead would use an action class which holds the logic in a re-usable way. The action class then can be called from a controller or from a job.
Here a (fairly basic) overview, for the sole purpose of giving an idea about the concept:
class MyWhateverAction
{
public function __construct($data) {
// whatever you need
}
public function execute()
{
// the logic which you now have in the controller
}
}
class MyWhateverController
{
public function synch($request, MyWhateverAction $action)
{
// do something to set $data
$action->execute($data)
return // whatever you need
}
}
class MyWhateverJob
{
public function handle($data, MyWhateverAction $action)
{
$action->execute($data)
}
}
More detailed infos about it:
a) https://stitcher.io/blog/laravel-queueable-actions
b) https://twitter.com/mmartin_joo/status/1509181862014509065?s=21

How to test Laravel 5 jobs?

I try to catch an event, when job is completed
Test code:
class MyTest extends TestCase {
public function testJobsEvents ()
{
Queue::after(function (JobProcessed $event) {
// if ( $job is 'MyJob1' ) then do test
dump($event->job->payload());
$event->job->payload()
});
$response = $this->post('/api/user', [ 'test' => 'data' ], $this->headers);
$response->assertSuccessful($response->isOk());
}
}
method in UserController:
public function userAction (Request $request) {
MyJob1::dispatch($request->toArray());
MyJob2::dispatch($request->toArray());
return response(null, 200);
}
My job:
class Job1 implements ShouldQueue {
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $data = [];
public function __construct($data)
{
$this->data= $data;
}
public function handle()
{
// Process uploaded
}
}
I need to check some data after job is complete but I get serialized data from
$event->job->payload() in Queue::after And I don't understand how to check job ?
Well, to test the logic inside handle method you just need to instantiate the job class & invoke the handle method.
public function testJobsEvents()
{
$job = new \App\Jobs\YourJob;
$job->handle();
// Assert the side effect of your job...
}
Remember, a job is just a class after all.
Laravel version ^5 || ^7
Synchronous Dispatching
If you would like to dispatch a job immediately (synchronously), you may use the dispatchNow method. When using this method, the job will not be queued and will be run immediately within the current process:
Job::dispatchNow()
Laravel 8 update
<?php
namespace Tests\Feature;
use App\Jobs\ShipOrder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Support\Facades\Bus;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_orders_can_be_shipped()
{
Bus::fake();
// Perform order shipping...
// Assert that a job was dispatched...
Bus::assertDispatched(ShipOrder::class);
// Assert a job was not dispatched...
Bus::assertNotDispatched(AnotherJob::class);
}
}
This my generic method, using a route
Route::get('job-tester/{job}', function ($job) {
if(env('APP_ENV') == 'local'){
$j = "\\App\Jobs\\".$job;
$j::dispatch();
}
});

Test Queue functionality?

According to the Laravel Documentation, I can use Queue::fake(); prevent jobs from being queued.
What is not clear how to test (PHPUnit) a few methods in the Job Class while it is not being queued.
For example:
class ActionJob extends Job
{
public $tries = 3;
protected $data;
public function __construct($data)
{
$this->data = $data;
}
public function handle()
{
if ($this->data['action'] == "deleteAllFiles") {
$this->deleteAllFiles();
}
}
protected function deleteAllFiles()
{
//delete all the files then return true
// if failed to delete return false
}
}
Here is example I want to test deleteAllFiles() - do I need to mock it?
The idea of using the fakes is that they're an alternative to mocking. So, yes, if you want to mock that deleteAllFiles() was called, then I don't believe you can do that with the fake.
However, you can assert that a certain attribute exists on the job.
One thing, it's not in your example, but make sure your job is implementing \Illuminate\Contracts\Queue\ShouldQueue.
Something like this
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class ActionJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $data; // Make sure this public so you can access it in your test
public function __construct($data)
{
$this->data = $data;
}
public function handle()
{
if ($this->data['action'] == "deleteAllFiles") {
$this->deleteAllFiles();
}
}
protected function deleteAllFiles()
{
// do stuff
}
}
Then in your test:
// ActionJobTest.php
Queue::fake();
// Do some things to set up date, call an endpoint, etc.
Queue::assertPushed(ActionJob::class, function ($job) {
return $job->data['action'] === 'deleteAllFiles';
});
If you want to assert on $data within the job, then you can make some other state change and assert on that in the Closure.
Side note: If the Job is Disptachable you can also assert like this:
// ActionJobTest.php
Bus::fake();
// Do some things to set up date, call an endpoint, etc.
Bus::assertDispatched(ActionJob::class, function ($job) {
return $job->data['action'] === 'deleteAllFiles';
});

Laravel integration testing jobs

I am trying to run an integration tests for my app. I have those jobs:
StartJob
PrepareJob
PeformJob
StartJob dispatches one or more PrepareJob, every PrepareJob dispatches one PerformJob.
Adding this
$this->expectsJobs(
[
StartJobs::class,
PrepareJob::class,
PerformJob::class
]
);
makes my test fail with error saying
1) JobsTest::testJobs
BadMethodCallException: Method Mockery_0_Illuminate_Contracts_Bus_Dispatcher::dispatchNow() does not exist on this mock object
Removing $this->expectsJobs makes all my tests pass, but I can't assert a given job was run, only whether it modified the DB to a given state.
StartJobs.php
class StartJobs extends Job implements ShouldQueue
{
use InteractsWithQueue;
use DispatchesJobs;
public function handle(Writer $writer)
{
$writer->info("[StartJob] Started");
for($i=0; $i < 5; $i++)
{
$this->dispatch(new PrepareJob());
}
$this->delete();
}
}
PrepareJob.php
class PrepareJob extends Job implements ShouldQueue
{
use InteractsWithQueue;
use DispatchesJobs;
public function handle(Writer $writer)
{
$writer->info("[PrepareJob] Started");
$this->dispatch(new PerformJob());
$this->delete();
}
}
PerformJob.php
class PerformJob extends Job implements ShouldQueue
{
use InteractsWithQueue;
public function handle(Writer $writer)
{
$writer->info("[PerformJob] Started");
$this->delete();
}
}
JobsTest.php
class JobsTest extends TestCase
{
/**
* #var Dispatcher
*/
protected $dispatcher;
protected function setUp()
{
parent::setUp();
$this->dispatcher = $this->app->make(Dispatcher::class);
}
public function testJobs()
{
$this->expectsJobs(
[
StartJobs::class,
PrepareJob::class,
PerformJob::class
]
);
$this->dispatcher->dispatch(new StartJobs());
}
}
I think it has to do something with how I am using a concrete dispatcher, while $this->expectsJob mocks the dispatcher. Might be related to this - https://github.com/laravel/lumen-framework/issues/207. What's the way to solve this?
To me it sounds like there is no dispatchNow()-method. In the Jobs your run dispatch() but the error says dispatchNow() does not exist.
Laravel didn't have the dispatchNow()-method before a certain version (i think Laravel 5.2 ... not sure) but just the dispatch(). Could be that the expectsJobs didn't think about that and fails.
You could try not passing it in one array but use 3 commands:
$this->expectsJobs(StartJobs::class);
$this->expectsJobs(PrepareJob::class);
$this->expectsJobs(PerformJob::class);
Maybe that helps.

Categories