Package Data | |
---|---|
Maintainer Username: | SamCarre |
Maintainer Contact: | 29132017+Sammyjo20@users.noreply.github.com (Sammyjo20) |
Package Create Date: | 2022-07-25 |
Package Last Update: | 2024-03-02 |
Home Page: | https://docs.laravel-haystack.dev |
Language: | PHP |
License: | MIT |
Last Refreshed: | 2024-04-25 15:13:05 |
Package Statistics | |
---|---|
Total Downloads: | 89,121 |
Monthly Downloads: | 6,512 |
Daily Downloads: | 354 |
Total Stars: | 592 |
Total Watchers: | 9 |
Total Forks: | 24 |
Total Open Issues: | 6 |
Beautifully simple but powerful database-driven job chains.
Laravel Haystack is a package that allows you to have a job chain stored in the database. Since all of the jobs in the chain are in the database, memory usage is low and you can delay jobs for a long time or have long running jobs without risking using all your memory. Laravel Haystack supports every queue connection/worker out of the box. (Database, Redis/Horizon, SQS).
$haystack = Haystack::build()
->withName('Podcast Chain')
->addJob(new RecordPodcast)
->addJob(new ProcessPodcast)
->addJob(new PublishPodcast)
->then(function () {
// Haystack completed
})
->catch(function () {
// Haystack failed
})
->finally(function () {
// Always run either on success or fail.
})
->paused(function () {
// Run if the haystack is paused
})
->withMiddleware([
// Middleware to apply on every job
])
->withDelay(60) // Add a delay to every job
->dispatch();
That's right! Let's just be clear that we're not talking about Batched Jobs. Laravel does have job chains but they have some considerations.
then
, catch
, finally
callable methods that batched jobs do.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.
then
, catch
and finally
.You can install the package via composer:
composer require sammyjo20/laravel-haystack
Requires Laravel 8+ and PHP 8.1
Then publish and run the migrations with:
php artisan vendor:publish --tag="haystack-migrations"
php artisan migrate
You can publish the config file with:
php artisan vendor:publish --tag="haystack-config"
To prepare your jobs for Laravel Haystack, you must add the StackableJob interface and Stackable trait to your jobs. This provides the properties and methods Haystack requires.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class ProcessPodcast implements ShouldQueue, StackableJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
Let’s create our first Haystack. To get started, import the Haystack model and then call the “build” static function on it. This will provide you with the HaystackBuilder class which can be used to define your haystack. You can then use the “addJob” method to add jobs to the Haystack. Make sure all the jobs have the StackableJob interface and Stackable trait.
<?php
use Sammyjo20\LaravelHaystack\Models\Haystack;
Haystack::build()
->addJob(new RecordPodcast)
->addJob(new PublishPodcast)
->addJob(new TweetAboutPodcast);
You may also use the
addJobs()
method which accepts an array or collection of stackable jobs.
Once you have created your Haystack, you can dispatch it using the dispatch
method.
<?php
use Sammyjo20\LaravelHaystack\Models\Haystack;
Haystack::build()
->addJob(new RecordPodcast)
->addJob(new PublishPodcast)
->addJob(new TweetAboutPodcast)
->dispatch();
Use the create method to create the model and store it somewhere for later processing. You can start the Haystack at any time.
<?php
use Sammyjo20\LaravelHaystack\Models\Haystack;
$haystack = Haystack::build()
->addJob(new RecordPodcast)
->addJob(new PublishPodcast)
->addJob(new TweetAboutPodcast)
->create();
// Do other things...
$haystack->start(); // Initiate haystack
If you need to cancel the Haystack during processing, you can do this by using the cancel
method on the Haystack model. If a job is being processed when you cancel it, it will process the next
job and then stop before the job is executed.
use Sammyjo20\LaravelHaystack\Models\Haystack;
$haystack = Haystack::build()
->addJob(new RecordPodcast)
->addJob(new PublishPodcast)
->addJob(new TweetAboutPodcast)
->create();
// Store Haystack
$haystack->start();
// Do other things...
$haystack->cancel();
When a haystack is cancelled, we will not run the
then
orcatch
closures, but we will execute thefinally
closure.
Laravel Haystack provides you with three really simple callback events that will be serialized and used at certain points in time.
The “then” event is triggered when the haystack has completed successfully.
$haystack = Haystack::build()
->addJob(new RecordPodcast)
->then(function () {
// Do something...
})
->dispatch();
The “catch” event is triggered when the haystack has failed. You will still have the failed job to see what the error was, but this is useful if you need to perform any cleanup.
$haystack = Haystack::build()
->addJob(new RecordPodcast)
->catch(function () {
// Do something...
})
->dispatch();
The “finally “event is always triggered at the end of a haystack, if it was successful or if it failed. This is very useful if you want to have loading states in your application.
$haystack = Haystack::build()
->addJob(new RecordPodcast)
->finally(function () {
// Do something...
})
->dispatch();
The "paused" event is triggered if the Haystack has been paused using the pauseHaystack
method or a job has been released using the longRelease
method. This is useful if you need to update the database
to mark an import as paused, especially if the pause is for a long time.
$haystack = Haystack::build()
->addJob(new RecordPodcast)
->paused(function () {
// Do something...
})
->dispatch();
Each of these methods support invokable classes.
$haystack = Haystack::build()
->addJob(new RecordPodcast)
->then(new Then)
->catch(new Catch)
->finally(new Finally)
->paused(new Paused)
->dispatch();
// Example Invokable class
class Then {
public function __invoke()
{
// Do something...
}
}
You can configure a global delay, connection and queue that will apply to all jobs in the haystack. You can also provide a per-job configuration if you choose to too.
You can use the withDelay
method to apply a global delay to every job.
$haystack = Haystack::build()
->withDelay(60)
->addJob(new RecordPodcast)
->addJob(new ProcessPodcast)
->dispatch();
You can use the onConnection
method to use a given connection for every job.
$haystack = Haystack::build()
->onConnection('redis')
->addJob(new RecordPodcast)
->addJob(new ProcessPodcast)
->dispatch();
You can use the onQueue
method to use a given queue for every job.
$haystack = Haystack::build()
->onQueue('podcasts')
->addJob(new RecordPodcast)
->addJob(new ProcessPodcast)
->dispatch();
You can also choose to use a different delay, connection or queue for every job!
$haystack = Haystack::build()
->onQueue('podcasts')
->addJob(new RecordPodcast, delay: 60, queue: 'low', connection: 'redis')
->addJob(new ProcessPodcast, delay: 120, queue: 'high', connection: 'sqs')
->dispatch();
If you have already configured the job with delay, connection or queue, it will use that configuration.
You can also provide middleware that will be applied to every job in the haystack. This is perfect if you don’t want to manually add the middleware to every job, or if the middleware can’t belong to the job on its own. This is extremely useful if you want to apply an API rate limiter to your requests. Just use the withMiddleware
method. It will accept either an array, a closure that returns an array or an invokable class that returns an array.
$haystack = Haystack::build()
->onQueue('podcasts')
->addJob(new RecordPodcast)
->addJob(new ProcessPodcast)
->withMiddleware([
(new RateLimited)->allows(30)->everyMinute(),
new OtherMiddleware,
])
->dispatch();
You must return an array in the closure for it to work.
$haystack = Haystack::build()
->onQueue('podcasts')
->addJob(new RecordPodcast)
->addJob(new ProcessPodcast)
->withMiddleware(function () {
return [
(new RateLimited)->allows(30)->everyMinute(),
new OtherMiddleware,
];
})
->dispatch();
You must return an array inside your invokable class for it to work.
$haystack = Haystack::build()
->onQueue('podcasts')
->addJob(new RecordPodcast)
->addJob(new ProcessPodcast)
->withMiddleware(new PodcastMiddleware)
->dispatch();
// Invokable class...
class PodcastMiddleware {
public function __invoke()
{
return [
(new RateLimited)->allows(30)->everyMinute(),
new OtherMiddleware,
];
}
}
You can append to the haystack inside a job. The appended job will go at the end of the chain. Just use the appendToHaystack
method. If you would like to append a job to the haystack to be processed
immediately, use the prependToHaystack
method.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class ProcessPodcast implements ShouldQueue, StackableJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
public function handle()
{
// Append a job to the end of the haystack
$this->appendToHaystack(new DifferentJob);
// Append a job and put it right at the top of the haystack
$this->prependToHaystack(new NextJob);
}
The
appendToHaystack
andprependToHaystack
methods also accept an array or Collection of jobs.
Haystack fully supports the existing release
and delay
methods in Laravel, however occasionally you may want to pause the haystack for an extended period of time, or release a job until the next day when an API rate limit is lifted. This can also be used as a longer delay if you are using Amazon SQS which only has a delay of 15 minutes.
Laravel Haystack can provide this by storing the resume date in the database and using the Scheduler to dispatch the haystack when it is ready. When Laravel Haystack is paused, no job is left in your queue.
Before you configure long releases or pauses, you must make sure your scheduler is running and the following line is added to your Console/Kernel.php file. If you can, provide the onOneServer
method as well which will prevent any accidental duplicate-resumes.
<?php
// Add to Console/Kernel.php
$schedule->command('haystacks:resume')->everyMinute();
// One server if you are running the scheduler on multiple servers
$schedule->command('haystacks:resume')->everyMinute()->onOneServer();
If you would like to release the current job back onto the queue, just use the longRelease
method inside your job’s handle method. You can provide an integer for seconds or a Carbon datetime instance.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class ProcessPodcast implements ShouldQueue, StackableJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
public function handle()
{
// Your application code...
$this->longRelease(300); // Release for 5 minutes
// Or use Carbon
$this->longRelease(now()->addDays(2)); // Release for 2 days.
}
If you want to process the current job but pause the Haystack for the next job, use the pauseHaystack
method. If you have disabled automatic processing, you can provide a delay to the nextJob
method.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class ProcessPodcast implements ShouldQueue, StackableJo
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
public function handle()
{
// Your application code...
$this->pauseHaystack(now()->addHours(4)); // Pause the haystack for 4 hours.
}
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.
<?php
$haystack = Haystack::build()
->addJob(new RetrieveDataFromApi)
->addJob(new ProcessDataFromApi)
->addJob(new StoreDataFromApi)
->dispatch();
Inside your job, you can use the setHaystackData()
method to store some data. This method accepts a key, value and optional Eloquent cast.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class RetrieveDataFromApi implements ShouldQueue, StackableJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
public function handle()
{
// Your application code...
$this->setHaystackData('username', 'Sammyjo20');
}
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.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class RetrieveDataFromApi implements ShouldQueue, StackableJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
public function handle()
{
// Your application code...
// Array Data, provide third argument to specify cast.
$this->setHaystackData('data', ['username' => 'Sammyjo20'], 'array');
// Carbon dates...
$this->setHaystackData('currentDate', now(), 'immutable_datetime');
// Supports custom casts
$this->setHaystackData('customData', $object, CustomCast::class);
}
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.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class RetrieveDataFromApi implements ShouldQueue, StackableJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
public function handle()
{
// Get data by key
$username = $this->getHaystackData('username'); // Sammyjo20
// Get all data
$allData = $this->allHaystackData(); // Collection: ['username' => 'Sammyjo20']
}
Laravel Haystack will conveniently pass a collection into your then/catch/finally closures with all the of the data that you stored inside the Haystack. You can then use this data however you wish.
<?php
$haystack = Haystack::build()
->addJob(new RetrieveDataFromApi)
->addJob(new ProcessDataFromApi)
->addJob(new StoreDataFromApi)
->then(function ($data) {
// $data: ['username' => 'Sammyjo20', 'other-key' => 'value', 'collection' => new Collection],
})
->catch(function ($data) {
// $data: ['username' => 'Sammyjo20', 'other-key' => 'value', 'collection' => new Collection],
})
->finally(function ($data) {
// $data: ['username' => 'Sammyjo20', 'other-key' => 'value', 'collection' => new Collection],
})
->dispatch();
If you would like to disable this functionality, you can provide the dontReturnData
method to the Haystack builder. If this method is provided, Haystack won't run the query that retrieves all the data at the end of a Haystack.
<?php
$haystack = Haystack::build()
->addJob(new RetrieveDataFromApi)
->addJob(new ProcessDataFromApi)
->addJob(new StoreDataFromApi)
->dontReturnData()
->then(function ($data) {
// $data: null
})
->dispatch();
If you would like to disable this feature entirely, you can set the return_all_haystack_data_when_finished
config variable to false.
Sometimes it's useful to set some data for your jobs to consume without having to pass every piece of data down into each job. You can use the withData
method while you are building your Haystack to add data before the Haystack starts. It also accepts a key, value and optional Eloquent cast.
<?php
$haystack = Haystack::build()
->addJob(new RetrieveDataFromApi)
->addJob(new ProcessDataFromApi)
->addJob(new StoreDataFromApi)
->withData('username', 'Sammyjo20')
->dispatch();
Sometimes you may with to give your haystack a custom name. This is especially useful for debugging, as you could check your database and see what haystacks are currently being processed. To give the haystack a name, use the withName
method when building the haystack.
<?php
$haystack = Haystack::build()
->withName('Process API Data')
->addJob(new RetrieveDataFromApi)
->addJob(new ProcessDataFromApi)
->addJob(new StoreDataFromApi)
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.
Since Haystack is no longer listening to your jobs, you will need to tell it when to dispatch the next job. You can do this by using the nextJob
method. Laravel Haystack will automatically stop when it gets to the end of the haystack.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class ProcessPodcast implements ShouldQueue, StackableJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
public function handle()
{
// Your application code...
$this->nextJob();
}
Since Haystack is no longer listening out for failed jobs, you will need to call the failHaystack
method in your job, preferably in the failed
method. If you do not provide this, Haystack won’t call your “catch” and “finally” methods.
<?php
namespace App\Jobs;
use Sammyjo20\LaravelHaystack\Contracts\StackableJob;
use Sammyjo20\LaravelHaystack\Concerns\Stackable;
class ProcessPodcast implements ShouldQueue, StackableJob
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Stackable
public function failed($exception)
{
$this->failHaystack();
}
Laravel Haystack will attempt to clean up every job on successful/failed haystacks, however there may be a situation, especially if you have not enabled the automatic processing. If you have disabled the option to automatically delete haystacks when they are finished they may build up quickly.
You can prevent this by running the following prune command every day in your scheduler:
<?php
// Add to Console/Kernel.php
use Sammyjo20\LaravelHaystack\Models\Haystack;
$schedule->command('model:prune', [
'--model' => [Haystack::class],
])->daily();
If a haystack was created by mistake, and you would like to delete it without directly accessing the database, you may use the haystacks:forget
command. The haystacks:forget
command accepts the ID of the haystack as its only argument:
php artisan haystacks:forget <id>
If you would like to clear all haystacks from the database, you may use the haystacks:clear
command. The haystacks:clear
command accepts no arguments:
php artisan haystacks:clear
While I never expect anything, if you would like to support my work, you can donate to my Ko-Fi page by simply buying me a coffee or two! https://ko-fi.com/sammyjo20
Thank you for using Laravel Haystack ❤️