Skip to content

Commit 8a77ec0

Browse files
Added automatic tracking of trackable jobs via queueing event listener
1 parent 4f0ed16 commit 8a77ec0

12 files changed

+185
-12
lines changed

README.md

+2-6
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class MyJob implements \Konekt\History\Contracts\TrackableJob
3737

3838
public function handle()
3939
{
40-
$tracker = JobTracker::of($this);
40+
$tracker = $this->jobTracker();
4141
$tracker->setProgressMax(count($this->dataToProcess));
4242
$tracker->started();
4343
try {
@@ -53,11 +53,7 @@ class MyJob implements \Konekt\History\Contracts\TrackableJob
5353
}
5454
}
5555

56-
$job = new MyJob();
57-
$job->generateJobTrackingId();
58-
59-
JobTracker::createFor($job);
60-
Bus::dispatch($job);
56+
MyJob::dispatch($myDataToProcess);
6157
```
6258

6359
## Features

docs/README.md

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
# Eloquent Model History Documentation
1+
# Eloquent Model History + Laravel Job History Documentation
22

3-
History is a Laravel package to manage the history of changes of Eloquent models.
3+
History is a Laravel package to manage the history of:
4+
5+
1. Changes, diff and comments of Eloquent models;
6+
2. Track and log the execution history of Laravel background jobs;
47

58
!> This library **DOES NOT automatically hook into the model's lifecycle events**, but allows the developer to manually register history events at arbitrary places in the application code.
69

710
## Features
811

12+
### Eloquent Model History
13+
914
- Record model creation, update, delete, and retrieval
1015
- Add optional comments to events
1116
- Add comment-only history events
@@ -15,6 +20,8 @@ History is a Laravel package to manage the history of changes of Eloquent models
1520
- Define included/excluded fields on a per-model basis
1621
- Has a diff of the changed fields (old/new values)
1722

23+
### Eloquent Model History
24+
1825
## Alternatives
1926

2027
If you need a strict audit tool for Laravel, that can automatically record changes, wherever they happen,

src/Concerns/CanBeTracked.php

+20-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
namespace Konekt\History\Concerns;
1616

1717
use Illuminate\Support\Str;
18+
use Konekt\History\JobTracker;
1819

1920
trait CanBeTracked
2021
{
21-
public string $job_tracking_id;
22+
private ?JobTracker $jobTracker = null;
2223

23-
public function getJobTrackingId(): string
24+
public ?string $job_tracking_id = null;
25+
26+
public function getJobTrackingId(): ?string
2427
{
2528
return $this->job_tracking_id;
2629
}
@@ -40,4 +43,19 @@ public function generateJobTrackingId(): static
4043
{
4144
return $this->setJobTrackingId(Str::ulid()->toBase58());
4245
}
46+
47+
public function hasTrackingId(): bool
48+
{
49+
return null !== $this->job_tracking_id;
50+
}
51+
52+
public function doesNotHaveTrackingIdYet(): bool
53+
{
54+
return null === $this->job_tracking_id;
55+
}
56+
57+
protected function jobTracker(): JobTracker
58+
{
59+
return $this->jobTracker ??= JobTracker::of($this);
60+
}
4361
}

src/Contracts/TrackableJob.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,13 @@
1616

1717
interface TrackableJob
1818
{
19-
public function getJobTrackingId(): string;
19+
public function getJobTrackingId(): ?string;
20+
21+
public function setJobTrackingId(string $trackingId): static;
22+
23+
public function generateJobTrackingId(): static;
24+
25+
public function hasTrackingId(): bool;
26+
27+
public function doesNotHaveTrackingIdYet(): bool;
2028
}

src/JobTracker.php

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ public static function of(TrackableJob $job): static
6060
*/
6161
public static function createFor(TrackableJob $job, int $maxProgress = 100): JobExecution
6262
{
63+
if ($job->doesNotHaveTrackingIdYet()) {
64+
// We could generate a tracking ID here, but that would mean
65+
// that the tracking ID may or may not be persisted along
66+
// with the job to the queue and lead to inconsistency
67+
throw new \LogicException('The job to be tracked does not have a tracking ID yet.');
68+
}
69+
6370
$result = JobExecutionProxy::create(array_merge([
6471
'queued_at' => Carbon::now(),
6572
'progress_max' => $maxProgress,

src/Listeners/StartJobTracking.php

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Konekt\History\Listeners;
6+
7+
use Illuminate\Queue\Events\JobQueueing;
8+
use Konekt\History\Contracts\TrackableJob;
9+
use Konekt\History\JobTracker;
10+
11+
class StartJobTracking
12+
{
13+
public function handle(JobQueueing $event)
14+
{
15+
if ($event->job instanceof TrackableJob && $event->job->doesNotHaveTrackingIdYet()) {
16+
$event->job->generateJobTrackingId();
17+
JobTracker::createFor($event->job);
18+
}
19+
}
20+
}

src/Models/JobExecution.php

+22
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
use Illuminate\Contracts\Auth\Authenticatable;
18+
use Illuminate\Database\Eloquent\Builder;
1819
use Illuminate\Database\Eloquent\Model;
1920
use Illuminate\Database\Eloquent\Relations\BelongsTo;
2021
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -51,6 +52,8 @@
5152
*
5253
* @property-read Model|Authenticatable $user
5354
* @property-read Collection $logs
55+
*
56+
* @method static Builder ofJobClass(string $jobClass)
5457
*/
5558
class JobExecution extends Model implements JobExecutionContract
5659
{
@@ -72,6 +75,20 @@ public static function findByTrackingId(string $id): ?self
7275
return static::where('tracking_id', $id)->first();
7376
}
7477

78+
public static function byJobClass(string $jobClass, bool $activeOnesOnly = false, ?int $limit = null): Collection
79+
{
80+
$query = static::ofJobClass($jobClass)->orderBy('queued_at', 'desc');
81+
if ($activeOnesOnly) {
82+
$query->whereNull('failed_at')->whereNull('completed_at');
83+
}
84+
85+
if (null !== $limit) {
86+
$query->take($limit);
87+
}
88+
89+
return $query->get();
90+
}
91+
7592
public function user(): BelongsTo
7693
{
7794
return $this->belongsTo(config('konekt.history.user_model', Auth::getProvider()->getModel()));
@@ -174,6 +191,11 @@ public function getUser(): ?Authenticatable
174191
return $this->user;
175192
}
176193

194+
protected function scopeOfJobClass(Builder $query, string $jobClass): Builder
195+
{
196+
return $query->where('job_class', $jobClass);
197+
}
198+
177199
protected function log(string $message, string $level, array $context): JobExecutionLogContract
178200
{
179201
return $this->logs()->create([

src/Providers/ModuleServiceProvider.php

+5
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
use Illuminate\Queue\Events\JobProcessed;
1818
use Illuminate\Queue\Events\JobProcessing;
19+
use Illuminate\Queue\Events\JobQueueing;
1920
use Illuminate\Support\Facades\App;
21+
use Illuminate\Support\Facades\Event;
2022
use Illuminate\Support\Facades\Queue;
2123
use Konekt\Concord\BaseModuleServiceProvider;
24+
use Konekt\History\Listeners\StartJobTracking;
2225
use Konekt\History\Models\JobExecution;
2326
use Konekt\History\Models\JobExecutionLog;
2427
use Konekt\History\Models\JobStatus;
@@ -46,6 +49,8 @@ public function boot(): void
4649
parent::boot();
4750
App::bind(JobInfo::SERVICE_NAME, fn () => null);
4851

52+
Event::listen(JobQueueing::class, StartJobTracking::class);
53+
4954
Queue::before(function (JobProcessing $event) {
5055
App::instance(
5156
JobInfo::SERVICE_NAME,

src/resources/database/migrations/2024_11_12_120632_create_job_executions_table.php

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function up(): void
2828
$table->timestamps();
2929

3030
$table->index('queued_at');
31+
$table->index('job_class');
3132

3233
$table->foreign('user_id')
3334
->references('id')

tests/Dummies/SampleTrackableJob.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function plantLogForTesting(string $level, string $message, array $contex
3737

3838
public function handle(): void
3939
{
40-
$tracker = JobTracker::of($this);
40+
$tracker = $this->jobTracker();
4141
$tracker->started();
4242

4343
foreach ($this->logs as $log) {

tests/JobExecutionTest.php

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Konekt\History\Tests;
6+
7+
use Illuminate\Support\Facades\Bus;
8+
use Konekt\History\JobTracker;
9+
use Konekt\History\Models\JobExecution;
10+
use Konekt\History\Models\JobStatus;
11+
use Konekt\History\Tests\Dummies\SampleTask;
12+
use Konekt\History\Tests\Dummies\SampleTrackableJob;
13+
14+
class JobExecutionTest extends TestCase
15+
{
16+
/** @test */
17+
public function it_can_find_and_entry_by_tracking_id()
18+
{
19+
$job = new SampleTrackableJob(new SampleTask());
20+
$job->generateJobTrackingId();
21+
JobTracker::createFor($job);
22+
23+
$execution = JobExecution::findByTrackingId($job->getJobTrackingId());
24+
25+
$this->assertInstanceOf(JobExecution::class, $execution);
26+
$this->assertEquals($job->getJobTrackingId(), $execution->tracking_id);
27+
}
28+
29+
/** @test */
30+
public function it_can_return_the_entries_of_a_given_job_class()
31+
{
32+
foreach (range(1, 10) as $i) {
33+
$job = new SampleTrackableJob(new SampleTask());
34+
$job->generateJobTrackingId();
35+
JobTracker::createFor($job);
36+
}
37+
38+
$entries = JobExecution::byJobClass(SampleTrackableJob::class);
39+
$this->assertCount(10, $entries);
40+
}
41+
42+
/** @test */
43+
public function it_can_return_the_active_entries_of_a_given_job_class()
44+
{
45+
foreach (range(1, 4) as $i) {
46+
$job = new SampleTrackableJob(new SampleTask(), JobStatus::COMPLETED());
47+
$job->generateJobTrackingId();
48+
JobTracker::createFor($job);
49+
50+
Bus::dispatch($job);
51+
}
52+
53+
foreach (range(1, 2) as $i) {
54+
$job = new SampleTrackableJob(new SampleTask());
55+
$job->generateJobTrackingId();
56+
JobTracker::createFor($job);
57+
58+
Bus::dispatch($job);
59+
}
60+
61+
$entries = JobExecution::byJobClass(SampleTrackableJob::class, true);
62+
$this->assertCount(2, $entries);
63+
}
64+
65+
/** @test */
66+
public function it_can_return_the_entries_of_a_given_job_class_and_limit_the_record_count()
67+
{
68+
foreach (range(1, 10) as $i) {
69+
$job = new SampleTrackableJob(new SampleTask());
70+
$job->generateJobTrackingId();
71+
JobTracker::createFor($job);
72+
}
73+
74+
$entries = JobExecution::byJobClass(SampleTrackableJob::class, false, 5);
75+
$this->assertCount(5, $entries);
76+
}
77+
}

tests/JobTrackerTest.php

+12
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Konekt\History\Tests;
66

77
use Illuminate\Contracts\Auth\Authenticatable;
8+
use Illuminate\Queue\Events\JobQueueing;
89
use Illuminate\Support\Carbon;
910
use Illuminate\Support\Facades\Auth;
1011
use Illuminate\Support\Facades\Bus;
@@ -17,6 +18,7 @@
1718
use Konekt\History\Events\TrackableJobLogCreated;
1819
use Konekt\History\Events\TrackableJobStarted;
1920
use Konekt\History\JobTracker;
21+
use Konekt\History\Listeners\StartJobTracking;
2022
use Konekt\History\Models\JobExecution;
2123
use Konekt\History\Models\JobStatus;
2224
use Konekt\History\Tests\Dummies\SampleTask;
@@ -133,6 +135,16 @@ public function it_can_log_during_execution()
133135
$this->assertCount(3, $logs);
134136
}
135137

138+
/** @test */
139+
public function the_start_job_tracking_listener_is_active()
140+
{
141+
// I wanted to test here whether the listener works well
142+
// and that the tracking id was initialized correctly
143+
// but during tests queue listeners aren't invoked
144+
Event::fake();
145+
Event::assertListening(JobQueueing::class, StartJobTracking::class);
146+
}
147+
136148
/** @test */
137149
public function it_emits_a_created_event_when_creating_via_the_tracker()
138150
{

0 commit comments

Comments
 (0)