Organize Realtime Job Management With Laravel and Vue Step by Step[Part 2]

<- Previous part

In previous part we created model for saving job execution progress. It shoud be applicable for any jobs. Now we needs scaffolding for create forms for run concrete jobs with its own parameters. Of course, we may create a table and eloquent model for each job, and in some cases it have a reason, but in this case we make create simple array with job definitions

Add in config/jobs.php some definitions

'definitions' => [
JobDefinition::create(DummyJob::class, 'jobs.forms.dummy')
->setTitle('Simple example job')
->setRules([
'delay' => 'integer|min:1|max:5',
'loop' => 'integer|min:1|max:5',
]),
JobDefinition::create(UrlChecker::class, 'jobs.forms.url')
->setTitle('Simple url checker')
->setRules([
'url' => 'url',
]),
JobDefinition::create(ContentChecker::class, 'jobs.forms.url')
->setTitle('Simple url content checker')
->setRules([
'url' => 'url',
]),
],

Prepare layout and views.

Create directory “jobs” in resources/views and add next views

As we keep in mind that exists different ways for store job definitions, we should create contract JobDefinitionRepository with a few methods

interface JobDefinitionRepository
{
/**
* @return Collection|\App\Lib\InteractiveJobs\JobDefinition[]
*/
public function jobDefinitions(): Collection;

public function findByName($jobName): ?JobDefinition;

public function exists($jobName): bool;
}

That is implementation, based on config values

Update InteractiveJobProvider

<?php
namespace App\Lib\InteractiveJobs;

use App\Lib\InteractiveJobs\Contracts\JobDefinitionRepository;
use Illuminate\Support\ServiceProvider;

class InteractiveJobsProvider extends ServiceProvider
{
public function boot()
{
/*We don't need subscription to queue events already, as
* we make own base class for interactive jobs
*/

}

public function provides()
{
return [JobDefinitionRepository::class];
}

public function register()
{
$this->app->singleton(
JobDefinitionRepository::class,
JobDefinitionConfigRepository::class
);
}
}

Lets go to update the JobController, (and routes)

Also we need to create ViewComposer for share definitions in jobs._menu

So now we can run http://your.app/jobs and check job forms from menu, but it didn’t works! 
 We needs observer for our Job Model where we can put the job command in a queue after the model has been created. Also we should update the DummyJob and make it inherited from the InteractiveJob class from previous part

<?php

namespace App\Lib\InteractiveJobs;

use App\Lib\InteractiveJobs\Models\Job;
use function dispatch;

class JobModelObserver
{
public function creating(Job $job)
{
$job
->fillDefaults();
}

public function created(Job $job)
{
try{
$command = app()->make(
$job->command, ['jobModel'=>$job]
);
dispatch($command)->onQueue($job->queue)->delay(2);
}catch (\Throwable $e){
$job
->delete();
}
}
}

register it in boot method of InteractiveJobsProvider `Job::observe(JobModelObserver::class);`

class DummyJob extends InteractiveJob
{

public function execute(DummyService $service)
{
$payload = $this->jobModel->payload;
$service->dummyJobLogic($payload['loop'], $payload['delay']);
}
}

Fix JobDbLogHandler class , because now we have put the Job Model as context, instead of array

//...
protected function write(array $record)
{
$jobContext = Config::get('jobs.context');
if($jobContext instanceof Job){
DB::table('job_logs')->insert([
'loggable_id'=>$jobContext->id,
'loggable_type'=>$jobContext->commandName(),
...

Oooh, seems it should be works now; Run it!

As you see, it works, but Job state is not updated. Put this methods in the model Job

public function updateProgress(int $value)
{
$this->progress = max($value, 100);
$this->save();
}

public function activate()
{
$this->state = JobState::PROCESSING;
$this->save();
}

public function retryAfterFail()
{
$this->state = JobState::RETRY;
$this->attempts += 1;
$this->save();
}

public function finish(bool $success = true)
{
$this->state = $success ? JobState::SUCCESS : JobState::FAIL;
$this->finished_at = Carbon::now();
$this->save();
}

And apply its in InteractiveJob class

protected function beforeStart()
{
$this->jobModel->activate();
}

protected function onFinish()
{
$this->jobModel->finish(true);
}

protected function onFail(\Throwable $e)
{
if ($this instanceof Reportable) {
$this->jobModel->report = collect(['error' => $e->getMessage()]);
}
if ($this->isWillBeRetry()) {
$this->jobModel->retryAfterFail();
} else {
$this->jobModel->finish(false);
}
$this->fail($e);
}

protected function isWillBeRetry(): bool
{
return is_null($this->job->maxTries())
|| $this->attempts() < $this->job->maxTries();
}

protected function isRetried(): bool
{
return $this->jobModel->attempts > 1;
}

You might have noticed that the code mentions the Reportable interface — it is just empty interface used as a flag that means implemented job generate report collection and it must be saved

Try to run a new job from site now, and ensure that the Job Model have been changed state after job execution. You can play with DummyJob execution and touch updateProgress method of the Job Model

Well, it seems that now there is only lack of interactivity

For this purpose we need to install several node packages

yarn add laravel-echo laravel-echo-server vue-echo vue-toasted

If you are not already familiar with the laravel-echo-server, look at github https://github.com/tlaverdure/laravel-echo-server

run laravel-echo-server init && laravel-echo-server client:add

next modify the newly generated laravel-echo-server.json set actual values for “authHost” and “databaseConfig.redis”

I made three kinds of job notifications for broadcasting — about progress changing, state changing and simple message, and one user notification

Now we will apply its in the Job model.

Firstly, add Notifable trait

use Notifable and declare

public function receivesBroadcastNotificationsOn()
{
return 'Job.' . $this->id;
}

replace method updateProgress

public function updateProgress(int $value, $message = null)
{
if ($message) {
$this->notifyNow(new LogMessage($message));
}
$this->progress = max($value, 100);
$this->save();
$this->notifyNow(new Progress($value));
}

and add new one

protected function stateChanged()
{
$this->notifyNow(new StateMessage($this->state));
$this->owner->notifyNow(
$this->jobState()->notice($this->identity())
);
}

Add $this->stateChanged(); at the end of methods ‘activate’, ‘finish’,’retryAfterFail’

I add notice method in the JobState class for generating state-based notices for user

//App/InteractiveJobs/JobState....
protected static $notices
= [
self::FAIL => [Notificator::TYPE_ERROR, 'Job Execution {job} failed'],
self::PROCESSING => [Notificator::TYPE_INFO, 'Job Execution {job} started'],
self::SUCCESS => [Notificator::TYPE_SUCCESS, 'Job Execution {job} finished'],
self::RETRY => [Notificator::TYPE_WARNING, 'Job Execution {job} failed, job will be restarted'],
];
public function notice($job = ''): Notification
{
[$type, $message] = self::$notices[$this->value];
$message = strtr($message, ['job'=>$job]);
return new UserMessage($type, $message);
}

Finally we uncomment string ‘Illuminate\Broadcasting\BroadcastServiceProvider::class‘ in the providers array of your config/app.php configuration file.

add to routes/channels.php

use App\User;

Broadcast::channel('User.{id}', function ($user, $id) {
return (int)$user->id === (int)$id;
});
Broadcast::channel('Job.{id}', function (User $user, $id) {
return $user->hasJob($id);
});

and add in the User model method for define broadcasting route

public function receivesBroadcastNotificationsOn()
{
return 'User.' . $this->id;
}

Continuation in the next part ->

Like what you read? Give Insolita a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.