Skip to content
This repository was archived by the owner on Dec 3, 2024. It is now read-only.

Commit 449c4b9

Browse files
authored
Merge pull request #11 from Sammyjo20/feature/haystack-state
Feature | Haystack Data
2 parents e40e383 + 77c3f84 commit 449c4b9

23 files changed

+763
-43
lines changed

README.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ That's right! Let's just be clear that we're not talking about **Batched Jobs**.
4040
- They are volatile, meaning if you lose one job in the chain - you lose the whole chain.
4141
- They do not provide the `then`, `catch`, `finally` callable methods that batched jobs do.
4242
- Long delays with memory based or SQS queue is not possible as you could lose the jobs due to expiry or if the server shuts down.
43+
- You can't share data between jobs as there is no "state" across the chain
4344

4445
Laravel Haystack aims to solve this by storing the job chain in the database and queuing one job at a time. When the job is completed, Laravel Haystack listens out for the "job completed" event and queues the next job in the chain from the database.
4546

@@ -49,6 +50,7 @@ Laravel Haystack aims to solve this by storing the job chain in the database and
4950
- It provides callback methods like `then`, `catch` and `finally`.
5051
- Global middleware that can be applied to every single job in the chain
5152
- Delay that can be added to every job in the chain
53+
- You can store and retrieve data/state that is accessible to every job in the chain.
5254
- You can store the model for later processing.
5355

5456
### Use Cases
@@ -429,6 +431,102 @@ class ProcessPodcast implements ShouldQueue, StackableJo
429431
}
430432
```
431433

434+
## Shared Job Data
435+
436+
Laravel Haystack has the ability for your jobs to store and retrieve state/data between jobs. This is really useful if you need to store data in the first job and then in the second job, process the data and in the final job, email the processed data. You are able to create a process pipeline since each job is processed in sequential order. This is really exciting because with traditional chained jobs, you cannot share data between jobs.
437+
438+
```php
439+
<?php
440+
441+
$haystack = Haystack::build()
442+
->addJob(new RetrieveDataFromApi)
443+
->addJob(new ProcessDataFromApi)
444+
->addJob(new StoreDataFromApi)
445+
->dispatch();
446+
```
447+
448+
### Storing data inside of jobs
449+
450+
Inside your job, you can use the `setHaystackData()` method to store some data. This method accepts a key, value and optional Eloquent cast.
451+
452+
```php
453+
<?php
454+
455+
namespace App\Jobs;
456+
457+
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
458+
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
459+
460+
class RetrieveDataFromApi implements ShouldQueue, StackableJob
461+
{
462+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
463+
464+
public function handle()
465+
{
466+
// Your application code...
467+
468+
$this->setHaystackData('username', 'Sammyjo20');
469+
}
470+
```
471+
472+
### Casting data
473+
474+
The `setHaystackData` method supports any data type. It supports fully casting your data into any of Eloquent's existing casts, or even your custom casts. Just provide a third argument to specify the cast.
475+
476+
```php
477+
<?php
478+
479+
namespace App\Jobs;
480+
481+
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
482+
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
483+
484+
class RetrieveDataFromApi implements ShouldQueue, StackableJob
485+
{
486+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
487+
488+
public function handle()
489+
{
490+
// Your application code...
491+
492+
// Array Data, provide third argument to specify cast.
493+
494+
$this->setHaystackData('data', ['username' => 'Sammyjo20'], 'array');
495+
496+
// Supports custom casts
497+
498+
$this->setHaystackData('customData', $object, CustomCast::class);
499+
}
500+
```
501+
502+
### Retrieving data inside of jobs
503+
504+
From one job you can set the data, but that data will be available to every job in the haystack there after. Just use the `getHaystackData` method to get data by key or use the `allHaystackData` to get a collection containing the haystack data.
505+
506+
```php
507+
<?php
508+
509+
namespace App\Jobs;
510+
511+
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
512+
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
513+
514+
class RetrieveDataFromApi implements ShouldQueue, StackableJob
515+
{
516+
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
517+
518+
public function handle()
519+
{
520+
// Get data by key
521+
522+
$username = $this->getHaystackData('username'); // Sammyjo20
523+
524+
// Get all data
525+
526+
$allData = $this->allHaystackData(); // Collection: ['username' => 'Sammyjo20']
527+
}
528+
```
529+
432530
## Manual Processing
433531

434532
By default, Laravel Haystack will use events to automatically process the next job in the haystack. If you would like to disable this functionality, you will need to make sure to disable the `automatic_processing` in your config/haystack.php file. After that, you will need to make sure your jobs tell Haystack when to process the next job.

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"spatie/laravel-package-tools": "^1.9.2"
2727
},
2828
"require-dev": {
29+
"jessarcher/laravel-castable-data-transfer-object": "^2.2",
2930
"laravel/pint": "^1.0",
3031
"orchestra/testbench": "^7.0",
3132
"pestphp/pest": "^1.21",

config/haystack.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
return [
44

5+
/*
6+
|--------------------------------------------------------------------------
7+
| Return All Haystack Data When Finished
8+
|--------------------------------------------------------------------------
9+
|
10+
| This value if set to true, will instruct Haystack to query all the
11+
| haystack data rows out of the database and return them to the
12+
| then/finally/catch blocks as a collection.
13+
|
14+
*/
15+
16+
'return_all_haystack_data_when_finished' => true,
17+
518
/*
619
|--------------------------------------------------------------------------
720
| Queue Haystack Jobs Automatically
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Sammyjo20\LaravelHaystack\Database\Factories;
4+
5+
use Sammyjo20\LaravelHaystack\Models\HaystackData;
6+
use Illuminate\Database\Eloquent\Factories\Factory;
7+
8+
class HaystackDataFactory extends Factory
9+
{
10+
protected $model = HaystackData::class;
11+
12+
/**
13+
* Definition
14+
*
15+
* @return array|mixed[]
16+
*/
17+
public function definition()
18+
{
19+
return [];
20+
}
21+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
use Sammyjo20\LaravelHaystack\Models\Haystack;
4+
use Illuminate\Database\Migrations\Migration;
5+
use Illuminate\Database\Schema\Blueprint;
6+
use Illuminate\Support\Facades\Schema;
7+
8+
return new class extends Migration
9+
{
10+
public function up()
11+
{
12+
Schema::create('haystack_data', function (Blueprint $table) {
13+
$table->id();
14+
$table->timestamps();
15+
$table->foreignIdFor(Haystack::class)->constrained()->cascadeOnDelete();
16+
$table->string('key');
17+
$table->binary('value');
18+
$table->string('cast')->nullable();
19+
20+
$table->unique(['haystack_id', 'key']);
21+
});
22+
}
23+
24+
public function down()
25+
{
26+
Schema::dropIfExists('haystack_data');
27+
}
28+
};

database/migrations/create_haystacks_table.php.stub

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ return new class extends Migration
1818
$table->dateTime('started_at')->nullable();
1919
$table->dateTime('resume_at')->nullable();
2020
$table->dateTime('finished_at')->nullable();
21+
$table->boolean('return_data')->default(true);
2122
});
2223
}
2324

src/Builders/HaystackBuilder.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ class HaystackBuilder
6969
*/
7070
protected ?Closure $globalMiddleware = null;
7171

72+
/**
73+
* Should we return the data when the haystack finishes?
74+
*
75+
* @var bool
76+
*/
77+
protected bool $returnDataOnFinish = true;
78+
7279
/**
7380
* Constructor
7481
*/
@@ -263,13 +270,26 @@ protected function createHaystack(): Haystack
263270
$haystack->on_catch = $this->onCatch;
264271
$haystack->on_finally = $this->onFinally;
265272
$haystack->middleware = $this->globalMiddleware;
273+
$haystack->return_data = $this->returnDataOnFinish;
266274
$haystack->save();
267275

268276
$haystack->bales()->insert($this->prepareJobsForInsert($haystack));
269277

270278
return $haystack;
271279
}
272280

281+
/**
282+
* Specify if you do not want haystack to return the data.
283+
*
284+
* @return $this
285+
*/
286+
public function dontReturnData(): static
287+
{
288+
$this->returnDataOnFinish = false;
289+
290+
return $this;
291+
}
292+
273293
/**
274294
* Get all the jobs in the builder.
275295
*

src/Concerns/ManagesBales.php

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
use Closure;
66
use Carbon\CarbonImmutable;
77
use Carbon\CarbonInterface;
8-
use Illuminate\Queue\Jobs\Job;
8+
use InvalidArgumentException;
9+
use Illuminate\Support\Collection;
910
use Illuminate\Contracts\Queue\ShouldQueue;
1011
use Sammyjo20\LaravelHaystack\Data\NextJob;
1112
use Sammyjo20\LaravelHaystack\Models\HaystackBale;
13+
use Sammyjo20\LaravelHaystack\Models\HaystackData;
1214
use Sammyjo20\LaravelHaystack\Helpers\CarbonHelper;
1315
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
1416
use Sammyjo20\LaravelHaystack\Data\PendingHaystackBale;
@@ -152,13 +154,17 @@ public function finish(bool $fail = false): void
152154

153155
$this->update(['finished_at' => now()]);
154156

157+
$returnAllData = config('haystack.return_all_haystack_data_when_finished', false);
158+
159+
$allData = $this->return_data === true && $returnAllData === true ? $this->allData() : null;
160+
155161
$fail === true
156-
? $this->executeClosure($this->on_catch)
157-
: $this->executeClosure($this->on_then);
162+
? $this->executeClosure($this->on_catch, $allData)
163+
: $this->executeClosure($this->on_then, $allData);
158164

159165
// Always execute the finally closure.
160166

161-
$this->executeClosure($this->on_finally);
167+
$this->executeClosure($this->on_finally, $allData);
162168

163169
// Now finally delete itself.
164170

@@ -213,12 +219,13 @@ public function appendPendingJob(PendingHaystackBale $pendingJob): void
213219
* Execute the closure.
214220
*
215221
* @param Closure|null $closure
222+
* @param Collection|null $data
216223
* @return void
217224
*/
218-
protected function executeClosure(?Closure $closure): void
225+
protected function executeClosure(?Closure $closure, ?Collection $data = null): void
219226
{
220227
if ($closure instanceof Closure) {
221-
$closure();
228+
$closure($data);
222229
}
223230
}
224231

@@ -232,4 +239,52 @@ public function pause(CarbonImmutable $resumeAt): void
232239
{
233240
$this->update(['resume_at' => $resumeAt]);
234241
}
242+
243+
/**
244+
* Store data on the Haystack.
245+
*
246+
* @param string $key
247+
* @param mixed $value
248+
* @param string|null $cast
249+
* @return ManagesBales|\Sammyjo20\LaravelHaystack\Models\Haystack
250+
*/
251+
public function setData(string $key, mixed $value, string $cast = null): self
252+
{
253+
if (is_null($cast) && is_string($value) === false && is_int($value) === false) {
254+
throw new InvalidArgumentException('You must specify a cast if the value is not a string or integer.');
255+
}
256+
257+
$this->data()->updateOrCreate(['key' => $key], [
258+
'cast' => $cast,
259+
'value' => $value,
260+
]);
261+
262+
return $this;
263+
}
264+
265+
/**
266+
* Retrieve data by a key from the Haystack.
267+
*
268+
* @param string $key
269+
* @param mixed|null $default
270+
* @return mixed
271+
*/
272+
public function getData(string $key, mixed $default = null): mixed
273+
{
274+
$data = $this->data()->where('key', $key)->first();
275+
276+
return $data instanceof HaystackData ? $data->value : $default;
277+
}
278+
279+
/**
280+
* Retrieve all the data from the Haystack.
281+
*
282+
* @return Collection
283+
*/
284+
public function allData(): Collection
285+
{
286+
return $this->data()->orderBy('id')->get()->mapWithKeys(function ($value, $key) {
287+
return [$value->key => $value->value];
288+
});
289+
}
235290
}

0 commit comments

Comments
 (0)